diff --git a/.dockerignore b/.dockerignore index c748048e0..d4f63d635 100644 --- a/.dockerignore +++ b/.dockerignore @@ -28,9 +28,9 @@ LICENSE CONTRIBUTING.md dist .git -migrations/ +server/migrations/ config/ build.ts tsconfig.json Dockerfile* -migrations/ +drizzle.config.ts diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index 7358fa2a8..fff21995d 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -29,7 +29,7 @@ jobs: permissions: write-all steps: - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} role-duration-seconds: 3600 @@ -62,7 +62,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Monitor storage space run: | @@ -77,7 +77,7 @@ jobs: fi - name: Log in to Docker Hub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: docker.io username: ${{ secrets.DOCKER_HUB_USERNAME }} @@ -134,7 +134,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Monitor storage space run: | @@ -149,7 +149,7 @@ jobs: fi - name: Log in to Docker Hub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: docker.io username: ${{ secrets.DOCKER_HUB_USERNAME }} @@ -201,10 +201,10 @@ jobs: timeout-minutes: 30 steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Log in to Docker Hub - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: docker.io username: ${{ secrets.DOCKER_HUB_USERNAME }} @@ -256,7 +256,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Extract tag name id: get-tag @@ -264,7 +264,7 @@ jobs: shell: bash - name: Install Go - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0 + uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0 with: go-version: 1.24 @@ -289,25 +289,17 @@ jobs: echo "LATEST_BADGER_TAG=$LATEST_TAG" >> $GITHUB_ENV shell: bash - - name: Update install/main.go - run: | - PANGOLIN_VERSION=${{ env.TAG }} - GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }} - BADGER_VERSION=${{ env.LATEST_BADGER_TAG }} - sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"$PANGOLIN_VERSION\"/" install/main.go - sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"$GERBIL_VERSION\"/" install/main.go - sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$BADGER_VERSION\"/" install/main.go - echo "Updated install/main.go with Pangolin version $PANGOLIN_VERSION, Gerbil version $GERBIL_VERSION, and Badger version $BADGER_VERSION" - cat install/main.go - shell: bash - - name: Build installer working-directory: install run: | - make go-build-release + make go-build-release \ + PANGOLIN_VERSION=${{ env.TAG }} \ + GERBIL_VERSION=${{ env.LATEST_GERBIL_TAG }} \ + BADGER_VERSION=${{ env.LATEST_BADGER_TAG }} + shell: bash - name: Upload artifacts from /install/bin - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: install-bin path: install/bin/ @@ -415,7 +407,7 @@ jobs: shell: bash - name: Login to GitHub Container Registry (for cosign) - uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0 with: registry: ghcr.io username: ${{ github.actor }} @@ -423,7 +415,7 @@ jobs: - name: Install cosign # cosign is used to sign and verify container images (key and keyless) - uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 + uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 - name: Dual-sign and verify (GHCR & Docker Hub) # Sign each image by digest using keyless (OIDC) and key-based signing, @@ -578,7 +570,7 @@ jobs: permissions: write-all steps: - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} role-duration-seconds: 3600 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 92e4e2da8..cf574dd3c 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -21,10 +21,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Set up Node.js - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: '24' diff --git a/.github/workflows/mirror.yaml b/.github/workflows/mirror.yaml index c9154c447..d6dfdb8fb 100644 --- a/.github/workflows/mirror.yaml +++ b/.github/workflows/mirror.yaml @@ -23,7 +23,7 @@ jobs: skopeo --version - name: Install cosign - uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0 + uses: sigstore/cosign-installer@ba7bc0a3fef59531c69a25acd34668d6d3fe6f22 # v4.1.0 - name: Input check run: | diff --git a/.github/workflows/restart-runners.yml b/.github/workflows/restart-runners.yml index 16901d1b2..6c0f7cbc1 100644 --- a/.github/workflows/restart-runners.yml +++ b/.github/workflows/restart-runners.yml @@ -14,7 +14,7 @@ jobs: permissions: write-all steps: - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} role-duration-seconds: 3600 diff --git a/.github/workflows/saas.yml b/.github/workflows/saas.yml index 93e5d1989..7c3d0adac 100644 --- a/.github/workflows/saas.yml +++ b/.github/workflows/saas.yml @@ -23,7 +23,7 @@ jobs: permissions: write-all steps: - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} role-duration-seconds: 3600 @@ -54,7 +54,7 @@ jobs: steps: - name: Checkout code - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download MaxMind GeoLite2 databases env: @@ -104,7 +104,7 @@ jobs: fi - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::${{ secrets.aws_account_id }}:role/${{ secrets.AWS_ROLE_NAME }} role-duration-seconds: 3600 @@ -145,7 +145,7 @@ jobs: permissions: write-all steps: - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v5 + uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/${{ secrets.AWS_ROLE_NAME }} role-duration-seconds: 3600 diff --git a/.github/workflows/stale-bot.yml b/.github/workflows/stale-bot.yml index 4df7e93ec..2db8632e9 100644 --- a/.github/workflows/stale-bot.yml +++ b/.github/workflows/stale-bot.yml @@ -14,7 +14,7 @@ jobs: stale: runs-on: ubuntu-latest steps: - - uses: actions/stale@997185467fa4f803885201cee163a9f38240193d # v10.1.1 + - uses: actions/stale@b5d41d4e1d5dceea10e7104786b73624c18a190f # v10.2.0 with: days-before-stale: 14 days-before-close: 14 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index eec4ff339..30567f0f7 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,10 +14,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Install Node - uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0 + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: '24' @@ -62,7 +62,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Build Docker image sqlite run: make dev-build-sqlite @@ -71,7 +71,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Build Docker image pg run: make dev-build-pg diff --git a/Dockerfile b/Dockerfile index 12c519b7e..9af37f89c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,9 @@ -FROM node:24-alpine AS base +# FROM node:24-slim AS base +FROM public.ecr.aws/docker/library/node:24-slim AS base WORKDIR /app -RUN apk add --no-cache python3 make g++ +RUN apt-get update && apt-get install -y python3 make g++ && rm -rf /var/lib/apt/lists/* COPY package*.json ./ @@ -23,15 +24,20 @@ RUN if [ "$BUILD" = "oss" ]; then rm -rf server/private; fi && \ npm run build:cli && \ test -f dist/server.mjs +# Create placeholder files for MaxMind databases to avoid COPY errors +# Real files should be present for saas builds, placeholders for oss builds +RUN touch /app/GeoLite2-Country.mmdb /app/GeoLite2-ASN.mmdb + FROM base AS builder RUN npm ci --omit=dev -FROM node:24-alpine AS runner +# FROM node:24-slim AS runner +FROM public.ecr.aws/docker/library/node:24-slim AS runner WORKDIR /app -RUN apk add --no-cache curl tzdata +RUN apt-get update && apt-get install -y curl tzdata && rm -rf /var/lib/apt/lists/* COPY --from=builder /app/node_modules ./node_modules COPY --from=builder /app/package.json ./package.json @@ -51,12 +57,16 @@ COPY public ./public # Copy MaxMind databases for SaaS builds ARG BUILD=oss + RUN mkdir -p ./maxmind -# This is only for saas +# Copy MaxMind databases (placeholders exist for oss builds, real files for saas) COPY --from=builder-dev /app/GeoLite2-Country.mmdb ./maxmind/GeoLite2-Country.mmdb COPY --from=builder-dev /app/GeoLite2-ASN.mmdb ./maxmind/GeoLite2-ASN.mmdb +# Remove MaxMind databases for non-saas builds (keep only for saas) +RUN if [ "$BUILD" != "saas" ]; then rm -rf ./maxmind; fi + # OCI Image Labels - Build Args for dynamic values ARG VERSION="dev" ARG REVISION="" diff --git a/README.md b/README.md index c566c8677..bac7b7e56 100644 --- a/README.md +++ b/README.md @@ -43,7 +43,7 @@

- Start testing Pangolin at app.pangolin.net + Get started with Pangolin at app.pangolin.net

@@ -60,9 +60,9 @@ Pangolin is an open-source, identity-based remote access platform built on WireG | | Description | |-----------------|--------------| +| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://docs.pangolin.net/manage/remote-node/understanding-nodes) and connect to our control plane. | | **Self-Host: Community Edition** | Free, open source, and licensed under AGPL-3. | | **Self-Host: Enterprise Edition** | Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses earning under \$100K USD annually. | -| **Pangolin Cloud** | Fully managed service with instant setup and pay-as-you-go pricing — no infrastructure required. Or, self-host your own [remote node](https://docs.pangolin.net/manage/remote-node/nodes) and connect to our control plane. | ## Key Features @@ -85,17 +85,16 @@ Download the Pangolin client for your platform: ## Get Started +### Sign up now + +Create an account at [app.pangolin.net](https://app.pangolin.net) to get started with Pangolin Cloud. A generous free tier is available. + ### Check out the docs We encourage everyone to read the full documentation first, which is available at [docs.pangolin.net](https://docs.pangolin.net). This README provides only a very brief subset of the docs to illustrate some basic ideas. -### Sign up and try now - -For Pangolin's managed service, you will first need to create an account at -[app.pangolin.net](https://app.pangolin.net). We have a generous free tier to get started. - ## Licensing Pangolin is dual licensed under the AGPL-3 and the [Fossorial Commercial License](https://pangolin.net/fcl.html). For inquiries about commercial licensing, please contact us at [contact@pangolin.net](mailto:contact@pangolin.net). diff --git a/cli/commands/generateOrgCaKeys.ts b/cli/commands/generateOrgCaKeys.ts index af822c819..fe38e0c56 100644 --- a/cli/commands/generateOrgCaKeys.ts +++ b/cli/commands/generateOrgCaKeys.ts @@ -3,7 +3,7 @@ import { db, orgs } from "@server/db"; import { eq } from "drizzle-orm"; import { encrypt } from "@server/lib/crypto"; import { configFilePath1, configFilePath2 } from "@server/lib/consts"; -import { generateCA } from "@server/private/lib/sshCA"; +import { generateCA } from "@server/lib/sshCA"; import fs from "fs"; import yaml from "js-yaml"; diff --git a/docker-compose.example.yml b/docker-compose.example.yml index 84a5140b4..50cb1bcc1 100644 --- a/docker-compose.example.yml +++ b/docker-compose.example.yml @@ -4,6 +4,12 @@ services: image: fosrl/pangolin:latest container_name: pangolin restart: unless-stopped + deploy: + resources: + limits: + memory: 1g + reservations: + memory: 256m volumes: - ./config:/app/config healthcheck: diff --git a/install/Makefile b/install/Makefile index 53365f509..8a836b77e 100644 --- a/install/Makefile +++ b/install/Makefile @@ -1,41 +1,24 @@ -all: update-versions go-build-release put-back -dev-all: dev-update-versions dev-build dev-clean +all: go-build-release + +# Build with version injection via ldflags +# Versions can be passed via: make go-build-release PANGOLIN_VERSION=x.x.x GERBIL_VERSION=x.x.x BADGER_VERSION=x.x.x +# Or fetched automatically if not provided (requires curl and jq) + +PANGOLIN_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name') +GERBIL_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') +BADGER_VERSION ?= $(shell curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') + +LDFLAGS = -X main.pangolinVersion=$(PANGOLIN_VERSION) \ + -X main.gerbilVersion=$(GERBIL_VERSION) \ + -X main.badgerVersion=$(BADGER_VERSION) go-build-release: - CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/installer_linux_amd64 - CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -o bin/installer_linux_arm64 + @echo "Building with versions - Pangolin: $(PANGOLIN_VERSION), Gerbil: $(GERBIL_VERSION), Badger: $(BADGER_VERSION)" + CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "$(LDFLAGS)" -o bin/installer_linux_amd64 + CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags "$(LDFLAGS)" -o bin/installer_linux_arm64 clean: rm -f bin/installer_linux_amd64 rm -f bin/installer_linux_arm64 -update-versions: - @echo "Fetching latest versions..." - cp main.go main.go.bak && \ - $(MAKE) dev-update-versions - -put-back: - mv main.go.bak main.go - -dev-update-versions: - if [ -z "$(tag)" ]; then \ - PANGOLIN_VERSION=$$(curl -s https://api.github.com/repos/fosrl/pangolin/tags | jq -r '.[0].name'); \ - else \ - PANGOLIN_VERSION=$(tag); \ - fi && \ - GERBIL_VERSION=$$(curl -s https://api.github.com/repos/fosrl/gerbil/tags | jq -r '.[0].name') && \ - BADGER_VERSION=$$(curl -s https://api.github.com/repos/fosrl/badger/tags | jq -r '.[0].name') && \ - echo "Latest versions - Pangolin: $$PANGOLIN_VERSION, Gerbil: $$GERBIL_VERSION, Badger: $$BADGER_VERSION" && \ - sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"$$PANGOLIN_VERSION\"/" main.go && \ - sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"$$GERBIL_VERSION\"/" main.go && \ - sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"$$BADGER_VERSION\"/" main.go && \ - echo "Updated main.go with latest versions" - -dev-build: go-build-release - -dev-clean: - @echo "Restoring version values ..." - sed -i "s/config.PangolinVersion = \".*\"/config.PangolinVersion = \"replaceme\"/" main.go && \ - sed -i "s/config.GerbilVersion = \".*\"/config.GerbilVersion = \"replaceme\"/" main.go && \ - sed -i "s/config.BadgerVersion = \".*\"/config.BadgerVersion = \"replaceme\"/" main.go - @echo "Restored version strings in main.go" +.PHONY: all go-build-release clean diff --git a/install/config.go b/install/config.go index e75dd50dd..548e2ab33 100644 --- a/install/config.go +++ b/install/config.go @@ -118,19 +118,19 @@ func copyDockerService(sourceFile, destFile, serviceName string) error { } // Parse source Docker Compose YAML - var sourceCompose map[string]interface{} + var sourceCompose map[string]any if err := yaml.Unmarshal(sourceData, &sourceCompose); err != nil { return fmt.Errorf("error parsing source Docker Compose file: %w", err) } // Parse destination Docker Compose YAML - var destCompose map[string]interface{} + var destCompose map[string]any if err := yaml.Unmarshal(destData, &destCompose); err != nil { return fmt.Errorf("error parsing destination Docker Compose file: %w", err) } // Get services section from source - sourceServices, ok := sourceCompose["services"].(map[string]interface{}) + sourceServices, ok := sourceCompose["services"].(map[string]any) if !ok { return fmt.Errorf("services section not found in source file or has invalid format") } @@ -142,10 +142,10 @@ func copyDockerService(sourceFile, destFile, serviceName string) error { } // Get or create services section in destination - destServices, ok := destCompose["services"].(map[string]interface{}) + destServices, ok := destCompose["services"].(map[string]any) if !ok { // If services section doesn't exist, create it - destServices = make(map[string]interface{}) + destServices = make(map[string]any) destCompose["services"] = destServices } @@ -187,13 +187,12 @@ func backupConfig() error { return nil } -func MarshalYAMLWithIndent(data interface{}, indent int) ([]byte, error) { +func MarshalYAMLWithIndent(data any, indent int) ([]byte, error) { buffer := new(bytes.Buffer) encoder := yaml.NewEncoder(buffer) encoder.SetIndent(indent) - err := encoder.Encode(data) - if err != nil { + if err := encoder.Encode(data); err != nil { return nil, err } @@ -209,7 +208,7 @@ func replaceInFile(filepath, oldStr, newStr string) error { } // Replace the string - newContent := strings.Replace(string(content), oldStr, newStr, -1) + newContent := strings.ReplaceAll(string(content), oldStr, newStr) // Write the modified content back to the file err = os.WriteFile(filepath, []byte(newContent), 0644) @@ -228,28 +227,28 @@ func CheckAndAddTraefikLogVolume(composePath string) error { } // Parse YAML into a generic map - var compose map[string]interface{} + var compose map[string]any if err := yaml.Unmarshal(data, &compose); err != nil { return fmt.Errorf("error parsing compose file: %w", err) } // Get services section - services, ok := compose["services"].(map[string]interface{}) + services, ok := compose["services"].(map[string]any) if !ok { return fmt.Errorf("services section not found or invalid") } // Get traefik service - traefik, ok := services["traefik"].(map[string]interface{}) + traefik, ok := services["traefik"].(map[string]any) if !ok { return fmt.Errorf("traefik service not found or invalid") } // Check volumes logVolume := "./config/traefik/logs:/var/log/traefik" - var volumes []interface{} + var volumes []any - if existingVolumes, ok := traefik["volumes"].([]interface{}); ok { + if existingVolumes, ok := traefik["volumes"].([]any); ok { // Check if volume already exists for _, v := range existingVolumes { if v.(string) == logVolume { @@ -295,13 +294,13 @@ func MergeYAML(baseFile, overlayFile string) error { } // Parse base YAML into a map - var baseMap map[string]interface{} + var baseMap map[string]any if err := yaml.Unmarshal(baseContent, &baseMap); err != nil { return fmt.Errorf("error parsing base YAML: %v", err) } // Parse overlay YAML into a map - var overlayMap map[string]interface{} + var overlayMap map[string]any if err := yaml.Unmarshal(overlayContent, &overlayMap); err != nil { return fmt.Errorf("error parsing overlay YAML: %v", err) } @@ -324,8 +323,8 @@ func MergeYAML(baseFile, overlayFile string) error { } // mergeMap recursively merges two maps -func mergeMap(base, overlay map[string]interface{}) map[string]interface{} { - result := make(map[string]interface{}) +func mergeMap(base, overlay map[string]any) map[string]any { + result := make(map[string]any) // Copy all key-values from base map for k, v := range base { @@ -336,8 +335,8 @@ func mergeMap(base, overlay map[string]interface{}) map[string]interface{} { for k, v := range overlay { // If both maps have the same key and both values are maps, merge recursively if baseVal, ok := base[k]; ok { - if baseMap, isBaseMap := baseVal.(map[string]interface{}); isBaseMap { - if overlayMap, isOverlayMap := v.(map[string]interface{}); isOverlayMap { + if baseMap, isBaseMap := baseVal.(map[string]any); isBaseMap { + if overlayMap, isOverlayMap := v.(map[string]any); isOverlayMap { result[k] = mergeMap(baseMap, overlayMap) continue } diff --git a/install/config/docker-compose.yml b/install/config/docker-compose.yml index 1d8b73b4b..c0206e5bf 100644 --- a/install/config/docker-compose.yml +++ b/install/config/docker-compose.yml @@ -4,6 +4,12 @@ services: image: docker.io/fosrl/pangolin:{{if .IsEnterprise}}ee-{{end}}{{.PangolinVersion}} container_name: pangolin restart: unless-stopped + deploy: + resources: + limits: + memory: 1g + reservations: + memory: 256m volumes: - ./config:/app/config healthcheck: @@ -38,9 +44,7 @@ services: image: docker.io/traefik:v3.6 container_name: traefik restart: unless-stopped -{{if .InstallGerbil}} - network_mode: service:gerbil # Ports appear on the gerbil service -{{end}}{{if not .InstallGerbil}} +{{if .InstallGerbil}} network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}} ports: - 443:443 - 80:80 diff --git a/install/containers.go b/install/containers.go index 333fd890c..b5d18423b 100644 --- a/install/containers.go +++ b/install/containers.go @@ -144,12 +144,13 @@ func installDocker() error { } func startDockerService() error { - if runtime.GOOS == "linux" { + switch runtime.GOOS { + case "linux": cmd := exec.Command("systemctl", "enable", "--now", "docker") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() - } else if runtime.GOOS == "darwin" { + case "darwin": // On macOS, Docker is usually started via the Docker Desktop application fmt.Println("Please start Docker Desktop manually on macOS.") return nil @@ -302,7 +303,7 @@ func pullContainers(containerType SupportedContainer) error { return nil } - return fmt.Errorf("Unsupported container type: %s", containerType) + return fmt.Errorf("unsupported container type: %s", containerType) } // startContainers starts the containers using the appropriate command. @@ -325,7 +326,7 @@ func startContainers(containerType SupportedContainer) error { return nil } - return fmt.Errorf("Unsupported container type: %s", containerType) + return fmt.Errorf("unsupported container type: %s", containerType) } // stopContainers stops the containers using the appropriate command. @@ -347,7 +348,7 @@ func stopContainers(containerType SupportedContainer) error { return nil } - return fmt.Errorf("Unsupported container type: %s", containerType) + return fmt.Errorf("unsupported container type: %s", containerType) } // restartContainer restarts a specific container using the appropriate command. @@ -369,5 +370,5 @@ func restartContainer(container string, containerType SupportedContainer) error return nil } - return fmt.Errorf("Unsupported container type: %s", containerType) + return fmt.Errorf("unsupported container type: %s", containerType) } diff --git a/install/crowdsec.go b/install/crowdsec.go index 401ef215c..c75dccf32 100644 --- a/install/crowdsec.go +++ b/install/crowdsec.go @@ -27,9 +27,18 @@ func installCrowdsec(config Config) error { os.Exit(1) } - os.MkdirAll("config/crowdsec/db", 0755) - os.MkdirAll("config/crowdsec/acquis.d", 0755) - os.MkdirAll("config/traefik/logs", 0755) + if err := os.MkdirAll("config/crowdsec/db", 0755); err != nil { + fmt.Printf("Error creating config files: %v\n", err) + os.Exit(1) + } + if err := os.MkdirAll("config/crowdsec/acquis.d", 0755); err != nil { + fmt.Printf("Error creating config files: %v\n", err) + os.Exit(1) + } + if err := os.MkdirAll("config/traefik/logs", 0755); err != nil { + fmt.Printf("Error creating config files: %v\n", err) + os.Exit(1) + } if err := copyDockerService("config/crowdsec/docker-compose.yml", "docker-compose.yml", "crowdsec"); err != nil { fmt.Printf("Error copying docker service: %v\n", err) @@ -153,34 +162,34 @@ func CheckAndAddCrowdsecDependency(composePath string) error { } // Parse YAML into a generic map - var compose map[string]interface{} + var compose map[string]any if err := yaml.Unmarshal(data, &compose); err != nil { return fmt.Errorf("error parsing compose file: %w", err) } // Get services section - services, ok := compose["services"].(map[string]interface{}) + services, ok := compose["services"].(map[string]any) if !ok { return fmt.Errorf("services section not found or invalid") } // Get traefik service - traefik, ok := services["traefik"].(map[string]interface{}) + traefik, ok := services["traefik"].(map[string]any) if !ok { return fmt.Errorf("traefik service not found or invalid") } // Get dependencies - dependsOn, ok := traefik["depends_on"].(map[string]interface{}) + dependsOn, ok := traefik["depends_on"].(map[string]any) if ok { // Append the new block for crowdsec - dependsOn["crowdsec"] = map[string]interface{}{ + dependsOn["crowdsec"] = map[string]any{ "condition": "service_healthy", } } else { // No dependencies exist, create it - traefik["depends_on"] = map[string]interface{}{ - "crowdsec": map[string]interface{}{ + traefik["depends_on"] = map[string]any{ + "crowdsec": map[string]any{ "condition": "service_healthy", }, } diff --git a/install/go.mod b/install/go.mod index c4e72fc71..da73eec0f 100644 --- a/install/go.mod +++ b/install/go.mod @@ -1,10 +1,38 @@ module installer -go 1.24.0 +go 1.25.0 require ( - golang.org/x/term v0.39.0 + github.com/charmbracelet/huh v0.8.0 + github.com/charmbracelet/lipgloss v1.1.0 + golang.org/x/term v0.41.0 gopkg.in/yaml.v3 v3.0.1 ) -require golang.org/x/sys v0.40.0 // indirect +require ( + github.com/atotto/clipboard v0.1.4 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/catppuccin/go v0.3.0 // indirect + github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 // indirect + github.com/charmbracelet/bubbletea v1.3.6 // indirect + github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect + github.com/charmbracelet/x/ansi v0.9.3 // indirect + github.com/charmbracelet/x/cellbuf v0.0.13 // indirect + github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 // indirect + github.com/charmbracelet/x/term v0.2.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-localereader v0.0.1 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect + github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/termenv v0.16.0 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.42.0 // indirect + golang.org/x/text v0.23.0 // indirect +) diff --git a/install/go.sum b/install/go.sum index e3e319c3d..e0b2a6c5e 100644 --- a/install/go.sum +++ b/install/go.sum @@ -1,7 +1,80 @@ -golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= -golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= -golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= +github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= +github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= +github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= +github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= +github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= +github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= +github.com/catppuccin/go v0.3.0 h1:d+0/YicIq+hSTo5oPuRi5kOpqkVA5tAsU6dNhvRu+aY= +github.com/catppuccin/go v0.3.0/go.mod h1:8IHJuMGaUUjQM82qBrGNBv7LFq6JI3NnQCF6MOlZjpc= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7 h1:JFgG/xnwFfbezlUnFMJy0nusZvytYysV4SCS2cYbvws= +github.com/charmbracelet/bubbles v0.21.1-0.20250623103423-23b8fd6302d7/go.mod h1:ISC1gtLcVilLOf23wvTfoQuYbW2q0JevFxPfUzZ9Ybw= +github.com/charmbracelet/bubbletea v1.3.6 h1:VkHIxPJQeDt0aFJIsVxw8BQdh/F/L2KKZGsK6et5taU= +github.com/charmbracelet/bubbletea v1.3.6/go.mod h1:oQD9VCRQFF8KplacJLo28/jofOI2ToOfGYeFgBBxHOc= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= +github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= +github.com/charmbracelet/huh v0.8.0 h1:Xz/Pm2h64cXQZn/Jvele4J3r7DDiqFCNIVteYukxDvY= +github.com/charmbracelet/huh v0.8.0/go.mod h1:5YVc+SlZ1IhQALxRPpkGwwEKftN/+OlJlnJYlDRFqN4= +github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= +github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= +github.com/charmbracelet/x/ansi v0.9.3 h1:BXt5DHS/MKF+LjuK4huWrC6NCvHtexww7dMayh6GXd0= +github.com/charmbracelet/x/ansi v0.9.3/go.mod h1:3RQDQ6lDnROptfpWuUVIUG64bD2g2BgntdxH0Ya5TeE= +github.com/charmbracelet/x/cellbuf v0.0.13 h1:/KBBKHuVRbq1lYx5BzEHBAFBP8VcQzJejZ/IA3iR28k= +github.com/charmbracelet/x/cellbuf v0.0.13/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= +github.com/charmbracelet/x/conpty v0.1.0 h1:4zc8KaIcbiL4mghEON8D72agYtSeIgq8FSThSPQIb+U= +github.com/charmbracelet/x/conpty v0.1.0/go.mod h1:rMFsDJoDwVmiYM10aD4bH2XiRgwI7NYJtQgl5yskjEQ= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86 h1:JSt3B+U9iqk37QUU2Rvb6DSBYRLtWqFqfxf8l5hOZUA= +github.com/charmbracelet/x/errors v0.0.0-20240508181413-e8d8b6e2de86/go.mod h1:2P0UgXMEa6TsToMSuFqKFQR+fZTO9CNGUNokkPatT/0= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= +github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0 h1:qko3AQ4gK1MTS/de7F5hPGx6/k1u0w4TeYmBFwzYVP4= +github.com/charmbracelet/x/exp/strings v0.0.0-20240722160745-212f7b056ed0/go.mod h1:pBhA0ybfXv6hDjQUZ7hk1lVxBiUbupdw5R31yPUViVQ= +github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= +github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= +github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY= +github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo= +github.com/charmbracelet/x/xpty v0.1.2 h1:Pqmu4TEJ8KeA9uSkISKMU3f+C1F6OGBn8ABuGlqCbtI= +github.com/charmbracelet/x/xpty v0.1.2/go.mod h1:XK2Z0id5rtLWcpeNiMYBccNNBrP2IJnzHI0Lq13Xzq4= +github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= +github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= +github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= +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/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= +github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4= +github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= +github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= +github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= +golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= +golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= +golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/install/input.go b/install/input.go index db70b4c00..8b444ecb9 100644 --- a/install/input.go +++ b/install/input.go @@ -1,92 +1,235 @@ package main import ( - "bufio" + "errors" "fmt" - "strings" - "syscall" + "os" + "strconv" + "github.com/charmbracelet/huh" "golang.org/x/term" ) -func readString(reader *bufio.Reader, prompt string, defaultValue string) string { +// pangolinTheme is the custom theme using brand colors +var pangolinTheme = ThemePangolin() + +// isAccessibleMode checks if we should use accessible mode (simple prompts) +// This is true for: non-TTY, TERM=dumb, or ACCESSIBLE env var set +func isAccessibleMode() bool { + // Check if stdin is not a terminal (piped input, CI, etc.) + if !term.IsTerminal(int(os.Stdin.Fd())) { + return true + } + // Check for dumb terminal + if os.Getenv("TERM") == "dumb" { + return true + } + // Check for explicit accessible mode request + if os.Getenv("ACCESSIBLE") != "" { + return true + } + return false +} + +// handleAbort checks if the error is a user abort (Ctrl+C) and exits if so +func handleAbort(err error) { + if err != nil && errors.Is(err, huh.ErrUserAborted) { + fmt.Println("\nInstallation cancelled.") + os.Exit(0) + } +} + +// runField runs a single field with the Pangolin theme, handling accessible mode +func runField(field huh.Field) error { + if isAccessibleMode() { + return field.RunAccessible(os.Stdout, os.Stdin) + } + form := huh.NewForm(huh.NewGroup(field)).WithTheme(pangolinTheme) + return form.Run() +} + +func readString(prompt string, defaultValue string) string { + var value string + + title := prompt if defaultValue != "" { - fmt.Printf("%s (default: %s): ", prompt, defaultValue) - } else { - fmt.Print(prompt + ": ") + title = fmt.Sprintf("%s (default: %s)", prompt, defaultValue) } - input, _ := reader.ReadString('\n') - input = strings.TrimSpace(input) - if input == "" { - return defaultValue - } - return input -} -func readStringNoDefault(reader *bufio.Reader, prompt string) string { - fmt.Print(prompt + ": ") - input, _ := reader.ReadString('\n') - return strings.TrimSpace(input) -} + input := huh.NewInput(). + Title(title). + Value(&value) -func readPassword(prompt string, reader *bufio.Reader) string { - if term.IsTerminal(int(syscall.Stdin)) { - fmt.Print(prompt + ": ") - // Read password without echo if we're in a terminal - password, err := term.ReadPassword(int(syscall.Stdin)) - fmt.Println() // Add a newline since ReadPassword doesn't add one - if err != nil { - return "" - } - input := strings.TrimSpace(string(password)) - if input == "" { - return readPassword(prompt, reader) - } - return input - } else { - // Fallback to reading from stdin if not in a terminal - return readString(reader, prompt, "") + // If no default value, this field is required + if defaultValue == "" { + input = input.Validate(func(s string) error { + if s == "" { + return fmt.Errorf("this field is required") + } + return nil + }) } -} -func readBool(reader *bufio.Reader, prompt string, defaultValue bool) bool { - defaultStr := "no" - if defaultValue { - defaultStr = "yes" - } - for { - input := readString(reader, prompt+" (yes/no)", defaultStr) - lower := strings.ToLower(input) - if lower == "yes" { - return true - } else if lower == "no" { - return false - } else { - fmt.Println("Please enter 'yes' or 'no'.") - } - } -} + err := runField(input) + handleAbort(err) -func readBoolNoDefault(reader *bufio.Reader, prompt string) bool { - for { - input := readStringNoDefault(reader, prompt+" (yes/no)") - lower := strings.ToLower(input) - if lower == "yes" { - return true - } else if lower == "no" { - return false - } else { - fmt.Println("Please enter 'yes' or 'no'.") - } + if value == "" { + value = defaultValue } -} -func readInt(reader *bufio.Reader, prompt string, defaultValue int) int { - input := readString(reader, prompt, fmt.Sprintf("%d", defaultValue)) - if input == "" { - return defaultValue + // Print the answer so it remains visible in terminal history (skip in accessible mode as it already shows) + if !isAccessibleMode() { + fmt.Printf("%s: %s\n", prompt, value) } - value := defaultValue - fmt.Sscanf(input, "%d", &value) + return value } + +func readStringNoDefault(prompt string) string { + var value string + + for { + input := huh.NewInput(). + Title(prompt). + Value(&value). + Validate(func(s string) error { + if s == "" { + return fmt.Errorf("this field is required") + } + return nil + }) + + err := runField(input) + handleAbort(err) + + if value != "" { + // Print the answer so it remains visible in terminal history + if !isAccessibleMode() { + fmt.Printf("%s: %s\n", prompt, value) + } + return value + } + } +} + +func readPassword(prompt string) string { + var value string + + for { + input := huh.NewInput(). + Title(prompt). + Value(&value). + EchoMode(huh.EchoModePassword). + Validate(func(s string) error { + if s == "" { + return fmt.Errorf("password is required") + } + return nil + }) + + err := runField(input) + handleAbort(err) + + if value != "" { + // Print confirmation without revealing the password + if !isAccessibleMode() { + fmt.Printf("%s: %s\n", prompt, "********") + } + return value + } + } +} + +func readBool(prompt string, defaultValue bool) bool { + var value = defaultValue + + confirm := huh.NewConfirm(). + Title(prompt). + Value(&value). + Affirmative("Yes"). + Negative("No") + + err := runField(confirm) + handleAbort(err) + + // Print the answer so it remains visible in terminal history + if !isAccessibleMode() { + answer := "No" + if value { + answer = "Yes" + } + fmt.Printf("%s: %s\n", prompt, answer) + } + + return value +} + +func readBoolNoDefault(prompt string) bool { + var value bool + + confirm := huh.NewConfirm(). + Title(prompt). + Value(&value). + Affirmative("Yes"). + Negative("No") + + err := runField(confirm) + handleAbort(err) + + // Print the answer so it remains visible in terminal history + if !isAccessibleMode() { + answer := "No" + if value { + answer = "Yes" + } + fmt.Printf("%s: %s\n", prompt, answer) + } + + return value +} + +func readInt(prompt string, defaultValue int) int { + var value string + + title := fmt.Sprintf("%s (default: %d)", prompt, defaultValue) + + input := huh.NewInput(). + Title(title). + Value(&value). + Validate(func(s string) error { + if s == "" { + return nil + } + _, err := strconv.Atoi(s) + if err != nil { + return fmt.Errorf("please enter a valid number") + } + return nil + }) + + err := runField(input) + handleAbort(err) + + if value == "" { + // Print the answer so it remains visible in terminal history + if !isAccessibleMode() { + fmt.Printf("%s: %d\n", prompt, defaultValue) + } + return defaultValue + } + + result, err := strconv.Atoi(value) + if err != nil { + if !isAccessibleMode() { + fmt.Printf("%s: %d\n", prompt, defaultValue) + } + return defaultValue + } + + // Print the answer so it remains visible in terminal history + if !isAccessibleMode() { + fmt.Printf("%s: %d\n", prompt, result) + } + + return result +} diff --git a/install/main.go b/install/main.go index 242af7416..9de332b60 100644 --- a/install/main.go +++ b/install/main.go @@ -1,13 +1,12 @@ package main import ( - "bufio" + "crypto/rand" "embed" + "encoding/base64" "fmt" "io" "io/fs" - "crypto/rand" - "encoding/base64" "net" "net/http" "net/url" @@ -20,11 +19,17 @@ import ( "time" ) -// DO NOT EDIT THIS FUNCTION; IT MATCHED BY REGEX IN CICD +// Version variables injected at build time via -ldflags +var ( + pangolinVersion string + gerbilVersion string + badgerVersion string +) + func loadVersions(config *Config) { - config.PangolinVersion = "replaceme" - config.GerbilVersion = "replaceme" - config.BadgerVersion = "replaceme" + config.PangolinVersion = pangolinVersion + config.GerbilVersion = gerbilVersion + config.BadgerVersion = badgerVersion } //go:embed config/* @@ -82,14 +87,12 @@ func main() { } } - reader := bufio.NewReader(os.Stdin) - var config Config var alreadyInstalled = false // check if there is already a config file if _, err := os.Stat("config/config.yml"); err != nil { - config = collectUserInput(reader) + config = collectUserInput() loadVersions(&config) config.DoCrowdsecInstall = false @@ -102,7 +105,10 @@ func main() { os.Exit(1) } - moveFile("config/docker-compose.yml", "docker-compose.yml") + if err := moveFile("config/docker-compose.yml", "docker-compose.yml"); err != nil { + fmt.Printf("Error moving docker-compose.yml: %v\n", err) + os.Exit(1) + } fmt.Println("\nConfiguration files created successfully!") @@ -117,13 +123,17 @@ func main() { fmt.Println("\n=== Starting installation ===") - if readBool(reader, "Would you like to install and start the containers?", true) { + if readBool("Would you like to install and start the containers?", true) { - config.InstallationContainerType = podmanOrDocker(reader) + config.InstallationContainerType = podmanOrDocker() if !isDockerInstalled() && runtime.GOOS == "linux" && config.InstallationContainerType == Docker { - if readBool(reader, "Docker is not installed. Would you like to install it?", true) { - installDocker() + if readBool("Docker is not installed. Would you like to install it?", true) { + if err := installDocker(); err != nil { + fmt.Printf("Error installing Docker: %v\n", err) + return + } + // try to start docker service but ignore errors if err := startDockerService(); err != nil { fmt.Println("Error starting Docker service:", err) @@ -132,7 +142,7 @@ func main() { } // wait 10 seconds for docker to start checking if docker is running every 2 seconds fmt.Println("Waiting for Docker to start...") - for i := 0; i < 5; i++ { + for range 5 { if isDockerRunning() { fmt.Println("Docker is running!") break @@ -167,7 +177,7 @@ func main() { fmt.Println("\n=== MaxMind Database Update ===") if _, err := os.Stat("config/GeoLite2-Country.mmdb"); err == nil { fmt.Println("MaxMind GeoLite2 Country database found.") - if readBool(reader, "Would you like to update the MaxMind database to the latest version?", false) { + if readBool("Would you like to update the MaxMind database to the latest version?", false) { if err := downloadMaxMindDatabase(); err != nil { fmt.Printf("Error updating MaxMind database: %v\n", err) fmt.Println("You can try updating it manually later if needed.") @@ -175,7 +185,7 @@ func main() { } } else { fmt.Println("MaxMind GeoLite2 Country database not found.") - if readBool(reader, "Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) { + if readBool("Would you like to download the MaxMind GeoLite2 database for geoblocking functionality?", false) { if err := downloadMaxMindDatabase(); err != nil { fmt.Printf("Error downloading MaxMind database: %v\n", err) fmt.Println("You can try downloading it manually later if needed.") @@ -192,11 +202,11 @@ func main() { if !checkIsCrowdsecInstalledInCompose() { fmt.Println("\n=== CrowdSec Install ===") // check if crowdsec is installed - if readBool(reader, "Would you like to install CrowdSec?", false) { + if readBool("Would you like to install CrowdSec?", false) { fmt.Println("This installer constitutes a minimal viable CrowdSec deployment. CrowdSec will add extra complexity to your Pangolin installation and may not work to the best of its abilities out of the box. Users are expected to implement configuration adjustments on their own to achieve the best security posture. Consult the CrowdSec documentation for detailed configuration instructions.") // BUG: crowdsec installation will be skipped if the user chooses to install on the first installation. - if readBool(reader, "Are you willing to manage CrowdSec?", false) { + if readBool("Are you willing to manage CrowdSec?", false) { if config.DashboardDomain == "" { traefikConfig, err := ReadTraefikConfig("config/traefik/traefik_config.yml") if err != nil { @@ -225,8 +235,8 @@ func main() { fmt.Printf("Let's Encrypt Email: %s\n", config.LetsEncryptEmail) fmt.Printf("Badger Version: %s\n", config.BadgerVersion) - if !readBool(reader, "Are these values correct?", true) { - config = collectUserInput(reader) + if !readBool("Are these values correct?", true) { + config = collectUserInput() } } @@ -235,7 +245,7 @@ func main() { if detectedType == Undefined { // If detection fails, prompt the user fmt.Println("Unable to detect container type from existing installation.") - config.InstallationContainerType = podmanOrDocker(reader) + config.InstallationContainerType = podmanOrDocker() } else { config.InstallationContainerType = detectedType fmt.Printf("Detected container type: %s\n", config.InstallationContainerType) @@ -277,8 +287,8 @@ func main() { fmt.Printf("\nTo complete the initial setup, please visit:\nhttps://%s/auth/initial-setup\n", config.DashboardDomain) } -func podmanOrDocker(reader *bufio.Reader) SupportedContainer { - inputContainer := readString(reader, "Would you like to run Pangolin as Docker or Podman containers?", "docker") +func podmanOrDocker() SupportedContainer { + inputContainer := readString("Would you like to run Pangolin as Docker or Podman containers?", "docker") chosenContainer := Docker if strings.EqualFold(inputContainer, "docker") { @@ -290,7 +300,8 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer { os.Exit(1) } - if chosenContainer == Podman { + switch chosenContainer { + case Podman: if !isPodmanInstalled() { fmt.Println("Podman or podman-compose is not installed. Please install both manually. Automated installation will be available in a later release.") os.Exit(1) @@ -299,7 +310,7 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer { if err := exec.Command("bash", "-c", "cat /etc/sysctl.d/99-podman.conf 2>/dev/null | grep 'net.ipv4.ip_unprivileged_port_start=' || cat /etc/sysctl.conf 2>/dev/null | grep 'net.ipv4.ip_unprivileged_port_start='").Run(); err != nil { fmt.Println("Would you like to configure ports >= 80 as unprivileged ports? This enables podman containers to listen on low-range ports.") fmt.Println("Pangolin will experience startup issues if this is not configured, because it needs to listen on port 80/443 by default.") - approved := readBool(reader, "The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system\". Approve?", true) + approved := readBool("The installer is about to execute \"echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system\". Approve?", true) if approved { if os.Geteuid() != 0 { fmt.Println("You need to run the installer as root for such a configuration.") @@ -311,7 +322,7 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer { // Linux only. if err := run("bash", "-c", "echo 'net.ipv4.ip_unprivileged_port_start=80' > /etc/sysctl.d/99-podman.conf && sysctl --system"); err != nil { - fmt.Printf("Error configuring unprivileged ports: %v\n", err) + fmt.Printf("Error configuring unprivileged ports: %v\n", err) os.Exit(1) } } else { @@ -321,7 +332,7 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer { fmt.Println("Unprivileged ports have been configured.") } - } else if chosenContainer == Docker { + case Docker: // check if docker is not installed and the user is root if !isDockerInstalled() { if os.Geteuid() != 0 { @@ -336,7 +347,7 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer { fmt.Println("The installer will not be able to run docker commands without running it as root.") os.Exit(1) } - } else { + default: // This shouldn't happen unless there's a third container runtime. os.Exit(1) } @@ -344,35 +355,35 @@ func podmanOrDocker(reader *bufio.Reader) SupportedContainer { return chosenContainer } -func collectUserInput(reader *bufio.Reader) Config { +func collectUserInput() Config { config := Config{} // Basic configuration fmt.Println("\n=== Basic Configuration ===") - config.IsEnterprise = readBoolNoDefault(reader, "Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.") + config.IsEnterprise = readBoolNoDefault("Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.") - config.BaseDomain = readString(reader, "Enter your base domain (no subdomain e.g. example.com)", "") + config.BaseDomain = readString("Enter your base domain (no subdomain e.g. example.com)", "") // Set default dashboard domain after base domain is collected defaultDashboardDomain := "" if config.BaseDomain != "" { defaultDashboardDomain = "pangolin." + config.BaseDomain } - config.DashboardDomain = readString(reader, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain) - config.LetsEncryptEmail = readString(reader, "Enter email for Let's Encrypt certificates", "") - config.InstallGerbil = readBool(reader, "Do you want to use Gerbil to allow tunneled connections", true) + config.DashboardDomain = readString("Enter the domain for the Pangolin dashboard", defaultDashboardDomain) + config.LetsEncryptEmail = readString("Enter email for Let's Encrypt certificates", "") + config.InstallGerbil = readBool("Do you want to use Gerbil to allow tunneled connections", true) // Email configuration fmt.Println("\n=== Email Configuration ===") - config.EnableEmail = readBool(reader, "Enable email functionality (SMTP)", false) + config.EnableEmail = readBool("Enable email functionality (SMTP)", false) if config.EnableEmail { - config.EmailSMTPHost = readString(reader, "Enter SMTP host", "") - config.EmailSMTPPort = readInt(reader, "Enter SMTP port (default 587)", 587) - config.EmailSMTPUser = readString(reader, "Enter SMTP username", "") - config.EmailSMTPPass = readString(reader, "Enter SMTP password", "") // Should this be readPassword? - config.EmailNoReply = readString(reader, "Enter no-reply email address (often the same as SMTP username)", "") + config.EmailSMTPHost = readString("Enter SMTP host", "") + config.EmailSMTPPort = readInt("Enter SMTP port (default 587)", 587) + config.EmailSMTPUser = readString("Enter SMTP username", "") + config.EmailSMTPPass = readPassword("Enter SMTP password") + config.EmailNoReply = readString("Enter no-reply email address (often the same as SMTP username)", "") } // Validate required fields @@ -393,8 +404,8 @@ func collectUserInput(reader *bufio.Reader) Config { fmt.Println("\n=== Advanced Configuration ===") - config.EnableIPv6 = readBool(reader, "Is your server IPv6 capable?", true) - config.EnableGeoblocking = readBool(reader, "Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", true) + config.EnableIPv6 = readBool("Is your server IPv6 capable?", true) + config.EnableGeoblocking = readBool("Do you want to download the MaxMind GeoLite2 database for geoblocking functionality?", true) if config.DashboardDomain == "" { fmt.Println("Error: Dashboard Domain name is required") @@ -405,10 +416,18 @@ func collectUserInput(reader *bufio.Reader) Config { } func createConfigFiles(config Config) error { - os.MkdirAll("config", 0755) - os.MkdirAll("config/letsencrypt", 0755) - os.MkdirAll("config/db", 0755) - os.MkdirAll("config/logs", 0755) + if err := os.MkdirAll("config", 0755); err != nil { + return fmt.Errorf("failed to create config directory: %v", err) + } + if err := os.MkdirAll("config/letsencrypt", 0755); err != nil { + return fmt.Errorf("failed to create letsencrypt directory: %v", err) + } + if err := os.MkdirAll("config/db", 0755); err != nil { + return fmt.Errorf("failed to create db directory: %v", err) + } + if err := os.MkdirAll("config/logs", 0755); err != nil { + return fmt.Errorf("failed to create logs directory: %v", err) + } // Walk through all embedded files err := fs.WalkDir(configFiles, "config", func(path string, d fs.DirEntry, err error) error { @@ -562,22 +581,24 @@ func showSetupTokenInstructions(containerType SupportedContainer, dashboardDomai fmt.Println("To get your setup token, you need to:") fmt.Println("") fmt.Println("1. Start the containers") - if containerType == Docker { + switch containerType { + case Docker: fmt.Println(" docker compose up -d") - } else if containerType == Podman { + case Podman: fmt.Println(" podman-compose up -d") - } else { } + fmt.Println("") fmt.Println("2. Wait for the Pangolin container to start and generate the token") fmt.Println("") fmt.Println("3. Check the container logs for the setup token") - if containerType == Docker { + switch containerType { + case Docker: fmt.Println(" docker logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'") - } else if containerType == Podman { + case Podman: fmt.Println(" podman logs pangolin | grep -A 2 -B 2 'SETUP TOKEN'") - } else { } + fmt.Println("") fmt.Println("4. Look for output like") fmt.Println(" === SETUP TOKEN GENERATED ===") @@ -639,10 +660,7 @@ func checkPortsAvailable(port int) error { addr := fmt.Sprintf(":%d", port) ln, err := net.Listen("tcp", addr) if err != nil { - return fmt.Errorf( - "ERROR: port %d is occupied or cannot be bound: %w\n\n", - port, err, - ) + return fmt.Errorf("ERROR: port %d is occupied or cannot be bound: %w", port, err) } if closeErr := ln.Close(); closeErr != nil { fmt.Fprintf(os.Stderr, diff --git a/install/theme.go b/install/theme.go new file mode 100644 index 000000000..61247cf1a --- /dev/null +++ b/install/theme.go @@ -0,0 +1,51 @@ +package main + +import ( + "github.com/charmbracelet/huh" + "github.com/charmbracelet/lipgloss" +) + +// Pangolin brand colors (converted from oklch to hex) +var ( + // Primary orange/amber - oklch(0.6717 0.1946 41.93) + primaryColor = lipgloss.AdaptiveColor{Light: "#D97706", Dark: "#F59E0B"} + // Muted foreground + mutedColor = lipgloss.AdaptiveColor{Light: "#737373", Dark: "#A3A3A3"} + // Success green + successColor = lipgloss.AdaptiveColor{Light: "#16A34A", Dark: "#22C55E"} + // Error red - oklch(0.577 0.245 27.325) + errorColor = lipgloss.AdaptiveColor{Light: "#DC2626", Dark: "#EF4444"} + // Normal text + normalFg = lipgloss.AdaptiveColor{Light: "#171717", Dark: "#FAFAFA"} +) + +// ThemePangolin returns a huh theme using Pangolin brand colors +func ThemePangolin() *huh.Theme { + t := huh.ThemeBase() + + // Focused state styles + t.Focused.Base = t.Focused.Base.BorderForeground(primaryColor) + t.Focused.Title = t.Focused.Title.Foreground(primaryColor).Bold(true) + t.Focused.Description = t.Focused.Description.Foreground(mutedColor) + t.Focused.ErrorIndicator = t.Focused.ErrorIndicator.Foreground(errorColor) + t.Focused.ErrorMessage = t.Focused.ErrorMessage.Foreground(errorColor) + t.Focused.SelectSelector = t.Focused.SelectSelector.Foreground(primaryColor) + t.Focused.NextIndicator = t.Focused.NextIndicator.Foreground(primaryColor) + t.Focused.PrevIndicator = t.Focused.PrevIndicator.Foreground(primaryColor) + t.Focused.Option = t.Focused.Option.Foreground(normalFg) + t.Focused.SelectedOption = t.Focused.SelectedOption.Foreground(primaryColor) + t.Focused.SelectedPrefix = lipgloss.NewStyle().Foreground(successColor).SetString("✓ ") + t.Focused.UnselectedPrefix = lipgloss.NewStyle().Foreground(mutedColor).SetString(" ") + t.Focused.FocusedButton = t.Focused.FocusedButton.Foreground(lipgloss.Color("#FFFFFF")).Background(primaryColor) + t.Focused.BlurredButton = t.Focused.BlurredButton.Foreground(normalFg).Background(lipgloss.AdaptiveColor{Light: "#E5E5E5", Dark: "#404040"}) + t.Focused.TextInput.Cursor = t.Focused.TextInput.Cursor.Foreground(primaryColor) + t.Focused.TextInput.Prompt = t.Focused.TextInput.Prompt.Foreground(primaryColor) + + // Blurred state inherits from focused but with hidden border + t.Blurred = t.Focused + t.Blurred.Base = t.Focused.Base.BorderStyle(lipgloss.HiddenBorder()) + t.Blurred.Title = t.Blurred.Title.Foreground(mutedColor).Bold(false) + t.Blurred.TextInput.Prompt = t.Blurred.TextInput.Prompt.Foreground(mutedColor) + + return t +} diff --git a/messages/bg-BG.json b/messages/bg-BG.json index 60e4b401e..0b96141e9 100644 --- a/messages/bg-BG.json +++ b/messages/bg-BG.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "Прокси заявки чрез HTTPS, използвайки напълно квалифицирано име на домейн.", "resourceRaw": "Суров TCP/UDP ресурс", "resourceRawDescription": "Прокси заявки чрез сурови TCP/UDP, използвайки порт номер.", + "resourceRawDescriptionCloud": "Получавайте заявки чрез суров TCP/UDP с използване на портен номер. Изисква се сайтовете да се свързват към отдалечен възел.", "resourceCreate": "Създайте ресурс", "resourceCreateDescription": "Следвайте стъпките по-долу, за да създадете нов ресурс", "resourceSeeAll": "Вижте всички ресурси", @@ -650,6 +651,7 @@ "resourcesErrorUpdate": "Неуспешно превключване на ресурса", "resourcesErrorUpdateDescription": "Възникна грешка при актуализиране на ресурса", "access": "Достъп", + "accessControl": "Контрол на достъпа", "shareLink": "{resource} Сподели връзка", "resourceSelect": "Изберете ресурс", "shareLinks": "Споделени връзки", @@ -1038,7 +1040,6 @@ "pageNotFoundDescription": "О, не! Страницата, която търсите, не съществува.", "overview": "Общ преглед", "home": "Начало", - "accessControl": "Контрол на достъпа", "settings": "Настройки", "usersAll": "Всички потребители", "license": "Лиценз", @@ -1101,6 +1102,12 @@ "actionGetUser": "Получаване на потребител", "actionGetOrgUser": "Вземете потребител на организация", "actionListOrgDomains": "Изброяване на домейни на организация", + "actionGetDomain": "Вземи домейн", + "actionCreateOrgDomain": "Създай домейн", + "actionUpdateOrgDomain": "Актуализирай домейн", + "actionDeleteOrgDomain": "Изтрий домейн", + "actionGetDNSRecords": "Вземи DNS записи", + "actionRestartOrgDomain": "Рестартирай домейн", "actionCreateSite": "Създаване на сайт", "actionDeleteSite": "Изтриване на сайта", "actionGetSite": "Вземете сайт", @@ -1419,6 +1426,7 @@ "domainPickerNamespace": "Име на пространство: {namespace}", "domainPickerShowMore": "Покажи повече", "regionSelectorTitle": "Избор на регион", + "domainPickerRemoteExitNodeWarning": "Предоставените домейни не се поддържат, когато сайтовете се свързват към отдалечени крайни възли. За да бъдат ресурсите налични на отдалечени възли, използвайте персонализиран домейн вместо това.", "regionSelectorInfo": "Изборът на регион ни помага да предоставим по-добра производителност за вашето местоположение. Не е необходимо да сте в същия регион като сървъра.", "regionSelectorPlaceholder": "Изберете регион", "regionSelectorComingSoon": "Очаква се скоро", @@ -1669,10 +1677,10 @@ "sshSudoModeCommandsDescription": "Потребителят може да изпълнява само определени команди с sudo.", "sshSudo": "Разреши sudo", "sshSudoCommands": "Sudo команди", - "sshSudoCommandsDescription": "Списък с команди, които потребителят е разрешено да изпълнява с sudo.", + "sshSudoCommandsDescription": "Списък, разделен със запетаи, с команди, които потребителят е позволено да изпълнява с sudo.", "sshCreateHomeDir": "Създай начална директория", "sshUnixGroups": "Unix групи", - "sshUnixGroupsDescription": "Unix групи, в които да добавите потребителя на целевия хост.", + "sshUnixGroupsDescription": "Списък, разделен със запетаи, с Unix групи, към които да се добави потребителят на целевия хост.", "retryAttempts": "Опити за повторно", "expectedResponseCodes": "Очаквани кодове за отговор", "expectedResponseCodesDescription": "HTTP статус код, указващ здравословно състояние. Ако бъде оставено празно, между 200-300 се счита за здравословно.", @@ -2335,8 +2343,8 @@ "logRetentionEndOfFollowingYear": "Край на следващата година", "actionLogsDescription": "Прегледайте историята на действията, извършени в тази организация", "accessLogsDescription": "Прегледайте заявките за удостоверяване на достъпа до ресурсите в тази организация", - "licenseRequiredToUse": "Изисква се лиценз за Enterprise Edition, за да използвате тази функция. Тази функция е също достъпна в Pangolin Cloud.", - "ossEnterpriseEditionRequired": "Необходимо е изданието Enterprise, за да използвате тази функция. Тази функция е също достъпна в Pangolin Cloud.", + "licenseRequiredToUse": "Изисква се лиценз за Enterprise Edition или Pangolin Cloud за използване на тази функция. Резервирайте демонстрация или пробен POC.", + "ossEnterpriseEditionRequired": "Enterprise Edition е необходим за използване на тази функция. Тази функция също е налична в Pangolin Cloud. Резервирайте демонстрация или пробен POC.", "certResolver": "Решавач на сертификати", "certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.", "selectCertResolver": "Изберете решавач на сертификати", @@ -2673,5 +2681,6 @@ "approvalsEmptyStateStep2Title": "Активирайте одобрения на устройства", "approvalsEmptyStateStep2Description": "Редактирайте ролята и активирайте опцията 'Изискване на одобрения за устройства'. Потребители с тази роля ще трябва администраторско одобрение за нови устройства.", "approvalsEmptyStatePreviewDescription": "Преглед: Когато е активирано, чакащите заявки за устройства ще се появят тук за преглед", - "approvalsEmptyStateButtonText": "Управлявайте роли" + "approvalsEmptyStateButtonText": "Управлявайте роли", + "domainErrorTitle": "Имаме проблем с проверката на вашия домейн" } diff --git a/messages/cs-CZ.json b/messages/cs-CZ.json index b7666db1b..cb5372b36 100644 --- a/messages/cs-CZ.json +++ b/messages/cs-CZ.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "Proxy požadavky přes HTTPS pomocí plně kvalifikovaného názvu domény.", "resourceRaw": "Surový TCP/UDP zdroj", "resourceRawDescription": "Proxy požadavky přes nezpracovaný TCP/UDP pomocí čísla portu.", + "resourceRawDescriptionCloud": "Proxy požadavky na syrové TCP/UDP pomocí čísla portu. Vyžaduje připojení stránek ke vzdálenému uzlu.", "resourceCreate": "Vytvořit zdroj", "resourceCreateDescription": "Postupujte podle níže uvedených kroků, abyste vytvořili a připojili nový zdroj", "resourceSeeAll": "Zobrazit všechny zdroje", @@ -650,6 +651,7 @@ "resourcesErrorUpdate": "Nepodařilo se přepnout zdroj", "resourcesErrorUpdateDescription": "Došlo k chybě při aktualizaci zdroje", "access": "Přístup", + "accessControl": "Kontrola přístupu", "shareLink": "{resource} Sdílet odkaz", "resourceSelect": "Vyberte zdroj", "shareLinks": "Sdílet odkazy", @@ -1038,7 +1040,6 @@ "pageNotFoundDescription": "Jejda! Stránka, kterou hledáte, neexistuje.", "overview": "Přehled", "home": "Domů", - "accessControl": "Kontrola přístupu", "settings": "Nastavení", "usersAll": "Všichni uživatelé", "license": "Licence", @@ -1101,6 +1102,12 @@ "actionGetUser": "Získat uživatele", "actionGetOrgUser": "Získat uživatele organizace", "actionListOrgDomains": "Seznam domén organizace", + "actionGetDomain": "Získat doménu", + "actionCreateOrgDomain": "Vytvořit doménu", + "actionUpdateOrgDomain": "Aktualizovat doménu", + "actionDeleteOrgDomain": "Odstranit doménu", + "actionGetDNSRecords": "Získat záznamy DNS", + "actionRestartOrgDomain": "Restartovat doménu", "actionCreateSite": "Vytvořit lokalitu", "actionDeleteSite": "Odstranění lokality", "actionGetSite": "Získat web", @@ -1419,6 +1426,7 @@ "domainPickerNamespace": "Jmenný prostor: {namespace}", "domainPickerShowMore": "Zobrazit více", "regionSelectorTitle": "Vybrat region", + "domainPickerRemoteExitNodeWarning": "Poskytnuté domény nejsou podporovány, když se stránky připojují k vzdáleným výstupním uzlům. Pro dostupné zdroje na vzdálených uzlech použijte vlastní doménu.", "regionSelectorInfo": "Výběr regionu nám pomáhá poskytovat lepší výkon pro vaši polohu. Nemusíte být ve stejném regionu jako váš server.", "regionSelectorPlaceholder": "Vyberte region", "regionSelectorComingSoon": "Již brzy", @@ -1669,10 +1677,10 @@ "sshSudoModeCommandsDescription": "Uživatel může spustit pouze zadané příkazy s sudo.", "sshSudo": "Povolit sudo", "sshSudoCommands": "Sudo příkazy", - "sshSudoCommandsDescription": "Seznam příkazů, které může uživatel spouštět s sudo.", + "sshSudoCommandsDescription": "Čárkami oddělený seznam příkazů, které může uživatel spouštět s sudo.", "sshCreateHomeDir": "Vytvořit domovský adresář", "sshUnixGroups": "Unixové skupiny", - "sshUnixGroupsDescription": "Unix skupiny přidají uživatele do cílového hostitele.", + "sshUnixGroupsDescription": "Čárkou oddělené skupiny Unix přidají uživatele do cílového hostitele.", "retryAttempts": "Opakovat pokusy", "expectedResponseCodes": "Očekávané kódy odezvy", "expectedResponseCodesDescription": "HTTP kód stavu, který označuje zdravý stav. Ponecháte-li prázdné, 200-300 je považováno za zdravé.", @@ -2335,8 +2343,8 @@ "logRetentionEndOfFollowingYear": "Konec následujícího roku", "actionLogsDescription": "Zobrazit historii akcí provedených v této organizaci", "accessLogsDescription": "Zobrazit žádosti o ověření přístupu pro zdroje v této organizaci", - "licenseRequiredToUse": "Pro použití této funkce je vyžadována licence Enterprise Edition . Tato funkce je také dostupná v Pangolin Cloud.", - "ossEnterpriseEditionRequired": "Enterprise Edition je vyžadována pro použití této funkce. Tato funkce je také k dispozici v Pangolin Cloud.", + "licenseRequiredToUse": "Pro použití této funkce je vyžadována licence Enterprise Edition nebo Pangolin Cloud . Zarezervujte si demo nebo POC zkušební verzi.", + "ossEnterpriseEditionRequired": "Enterprise Edition je vyžadována pro použití této funkce. Tato funkce je také k dispozici v Pangolin Cloud. Rezervujte si demo nebo POC zkušební verzi.", "certResolver": "Oddělovač certifikátů", "certResolverDescription": "Vyberte řešitele certifikátů pro tento dokument.", "selectCertResolver": "Vyberte řešič certifikátů", @@ -2673,5 +2681,6 @@ "approvalsEmptyStateStep2Title": "Povolit schválení zařízení", "approvalsEmptyStateStep2Description": "Upravte roli a povolte možnost 'Vyžadovat schválení zařízení'. Uživatelé s touto rolí budou potřebovat schválení pro nová zařízení správce.", "approvalsEmptyStatePreviewDescription": "Náhled: Pokud je povoleno, čekající na zařízení se zde zobrazí žádosti o recenzi", - "approvalsEmptyStateButtonText": "Spravovat role" + "approvalsEmptyStateButtonText": "Spravovat role", + "domainErrorTitle": "Máme problém s ověřením tvé domény" } diff --git a/messages/de-DE.json b/messages/de-DE.json index 15663fa47..150a8597e 100644 --- a/messages/de-DE.json +++ b/messages/de-DE.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "Proxy-Anfragen über HTTPS mit einem voll qualifizierten Domain-Namen.", "resourceRaw": "Direkte TCP/UDP Ressource (raw)", "resourceRawDescription": "Proxy-Anfragen über rohes TCP/UDP mit einer Portnummer.", + "resourceRawDescriptionCloud": "Proxy-Anfragen über rohe TCP/UDP mit Portnummer. Benötigt Sites, um sich mit einem entfernten Knoten zu verbinden.", "resourceCreate": "Ressource erstellen", "resourceCreateDescription": "Folgen Sie den Schritten unten, um eine neue Ressource zu erstellen", "resourceSeeAll": "Alle Ressourcen anzeigen", @@ -650,6 +651,7 @@ "resourcesErrorUpdate": "Fehler beim Umschalten der Ressource", "resourcesErrorUpdateDescription": "Beim Aktualisieren der Ressource ist ein Fehler aufgetreten", "access": "Zugriff", + "accessControl": "Zugriffskontrolle", "shareLink": "{resource} Freigabe-Link", "resourceSelect": "Ressource auswählen", "shareLinks": "Freigabe-Links", @@ -1038,7 +1040,6 @@ "pageNotFoundDescription": "Hoppla! Die gesuchte Seite existiert nicht.", "overview": "Übersicht", "home": "Startseite", - "accessControl": "Zugriffskontrolle", "settings": "Einstellungen", "usersAll": "Alle Benutzer", "license": "Lizenz", @@ -1101,6 +1102,12 @@ "actionGetUser": "Benutzer abrufen", "actionGetOrgUser": "Organisationsbenutzer abrufen", "actionListOrgDomains": "Organisationsdomains auflisten", + "actionGetDomain": "Domain abrufen", + "actionCreateOrgDomain": "Domain erstellen", + "actionUpdateOrgDomain": "Domain aktualisieren", + "actionDeleteOrgDomain": "Domain löschen", + "actionGetDNSRecords": "DNS-Einträge abrufen", + "actionRestartOrgDomain": "Domain neu starten", "actionCreateSite": "Standort erstellen", "actionDeleteSite": "Standort löschen", "actionGetSite": "Standort abrufen", @@ -1419,6 +1426,7 @@ "domainPickerNamespace": "Namespace: {namespace}", "domainPickerShowMore": "Mehr anzeigen", "regionSelectorTitle": "Region auswählen", + "domainPickerRemoteExitNodeWarning": "Angegebene Domains werden nicht unterstützt, wenn sich Websites mit externen Exit-Knoten verbinden. Damit Ressourcen auf entfernten Knoten verfügbar sind, verwenden Sie stattdessen eine eigene Domain.", "regionSelectorInfo": "Das Auswählen einer Region hilft uns, eine bessere Leistung für Ihren Standort bereitzustellen. Sie müssen sich nicht in derselben Region wie Ihr Server befinden.", "regionSelectorPlaceholder": "Wähle eine Region", "regionSelectorComingSoon": "Kommt bald", @@ -1669,10 +1677,10 @@ "sshSudoModeCommandsDescription": "Benutzer kann nur die angegebenen Befehle mit sudo ausführen.", "sshSudo": "sudo erlauben", "sshSudoCommands": "Sudo-Befehle", - "sshSudoCommandsDescription": "Liste der Befehle, die der Benutzer mit sudo ausführen darf.", + "sshSudoCommandsDescription": "Kommagetrennte Liste von Befehlen, die der Benutzer mit sudo ausführen darf.", "sshCreateHomeDir": "Home-Verzeichnis erstellen", "sshUnixGroups": "Unix-Gruppen", - "sshUnixGroupsDescription": "Unix-Gruppen, zu denen der Benutzer auf dem Ziel-Host hinzugefügt wird.", + "sshUnixGroupsDescription": "Durch Komma getrennte Unix-Gruppen, um den Benutzer auf dem Zielhost hinzuzufügen.", "retryAttempts": "Wiederholungsversuche", "expectedResponseCodes": "Erwartete Antwortcodes", "expectedResponseCodesDescription": "HTTP-Statuscode, der einen gesunden Zustand anzeigt. Wenn leer gelassen, wird 200-300 als gesund angesehen.", @@ -2335,8 +2343,8 @@ "logRetentionEndOfFollowingYear": "Ende des folgenden Jahres", "actionLogsDescription": "Verlauf der in dieser Organisation durchgeführten Aktionen anzeigen", "accessLogsDescription": "Zugriffsauth-Anfragen für Ressourcen in dieser Organisation anzeigen", - "licenseRequiredToUse": "Um diese Funktion nutzen zu können, ist eine Enterprise Edition Lizenz erforderlich. Diese Funktion ist auch in der Pangolin Cloud verfügbar.", - "ossEnterpriseEditionRequired": "Um diese Funktion nutzen zu können, ist die Enterprise Edition erforderlich. Diese Funktion ist auch in der Pangolin Cloud verfügbar.", + "licenseRequiredToUse": "Eine Enterprise Edition Lizenz oder Pangolin Cloud wird benötigt, um diese Funktion nutzen zu können. Buchen Sie eine Demo oder POC Testversion.", + "ossEnterpriseEditionRequired": "Die Enterprise Edition wird benötigt, um diese Funktion nutzen zu können. Diese Funktion ist auch in Pangolin Cloudverfügbar. Buchen Sie eine Demo oder POC Testversion.", "certResolver": "Zertifikatsauflöser", "certResolverDescription": "Wählen Sie den Zertifikatslöser aus, der für diese Ressource verwendet werden soll.", "selectCertResolver": "Zertifikatsauflöser auswählen", @@ -2673,5 +2681,6 @@ "approvalsEmptyStateStep2Title": "Gerätegenehmigungen aktivieren", "approvalsEmptyStateStep2Description": "Bearbeite eine Rolle und aktiviere die Option 'Gerätegenehmigung erforderlich'. Benutzer mit dieser Rolle benötigen Administrator-Genehmigung für neue Geräte.", "approvalsEmptyStatePreviewDescription": "Vorschau: Wenn aktiviert, werden ausstehende Geräteanfragen hier zur Überprüfung angezeigt", - "approvalsEmptyStateButtonText": "Rollen verwalten" + "approvalsEmptyStateButtonText": "Rollen verwalten", + "domainErrorTitle": "Wir haben Probleme mit der Überprüfung deiner Domain" } diff --git a/messages/en-US.json b/messages/en-US.json index d872d8e39..895ee1332 100644 --- a/messages/en-US.json +++ b/messages/en-US.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "Proxy requests over HTTPS using a fully qualified domain name.", "resourceRaw": "Raw TCP/UDP Resource", "resourceRawDescription": "Proxy requests over raw TCP/UDP using a port number.", + "resourceRawDescriptionCloud": "Proxy requests over raw TCP/UDP using a port number. Requires sites to connect to a remote node.", "resourceCreate": "Create Resource", "resourceCreateDescription": "Follow the steps below to create a new resource", "resourceSeeAll": "See All Resources", @@ -649,7 +650,8 @@ "resourcesUsersRolesAccess": "User and role-based access control", "resourcesErrorUpdate": "Failed to toggle resource", "resourcesErrorUpdateDescription": "An error occurred while updating the resource", - "access": "Access Control", + "access": "Access", + "accessControl": "Access Control", "shareLink": "{resource} Share Link", "resourceSelect": "Select resource", "shareLinks": "Share Links", @@ -1101,6 +1103,12 @@ "actionGetUser": "Get User", "actionGetOrgUser": "Get Organization User", "actionListOrgDomains": "List Organization Domains", + "actionGetDomain": "Get Domain", + "actionCreateOrgDomain": "Create Domain", + "actionUpdateOrgDomain": "Update Domain", + "actionDeleteOrgDomain": "Delete Domain", + "actionGetDNSRecords": "Get DNS Records", + "actionRestartOrgDomain": "Restart Domain", "actionCreateSite": "Create Site", "actionDeleteSite": "Delete Site", "actionGetSite": "Get Site", @@ -1112,6 +1120,7 @@ "setupTokenDescription": "Enter the setup token from the server console.", "setupTokenRequired": "Setup token is required", "actionUpdateSite": "Update Site", + "actionResetSiteBandwidth": "Reset Organization Bandwidth", "actionListSiteRoles": "List Allowed Site Roles", "actionCreateResource": "Create Resource", "actionDeleteResource": "Delete Resource", @@ -1419,6 +1428,7 @@ "domainPickerNamespace": "Namespace: {namespace}", "domainPickerShowMore": "Show More", "regionSelectorTitle": "Select Region", + "domainPickerRemoteExitNodeWarning": "Provided domains are not supported when sites connect to remote exit nodes. For resources to be available on remote nodes, use a custom domain instead.", "regionSelectorInfo": "Selecting a region helps us provide better performance for your location. You do not have to be in the same region as your server.", "regionSelectorPlaceholder": "Choose a region", "regionSelectorComingSoon": "Coming Soon", @@ -1669,10 +1679,10 @@ "sshSudoModeCommandsDescription": "User can run only the specified commands with sudo.", "sshSudo": "Allow sudo", "sshSudoCommands": "Sudo Commands", - "sshSudoCommandsDescription": "List of commands the user is allowed to run with sudo.", + "sshSudoCommandsDescription": "Comma separated list of commands the user is allowed to run with sudo.", "sshCreateHomeDir": "Create Home Directory", "sshUnixGroups": "Unix Groups", - "sshUnixGroupsDescription": "Unix groups to add the user to on the target host.", + "sshUnixGroupsDescription": "Comma separated Unix groups to add the user to on the target host.", "retryAttempts": "Retry Attempts", "expectedResponseCodes": "Expected Response Codes", "expectedResponseCodesDescription": "HTTP status code that indicates healthy status. If left blank, 200-300 is considered healthy.", @@ -2335,8 +2345,8 @@ "logRetentionEndOfFollowingYear": "End of following year", "actionLogsDescription": "View a history of actions performed in this organization", "accessLogsDescription": "View access auth requests for resources in this organization", - "licenseRequiredToUse": "An Enterprise Edition license or Pangolin Cloud is required to use this feature.", - "ossEnterpriseEditionRequired": "The Enterprise Edition is required to use this feature. This feature is also available in Pangolin Cloud.", + "licenseRequiredToUse": "An Enterprise Edition license or Pangolin Cloud is required to use this feature. Book a demo or POC trial.", + "ossEnterpriseEditionRequired": "The Enterprise Edition is required to use this feature. This feature is also available in Pangolin Cloud. Book a demo or POC trial.", "certResolver": "Certificate Resolver", "certResolverDescription": "Select the certificate resolver to use for this resource.", "selectCertResolver": "Select Certificate Resolver", @@ -2542,7 +2552,7 @@ "internalResourceAuthDaemonSite": "On Site", "internalResourceAuthDaemonSiteDescription": "Auth daemon runs on the site (Newt).", "internalResourceAuthDaemonRemote": "Remote Host", - "internalResourceAuthDaemonRemoteDescription": "Auth daemon runs on a host that is not the site.", + "internalResourceAuthDaemonRemoteDescription": "Auth daemon runs on this resource's destination - not the site.", "internalResourceAuthDaemonPort": "Daemon Port (optional)", "orgAuthWhatsThis": "Where can I find my organization ID?", "learnMore": "Learn more", @@ -2673,5 +2683,6 @@ "approvalsEmptyStateStep2Title": "Enable Device Approvals", "approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.", "approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review", - "approvalsEmptyStateButtonText": "Manage Roles" + "approvalsEmptyStateButtonText": "Manage Roles", + "domainErrorTitle": "We are having trouble verifying your domain" } diff --git a/messages/es-ES.json b/messages/es-ES.json index 6bb73cdae..e33a85ace 100644 --- a/messages/es-ES.json +++ b/messages/es-ES.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "Proxy proporciona solicitudes sobre HTTPS usando un nombre de dominio completamente calificado.", "resourceRaw": "Recurso TCP/UDP sin procesar", "resourceRawDescription": "Proxy proporciona solicitudes sobre TCP/UDP usando un número de puerto.", + "resourceRawDescriptionCloud": "Las peticiones de proxy sobre TCP/UDP crudas usando un número de puerto. Requiere que los sitios se conecten a un nodo remoto.", "resourceCreate": "Crear Recurso", "resourceCreateDescription": "Siga los siguientes pasos para crear un nuevo recurso", "resourceSeeAll": "Ver todos los recursos", @@ -650,6 +651,7 @@ "resourcesErrorUpdate": "Error al cambiar el recurso", "resourcesErrorUpdateDescription": "Se ha producido un error al actualizar el recurso", "access": "Acceder", + "accessControl": "Control de acceso", "shareLink": "{resource} Compartir Enlace", "resourceSelect": "Seleccionar recurso", "shareLinks": "Compartir enlaces", @@ -1038,7 +1040,6 @@ "pageNotFoundDescription": "¡Vaya! La página que estás buscando no existe.", "overview": "Resumen", "home": "Inicio", - "accessControl": "Control de acceso", "settings": "Ajustes", "usersAll": "Todos los usuarios", "license": "Licencia", @@ -1101,6 +1102,12 @@ "actionGetUser": "Obtener usuario", "actionGetOrgUser": "Obtener usuario de la organización", "actionListOrgDomains": "Listar dominios de la organización", + "actionGetDomain": "Obtener dominio", + "actionCreateOrgDomain": "Crear dominio", + "actionUpdateOrgDomain": "Actualizar dominio", + "actionDeleteOrgDomain": "Eliminar dominio", + "actionGetDNSRecords": "Obtener registros DNS", + "actionRestartOrgDomain": "Reiniciar dominio", "actionCreateSite": "Crear sitio", "actionDeleteSite": "Eliminar sitio", "actionGetSite": "Obtener sitio", @@ -1419,6 +1426,7 @@ "domainPickerNamespace": "Espacio de nombres: {namespace}", "domainPickerShowMore": "Mostrar más", "regionSelectorTitle": "Seleccionar Región", + "domainPickerRemoteExitNodeWarning": "Los dominios suministrados no son compatibles cuando los sitios se conectan a nodos de salida remotos. Para que los recursos estén disponibles en nodos remotos, utilice un dominio personalizado en su lugar.", "regionSelectorInfo": "Seleccionar una región nos ayuda a brindar un mejor rendimiento para tu ubicación. No tienes que estar en la misma región que tu servidor.", "regionSelectorPlaceholder": "Elige una región", "regionSelectorComingSoon": "Próximamente", @@ -1669,10 +1677,10 @@ "sshSudoModeCommandsDescription": "El usuario sólo puede ejecutar los comandos especificados con sudo.", "sshSudo": "Permitir sudo", "sshSudoCommands": "Comandos Sudo", - "sshSudoCommandsDescription": "Lista de comandos que el usuario puede ejecutar con sudo.", + "sshSudoCommandsDescription": "Lista separada por comas de comandos que el usuario puede ejecutar con sudo.", "sshCreateHomeDir": "Crear directorio principal", "sshUnixGroups": "Grupos Unix", - "sshUnixGroupsDescription": "Grupos Unix para agregar el usuario en el host de destino.", + "sshUnixGroupsDescription": "Grupos Unix separados por comas para agregar el usuario en el host de destino.", "retryAttempts": "Intentos de Reintento", "expectedResponseCodes": "Códigos de respuesta esperados", "expectedResponseCodesDescription": "Código de estado HTTP que indica un estado saludable. Si se deja en blanco, se considera saludable de 200 a 300.", @@ -2335,8 +2343,8 @@ "logRetentionEndOfFollowingYear": "Fin del año siguiente", "actionLogsDescription": "Ver un historial de acciones realizadas en esta organización", "accessLogsDescription": "Ver solicitudes de acceso a los recursos de esta organización", - "licenseRequiredToUse": "Se requiere una licencia Enterprise Edition para utilizar esta función. Esta característica también está disponible en Pangolin Cloud.", - "ossEnterpriseEditionRequired": "La versión Enterprise es necesaria para utilizar esta función. Esta función también está disponible en Pangolin Cloud.", + "licenseRequiredToUse": "Se requiere una licencia Enterprise Edition o Pangolin Cloud para usar esta función. Reserve una demostración o prueba POC.", + "ossEnterpriseEditionRequired": "La Enterprise Edition es necesaria para utilizar esta función. Esta función también está disponible en Pangolin Cloud. Reserva una demostración o prueba POC.", "certResolver": "Resolver certificado", "certResolverDescription": "Seleccione la resolución de certificados a utilizar para este recurso.", "selectCertResolver": "Seleccionar Resolver Certificado", @@ -2673,5 +2681,6 @@ "approvalsEmptyStateStep2Title": "Habilitar aprobaciones de dispositivo", "approvalsEmptyStateStep2Description": "Editar un rol y habilitar la opción 'Requerir aprobaciones de dispositivos'. Los usuarios con este rol necesitarán la aprobación del administrador para nuevos dispositivos.", "approvalsEmptyStatePreviewDescription": "Vista previa: Cuando está habilitado, las solicitudes de dispositivo pendientes aparecerán aquí para su revisión", - "approvalsEmptyStateButtonText": "Administrar roles" + "approvalsEmptyStateButtonText": "Administrar roles", + "domainErrorTitle": "Estamos teniendo problemas para verificar su dominio" } diff --git a/messages/fr-FR.json b/messages/fr-FR.json index 004354f16..ec3dbffb8 100644 --- a/messages/fr-FR.json +++ b/messages/fr-FR.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "Proxy les demandes sur HTTPS en utilisant un nom de domaine entièrement qualifié.", "resourceRaw": "Ressource TCP/UDP brute", "resourceRawDescription": "Proxy les demandes sur TCP/UDP brut en utilisant un numéro de port.", + "resourceRawDescriptionCloud": "Requêtes de proxy sur TCP/UDP brute en utilisant un numéro de port. Nécessite des sites pour se connecter à un noeud distant.", "resourceCreate": "Créer une ressource", "resourceCreateDescription": "Suivez les étapes ci-dessous pour créer une nouvelle ressource", "resourceSeeAll": "Voir toutes les ressources", @@ -650,6 +651,7 @@ "resourcesErrorUpdate": "Échec de la bascule de la ressource", "resourcesErrorUpdateDescription": "Une erreur s'est produite lors de la mise à jour de la ressource", "access": "Accès", + "accessControl": "Contrôle d'accès", "shareLink": "Lien de partage {resource}", "resourceSelect": "Sélectionner une ressource", "shareLinks": "Liens de partage", @@ -1038,7 +1040,6 @@ "pageNotFoundDescription": "Oups! La page que vous recherchez n'existe pas.", "overview": "Vue d'ensemble", "home": "Accueil", - "accessControl": "Contrôle d'accès", "settings": "Paramètres", "usersAll": "Tous les utilisateurs", "license": "Licence", @@ -1101,6 +1102,12 @@ "actionGetUser": "Obtenir l'utilisateur", "actionGetOrgUser": "Obtenir l'utilisateur de l'organisation", "actionListOrgDomains": "Lister les domaines de l'organisation", + "actionGetDomain": "Obtenir un domaine", + "actionCreateOrgDomain": "Créer un domaine", + "actionUpdateOrgDomain": "Mettre à jour le domaine", + "actionDeleteOrgDomain": "Supprimer le domaine", + "actionGetDNSRecords": "Récupérer les enregistrements DNS", + "actionRestartOrgDomain": "Redémarrer le domaine", "actionCreateSite": "Créer un site", "actionDeleteSite": "Supprimer un site", "actionGetSite": "Obtenir un site", @@ -1419,6 +1426,7 @@ "domainPickerNamespace": "Espace de noms : {namespace}", "domainPickerShowMore": "Afficher plus", "regionSelectorTitle": "Sélectionner Région", + "domainPickerRemoteExitNodeWarning": "Les domaines fournis ne sont pas pris en charge lorsque les sites se connectent à des nœuds de sortie distants. Pour que les ressources soient disponibles sur des nœuds distants, utilisez un domaine personnalisé à la place.", "regionSelectorInfo": "Sélectionner une région nous aide à offrir de meilleures performances pour votre localisation. Vous n'avez pas besoin d'être dans la même région que votre serveur.", "regionSelectorPlaceholder": "Choisissez une région", "regionSelectorComingSoon": "Bientôt disponible", @@ -1669,10 +1677,10 @@ "sshSudoModeCommandsDescription": "L'utilisateur ne peut exécuter que les commandes spécifiées avec sudo.", "sshSudo": "Autoriser sudo", "sshSudoCommands": "Commandes Sudo", - "sshSudoCommandsDescription": "Liste des commandes que l'utilisateur est autorisé à exécuter avec sudo.", + "sshSudoCommandsDescription": "Liste des commandes séparées par des virgules que l'utilisateur est autorisé à exécuter avec sudo.", "sshCreateHomeDir": "Créer un répertoire personnel", "sshUnixGroups": "Groupes Unix", - "sshUnixGroupsDescription": "Groupes Unix à ajouter à l'utilisateur sur l'hôte cible.", + "sshUnixGroupsDescription": "Groupes Unix séparés par des virgules pour ajouter l'utilisateur sur l'hôte cible.", "retryAttempts": "Tentatives de réessai", "expectedResponseCodes": "Codes de réponse attendus", "expectedResponseCodesDescription": "Code de statut HTTP indiquant un état de santé satisfaisant. Si non renseigné, 200-300 est considéré comme satisfaisant.", @@ -2335,8 +2343,8 @@ "logRetentionEndOfFollowingYear": "Fin de l'année suivante", "actionLogsDescription": "Voir l'historique des actions effectuées dans cette organisation", "accessLogsDescription": "Voir les demandes d'authentification d'accès aux ressources de cette organisation", - "licenseRequiredToUse": "Une licence Enterprise Edition est nécessaire pour utiliser cette fonctionnalité. Cette fonctionnalité est également disponible dans Pangolin Cloud.", - "ossEnterpriseEditionRequired": "La version Enterprise Edition est requise pour utiliser cette fonctionnalité. Cette fonctionnalité est également disponible dans Pangolin Cloud.", + "licenseRequiredToUse": "Une licence Enterprise Edition ou Pangolin Cloud est requise pour utiliser cette fonctionnalité. Réservez une démonstration ou une évaluation de POC.", + "ossEnterpriseEditionRequired": "La version Enterprise Edition est requise pour utiliser cette fonctionnalité. Cette fonctionnalité est également disponible dans Pangolin Cloud. Réservez une démo ou un essai POC.", "certResolver": "Résolveur de certificat", "certResolverDescription": "Sélectionnez le solveur de certificat à utiliser pour cette ressource.", "selectCertResolver": "Sélectionnez le résolveur de certificat", @@ -2673,5 +2681,6 @@ "approvalsEmptyStateStep2Title": "Activer les autorisations de l'appareil", "approvalsEmptyStateStep2Description": "Modifier un rôle et activer l'option 'Exiger les autorisations de l'appareil'. Les utilisateurs avec ce rôle auront besoin de l'approbation de l'administrateur pour les nouveaux appareils.", "approvalsEmptyStatePreviewDescription": "Aperçu: Lorsque cette option est activée, les demandes de périphérique en attente apparaîtront ici pour vérification", - "approvalsEmptyStateButtonText": "Gérer les rôles" + "approvalsEmptyStateButtonText": "Gérer les rôles", + "domainErrorTitle": "Nous avons des difficultés à vérifier votre domaine" } diff --git a/messages/it-IT.json b/messages/it-IT.json index 5a60a2963..adab7879a 100644 --- a/messages/it-IT.json +++ b/messages/it-IT.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "Richieste proxy su HTTPS usando un nome di dominio completo.", "resourceRaw": "Risorsa Raw TCP/UDP", "resourceRawDescription": "Richieste proxy su TCP/UDP grezzo utilizzando un numero di porta.", + "resourceRawDescriptionCloud": "Richiesta proxy su TCP/UDP grezzo utilizzando un numero di porta. Richiede siti per connettersi a un nodo remoto.", "resourceCreate": "Crea Risorsa", "resourceCreateDescription": "Segui i passaggi seguenti per creare una nuova risorsa", "resourceSeeAll": "Vedi Tutte Le Risorse", @@ -650,6 +651,7 @@ "resourcesErrorUpdate": "Impossibile attivare/disattivare la risorsa", "resourcesErrorUpdateDescription": "Si è verificato un errore durante l'aggiornamento della risorsa", "access": "Accesso", + "accessControl": "Controllo Accessi", "shareLink": "Link di Condivisione {resource}", "resourceSelect": "Seleziona risorsa", "shareLinks": "Link di Condivisione", @@ -1038,7 +1040,6 @@ "pageNotFoundDescription": "Oops! La pagina che stai cercando non esiste.", "overview": "Panoramica", "home": "Home", - "accessControl": "Controllo Accessi", "settings": "Impostazioni", "usersAll": "Tutti Gli Utenti", "license": "Licenza", @@ -1101,6 +1102,12 @@ "actionGetUser": "Ottieni Utente", "actionGetOrgUser": "Ottieni Utente Organizzazione", "actionListOrgDomains": "Elenca Domini Organizzazione", + "actionGetDomain": "Ottieni Dominio", + "actionCreateOrgDomain": "Crea Dominio", + "actionUpdateOrgDomain": "Aggiorna Dominio", + "actionDeleteOrgDomain": "Elimina Dominio", + "actionGetDNSRecords": "Ottieni Record DNS", + "actionRestartOrgDomain": "Riavvia Dominio", "actionCreateSite": "Crea Sito", "actionDeleteSite": "Elimina Sito", "actionGetSite": "Ottieni Sito", @@ -1419,6 +1426,7 @@ "domainPickerNamespace": "Namespace: {namespace}", "domainPickerShowMore": "Mostra Altro", "regionSelectorTitle": "Seleziona regione", + "domainPickerRemoteExitNodeWarning": "I domini forniti non sono supportati quando i siti si connettono a nodi di uscita remoti. Affinché le risorse siano disponibili su nodi remoti, utilizza invece un dominio personalizzato.", "regionSelectorInfo": "Selezionare una regione ci aiuta a fornire migliori performance per la tua posizione. Non devi necessariamente essere nella stessa regione del tuo server.", "regionSelectorPlaceholder": "Scegli una regione", "regionSelectorComingSoon": "Prossimamente", @@ -1669,10 +1677,10 @@ "sshSudoModeCommandsDescription": "L'utente può eseguire solo i comandi specificati con sudo.", "sshSudo": "Consenti sudo", "sshSudoCommands": "Comandi Sudo", - "sshSudoCommandsDescription": "Elenco di comandi che l'utente può eseguire con sudo.", + "sshSudoCommandsDescription": "Elenco di comandi separati da virgole che l'utente può eseguire con sudo.", "sshCreateHomeDir": "Crea Cartella Home", "sshUnixGroups": "Gruppi Unix", - "sshUnixGroupsDescription": "Gruppi Unix su cui aggiungere l'utente sull'host di destinazione.", + "sshUnixGroupsDescription": "Gruppi Unix separati da virgole per aggiungere l'utente sull'host di destinazione.", "retryAttempts": "Tentativi di Riprova", "expectedResponseCodes": "Codici di Risposta Attesi", "expectedResponseCodesDescription": "Codice di stato HTTP che indica lo stato di salute. Se lasciato vuoto, considerato sano è compreso tra 200-300.", @@ -2335,8 +2343,8 @@ "logRetentionEndOfFollowingYear": "Fine dell'anno successivo", "actionLogsDescription": "Visualizza una cronologia delle azioni eseguite in questa organizzazione", "accessLogsDescription": "Visualizza le richieste di autenticazione di accesso per le risorse in questa organizzazione", - "licenseRequiredToUse": "Per utilizzare questa funzione è necessaria una licenza Enterprise Edition . Questa funzionalità è disponibile anche in Pangolin Cloud.", - "ossEnterpriseEditionRequired": "L' Enterprise Edition è necessaria per utilizzare questa funzione. Questa funzionalità è disponibile anche in Pangolin Cloud.", + "licenseRequiredToUse": "Per utilizzare questa funzione è necessaria una licenza Enterprise Edition o Pangolin Cloud . Prenota una demo o una prova POC.", + "ossEnterpriseEditionRequired": "L' Enterprise Edition è necessaria per utilizzare questa funzione. Questa funzione è disponibile anche in Pangolin Cloud. Prenota una demo o una prova POC.", "certResolver": "Risolutore Di Certificato", "certResolverDescription": "Selezionare il risolutore di certificati da usare per questa risorsa.", "selectCertResolver": "Seleziona Risolutore Di Certificato", @@ -2673,5 +2681,6 @@ "approvalsEmptyStateStep2Title": "Abilita Approvazioni Dispositivo", "approvalsEmptyStateStep2Description": "Modifica un ruolo e abilita l'opzione 'Richiedi l'approvazione del dispositivo'. Gli utenti con questo ruolo avranno bisogno dell'approvazione dell'amministratore per i nuovi dispositivi.", "approvalsEmptyStatePreviewDescription": "Anteprima: quando abilitato, le richieste di dispositivo in attesa appariranno qui per la revisione", - "approvalsEmptyStateButtonText": "Gestisci Ruoli" + "approvalsEmptyStateButtonText": "Gestisci Ruoli", + "domainErrorTitle": "Stiamo avendo problemi a verificare il tuo dominio" } diff --git a/messages/ko-KR.json b/messages/ko-KR.json index 038658108..59f464305 100644 --- a/messages/ko-KR.json +++ b/messages/ko-KR.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "완전한 도메인 이름을 사용해 RAW 또는 HTTPS로 프록시 요청을 수행합니다.", "resourceRaw": "원시 TCP/UDP 리소스", "resourceRawDescription": "포트 번호를 사용하여 RAW TCP/UDP로 요청을 프록시합니다.", + "resourceRawDescriptionCloud": "포트 번호를 사용하여 원격 노드에 연결해야 합니다. 원격 노드에서 리소스를 사용하려면 사용자 지정 도메인을 사용하십시오.", "resourceCreate": "리소스 생성", "resourceCreateDescription": "아래 단계를 따라 새 리소스를 생성하세요.", "resourceSeeAll": "모든 리소스 보기", @@ -650,6 +651,7 @@ "resourcesErrorUpdate": "리소스를 전환하는 데 실패했습니다.", "resourcesErrorUpdateDescription": "리소스를 업데이트하는 동안 오류가 발생했습니다.", "access": "접속", + "accessControl": "액세스 제어", "shareLink": "{resource} 공유 링크", "resourceSelect": "리소스 선택", "shareLinks": "공유 링크", @@ -1038,7 +1040,6 @@ "pageNotFoundDescription": "앗! 찾고 있는 페이지가 존재하지 않습니다.", "overview": "개요", "home": "홈", - "accessControl": "액세스 제어", "settings": "설정", "usersAll": "모든 사용자", "license": "라이선스", @@ -1101,6 +1102,12 @@ "actionGetUser": "사용자 조회", "actionGetOrgUser": "조직 사용자 가져오기", "actionListOrgDomains": "조직 도메인 목록", + "actionGetDomain": "도메인 가져오기", + "actionCreateOrgDomain": "도메인 생성", + "actionUpdateOrgDomain": "도메인 업데이트", + "actionDeleteOrgDomain": "도메인 삭제", + "actionGetDNSRecords": "DNS 레코드 가져오기", + "actionRestartOrgDomain": "도메인 재시작", "actionCreateSite": "사이트 생성", "actionDeleteSite": "사이트 삭제", "actionGetSite": "사이트 가져오기", @@ -1419,6 +1426,7 @@ "domainPickerNamespace": "이름 공간: {namespace}", "domainPickerShowMore": "더보기", "regionSelectorTitle": "지역 선택", + "domainPickerRemoteExitNodeWarning": "제공된 도메인은 원격 종료 노드에 연결된 사이트에서 지원되지 않습니다. 원격 노드에서 리소스를 사용하려면 사용자 지정 도메인을 사용하십시오.", "regionSelectorInfo": "지역을 선택하면 위치에 따라 더 나은 성능이 제공됩니다. 서버와 같은 지역에 있을 필요는 없습니다.", "regionSelectorPlaceholder": "지역 선택", "regionSelectorComingSoon": "곧 출시 예정", @@ -1669,10 +1677,10 @@ "sshSudoModeCommandsDescription": "사용자는 sudo로 지정된 명령만 실행할 수 있습니다.", "sshSudo": "Sudo 허용", "sshSudoCommands": "Sudo 명령", - "sshSudoCommandsDescription": "사용자가 sudo로 실행할 수 있도록 허용된 명령 목록입니다.", + "sshSudoCommandsDescription": "사용자가 sudo로 실행할 수 있는 명령어의 쉼표로 구분된 목록입니다.", "sshCreateHomeDir": "홈 디렉터리 생성", "sshUnixGroups": "유닉스 그룹", - "sshUnixGroupsDescription": "대상 호스트에서 사용자를 추가할 유닉스 그룹입니다.", + "sshUnixGroupsDescription": "대상 호스트에서 사용자에게 추가할 유닉스 그룹의 쉼표로 구분된 목록입니다.", "retryAttempts": "재시도 횟수", "expectedResponseCodes": "예상 응답 코드", "expectedResponseCodesDescription": "정상 상태를 나타내는 HTTP 상태 코드입니다. 비워 두면 200-300이 정상으로 간주됩니다.", @@ -2335,8 +2343,8 @@ "logRetentionEndOfFollowingYear": "다음 연도 말", "actionLogsDescription": "이 조직에서 수행된 작업의 기록을 봅니다", "accessLogsDescription": "이 조직의 자원에 대한 접근 인증 요청을 확인합니다", - "licenseRequiredToUse": "이 기능을 사용하려면 엔터프라이즈 에디션 라이선스가 필요합니다. 이 기능은 판골린 클라우드에서도 사용할 수 있습니다.", - "ossEnterpriseEditionRequired": "이 기능을 사용하려면 엔터프라이즈 에디션이 필요합니다. 이 기능은 판골린 클라우드에서도 사용할 수 있습니다.", + "licenseRequiredToUse": "이 기능을 사용하려면 엔터프라이즈 에디션 라이선스가 필요합니다. 이 기능은 판골린 클라우드에서도 사용할 수 있습니다. 데모 또는 POC 체험을 예약하세요.", + "ossEnterpriseEditionRequired": "이 기능을 사용하려면 엔터프라이즈 에디션이(가) 필요합니다. 이 기능은 판골린 클라우드에서도 사용할 수 있습니다. 데모 또는 POC 체험을 예약하세요.", "certResolver": "인증서 해결사", "certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.", "selectCertResolver": "인증서 해결사 선택", @@ -2673,5 +2681,6 @@ "approvalsEmptyStateStep2Title": "장치 승인 활성화", "approvalsEmptyStateStep2Description": "역할을 편집하고 '장치 승인 요구' 옵션을 활성화하세요. 이 역할을 가진 사용자는 새 장치에 대해 관리자의 승인이 필요합니다.", "approvalsEmptyStatePreviewDescription": "미리 보기: 활성화된 경우, 승인 대기 중인 장치 요청이 검토용으로 여기에 표시됩니다.", - "approvalsEmptyStateButtonText": "역할 관리" + "approvalsEmptyStateButtonText": "역할 관리", + "domainErrorTitle": "도메인 확인에 문제가 발생했습니다." } diff --git a/messages/nb-NO.json b/messages/nb-NO.json index 503f42659..e8a9fa9a3 100644 --- a/messages/nb-NO.json +++ b/messages/nb-NO.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "Proxy forespørsler over HTTPS ved å bruke et fullstendig kvalifisert domenenavn.", "resourceRaw": "Rå TCP/UDP-ressurs", "resourceRawDescription": "Proxy forespørsler over rå TCP/UDP ved å bruke et portnummer.", + "resourceRawDescriptionCloud": "Proxy forespørsler om rå TCP/UDP ved hjelp av et portnummer. Krever sider for å koble til en ekstern node.", "resourceCreate": "Opprett ressurs", "resourceCreateDescription": "Følg trinnene nedenfor for å opprette en ny ressurs", "resourceSeeAll": "Se alle ressurser", @@ -650,6 +651,7 @@ "resourcesErrorUpdate": "Feilet å slå av/på ressurs", "resourcesErrorUpdateDescription": "En feil oppstod under oppdatering av ressursen", "access": "Tilgang", + "accessControl": "Tilgangskontroll", "shareLink": "{resource} Del Lenke", "resourceSelect": "Velg ressurs", "shareLinks": "Del lenker", @@ -1038,7 +1040,6 @@ "pageNotFoundDescription": "Oops! Siden du leter etter finnes ikke.", "overview": "Oversikt", "home": "Hjem", - "accessControl": "Tilgangskontroll", "settings": "Innstillinger", "usersAll": "Alle brukere", "license": "Lisens", @@ -1101,6 +1102,12 @@ "actionGetUser": "Hent bruker", "actionGetOrgUser": "Hent organisasjonsbruker", "actionListOrgDomains": "List opp organisasjonsdomener", + "actionGetDomain": "Få Domene", + "actionCreateOrgDomain": "Opprett domene", + "actionUpdateOrgDomain": "Oppdater domene", + "actionDeleteOrgDomain": "Slett domene", + "actionGetDNSRecords": "Hent DNS-oppføringer", + "actionRestartOrgDomain": "Omstart Domene", "actionCreateSite": "Opprett område", "actionDeleteSite": "Slett område", "actionGetSite": "Hent område", @@ -1419,6 +1426,7 @@ "domainPickerNamespace": "Navnerom: {namespace}", "domainPickerShowMore": "Vis mer", "regionSelectorTitle": "Velg Region", + "domainPickerRemoteExitNodeWarning": "Tilbudte domener støttes ikke når sider kobles til eksterne avkjøringsnoder. For ressurser som skal være tilgjengelige på eksterne noder, brukes et egendefinert domene i stedet.", "regionSelectorInfo": "Å velge en region hjelper oss med å gi bedre ytelse for din lokasjon. Du trenger ikke være i samme region som serveren.", "regionSelectorPlaceholder": "Velg en region", "regionSelectorComingSoon": "Kommer snart", @@ -1669,10 +1677,10 @@ "sshSudoModeCommandsDescription": "Brukeren kan bare kjøre de angitte kommandoene med sudo.", "sshSudo": "Tillat sudo", "sshSudoCommands": "Sudo kommandoer", - "sshSudoCommandsDescription": "Liste av kommandoer brukeren har lov til å kjøre med sudo.", + "sshSudoCommandsDescription": "Kommaseparert liste med kommandoer brukeren kan kjøre med sudo.", "sshCreateHomeDir": "Opprett hjemmappe", "sshUnixGroups": "Unix grupper", - "sshUnixGroupsDescription": "Unix grupper for å legge til brukeren til målverten.", + "sshUnixGroupsDescription": "Kommaseparerte Unix grupper for å legge brukeren til på mål-verten.", "retryAttempts": "Forsøk på nytt", "expectedResponseCodes": "Forventede svarkoder", "expectedResponseCodesDescription": "HTTP-statuskode som indikerer sunn status. Hvis den blir stående tom, regnes 200-300 som sunn.", @@ -2335,8 +2343,8 @@ "logRetentionEndOfFollowingYear": "Slutt på neste år", "actionLogsDescription": "Vis historikk for handlinger som er utført i denne organisasjonen", "accessLogsDescription": "Vis autoriseringsforespørsler for ressurser i denne organisasjonen", - "licenseRequiredToUse": "En Enterprise Edition lisens er påkrevd for å bruke denne funksjonen. Denne funksjonen er også tilgjengelig i Pangolin Cloud.", - "ossEnterpriseEditionRequired": "Enterprise Edition er nødvendig for å bruke denne funksjonen. Denne funksjonen er også tilgjengelig i Pangolin Cloud.", + "licenseRequiredToUse": "En Enterprise Edition lisens eller Pangolin Cloud er påkrevd for å bruke denne funksjonen. Bestill en demo eller POC prøveversjon.", + "ossEnterpriseEditionRequired": "Enterprise Edition er nødvendig for å bruke denne funksjonen. Denne funksjonen er også tilgjengelig i Pangolin Cloud. Bestill en demo eller POC studie.", "certResolver": "Sertifikat løser", "certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.", "selectCertResolver": "Velg sertifikatløser", @@ -2673,5 +2681,6 @@ "approvalsEmptyStateStep2Title": "Aktiver enhetsgodkjenninger", "approvalsEmptyStateStep2Description": "Rediger en rolle og aktiver alternativet 'Kreve enhetsgodkjenninger'. Brukere med denne rollen vil trenge administratorgodkjenning for nye enheter.", "approvalsEmptyStatePreviewDescription": "Forhåndsvisning: Når aktivert, ventende enhets forespørsler vil vises her for vurdering", - "approvalsEmptyStateButtonText": "Administrer Roller" + "approvalsEmptyStateButtonText": "Administrer Roller", + "domainErrorTitle": "Vi har problemer med å verifisere domenet ditt" } diff --git a/messages/nl-NL.json b/messages/nl-NL.json index caa2ed17a..32580cc45 100644 --- a/messages/nl-NL.json +++ b/messages/nl-NL.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "Proxyverzoeken via HTTPS met een volledig gekwalificeerde domeinnaam.", "resourceRaw": "TCP/UDP bron", "resourceRawDescription": "Proxyverzoeken via ruwe TCP/UDP met een poortnummer.", + "resourceRawDescriptionCloud": "Proxy verzoeken over rauwe TCP/UDP met behulp van een poortnummer. Vereist sites om verbinding te maken met een remote node.", "resourceCreate": "Bron maken", "resourceCreateDescription": "Volg de onderstaande stappen om een nieuwe bron te maken", "resourceSeeAll": "Alle bronnen bekijken", @@ -650,6 +651,7 @@ "resourcesErrorUpdate": "Bron wisselen mislukt", "resourcesErrorUpdateDescription": "Er is een fout opgetreden tijdens het bijwerken van het document", "access": "Toegangsrechten", + "accessControl": "Toegangs controle", "shareLink": "{resource} Share link", "resourceSelect": "Selecteer resource", "shareLinks": "Links delen", @@ -1038,7 +1040,6 @@ "pageNotFoundDescription": "Oeps! De pagina die je zoekt bestaat niet.", "overview": "Overzicht.", "home": "Startpagina", - "accessControl": "Toegangs controle", "settings": "Instellingen", "usersAll": "Alle gebruikers", "license": "Licentie", @@ -1101,6 +1102,12 @@ "actionGetUser": "Gebruiker ophalen", "actionGetOrgUser": "Krijg organisatie-gebruiker", "actionListOrgDomains": "Lijst organisatie domeinen", + "actionGetDomain": "Domein verkrijgen", + "actionCreateOrgDomain": "Domein aanmaken", + "actionUpdateOrgDomain": "Domein bijwerken", + "actionDeleteOrgDomain": "Domein verwijderen", + "actionGetDNSRecords": "Krijg DNS Records", + "actionRestartOrgDomain": "Domein opnieuw starten", "actionCreateSite": "Site aanmaken", "actionDeleteSite": "Site verwijderen", "actionGetSite": "Site ophalen", @@ -1419,6 +1426,7 @@ "domainPickerNamespace": "Naamruimte: {namespace}", "domainPickerShowMore": "Meer weergeven", "regionSelectorTitle": "Selecteer Regio", + "domainPickerRemoteExitNodeWarning": "Opgegeven domeinen worden niet ondersteund wanneer websites verbinding maken met externe sluitnodes. Gebruik in plaats daarvan een aangepast domein. Om bronnen beschikbaar te maken op externe nodes.", "regionSelectorInfo": "Het selecteren van een regio helpt ons om betere prestaties te leveren voor uw locatie. U hoeft niet in dezelfde regio als uw server te zijn.", "regionSelectorPlaceholder": "Kies een regio", "regionSelectorComingSoon": "Komt binnenkort", @@ -1669,10 +1677,10 @@ "sshSudoModeCommandsDescription": "Gebruiker kan alleen de opgegeven commando's uitvoeren met de sudo.", "sshSudo": "sudo toestaan", "sshSudoCommands": "Sudo Commando's", - "sshSudoCommandsDescription": "Lijst van commando's die de gebruiker mag uitvoeren met een sudo.", + "sshSudoCommandsDescription": "Komma's gescheiden lijst van commando's waar de gebruiker een sudo mee mag uitvoeren.", "sshCreateHomeDir": "Maak Home Directory", "sshUnixGroups": "Unix groepen", - "sshUnixGroupsDescription": "Unix groepen om de gebruiker toe te voegen aan de doel host.", + "sshUnixGroupsDescription": "Door komma's gescheiden Unix-groepen om de gebruiker toe te voegen aan de doelhost.", "retryAttempts": "Herhaal Pogingen", "expectedResponseCodes": "Verwachte Reactiecodes", "expectedResponseCodesDescription": "HTTP-statuscode die gezonde status aangeeft. Indien leeg wordt 200-300 als gezond beschouwd.", @@ -2335,8 +2343,8 @@ "logRetentionEndOfFollowingYear": "Einde van volgend jaar", "actionLogsDescription": "Bekijk een geschiedenis van acties die worden uitgevoerd in deze organisatie", "accessLogsDescription": "Toegangsverificatieverzoeken voor resources in deze organisatie bekijken", - "licenseRequiredToUse": "Een Enterprise Edition licentie is vereist om deze functie te gebruiken. Deze functie is ook beschikbaar in Pangolin Cloud.", - "ossEnterpriseEditionRequired": "De Enterprise Edition is vereist om deze functie te gebruiken. Deze functie is ook beschikbaar in Pangolin Cloud.", + "licenseRequiredToUse": "Een Enterprise Edition licentie of Pangolin Cloud is vereist om deze functie te gebruiken. Boek een demo of POC trial.", + "ossEnterpriseEditionRequired": "De Enterprise Edition is vereist om deze functie te gebruiken. Deze functie is ook beschikbaar in Pangolin Cloud. Boek een demo of POC trial.", "certResolver": "Certificaat Resolver", "certResolverDescription": "Selecteer de certificaat resolver die moet worden gebruikt voor deze resource.", "selectCertResolver": "Certificaat Resolver selecteren", @@ -2673,5 +2681,6 @@ "approvalsEmptyStateStep2Title": "Toestel goedkeuringen inschakelen", "approvalsEmptyStateStep2Description": "Bewerk een rol en schakel de optie 'Vereist Apparaat Goedkeuringen' in. Gebruikers met deze rol hebben admin goedkeuring nodig voor nieuwe apparaten.", "approvalsEmptyStatePreviewDescription": "Voorbeeld: Indien ingeschakeld, zullen in afwachting van apparaatverzoeken hier verschijnen om te beoordelen", - "approvalsEmptyStateButtonText": "Rollen beheren" + "approvalsEmptyStateButtonText": "Rollen beheren", + "domainErrorTitle": "We ondervinden problemen bij het controleren van uw domein" } diff --git a/messages/pl-PL.json b/messages/pl-PL.json index 6203f4cc2..ba0587b94 100644 --- a/messages/pl-PL.json +++ b/messages/pl-PL.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "Proxy zapytań przez HTTPS przy użyciu w pełni kwalifikowanej nazwy domeny.", "resourceRaw": "Surowy zasób TCP/UDP", "resourceRawDescription": "Proxy zapytań przez surowe TCP/UDP przy użyciu numeru portu.", + "resourceRawDescriptionCloud": "Żądania proxy nad surowym TCP/UDP przy użyciu numeru portu. Wymaga stron aby połączyć się ze zdalnym węzłem.", "resourceCreate": "Utwórz zasób", "resourceCreateDescription": "Wykonaj poniższe kroki, aby utworzyć nowy zasób", "resourceSeeAll": "Zobacz wszystkie zasoby", @@ -650,6 +651,7 @@ "resourcesErrorUpdate": "Nie udało się przełączyć zasobu", "resourcesErrorUpdateDescription": "Wystąpił błąd podczas aktualizacji zasobu", "access": "Dostęp", + "accessControl": "Kontrola dostępu", "shareLink": "Link udostępniania {resource}", "resourceSelect": "Wybierz zasób", "shareLinks": "Linki udostępniania", @@ -1038,7 +1040,6 @@ "pageNotFoundDescription": "Ups! Strona, której szukasz, nie istnieje.", "overview": "Przegląd", "home": "Strona główna", - "accessControl": "Kontrola dostępu", "settings": "Ustawienia", "usersAll": "Wszyscy użytkownicy", "license": "Licencja", @@ -1101,6 +1102,12 @@ "actionGetUser": "Pobierz użytkownika", "actionGetOrgUser": "Pobierz użytkownika organizacji", "actionListOrgDomains": "Lista domen organizacji", + "actionGetDomain": "Pobierz domenę", + "actionCreateOrgDomain": "Utwórz domenę", + "actionUpdateOrgDomain": "Aktualizuj domenę", + "actionDeleteOrgDomain": "Usuń domenę", + "actionGetDNSRecords": "Pobierz rekordy DNS", + "actionRestartOrgDomain": "Zrestartuj domenę", "actionCreateSite": "Utwórz witrynę", "actionDeleteSite": "Usuń witrynę", "actionGetSite": "Pobierz witrynę", @@ -1419,6 +1426,7 @@ "domainPickerNamespace": "Przestrzeń nazw: {namespace}", "domainPickerShowMore": "Pokaż więcej", "regionSelectorTitle": "Wybierz region", + "domainPickerRemoteExitNodeWarning": "Podane domeny nie są obsługiwane, gdy witryny łączą się ze zdalnymi węzłami wyjścia. Aby zasoby były dostępne w węzłach zdalnych, użyj domeny niestandardowej.", "regionSelectorInfo": "Wybór regionu pomaga nam zapewnić lepszą wydajność dla Twojej lokalizacji. Nie musisz być w tym samym regionie co Twój serwer.", "regionSelectorPlaceholder": "Wybierz region", "regionSelectorComingSoon": "Wkrótce dostępne", @@ -1669,10 +1677,10 @@ "sshSudoModeCommandsDescription": "Użytkownik może uruchamiać tylko określone polecenia z sudo.", "sshSudo": "Zezwól na sudo", "sshSudoCommands": "Komendy Sudo", - "sshSudoCommandsDescription": "Lista poleceń, które użytkownik może uruchamiać z sudo.", + "sshSudoCommandsDescription": "Lista poleceń oddzielonych przecinkami, które użytkownik może uruchamiać z sudo.", "sshCreateHomeDir": "Utwórz katalog domowy", "sshUnixGroups": "Grupy Unix", - "sshUnixGroupsDescription": "Grupy Unix do dodania użytkownika do docelowego hosta.", + "sshUnixGroupsDescription": "Oddzielone przecinkami grupy Unix, aby dodać użytkownika do docelowego hosta.", "retryAttempts": "Próby Ponowienia", "expectedResponseCodes": "Oczekiwane Kody Odpowiedzi", "expectedResponseCodesDescription": "Kod statusu HTTP, który wskazuje zdrowy status. Jeśli pozostanie pusty, uznaje się 200-300 za zdrowy.", @@ -2335,8 +2343,8 @@ "logRetentionEndOfFollowingYear": "Koniec następnego roku", "actionLogsDescription": "Zobacz historię działań wykonywanych w tej organizacji", "accessLogsDescription": "Wyświetl prośby o autoryzację dostępu do zasobów w tej organizacji", - "licenseRequiredToUse": "Do korzystania z tej funkcji wymagana jest licencja Enterprise Edition . Ta funkcja jest również dostępna w Pangolin Cloud.", - "ossEnterpriseEditionRequired": "Enterprise Edition jest wymagany do korzystania z tej funkcji. Ta funkcja jest również dostępna w Pangolin Cloud.", + "licenseRequiredToUse": "Do korzystania z tej funkcji wymagana jest licencja Enterprise Edition lub Pangolin Cloud . Zarezerwuj wersję demonstracyjną lub wersję próbną POC.", + "ossEnterpriseEditionRequired": "Enterprise Edition jest wymagany do korzystania z tej funkcji. Ta funkcja jest również dostępna w Pangolin Cloud. Zarezerwuj demo lub okres próbny POC.", "certResolver": "Rozwiązywanie certyfikatów", "certResolverDescription": "Wybierz resolver certyfikatów do użycia dla tego zasobu.", "selectCertResolver": "Wybierz Resolver certyfikatów", @@ -2673,5 +2681,6 @@ "approvalsEmptyStateStep2Title": "Włącz zatwierdzanie urządzenia", "approvalsEmptyStateStep2Description": "Edytuj rolę i włącz opcję \"Wymagaj zatwierdzenia urządzenia\". Użytkownicy z tą rolą będą potrzebowali zatwierdzenia administratora dla nowych urządzeń.", "approvalsEmptyStatePreviewDescription": "Podgląd: Gdy włączone, oczekujące prośby o sprawdzenie pojawią się tutaj", - "approvalsEmptyStateButtonText": "Zarządzaj rolami" + "approvalsEmptyStateButtonText": "Zarządzaj rolami", + "domainErrorTitle": "Mamy problem z weryfikacją Twojej domeny" } diff --git a/messages/pt-PT.json b/messages/pt-PT.json index b623b2b25..3ce98fff6 100644 --- a/messages/pt-PT.json +++ b/messages/pt-PT.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "Proxies requests sobre HTTPS usando um nome de domínio totalmente qualificado.", "resourceRaw": "Recurso TCP/UDP bruto", "resourceRawDescription": "Proxies solicitações sobre TCP/UDP bruto usando um número de porta.", + "resourceRawDescriptionCloud": "Proxy solicita por TCP/UDP bruto usando um número de porta. Requer que sites se conectem a um nó remoto.", "resourceCreate": "Criar Recurso", "resourceCreateDescription": "Siga os passos abaixo para criar um novo recurso", "resourceSeeAll": "Ver todos os recursos", @@ -650,6 +651,7 @@ "resourcesErrorUpdate": "Falha ao alternar recurso", "resourcesErrorUpdateDescription": "Ocorreu um erro ao atualizar o recurso", "access": "Acesso", + "accessControl": "Controle de Acesso", "shareLink": "Link de Compartilhamento {resource}", "resourceSelect": "Selecionar recurso", "shareLinks": "Links de Compartilhamento", @@ -1038,7 +1040,6 @@ "pageNotFoundDescription": "Ops! A página que você está procurando não existe.", "overview": "Visão Geral", "home": "Início", - "accessControl": "Controle de Acesso", "settings": "Configurações", "usersAll": "Todos os Utilizadores", "license": "Licença", @@ -1101,6 +1102,12 @@ "actionGetUser": "Obter Usuário", "actionGetOrgUser": "Obter Utilizador da Organização", "actionListOrgDomains": "Listar Domínios da Organização", + "actionGetDomain": "Obter domínio", + "actionCreateOrgDomain": "Criar domínio", + "actionUpdateOrgDomain": "Atualizar domínio", + "actionDeleteOrgDomain": "Excluir domínio", + "actionGetDNSRecords": "Obter registros de DNS", + "actionRestartOrgDomain": "Reiniciar domínio", "actionCreateSite": "Criar Site", "actionDeleteSite": "Eliminar Site", "actionGetSite": "Obter Site", @@ -1419,6 +1426,7 @@ "domainPickerNamespace": "Namespace: {namespace}", "domainPickerShowMore": "Mostrar Mais", "regionSelectorTitle": "Selecionar Região", + "domainPickerRemoteExitNodeWarning": "Domínios fornecidos não são suportados quando os sites se conectam a nós de saída remota. Para recursos disponíveis em nós remotos, use um domínio personalizado.", "regionSelectorInfo": "Selecionar uma região nos ajuda a fornecer melhor desempenho para sua localização. Você não precisa estar na mesma região que seu servidor.", "regionSelectorPlaceholder": "Escolher uma região", "regionSelectorComingSoon": "Em breve", @@ -1669,10 +1677,10 @@ "sshSudoModeCommandsDescription": "Usuário só pode executar os comandos especificados com sudo.", "sshSudo": "Permitir sudo", "sshSudoCommands": "Comandos Sudo", - "sshSudoCommandsDescription": "Lista de comandos com permissão de executar com o sudo.", + "sshSudoCommandsDescription": "Lista separada por vírgulas de comandos que o usuário pode executar com sudo.", "sshCreateHomeDir": "Criar Diretório Inicial", "sshUnixGroups": "Grupos Unix", - "sshUnixGroupsDescription": "Grupos Unix para adicionar o usuário no host de destino.", + "sshUnixGroupsDescription": "Grupos Unix separados por vírgulas para adicionar o usuário no host alvo.", "retryAttempts": "Tentativas de Repetição", "expectedResponseCodes": "Códigos de Resposta Esperados", "expectedResponseCodesDescription": "Código de status HTTP que indica estado saudável. Se deixado em branco, 200-300 é considerado saudável.", @@ -2335,8 +2343,8 @@ "logRetentionEndOfFollowingYear": "Fim do ano seguinte", "actionLogsDescription": "Visualizar histórico de ações realizadas nesta organização", "accessLogsDescription": "Ver solicitações de autenticação de recursos nesta organização", - "licenseRequiredToUse": "Uma licença Enterprise Edition é necessária para usar este recurso. Este recurso também está disponível no Pangolin Cloud.", - "ossEnterpriseEditionRequired": "O Enterprise Edition é necessário para usar este recurso. Este recurso também está disponível no Pangolin Cloud.", + "licenseRequiredToUse": "Uma licença Enterprise Edition ou Pangolin Cloud é necessária para usar este recurso. Reserve um teste de demonstração ou POC.", + "ossEnterpriseEditionRequired": "O Enterprise Edition é necessário para usar este recurso. Este recurso também está disponível no Pangolin Cloud. Reserve uma demonstração ou avaliação POC.", "certResolver": "Resolvedor de Certificado", "certResolverDescription": "Selecione o resolvedor de certificados para este recurso.", "selectCertResolver": "Selecionar solucionador de certificado", @@ -2673,5 +2681,6 @@ "approvalsEmptyStateStep2Title": "Habilitar Aprovações do Dispositivo", "approvalsEmptyStateStep2Description": "Editar uma função e habilitar a opção 'Exigir aprovação de dispositivos'. Usuários com essa função precisarão de aprovação de administrador para novos dispositivos.", "approvalsEmptyStatePreviewDescription": "Pré-visualização: Quando ativado, solicitações de dispositivo pendentes aparecerão aqui para revisão", - "approvalsEmptyStateButtonText": "Gerir Funções" + "approvalsEmptyStateButtonText": "Gerir Funções", + "domainErrorTitle": "Estamos tendo problemas ao verificar seu domínio" } diff --git a/messages/ru-RU.json b/messages/ru-RU.json index f4dd0ac39..12043d8a2 100644 --- a/messages/ru-RU.json +++ b/messages/ru-RU.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "Проксировать запросы через HTTPS с использованием полного доменного имени.", "resourceRaw": "Сырой TCP/UDP-ресурс", "resourceRawDescription": "Проксировать запросы по сырому TCP/UDP с использованием номера порта.", + "resourceRawDescriptionCloud": "Прокси запросы через необработанный TCP/UDP с использованием номера порта. Требуется подключение сайтов к удаленному узлу.", "resourceCreate": "Создание ресурса", "resourceCreateDescription": "Следуйте инструкциям ниже для создания нового ресурса", "resourceSeeAll": "Посмотреть все ресурсы", @@ -650,6 +651,7 @@ "resourcesErrorUpdate": "Не удалось переключить ресурс", "resourcesErrorUpdateDescription": "Произошла ошибка при обновлении ресурса", "access": "Доступ", + "accessControl": "Контроль доступа", "shareLink": "Общая ссылка {resource}", "resourceSelect": "Выберите ресурс", "shareLinks": "Общие ссылки", @@ -1038,7 +1040,6 @@ "pageNotFoundDescription": "Упс! Страница, которую вы ищете, не существует.", "overview": "Обзор", "home": "Главная", - "accessControl": "Контроль доступа", "settings": "Настройки", "usersAll": "Все пользователи", "license": "Лицензия", @@ -1101,6 +1102,12 @@ "actionGetUser": "Получить пользователя", "actionGetOrgUser": "Получить пользователя организации", "actionListOrgDomains": "Список доменов организации", + "actionGetDomain": "Получить домен", + "actionCreateOrgDomain": "Создать домен", + "actionUpdateOrgDomain": "Обновить домен", + "actionDeleteOrgDomain": "Удалить домен", + "actionGetDNSRecords": "Получить записи DNS", + "actionRestartOrgDomain": "Перезапустить домен", "actionCreateSite": "Создать сайт", "actionDeleteSite": "Удалить сайт", "actionGetSite": "Получить сайт", @@ -1419,6 +1426,7 @@ "domainPickerNamespace": "Пространство имен: {namespace}", "domainPickerShowMore": "Показать еще", "regionSelectorTitle": "Выберите регион", + "domainPickerRemoteExitNodeWarning": "Предоставленные домены не поддерживаются при подключении сайтов к удаленным узлам. Для доступа к ресурсам на удаленных узлах используйте пользовательский домен.", "regionSelectorInfo": "Выбор региона помогает нам обеспечить лучшее качество обслуживания для вашего расположения. Вам необязательно находиться в том же регионе, что и ваш сервер.", "regionSelectorPlaceholder": "Выбор региона", "regionSelectorComingSoon": "Скоро будет", @@ -1669,10 +1677,10 @@ "sshSudoModeCommandsDescription": "Пользователь может запускать только указанные команды с помощью sudo.", "sshSudo": "Разрешить sudo", "sshSudoCommands": "Sudo Команды", - "sshSudoCommandsDescription": "Список команд, которые пользователю разрешено запускать с помощью sudo.", + "sshSudoCommandsDescription": "Список команд, разделенных запятыми, которые пользователю разрешено запускать с помощью sudo.", "sshCreateHomeDir": "Создать домашний каталог", "sshUnixGroups": "Unix группы", - "sshUnixGroupsDescription": "Unix группы для добавления пользователя на целевой хост.", + "sshUnixGroupsDescription": "Группы Unix через запятую, чтобы добавить пользователя на целевой хост.", "retryAttempts": "Количество попыток повторного запроса", "expectedResponseCodes": "Ожидаемые коды ответов", "expectedResponseCodesDescription": "HTTP-код состояния, указывающий на здоровое состояние. Если оставить пустым, 200-300 считается здоровым.", @@ -2335,8 +2343,8 @@ "logRetentionEndOfFollowingYear": "Конец следующего года", "actionLogsDescription": "Просмотр истории действий, выполненных в этой организации", "accessLogsDescription": "Просмотр запросов авторизации доступа к ресурсам этой организации", - "licenseRequiredToUse": "Лицензия на Enterprise Edition требуется для использования этой функции. Эта функция также доступна в Pangolin Cloud.", - "ossEnterpriseEditionRequired": "Для использования этой функции требуется Enterprise Edition. Эта функция также доступна в Pangolin Cloud.", + "licenseRequiredToUse": "Требуется лицензия на Enterprise Edition или Pangolin Cloud для использования этой функции. Забронируйте демонстрацию или пробный POC.", + "ossEnterpriseEditionRequired": "Enterprise Edition требуется для использования этой функции. Эта функция также доступна в Pangolin Cloud. Забронируйте демонстрацию или пробный POC.", "certResolver": "Резольвер сертификата", "certResolverDescription": "Выберите резолвер сертификата, который будет использоваться для этого ресурса.", "selectCertResolver": "Выберите резолвер сертификата", @@ -2673,5 +2681,6 @@ "approvalsEmptyStateStep2Title": "Включить утверждения устройства", "approvalsEmptyStateStep2Description": "Редактировать роль и включить опцию 'Требовать утверждения устройств'. Пользователям с этой ролью потребуется подтверждение администратора для новых устройств.", "approvalsEmptyStatePreviewDescription": "Предпросмотр: Если включено, ожидающие запросы на устройство появятся здесь для проверки", - "approvalsEmptyStateButtonText": "Управление ролями" + "approvalsEmptyStateButtonText": "Управление ролями", + "domainErrorTitle": "У нас возникли проблемы с проверкой вашего домена" } diff --git a/messages/tr-TR.json b/messages/tr-TR.json index f853629d0..362f891fb 100644 --- a/messages/tr-TR.json +++ b/messages/tr-TR.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "Tam nitelikli bir etki alanı adı kullanarak HTTPS üzerinden proxy isteklerini yönlendirin.", "resourceRaw": "Ham TCP/UDP Kaynağı", "resourceRawDescription": "Port numarası kullanarak ham TCP/UDP üzerinden proxy isteklerini yönlendirin.", + "resourceRawDescriptionCloud": "Proxy isteklerini bir port numarası kullanarak ham TCP/UDP üzerinden yapın. Sitelerin uzak bir düğüme bağlanması gereklidir.", "resourceCreate": "Kaynak Oluştur", "resourceCreateDescription": "Yeni bir kaynak oluşturmak için aşağıdaki adımları izleyin", "resourceSeeAll": "Tüm Kaynakları Gör", @@ -650,6 +651,7 @@ "resourcesErrorUpdate": "Kaynak değiştirilemedi", "resourcesErrorUpdateDescription": "Kaynak güncellenirken bir hata oluştu", "access": "Erişim", + "accessControl": "Erişim Kontrolü", "shareLink": "{resource} Paylaşım Bağlantısı", "resourceSelect": "Kaynak seçin", "shareLinks": "Paylaşım Bağlantıları", @@ -1038,7 +1040,6 @@ "pageNotFoundDescription": "Oops! Aradığınız sayfa mevcut değil.", "overview": "Genel Bakış", "home": "Ana Sayfa", - "accessControl": "Erişim Kontrolü", "settings": "Ayarlar", "usersAll": "Tüm Kullanıcılar", "license": "Lisans", @@ -1101,6 +1102,12 @@ "actionGetUser": "Kullanıcıyı Getir", "actionGetOrgUser": "Kuruluş Kullanıcısını Al", "actionListOrgDomains": "Kuruluş Alan Adlarını Listele", + "actionGetDomain": "Alan Adını Al", + "actionCreateOrgDomain": "Alan Adı Oluştur", + "actionUpdateOrgDomain": "Alan Adını Güncelle", + "actionDeleteOrgDomain": "Alan Adını Sil", + "actionGetDNSRecords": "DNS Kayıtlarını Al", + "actionRestartOrgDomain": "Alanı Yeniden Başlat", "actionCreateSite": "Site Oluştur", "actionDeleteSite": "Siteyi Sil", "actionGetSite": "Siteyi Al", @@ -1419,6 +1426,7 @@ "domainPickerNamespace": "Ad Alanı: {namespace}", "domainPickerShowMore": "Daha Fazla Göster", "regionSelectorTitle": "Bölge Seç", + "domainPickerRemoteExitNodeWarning": "Belirtilen alan adları, siteler uzak çıkış düğümlerine bağlandığında desteklenmez. Kaynakların uzak düğümlerde kullanılabilir olması için özel bir alan adı kullanın.", "regionSelectorInfo": "Bir bölge seçmek, konumunuz için daha iyi performans sağlamamıza yardımcı olur. Sunucunuzla aynı bölgede olmanıza gerek yoktur.", "regionSelectorPlaceholder": "Bölge Seçin", "regionSelectorComingSoon": "Yakında Geliyor", @@ -1669,10 +1677,10 @@ "sshSudoModeCommandsDescription": "Kullanıcı sadece belirtilen komutları sudo ile çalıştırabilir.", "sshSudo": "Sudo'ya izin ver", "sshSudoCommands": "Sudo Komutları", - "sshSudoCommandsDescription": "Kullanıcının sudo ile çalıştırmasına izin verilen komutların listesi.", + "sshSudoCommandsDescription": "Kullanıcının sudo ile çalıştırmasına izin verilen komutların virgülle ayrılmış listesi.", "sshCreateHomeDir": "Ev Dizini Oluştur", "sshUnixGroups": "Unix Grupları", - "sshUnixGroupsDescription": "Hedef ana bilgisayarda kullanıcıya eklemek için Unix grupları.", + "sshUnixGroupsDescription": "Hedef konakta kullanıcıya eklenecek Unix gruplarının virgülle ayrılmış listesi.", "retryAttempts": "Tekrar Deneme Girişimleri", "expectedResponseCodes": "Beklenen Yanıt Kodları", "expectedResponseCodesDescription": "Sağlıklı durumu gösteren HTTP durum kodu. Boş bırakılırsa, 200-300 arası sağlıklı kabul edilir.", @@ -2335,8 +2343,8 @@ "logRetentionEndOfFollowingYear": "Bir sonraki yılın sonu", "actionLogsDescription": "Bu organizasyondaki eylemler geçmişini görüntüleyin", "accessLogsDescription": "Bu organizasyondaki kaynaklar için erişim kimlik doğrulama isteklerini görüntüleyin", - "licenseRequiredToUse": "Bu özelliği kullanmak için bir Enterprise Edition lisansı gereklidir. Bu özellik ayrıca Pangolin Cloud'da da mevcuttur.", - "ossEnterpriseEditionRequired": "Bu özelliği kullanmak için Enterprise Edition gereklidir. Bu özellik ayrıca Pangolin Cloud'da da mevcuttur.", + "licenseRequiredToUse": "Bu özelliği kullanmak için bir Enterprise Edition lisansı veya Pangolin Cloud gereklidir. Tanıtım veya POC denemesi ayarlayın.", + "ossEnterpriseEditionRequired": "Bu özelliği kullanmak için Enterprise Edition gereklidir. Bu özellik ayrıca Pangolin Cloud’da da mevcuttur. Tanıtım veya POC denemesi ayarlayın.", "certResolver": "Sertifika Çözücü", "certResolverDescription": "Bu kaynak için kullanılacak sertifika çözücüsünü seçin.", "selectCertResolver": "Sertifika Çözücü Seçin", @@ -2673,5 +2681,6 @@ "approvalsEmptyStateStep2Title": "Cihaz Onaylarını Etkinleştir", "approvalsEmptyStateStep2Description": "Bir rolü düzenleyin ve 'Cihaz Onaylarını Gerektir' seçeneğini etkinleştirin. Bu role sahip kullanıcıların yeni cihazlar için yönetici onayına ihtiyacı olacaktır.", "approvalsEmptyStatePreviewDescription": "Önizleme: Etkinleştirildiğinde, bekleyen cihaz talepleri incelenmek üzere burada görünecektir.", - "approvalsEmptyStateButtonText": "Rolleri Yönet" + "approvalsEmptyStateButtonText": "Rolleri Yönet", + "domainErrorTitle": "Alan adınızı doğrulamada sorun yaşıyoruz" } diff --git a/messages/zh-CN.json b/messages/zh-CN.json index 29fe80390..a7f2682fa 100644 --- a/messages/zh-CN.json +++ b/messages/zh-CN.json @@ -175,6 +175,7 @@ "resourceHTTPDescription": "通过使用完全限定的域名的HTTPS代理请求。", "resourceRaw": "TCP/UDP 资源", "resourceRawDescription": "通过使用端口号的原始TCP/UDP代理请求。", + "resourceRawDescriptionCloud": "正在使用端口号使用 TCP/UDP 代理请求。需要站点连接到远程节点。", "resourceCreate": "创建资源", "resourceCreateDescription": "按照下面的步骤创建新资源", "resourceSeeAll": "查看所有资源", @@ -650,6 +651,7 @@ "resourcesErrorUpdate": "切换资源失败", "resourcesErrorUpdateDescription": "更新资源时出错", "access": "访问权限", + "accessControl": "访问控制", "shareLink": "{resource} 的分享链接", "resourceSelect": "选择资源", "shareLinks": "分享链接", @@ -1038,7 +1040,6 @@ "pageNotFoundDescription": "哎呀!您正在查找的页面不存在。", "overview": "概览", "home": "首页", - "accessControl": "访问控制", "settings": "设置", "usersAll": "所有用户", "license": "许可协议", @@ -1101,6 +1102,12 @@ "actionGetUser": "获取用户", "actionGetOrgUser": "获取组织用户", "actionListOrgDomains": "列出组织域", + "actionGetDomain": "获取域", + "actionCreateOrgDomain": "创建域", + "actionUpdateOrgDomain": "更新域", + "actionDeleteOrgDomain": "删除域", + "actionGetDNSRecords": "获取 DNS 记录", + "actionRestartOrgDomain": "重新启动域", "actionCreateSite": "创建站点", "actionDeleteSite": "删除站点", "actionGetSite": "获取站点", @@ -1419,6 +1426,7 @@ "domainPickerNamespace": "命名空间:{namespace}", "domainPickerShowMore": "显示更多", "regionSelectorTitle": "选择区域", + "domainPickerRemoteExitNodeWarning": "当站点连接到远程退出节点时不支持所提供的域。为了资源可在远程节点上使用,请使用自定义域名。", "regionSelectorInfo": "选择区域以帮助提升您所在地的性能。您不必与服务器在相同的区域。", "regionSelectorPlaceholder": "选择一个区域", "regionSelectorComingSoon": "即将推出", @@ -1669,10 +1677,10 @@ "sshSudoModeCommandsDescription": "用户只能用 sudo 运行指定的命令。", "sshSudo": "允许Sudo", "sshSudoCommands": "Sudo 命令", - "sshSudoCommandsDescription": "允许用户使用 sudo 运行的命令列表。", + "sshSudoCommandsDescription": "逗号分隔的用户允许使用 sudo 运行的命令列表。", "sshCreateHomeDir": "创建主目录", "sshUnixGroups": "Unix 组", - "sshUnixGroupsDescription": "将用户添加到目标主机的Unix组。", + "sshUnixGroupsDescription": "用逗号分隔了Unix组,将用户添加到目标主机上。", "retryAttempts": "重试次数", "expectedResponseCodes": "期望响应代码", "expectedResponseCodesDescription": "HTTP 状态码表示健康状态。如留空,200-300 被视为健康。", @@ -2335,8 +2343,8 @@ "logRetentionEndOfFollowingYear": "下一年结束", "actionLogsDescription": "查看此机构执行的操作历史", "accessLogsDescription": "查看此机构资源的访问认证请求", - "licenseRequiredToUse": "需要 Enterprise Edition 许可才能使用此功能。此功能也可在 Pangolin Cloud 中使用。", - "ossEnterpriseEditionRequired": "Enterprise Edition 需要使用此功能。此功能也可在 Pangolin Cloud 中使用。", + "licenseRequiredToUse": "使用此功能需要企业版许可证或Pangolin Cloud预约演示或POC试用。", + "ossEnterpriseEditionRequired": "需要 Enterprise Edition 才能使用此功能。 此功能也可在 Pangolin Cloud上获取。 预订演示或POC 试用。", "certResolver": "证书解决器", "certResolverDescription": "选择用于此资源的证书解析器。", "selectCertResolver": "选择证书解析", @@ -2673,5 +2681,6 @@ "approvalsEmptyStateStep2Title": "启用设备批准", "approvalsEmptyStateStep2Description": "编辑角色并启用“需要设备审批”选项。具有此角色的用户需要管理员批准新设备。", "approvalsEmptyStatePreviewDescription": "预览:如果启用,待处理设备请求将出现在这里供审核", - "approvalsEmptyStateButtonText": "管理角色" + "approvalsEmptyStateButtonText": "管理角色", + "domainErrorTitle": "我们在验证您的域名时遇到了问题" } diff --git a/package-lock.json b/package-lock.json index dabdcd33d..7b63f1691 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,8 @@ "version": "0.0.0", "license": "SEE LICENSE IN LICENSE AND README.md", "dependencies": { - "@asteasolutions/zod-to-openapi": "8.4.0", - "@aws-sdk/client-s3": "3.989.0", + "@asteasolutions/zod-to-openapi": "8.4.1", + "@aws-sdk/client-s3": "3.1011.0", "@faker-js/faker": "10.3.0", "@headlessui/react": "2.2.9", "@hookform/resolvers": "5.2.2", @@ -36,11 +36,11 @@ "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-tooltip": "1.2.8", - "@react-email/components": "1.0.7", + "@react-email/components": "1.0.8", "@react-email/render": "2.0.4", - "@react-email/tailwind": "2.0.4", - "@simplewebauthn/browser": "13.2.2", - "@simplewebauthn/server": "13.2.2", + "@react-email/tailwind": "2.0.5", + "@simplewebauthn/browser": "13.3.0", + "@simplewebauthn/server": "13.3.0", "@tailwindcss/forms": "0.5.11", "@tanstack/react-query": "5.90.21", "@tanstack/react-table": "8.21.3", @@ -57,42 +57,42 @@ "d3": "7.9.0", "drizzle-orm": "0.45.1", "express": "5.2.1", - "express-rate-limit": "8.2.1", - "glob": "13.0.3", + "express-rate-limit": "8.3.0", + "glob": "13.0.6", "helmet": "8.1.0", "http-errors": "2.0.1", "input-otp": "1.4.2", - "ioredis": "5.9.3", + "ioredis": "5.10.0", "jmespath": "0.16.0", "js-yaml": "4.1.1", "jsonwebtoken": "9.0.3", - "lucide-react": "0.563.0", + "lucide-react": "0.577.0", "maxmind": "5.0.5", "moment": "2.30.1", "next": "15.5.12", - "next-intl": "4.8.2", + "next-intl": "4.8.3", "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", "nodemailer": "8.0.1", "oslo": "1.2.1", - "pg": "8.18.0", - "posthog-node": "5.24.15", + "pg": "8.20.0", + "posthog-node": "5.28.0", "qrcode.react": "4.2.0", "react": "19.2.4", - "react-day-picker": "9.13.2", + "react-day-picker": "9.14.0", "react-dom": "19.2.4", "react-easy-sort": "1.8.0", - "react-hook-form": "7.71.1", - "react-icons": "5.5.0", + "react-hook-form": "7.71.2", + "react-icons": "5.6.0", "recharts": "2.15.4", - "reodotdev": "1.0.0", + "reodotdev": "1.1.0", "resend": "6.9.2", "semver": "7.7.4", "sshpk": "^1.18.0", - "stripe": "20.3.1", + "stripe": "20.4.1", "swagger-ui-express": "5.0.1", - "tailwind-merge": "3.4.0", + "tailwind-merge": "3.5.0", "topojson-client": "3.1.0", "tw-animate-css": "1.4.0", "use-debounce": "^10.1.0", @@ -108,10 +108,10 @@ "zod-validation-error": "5.0.0" }, "devDependencies": { - "@dotenvx/dotenvx": "1.52.0", + "@dotenvx/dotenvx": "1.54.1", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@react-email/preview-server": "5.2.8", - "@tailwindcss/postcss": "4.1.18", + "@react-email/preview-server": "5.2.10", + "@tailwindcss/postcss": "4.2.1", "@tanstack/react-query-devtools": "5.91.3", "@types/better-sqlite3": "7.6.13", "@types/cookie-parser": "1.4.10", @@ -123,10 +123,10 @@ "@types/jmespath": "0.15.2", "@types/js-yaml": "4.0.9", "@types/jsonwebtoken": "9.0.10", - "@types/node": "25.2.3", - "@types/nodemailer": "7.0.9", + "@types/node": "25.3.5", + "@types/nodemailer": "7.0.11", "@types/nprogress": "0.2.3", - "@types/pg": "8.16.0", + "@types/pg": "8.18.0", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@types/semver": "7.7.1", @@ -136,19 +136,19 @@ "@types/ws": "8.18.1", "@types/yargs": "17.0.35", "babel-plugin-react-compiler": "1.0.0", - "drizzle-kit": "0.31.9", + "drizzle-kit": "0.31.10", "esbuild": "0.27.3", "esbuild-node-externals": "1.20.1", - "eslint": "9.39.2", - "eslint-config-next": "16.1.6", - "postcss": "8.5.6", + "eslint": "10.0.3", + "eslint-config-next": "16.1.7", + "postcss": "8.5.8", "prettier": "3.8.1", - "react-email": "5.2.8", - "tailwindcss": "4.1.18", + "react-email": "5.2.10", + "tailwindcss": "4.2.1", "tsc-alias": "1.8.16", "tsx": "4.21.0", "typescript": "5.9.3", - "typescript-eslint": "8.55.0" + "typescript-eslint": "8.56.1" } }, "node_modules/@alloc/quick-lru": { @@ -164,24 +164,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@asteasolutions/zod-to-openapi": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.4.0.tgz", - "integrity": "sha512-Ckp971tmTw4pnv+o7iK85ldBHBKk6gxMaoNyLn3c2Th/fKoTG8G3jdYuOanpdGqwlDB0z01FOjry2d32lfTqrA==", + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/@asteasolutions/zod-to-openapi/-/zod-to-openapi-8.4.1.tgz", + "integrity": "sha512-WmJUsFINbnWxGvHSd16aOjgKf+5GsfdxruO2YDLcgplsidakCauik1lhlk83YDH06265Yd1XtUyF24o09uygpw==", "license": "MIT", "dependencies": { "openapi3-ts": "^4.1.2" @@ -208,6 +194,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", @@ -270,6 +257,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", @@ -284,6 +272,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -295,6 +284,7 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" @@ -307,6 +297,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" @@ -319,6 +310,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", @@ -387,112 +379,65 @@ } }, "node_modules/@aws-sdk/client-s3": { - "version": "3.989.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.989.0.tgz", - "integrity": "sha512-ccz2miIetWAgrJYmKCpSnRjF8jew7DPstl54nufhfPMtM1MLxD2z55eSk1eJj3Umhu4CioNN1aY1ILT7fwlSiw==", + "version": "3.1011.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.1011.0.tgz", + "integrity": "sha512-jY7CGX+vfM/DSi4K8UwaZKoXnhqchmAbKFB1kIuHMfPPqW7l3jC/fUVDb95/njMsB2ymYOTusZEzoCTeUB/4qA==", + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha1-browser": "5.2.0", "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/credential-provider-node": "^3.972.8", - "@aws-sdk/middleware-bucket-endpoint": "^3.972.3", - "@aws-sdk/middleware-expect-continue": "^3.972.3", - "@aws-sdk/middleware-flexible-checksums": "^3.972.7", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-location-constraint": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-sdk-s3": "^3.972.9", - "@aws-sdk/middleware-ssec": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.9", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/signature-v4-multi-region": "3.989.0", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.989.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.7", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.23.0", - "@smithy/eventstream-serde-browser": "^4.2.8", - "@smithy/eventstream-serde-config-resolver": "^4.3.8", - "@smithy/eventstream-serde-node": "^4.2.8", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-blob-browser": "^4.2.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/hash-stream-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/md5-js": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.14", - "@smithy/middleware-retry": "^4.4.31", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.10", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.3", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.30", - "@smithy/util-defaults-mode-node": "^4.2.33", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-stream": "^4.5.12", - "@smithy/util-utf8": "^4.2.0", - "@smithy/util-waiter": "^4.2.8", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/client-sso": { - "version": "3.989.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.989.0.tgz", - "integrity": "sha512-3sC+J1ru5VFXLgt9KZmXto0M7mnV5RkS6FNGwRMK3XrojSjHso9DLOWjbnXhbNv4motH8vu53L1HK2VC1+Nj5w==", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.9", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.989.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.7", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.23.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.14", - "@smithy/middleware-retry": "^4.4.31", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.10", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.3", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.30", - "@smithy/util-defaults-mode-node": "^4.2.33", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-node": "^3.972.21", + "@aws-sdk/middleware-bucket-endpoint": "^3.972.8", + "@aws-sdk/middleware-expect-continue": "^3.972.8", + "@aws-sdk/middleware-flexible-checksums": "^3.974.0", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-location-constraint": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-sdk-s3": "^3.972.20", + "@aws-sdk/middleware-ssec": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/signature-v4-multi-region": "^3.996.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/eventstream-serde-browser": "^4.2.12", + "@smithy/eventstream-serde-config-resolver": "^4.3.12", + "@smithy/eventstream-serde-node": "^4.2.12", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-blob-browser": "^4.2.13", + "@smithy/hash-node": "^4.2.12", + "@smithy/hash-stream-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/md5-js": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-stream": "^4.5.19", + "@smithy/util-utf8": "^4.2.2", + "@smithy/util-waiter": "^4.2.13", "tslib": "^2.6.2" }, "engines": { @@ -500,22 +445,23 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.973.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.9.tgz", - "integrity": "sha512-cyUOfJSizn8da7XrBEFBf4UMI4A6JQNX6ZFcKtYmh/CrwfzsDcabv3k/z0bNwQ3pX5aeq5sg/8Bs/ASiL0bJaA==", + "version": "3.973.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.20.tgz", + "integrity": "sha512-i3GuX+lowD892F3IuJf8o6AbyDupMTdyTxQrCJGcn71ni5hTZ82L4nQhcdumxZ7XPJRJJVHS/CR3uYOIIs0PVA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/xml-builder": "^3.972.4", - "@smithy/core": "^3.23.0", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.11.3", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/xml-builder": "^3.972.11", + "@smithy/core": "^3.23.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -523,11 +469,12 @@ } }, "node_modules/@aws-sdk/crc64-nvme": { - "version": "3.972.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.0.tgz", - "integrity": "sha512-ThlLhTqX68jvoIVv+pryOdb5coP1cX1/MaTbB9xkGDCbWbsqQcLqzPxuSoW1DCnAAIacmXCWpzUNOB9pv+xXQw==", + "version": "3.972.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/crc64-nvme/-/crc64-nvme-3.972.5.tgz", + "integrity": "sha512-2VbTstbjKdT+yKi8m7b3a9CiVac+pL/IY2PHJwsaGkkHmuuqkJZIErPck1h6P3T9ghQMLSdMPyW6Qp7Di5swFg==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -535,14 +482,15 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.7.tgz", - "integrity": "sha512-r8kBtglvLjGxBT87l6Lqkh9fL8yJJ6O4CYQPjKlj3AkCuL4/4784x3rxxXWw9LTKXOo114VB6mjxAuy5pI7XIg==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.18.tgz", + "integrity": "sha512-X0B8AlQY507i5DwjLByeU2Af4ARsl9Vr84koDcXCbAkplmU+1xBFWxEPrWRAoh56waBne/yJqEloSwvRf4x6XA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -550,19 +498,20 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.9.tgz", - "integrity": "sha512-40caFblEg/TPrp9EpvyMxp4xlJ5TuTI+A8H6g8FhHn2hfH2PObFAPLF9d5AljK/G69E1YtTklkuQeAwPlV3w8Q==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.20.tgz", + "integrity": "sha512-ey9Lelj001+oOfrbKmS6R2CJAiXX7QKY4Vj9VJv6L2eE6/VjD8DocHIoYqztTm70xDLR4E1jYPTKfIui+eRNDA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/types": "^3.973.1", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.10", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.3", - "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.12", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.19", "tslib": "^2.6.2" }, "engines": { @@ -570,23 +519,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.7.tgz", - "integrity": "sha512-zeYKrMwM5bCkHFho/x3+1OL0vcZQ0OhTR7k35tLq74+GP5ieV3juHXTZfa2LVE0Bg75cHIIerpX0gomVOhzo/w==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.20.tgz", + "integrity": "sha512-5flXSnKHMloObNF+9N0cupKegnH1Z37cdVlpETVgx8/rAhCe+VNlkcZH3HDg2SDn9bI765S+rhNPXGDJJPfbtA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/credential-provider-env": "^3.972.7", - "@aws-sdk/credential-provider-http": "^3.972.9", - "@aws-sdk/credential-provider-login": "^3.972.7", - "@aws-sdk/credential-provider-process": "^3.972.7", - "@aws-sdk/credential-provider-sso": "^3.972.7", - "@aws-sdk/credential-provider-web-identity": "^3.972.7", - "@aws-sdk/nested-clients": "3.989.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-login": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -594,17 +544,18 @@ } }, "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.7.tgz", - "integrity": "sha512-Q103cLU6OjAllYjX7+V+PKQw654jjvZUkD+lbUUiFbqut6gR5zwl1DrelvJPM5hnzIty7BCaxaRB3KMuz3M/ug==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.20.tgz", + "integrity": "sha512-gEWo54nfqp2jABMu6HNsjVC4hDLpg9HC8IKSJnp0kqWtxIJYHTmiLSsIfI4ScQjxEwpB+jOOH8dOLax1+hy/Hw==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/nested-clients": "3.989.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -612,21 +563,22 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.8.tgz", - "integrity": "sha512-AaDVOT7iNJyLjc3j91VlucPZ4J8Bw+eu9sllRDugJqhHWYyR3Iyp2huBUW8A3+DfHoh70sxGkY92cThAicSzlQ==", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.21.tgz", + "integrity": "sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.7", - "@aws-sdk/credential-provider-http": "^3.972.9", - "@aws-sdk/credential-provider-ini": "^3.972.7", - "@aws-sdk/credential-provider-process": "^3.972.7", - "@aws-sdk/credential-provider-sso": "^3.972.7", - "@aws-sdk/credential-provider-web-identity": "^3.972.7", - "@aws-sdk/types": "^3.973.1", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/credential-provider-env": "^3.972.18", + "@aws-sdk/credential-provider-http": "^3.972.20", + "@aws-sdk/credential-provider-ini": "^3.972.20", + "@aws-sdk/credential-provider-process": "^3.972.18", + "@aws-sdk/credential-provider-sso": "^3.972.20", + "@aws-sdk/credential-provider-web-identity": "^3.972.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -634,15 +586,16 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.7.tgz", - "integrity": "sha512-hxMo1V3ujWWrQSONxQJAElnjredkRpB6p8SDjnvRq70IwYY38R/CZSys0IbhRPxdgWZ5j12yDRk2OXhxw4Gj3g==", + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.18.tgz", + "integrity": "sha512-Tpl7SRaPoOLT32jbTWchPsn52hYYgJ0kpiFgnwk8pxTANQdUymVSZkzFvv1+oOgZm1CrbQUP9MBeoMZ9IzLZjA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -650,17 +603,18 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.7.tgz", - "integrity": "sha512-ZGKBOHEj8Ap15jhG2XMncQmKLTqA++2DVU2eZfLu3T/pkwDyhCp5eZv5c/acFxbZcA/6mtxke+vzO/n+aeHs4A==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.20.tgz", + "integrity": "sha512-p+R+PYR5Z7Gjqf/6pvbCnzEHcqPCpLzR7Yf127HjJ6EAb4hUcD+qsNRnuww1sB/RmSeCLxyay8FMyqREw4p1RA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.989.0", - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/token-providers": "3.989.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/token-providers": "3.1009.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -668,16 +622,17 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.7.tgz", - "integrity": "sha512-AbYupBIoSJoVMlbMqBhNvPhqj+CdGtzW7Uk4ZIMBm2br18pc3rkG1VaKVFV85H87QCvLHEnni1idJjaX1wOmIw==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.20.tgz", + "integrity": "sha512-rWCmh8o7QY4CsUj63qopzMzkDq/yPpkrpb+CnjBEFSOg/02T/we7sSTVg4QsDiVS9uwZ8VyONhq98qt+pIh3KA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/nested-clients": "3.989.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -685,17 +640,17 @@ } }, "node_modules/@aws-sdk/middleware-bucket-endpoint": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.3.tgz", - "integrity": "sha512-fmbgWYirF67YF1GfD7cg5N6HHQ96EyRNx/rDIrTF277/zTWVuPI2qS/ZHgofwR1NZPe/NWvoppflQY01LrbVLg==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.972.8.tgz", + "integrity": "sha512-WR525Rr2QJSETa9a050isktyWi/4yIGcmY3BQ1kpHqb0LqUglQHCS8R27dTJxxWNZvQ0RVGtEZjTCbZJpyF3Aw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-arn-parser": "^3.972.2", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-config-provider": "^4.2.0", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -703,14 +658,14 @@ } }, "node_modules/@aws-sdk/middleware-expect-continue": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.3.tgz", - "integrity": "sha512-4msC33RZsXQpUKR5QR4HnvBSNCPLGHmB55oDiROqqgyOc+TOfVu2xgi5goA7ms6MdZLeEh2905UfWMnMMF4mRg==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.972.8.tgz", + "integrity": "sha512-5DTBTiotEES1e2jOHAq//zyzCjeMB78lEHd35u15qnrid4Nxm7diqIf9fQQ3Ov0ChH1V3Vvt13thOnrACmfGVQ==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -718,23 +673,24 @@ } }, "node_modules/@aws-sdk/middleware-flexible-checksums": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.972.7.tgz", - "integrity": "sha512-YU/5rpz8k2mwFGi2M0px9ChOQZY7Bbow5knB2WLRVPqDM/cG8T5zj55UaWS1qcaFpE7vCX9a9/kvYBlKGcD+KA==", + "version": "3.974.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.974.0.tgz", + "integrity": "sha512-BmdDjqvnuYaC4SY7ypHLXfCSsGYGUZkjCLSZyUAAYn1YT28vbNMJNDwhlfkvvE+hQHG5RJDlEmYuvBxcB9jX1g==", + "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", "@aws-crypto/crc32c": "5.2.0", "@aws-crypto/util": "5.2.0", - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/crc64-nvme": "3.972.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.12", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/crc64-nvme": "^3.972.5", + "@aws-sdk/types": "^3.973.6", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.19", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -742,13 +698,14 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.3.tgz", - "integrity": "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.8.tgz", + "integrity": "sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -756,13 +713,13 @@ } }, "node_modules/@aws-sdk/middleware-location-constraint": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.3.tgz", - "integrity": "sha512-nIg64CVrsXp67vbK0U1/Is8rik3huS3QkRHn2DRDx4NldrEFMgdkZGI/+cZMKD9k4YOS110Dfu21KZLHrFA/1g==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.972.8.tgz", + "integrity": "sha512-KaUoFuoFPziIa98DSQsTPeke1gvGXlc5ZGMhy+b+nLxZ4A7jmJgLzjEF95l8aOQN2T/qlPP3MrAyELm8ExXucw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -770,12 +727,13 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.3.tgz", - "integrity": "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.8.tgz", + "integrity": "sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -783,14 +741,15 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.3.tgz", - "integrity": "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.8.tgz", + "integrity": "sha512-BnnvYs2ZEpdlmZ2PNlV2ZyQ8j8AEkMTjN79y/YA475ER1ByFYrkVR85qmhni8oeTaJcDqbx364wDpitDAA/wCA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", + "@aws-sdk/types": "^3.973.6", "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -798,23 +757,24 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.9.tgz", - "integrity": "sha512-F4Ak2HM7te/o3izFTqg/jUTBLjavpaJ5iynKM6aLMwNddXbwAZQ1VbIG8RFUHBo7fBHj2eeN2FNLtIFT4ejWYQ==", + "version": "3.972.20", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.972.20.tgz", + "integrity": "sha512-yhva/xL5H4tWQgsBjwV+RRD0ByCzg0TcByDCLp3GXdn/wlyRNfy8zsswDtCvr1WSKQkSQYlyEzPuWkJG0f5HvQ==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-arn-parser": "^3.972.2", - "@smithy/core": "^3.23.0", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/signature-v4": "^5.3.8", - "@smithy/smithy-client": "^4.11.3", - "@smithy/types": "^4.12.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.12", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-arn-parser": "^3.972.3", + "@smithy/core": "^3.23.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.19", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -822,13 +782,13 @@ } }, "node_modules/@aws-sdk/middleware-ssec": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.3.tgz", - "integrity": "sha512-dU6kDuULN3o3jEHcjm0c4zWJlY1zWVkjG9NPe9qxYLLpcbdj5kRYBS2DdWYD+1B9f910DezRuws7xDEqKkHQIg==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.972.8.tgz", + "integrity": "sha512-wqlK0yO/TxEC2UsY9wIlqeeutF6jjLe0f96Pbm40XscTo57nImUk9lBcw0dPgsm0sppFtAkSlDrfpK+pC30Wqw==", "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -836,16 +796,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.972.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.9.tgz", - "integrity": "sha512-1g1B7yf7KzessB0mKNiV9gAHEwbM662xgU+VE4LxyGe6kVGZ8LqYsngjhE+Stna09CJ7Pxkjr6Uq1OtbGwJJJg==", + "version": "3.972.21", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.21.tgz", + "integrity": "sha512-62XRl1GDYPpkt7cx1AX1SPy9wgNE9Iw/NPuurJu4lmhCWS7sGKO+kS53TQ8eRmIxy3skmvNInnk0ZbWrU5Dpyg==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.989.0", - "@smithy/core": "^3.23.0", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@smithy/core": "^3.23.11", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-retry": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -853,47 +815,48 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.989.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.989.0.tgz", - "integrity": "sha512-Dbk2HMPU3mb6RrSRzgf0WCaWSbgtZG258maCpuN2/ONcAQNpOTw99V5fU5CA1qVK6Vkm4Fwj2cnOnw7wbGVlOw==", + "version": "3.996.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.10.tgz", + "integrity": "sha512-SlDol5Z+C7Ivnc2rKGqiqfSUmUZzY1qHfVs9myt/nxVwswgfpjdKahyTzLTx802Zfq0NFRs7AejwKzzzl5Co2w==", + "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/middleware-host-header": "^3.972.3", - "@aws-sdk/middleware-logger": "^3.972.3", - "@aws-sdk/middleware-recursion-detection": "^3.972.3", - "@aws-sdk/middleware-user-agent": "^3.972.9", - "@aws-sdk/region-config-resolver": "^3.972.3", - "@aws-sdk/types": "^3.973.1", - "@aws-sdk/util-endpoints": "3.989.0", - "@aws-sdk/util-user-agent-browser": "^3.972.3", - "@aws-sdk/util-user-agent-node": "^3.972.7", - "@smithy/config-resolver": "^4.4.6", - "@smithy/core": "^3.23.0", - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/hash-node": "^4.2.8", - "@smithy/invalid-dependency": "^4.2.8", - "@smithy/middleware-content-length": "^4.2.8", - "@smithy/middleware-endpoint": "^4.4.14", - "@smithy/middleware-retry": "^4.4.31", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/node-http-handler": "^4.4.10", - "@smithy/protocol-http": "^5.3.8", - "@smithy/smithy-client": "^4.11.3", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-body-length-node": "^4.2.1", - "@smithy/util-defaults-mode-browser": "^4.3.30", - "@smithy/util-defaults-mode-node": "^4.2.33", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/util-utf8": "^4.2.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/middleware-host-header": "^3.972.8", + "@aws-sdk/middleware-logger": "^3.972.8", + "@aws-sdk/middleware-recursion-detection": "^3.972.8", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/region-config-resolver": "^3.972.8", + "@aws-sdk/types": "^3.973.6", + "@aws-sdk/util-endpoints": "^3.996.5", + "@aws-sdk/util-user-agent-browser": "^3.972.8", + "@aws-sdk/util-user-agent-node": "^3.973.7", + "@smithy/config-resolver": "^4.4.11", + "@smithy/core": "^3.23.11", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/hash-node": "^4.2.12", + "@smithy/invalid-dependency": "^4.2.12", + "@smithy/middleware-content-length": "^4.2.12", + "@smithy/middleware-endpoint": "^4.4.25", + "@smithy/middleware-retry": "^4.4.42", + "@smithy/middleware-serde": "^4.2.14", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/node-http-handler": "^4.4.16", + "@smithy/protocol-http": "^5.3.12", + "@smithy/smithy-client": "^4.12.5", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.41", + "@smithy/util-defaults-mode-node": "^4.2.44", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -901,14 +864,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.3.tgz", - "integrity": "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.8.tgz", + "integrity": "sha512-1eD4uhTDeambO/PNIDVG19A6+v4NdD7xzwLHDutHsUqz0B+i661MwQB2eYO4/crcCvCiQG4SRm1k81k54FEIvw==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/config-resolver": "^4.4.6", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/config-resolver": "^4.4.11", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -916,15 +880,16 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.989.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.989.0.tgz", - "integrity": "sha512-rVhR/BUZdnru7tLlxWD+uzoKB1LAs2L0pcoh6rYgIYuCtQflnsC6Ud0SpfqIsOapBSBKXdoW73IITFf+XFMdCQ==", + "version": "3.996.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.8.tgz", + "integrity": "sha512-n1qYFD+tbqZuyskVaxUE+t10AUz9g3qzDw3Tp6QZDKmqsjfDmZBd4GIk2EKJJNtcCBtE5YiUjDYA+3djFAFBBg==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "^3.972.9", - "@aws-sdk/types": "^3.973.1", - "@smithy/protocol-http": "^5.3.8", - "@smithy/signature-v4": "^5.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/middleware-sdk-s3": "^3.972.20", + "@aws-sdk/types": "^3.973.6", + "@smithy/protocol-http": "^5.3.12", + "@smithy/signature-v4": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -932,16 +897,17 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.989.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.989.0.tgz", - "integrity": "sha512-OdBByMv+OjOZoekrk4THPFpLuND5aIQbDHCGh3n2rvifAbm31+6e0OLhxSeCF1UMPm+nKq12bXYYEoCIx5SQBg==", + "version": "3.1009.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1009.0.tgz", + "integrity": "sha512-KCPLuTqN9u0Rr38Arln78fRG9KXpzsPWmof+PZzfAHMMQq2QED6YjQrkrfiH7PDefLWEposY1o4/eGwrmKA4JA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "^3.973.9", - "@aws-sdk/nested-clients": "3.989.0", - "@aws-sdk/types": "^3.973.1", - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@aws-sdk/core": "^3.973.20", + "@aws-sdk/nested-clients": "^3.996.10", + "@aws-sdk/types": "^3.973.6", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -949,12 +915,12 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.973.1", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.1.tgz", - "integrity": "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg==", + "version": "3.973.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.6.tgz", + "integrity": "sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -962,9 +928,9 @@ } }, "node_modules/@aws-sdk/util-arn-parser": { - "version": "3.972.2", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.2.tgz", - "integrity": "sha512-VkykWbqMjlSgBFDyrY3nOSqupMc6ivXuGmvci6Q3NnLq5kC+mKQe2QBZ4nrWRE/jqOxeFP2uYzLtwncYYcvQDg==", + "version": "3.972.3", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.972.3.tgz", + "integrity": "sha512-HzSD8PMFrvgi2Kserxuff5VitNq2sgf3w9qxmskKDiDTThWfVteJxuCS9JXiPIPtmCrp+7N9asfIaVhBFORllA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -974,14 +940,15 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.989.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.989.0.tgz", - "integrity": "sha512-eKmAOeQM4Qusq0jtcbZPiNWky8XaojByKC/n+THbJ8vJf7t4ys8LlcZ4PrBSHZISe9cC484mQsPVOQh6iySjqw==", + "version": "3.996.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.5.tgz", + "integrity": "sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-endpoints": "^3.2.8", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-endpoints": "^3.3.3", "tslib": "^2.6.2" }, "engines": { @@ -1001,25 +968,28 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.972.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.3.tgz", - "integrity": "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw==", + "version": "3.972.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.8.tgz", + "integrity": "sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "^3.973.1", - "@smithy/types": "^4.12.0", + "@aws-sdk/types": "^3.973.6", + "@smithy/types": "^4.13.1", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.972.7", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.972.7.tgz", - "integrity": "sha512-oyhv+FjrgHjP+F16cmsrJzNP4qaRJzkV1n9Lvv4uyh3kLqo3rIe9NSBSBa35f2TedczfG2dD+kaQhHBB47D6Og==", + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.7.tgz", + "integrity": "sha512-Hz6EZMUAEzqUd7e+vZ9LE7mn+5gMbxltXy18v+YSFY+9LBJz15wkNZvw5JqfX3z0FS9n3bgUtz3L5rAsfh4YlA==", + "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "^3.972.9", - "@aws-sdk/types": "^3.973.1", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", + "@aws-sdk/middleware-user-agent": "^3.972.21", + "@aws-sdk/types": "^3.973.6", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -1035,12 +1005,13 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.4.tgz", - "integrity": "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q==", + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.12.tgz", + "integrity": "sha512-xjyucfn+F+kMf25c+LIUnvX3oyLSlj9T0Vncs5WMQI6G36JdnSwC8g0qf8RajfmSClXr660EpTz7FFKluZ4BqQ==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "fast-xml-parser": "5.3.4", + "@smithy/types": "^4.13.1", + "fast-xml-parser": "5.5.6", "tslib": "^2.6.2" }, "engines": { @@ -1048,21 +1019,22 @@ } }, "node_modules/@aws/lambda-invoke-store": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz", - "integrity": "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "license": "Apache-2.0", "engines": { "node": ">=18.0.0" } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -1071,9 +1043,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -1081,23 +1053,22 @@ } }, "node_modules/@babel/core": { - "version": "7.26.10", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz", - "integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.10", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.10", - "@babel/parser": "^7.26.10", - "@babel/template": "^7.26.9", - "@babel/traverse": "^7.26.10", - "@babel/types": "^7.26.10", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -1112,6 +1083,41 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/@babel/parser": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/core/node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -1123,14 +1129,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -1140,13 +1146,13 @@ } }, "node_modules/@babel/generator/node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1156,13 +1162,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -1193,27 +1199,27 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports/node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1223,18 +1229,18 @@ } }, "node_modules/@babel/helper-module-imports/node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -1242,15 +1248,15 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1260,13 +1266,13 @@ } }, "node_modules/@babel/helper-module-transforms/node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1276,18 +1282,18 @@ } }, "node_modules/@babel/helper-module-transforms/node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -1325,14 +1331,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -1364,28 +1370,28 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/template/node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", + "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -1414,9 +1420,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "devOptional": true, "license": "MIT", "dependencies": { @@ -1454,9 +1460,9 @@ "license": "MIT" }, "node_modules/@dotenvx/dotenvx": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.52.0.tgz", - "integrity": "sha512-CaQcc8JvtzQhUSm9877b6V4Tb7HCotkcyud9X2YwdqtQKwgljkMRwU96fVYKnzN3V0Hj74oP7Es+vZ0mS+Aa1w==", + "version": "1.54.1", + "resolved": "https://registry.npmjs.org/@dotenvx/dotenvx/-/dotenvx-1.54.1.tgz", + "integrity": "sha512-41gU3q7v05GM92QPuPUf4CmUw+mmF8p4wLUh6MCRlxpCkJ9ByLcY9jUf6MwrMNmiKyG/rIckNxj9SCfmNCmCqw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -1542,418 +1548,6 @@ "source-map-support": "^0.5.21" } }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", - "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", - "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", - "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", - "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", - "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", - "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", - "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", - "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", - "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", - "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", - "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", - "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", - "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", - "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", - "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", - "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", - "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", - "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", - "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", - "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", - "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", - "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild-kit/core-utils/node_modules/esbuild": { - "version": "0.18.20", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", - "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/android-arm": "0.18.20", - "@esbuild/android-arm64": "0.18.20", - "@esbuild/android-x64": "0.18.20", - "@esbuild/darwin-arm64": "0.18.20", - "@esbuild/darwin-x64": "0.18.20", - "@esbuild/freebsd-arm64": "0.18.20", - "@esbuild/freebsd-x64": "0.18.20", - "@esbuild/linux-arm": "0.18.20", - "@esbuild/linux-arm64": "0.18.20", - "@esbuild/linux-ia32": "0.18.20", - "@esbuild/linux-loong64": "0.18.20", - "@esbuild/linux-mips64el": "0.18.20", - "@esbuild/linux-ppc64": "0.18.20", - "@esbuild/linux-riscv64": "0.18.20", - "@esbuild/linux-s390x": "0.18.20", - "@esbuild/linux-x64": "0.18.20", - "@esbuild/netbsd-x64": "0.18.20", - "@esbuild/openbsd-x64": "0.18.20", - "@esbuild/sunos-x64": "0.18.20", - "@esbuild/win32-arm64": "0.18.20", - "@esbuild/win32-ia32": "0.18.20", - "@esbuild/win32-x64": "0.18.20" - } - }, "node_modules/@esbuild-kit/esm-loader": { "version": "2.6.5", "resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz", @@ -2467,118 +2061,68 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", - "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.7", + "@eslint/object-schema": "^3.0.3", "debug": "^4.3.1", - "minimatch": "^3.1.2" + "minimatch": "^10.2.4" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/config-helpers": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", - "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0" + "@eslint/core": "^1.1.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", - "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/@eslint/eslintrc": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", - "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.1", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@eslint/js": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", - "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://eslint.org/donate" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/object-schema": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", - "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", - "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.17.0", + "@eslint/core": "^1.1.1", "levn": "^0.4.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@faker-js/faker": { @@ -2662,16 +2206,6 @@ "tslib": "^2.8.1" } }, - "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz", - "integrity": "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==", - "license": "MIT", - "dependencies": { - "@formatjs/fast-memoize": "3.1.0", - "tslib": "^2.8.1" - } - }, "node_modules/@formatjs/fast-memoize": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-3.1.0.tgz", @@ -2703,12 +2237,13 @@ } }, "node_modules/@formatjs/intl-localematcher": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz", - "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.8.1.tgz", + "integrity": "sha512-xwEuwQFdtSq1UKtQnyTZWC+eHdv7Uygoa+H2k/9uzBVQjDyp9r20LNDNKedWXll7FssT3GRHvqsdJGYSUWqYFA==", "license": "MIT", "dependencies": { - "tslib": "2" + "@formatjs/fast-memoize": "3.1.0", + "tslib": "^2.8.1" } }, "node_modules/@headlessui/react": { @@ -2818,7 +2353,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2841,7 +2375,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2864,7 +2397,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2881,7 +2413,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2898,7 +2429,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2915,7 +2445,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2932,7 +2461,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2949,7 +2477,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2966,7 +2493,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2983,7 +2509,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3000,7 +2525,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -3017,7 +2541,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -3040,7 +2563,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -3063,7 +2585,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -3086,7 +2607,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -3109,7 +2629,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -3132,7 +2651,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -3155,7 +2673,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -3178,7 +2695,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -3198,7 +2714,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3218,7 +2733,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3238,7 +2752,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -3252,42 +2765,11 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.0.tgz", - "integrity": "sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", "license": "MIT" }, - "node_modules/@isaacs/balanced-match": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", - "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/brace-expansion": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.1.tgz", - "integrity": "sha512-WMz71T1JS624nWj2n2fnYAuPovhv7EUhk69R6i9dsVyzxt5eM3bjwvgk9L+APE1TRscGysAVMANkB0jh0LQZrQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@isaacs/balanced-match": "^4.0.1" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@isaacs/cliui": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", - "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", - "engines": { - "node": ">=18" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3385,9 +2867,9 @@ "integrity": "sha512-pUvdJN1on574wQHjaBfNGDt9Mz5utDSZFsIIQkMzPgNS8ZvT4H2mwOrOIClwsQOb6EGx5M76/CZr6G8i6pSpLg==" }, "node_modules/@next/eslint-plugin-next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.6.tgz", - "integrity": "sha512-/Qq3PTagA6+nYVfryAtQ7/9FEr/6YVyvOtl6rZnGsbReGLf0jZU6gkpr1FuChAQpvV46a78p4cmHOVP8mbfSMQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.1.7.tgz", + "integrity": "sha512-v/bRGOJlfRCO+NDKt0bZlIIWjhMKU8xbgEQBo+rV9C8S6czZvs96LZ/v24/GvpEnovZlL4QDpku/RzWHVbmPpA==", "dev": true, "license": "MIT", "dependencies": { @@ -3520,7 +3002,6 @@ "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -4505,92 +3986,92 @@ } }, "node_modules/@peculiar/asn1-cms": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz", - "integrity": "sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.1.tgz", + "integrity": "sha512-vdG4fBF6Lkirkcl53q6eOdn3XYKt+kJTG59edgRZORlg/3atWWEReRCx5rYE1ZzTTX6vLK5zDMjHh7vbrcXGtw==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", - "@peculiar/asn1-x509-attr": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-csr": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.0.tgz", - "integrity": "sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.1.tgz", + "integrity": "sha512-WRWnKfIocHyzFYQTka8O/tXCiBquAPSrRjXbOkHbO4qdmS6loffCEGs+rby6WxxGdJCuunnhS2duHURhjyio6w==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-ecc": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.0.tgz", - "integrity": "sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.1.tgz", + "integrity": "sha512-+Vqw8WFxrtDIN5ehUdvlN2m73exS2JVG0UAyfVB31gIfor3zWEAQPD+K9ydCxaj3MLen9k0JhKpu9LqviuCE1g==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-pfx": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.0.tgz", - "integrity": "sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.1.tgz", + "integrity": "sha512-nB5jVQy3MAAWvq0KY0R2JUZG8bO/bTLpnwyOzXyEh/e54ynGTatAR+csOnXkkVD9AFZ2uL8Z7EV918+qB1qDvw==", "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-pkcs8": "^2.6.0", - "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-pkcs8": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.0.tgz", - "integrity": "sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.1.tgz", + "integrity": "sha512-JB5iQ9Izn5yGMw3ZG4Nw3Xn/hb/G38GYF3lf7WmJb8JZUydhVGEjK/ZlFSWhnlB7K/4oqEs8HnfFIKklhR58Tw==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-pkcs9": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.0.tgz", - "integrity": "sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.1.tgz", + "integrity": "sha512-5EV8nZoMSxeWmcxWmmcolg22ojZRgJg+Y9MX2fnE2bGRo5KQLqV5IL9kdSQDZxlHz95tHvIq9F//bvL1OeNILw==", "license": "MIT", "dependencies": { - "@peculiar/asn1-cms": "^2.6.0", - "@peculiar/asn1-pfx": "^2.6.0", - "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-cms": "^2.6.1", + "@peculiar/asn1-pfx": "^2.6.1", + "@peculiar/asn1-pkcs8": "^2.6.1", "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", - "@peculiar/asn1-x509-attr": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/asn1-x509-attr": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/asn1-rsa": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.0.tgz", - "integrity": "sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.1.tgz", + "integrity": "sha512-1nVMEh46SElUt5CB3RUTV4EG/z7iYc7EoaDY5ECwganibQPkZ/Y2eMsTKB/LeyrUJ+W/tKoD9WUqIy8vB+CEdA==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } @@ -4607,9 +4088,9 @@ } }, "node_modules/@peculiar/asn1-x509": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.0.tgz", - "integrity": "sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.1.tgz", + "integrity": "sha512-O9jT5F1A2+t3r7C4VT7LYGXqkGLK7Kj1xFpz7U0isPrubwU5PbDoyYtx6MiGst29yq7pXN5vZbQFKRCP+lLZlA==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", @@ -4619,21 +4100,21 @@ } }, "node_modules/@peculiar/asn1-x509-attr": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.0.tgz", - "integrity": "sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==", + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.1.tgz", + "integrity": "sha512-tlW6cxoHwgcQghnJwv3YS+9OO1737zgPogZ+CgWRUK4roEwIPzRH4JEiG770xe5HX2ATfCpmX60gurfWIF9dcQ==", "license": "MIT", "dependencies": { "@peculiar/asn1-schema": "^2.6.0", - "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", "asn1js": "^3.0.6", "tslib": "^2.8.1" } }, "node_modules/@peculiar/x509": { - "version": "1.14.2", - "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.2.tgz", - "integrity": "sha512-r2w1Hg6pODDs0zfAKHkSS5HLkOLSeburtcgwvlLLWWCixw+MmW3U6kD5ddyvc2Y2YdbGuVwCF2S2ASoU1cFAag==", + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.3.tgz", + "integrity": "sha512-C2Xj8FZ0uHWeCXXqX5B4/gVFQmtSkiuOolzAgutjTfseNOHT3pUjljDZsTSxXFGgio54bCzVFqmEOUrIVk8RDA==", "license": "MIT", "dependencies": { "@peculiar/asn1-cms": "^2.6.0", @@ -4649,13 +4130,14 @@ "tsyringe": "^4.10.0" }, "engines": { - "node": ">=22.0.0" + "node": ">=20.0.0" } }, "node_modules/@posthog/core": { - "version": "1.22.0", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.22.0.tgz", - "integrity": "sha512-WkmOnq95aAOu6yk6r5LWr5cfXsQdpVbWDCwOxQwxSne8YV6GuZET1ziO5toSQXgrgbdcjrSz2/GopAfiL6iiAA==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.23.2.tgz", + "integrity": "sha512-zTDdda9NuSHrnwSOfFMxX/pyXiycF4jtU1kTr8DL61dHhV+7LF6XF1ndRZZTuaGGbfbb/GJYkEsjEX9SXfNZeQ==", + "license": "MIT", "dependencies": { "cross-spawn": "^7.0.6" } @@ -6959,9 +6441,9 @@ } }, "node_modules/@react-email/components": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.7.tgz", - "integrity": "sha512-mY+v4C1SMaGOKuKp0QWDQLGK+3fvH06ZE10EVavv+T6tQneDHq9cpQ9NdCrvuO1nWZnWrA/0tRpvyqyF0uo93w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-1.0.8.tgz", + "integrity": "sha512-zY81ED6o5MWMzBkr9uZFuT24lWarT+xIbOZxI6C9dsFmCWBczM8IE1BgOI8rhpUK4JcYVDy1uKxYAFqsx2Bc4w==", "license": "MIT", "dependencies": { "@react-email/body": "0.2.1", @@ -6982,7 +6464,7 @@ "@react-email/render": "2.0.4", "@react-email/row": "0.0.13", "@react-email/section": "0.0.17", - "@react-email/tailwind": "2.0.4", + "@react-email/tailwind": "2.0.5", "@react-email/text": "0.1.6" }, "engines": { @@ -7116,469 +6598,27 @@ } }, "node_modules/@react-email/preview-server": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/@react-email/preview-server/-/preview-server-5.2.8.tgz", - "integrity": "sha512-drQ0C7vi7P0uE7Ox1Cyiujsx0oqp2RbIscOdSBR5qvzw3EKjlGbW2pWjQ000cjxTq3Si7lqlRKhOIF8MzOnqHw==", + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/@react-email/preview-server/-/preview-server-5.2.10.tgz", + "integrity": "sha512-cYi21KF+Z/HGXT8RpkQMNFFubBafxyoB9Hn/wrslfDNtdoews2MdsDo6XXKkZvDTRG9SxQN3HGk4v4aoQZc20g==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "0.25.10", - "next": "16.1.6" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@react-email/preview-server/node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "esbuild": "0.27.3", + "next": "16.1.7" } }, "node_modules/@react-email/preview-server/node_modules/@next/env": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.6.tgz", - "integrity": "sha512-N1ySLuZjnAtN3kFnwhAwPvZah8RJxKasD7x1f8shFqhncnWZn4JMfg37diLNuoHsLAlrDfM3g4mawVdtAG8XLQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/env/-/env-16.1.7.tgz", + "integrity": "sha512-rJJbIdJB/RQr2F1nylZr/PJzamvNNhfr3brdKP6s/GW850jbtR70QlSfFselvIBbcPUOlQwBakexjFzqLzF6pg==", "dev": true, "license": "MIT" }, "node_modules/@react-email/preview-server/node_modules/@next/swc-darwin-arm64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.6.tgz", - "integrity": "sha512-wTzYulosJr/6nFnqGW7FrG3jfUUlEf8UjGA0/pyypJl42ExdVgC6xJgcXQ+V8QFn6niSG2Pb8+MIG1mZr2vczw==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.1.7.tgz", + "integrity": "sha512-b2wWIE8sABdyafc4IM8r5Y/dS6kD80JRtOGrUiKTsACFQfWWgUQ2NwoUX1yjFMXVsAwcQeNpnucF2ZrujsBBPg==", "cpu": [ "arm64" ], @@ -7593,9 +6633,9 @@ } }, "node_modules/@react-email/preview-server/node_modules/@next/swc-darwin-x64": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.6.tgz", - "integrity": "sha512-BLFPYPDO+MNJsiDWbeVzqvYd4NyuRrEYVB5k2N3JfWncuHAy2IVwMAOlVQDFjj+krkWzhY2apvmekMkfQR0CUQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.1.7.tgz", + "integrity": "sha512-zcnVaaZulS1WL0Ss38R5Q6D2gz7MtBu8GZLPfK+73D/hp4GFMrC2sudLky1QibfV7h6RJBJs/gOFvYP0X7UVlQ==", "cpu": [ "x64" ], @@ -7610,9 +6650,9 @@ } }, "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-arm64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.6.tgz", - "integrity": "sha512-OJYkCd5pj/QloBvoEcJ2XiMnlJkRv9idWA/j0ugSuA34gMT6f5b7vOiCQHVRpvStoZUknhl6/UxOXL4OwtdaBw==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.1.7.tgz", + "integrity": "sha512-2ant89Lux/Q3VyC8vNVg7uBaFVP9SwoK2jJOOR0L8TQnX8CAYnh4uctAScy2Hwj2dgjVHqHLORQZJ2wH6VxhSQ==", "cpu": [ "arm64" ], @@ -7627,9 +6667,9 @@ } }, "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-arm64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.6.tgz", - "integrity": "sha512-S4J2v+8tT3NIO9u2q+S0G5KdvNDjXfAv06OhfOzNDaBn5rw84DGXWndOEB7d5/x852A20sW1M56vhC/tRVbccQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.1.7.tgz", + "integrity": "sha512-uufcze7LYv0FQg9GnNeZ3/whYfo+1Q3HnQpm16o6Uyi0OVzLlk2ZWoY7j07KADZFY8qwDbsmFnMQP3p3+Ftprw==", "cpu": [ "arm64" ], @@ -7644,9 +6684,9 @@ } }, "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-x64-gnu": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.6.tgz", - "integrity": "sha512-2eEBDkFlMMNQnkTyPBhQOAyn2qMxyG2eE7GPH2WIDGEpEILcBPI/jdSv4t6xupSP+ot/jkfrCShLAa7+ZUPcJQ==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.1.7.tgz", + "integrity": "sha512-KWVf2gxYvHtvuT+c4MBOGxuse5TD7DsMFYSxVxRBnOzok/xryNeQSjXgxSv9QpIVlaGzEn/pIuI6Koosx8CGWA==", "cpu": [ "x64" ], @@ -7661,9 +6701,9 @@ } }, "node_modules/@react-email/preview-server/node_modules/@next/swc-linux-x64-musl": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.6.tgz", - "integrity": "sha512-oicJwRlyOoZXVlxmIMaTq7f8pN9QNbdes0q2FXfRsPhfCi8n8JmOZJm5oo1pwDaFbnnD421rVU409M3evFbIqg==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.1.7.tgz", + "integrity": "sha512-HguhaGwsGr1YAGs68uRKc4aGWxLET+NevJskOcCAwXbwj0fYX0RgZW2gsOCzr9S11CSQPIkxmoSbuVaBp4Z3dA==", "cpu": [ "x64" ], @@ -7678,9 +6718,9 @@ } }, "node_modules/@react-email/preview-server/node_modules/@next/swc-win32-arm64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.6.tgz", - "integrity": "sha512-gQmm8izDTPgs+DCWH22kcDmuUp7NyiJgEl18bcr8irXA5N2m2O+JQIr6f3ct42GOs9c0h8QF3L5SzIxcYAAXXw==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.1.7.tgz", + "integrity": "sha512-S0n3KrDJokKTeFyM/vGGGR8+pCmXYrjNTk2ZozOL1C/JFdfUIL9O1ATaJOl5r2POe56iRChbsszrjMAdWSv7kQ==", "cpu": [ "arm64" ], @@ -7695,9 +6735,9 @@ } }, "node_modules/@react-email/preview-server/node_modules/@next/swc-win32-x64-msvc": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.6.tgz", - "integrity": "sha512-NRfO39AIrzBnixKbjuo2YiYhB6o9d8v/ymU9m/Xk8cyVk+k7XylniXkHwjs4s70wedVffc6bQNbufk5v0xEm0A==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.1.7.tgz", + "integrity": "sha512-mwgtg8CNZGYm06LeEd+bNnOUfwOyNem/rOiP14Lsz+AnUY92Zq/LXwtebtUiaeVkhbroRCQ0c8GlR4UT1U+0yg==", "cpu": [ "x64" ], @@ -7721,58 +6761,16 @@ "tslib": "^2.8.0" } }, - "node_modules/@react-email/preview-server/node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" - } - }, "node_modules/@react-email/preview-server/node_modules/next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-16.1.6.tgz", - "integrity": "sha512-hkyRkcu5x/41KoqnROkfTm2pZVbKxvbZRuNvKXLRXxs3VfyO0WhY50TQS40EuKO9SW3rBj/sF3WbVwDACeMZyw==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/next/-/next-16.1.7.tgz", + "integrity": "sha512-WM0L7WrSvKwoLegLYr6V+mz+RIofqQgVAfHhMp9a88ms0cFX8iX9ew+snpWlSBwpkURJOUdvCEt3uLl3NNzvWg==", "dev": true, "license": "MIT", "dependencies": { - "@next/env": "16.1.6", + "@next/env": "16.1.7", "@swc/helpers": "0.5.15", - "baseline-browser-mapping": "^2.8.3", + "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" @@ -7784,14 +6782,14 @@ "node": ">=20.9.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "16.1.6", - "@next/swc-darwin-x64": "16.1.6", - "@next/swc-linux-arm64-gnu": "16.1.6", - "@next/swc-linux-arm64-musl": "16.1.6", - "@next/swc-linux-x64-gnu": "16.1.6", - "@next/swc-linux-x64-musl": "16.1.6", - "@next/swc-win32-arm64-msvc": "16.1.6", - "@next/swc-win32-x64-msvc": "16.1.6", + "@next/swc-darwin-arm64": "16.1.7", + "@next/swc-darwin-x64": "16.1.7", + "@next/swc-linux-arm64-gnu": "16.1.7", + "@next/swc-linux-arm64-musl": "16.1.7", + "@next/swc-linux-x64-gnu": "16.1.7", + "@next/swc-linux-x64-musl": "16.1.7", + "@next/swc-win32-arm64-msvc": "16.1.7", + "@next/swc-win32-x64-msvc": "16.1.7", "sharp": "^0.34.4" }, "peerDependencies": { @@ -7888,9 +6886,9 @@ } }, "node_modules/@react-email/tailwind": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.4.tgz", - "integrity": "sha512-cDp8Ss6LJKI8zBLKE+tsXFurn6I2nnQNg1qqjfZuNPNoToN1Uyx3egW0bwSVk1JjrNWx/Xnme7ZxvNLRrU9K0Q==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-2.0.5.tgz", + "integrity": "sha512-7Ey+kiWliJdxPMCLYsdDts8ffp4idlP//w4Ui3q/A5kokVaLSNKG8DOg/8qAuzWmRiGwNQVOKBk7PXNlK5W+sg==", "license": "MIT", "dependencies": { "tailwindcss": "^4.1.18" @@ -7950,7 +6948,6 @@ "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.1.6.tgz", "integrity": "sha512-TYqkioRS45wTR5il3dYk/SbUjjEdhSwh9BtRNB99qNH1pXAwA45H7rAuxehiu8iJQJH0IyIr+6n62gBz9ezmsw==", "license": "MIT", - "peer": true, "engines": { "node": ">=20.0.0" }, @@ -8022,37 +7019,37 @@ } }, "node_modules/@simplewebauthn/browser": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.2.2.tgz", - "integrity": "sha512-FNW1oLQpTJyqG5kkDg5ZsotvWgmBaC6jCHR7Ej0qUNep36Wl9tj2eZu7J5rP+uhXgHaLk+QQ3lqcw2vS5MX1IA==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/browser/-/browser-13.3.0.tgz", + "integrity": "sha512-BE/UWv6FOToAdVk0EokzkqQQDOWtNydYlY6+OrmiZ5SCNmb41VehttboTetUM3T/fr6EAFYVXjz4My2wg230rQ==", "license": "MIT" }, "node_modules/@simplewebauthn/server": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.2.2.tgz", - "integrity": "sha512-HcWLW28yTMGXpwE9VLx9J+N2KEUaELadLrkPEEI9tpI5la70xNEVEsu/C+m3u7uoq4FulLqZQhgBCzR9IZhFpA==", + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@simplewebauthn/server/-/server-13.3.0.tgz", + "integrity": "sha512-MLHYFrYG8/wK2i+86XMhiecK72nMaHKKt4bo+7Q1TbuG9iGjlSdfkPWKO5ZFE/BX+ygCJ7pr8H/AJeyAj1EaTQ==", "license": "MIT", "dependencies": { "@hexagon/base64": "^1.1.27", "@levischuck/tiny-cbor": "^0.2.2", - "@peculiar/asn1-android": "^2.3.10", - "@peculiar/asn1-ecc": "^2.3.8", - "@peculiar/asn1-rsa": "^2.3.8", - "@peculiar/asn1-schema": "^2.3.8", - "@peculiar/asn1-x509": "^2.3.8", - "@peculiar/x509": "^1.13.0" + "@peculiar/asn1-android": "^2.6.0", + "@peculiar/asn1-ecc": "^2.6.1", + "@peculiar/asn1-rsa": "^2.6.1", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.1", + "@peculiar/x509": "^1.14.3" }, "engines": { "node": ">=20.0.0" } }, "node_modules/@smithy/abort-controller": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.8.tgz", - "integrity": "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.12.tgz", + "integrity": "sha512-xolrFw6b+2iYGl6EcOL7IJY71vvyZ0DJ3mcKtpykqPe2uscwtzDZJa1uVQXyP7w9Dd+kGwYnPbMsJrGISKiY/Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8060,9 +7057,9 @@ } }, "node_modules/@smithy/chunked-blob-reader": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.0.tgz", - "integrity": "sha512-WmU0TnhEAJLWvfSeMxBNe5xtbselEO8+4wG0NtZeL8oR21WgH1xiO37El+/Y+H/Ie4SCwBy3MxYWmOYaGgZueA==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.2.2.tgz", + "integrity": "sha512-St+kVicSyayWQca+I1rGitaOEH6uKgE8IUWoYnnEX26SWdWQcL6LvMSD19Lg+vYHKdT9B2Zuu7rd3i6Wnyb/iw==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -8072,12 +7069,12 @@ } }, "node_modules/@smithy/chunked-blob-reader-native": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.1.tgz", - "integrity": "sha512-lX9Ay+6LisTfpLid2zZtIhSEjHMZoAR5hHCR4H7tBz/Zkfr5ea8RcQ7Tk4mi0P76p4cN+Btz16Ffno7YHpKXnQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.2.3.tgz", + "integrity": "sha512-jA5k5Udn7Y5717L86h4EIv06wIr3xn8GM1qHRi/Nf31annXcXHJjBKvgztnbn2TxH3xWrPBfgwHsOwZf0UmQWw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-base64": "^4.3.0", + "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -8085,15 +7082,16 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.4.6", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.6.tgz", - "integrity": "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ==", + "version": "4.4.11", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.11.tgz", + "integrity": "sha512-YxFiiG4YDAtX7WMN7RuhHZLeTmRRAOyCbr+zB8e3AQzHPnUhS8zXjB1+cniPVQI3xbWsQPM0X2aaIkO/ME0ymw==", + "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-config-provider": "^4.2.0", - "@smithy/util-endpoints": "^3.2.8", - "@smithy/util-middleware": "^4.2.8", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.3", + "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -8101,19 +7099,20 @@ } }, "node_modules/@smithy/core": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.0.tgz", - "integrity": "sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg==", + "version": "3.23.12", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.12.tgz", + "integrity": "sha512-o9VycsYNtgC+Dy3I0yrwCqv9CWicDnke0L7EVOrZtJpjb2t0EjaEofmMrYc0T1Kn3yk32zm6cspxF9u9Bj7e5w==", + "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.2.9", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-body-length-browser": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-stream": "^4.5.12", - "@smithy/util-utf8": "^4.2.0", - "@smithy/uuid": "^1.1.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-stream": "^4.5.20", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, "engines": { @@ -8121,14 +7120,15 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.8.tgz", - "integrity": "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.12.tgz", + "integrity": "sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==", + "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -8136,14 +7136,14 @@ } }, "node_modules/@smithy/eventstream-codec": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.8.tgz", - "integrity": "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.12.tgz", + "integrity": "sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==", "license": "Apache-2.0", "dependencies": { "@aws-crypto/crc32": "5.2.0", - "@smithy/types": "^4.12.0", - "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -8151,13 +7151,13 @@ } }, "node_modules/@smithy/eventstream-serde-browser": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.8.tgz", - "integrity": "sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.12.tgz", + "integrity": "sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/eventstream-serde-universal": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8165,12 +7165,12 @@ } }, "node_modules/@smithy/eventstream-serde-config-resolver": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.8.tgz", - "integrity": "sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ==", + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.12.tgz", + "integrity": "sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8178,13 +7178,13 @@ } }, "node_modules/@smithy/eventstream-serde-node": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.8.tgz", - "integrity": "sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.12.tgz", + "integrity": "sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-serde-universal": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/eventstream-serde-universal": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8192,13 +7192,13 @@ } }, "node_modules/@smithy/eventstream-serde-universal": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.8.tgz", - "integrity": "sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.12.tgz", + "integrity": "sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/eventstream-codec": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/eventstream-codec": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8206,14 +7206,15 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.3.9", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.9.tgz", - "integrity": "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA==", + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.15.tgz", + "integrity": "sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==", + "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/querystring-builder": "^4.2.8", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", "tslib": "^2.6.2" }, "engines": { @@ -8221,14 +7222,14 @@ } }, "node_modules/@smithy/hash-blob-browser": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.9.tgz", - "integrity": "sha512-m80d/iicI7DlBDxyQP6Th7BW/ejDGiF0bgI754+tiwK0lgMkcaIBgvwwVc7OFbY4eUzpGtnig52MhPAEJ7iNYg==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.2.13.tgz", + "integrity": "sha512-YrF4zWKh+ghLuquldj6e/RzE3xZYL8wIPfkt0MqCRphVICjyyjH8OwKD7LLlKpVEbk4FLizFfC1+gwK6XQdR3g==", "license": "Apache-2.0", "dependencies": { - "@smithy/chunked-blob-reader": "^5.2.0", - "@smithy/chunked-blob-reader-native": "^4.2.1", - "@smithy/types": "^4.12.0", + "@smithy/chunked-blob-reader": "^5.2.2", + "@smithy/chunked-blob-reader-native": "^4.2.3", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8236,13 +7237,14 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.8.tgz", - "integrity": "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.12.tgz", + "integrity": "sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/types": "^4.13.1", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -8250,13 +7252,13 @@ } }, "node_modules/@smithy/hash-stream-node": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.8.tgz", - "integrity": "sha512-v0FLTXgHrTeheYZFGhR+ehX5qUm4IQsjAiL9qehad2cyjMWcN2QG6/4mSwbSgEQzI7jwfoXj7z4fxZUx/Mhj2w==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.2.12.tgz", + "integrity": "sha512-O3YbmGExeafuM/kP7Y8r6+1y0hIh3/zn6GROx0uNlB54K9oihAL75Qtc+jFfLNliTi6pxOAYZrRKD9A7iA6UFw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/types": "^4.13.1", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -8264,11 +7266,12 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.8.tgz", - "integrity": "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.12.tgz", + "integrity": "sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8276,9 +7279,9 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", - "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -8288,13 +7291,13 @@ } }, "node_modules/@smithy/md5-js": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.8.tgz", - "integrity": "sha512-oGMaLj4tVZzLi3itBa9TCswgMBr7k9b+qKYowQ6x1rTyTuO1IU2YHdHUa+891OsOH+wCsH7aTPRsTJO3RMQmjQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.2.12.tgz", + "integrity": "sha512-W/oIpHCpWU2+iAkfZYyGWE+qkpuf3vEXHLxQQDx9FPNZTTdnul0dZ2d/gUFrtQ5je1G2kp4cjG0/24YueG2LbQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/types": "^4.13.1", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -8302,12 +7305,13 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.8.tgz", - "integrity": "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.12.tgz", + "integrity": "sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==", + "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8315,17 +7319,18 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.4.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.14.tgz", - "integrity": "sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag==", + "version": "4.4.26", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.26.tgz", + "integrity": "sha512-8Qfikvd2GVKSm8S6IbjfwFlRY9VlMrj0Dp4vTwAuhqbX7NhJKE5DQc2bnfJIcY0B+2YKMDBWfvexbSZeejDgeg==", + "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.0", - "@smithy/middleware-serde": "^4.2.9", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", - "@smithy/url-parser": "^4.2.8", - "@smithy/util-middleware": "^4.2.8", + "@smithy/core": "^3.23.12", + "@smithy/middleware-serde": "^4.2.15", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", + "@smithy/url-parser": "^4.2.12", + "@smithy/util-middleware": "^4.2.12", "tslib": "^2.6.2" }, "engines": { @@ -8333,18 +7338,19 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.4.31", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.31.tgz", - "integrity": "sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg==", + "version": "4.4.43", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.43.tgz", + "integrity": "sha512-ZwsifBdyuNHrFGmbc7bAfP2b54+kt9J2rhFd18ilQGAB+GDiP4SrawqyExbB7v455QVR7Psyhb2kjULvBPIhvA==", + "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/service-error-classification": "^4.2.8", - "@smithy/smithy-client": "^4.11.3", - "@smithy/types": "^4.12.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-retry": "^4.2.8", - "@smithy/uuid": "^1.1.0", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-retry": "^4.2.12", + "@smithy/uuid": "^1.1.2", "tslib": "^2.6.2" }, "engines": { @@ -8352,12 +7358,14 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.2.9", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.9.tgz", - "integrity": "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ==", + "version": "4.2.15", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.15.tgz", + "integrity": "sha512-ExYhcltZSli0pgAKOpQQe1DLFBLryeZ22605y/YS+mQpdNWekum9Ujb/jMKfJKgjtz1AZldtwA/wCYuKJgjjlg==", + "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", + "@smithy/core": "^3.23.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8365,11 +7373,12 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.8.tgz", - "integrity": "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.12.tgz", + "integrity": "sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8377,14 +7386,14 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.3.8", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.8.tgz", - "integrity": "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg==", + "version": "4.3.12", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.12.tgz", + "integrity": "sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.8", - "@smithy/shared-ini-file-loader": "^4.4.3", - "@smithy/types": "^4.12.0", + "@smithy/property-provider": "^4.2.12", + "@smithy/shared-ini-file-loader": "^4.4.7", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8392,14 +7401,15 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.4.10", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.10.tgz", - "integrity": "sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.0.tgz", + "integrity": "sha512-Rnq9vQWiR1+/I6NZZMNzJHV6pZYyEHt2ZnuV3MG8z2NNenC4i/8Kzttz7CjZiHSmsN5frhXhg17z3Zqjjhmz1A==", + "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/querystring-builder": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/abort-controller": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/querystring-builder": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8407,12 +7417,12 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.8.tgz", - "integrity": "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.12.tgz", + "integrity": "sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8420,12 +7430,12 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.8.tgz", - "integrity": "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ==", + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.12.tgz", + "integrity": "sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8433,12 +7443,13 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.8.tgz", - "integrity": "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.12.tgz", + "integrity": "sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", - "@smithy/util-uri-escape": "^4.2.0", + "@smithy/types": "^4.13.1", + "@smithy/util-uri-escape": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -8446,11 +7457,12 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.8.tgz", - "integrity": "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.12.tgz", + "integrity": "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8458,23 +7470,24 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.8.tgz", - "integrity": "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.12.tgz", + "integrity": "sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0" + "@smithy/types": "^4.13.1" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.3.tgz", - "integrity": "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg==", + "version": "4.4.7", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.7.tgz", + "integrity": "sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==", "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8482,17 +7495,18 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.3.8", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.8.tgz", - "integrity": "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg==", + "version": "5.3.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.12.tgz", + "integrity": "sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==", + "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-middleware": "^4.2.8", - "@smithy/util-uri-escape": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.12", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -8500,16 +7514,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.11.3", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.11.3.tgz", - "integrity": "sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg==", + "version": "4.12.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.6.tgz", + "integrity": "sha512-aib3f0jiMsJ6+cvDnXipBsGDL7ztknYSVqJs1FdN9P+u9tr/VzOR7iygSh6EUOdaBeMCMSh3N0VdyYsG4o91DQ==", + "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.23.0", - "@smithy/middleware-endpoint": "^4.4.14", - "@smithy/middleware-stack": "^4.2.8", - "@smithy/protocol-http": "^5.3.8", - "@smithy/types": "^4.12.0", - "@smithy/util-stream": "^4.5.12", + "@smithy/core": "^3.23.12", + "@smithy/middleware-endpoint": "^4.4.26", + "@smithy/middleware-stack": "^4.2.12", + "@smithy/protocol-http": "^5.3.12", + "@smithy/types": "^4.13.1", + "@smithy/util-stream": "^4.5.20", "tslib": "^2.6.2" }, "engines": { @@ -8517,9 +7532,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.12.0.tgz", - "integrity": "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw==", + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz", + "integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -8529,12 +7544,13 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.8.tgz", - "integrity": "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.12.tgz", + "integrity": "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==", + "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/querystring-parser": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8542,13 +7558,13 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", - "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -8556,9 +7572,10 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", - "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -8567,9 +7584,10 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", - "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -8578,12 +7596,12 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", - "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.2.0", + "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -8591,9 +7609,9 @@ } }, "node_modules/@smithy/util-config-provider": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", - "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -8603,13 +7621,14 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.3.30", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.30.tgz", - "integrity": "sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng==", + "version": "4.3.42", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.42.tgz", + "integrity": "sha512-0vjwmcvkWAUtikXnWIUOyV6IFHTEeQUYh3JUZcDgcszF+hD/StAsQ3rCZNZEPHgI9kVNcbnyc8P2CBHnwgmcwg==", + "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.11.3", - "@smithy/types": "^4.12.0", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8617,16 +7636,17 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.2.33", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.33.tgz", - "integrity": "sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA==", + "version": "4.2.45", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.45.tgz", + "integrity": "sha512-q5dOqqfTgUcLe38TAGiFn9srToKj2YCHJ34QGOLzM+xYLLA+qRZv7N+33kl1MERVusue36ZHnlNaNEvY/PzSrw==", + "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.4.6", - "@smithy/credential-provider-imds": "^4.2.8", - "@smithy/node-config-provider": "^4.3.8", - "@smithy/property-provider": "^4.2.8", - "@smithy/smithy-client": "^4.11.3", - "@smithy/types": "^4.12.0", + "@smithy/config-resolver": "^4.4.11", + "@smithy/credential-provider-imds": "^4.2.12", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/property-provider": "^4.2.12", + "@smithy/smithy-client": "^4.12.6", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8634,12 +7654,13 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.8.tgz", - "integrity": "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.3.tgz", + "integrity": "sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==", + "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.3.8", - "@smithy/types": "^4.12.0", + "@smithy/node-config-provider": "^4.3.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8647,9 +7668,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", - "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" @@ -8659,11 +7680,12 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.8.tgz", - "integrity": "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.12.tgz", + "integrity": "sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==", + "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.12.0", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8671,12 +7693,13 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.8.tgz", - "integrity": "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg==", + "version": "4.2.12", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.12.tgz", + "integrity": "sha512-1zopLDUEOwumjcHdJ1mwBHddubYF8GMQvstVCLC54Y46rqoHwlIU+8ZzUeaBcD+WCJHyDGSeZ2ml9YSe9aqcoQ==", + "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/service-error-classification": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8684,17 +7707,18 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.5.12", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.12.tgz", - "integrity": "sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg==", + "version": "4.5.20", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.20.tgz", + "integrity": "sha512-4yXLm5n/B5SRBR2p8cZ90Sbv4zL4NKsgxdzCzp/83cXw2KxLEumt5p+GAVyRNZgQOSrzXn9ARpO0lUe8XSlSDw==", + "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.3.9", - "@smithy/node-http-handler": "^4.4.10", - "@smithy/types": "^4.12.0", - "@smithy/util-base64": "^4.3.0", - "@smithy/util-buffer-from": "^4.2.0", - "@smithy/util-hex-encoding": "^4.2.0", - "@smithy/util-utf8": "^4.2.0", + "@smithy/fetch-http-handler": "^5.3.15", + "@smithy/node-http-handler": "^4.5.0", + "@smithy/types": "^4.13.1", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -8702,9 +7726,10 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", - "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -8713,12 +7738,12 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", - "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-buffer-from": "^4.2.2", "tslib": "^2.6.2" }, "engines": { @@ -8726,13 +7751,13 @@ } }, "node_modules/@smithy/util-waiter": { - "version": "4.2.8", - "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.8.tgz", - "integrity": "sha512-n+lahlMWk+aejGuax7DPWtqav8HYnWxQwR+LCG2BgCUmaGcTe9qZCFsmw8TMg9iG75HOwhrJCX9TCJRLH+Yzqg==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.2.13.tgz", + "integrity": "sha512-2zdZ9DTHngRtcYxJK1GUDxruNr53kv5W2Lupe0LMU+Imr6ohQg8M2T14MNkj1Y0wS3FFwpgpGQyvuaMF7CiTmQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.2.8", - "@smithy/types": "^4.12.0", + "@smithy/abort-controller": "^4.2.12", + "@smithy/types": "^4.13.1", "tslib": "^2.6.2" }, "engines": { @@ -8740,9 +7765,10 @@ } }, "node_modules/@smithy/uuid": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", - "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "license": "Apache-2.0", "dependencies": { "tslib": "^2.6.2" }, @@ -9001,6 +8027,15 @@ "@swc/counter": "^0.1.3" } }, + "node_modules/@tabby_ai/hijri-converter": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@tabby_ai/hijri-converter/-/hijri-converter-1.0.5.tgz", + "integrity": "sha512-r5bClKrcIusDoo049dSL8CawnHR6mRdDwhlQuIgZRNty68q0x8k3Lf1BtPAMxRf/GgnHBnIO4ujd3+GQdLWzxQ==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/@tailwindcss/forms": { "version": "0.5.11", "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.11.tgz", @@ -9014,49 +8049,49 @@ } }, "node_modules/@tailwindcss/node": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.18.tgz", - "integrity": "sha512-DoR7U1P7iYhw16qJ49fgXUlry1t4CpXeErJHnQ44JgTSKMaZUdf17cfn5mHchfJ4KRBZRFA/Coo+MUF5+gOaCQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz", + "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/remapping": "^2.3.4", - "enhanced-resolve": "^5.18.3", + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "^5.19.0", "jiti": "^2.6.1", - "lightningcss": "1.30.2", + "lightningcss": "1.31.1", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", - "tailwindcss": "4.1.18" + "tailwindcss": "4.2.1" } }, "node_modules/@tailwindcss/oxide": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.18.tgz", - "integrity": "sha512-EgCR5tTS5bUSKQgzeMClT6iCY3ToqE1y+ZB0AKldj809QXk1Y+3jB0upOYZrn9aGIzPtUsP7sX4QQ4XtjBB95A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz", + "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 10" + "node": ">= 20" }, "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-arm64": "4.1.18", - "@tailwindcss/oxide-darwin-x64": "4.1.18", - "@tailwindcss/oxide-freebsd-x64": "4.1.18", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.18", - "@tailwindcss/oxide-linux-arm64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-arm64-musl": "4.1.18", - "@tailwindcss/oxide-linux-x64-gnu": "4.1.18", - "@tailwindcss/oxide-linux-x64-musl": "4.1.18", - "@tailwindcss/oxide-wasm32-wasi": "4.1.18", - "@tailwindcss/oxide-win32-arm64-msvc": "4.1.18", - "@tailwindcss/oxide-win32-x64-msvc": "4.1.18" + "@tailwindcss/oxide-android-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-arm64": "4.2.1", + "@tailwindcss/oxide-darwin-x64": "4.2.1", + "@tailwindcss/oxide-freebsd-x64": "4.2.1", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1", + "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-arm64-musl": "4.2.1", + "@tailwindcss/oxide-linux-x64-gnu": "4.2.1", + "@tailwindcss/oxide-linux-x64-musl": "4.2.1", + "@tailwindcss/oxide-wasm32-wasi": "4.2.1", + "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1", + "@tailwindcss/oxide-win32-x64-msvc": "4.2.1" } }, "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.18.tgz", - "integrity": "sha512-dJHz7+Ugr9U/diKJA0W6N/6/cjI+ZTAoxPf9Iz9BFRF2GzEX8IvXxFIi/dZBloVJX/MZGvRuFA9rqwdiIEZQ0Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz", + "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==", "cpu": [ "arm64" ], @@ -9067,13 +8102,13 @@ "android" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.18.tgz", - "integrity": "sha512-Gc2q4Qhs660bhjyBSKgq6BYvwDz4G+BuyJ5H1xfhmDR3D8HnHCmT/BSkvSL0vQLy/nkMLY20PQ2OoYMO15Jd0A==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz", + "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==", "cpu": [ "arm64" ], @@ -9084,13 +8119,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.18.tgz", - "integrity": "sha512-FL5oxr2xQsFrc3X9o1fjHKBYBMD1QZNyc1Xzw/h5Qu4XnEBi3dZn96HcHm41c/euGV+GRiXFfh2hUCyKi/e+yw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz", + "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==", "cpu": [ "x64" ], @@ -9101,13 +8136,13 @@ "darwin" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.18.tgz", - "integrity": "sha512-Fj+RHgu5bDodmV1dM9yAxlfJwkkWvLiRjbhuO2LEtwtlYlBgiAT4x/j5wQr1tC3SANAgD+0YcmWVrj8R9trVMA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz", + "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==", "cpu": [ "x64" ], @@ -9118,13 +8153,13 @@ "freebsd" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.18.tgz", - "integrity": "sha512-Fp+Wzk/Ws4dZn+LV2Nqx3IilnhH51YZoRaYHQsVq3RQvEl+71VGKFpkfHrLM/Li+kt5c0DJe/bHXK1eHgDmdiA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz", + "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==", "cpu": [ "arm" ], @@ -9135,13 +8170,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.18.tgz", - "integrity": "sha512-S0n3jboLysNbh55Vrt7pk9wgpyTTPD0fdQeh7wQfMqLPM/Hrxi+dVsLsPrycQjGKEQk85Kgbx+6+QnYNiHalnw==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz", + "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==", "cpu": [ "arm64" ], @@ -9152,13 +8187,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.18.tgz", - "integrity": "sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz", + "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==", "cpu": [ "arm64" ], @@ -9169,13 +8204,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.18.tgz", - "integrity": "sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz", + "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==", "cpu": [ "x64" ], @@ -9186,13 +8221,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.18.tgz", - "integrity": "sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz", + "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==", "cpu": [ "x64" ], @@ -9203,13 +8238,13 @@ "linux" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.18.tgz", - "integrity": "sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz", + "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==", "bundleDependencies": [ "@napi-rs/wasm-runtime", "@emnapi/core", @@ -9225,19 +8260,19 @@ "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", + "@emnapi/core": "^1.8.1", + "@emnapi/runtime": "^1.8.1", "@emnapi/wasi-threads": "^1.1.0", - "@napi-rs/wasm-runtime": "^1.1.0", + "@napi-rs/wasm-runtime": "^1.1.1", "@tybys/wasm-util": "^0.10.1", - "tslib": "^2.4.0" + "tslib": "^2.8.1" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.7.1", + "version": "1.8.1", "dev": true, "inBundle": true, "license": "MIT", @@ -9248,7 +8283,7 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.7.1", + "version": "1.8.1", "dev": true, "inBundle": true, "license": "MIT", @@ -9268,7 +8303,7 @@ } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.0", + "version": "1.1.1", "dev": true, "inBundle": true, "license": "MIT", @@ -9277,6 +8312,10 @@ "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1", "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" } }, "node_modules/@tailwindcss/oxide-wasm32-wasi/node_modules/@tybys/wasm-util": { @@ -9297,9 +8336,9 @@ "optional": true }, "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.18.tgz", - "integrity": "sha512-HjSA7mr9HmC8fu6bdsZvZ+dhjyGCLdotjVOgLA2vEqxEBZaQo9YTX4kwgEvPCpRh8o4uWc4J/wEoFzhEmjvPbA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz", + "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==", "cpu": [ "arm64" ], @@ -9310,13 +8349,13 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.18.tgz", - "integrity": "sha512-bJWbyYpUlqamC8dpR7pfjA0I7vdF6t5VpUGMWRkXVE3AXgIZjYUYAK7II1GNaxR8J1SSrSrppRar8G++JekE3Q==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz", + "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==", "cpu": [ "x64" ], @@ -9327,21 +8366,21 @@ "win32" ], "engines": { - "node": ">= 10" + "node": ">= 20" } }, "node_modules/@tailwindcss/postcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.18.tgz", - "integrity": "sha512-Ce0GFnzAOuPyfV5SxjXGn0CubwGcuDB0zcdaPuCSzAa/2vII24JTkH+I6jcbXLb1ctjZMZZI6OjDaLPJQL1S0g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.2.1.tgz", + "integrity": "sha512-OEwGIBnXnj7zJeonOh6ZG9woofIjGrd2BORfvE5p9USYKDCZoQmfqLcfNiRWoJlRWLdNPn2IgVZuWAOM4iTYMw==", "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", - "@tailwindcss/node": "4.1.18", - "@tailwindcss/oxide": "4.1.18", - "postcss": "^8.4.41", - "tailwindcss": "4.1.18" + "@tailwindcss/node": "4.2.1", + "@tailwindcss/oxide": "4.2.1", + "postcss": "^8.5.6", + "tailwindcss": "4.2.1" } }, "node_modules/@tanstack/query-core": { @@ -9369,7 +8408,6 @@ "version": "5.90.21", "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", - "peer": true, "dependencies": { "@tanstack/query-core": "5.90.20" }, @@ -9485,7 +8523,6 @@ "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*" } @@ -9813,6 +8850,13 @@ "@types/d3-selection": "*" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -9826,7 +8870,6 @@ "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -9917,19 +8960,19 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.2.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz", - "integrity": "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==", + "version": "25.3.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.5.tgz", + "integrity": "sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==", "devOptional": true, - "peer": true, + "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~7.18.0" } }, "node_modules/@types/nodemailer": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.9.tgz", - "integrity": "sha512-vI8oF1M+8JvQhsId0Pc38BdUP2evenIIys7c7p+9OZXSPOH5c1dyINP1jT8xQ2xPuBUXmIC87s+91IZMDjH8Ow==", + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.11.tgz", + "integrity": "sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==", "dev": true, "license": "MIT", "dependencies": { @@ -9944,12 +8987,11 @@ "license": "MIT" }, "node_modules/@types/pg": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.16.0.tgz", - "integrity": "sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.18.0.tgz", + "integrity": "sha512-gT+oueVQkqnj6ajGJXblFR4iavIXWsGAFCk3dP4Kki5+a9R4NMt0JARdk6s8cUKcfUoqP5dAtDSLU8xYUTFV+Q==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@types/node": "*", "pg-protocol": "*", @@ -9975,7 +9017,6 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -9986,7 +9027,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -10073,7 +9113,8 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT", - "optional": true + "optional": true, + "peer": true }, "node_modules/@types/ws": { "version": "8.18.1", @@ -10103,16 +9144,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.55.0.tgz", - "integrity": "sha512-1y/MVSz0NglV1ijHC8OT49mPJ4qhPYjiK08YUQVbIOyu+5k862LKUHFkpKHWu//zmr7hDR2rhwUm6gnCGNmGBQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.56.1.tgz", + "integrity": "sha512-Jz9ZztpB37dNC+HU2HI28Bs9QXpzCz+y/twHOwhyrIRdbuVDxSytJNDl6z/aAKlaRIwC7y8wJdkBv7FxYGgi0A==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.55.0", - "@typescript-eslint/type-utils": "8.55.0", - "@typescript-eslint/utils": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/type-utils": "8.56.1", + "@typescript-eslint/utils": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" @@ -10125,8 +9167,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.55.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.56.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -10135,21 +9177,22 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.55.0.tgz", - "integrity": "sha512-4z2nCSBfVIMnbuu8uinj+f0o4qOeggYJLbjpPHka3KH1om7e+H9yLKTYgksTaHcGco+NClhhY2vyO3HsMH1RGw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.56.1.tgz", + "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.55.0", - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/typescript-estree": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0", + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3" }, "engines": { @@ -10160,18 +9203,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.55.0.tgz", - "integrity": "sha512-zRcVVPFUYWa3kNnjaZGXSu3xkKV1zXy8M4nO/pElzQhFweb7PPtluDLQtKArEOGmjXoRjnUZ29NjOiF0eCDkcQ==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.56.1.tgz", + "integrity": "sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.55.0", - "@typescript-eslint/types": "^8.55.0", + "@typescript-eslint/tsconfig-utils": "^8.56.1", + "@typescript-eslint/types": "^8.56.1", "debug": "^4.4.3" }, "engines": { @@ -10186,13 +9230,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.55.0.tgz", - "integrity": "sha512-fVu5Omrd3jeqeQLiB9f1YsuK/iHFOwb04bCtY4BSCLgjNbOD33ZdV6KyEqplHr+IlpgT0QTZ/iJ+wT7hvTx49Q==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.56.1.tgz", + "integrity": "sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0" + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10203,10 +9248,11 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.55.0.tgz", - "integrity": "sha512-1R9cXqY7RQd7WuqSN47PK9EDpgFUK3VqdmbYrvWJZYDd0cavROGn+74ktWBlmJ13NXUQKlZ/iAEQHI/V0kKe0Q==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.56.1.tgz", + "integrity": "sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -10219,14 +9265,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.55.0.tgz", - "integrity": "sha512-x1iH2unH4qAt6I37I2CGlsNs+B9WGxurP2uyZLRz6UJoZWDBx9cJL1xVN/FiOmHEONEg6RIufdvyT0TEYIgC5g==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.56.1.tgz", + "integrity": "sha512-yB/7dxi7MgTtGhZdaHCemf7PuwrHMenHjmzgUW1aJpO+bBU43OycnM3Wn+DdvDO/8zzA9HlhaJ0AUGuvri4oGg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/typescript-estree": "8.55.0", - "@typescript-eslint/utils": "8.55.0", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, @@ -10238,15 +9285,16 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.55.0.tgz", - "integrity": "sha512-ujT0Je8GI5BJWi+/mMoR0wxwVEQaxM+pi30xuMiJETlX80OPovb2p9E8ss87gnSVtYXtJoU9U1Cowcr6w2FE0w==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.56.1.tgz", + "integrity": "sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==", "dev": true, + "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -10256,17 +9304,18 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.55.0.tgz", - "integrity": "sha512-EwrH67bSWdx/3aRQhCoxDaHM+CrZjotc2UCCpEDVqfCE+7OjKAGWNY2HsCSTEVvWH2clYQK8pdeLp42EVs+xQw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.56.1.tgz", + "integrity": "sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.55.0", - "@typescript-eslint/tsconfig-utils": "8.55.0", - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/visitor-keys": "8.55.0", + "@typescript-eslint/project-service": "8.56.1", + "@typescript-eslint/tsconfig-utils": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/visitor-keys": "8.56.1", "debug": "^4.4.3", - "minimatch": "^9.0.5", + "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.4.0" @@ -10282,40 +9331,17 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "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": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/@typescript-eslint/utils": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.55.0.tgz", - "integrity": "sha512-BqZEsnPGdYpgyEIkDC1BadNY8oMwckftxBT+C8W0g1iKPdeqKZBtTfnvcq0nf60u7MkjFO8RBvpRGZBPw4L2ow==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.56.1.tgz", + "integrity": "sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.55.0", - "@typescript-eslint/types": "8.55.0", - "@typescript-eslint/typescript-estree": "8.55.0" + "@typescript-eslint/scope-manager": "8.56.1", + "@typescript-eslint/types": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10325,18 +9351,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.55.0.tgz", - "integrity": "sha512-AxNRwEie8Nn4eFS1FzDMJWIISMGoXMb037sgCBJ3UR6o0fQTzr2tqN9WT+DkWJPhIdQCfV7T6D387566VtnCJA==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.56.1.tgz", + "integrity": "sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.55.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.56.1", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -10629,12 +9656,11 @@ } }, "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -10653,9 +9679,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -10723,22 +9749,6 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -11064,9 +10074,9 @@ } }, "node_modules/axe-core": { - "version": "4.11.0", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.0.tgz", - "integrity": "sha512-ilYanEU8vxxBexpJd8cWM4ElSQq4QctCLKih0TSfjIfCQTeyH/6zVrmIJfLPrKTKJRbiG+cfnZbQIjAlJmF1jQ==", + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.1.tgz", + "integrity": "sha512-BASOg+YwO2C+346x3LZOeoovTIoTrRqEsqMa6fmfAV0P+U9mFr9NsyOEpiYvFjbc64NMrSswhV50WdXzdb/Z5A==", "dev": true, "license": "MPL-2.0", "engines": { @@ -11099,17 +10109,18 @@ "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/types": "^7.26.0" } }, "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } }, "node_modules/base64-js": { "version": "1.5.1", @@ -11142,13 +10153,16 @@ } }, "node_modules/baseline-browser-mapping": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.4.tgz", - "integrity": "sha512-ZCQ9GEWl73BVm8bu5Fts8nt7MHdbt5vY9bP6WGnUh+r3l8M7CgfyTlwsgCbMC66BNxPr6Xoce3j66Ms5YUQTNA==", + "version": "2.10.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", + "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", "dev": true, "license": "Apache-2.0", "bin": { - "baseline-browser-mapping": "dist/cli.js" + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/bcrypt-pbkdf": { @@ -11166,7 +10180,6 @@ "integrity": "sha512-Ba0KR+Fzxh2jDRhdg6TSH0SJGzb8C0aBY4hR8w8madIdIzzC6Y1+kx5qR6eS1Z+Gy20h6ZU28aeyg0z1VIrShQ==", "hasInstallScript": true, "license": "MIT", - "peer": true, "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" @@ -11248,17 +10261,19 @@ "node_modules/bowser": { "version": "2.14.1", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==" + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "dev": true, + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -11293,7 +10308,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", @@ -11402,16 +10416,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/caniuse-lite": { "version": "1.0.30001759", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", @@ -11442,23 +10446,6 @@ "url": "https://www.paypal.me/kirilvatev" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -11644,26 +10631,6 @@ "node": ">=18" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, - "license": "MIT" - }, "node_modules/color-string": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.4.tgz", @@ -12247,7 +11214,6 @@ "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "license": "ISC", - "peer": true, "engines": { "node": ">=12" } @@ -12684,10 +11650,14 @@ } }, "node_modules/dompurify": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.7.tgz", - "integrity": "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz", + "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==", "license": "(MPL-2.0 OR Apache-2.0)", + "peer": true, + "engines": { + "node": ">=20" + }, "optionalDependencies": { "@types/trusted-types": "^2.0.7" } @@ -12736,504 +11706,21 @@ } }, "node_modules/drizzle-kit": { - "version": "0.31.9", - "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.9.tgz", - "integrity": "sha512-GViD3IgsXn7trFyBUUHyTFBpH/FsHTxYJ66qdbVggxef4UBPHRYxQaRzYLTuekYnk9i5FIEL9pbBIwMqX/Uwrg==", + "version": "0.31.10", + "resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.10.tgz", + "integrity": "sha512-7OZcmQUrdGI+DUNNsKBn1aW8qSoKuTH7d0mYgSP8bAzdFzKoovxEFnoGQp2dVs82EOJeYycqRtciopszwUf8bw==", "dev": true, + "license": "MIT", "dependencies": { "@drizzle-team/brocli": "^0.10.2", "@esbuild-kit/esm-loader": "^2.5.5", "esbuild": "^0.25.4", - "esbuild-register": "^3.5.0" + "tsx": "^4.21.0" }, "bin": { "drizzle-kit": "bin.cjs" } }, - "node_modules/drizzle-kit/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/drizzle-kit/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, "node_modules/drizzle-orm": { "version": "0.45.1", "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.1.tgz", @@ -13417,9 +11904,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.266", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.266.tgz", - "integrity": "sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==", + "version": "1.5.321", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz", + "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==", "dev": true, "license": "ISC" }, @@ -13583,14 +12070,14 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", "dev": true, "license": "MIT", "dependencies": { "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" + "tapable": "^2.3.0" }, "engines": { "node": ">=10.13.0" @@ -13622,9 +12109,9 @@ } }, "node_modules/es-abstract": { - "version": "1.24.0", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.0.tgz", - "integrity": "sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { @@ -13709,27 +12196,28 @@ } }, "node_modules/es-iterator-helpers": { - "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==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", + "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.24.1", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0", "safe-array-concat": "^1.1.3" }, "engines": { @@ -13801,7 +12289,6 @@ "dev": true, "hasInstallScript": true, "license": "MIT", - "peer": true, "bin": { "esbuild": "bin/esbuild" }, @@ -13853,19 +12340,6 @@ "esbuild": "0.12 - 0.27" } }, - "node_modules/esbuild-register": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz", - "integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==", - "dev": true, - "license": "MIT", - "dependencies": { - "debug": "^4.3.4" - }, - "peerDependencies": { - "esbuild": ">=0.12 <1" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -13895,34 +12369,30 @@ } }, "node_modules/eslint": { - "version": "9.39.2", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", - "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.0.3.tgz", + "integrity": "sha512-COV33RzXZkqhG9P2rZCFl9ZmJ7WL+gQSCRzE7RhkbclbQPtLAWReL7ysA0Sh4c8Im2U9ynybdR56PV0XcKvqaQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.1", - "@eslint/config-helpers": "^0.4.2", - "@eslint/core": "^0.17.0", - "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.2", - "@eslint/plugin-kit": "^0.4.1", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.2", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.12.4", - "chalk": "^4.0.0", + "ajv": "^6.14.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^8.4.0", - "eslint-visitor-keys": "^4.2.1", - "espree": "^10.4.0", - "esquery": "^1.5.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.1.1", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -13932,8 +12402,7 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -13941,7 +12410,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://eslint.org/donate" @@ -13956,13 +12425,13 @@ } }, "node_modules/eslint-config-next": { - "version": "16.1.6", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.6.tgz", - "integrity": "sha512-vKq40io2B0XtkkNDYyleATwblNt8xuh3FWp8SpSz3pt7P01OkBFlKsJZ2mWt5WsCySlDQLckb1zMY9yE9Qy0LA==", + "version": "16.1.7", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.1.7.tgz", + "integrity": "sha512-FTq1i/QDltzq+zf9aB/cKWAiZ77baG0V7h8dRQh3thVx7I4dwr6ZXQrWKAaTB7x5VwVXlzoUTyMLIVQPLj2gJg==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "16.1.6", + "@next/eslint-plugin-next": "16.1.7", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", @@ -13982,42 +12451,25 @@ } } }, - "node_modules/eslint-config-next/node_modules/globals": { - "version": "16.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", - "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "node_modules/eslint-config-next/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, - "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==", + "node_modules/eslint-config-next/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { - "debug": "^3.2.7", - "is-core-module": "^2.13.0", - "resolve": "^1.22.4" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-import-resolver-typescript": { + "node_modules/eslint-config-next/node_modules/eslint-import-resolver-typescript": { "version": "3.10.1", "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.10.1.tgz", "integrity": "sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==", @@ -14052,41 +12504,12 @@ } } }, - "node_modules/eslint-module-utils": { - "version": "2.12.1", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", - "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", - "dev": true, - "license": "MIT", - "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==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.1" - } - }, - "node_modules/eslint-plugin-import": { + "node_modules/eslint-config-next/node_modules/eslint-plugin-import": { "version": "2.32.0", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", @@ -14115,7 +12538,7 @@ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, - "node_modules/eslint-plugin-import/node_modules/debug": { + "node_modules/eslint-config-next/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==", @@ -14125,17 +12548,7 @@ "ms": "^2.1.1" } }, - "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==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/eslint-plugin-jsx-a11y": { + "node_modules/eslint-config-next/node_modules/eslint-plugin-jsx-a11y": { "version": "6.10.2", "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", @@ -14165,7 +12578,7 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, - "node_modules/eslint-plugin-react": { + "node_modules/eslint-config-next/node_modules/eslint-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", @@ -14198,7 +12611,7 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, - "node_modules/eslint-plugin-react-hooks": { + "node_modules/eslint-config-next/node_modules/eslint-plugin-react-hooks": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", @@ -14218,7 +12631,67 @@ "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-hooks/node_modules/zod-validation-error": { + "node_modules/eslint-config-next/node_modules/globals": { + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-config-next/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/eslint-config-next/node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-config-next/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-config-next/node_modules/zod-validation-error": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", @@ -14231,86 +12704,110 @@ "zod": "^3.25.0 || ^4.0.0" } }, - "node_modules/eslint-plugin-react/node_modules/resolve": { - "version": "2.0.0-next.5", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz", - "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==", + "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==", "dev": true, "license": "MIT", "dependencies": { + "debug": "^3.2.7", "is-core-module": "^2.13.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "resolve": "^1.22.4" } }, - "node_modules/eslint-plugin-react/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "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==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" } }, "node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.15.0", + "acorn": "^8.16.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" + "eslint-visitor-keys": "^5.0.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -14406,7 +12903,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -14446,12 +12942,12 @@ } }, "node_modules/express-rate-limit": { - "version": "8.2.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.2.1.tgz", - "integrity": "sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==", + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.0.tgz", + "integrity": "sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==", "license": "MIT", "dependencies": { - "ip-address": "10.0.1" + "ip-address": "10.1.0" }, "engines": { "node": ">= 16" @@ -14571,18 +13067,36 @@ ], "license": "BSD-3-Clause" }, - "node_modules/fast-xml-parser": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.3.4.tgz", - "integrity": "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA==", + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } ], + "license": "MIT", "dependencies": { - "strnum": "^2.1.0" + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.6", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.6.tgz", + "integrity": "sha512-3+fdZyBRVg29n4rXP0joHthhcHdPUHaIC16cuyyd1iLsuaO6Vea36MPrxgAzbZna8lhvZeRL8Bc9GP56/J9xEw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.1.3", + "strnum": "^2.1.2" }, "bin": { "fxparser": "src/cli/cli.js" @@ -14763,36 +13277,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/foreground-child": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", - "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cross-spawn": "^7.0.6", - "signal-exit": "^4.0.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/foreground-child/node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -15063,16 +13547,17 @@ "license": "MIT" }, "node_modules/glob": { - "version": "13.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.3.tgz", - "integrity": "sha512-/g3B0mC+4x724v1TgtBlBtt2hPi/EWptsIAmXUx9Z2rvBYleQcsrmaOzd5LyL50jf/Soi83ZDJmw2+XqvH/EeA==", + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "license": "BlueOak-1.0.0", "dependencies": { - "minimatch": "^10.2.0", - "minipass": "^7.1.2", - "path-scurry": "^2.0.0" + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -15091,42 +13576,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/balanced-match": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.2.tgz", - "integrity": "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg==", - "dependencies": { - "jackspeak": "^4.2.3" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.2.tgz", - "integrity": "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.0.tgz", - "integrity": "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w==", - "dependencies": { - "brace-expansion": "^5.0.2" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/globals": { "version": "11.12.0", "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", @@ -15207,16 +13656,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/has-property-descriptors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", @@ -15389,9 +13828,9 @@ } }, "node_modules/icu-minify": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.8.2.tgz", - "integrity": "sha512-LHBQV+skKkjZSPd590pZ7ZAHftUgda3eFjeuNwA8/15L8T8loCNBktKQyTlkodAU86KovFXeg/9WntlAo5wA5A==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/icu-minify/-/icu-minify-4.8.3.tgz", + "integrity": "sha512-65Av7FLosNk7bPbmQx5z5XG2Y3T2GFppcjiXh4z1idHeVgQxlDpAmkGoYI0eFzAvrOnjpWTL5FmPDhsdfRMPEA==", "funding": [ { "type": "individual", @@ -15433,23 +13872,6 @@ "node": ">= 4" } }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -15519,11 +13941,12 @@ } }, "node_modules/ioredis": { - "version": "5.9.3", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.9.3.tgz", - "integrity": "sha512-VI5tMCdeoxZWU5vjHWsiE/Su76JGhBvWF1MJnV9ZtGltHk9BmD48oDq8Tj8haZ85aceXZMxLNDQZRVo5QKNgXA==", + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.0.tgz", + "integrity": "sha512-HVBe9OFuqs+Z6n64q09PQvP1/R4Bm+30PAyyD4wIEqssh3v9L21QjCVk4kRLucMBcDokJTcLjsGeVRlq/nH6DA==", + "license": "MIT", "dependencies": { - "@ioredis/commands": "1.5.0", + "@ioredis/commands": "1.5.1", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -15542,9 +13965,9 @@ } }, "node_modules/ip-address": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz", - "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", "license": "MIT", "engines": { "node": ">= 12" @@ -16024,20 +14447,6 @@ "node": ">= 0.4" } }, - "node_modules/jackspeak": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", - "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", - "dependencies": { - "@isaacs/cliui": "^9.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -16264,9 +14673,9 @@ } }, "node_modules/lightningcss": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.2.tgz", - "integrity": "sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -16280,23 +14689,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.30.2", - "lightningcss-darwin-arm64": "1.30.2", - "lightningcss-darwin-x64": "1.30.2", - "lightningcss-freebsd-x64": "1.30.2", - "lightningcss-linux-arm-gnueabihf": "1.30.2", - "lightningcss-linux-arm64-gnu": "1.30.2", - "lightningcss-linux-arm64-musl": "1.30.2", - "lightningcss-linux-x64-gnu": "1.30.2", - "lightningcss-linux-x64-musl": "1.30.2", - "lightningcss-win32-arm64-msvc": "1.30.2", - "lightningcss-win32-x64-msvc": "1.30.2" + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.30.2.tgz", - "integrity": "sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", "cpu": [ "arm64" ], @@ -16315,9 +14724,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.2.tgz", - "integrity": "sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", "cpu": [ "arm64" ], @@ -16336,9 +14745,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.2.tgz", - "integrity": "sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", "cpu": [ "x64" ], @@ -16357,9 +14766,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.2.tgz", - "integrity": "sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", "cpu": [ "x64" ], @@ -16378,9 +14787,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.2.tgz", - "integrity": "sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", "cpu": [ "arm" ], @@ -16399,9 +14808,9 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.2.tgz", - "integrity": "sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", "cpu": [ "arm64" ], @@ -16420,9 +14829,9 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.2.tgz", - "integrity": "sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", "cpu": [ "arm64" ], @@ -16441,9 +14850,9 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.2.tgz", - "integrity": "sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", "cpu": [ "x64" ], @@ -16462,9 +14871,9 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.2.tgz", - "integrity": "sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", "cpu": [ "x64" ], @@ -16483,9 +14892,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.2.tgz", - "integrity": "sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", "cpu": [ "arm64" ], @@ -16504,9 +14913,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.30.2", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.2.tgz", - "integrity": "sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==", + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", "cpu": [ "x64" ], @@ -16541,9 +14950,9 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, "node_modules/lodash.defaults": { @@ -16594,13 +15003,6 @@ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.once": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", @@ -16647,9 +15049,9 @@ } }, "node_modules/lucide-react": { - "version": "0.563.0", - "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz", - "integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==", + "version": "0.577.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.577.0.tgz", + "integrity": "sha512-4LjoFv2eEPwYDPg/CUdBJQSDfPyzXCRrVW1X7jrx/trgxnxkHFjnVZINbzvzxjN70dxychOfg+FTYwBiS3pQ5A==", "license": "ISC", "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -16856,16 +15258,18 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minimist": { @@ -16878,10 +15282,10 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", - "license": "ISC", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -16916,6 +15320,7 @@ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz", "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==", "license": "MIT", + "peer": true, "dependencies": { "dompurify": "3.2.7", "marked": "14.0.0" @@ -16926,6 +15331,7 @@ "resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz", "integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==", "license": "MIT", + "peer": true, "bin": { "marked": "bin/marked.js" }, @@ -17013,7 +15419,6 @@ "version": "15.5.12", "resolved": "https://registry.npmjs.org/next/-/next-15.5.12.tgz", "integrity": "sha512-Fi/wQ4Etlrn60rz78bebG1i1SR20QxvV8tVp6iJspjLUSHcZoeUXCt+vmWoEcza85ElZzExK/jJ/F6SvtGktjA==", - "peer": true, "dependencies": { "@next/env": "15.5.12", "@swc/helpers": "0.5.15", @@ -17062,9 +15467,9 @@ } }, "node_modules/next-intl": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.8.2.tgz", - "integrity": "sha512-GuuwyvyEI49/oehQbBXEoY8KSIYCzmfMLhmIwhMXTb+yeBmly1PnJcpgph3KczQ+HTJMXwXCmkizgtT8jBMf3A==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.8.3.tgz", + "integrity": "sha512-PvdBDWg+Leh7BR7GJUQbCDVVaBRn37GwDBWc9sv0rVQOJDQ5JU1rVzx9EEGuOGYo0DHAl70++9LQ7HxTawdL7w==", "funding": [ { "type": "individual", @@ -17073,14 +15478,14 @@ ], "license": "MIT", "dependencies": { - "@formatjs/intl-localematcher": "^0.5.4", + "@formatjs/intl-localematcher": "^0.8.1", "@parcel/watcher": "^2.4.1", "@swc/core": "^1.15.2", - "icu-minify": "^4.8.2", + "icu-minify": "^4.8.3", "negotiator": "^1.0.0", - "next-intl-swc-plugin-extractor": "^4.8.2", + "next-intl-swc-plugin-extractor": "^4.8.3", "po-parser": "^2.1.1", - "use-intl": "^4.8.2" + "use-intl": "^4.8.3" }, "peerDependencies": { "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0", @@ -17094,9 +15499,9 @@ } }, "node_modules/next-intl-swc-plugin-extractor": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.8.2.tgz", - "integrity": "sha512-sHDs36L1VZmFHj3tPHsD+KZJtnsRudHlNvT0ieIe3iFVn5OpGLTxW3d/Zc/2LXSj5GpGuR6wQeikbhFjU9tMQQ==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/next-intl-swc-plugin-extractor/-/next-intl-swc-plugin-extractor-4.8.3.tgz", + "integrity": "sha512-YcaT+R9z69XkGhpDarVFWUprrCMbxgIQYPUaXoE6LGVnLjGdo8hu3gL6bramDVjNKViYY8a/pXPy7Bna0mXORg==", "license": "MIT" }, "node_modules/next-themes": { @@ -17194,10 +15599,39 @@ "node": ">= 8.0.0" } }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/node-releases": { - "version": "2.0.27", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", - "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "dev": true, "license": "MIT" }, @@ -17814,26 +16248,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/package-json-from-dist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", - "dev": true, - "license": "BlueOak-1.0.0" - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parseley": { "version": "0.12.1", "resolved": "https://registry.npmjs.org/parseley/-/parseley-0.12.1.tgz", @@ -17866,6 +16280,21 @@ "node": ">=8" } }, + "node_modules/path-expression-matcher": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.1.3.tgz", + "integrity": "sha512-qdVgY8KXmVdJZRSS1JdEPOKPdTiEK/pi0RkcT2sw1RhXxohdujUlJFPuS1TSkevZ9vzd3ZlL7ULl1MHGTApKzQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -17883,16 +16312,16 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz", - "integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", "license": "BlueOak-1.0.0", "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" }, "engines": { - "node": "20 || >=22" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -17944,15 +16373,14 @@ } }, "node_modules/pg": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.18.0.tgz", - "integrity": "sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz", + "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==", "license": "MIT", - "peer": true, "dependencies": { - "pg-connection-string": "^2.11.0", - "pg-pool": "^3.11.0", - "pg-protocol": "^1.11.0", + "pg-connection-string": "^2.12.0", + "pg-pool": "^3.13.0", + "pg-protocol": "^1.13.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, @@ -17979,9 +16407,9 @@ "optional": true }, "node_modules/pg-connection-string": { - "version": "2.11.0", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.11.0.tgz", - "integrity": "sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==", + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz", + "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==", "license": "MIT" }, "node_modules/pg-int8": { @@ -17994,18 +16422,18 @@ } }, "node_modules/pg-pool": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.11.0.tgz", - "integrity": "sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz", + "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==", "license": "MIT", "peerDependencies": { "pg": ">=8.0" } }, "node_modules/pg-protocol": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.11.0.tgz", - "integrity": "sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz", + "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==", "license": "MIT" }, "node_modules/pg-types": { @@ -18096,12 +16524,13 @@ "node_modules/postal-mime": { "version": "2.7.3", "resolved": "https://registry.npmjs.org/postal-mime/-/postal-mime-2.7.3.tgz", - "integrity": "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==" + "integrity": "sha512-MjhXadAJaWgYzevi46+3kLak8y6gbg0ku14O1gO/LNOuay8dO+1PtcSGvAdgDR0DoIsSaiIA8y/Ddw6MnrO0Tw==", + "license": "MIT-0" }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", "dev": true, "funding": [ { @@ -18167,14 +16596,23 @@ } }, "node_modules/posthog-node": { - "version": "5.24.15", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.24.15.tgz", - "integrity": "sha512-0QnWVOZAPwEAlp+r3r0jIGfk2IaNYM/2YnEJJhBMJZXs4LpHcTu7mX42l+e95o9xX87YpVuZU0kOkmtQUxgnOA==", + "version": "5.28.0", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-5.28.0.tgz", + "integrity": "sha512-EETYV0zA+7BLQmXzY+vGyDMoQK8uHf8f/1utbRjKncI41gPkw+4piGP7l4UT5Luld+4vQpJPOR1q1YrbXm7XjQ==", + "license": "MIT", "dependencies": { - "@posthog/core": "1.22.0" + "@posthog/core": "1.23.2" }, "engines": { "node": "^20.20.0 || >=22.22.0" + }, + "peerDependencies": { + "rxjs": "^7.0.0" + }, + "peerDependenciesMeta": { + "rxjs": { + "optional": true + } } }, "node_modules/prebuild-install": { @@ -18329,9 +16767,9 @@ } }, "node_modules/qs": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", - "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==", + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==", "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" @@ -18443,19 +16881,20 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } }, "node_modules/react-day-picker": { - "version": "9.13.2", - "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.13.2.tgz", - "integrity": "sha512-IMPiXfXVIAuR5Yk58DDPBC8QKClrhdXV+Tr/alBrwrHUw0qDDYB1m5zPNuTnnPIr/gmJ4ChMxmtqPdxm8+R4Eg==", + "version": "9.14.0", + "resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.14.0.tgz", + "integrity": "sha512-tBaoDWjPwe0M5pGrum4H0SR6Lyk+BO9oHnp9JbKpGKW2mlraNPgP9BMfsg5pWpwrssARmeqk7YBl2oXutZTaHA==", + "license": "MIT", "dependencies": { "@date-fns/tz": "^1.4.1", + "@tabby_ai/hijri-converter": "1.0.5", "date-fns": "^4.1.0", - "date-fns-jalali": "^4.1.0-0" + "date-fns-jalali": "4.1.0-0" }, "engines": { "node": ">=18" @@ -18473,7 +16912,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -18498,20 +16936,20 @@ } }, "node_modules/react-email": { - "version": "5.2.8", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-5.2.8.tgz", - "integrity": "sha512-noPcnpl78vsyBnhiKCzxK9Mdsv7ncAYI80osS5kbMgaKH2IgPtPab5BzLJX6INXuiNk5ju+9YRnCjPoPTOHZjA==", + "version": "5.2.10", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-5.2.10.tgz", + "integrity": "sha512-Ys8yR5/a0nXf5u2GlT2UV93PJHC3ZnuMnNebEn7I5UE9XfMFPtlpgDs02mPJOJn49fhJjDTWIUlZD1vmQPDgJg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.0", - "@babel/traverse": "^7.27.0", + "@babel/parser": "7.27.0", + "@babel/traverse": "7.27.0", "chokidar": "^4.0.3", "commander": "^13.0.0", "conf": "^15.0.2", "debounce": "^2.0.0", - "esbuild": "^0.25.0", - "glob": "^11.0.0", + "esbuild": "0.27.3", + "glob": "^13.0.6", "jiti": "2.4.2", "log-symbols": "^7.0.0", "mime-types": "^3.0.0", @@ -18523,454 +16961,12 @@ "tsconfig-paths": "4.2.0" }, "bin": { - "email": "dist/index.js" + "email": "dist/index.mjs" }, "engines": { "node": ">=20.0.0" } }, - "node_modules/react-email/node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/react-email/node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/react-email/node_modules/chalk": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", @@ -19017,72 +17013,6 @@ "dev": true, "license": "MIT" }, - "node_modules/react-email/node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" - } - }, - "node_modules/react-email/node_modules/glob": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", - "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "foreground-child": "^3.3.1", - "jackspeak": "^4.1.1", - "minimatch": "^10.1.1", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^2.0.0" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/react-email/node_modules/is-interactive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", @@ -19136,22 +17066,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-email/node_modules/minimatch": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", - "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/brace-expansion": "^5.0.0" - }, - "engines": { - "node": "20 || >=22" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/react-email/node_modules/onetime": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", @@ -19286,11 +17200,10 @@ } }, "node_modules/react-hook-form": { - "version": "7.71.1", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.1.tgz", - "integrity": "sha512-9SUJKCGKo8HUSsCO+y0CtqkqI5nNuaDqTxyqPsZPqIwudpj4rCrAz/jZV+jn57bx5gtZKOh3neQu94DXMc+w5w==", + "version": "7.71.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", + "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -19303,9 +17216,9 @@ } }, "node_modules/react-icons": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz", - "integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.6.0.tgz", + "integrity": "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA==", "license": "MIT", "peerDependencies": { "react": "*" @@ -19554,11 +17467,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/reo-census": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/reo-census/-/reo-census-1.2.8.tgz", + "integrity": "sha512-UMwpNwOieUTeymIITWCbo0In0FHGWZwnXIIYphpCPO/Bjl5z/385DWLnTxBcfFFCH/r/fEq/TZ0ivlPb6Smi9Q==", + "hasInstallScript": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/reodotdev": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/reodotdev/-/reodotdev-1.0.0.tgz", - "integrity": "sha512-wXe1vJucZjrhQL0SxOL9EvmJrtbMCIEGMdZX5lj/57n2T3UhBHZsAcM5TQASJ0T6ZBbrETRnMhH33bsbJeRO6Q==", - "license": "MIT" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reodotdev/-/reodotdev-1.1.0.tgz", + "integrity": "sha512-BeIlYk59p4Gw+zPHJj249xPBQ0wHfI8NsksVFRTdPLkPXDSYyn6IBvbR0s7pELK9qk3p79UBcBWP84IsYLsvbg==", + "license": "MIT", + "dependencies": { + "reo-census": "^1.2.6" + } }, "node_modules/require-from-string": { "version": "2.0.2", @@ -19574,6 +17500,7 @@ "version": "6.9.2", "resolved": "https://registry.npmjs.org/resend/-/resend-6.9.2.tgz", "integrity": "sha512-uIM6CQ08tS+hTCRuKBFbOBvHIGaEhqZe8s4FOgqsVXSbQLAhmNWpmUhG3UAtRnmcwTWFUqnHa/+Vux8YGPyDBA==", + "license": "MIT", "dependencies": { "postal-mime": "2.7.3", "svix": "1.84.1" @@ -19611,16 +17538,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/resolve-pkg-maps": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", @@ -20587,9 +18504,9 @@ } }, "node_modules/stripe": { - "version": "20.3.1", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.3.1.tgz", - "integrity": "sha512-k990yOT5G5rhX3XluRPw5Y8RLdJDW4dzQ29wWT66piHrbnM2KyamJ1dKgPsw4HzGHRWjDiSSdcI2WdxQUPV3aQ==", + "version": "20.4.1", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.4.1.tgz", + "integrity": "sha512-axCguHItc8Sxt0HC6aSkdVRPffjYPV7EQqZRb2GkIa8FzWDycE7nHJM19C6xAIynH1Qp1/BHiopSi96jGBxT0w==", "license": "MIT", "engines": { "node": ">=16" @@ -20604,15 +18521,16 @@ } }, "node_modules/strnum": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.2.tgz", - "integrity": "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz", + "integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==", "funding": [ { "type": "github", "url": "https://github.com/sponsors/NaturalIntelligence" } - ] + ], + "license": "MIT" }, "node_modules/stubborn-fs": { "version": "2.0.0", @@ -20654,19 +18572,6 @@ } } }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -20747,9 +18652,9 @@ } }, "node_modules/tailwind-merge": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", - "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.5.0.tgz", + "integrity": "sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==", "license": "MIT", "funding": { "type": "github", @@ -20757,11 +18662,10 @@ } }, "node_modules/tailwindcss": { - "version": "4.1.18", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.18.tgz", - "integrity": "sha512-4+Z+0yiYyEtUVCScyfHCxOYP06L5Ne+JiHhY2IjR2KWMIWhJOYZKLSGZaP5HkZ8+bY0cxfzwDE5uOmzFXyIwxw==", - "license": "MIT", - "peer": true + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz", + "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==", + "license": "MIT" }, "node_modules/tapable": { "version": "2.3.0", @@ -20908,6 +18812,7 @@ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18.12" }, @@ -21235,7 +19140,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -21245,15 +19149,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.55.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.55.0.tgz", - "integrity": "sha512-HE4wj+r5lmDVS9gdaN0/+iqNvPZwGfnJ5lZuz7s5vLlg9ODw0bIiiETaios9LvFI1U94/VBXGm3CB2Y5cNFMpw==", + "version": "8.56.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.56.1.tgz", + "integrity": "sha512-U4lM6pjmBX7J5wk4szltF7I1cGBHXZopnAXCMXb3+fZ3B/0Z3hq3wS/CCUB2NZBNAExK92mCU2tEohWuwVMsDQ==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.55.0", - "@typescript-eslint/parser": "8.55.0", - "@typescript-eslint/typescript-estree": "8.55.0", - "@typescript-eslint/utils": "8.55.0" + "@typescript-eslint/eslint-plugin": "8.56.1", + "@typescript-eslint/parser": "8.56.1", + "@typescript-eslint/typescript-estree": "8.56.1", + "@typescript-eslint/utils": "8.56.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -21263,7 +19168,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -21300,9 +19205,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "devOptional": true, "license": "MIT" }, @@ -21351,9 +19256,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", - "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -21425,9 +19330,9 @@ } }, "node_modules/use-intl": { - "version": "4.8.2", - "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.2.tgz", - "integrity": "sha512-3VNXZgDnPFqhIYosQ9W1Hc6K5q+ZelMfawNbexdwL/dY7BTHbceLUBX5Eeex9lgogxTp0pf1SjHuhYNAjr9H3g==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.8.3.tgz", + "integrity": "sha512-nLxlC/RH+le6g3amA508Itnn/00mE+J22ui21QhOWo5V9hCEC43+WtnRAITbJW0ztVZphev5X9gvOf2/Dk9PLA==", "funding": [ { "type": "individual", @@ -21438,7 +19343,7 @@ "dependencies": { "@formatjs/fast-memoize": "^3.1.0", "@schummar/icu-type-parser": "1.21.5", - "icu-minify": "^4.8.2", + "icu-minify": "^4.8.3", "intl-messageformat": "^11.1.0" }, "peerDependencies": { @@ -21662,7 +19567,6 @@ "resolved": "https://registry.npmjs.org/winston/-/winston-3.19.0.tgz", "integrity": "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA==", "license": "MIT", - "peer": true, "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", @@ -21869,7 +19773,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index f7ac6fbce..05ae3b49f 100644 --- a/package.json +++ b/package.json @@ -32,8 +32,8 @@ "format": "prettier --write ." }, "dependencies": { - "@asteasolutions/zod-to-openapi": "8.4.0", - "@aws-sdk/client-s3": "3.989.0", + "@asteasolutions/zod-to-openapi": "8.4.1", + "@aws-sdk/client-s3": "3.1011.0", "@faker-js/faker": "10.3.0", "@headlessui/react": "2.2.9", "@hookform/resolvers": "5.2.2", @@ -59,11 +59,11 @@ "@radix-ui/react-tabs": "1.1.13", "@radix-ui/react-toast": "1.2.15", "@radix-ui/react-tooltip": "1.2.8", - "@react-email/components": "1.0.7", + "@react-email/components": "1.0.8", "@react-email/render": "2.0.4", - "@react-email/tailwind": "2.0.4", - "@simplewebauthn/browser": "13.2.2", - "@simplewebauthn/server": "13.2.2", + "@react-email/tailwind": "2.0.5", + "@simplewebauthn/browser": "13.3.0", + "@simplewebauthn/server": "13.3.0", "@tailwindcss/forms": "0.5.11", "@tanstack/react-query": "5.90.21", "@tanstack/react-table": "8.21.3", @@ -80,42 +80,42 @@ "d3": "7.9.0", "drizzle-orm": "0.45.1", "express": "5.2.1", - "express-rate-limit": "8.2.1", - "glob": "13.0.3", + "express-rate-limit": "8.3.0", + "glob": "13.0.6", "helmet": "8.1.0", "http-errors": "2.0.1", "input-otp": "1.4.2", - "ioredis": "5.9.3", + "ioredis": "5.10.0", "jmespath": "0.16.0", "js-yaml": "4.1.1", "jsonwebtoken": "9.0.3", - "lucide-react": "0.563.0", + "lucide-react": "0.577.0", "maxmind": "5.0.5", "moment": "2.30.1", "next": "15.5.12", - "next-intl": "4.8.2", + "next-intl": "4.8.3", "next-themes": "0.4.6", "nextjs-toploader": "3.9.17", "node-cache": "5.1.2", "nodemailer": "8.0.1", "oslo": "1.2.1", - "pg": "8.18.0", - "posthog-node": "5.24.15", + "pg": "8.20.0", + "posthog-node": "5.28.0", "qrcode.react": "4.2.0", "react": "19.2.4", - "react-day-picker": "9.13.2", + "react-day-picker": "9.14.0", "react-dom": "19.2.4", "react-easy-sort": "1.8.0", - "react-hook-form": "7.71.1", - "react-icons": "5.5.0", + "react-hook-form": "7.71.2", + "react-icons": "5.6.0", "recharts": "2.15.4", - "reodotdev": "1.0.0", + "reodotdev": "1.1.0", "resend": "6.9.2", "semver": "7.7.4", "sshpk": "^1.18.0", - "stripe": "20.3.1", + "stripe": "20.4.1", "swagger-ui-express": "5.0.1", - "tailwind-merge": "3.4.0", + "tailwind-merge": "3.5.0", "topojson-client": "3.1.0", "tw-animate-css": "1.4.0", "use-debounce": "^10.1.0", @@ -131,10 +131,10 @@ "zod-validation-error": "5.0.0" }, "devDependencies": { - "@dotenvx/dotenvx": "1.52.0", + "@dotenvx/dotenvx": "1.54.1", "@esbuild-plugins/tsconfig-paths": "0.1.2", - "@react-email/preview-server": "5.2.8", - "@tailwindcss/postcss": "4.1.18", + "@react-email/preview-server": "5.2.10", + "@tailwindcss/postcss": "4.2.1", "@tanstack/react-query-devtools": "5.91.3", "@types/better-sqlite3": "7.6.13", "@types/cookie-parser": "1.4.10", @@ -146,10 +146,10 @@ "@types/jmespath": "0.15.2", "@types/js-yaml": "4.0.9", "@types/jsonwebtoken": "9.0.10", - "@types/node": "25.2.3", - "@types/nodemailer": "7.0.9", + "@types/node": "25.3.5", + "@types/nodemailer": "7.0.11", "@types/nprogress": "0.2.3", - "@types/pg": "8.16.0", + "@types/pg": "8.18.0", "@types/react": "19.2.14", "@types/react-dom": "19.2.3", "@types/semver": "7.7.1", @@ -159,18 +159,22 @@ "@types/ws": "8.18.1", "@types/yargs": "17.0.35", "babel-plugin-react-compiler": "1.0.0", - "drizzle-kit": "0.31.9", + "drizzle-kit": "0.31.10", "esbuild": "0.27.3", "esbuild-node-externals": "1.20.1", - "eslint": "9.39.2", - "eslint-config-next": "16.1.6", - "postcss": "8.5.6", + "eslint": "10.0.3", + "eslint-config-next": "16.1.7", + "postcss": "8.5.8", "prettier": "3.8.1", - "react-email": "5.2.8", - "tailwindcss": "4.1.18", + "react-email": "5.2.10", + "tailwindcss": "4.2.1", "tsc-alias": "1.8.16", "tsx": "4.21.0", "typescript": "5.9.3", - "typescript-eslint": "8.55.0" + "typescript-eslint": "8.56.1" + }, + "overrides": { + "esbuild": "0.27.3", + "dompurify": "3.3.2" } } diff --git a/server/auth/actions.ts b/server/auth/actions.ts index feb91560a..6a5ed15dc 100644 --- a/server/auth/actions.ts +++ b/server/auth/actions.ts @@ -19,6 +19,7 @@ export enum ActionsEnum { getSite = "getSite", listSites = "listSites", updateSite = "updateSite", + resetSiteBandwidth = "resetSiteBandwidth", reGenerateSecret = "reGenerateSecret", createResource = "createResource", deleteResource = "deleteResource", diff --git a/server/auth/sessions/resource.ts b/server/auth/sessions/resource.ts index 3b9da3d73..a1ae13373 100644 --- a/server/auth/sessions/resource.ts +++ b/server/auth/sessions/resource.ts @@ -87,7 +87,7 @@ export async function validateResourceSessionToken( if (Date.now() >= resourceSession.expiresAt) { await db .delete(resourceSessions) - .where(eq(resourceSessions.sessionId, resourceSessions.sessionId)); + .where(eq(resourceSessions.sessionId, sessionId)); return { resourceSession: null }; } else if ( Date.now() >= @@ -181,7 +181,7 @@ export function serializeResourceSessionCookie( return `${cookieName}_s.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Secure; Domain=${domain}`; } else { if (expiresAt === undefined) { - return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=$domain}`; + return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Path=/; Domain=${domain}`; } return `${cookieName}.${now}=${token}; HttpOnly; SameSite=Lax; Expires=${expiresAt.toUTCString()}; Path=/; Domain=${domain}`; } diff --git a/server/cleanup.ts b/server/cleanup.ts index e494fcdc9..137654827 100644 --- a/server/cleanup.ts +++ b/server/cleanup.ts @@ -1,6 +1,10 @@ +import { flushBandwidthToDb } from "@server/routers/newt/handleReceiveBandwidthMessage"; +import { flushSiteBandwidthToDb } from "@server/routers/gerbil/receiveBandwidth"; import { cleanup as wsCleanup } from "#dynamic/routers/ws"; async function cleanup() { + await flushBandwidthToDb(); + await flushSiteBandwidthToDb(); await wsCleanup(); process.exit(0); @@ -10,4 +14,4 @@ export async function initCleanup() { // Handle process termination process.on("SIGTERM", () => cleanup()); process.on("SIGINT", () => cleanup()); -} +} \ No newline at end of file diff --git a/server/db/pg/index.ts b/server/db/pg/index.ts index 86e318022..f8c04ac9e 100644 --- a/server/db/pg/index.ts +++ b/server/db/pg/index.ts @@ -1,4 +1,5 @@ export * from "./driver"; +export * from "./logsDriver"; export * from "./safeRead"; export * from "./schema/schema"; export * from "./schema/privateSchema"; diff --git a/server/db/pg/logsDriver.ts b/server/db/pg/logsDriver.ts new file mode 100644 index 000000000..49e26f89f --- /dev/null +++ b/server/db/pg/logsDriver.ts @@ -0,0 +1,87 @@ +import { drizzle as DrizzlePostgres } from "drizzle-orm/node-postgres"; +import { Pool } from "pg"; +import { readConfigFile } from "@server/lib/readConfigFile"; +import { withReplicas } from "drizzle-orm/pg-core"; +import { build } from "@server/build"; +import { db as mainDb, primaryDb as mainPrimaryDb } from "./driver"; + +function createLogsDb() { + // Only use separate logs database in SaaS builds + if (build !== "saas") { + return mainDb; + } + + const config = readConfigFile(); + + // Merge configs, prioritizing private config + const logsConfig = config.postgres_logs; + + // Check environment variable first + let connectionString = process.env.POSTGRES_LOGS_CONNECTION_STRING; + let replicaConnections: Array<{ connection_string: string }> = []; + + if (!connectionString && logsConfig) { + connectionString = logsConfig.connection_string; + replicaConnections = logsConfig.replicas || []; + } + + // If POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS is set, use it + if (process.env.POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS) { + replicaConnections = + process.env.POSTGRES_LOGS_REPLICA_CONNECTION_STRINGS.split(",").map( + (conn) => ({ + connection_string: conn.trim() + }) + ); + } + + // If no logs database is configured, fall back to main database + if (!connectionString) { + return mainDb; + } + + // Create separate connection pool for logs database + const poolConfig = logsConfig?.pool || config.postgres?.pool; + const primaryPool = new Pool({ + connectionString, + max: poolConfig?.max_connections || 20, + idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000, + connectionTimeoutMillis: poolConfig?.connection_timeout_ms || 5000 + }); + + const replicas = []; + + if (!replicaConnections.length) { + replicas.push( + DrizzlePostgres(primaryPool, { + logger: process.env.QUERY_LOGGING == "true" + }) + ); + } else { + for (const conn of replicaConnections) { + const replicaPool = new Pool({ + connectionString: conn.connection_string, + max: poolConfig?.max_replica_connections || 20, + idleTimeoutMillis: poolConfig?.idle_timeout_ms || 30000, + connectionTimeoutMillis: + poolConfig?.connection_timeout_ms || 5000 + }); + replicas.push( + DrizzlePostgres(replicaPool, { + logger: process.env.QUERY_LOGGING == "true" + }) + ); + } + } + + return withReplicas( + DrizzlePostgres(primaryPool, { + logger: process.env.QUERY_LOGGING == "true" + }), + replicas as any + ); +} + +export const logsDb = createLogsDb(); +export default logsDb; +export const primaryLogsDb = logsDb.$primary; diff --git a/server/db/pg/schema/privateSchema.ts b/server/db/pg/schema/privateSchema.ts index 6ff542de2..c9d7cc907 100644 --- a/server/db/pg/schema/privateSchema.ts +++ b/server/db/pg/schema/privateSchema.ts @@ -328,6 +328,14 @@ export const approvals = pgTable("approvals", { .notNull() }); +export const bannedEmails = pgTable("bannedEmails", { + email: varchar("email", { length: 255 }).primaryKey(), +}); + +export const bannedIps = pgTable("bannedIps", { + ip: varchar("ip", { length: 255 }).primaryKey(), +}); + export type Approval = InferSelectModel; export type Limit = InferSelectModel; export type Account = InferSelectModel; diff --git a/server/db/pg/schema/schema.ts b/server/db/pg/schema/schema.ts index 1d38bfb3e..0346495e3 100644 --- a/server/db/pg/schema/schema.ts +++ b/server/db/pg/schema/schema.ts @@ -23,7 +23,8 @@ export const domains = pgTable("domains", { tries: integer("tries").notNull().default(0), certResolver: varchar("certResolver"), customCertResolver: varchar("customCertResolver"), - preferWildcardCert: boolean("preferWildcardCert") + preferWildcardCert: boolean("preferWildcardCert"), + errorMessage: text("errorMessage") }); export const dnsRecords = pgTable("dnsRecords", { @@ -89,6 +90,7 @@ export const sites = pgTable("sites", { lastBandwidthUpdate: varchar("lastBandwidthUpdate"), type: varchar("type").notNull(), // "newt" or "wireguard" online: boolean("online").notNull().default(false), + lastPing: integer("lastPing"), address: varchar("address"), endpoint: varchar("endpoint"), publicKey: varchar("publicKey"), @@ -284,6 +286,7 @@ export const users = pgTable("user", { dateCreated: varchar("dateCreated").notNull(), termsAcceptedTimestamp: varchar("termsAcceptedTimestamp"), termsVersion: varchar("termsVersion"), + marketingEmailConsent: boolean("marketingEmailConsent").default(false), serverAdmin: boolean("serverAdmin").notNull().default(false), lastPasswordChange: bigint("lastPasswordChange", { mode: "number" }) }); @@ -733,6 +736,7 @@ export const clientSitesAssociationsCache = pgTable( .notNull(), siteId: integer("siteId").notNull(), isRelayed: boolean("isRelayed").notNull().default(false), + isJitMode: boolean("isJitMode").notNull().default(false), endpoint: varchar("endpoint"), publicKey: varchar("publicKey") // this will act as the session's public key for hole punching so we can track when it changes } diff --git a/server/db/sqlite/index.ts b/server/db/sqlite/index.ts index 86e318022..f8c04ac9e 100644 --- a/server/db/sqlite/index.ts +++ b/server/db/sqlite/index.ts @@ -1,4 +1,5 @@ export * from "./driver"; +export * from "./logsDriver"; export * from "./safeRead"; export * from "./schema/schema"; export * from "./schema/privateSchema"; diff --git a/server/db/sqlite/logsDriver.ts b/server/db/sqlite/logsDriver.ts new file mode 100644 index 000000000..f70c79fc5 --- /dev/null +++ b/server/db/sqlite/logsDriver.ts @@ -0,0 +1,7 @@ +import { db as mainDb } from "./driver"; + +// SQLite doesn't support separate databases for logs in the same way as Postgres +// Always use the main database connection for SQLite +export const logsDb = mainDb; +export default logsDb; +export const primaryLogsDb = logsDb; \ No newline at end of file diff --git a/server/db/sqlite/schema/privateSchema.ts b/server/db/sqlite/schema/privateSchema.ts index 40f6d7134..8baeb5220 100644 --- a/server/db/sqlite/schema/privateSchema.ts +++ b/server/db/sqlite/schema/privateSchema.ts @@ -318,6 +318,15 @@ export const approvals = sqliteTable("approvals", { .notNull() }); + +export const bannedEmails = sqliteTable("bannedEmails", { + email: text("email").primaryKey() +}); + +export const bannedIps = sqliteTable("bannedIps", { + ip: text("ip").primaryKey() +}); + export type Approval = InferSelectModel; export type Limit = InferSelectModel; export type Account = InferSelectModel; diff --git a/server/db/sqlite/schema/schema.ts b/server/db/sqlite/schema/schema.ts index 2d475808b..a4a0c6b8e 100644 --- a/server/db/sqlite/schema/schema.ts +++ b/server/db/sqlite/schema/schema.ts @@ -19,7 +19,8 @@ export const domains = sqliteTable("domains", { failed: integer("failed", { mode: "boolean" }).notNull().default(false), tries: integer("tries").notNull().default(0), certResolver: text("certResolver"), - preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" }) + preferWildcardCert: integer("preferWildcardCert", { mode: "boolean" }), + errorMessage: text("errorMessage") }); export const dnsRecords = sqliteTable("dnsRecords", { @@ -95,6 +96,7 @@ export const sites = sqliteTable("sites", { lastBandwidthUpdate: text("lastBandwidthUpdate"), type: text("type").notNull(), // "newt" or "wireguard" online: integer("online", { mode: "boolean" }).notNull().default(false), + lastPing: integer("lastPing"), // exit node stuff that is how to connect to the site when it has a wg server address: text("address"), // this is the address of the wireguard interface in newt @@ -320,6 +322,9 @@ export const users = sqliteTable("user", { dateCreated: text("dateCreated").notNull(), termsAcceptedTimestamp: text("termsAcceptedTimestamp"), termsVersion: text("termsVersion"), + marketingEmailConsent: integer("marketingEmailConsent", { + mode: "boolean" + }).default(false), serverAdmin: integer("serverAdmin", { mode: "boolean" }) .notNull() .default(false), @@ -412,6 +417,9 @@ export const clientSitesAssociationsCache = sqliteTable( isRelayed: integer("isRelayed", { mode: "boolean" }) .notNull() .default(false), + isJitMode: integer("isJitMode", { mode: "boolean" }) + .notNull() + .default(false), endpoint: text("endpoint"), publicKey: text("publicKey") // this will act as the session's public key for hole punching so we can track when it changes } diff --git a/server/integrationApiServer.ts b/server/integrationApiServer.ts index 6d513cf6b..ce029d9b5 100644 --- a/server/integrationApiServer.ts +++ b/server/integrationApiServer.ts @@ -17,6 +17,7 @@ import fs from "fs"; import path from "path"; import { APP_PATH } from "./lib/consts"; import yaml from "js-yaml"; +import { z } from "zod"; const dev = process.env.ENVIRONMENT !== "prod"; const externalPort = config.getRawConfig().server.integration_port; @@ -38,12 +39,24 @@ export function createIntegrationApiServer() { apiServer.use(cookieParser()); apiServer.use(express.json()); + const openApiDocumentation = getOpenApiDocumentation(); + apiServer.use( "/v1/docs", swaggerUi.serve, - swaggerUi.setup(getOpenApiDocumentation()) + swaggerUi.setup(openApiDocumentation) ); + // Unauthenticated OpenAPI spec endpoints + apiServer.get("/v1/openapi.json", (_req, res) => { + res.json(openApiDocumentation); + }); + + apiServer.get("/v1/openapi.yaml", (_req, res) => { + const yamlOutput = yaml.dump(openApiDocumentation); + res.type("application/yaml").send(yamlOutput); + }); + // API routes const prefix = `/v1`; apiServer.use(logIncomingMiddleware); @@ -75,16 +88,6 @@ function getOpenApiDocumentation() { } ); - for (const def of registry.definitions) { - if (def.type === "route") { - def.route.security = [ - { - [bearerAuth.name]: [] - } - ]; - } - } - registry.registerPath({ method: "get", path: "/", @@ -94,6 +97,74 @@ function getOpenApiDocumentation() { responses: {} }); + registry.registerPath({ + method: "get", + path: "/openapi.json", + description: "Get OpenAPI specification as JSON", + tags: [], + request: {}, + responses: { + "200": { + description: "OpenAPI specification as JSON", + content: { + "application/json": { + schema: { + type: "object" + } + } + } + } + } + }); + + registry.registerPath({ + method: "get", + path: "/openapi.yaml", + description: "Get OpenAPI specification as YAML", + tags: [], + request: {}, + responses: { + "200": { + description: "OpenAPI specification as YAML", + content: { + "application/yaml": { + schema: { + type: "string" + } + } + } + } + } + }); + + for (const def of registry.definitions) { + if (def.type === "route") { + def.route.security = [ + { + [bearerAuth.name]: [] + } + ]; + + // Ensure every route has a generic JSON response schema so Swagger UI can render responses + const existingResponses = def.route.responses; + const hasExistingResponses = + existingResponses && Object.keys(existingResponses).length > 0; + + if (!hasExistingResponses) { + def.route.responses = { + "*": { + description: "", + content: { + "application/json": { + schema: z.object({}) + } + } + } + }; + } + } + } + const generator = new OpenApiGeneratorV3(registry.definitions); const generated = generator.generateDocument({ diff --git a/server/internalServer.ts b/server/internalServer.ts index d15e3c45d..7ba046e4b 100644 --- a/server/internalServer.ts +++ b/server/internalServer.ts @@ -16,6 +16,11 @@ const internalPort = config.getRawConfig().server.internal_port; export function createInternalServer() { const internalServer = express(); + const trustProxy = config.getRawConfig().server.trust_proxy; + if (trustProxy) { + internalServer.set("trust proxy", trustProxy); + } + internalServer.use(helmet()); internalServer.use(cors()); internalServer.use(stripDuplicateSesions); diff --git a/server/lib/billing/tierMatrix.ts b/server/lib/billing/tierMatrix.ts index 20f8001de..c08bcea71 100644 --- a/server/lib/billing/tierMatrix.ts +++ b/server/lib/billing/tierMatrix.ts @@ -48,5 +48,5 @@ export const tierMatrix: Record = { "enterprise" ], [TierFeature.AutoProvisioning]: ["tier1", "tier3", "enterprise"], - [TierFeature.SshPam]: ["enterprise"] + [TierFeature.SshPam]: ["tier1", "tier3", "enterprise"] }; diff --git a/server/lib/billing/usageService.ts b/server/lib/billing/usageService.ts index d72992841..9cb24bbeb 100644 --- a/server/lib/billing/usageService.ts +++ b/server/lib/billing/usageService.ts @@ -12,7 +12,7 @@ import { import { FeatureId, getFeatureMeterId } from "./features"; import logger from "@server/logger"; import { build } from "@server/build"; -import cache from "@server/lib/cache"; +import cache from "#dynamic/lib/cache"; export function noop() { if (build !== "saas") { @@ -230,7 +230,7 @@ export class UsageService { const orgIdToUse = await this.getBillingOrg(orgId); const cacheKey = `customer_${orgIdToUse}_${featureId}`; - const cached = cache.get(cacheKey); + const cached = await cache.get(cacheKey); if (cached) { return cached; @@ -253,7 +253,7 @@ export class UsageService { const customerId = customer.customerId; // Cache the result - cache.set(cacheKey, customerId, 300); // 5 minute TTL + await cache.set(cacheKey, customerId, 300); // 5 minute TTL return customerId; } catch (error) { diff --git a/server/lib/blueprints/applyBlueprint.ts b/server/lib/blueprints/applyBlueprint.ts index ac2f9508e..a304bb392 100644 --- a/server/lib/blueprints/applyBlueprint.ts +++ b/server/lib/blueprints/applyBlueprint.ts @@ -107,7 +107,7 @@ export async function applyBlueprint({ [target], matchingHealthcheck ? [matchingHealthcheck] : [], result.proxyResource.protocol, - result.proxyResource.proxyPort + site.newt.version ); } } diff --git a/server/lib/blueprints/clientResources.ts b/server/lib/blueprints/clientResources.ts index 64de98670..80c691c63 100644 --- a/server/lib/blueprints/clientResources.ts +++ b/server/lib/blueprints/clientResources.ts @@ -11,7 +11,7 @@ import { userSiteResources } from "@server/db"; import { sites } from "@server/db"; -import { eq, and, ne, inArray } from "drizzle-orm"; +import { eq, and, ne, inArray, or } from "drizzle-orm"; import { Config } from "./types"; import logger from "@server/logger"; import { getNextAvailableAliasAddress } from "../ip"; @@ -142,7 +142,10 @@ export async function updateClientResources( .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) .where( and( - inArray(users.username, resourceData.users), + or( + inArray(users.username, resourceData.users), + inArray(users.email, resourceData.users) + ), eq(userOrgs.orgId, orgId) ) ); @@ -276,7 +279,10 @@ export async function updateClientResources( .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) .where( and( - inArray(users.username, resourceData.users), + or( + inArray(users.username, resourceData.users), + inArray(users.email, resourceData.users) + ), eq(userOrgs.orgId, orgId) ) ); diff --git a/server/lib/blueprints/proxyResources.ts b/server/lib/blueprints/proxyResources.ts index 55a7712b9..2696b68c8 100644 --- a/server/lib/blueprints/proxyResources.ts +++ b/server/lib/blueprints/proxyResources.ts @@ -212,7 +212,10 @@ export async function updateProxyResources( } else { // Update existing resource - const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.maintencePage); + const isLicensed = await isLicensedOrSubscribed( + orgId, + tierMatrix.maintencePage + ); if (!isLicensed) { resourceData.maintenance = undefined; } @@ -590,7 +593,10 @@ export async function updateProxyResources( existingRule.action !== getRuleAction(rule.action) || existingRule.match !== rule.match.toUpperCase() || existingRule.value !== - getRuleValue(rule.match.toUpperCase(), rule.value) || + getRuleValue( + rule.match.toUpperCase(), + rule.value + ) || existingRule.priority !== intendedPriority ) { validateRule(rule); @@ -648,7 +654,10 @@ export async function updateProxyResources( ); } - const isLicensed = await isLicensedOrSubscribed(orgId, tierMatrix.maintencePage); + const isLicensed = await isLicensedOrSubscribed( + orgId, + tierMatrix.maintencePage + ); if (!isLicensed) { resourceData.maintenance = undefined; } @@ -935,7 +944,12 @@ async function syncUserResources( .select() .from(users) .innerJoin(userOrgs, eq(users.userId, userOrgs.userId)) - .where(and(eq(users.username, username), eq(userOrgs.orgId, orgId))) + .where( + and( + or(eq(users.username, username), eq(users.email, username)), + eq(userOrgs.orgId, orgId) + ) + ) .limit(1); if (!user) { diff --git a/server/lib/blueprints/types.ts b/server/lib/blueprints/types.ts index edf4b0c7f..2239e4f9a 100644 --- a/server/lib/blueprints/types.ts +++ b/server/lib/blueprints/types.ts @@ -69,7 +69,7 @@ export const AuthSchema = z.object({ .refine((roles) => !roles.includes("Admin"), { error: "Admin role cannot be included in sso-roles" }), - "sso-users": z.array(z.email()).optional().default([]), + "sso-users": z.array(z.string()).optional().default([]), "whitelist-users": z.array(z.email()).optional().default([]), "auto-login-idp": z.int().positive().optional() }); @@ -335,7 +335,7 @@ export const ClientResourceSchema = z .refine((roles) => !roles.includes("Admin"), { error: "Admin role cannot be included in roles" }), - users: z.array(z.email()).optional().default([]), + users: z.array(z.string()).optional().default([]), machines: z.array(z.string()).optional().default([]) }) .refine( diff --git a/server/lib/cache.ts b/server/lib/cache.ts index 4910d9452..f089a6387 100644 --- a/server/lib/cache.ts +++ b/server/lib/cache.ts @@ -1,9 +1,9 @@ import NodeCache from "node-cache"; import logger from "@server/logger"; -// Create cache with maxKeys limit to prevent memory leaks +// Create local cache with maxKeys limit to prevent memory leaks // With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient -export const cache = new NodeCache({ +export const localCache = new NodeCache({ stdTTL: 3600, checkperiod: 120, maxKeys: 10000 @@ -11,10 +11,151 @@ export const cache = new NodeCache({ // Log cache statistics periodically for monitoring setInterval(() => { - const stats = cache.getStats(); + const stats = localCache.getStats(); logger.debug( - `Cache stats - Keys: ${stats.keys}, Hits: ${stats.hits}, Misses: ${stats.misses}, Hit rate: ${stats.hits > 0 ? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(2) : 0}%` + `Local cache stats - Keys: ${stats.keys}, Hits: ${stats.hits}, Misses: ${stats.misses}, Hit rate: ${stats.hits > 0 ? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(2) : 0}%` ); }, 300000); // Every 5 minutes +/** + * Adaptive cache that uses Redis when available in multi-node environments, + * otherwise falls back to local memory cache for single-node deployments. + */ +class AdaptiveCache { + /** + * Set a value in the cache + * @param key - Cache key + * @param value - Value to cache (will be JSON stringified for Redis) + * @param ttl - Time to live in seconds (0 = no expiration) + * @returns boolean indicating success + */ + async set(key: string, value: any, ttl?: number): Promise { + const effectiveTtl = ttl === 0 ? undefined : ttl; + + // Use local cache as fallback or primary + const success = localCache.set(key, value, effectiveTtl || 0); + if (success) { + logger.debug(`Set key in local cache: ${key}`); + } + return success; + } + + /** + * Get a value from the cache + * @param key - Cache key + * @returns The cached value or undefined if not found + */ + async get(key: string): Promise { + // Use local cache as fallback or primary + const value = localCache.get(key); + if (value !== undefined) { + logger.debug(`Cache hit in local cache: ${key}`); + } else { + logger.debug(`Cache miss in local cache: ${key}`); + } + return value; + } + + /** + * Delete a value from the cache + * @param key - Cache key or array of keys + * @returns Number of deleted entries + */ + async del(key: string | string[]): Promise { + const keys = Array.isArray(key) ? key : [key]; + let deletedCount = 0; + + // Use local cache as fallback or primary + for (const k of keys) { + const success = localCache.del(k); + if (success > 0) { + deletedCount++; + logger.debug(`Deleted key from local cache: ${k}`); + } + } + + return deletedCount; + } + + /** + * Check if a key exists in the cache + * @param key - Cache key + * @returns boolean indicating if key exists + */ + async has(key: string): Promise { + // Use local cache as fallback or primary + return localCache.has(key); + } + + /** + * Get multiple values from the cache + * @param keys - Array of cache keys + * @returns Array of values (undefined for missing keys) + */ + async mget(keys: string[]): Promise<(T | undefined)[]> { + // Use local cache as fallback or primary + return keys.map((key) => localCache.get(key)); + } + + /** + * Flush all keys from the cache + */ + async flushAll(): Promise { + localCache.flushAll(); + logger.debug("Flushed local cache"); + } + + /** + * Get cache statistics + * Note: Only returns local cache stats, Redis stats are not included + */ + getStats() { + return localCache.getStats(); + } + + /** + * Get the current cache backend being used + * @returns "redis" if Redis is available and healthy, "local" otherwise + */ + getCurrentBackend(): "redis" | "local" { + return "local"; + } + + /** + * Take a key from the cache and delete it + * @param key - Cache key + * @returns The value or undefined if not found + */ + async take(key: string): Promise { + const value = await this.get(key); + if (value !== undefined) { + await this.del(key); + } + return value; + } + + /** + * Get TTL (time to live) for a key + * @param key - Cache key + * @returns TTL in seconds, 0 if no expiration, -1 if key doesn't exist + */ + getTtl(key: string): number { + const ttl = localCache.getTtl(key); + if (ttl === undefined) { + return -1; + } + return Math.max(0, Math.floor((ttl - Date.now()) / 1000)); + } + + /** + * Get all keys from the cache + * Note: Only returns local cache keys, Redis keys are not included + */ + keys(): string[] { + return localCache.keys(); + } +} + +// Export singleton instance +export const cache = new AdaptiveCache(); export default cache; diff --git a/server/lib/cleanupLogs.ts b/server/lib/cleanupLogs.ts index 96a589ee4..8eb4ca77f 100644 --- a/server/lib/cleanupLogs.ts +++ b/server/lib/cleanupLogs.ts @@ -4,8 +4,12 @@ import { cleanUpOldLogs as cleanUpOldActionLogs } from "#dynamic/middlewares/log import { cleanUpOldLogs as cleanUpOldRequestLogs } from "@server/routers/badger/logRequestAudit"; import { gt, or } from "drizzle-orm"; import { cleanUpOldFingerprintSnapshots } from "@server/routers/olm/fingerprintingUtils"; +import { build } from "@server/build"; export function initLogCleanupInterval() { + if (build == "saas") { // skip log cleanup for saas builds + return null; + } return setInterval( async () => { const orgsToClean = await db diff --git a/server/lib/clientVersionChecks.ts b/server/lib/clientVersionChecks.ts new file mode 100644 index 000000000..330959e7c --- /dev/null +++ b/server/lib/clientVersionChecks.ts @@ -0,0 +1,20 @@ +import semver from "semver"; + +export function canCompress( + clientVersion: string | null | undefined, + type: "newt" | "olm" +): boolean { + try { + if (!clientVersion) return false; + // check if it is a valid semver + if (!semver.valid(clientVersion)) return false; + if (type === "newt") { + return semver.gte(clientVersion, "1.10.3"); + } else if (type === "olm") { + return semver.gte(clientVersion, "1.4.3"); + } + return false; + } catch { + return false; + } +} diff --git a/server/lib/consts.ts b/server/lib/consts.ts index 4f7e4d62c..d53bd70bb 100644 --- a/server/lib/consts.ts +++ b/server/lib/consts.ts @@ -2,7 +2,7 @@ import path from "path"; import { fileURLToPath } from "url"; // This is a placeholder value replaced by the build process -export const APP_VERSION = "1.15.4"; +export const APP_VERSION = "1.16.0"; export const __FILENAME = fileURLToPath(import.meta.url); export const __DIRNAME = path.dirname(__FILENAME); diff --git a/server/lib/deleteOrg.ts b/server/lib/deleteOrg.ts index cca2ea974..065f216a1 100644 --- a/server/lib/deleteOrg.ts +++ b/server/lib/deleteOrg.ts @@ -85,9 +85,7 @@ export async function deleteOrgById( deletedNewtIds.push(deletedNewt.newtId); await trx .delete(newtSessions) - .where( - eq(newtSessions.newtId, deletedNewt.newtId) - ); + .where(eq(newtSessions.newtId, deletedNewt.newtId)); } } } @@ -121,33 +119,38 @@ export async function deleteOrgById( eq(clientSitesAssociationsCache.clientId, client.clientId) ); } + + await trx.delete(resources).where(eq(resources.orgId, orgId)); + const allOrgDomains = await trx .select() .from(orgDomains) - .innerJoin(domains, eq(domains.domainId, orgDomains.domainId)) + .innerJoin(domains, eq(orgDomains.domainId, domains.domainId)) .where( and( eq(orgDomains.orgId, orgId), eq(domains.configManaged, false) ) ); + logger.info(`Found ${allOrgDomains.length} domains to delete`); const domainIdsToDelete: string[] = []; for (const orgDomain of allOrgDomains) { const domainId = orgDomain.domains.domainId; - const orgCount = await trx - .select({ count: sql`count(*)` }) + const [orgCount] = await trx + .select({ count: count() }) .from(orgDomains) .where(eq(orgDomains.domainId, domainId)); - if (orgCount[0].count === 1) { + logger.info(`Found ${orgCount.count} orgs using domain ${domainId}`); + if (orgCount.count === 1) { domainIdsToDelete.push(domainId); } } + logger.info(`Found ${domainIdsToDelete.length} domains to delete`); if (domainIdsToDelete.length > 0) { await trx .delete(domains) .where(inArray(domains.domainId, domainIdsToDelete)); } - await trx.delete(resources).where(eq(resources.orgId, orgId)); await usageService.add(orgId, FeatureId.ORGINIZATIONS, -1, trx); // here we are decreasing the org count BEFORE deleting the org because we need to still be able to get the org to get the billing org inside of here @@ -231,15 +234,13 @@ export function sendTerminationMessages(result: DeleteOrgByIdResult): void { ); } for (const olmId of result.olmsToTerminate) { - sendTerminateClient( - 0, - OlmErrorCodes.TERMINATED_REKEYED, - olmId - ).catch((error) => { - logger.error( - "Failed to send termination message to olm:", - error - ); - }); + sendTerminateClient(0, OlmErrorCodes.TERMINATED_REKEYED, olmId).catch( + (error) => { + logger.error( + "Failed to send termination message to olm:", + error + ); + } + ); } } diff --git a/server/lib/readConfigFile.ts b/server/lib/readConfigFile.ts index bfca59707..cca0aa6aa 100644 --- a/server/lib/readConfigFile.ts +++ b/server/lib/readConfigFile.ts @@ -189,6 +189,46 @@ export const configSchema = z .prefault({}) }) .optional(), + postgres_logs: z + .object({ + connection_string: z + .string() + .optional() + .transform(getEnvOrYaml("POSTGRES_LOGS_CONNECTION_STRING")), + replicas: z + .array( + z.object({ + connection_string: z.string() + }) + ) + .optional(), + pool: z + .object({ + max_connections: z + .number() + .positive() + .optional() + .default(20), + max_replica_connections: z + .number() + .positive() + .optional() + .default(10), + idle_timeout_ms: z + .number() + .positive() + .optional() + .default(30000), + connection_timeout_ms: z + .number() + .positive() + .optional() + .default(5000) + }) + .optional() + .prefault({}) + }) + .optional(), traefik: z .object({ http_entrypoint: z.string().optional().default("web"), diff --git a/server/lib/rebuildClientAssociations.ts b/server/lib/rebuildClientAssociations.ts index 7ec767492..2195a913e 100644 --- a/server/lib/rebuildClientAssociations.ts +++ b/server/lib/rebuildClientAssociations.ts @@ -478,6 +478,7 @@ async function handleMessagesForSiteClients( } if (isAdd) { + // TODO: if we are in jit mode here should we really be sending this? await initPeerAddHandshake( // this will kick off the add peer process for the client client.clientId, @@ -572,7 +573,7 @@ export async function updateClientSiteDestinations( destinations: [ { destinationIP: site.sites.subnet.split("/")[0], - destinationPort: site.sites.listenPort || 0 + destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated } ] }; @@ -580,7 +581,7 @@ export async function updateClientSiteDestinations( // add to the existing destinations destinations.destinations.push({ destinationIP: site.sites.subnet.split("/")[0], - destinationPort: site.sites.listenPort || 0 + destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated }); } @@ -670,7 +671,11 @@ async function handleSubnetProxyTargetUpdates( `Adding ${targetsToAdd.length} subnet proxy targets for siteResource ${siteResource.siteResourceId}` ); proxyJobs.push( - addSubnetProxyTargets(newt.newtId, targetsToAdd) + addSubnetProxyTargets( + newt.newtId, + targetsToAdd, + newt.version + ) ); } @@ -706,7 +711,11 @@ async function handleSubnetProxyTargetUpdates( `Removing ${targetsToRemove.length} subnet proxy targets for siteResource ${siteResource.siteResourceId}` ); proxyJobs.push( - removeSubnetProxyTargets(newt.newtId, targetsToRemove) + removeSubnetProxyTargets( + newt.newtId, + targetsToRemove, + newt.version + ) ); } @@ -1081,6 +1090,7 @@ async function handleMessagesForClientSites( continue; } + // TODO: if we are in jit mode here should we really be sending this? await initPeerAddHandshake( // this will kick off the add peer process for the client client.clientId, @@ -1147,7 +1157,7 @@ async function handleMessagesForClientResources( // Add subnet proxy targets for each site for (const [siteId, resources] of addedBySite.entries()) { const [newt] = await trx - .select({ newtId: newts.newtId }) + .select({ newtId: newts.newtId, version: newts.version }) .from(newts) .where(eq(newts.siteId, siteId)) .limit(1); @@ -1169,7 +1179,13 @@ async function handleMessagesForClientResources( ]); if (targets.length > 0) { - proxyJobs.push(addSubnetProxyTargets(newt.newtId, targets)); + proxyJobs.push( + addSubnetProxyTargets( + newt.newtId, + targets, + newt.version + ) + ); } try { @@ -1218,7 +1234,7 @@ async function handleMessagesForClientResources( // Remove subnet proxy targets for each site for (const [siteId, resources] of removedBySite.entries()) { const [newt] = await trx - .select({ newtId: newts.newtId }) + .select({ newtId: newts.newtId, version: newts.version }) .from(newts) .where(eq(newts.siteId, siteId)) .limit(1); @@ -1241,7 +1257,11 @@ async function handleMessagesForClientResources( if (targets.length > 0) { proxyJobs.push( - removeSubnetProxyTargets(newt.newtId, targets) + removeSubnetProxyTargets( + newt.newtId, + targets, + newt.version + ) ); } diff --git a/server/lib/resend.ts b/server/lib/resend.ts deleted file mode 100644 index 0c21b1bef..000000000 --- a/server/lib/resend.ts +++ /dev/null @@ -1,16 +0,0 @@ -export enum AudienceIds { - SignUps = "", - Subscribed = "", - Churned = "", - Newsletter = "" -} - -let resend; -export default resend; - -export async function moveEmailToAudience( - email: string, - audienceId: AudienceIds -) { - return; -} diff --git a/server/lib/sanitize.ts b/server/lib/sanitize.ts new file mode 100644 index 000000000..9eba8a583 --- /dev/null +++ b/server/lib/sanitize.ts @@ -0,0 +1,40 @@ +/** + * Sanitize a string field before inserting into a database TEXT column. + * + * Two passes are applied: + * + * 1. Lone UTF-16 surrogates – JavaScript strings can hold unpaired surrogates + * (e.g. \uD800 without a following \uDC00-\uDFFF codepoint). These are + * valid in JS but cannot be encoded as UTF-8, triggering + * `report_invalid_encoding` in SQLite / Postgres. They are replaced with + * the Unicode replacement character U+FFFD so the data is preserved as a + * visible signal that something was malformed. + * + * 2. Null bytes and C0 control characters – SQLite stores TEXT as + * null-terminated C strings, so \x00 in a value causes + * `report_invalid_encoding`. Bots and scanners routinely inject null bytes + * into URLs (e.g. `/path\u0000.jpg`). All C0 control characters in the + * range \x00-\x1F are stripped except for the three that are legitimate in + * text payloads: HT (\x09), LF (\x0A), and CR (\x0D). DEL (\x7F) is also + * stripped. + */ +export function sanitizeString(value: string): string; +export function sanitizeString( + value: string | null | undefined +): string | undefined; +export function sanitizeString( + value: string | null | undefined +): string | undefined { + if (value == null) return undefined; + return ( + value + // Replace lone high surrogates (not followed by a low surrogate) + // and lone low surrogates (not preceded by a high surrogate). + .replace( + /[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(? dayInMs) { logger.info("Fetching certificates due to 24-hour renewal check"); return true; @@ -265,7 +266,7 @@ export class TraefikConfigManager { return true; } - // Check if any local certificates are missing or appear to be outdated + // Check if any local certificates are missing (needs immediate fetch) for (const domain of domainsNeedingCerts) { const localState = this.lastLocalCertificateState.get(domain); if (!localState || !localState.exists) { @@ -274,17 +275,46 @@ export class TraefikConfigManager { ); return true; } + } - // Check if certificate is expiring soon (within 30 days) - if (localState.expiresAt) { - const nowInSeconds = Math.floor(Date.now() / 1000); - const secondsUntilExpiry = localState.expiresAt - nowInSeconds; - const daysUntilExpiry = secondsUntilExpiry / (60 * 60 * 24); - if (daysUntilExpiry < 30) { - logger.info( - `Fetching certificates due to upcoming expiry for ${domain} (${Math.round(daysUntilExpiry)} days remaining)` - ); - return true; + // For expiry checks, throttle to every 6 hours to avoid querying the + // API/DB on every monitor loop. The certificate-service renews certs + // 45 days before expiry, so checking every 6 hours is plenty frequent + // to pick up renewed certs promptly. + const renewalCheckIntervalMs = 6 * 60 * 60 * 1000; // 6 hours + if (timeSinceLastFetch > renewalCheckIntervalMs) { + // Check non-wildcard certs for expiry (within 45 days to match + // the server-side renewal window in certificate-service) + for (const domain of domainsNeedingCerts) { + const localState = this.lastLocalCertificateState.get(domain); + if (localState?.expiresAt) { + const nowInSeconds = Math.floor(Date.now() / 1000); + const secondsUntilExpiry = + localState.expiresAt - nowInSeconds; + const daysUntilExpiry = secondsUntilExpiry / (60 * 60 * 24); + if (daysUntilExpiry < 45) { + logger.info( + `Fetching certificates due to upcoming expiry for ${domain} (${Math.round(daysUntilExpiry)} days remaining)` + ); + return true; + } + } + } + + // Also check wildcard certificates for expiry. These are not + // included in domainsNeedingCerts since their subdomains are + // filtered out, so we must check them separately. + for (const [certDomain, state] of this.lastLocalCertificateState) { + if (state.exists && state.wildcard && state.expiresAt) { + const nowInSeconds = Math.floor(Date.now() / 1000); + const secondsUntilExpiry = state.expiresAt - nowInSeconds; + const daysUntilExpiry = secondsUntilExpiry / (60 * 60 * 24); + if (daysUntilExpiry < 45) { + logger.info( + `Fetching certificates due to upcoming expiry for wildcard cert ${certDomain} (${Math.round(daysUntilExpiry)} days remaining)` + ); + return true; + } } } } @@ -361,6 +391,26 @@ export class TraefikConfigManager { } } + // Also include wildcard cert base domains that are + // expiring or expired so they get re-fetched even though + // their subdomains were filtered out above. + for (const [certDomain, state] of this + .lastLocalCertificateState) { + if (state.exists && state.wildcard && state.expiresAt) { + const nowInSeconds = Math.floor(Date.now() / 1000); + const secondsUntilExpiry = + state.expiresAt - nowInSeconds; + const daysUntilExpiry = + secondsUntilExpiry / (60 * 60 * 24); + if (daysUntilExpiry < 45) { + domainsToFetch.add(certDomain); + logger.info( + `Including expiring wildcard cert domain ${certDomain} in fetch (${Math.round(daysUntilExpiry)} days remaining)` + ); + } + } + } + if (domainsToFetch.size > 0) { // Get valid certificates for domains not covered by wildcards validCertificates = @@ -507,11 +557,18 @@ export class TraefikConfigManager { config.getRawConfig().server .session_cookie_name, - // deprecated accessTokenQueryParam: config.getRawConfig().server .resource_access_token_param, + accessTokenIdHeader: + config.getRawConfig().server + .resource_access_token_headers.id, + + accessTokenHeader: + config.getRawConfig().server + .resource_access_token_headers.token, + resourceSessionRequestParam: config.getRawConfig().server .resource_session_request_param diff --git a/server/lib/traefik/getTraefikConfig.ts b/server/lib/traefik/getTraefikConfig.ts index 06754ffa2..abd0a8de0 100644 --- a/server/lib/traefik/getTraefikConfig.ts +++ b/server/lib/traefik/getTraefikConfig.ts @@ -14,7 +14,7 @@ import logger from "@server/logger"; import config from "@server/lib/config"; import { resources, sites, Target, targets } from "@server/db"; import createPathRewriteMiddleware from "./middleware"; -import { sanitize, validatePathRewriteConfig } from "./utils"; +import { sanitize, encodePath, validatePathRewriteConfig } from "./utils"; const redirectHttpsMiddlewareName = "redirect-to-https"; const badgerMiddlewareName = "badger"; @@ -44,7 +44,7 @@ export async function getTraefikConfig( filterOutNamespaceDomains = false, // UNUSED BUT USED IN PRIVATE generateLoginPageRouters = false, // UNUSED BUT USED IN PRIVATE allowRawResources = true, - allowMaintenancePage = true, // UNUSED BUT USED IN PRIVATE + allowMaintenancePage = true // UNUSED BUT USED IN PRIVATE ): Promise { // Get resources with their targets and sites in a single optimized query // Start from sites on this exit node, then join to targets and resources @@ -127,7 +127,7 @@ export async function getTraefikConfig( resourcesWithTargetsAndSites.forEach((row) => { const resourceId = row.resourceId; const resourceName = sanitize(row.resourceName) || ""; - const targetPath = sanitize(row.path) || ""; // Handle null/undefined paths + const targetPath = encodePath(row.path); // Use encodePath to avoid collisions (e.g. "/a/b" vs "/a-b") const pathMatchType = row.pathMatchType || ""; const rewritePath = row.rewritePath || ""; const rewritePathType = row.rewritePathType || ""; @@ -145,7 +145,7 @@ export async function getTraefikConfig( const mapKey = [resourceId, pathKey].filter(Boolean).join("-"); const key = sanitize(mapKey); - if (!resourcesMap.has(key)) { + if (!resourcesMap.has(mapKey)) { const validation = validatePathRewriteConfig( row.path, row.pathMatchType, @@ -160,9 +160,10 @@ export async function getTraefikConfig( return; } - resourcesMap.set(key, { + resourcesMap.set(mapKey, { resourceId: row.resourceId, name: resourceName, + key: key, fullDomain: row.fullDomain, ssl: row.ssl, http: row.http, @@ -190,7 +191,7 @@ export async function getTraefikConfig( }); } - resourcesMap.get(key).targets.push({ + resourcesMap.get(mapKey).targets.push({ resourceId: row.resourceId, targetId: row.targetId, ip: row.ip, @@ -227,8 +228,9 @@ export async function getTraefikConfig( }; // get the key and the resource - for (const [key, resource] of resourcesMap.entries()) { + for (const [, resource] of resourcesMap.entries()) { const targets = resource.targets as TargetWithSite[]; + const key = resource.key; const routerName = `${key}-${resource.name}-router`; const serviceName = `${key}-${resource.name}-service`; @@ -477,7 +479,10 @@ export async function getTraefikConfig( // TODO: HOW TO HANDLE ^^^^^^ BETTER const anySitesOnline = targets.some( - (target) => target.site.online + (target) => + target.site.online || + target.site.type === "local" || + target.site.type === "wireguard" ); return ( @@ -490,7 +495,7 @@ export async function getTraefikConfig( if (target.health == "unhealthy") { return false; } - + // If any sites are online, exclude offline sites if (anySitesOnline && !target.site.online) { return false; @@ -605,7 +610,10 @@ export async function getTraefikConfig( servers: (() => { // Check if any sites are online const anySitesOnline = targets.some( - (target) => target.site.online + (target) => + target.site.online || + target.site.type === "local" || + target.site.type === "wireguard" ); return targets @@ -613,7 +621,7 @@ export async function getTraefikConfig( if (!target.enabled) { return false; } - + // If any sites are online, exclude offline sites if (anySitesOnline && !target.site.online) { return false; diff --git a/server/lib/traefik/pathEncoding.test.ts b/server/lib/traefik/pathEncoding.test.ts new file mode 100644 index 000000000..83d53a039 --- /dev/null +++ b/server/lib/traefik/pathEncoding.test.ts @@ -0,0 +1,323 @@ +import { assertEquals } from "../../../test/assert"; + +// ── Pure function copies (inlined to avoid pulling in server dependencies) ── + +function sanitize(input: string | null | undefined): string | undefined { + if (!input) return undefined; + if (input.length > 50) { + input = input.substring(0, 50); + } + return input + .replace(/[^a-zA-Z0-9-]/g, "-") + .replace(/-+/g, "-") + .replace(/^-|-$/g, ""); +} + +function encodePath(path: string | null | undefined): string { + if (!path) return ""; + return path.replace(/[^a-zA-Z0-9]/g, (ch) => { + return ch.charCodeAt(0).toString(16); + }); +} + +// ── Helpers ────────────────────────────────────────────────────────── + +/** + * Exact replica of the OLD key computation from upstream main. + * Uses sanitize() for paths — this is what had the collision bug. + */ +function oldKeyComputation( + resourceId: number, + path: string | null, + pathMatchType: string | null, + rewritePath: string | null, + rewritePathType: string | null +): string { + const targetPath = sanitize(path) || ""; + const pmt = pathMatchType || ""; + const rp = rewritePath || ""; + const rpt = rewritePathType || ""; + const pathKey = [targetPath, pmt, rp, rpt].filter(Boolean).join("-"); + const mapKey = [resourceId, pathKey].filter(Boolean).join("-"); + return sanitize(mapKey) || ""; +} + +/** + * Replica of the NEW key computation from our fix. + * Uses encodePath() for paths — collision-free. + */ +function newKeyComputation( + resourceId: number, + path: string | null, + pathMatchType: string | null, + rewritePath: string | null, + rewritePathType: string | null +): string { + const targetPath = encodePath(path); + const pmt = pathMatchType || ""; + const rp = rewritePath || ""; + const rpt = rewritePathType || ""; + const pathKey = [targetPath, pmt, rp, rpt].filter(Boolean).join("-"); + const mapKey = [resourceId, pathKey].filter(Boolean).join("-"); + return sanitize(mapKey) || ""; +} + +// ── Tests ──────────────────────────────────────────────────────────── + +function runTests() { + console.log("Running path encoding tests...\n"); + + let passed = 0; + + // ── encodePath unit tests ──────────────────────────────────────── + + // Test 1: null/undefined/empty + { + assertEquals(encodePath(null), "", "null should return empty"); + assertEquals( + encodePath(undefined), + "", + "undefined should return empty" + ); + assertEquals(encodePath(""), "", "empty string should return empty"); + console.log(" PASS: encodePath handles null/undefined/empty"); + passed++; + } + + // Test 2: root path + { + assertEquals(encodePath("/"), "2f", "/ should encode to 2f"); + console.log(" PASS: encodePath encodes root path"); + passed++; + } + + // Test 3: alphanumeric passthrough + { + assertEquals(encodePath("/api"), "2fapi", "/api encodes slash only"); + assertEquals(encodePath("/v1"), "2fv1", "/v1 encodes slash only"); + assertEquals(encodePath("abc"), "abc", "plain alpha passes through"); + console.log(" PASS: encodePath preserves alphanumeric chars"); + passed++; + } + + // Test 4: all special chars produce unique hex + { + const paths = ["/a/b", "/a-b", "/a.b", "/a_b", "/a b"]; + const results = paths.map((p) => encodePath(p)); + const unique = new Set(results); + assertEquals( + unique.size, + paths.length, + "all special-char paths must produce unique encodings" + ); + console.log( + " PASS: encodePath produces unique output for different special chars" + ); + passed++; + } + + // Test 5: output is always alphanumeric (safe for Traefik names) + { + const paths = [ + "/", + "/api", + "/a/b", + "/a-b", + "/a.b", + "/complex/path/here" + ]; + for (const p of paths) { + const e = encodePath(p); + assertEquals( + /^[a-zA-Z0-9]+$/.test(e), + true, + `encodePath("${p}") = "${e}" must be alphanumeric` + ); + } + console.log(" PASS: encodePath output is always alphanumeric"); + passed++; + } + + // Test 6: deterministic + { + assertEquals( + encodePath("/api"), + encodePath("/api"), + "same input same output" + ); + assertEquals( + encodePath("/a/b/c"), + encodePath("/a/b/c"), + "same input same output" + ); + console.log(" PASS: encodePath is deterministic"); + passed++; + } + + // Test 7: many distinct paths never collide + { + const paths = [ + "/", + "/api", + "/api/v1", + "/api/v2", + "/a/b", + "/a-b", + "/a.b", + "/a_b", + "/health", + "/health/check", + "/admin", + "/admin/users", + "/api/v1/users", + "/api/v1/posts", + "/app", + "/app/dashboard" + ]; + const encoded = new Set(paths.map((p) => encodePath(p))); + assertEquals( + encoded.size, + paths.length, + `expected ${paths.length} unique encodings, got ${encoded.size}` + ); + console.log(" PASS: 16 realistic paths all produce unique encodings"); + passed++; + } + + // ── Collision fix: the actual bug we're fixing ─────────────────── + + // Test 8: /a/b and /a-b now have different keys (THE BUG FIX) + { + const keyAB = newKeyComputation(1, "/a/b", "prefix", null, null); + const keyDash = newKeyComputation(1, "/a-b", "prefix", null, null); + assertEquals( + keyAB !== keyDash, + true, + "/a/b and /a-b MUST have different keys" + ); + console.log(" PASS: collision fix — /a/b vs /a-b have different keys"); + passed++; + } + + // Test 9: demonstrate the old bug — old code maps /a/b and /a-b to same key + { + const oldKeyAB = oldKeyComputation(1, "/a/b", "prefix", null, null); + const oldKeyDash = oldKeyComputation(1, "/a-b", "prefix", null, null); + assertEquals( + oldKeyAB, + oldKeyDash, + "old code MUST have this collision (confirms the bug exists)" + ); + console.log(" PASS: confirmed old code bug — /a/b and /a-b collided"); + passed++; + } + + // Test 10: /api/v1 and /api-v1 — old code collision, new code fixes it + { + const oldKey1 = oldKeyComputation(1, "/api/v1", "prefix", null, null); + const oldKey2 = oldKeyComputation(1, "/api-v1", "prefix", null, null); + assertEquals( + oldKey1, + oldKey2, + "old code collision for /api/v1 vs /api-v1" + ); + + const newKey1 = newKeyComputation(1, "/api/v1", "prefix", null, null); + const newKey2 = newKeyComputation(1, "/api-v1", "prefix", null, null); + assertEquals( + newKey1 !== newKey2, + true, + "new code must separate /api/v1 and /api-v1" + ); + console.log(" PASS: collision fix — /api/v1 vs /api-v1"); + passed++; + } + + // Test 11: /app.v2 and /app/v2 and /app-v2 — three-way collision fixed + { + const a = newKeyComputation(1, "/app.v2", "prefix", null, null); + const b = newKeyComputation(1, "/app/v2", "prefix", null, null); + const c = newKeyComputation(1, "/app-v2", "prefix", null, null); + const keys = new Set([a, b, c]); + assertEquals( + keys.size, + 3, + "three paths must produce three unique keys" + ); + console.log( + " PASS: collision fix — three-way /app.v2, /app/v2, /app-v2" + ); + passed++; + } + + // ── Edge cases ─────────────────────────────────────────────────── + + // Test 12: same path in different resources — always separate + { + const key1 = newKeyComputation(1, "/api", "prefix", null, null); + const key2 = newKeyComputation(2, "/api", "prefix", null, null); + assertEquals( + key1 !== key2, + true, + "different resources with same path must have different keys" + ); + console.log(" PASS: edge case — same path, different resources"); + passed++; + } + + // Test 13: same resource, different pathMatchType — separate keys + { + const exact = newKeyComputation(1, "/api", "exact", null, null); + const prefix = newKeyComputation(1, "/api", "prefix", null, null); + assertEquals( + exact !== prefix, + true, + "exact vs prefix must have different keys" + ); + console.log(" PASS: edge case — same path, different match types"); + passed++; + } + + // Test 14: same resource and path, different rewrite config — separate keys + { + const noRewrite = newKeyComputation(1, "/api", "prefix", null, null); + const withRewrite = newKeyComputation( + 1, + "/api", + "prefix", + "/backend", + "prefix" + ); + assertEquals( + noRewrite !== withRewrite, + true, + "with vs without rewrite must have different keys" + ); + console.log(" PASS: edge case — same path, different rewrite config"); + passed++; + } + + // Test 15: paths with special URL characters + { + const paths = ["/api?foo", "/api#bar", "/api%20baz", "/api+qux"]; + const keys = new Set( + paths.map((p) => newKeyComputation(1, p, "prefix", null, null)) + ); + assertEquals( + keys.size, + paths.length, + "special URL chars must produce unique keys" + ); + console.log(" PASS: edge case — special URL characters in paths"); + passed++; + } + + console.log(`\nAll ${passed} tests passed!`); +} + +try { + runTests(); +} catch (error) { + console.error("Test failed:", error); + process.exit(1); +} diff --git a/server/lib/traefik/utils.ts b/server/lib/traefik/utils.ts index ec0eae5b3..34c293340 100644 --- a/server/lib/traefik/utils.ts +++ b/server/lib/traefik/utils.ts @@ -13,6 +13,26 @@ export function sanitize(input: string | null | undefined): string | undefined { .replace(/^-|-$/g, ""); } +/** + * Encode a URL path into a collision-free alphanumeric string suitable for use + * in Traefik map keys. + * + * Unlike sanitize(), this preserves uniqueness by encoding each non-alphanumeric + * character as its hex code. Different paths always produce different outputs. + * + * encodePath("/api") => "2fapi" + * encodePath("/a/b") => "2fa2fb" + * encodePath("/a-b") => "2fa2db" (different from /a/b) + * encodePath("/") => "2f" + * encodePath(null) => "" + */ +export function encodePath(path: string | null | undefined): string { + if (!path) return ""; + return path.replace(/[^a-zA-Z0-9]/g, (ch) => { + return ch.charCodeAt(0).toString(16); + }); +} + export function validatePathRewriteConfig( path: string | null, pathMatchType: string | null, diff --git a/server/middlewares/integration/index.ts b/server/middlewares/integration/index.ts index 565751913..df186c1c8 100644 --- a/server/middlewares/integration/index.ts +++ b/server/middlewares/integration/index.ts @@ -14,3 +14,4 @@ export * from "./verifyApiKeyApiKeyAccess"; export * from "./verifyApiKeyClientAccess"; export * from "./verifyApiKeySiteResourceAccess"; export * from "./verifyApiKeyIdpAccess"; +export * from "./verifyApiKeyDomainAccess"; diff --git a/server/middlewares/integration/verifyApiKeyDomainAccess.ts b/server/middlewares/integration/verifyApiKeyDomainAccess.ts new file mode 100644 index 000000000..db0f5d95d --- /dev/null +++ b/server/middlewares/integration/verifyApiKeyDomainAccess.ts @@ -0,0 +1,90 @@ +import { Request, Response, NextFunction } from "express"; +import { db, domains, orgDomains, apiKeyOrg } from "@server/db"; +import { and, eq } from "drizzle-orm"; +import createHttpError from "http-errors"; +import HttpCode from "@server/types/HttpCode"; + +export async function verifyApiKeyDomainAccess( + req: Request, + res: Response, + next: NextFunction +) { + try { + const apiKey = req.apiKey; + const domainId = + req.params.domainId || req.body.domainId || req.query.domainId; + const orgId = req.params.orgId; + + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + if (!domainId) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid domain ID") + ); + } + + if (apiKey.isRoot) { + // Root keys can access any domain in any org + return next(); + } + + // Verify domain exists and belongs to the organization + const [domain] = await db + .select() + .from(domains) + .innerJoin(orgDomains, eq(orgDomains.domainId, domains.domainId)) + .where( + and( + eq(orgDomains.domainId, domainId), + eq(orgDomains.orgId, orgId) + ) + ) + .limit(1); + + if (!domain) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `Domain with ID ${domainId} not found in organization ${orgId}` + ) + ); + } + + // Verify the API key has access to this organization + if (!req.apiKeyOrg) { + const apiKeyOrgRes = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, orgId) + ) + ) + .limit(1); + req.apiKeyOrg = apiKeyOrgRes[0]; + } + + if (!req.apiKeyOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to this organization" + ) + ); + } + + return next(); + } catch (error) { + return next( + createHttpError( + HttpCode.INTERNAL_SERVER_ERROR, + "Error verifying domain access" + ) + ); + } +} diff --git a/server/openApi.ts b/server/openApi.ts index 886265682..26c9e2f2e 100644 --- a/server/openApi.ts +++ b/server/openApi.ts @@ -5,17 +5,20 @@ export const registry = new OpenAPIRegistry(); export enum OpenAPITags { Site = "Site", Org = "Organization", - Resource = "Resource", + PublicResource = "Public Resource", + PrivateResource = "Private Resource", Role = "Role", User = "User", - Invitation = "Invitation", - Target = "Target", + Invitation = "User Invitation", + Target = "Resource Target", Rule = "Rule", AccessToken = "Access Token", - Idp = "Identity Provider", + GlobalIdp = "Identity Provider (Global)", + OrgIdp = "Identity Provider (Organization Only)", Client = "Client", ApiKey = "API Key", Domain = "Domain", Blueprint = "Blueprint", - Ssh = "SSH" + Ssh = "SSH", + Logs = "Logs" } diff --git a/server/private/cleanup.ts b/server/private/cleanup.ts index e9b305270..0bd9822dd 100644 --- a/server/private/cleanup.ts +++ b/server/private/cleanup.ts @@ -13,8 +13,12 @@ import { rateLimitService } from "#private/lib/rateLimit"; import { cleanup as wsCleanup } from "#private/routers/ws"; +import { flushBandwidthToDb } from "@server/routers/newt/handleReceiveBandwidthMessage"; +import { flushSiteBandwidthToDb } from "@server/routers/gerbil/receiveBandwidth"; async function cleanup() { + await flushBandwidthToDb(); + await flushSiteBandwidthToDb(); await rateLimitService.cleanup(); await wsCleanup(); @@ -25,4 +29,4 @@ export async function initCleanup() { // Handle process termination process.on("SIGTERM", () => cleanup()); process.on("SIGINT", () => cleanup()); -} +} \ No newline at end of file diff --git a/server/private/lib/cache.ts b/server/private/lib/cache.ts new file mode 100644 index 000000000..e8c03ba3d --- /dev/null +++ b/server/private/lib/cache.ts @@ -0,0 +1,287 @@ +import NodeCache from "node-cache"; +import logger from "@server/logger"; +import { redisManager } from "@server/private/lib/redis"; + +// Create local cache with maxKeys limit to prevent memory leaks +// With ~10k requests/day and 5min TTL, 10k keys should be more than sufficient +export const localCache = new NodeCache({ + stdTTL: 3600, + checkperiod: 120, + maxKeys: 10000 +}); + +// Log cache statistics periodically for monitoring +setInterval(() => { + const stats = localCache.getStats(); + logger.debug( + `Local cache stats - Keys: ${stats.keys}, Hits: ${stats.hits}, Misses: ${stats.misses}, Hit rate: ${stats.hits > 0 ? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(2) : 0}%` + ); +}, 300000); // Every 5 minutes + +/** + * Adaptive cache that uses Redis when available in multi-node environments, + * otherwise falls back to local memory cache for single-node deployments. + */ +class AdaptiveCache { + private useRedis(): boolean { + return ( + redisManager.isRedisEnabled() && + redisManager.getHealthStatus().isHealthy + ); + } + + /** + * Set a value in the cache + * @param key - Cache key + * @param value - Value to cache (will be JSON stringified for Redis) + * @param ttl - Time to live in seconds (0 = no expiration; omit = 3600s for Redis) + * @returns boolean indicating success + */ + async set(key: string, value: any, ttl?: number): Promise { + const effectiveTtl = ttl === 0 ? undefined : ttl; + const redisTtl = ttl === 0 ? undefined : (ttl ?? 3600); + + if (this.useRedis()) { + try { + const serialized = JSON.stringify(value); + const success = await redisManager.set( + key, + serialized, + redisTtl + ); + + if (success) { + logger.debug(`Set key in Redis: ${key}`); + return true; + } + + // Redis failed, fall through to local cache + logger.debug( + `Redis set failed for key ${key}, falling back to local cache` + ); + } catch (error) { + logger.error(`Redis set error for key ${key}:`, error); + // Fall through to local cache + } + } + + // Use local cache as fallback or primary + const success = localCache.set(key, value, effectiveTtl || 0); + if (success) { + logger.debug(`Set key in local cache: ${key}`); + } + return success; + } + + /** + * Get a value from the cache + * @param key - Cache key + * @returns The cached value or undefined if not found + */ + async get(key: string): Promise { + if (this.useRedis()) { + try { + const value = await redisManager.get(key); + + if (value !== null) { + logger.debug(`Cache hit in Redis: ${key}`); + return JSON.parse(value) as T; + } + + logger.debug(`Cache miss in Redis: ${key}`); + return undefined; + } catch (error) { + logger.error(`Redis get error for key ${key}:`, error); + // Fall through to local cache + } + } + + // Use local cache as fallback or primary + const value = localCache.get(key); + if (value !== undefined) { + logger.debug(`Cache hit in local cache: ${key}`); + } else { + logger.debug(`Cache miss in local cache: ${key}`); + } + return value; + } + + /** + * Delete a value from the cache + * @param key - Cache key or array of keys + * @returns Number of deleted entries + */ + async del(key: string | string[]): Promise { + const keys = Array.isArray(key) ? key : [key]; + let deletedCount = 0; + + if (this.useRedis()) { + try { + for (const k of keys) { + const success = await redisManager.del(k); + if (success) { + deletedCount++; + logger.debug(`Deleted key from Redis: ${k}`); + } + } + + if (deletedCount === keys.length) { + return deletedCount; + } + + // Some Redis deletes failed, fall through to local cache + logger.debug( + `Some Redis deletes failed, falling back to local cache` + ); + } catch (error) { + logger.error( + `Redis del error for keys ${keys.join(", ")}:`, + error + ); + // Fall through to local cache + deletedCount = 0; + } + } + + // Use local cache as fallback or primary + for (const k of keys) { + const success = localCache.del(k); + if (success > 0) { + deletedCount++; + logger.debug(`Deleted key from local cache: ${k}`); + } + } + + return deletedCount; + } + + /** + * Check if a key exists in the cache + * @param key - Cache key + * @returns boolean indicating if key exists + */ + async has(key: string): Promise { + if (this.useRedis()) { + try { + const value = await redisManager.get(key); + return value !== null; + } catch (error) { + logger.error(`Redis has error for key ${key}:`, error); + // Fall through to local cache + } + } + + // Use local cache as fallback or primary + return localCache.has(key); + } + + /** + * Get multiple values from the cache + * @param keys - Array of cache keys + * @returns Array of values (undefined for missing keys) + */ + async mget(keys: string[]): Promise<(T | undefined)[]> { + if (this.useRedis()) { + try { + const results: (T | undefined)[] = []; + + for (const key of keys) { + const value = await redisManager.get(key); + if (value !== null) { + results.push(JSON.parse(value) as T); + } else { + results.push(undefined); + } + } + + return results; + } catch (error) { + logger.error(`Redis mget error:`, error); + // Fall through to local cache + } + } + + // Use local cache as fallback or primary + return keys.map((key) => localCache.get(key)); + } + + /** + * Flush all keys from the cache + */ + async flushAll(): Promise { + if (this.useRedis()) { + logger.warn( + "Adaptive cache flushAll called - Redis flush not implemented, only local cache will be flushed" + ); + } + + localCache.flushAll(); + logger.debug("Flushed local cache"); + } + + /** + * Get cache statistics + * Note: Only returns local cache stats, Redis stats are not included + */ + getStats() { + return localCache.getStats(); + } + + /** + * Get the current cache backend being used + * @returns "redis" if Redis is available and healthy, "local" otherwise + */ + getCurrentBackend(): "redis" | "local" { + return this.useRedis() ? "redis" : "local"; + } + + /** + * Take a key from the cache and delete it + * @param key - Cache key + * @returns The value or undefined if not found + */ + async take(key: string): Promise { + const value = await this.get(key); + if (value !== undefined) { + await this.del(key); + } + return value; + } + + /** + * Get TTL (time to live) for a key + * @param key - Cache key + * @returns TTL in seconds, 0 if no expiration, -1 if key doesn't exist + */ + getTtl(key: string): number { + // Note: This only works for local cache, Redis TTL is not supported + if (this.useRedis()) { + logger.warn( + `getTtl called for key ${key} but Redis TTL lookup is not implemented` + ); + } + + const ttl = localCache.getTtl(key); + if (ttl === undefined) { + return -1; + } + return Math.max(0, Math.floor((ttl - Date.now()) / 1000)); + } + + /** + * Get all keys from the cache + * Note: Only returns local cache keys, Redis keys are not included + */ + keys(): string[] { + if (this.useRedis()) { + logger.warn( + "keys() called but Redis keys are not included, only local cache keys returned" + ); + } + return localCache.keys(); + } +} + +// Export singleton instance +export const cache = new AdaptiveCache(); +export default cache; diff --git a/server/private/lib/certificates.ts b/server/private/lib/certificates.ts index bc1dffcd4..1ec524bb0 100644 --- a/server/private/lib/certificates.ts +++ b/server/private/lib/certificates.ts @@ -15,9 +15,8 @@ import config from "./config"; import { certificates, db } from "@server/db"; import { and, eq, isNotNull, or, inArray, sql } from "drizzle-orm"; import { decryptData } from "@server/lib/encryption"; -import * as fs from "fs"; import logger from "@server/logger"; -import cache from "@server/lib/cache"; +import cache from "#private/lib/cache"; let encryptionKeyHex = ""; let encryptionKey: Buffer; @@ -55,7 +54,7 @@ export async function getValidCertificatesForDomains( if (useCache) { for (const domain of domains) { const cacheKey = `cert:${domain}`; - const cachedCert = cache.get(cacheKey); + const cachedCert = await cache.get(cacheKey); if (cachedCert) { finalResults.push(cachedCert); // Valid cache hit } else { @@ -169,7 +168,7 @@ export async function getValidCertificatesForDomains( // Add to cache for future requests, using the *requested domain* as the key if (useCache) { const cacheKey = `cert:${domain}`; - cache.set(cacheKey, resultCert, 180); + await cache.set(cacheKey, resultCert, 180); } } } diff --git a/server/private/lib/logAccessAudit.ts b/server/private/lib/logAccessAudit.ts index 33dcaf1ff..91db548f7 100644 --- a/server/private/lib/logAccessAudit.ts +++ b/server/private/lib/logAccessAudit.ts @@ -11,17 +11,17 @@ * This file is not licensed under the AGPLv3. */ -import { accessAuditLog, db, orgs } from "@server/db"; +import { accessAuditLog, logsDb, db, orgs } from "@server/db"; import { getCountryCodeForIp } from "@server/lib/geoip"; import logger from "@server/logger"; import { and, eq, lt } from "drizzle-orm"; -import cache from "@server/lib/cache"; +import cache from "#private/lib/cache"; import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs"; import { stripPortFromHost } from "@server/lib/ip"; async function getAccessDays(orgId: string): Promise { // check cache first - const cached = cache.get(`org_${orgId}_accessDays`); + const cached = await cache.get(`org_${orgId}_accessDays`); if (cached !== undefined) { return cached; } @@ -39,7 +39,7 @@ async function getAccessDays(orgId: string): Promise { } // store the result in cache - cache.set( + await cache.set( `org_${orgId}_accessDays`, org.settingsLogRetentionDaysAction, 300 @@ -52,7 +52,7 @@ export async function cleanUpOldLogs(orgId: string, retentionDays: number) { const cutoffTimestamp = calculateCutoffTimestamp(retentionDays); try { - await db + await logsDb .delete(accessAuditLog) .where( and( @@ -124,7 +124,7 @@ export async function logAccessAudit(data: { ? await getCountryCodeFromIp(data.requestIp) : undefined; - await db.insert(accessAuditLog).values({ + await logsDb.insert(accessAuditLog).values({ timestamp: timestamp, orgId: data.orgId, actorType, @@ -146,14 +146,14 @@ export async function logAccessAudit(data: { async function getCountryCodeFromIp(ip: string): Promise { const geoIpCacheKey = `geoip_access:${ip}`; - let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey); + let cachedCountryCode: string | undefined = await cache.get(geoIpCacheKey); if (!cachedCountryCode) { cachedCountryCode = await getCountryCodeForIp(ip); // do it locally // Only cache successful lookups to avoid filling cache with undefined values if (cachedCountryCode) { // Cache for longer since IP geolocation doesn't change frequently - cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes + await cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes } } diff --git a/server/private/lib/readConfigFile.ts b/server/private/lib/readConfigFile.ts index a9de84e82..0ce6d0272 100644 --- a/server/private/lib/readConfigFile.ts +++ b/server/private/lib/readConfigFile.ts @@ -38,10 +38,6 @@ export const privateConfigSchema = z.object({ .string() .optional() .transform(getEnvOrYaml("SERVER_ENCRYPTION_KEY")), - resend_api_key: z - .string() - .optional() - .transform(getEnvOrYaml("RESEND_API_KEY")), reo_client_id: z .string() .optional() diff --git a/server/private/lib/resend.ts b/server/private/lib/resend.ts deleted file mode 100644 index 42a11c152..000000000 --- a/server/private/lib/resend.ts +++ /dev/null @@ -1,127 +0,0 @@ -/* - * This file is part of a proprietary work. - * - * Copyright (c) 2025 Fossorial, Inc. - * All rights reserved. - * - * This file is licensed under the Fossorial Commercial License. - * You may not use this file except in compliance with the License. - * Unauthorized use, copying, modification, or distribution is strictly prohibited. - * - * This file is not licensed under the AGPLv3. - */ - -import { Resend } from "resend"; -import privateConfig from "#private/lib/config"; -import logger from "@server/logger"; - -export enum AudienceIds { - SignUps = "6c4e77b2-0851-4bd6-bac8-f51f91360f1a", - Subscribed = "870b43fd-387f-44de-8fc1-707335f30b20", - Churned = "f3ae92bd-2fdb-4d77-8746-2118afd62549", - Newsletter = "5500c431-191c-42f0-a5d4-8b6d445b4ea0" -} - -const resend = new Resend( - privateConfig.getRawPrivateConfig().server.resend_api_key || "missing" -); - -export default resend; - -export async function moveEmailToAudience( - email: string, - audienceId: AudienceIds -) { - if (process.env.ENVIRONMENT !== "prod") { - logger.debug( - `Skipping moving email ${email} to audience ${audienceId} in non-prod environment` - ); - return; - } - const { error, data } = await retryWithBackoff(async () => { - const { data, error } = await resend.contacts.create({ - email, - unsubscribed: false, - audienceId - }); - if (error) { - throw new Error( - `Error adding email ${email} to audience ${audienceId}: ${error}` - ); - } - return { error, data }; - }); - - if (error) { - logger.error( - `Error adding email ${email} to audience ${audienceId}: ${error}` - ); - return; - } - - if (data) { - logger.debug( - `Added email ${email} to audience ${audienceId} with contact ID ${data.id}` - ); - } - - const otherAudiences = Object.values(AudienceIds).filter( - (id) => id !== audienceId - ); - - for (const otherAudienceId of otherAudiences) { - const { error, data } = await retryWithBackoff(async () => { - const { data, error } = await resend.contacts.remove({ - email, - audienceId: otherAudienceId - }); - if (error) { - throw new Error( - `Error removing email ${email} from audience ${otherAudienceId}: ${error}` - ); - } - return { error, data }; - }); - - if (error) { - logger.error( - `Error removing email ${email} from audience ${otherAudienceId}: ${error}` - ); - } - - if (data) { - logger.info( - `Removed email ${email} from audience ${otherAudienceId}` - ); - } - } -} - -type RetryOptions = { - retries?: number; - initialDelayMs?: number; - factor?: number; -}; - -export async function retryWithBackoff( - fn: () => Promise, - options: RetryOptions = {} -): Promise { - const { retries = 5, initialDelayMs = 500, factor = 2 } = options; - - let attempt = 0; - let delay = initialDelayMs; - - while (true) { - try { - return await fn(); - } catch (err) { - attempt++; - - if (attempt > retries) throw err; - - await new Promise((resolve) => setTimeout(resolve, delay)); - delay *= factor; - } - } -} diff --git a/server/private/lib/traefik/getTraefikConfig.ts b/server/private/lib/traefik/getTraefikConfig.ts index f0343c5d4..7fc0ae647 100644 --- a/server/private/lib/traefik/getTraefikConfig.ts +++ b/server/private/lib/traefik/getTraefikConfig.ts @@ -34,7 +34,11 @@ import { import logger from "@server/logger"; import config from "@server/lib/config"; import { orgs, resources, sites, Target, targets } from "@server/db"; -import { sanitize, validatePathRewriteConfig } from "@server/lib/traefik/utils"; +import { + sanitize, + encodePath, + validatePathRewriteConfig +} from "@server/lib/traefik/utils"; import privateConfig from "#private/lib/config"; import createPathRewriteMiddleware from "@server/lib/traefik/middleware"; import { @@ -170,7 +174,7 @@ export async function getTraefikConfig( resourcesWithTargetsAndSites.forEach((row) => { const resourceId = row.resourceId; const resourceName = sanitize(row.resourceName) || ""; - const targetPath = sanitize(row.path) || ""; // Handle null/undefined paths + const targetPath = encodePath(row.path); // Use encodePath to avoid collisions (e.g. "/a/b" vs "/a-b") const pathMatchType = row.pathMatchType || ""; const rewritePath = row.rewritePath || ""; const rewritePathType = row.rewritePathType || ""; @@ -192,7 +196,7 @@ export async function getTraefikConfig( const mapKey = [resourceId, pathKey].filter(Boolean).join("-"); const key = sanitize(mapKey); - if (!resourcesMap.has(key)) { + if (!resourcesMap.has(mapKey)) { const validation = validatePathRewriteConfig( row.path, row.pathMatchType, @@ -207,9 +211,10 @@ export async function getTraefikConfig( return; } - resourcesMap.set(key, { + resourcesMap.set(mapKey, { resourceId: row.resourceId, name: resourceName, + key: key, fullDomain: row.fullDomain, ssl: row.ssl, http: row.http, @@ -243,7 +248,7 @@ export async function getTraefikConfig( } // Add target with its associated site data - resourcesMap.get(key).targets.push({ + resourcesMap.get(mapKey).targets.push({ resourceId: row.resourceId, targetId: row.targetId, ip: row.ip, @@ -296,8 +301,9 @@ export async function getTraefikConfig( }; // get the key and the resource - for (const [key, resource] of resourcesMap.entries()) { + for (const [, resource] of resourcesMap.entries()) { const targets = resource.targets as TargetWithSite[]; + const key = resource.key; const routerName = `${key}-${resource.name}-router`; const serviceName = `${key}-${resource.name}-service`; @@ -665,7 +671,10 @@ export async function getTraefikConfig( // TODO: HOW TO HANDLE ^^^^^^ BETTER const anySitesOnline = targets.some( - (target) => target.site.online + (target) => + target.site.online || + target.site.type === "local" || + target.site.type === "wireguard" ); return ( @@ -793,7 +802,10 @@ export async function getTraefikConfig( servers: (() => { // Check if any sites are online const anySitesOnline = targets.some( - (target) => target.site.online + (target) => + target.site.online || + target.site.type === "local" || + target.site.type === "wireguard" ); return targets diff --git a/server/private/middlewares/logActionAudit.ts b/server/private/middlewares/logActionAudit.ts index 17cc67c08..f62f43d3a 100644 --- a/server/private/middlewares/logActionAudit.ts +++ b/server/private/middlewares/logActionAudit.ts @@ -12,18 +12,18 @@ */ import { ActionsEnum } from "@server/auth/actions"; -import { actionAuditLog, db, orgs } from "@server/db"; +import { actionAuditLog, logsDb, db, orgs } from "@server/db"; import logger from "@server/logger"; import HttpCode from "@server/types/HttpCode"; import { Request, Response, NextFunction } from "express"; import createHttpError from "http-errors"; import { and, eq, lt } from "drizzle-orm"; -import cache from "@server/lib/cache"; +import cache from "#private/lib/cache"; import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs"; async function getActionDays(orgId: string): Promise { // check cache first - const cached = cache.get(`org_${orgId}_actionDays`); + const cached = await cache.get(`org_${orgId}_actionDays`); if (cached !== undefined) { return cached; } @@ -41,7 +41,7 @@ async function getActionDays(orgId: string): Promise { } // store the result in cache - cache.set( + await cache.set( `org_${orgId}_actionDays`, org.settingsLogRetentionDaysAction, 300 @@ -54,7 +54,7 @@ export async function cleanUpOldLogs(orgId: string, retentionDays: number) { const cutoffTimestamp = calculateCutoffTimestamp(retentionDays); try { - await db + await logsDb .delete(actionAuditLog) .where( and( @@ -123,7 +123,7 @@ export function logActionAudit(action: ActionsEnum) { metadata = JSON.stringify(req.params); } - await db.insert(actionAuditLog).values({ + await logsDb.insert(actionAuditLog).values({ timestamp, orgId, actorType, diff --git a/server/private/routers/auditLogs/exportAccessAuditLog.ts b/server/private/routers/auditLogs/exportAccessAuditLog.ts index 7e912f8c8..68a78ff6e 100644 --- a/server/private/routers/auditLogs/exportAccessAuditLog.ts +++ b/server/private/routers/auditLogs/exportAccessAuditLog.ts @@ -32,7 +32,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/logs/access/export", description: "Export the access audit log for an organization as CSV", - tags: [OpenAPITags.Org], + tags: [OpenAPITags.Logs], request: { query: queryAccessAuditLogsQuery, params: queryAccessAuditLogsParams diff --git a/server/private/routers/auditLogs/exportActionAuditLog.ts b/server/private/routers/auditLogs/exportActionAuditLog.ts index d8987916b..853183b92 100644 --- a/server/private/routers/auditLogs/exportActionAuditLog.ts +++ b/server/private/routers/auditLogs/exportActionAuditLog.ts @@ -32,7 +32,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/logs/action/export", description: "Export the action audit log for an organization as CSV", - tags: [OpenAPITags.Org], + tags: [OpenAPITags.Logs], request: { query: queryActionAuditLogsQuery, params: queryActionAuditLogsParams diff --git a/server/private/routers/auditLogs/queryAccessAuditLog.ts b/server/private/routers/auditLogs/queryAccessAuditLog.ts index 96d241fbb..f0f45a826 100644 --- a/server/private/routers/auditLogs/queryAccessAuditLog.ts +++ b/server/private/routers/auditLogs/queryAccessAuditLog.ts @@ -11,11 +11,11 @@ * This file is not licensed under the AGPLv3. */ -import { accessAuditLog, db, resources } from "@server/db"; +import { accessAuditLog, logsDb, resources, db, primaryDb } from "@server/db"; import { registry } from "@server/openApi"; import { NextFunction } from "express"; import { Request, Response } from "express"; -import { eq, gt, lt, and, count, desc } from "drizzle-orm"; +import { eq, gt, lt, and, count, desc, inArray } from "drizzle-orm"; import { OpenAPITags } from "@server/openApi"; import { z } from "zod"; import createHttpError from "http-errors"; @@ -115,15 +115,13 @@ function getWhere(data: Q) { } export function queryAccess(data: Q) { - return db + return logsDb .select({ orgId: accessAuditLog.orgId, action: accessAuditLog.action, actorType: accessAuditLog.actorType, actorId: accessAuditLog.actorId, resourceId: accessAuditLog.resourceId, - resourceName: resources.name, - resourceNiceId: resources.niceId, ip: accessAuditLog.ip, location: accessAuditLog.location, userAgent: accessAuditLog.userAgent, @@ -133,16 +131,46 @@ export function queryAccess(data: Q) { actor: accessAuditLog.actor }) .from(accessAuditLog) - .leftJoin( - resources, - eq(accessAuditLog.resourceId, resources.resourceId) - ) .where(getWhere(data)) .orderBy(desc(accessAuditLog.timestamp), desc(accessAuditLog.id)); } +async function enrichWithResourceDetails(logs: Awaited>) { + // If logs database is the same as main database, we can do a join + // Otherwise, we need to fetch resource details separately + const resourceIds = logs + .map(log => log.resourceId) + .filter((id): id is number => id !== null && id !== undefined); + + if (resourceIds.length === 0) { + return logs.map(log => ({ ...log, resourceName: null, resourceNiceId: null })); + } + + // Fetch resource details from main database + const resourceDetails = await primaryDb + .select({ + resourceId: resources.resourceId, + name: resources.name, + niceId: resources.niceId + }) + .from(resources) + .where(inArray(resources.resourceId, resourceIds)); + + // Create a map for quick lookup + const resourceMap = new Map( + resourceDetails.map(r => [r.resourceId, { name: r.name, niceId: r.niceId }]) + ); + + // Enrich logs with resource details + return logs.map(log => ({ + ...log, + resourceName: log.resourceId ? resourceMap.get(log.resourceId)?.name ?? null : null, + resourceNiceId: log.resourceId ? resourceMap.get(log.resourceId)?.niceId ?? null : null + })); +} + export function countAccessQuery(data: Q) { - const countQuery = db + const countQuery = logsDb .select({ count: count() }) .from(accessAuditLog) .where(getWhere(data)); @@ -161,7 +189,7 @@ async function queryUniqueFilterAttributes( ); // Get unique actors - const uniqueActors = await db + const uniqueActors = await logsDb .selectDistinct({ actor: accessAuditLog.actor }) @@ -169,7 +197,7 @@ async function queryUniqueFilterAttributes( .where(baseConditions); // Get unique locations - const uniqueLocations = await db + const uniqueLocations = await logsDb .selectDistinct({ locations: accessAuditLog.location }) @@ -177,25 +205,40 @@ async function queryUniqueFilterAttributes( .where(baseConditions); // Get unique resources with names - const uniqueResources = await db + const uniqueResources = await logsDb .selectDistinct({ - id: accessAuditLog.resourceId, - name: resources.name + id: accessAuditLog.resourceId }) .from(accessAuditLog) - .leftJoin( - resources, - eq(accessAuditLog.resourceId, resources.resourceId) - ) .where(baseConditions); + // Fetch resource names from main database for the unique resource IDs + const resourceIds = uniqueResources + .map(row => row.id) + .filter((id): id is number => id !== null); + + let resourcesWithNames: Array<{ id: number; name: string | null }> = []; + + if (resourceIds.length > 0) { + const resourceDetails = await primaryDb + .select({ + resourceId: resources.resourceId, + name: resources.name + }) + .from(resources) + .where(inArray(resources.resourceId, resourceIds)); + + resourcesWithNames = resourceDetails.map(r => ({ + id: r.resourceId, + name: r.name + })); + } + return { actors: uniqueActors .map((row) => row.actor) .filter((actor): actor is string => actor !== null), - resources: uniqueResources.filter( - (row): row is { id: number; name: string | null } => row.id !== null - ), + resources: resourcesWithNames, locations: uniqueLocations .map((row) => row.locations) .filter((location): location is string => location !== null) @@ -206,7 +249,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/logs/access", description: "Query the access audit log for an organization", - tags: [OpenAPITags.Org], + tags: [OpenAPITags.Logs], request: { query: queryAccessAuditLogsQuery, params: queryAccessAuditLogsParams @@ -243,7 +286,10 @@ export async function queryAccessAuditLogs( const baseQuery = queryAccess(data); - const log = await baseQuery.limit(data.limit).offset(data.offset); + const logsRaw = await baseQuery.limit(data.limit).offset(data.offset); + + // Enrich with resource details (handles cross-database scenario) + const log = await enrichWithResourceDetails(logsRaw); const totalCountResult = await countAccessQuery(data); const totalCount = totalCountResult[0].count; diff --git a/server/private/routers/auditLogs/queryActionAuditLog.ts b/server/private/routers/auditLogs/queryActionAuditLog.ts index 7eed741b3..8bbe73ee1 100644 --- a/server/private/routers/auditLogs/queryActionAuditLog.ts +++ b/server/private/routers/auditLogs/queryActionAuditLog.ts @@ -11,7 +11,7 @@ * This file is not licensed under the AGPLv3. */ -import { actionAuditLog, db } from "@server/db"; +import { actionAuditLog, logsDb } from "@server/db"; import { registry } from "@server/openApi"; import { NextFunction } from "express"; import { Request, Response } from "express"; @@ -97,7 +97,7 @@ function getWhere(data: Q) { } export function queryAction(data: Q) { - return db + return logsDb .select({ orgId: actionAuditLog.orgId, action: actionAuditLog.action, @@ -113,7 +113,7 @@ export function queryAction(data: Q) { } export function countActionQuery(data: Q) { - const countQuery = db + const countQuery = logsDb .select({ count: count() }) .from(actionAuditLog) .where(getWhere(data)); @@ -132,14 +132,14 @@ async function queryUniqueFilterAttributes( ); // Get unique actors - const uniqueActors = await db + const uniqueActors = await logsDb .selectDistinct({ actor: actionAuditLog.actor }) .from(actionAuditLog) .where(baseConditions); - const uniqueActions = await db + const uniqueActions = await logsDb .selectDistinct({ action: actionAuditLog.action }) @@ -160,7 +160,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/logs/action", description: "Query the action audit log for an organization", - tags: [OpenAPITags.Org], + tags: [OpenAPITags.Logs], request: { query: queryActionAuditLogsQuery, params: queryActionAuditLogsParams diff --git a/server/private/routers/billing/getOrgUsage.ts b/server/private/routers/billing/getOrgUsage.ts index 4c9f22f3d..cc722cec8 100644 --- a/server/private/routers/billing/getOrgUsage.ts +++ b/server/private/routers/billing/getOrgUsage.ts @@ -31,16 +31,16 @@ const getOrgSchema = z.strictObject({ orgId: z.string() }); -registry.registerPath({ - method: "get", - path: "/org/{orgId}/billing/usage", - description: "Get an organization's billing usage", - tags: [OpenAPITags.Org], - request: { - params: getOrgSchema - }, - responses: {} -}); +// registry.registerPath({ +// method: "get", +// path: "/org/{orgId}/billing/usage", +// description: "Get an organization's billing usage", +// tags: [OpenAPITags.Org], +// request: { +// params: getOrgSchema +// }, +// responses: {} +// }); export async function getOrgUsage( req: Request, diff --git a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts index 1152f223e..a40142526 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionCreated.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionCreated.ts @@ -24,7 +24,6 @@ import { eq, and } from "drizzle-orm"; import logger from "@server/logger"; import stripe from "#private/lib/stripe"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; -import { AudienceIds, moveEmailToAudience } from "#private/lib/resend"; import { getSubType } from "./getSubType"; import privateConfig from "#private/lib/config"; import { getLicensePriceSet, LicenseId } from "@server/lib/billing/licenses"; @@ -172,7 +171,7 @@ export async function handleSubscriptionCreated( const email = orgUserRes.user.email; if (email) { - moveEmailToAudience(email, AudienceIds.Subscribed); + // TODO: update user in Sendy } } } else if (type === "license") { diff --git a/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts index d92741be8..a029fc5c3 100644 --- a/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts +++ b/server/private/routers/billing/hooks/handleSubscriptionDeleted.ts @@ -23,7 +23,6 @@ import { import { eq, and } from "drizzle-orm"; import logger from "@server/logger"; import { handleSubscriptionLifesycle } from "../subscriptionLifecycle"; -import { AudienceIds, moveEmailToAudience } from "#private/lib/resend"; import { getSubType } from "./getSubType"; import stripe from "#private/lib/stripe"; import privateConfig from "#private/lib/config"; @@ -109,7 +108,7 @@ export async function handleSubscriptionDeleted( const email = orgUserRes.user.email; if (email) { - moveEmailToAudience(email, AudienceIds.Churned); + // TODO: update user in Sendy } } } else if (type === "license") { diff --git a/server/private/routers/external.ts b/server/private/routers/external.ts index a13523420..df8ea8cbb 100644 --- a/server/private/routers/external.ts +++ b/server/private/routers/external.ts @@ -480,9 +480,9 @@ authenticated.get( authenticated.post( "/re-key/:clientId/regenerate-client-secret", + verifyClientAccess, // this is first to set the org id verifyValidLicense, verifyValidSubscription(tierMatrix.rotateCredentials), - verifyClientAccess, // this is first to set the org id verifyLimits, verifyUserHasAction(ActionsEnum.reGenerateSecret), reKey.reGenerateClientSecret @@ -490,9 +490,9 @@ authenticated.post( authenticated.post( "/re-key/:siteId/regenerate-site-secret", + verifySiteAccess, // this is first to set the org id verifyValidLicense, verifyValidSubscription(tierMatrix.rotateCredentials), - verifySiteAccess, // this is first to set the org id verifyLimits, verifyUserHasAction(ActionsEnum.reGenerateSecret), reKey.reGenerateSiteSecret @@ -515,6 +515,6 @@ authenticated.post( verifyOrgAccess, verifyLimits, verifyUserHasAction(ActionsEnum.signSshKey), - logActionAudit(ActionsEnum.signSshKey), + // logActionAudit(ActionsEnum.signSshKey), // it is handled inside of the function below so we can include more metadata ssh.signSshKey ); diff --git a/server/private/routers/hybrid.ts b/server/private/routers/hybrid.ts index 0e5d1ec2e..a38385b0c 100644 --- a/server/private/routers/hybrid.ts +++ b/server/private/routers/hybrid.ts @@ -15,6 +15,7 @@ import { verifySessionRemoteExitNodeMiddleware } from "#private/middlewares/veri import { Router } from "express"; import { db, + logsDb, exitNodes, Resource, ResourcePassword, @@ -81,6 +82,7 @@ import { verifyResourceAccessToken } from "@server/auth/verifyResourceAccessToke import semver from "semver"; import { maxmindAsnLookup } from "@server/db/maxmindAsn"; import { checkOrgAccessPolicy } from "@server/lib/checkOrgAccessPolicy"; +import { sanitizeString } from "@server/lib/sanitize"; // Zod schemas for request validation const getResourceByDomainParamsSchema = z.strictObject({ @@ -1859,24 +1861,24 @@ hybridRouter.post( }) .map((logEntry) => ({ timestamp: logEntry.timestamp, - orgId: logEntry.orgId, - actorType: logEntry.actorType, - actor: logEntry.actor, - actorId: logEntry.actorId, - metadata: logEntry.metadata, + orgId: sanitizeString(logEntry.orgId), + actorType: sanitizeString(logEntry.actorType), + actor: sanitizeString(logEntry.actor), + actorId: sanitizeString(logEntry.actorId), + metadata: sanitizeString(logEntry.metadata), action: logEntry.action, resourceId: logEntry.resourceId, reason: logEntry.reason, - location: logEntry.location, + location: sanitizeString(logEntry.location), // userAgent: data.userAgent, // TODO: add this // headers: data.body.headers, // query: data.body.query, - originalRequestURL: logEntry.originalRequestURL, - scheme: logEntry.scheme, - host: logEntry.host, - path: logEntry.path, - method: logEntry.method, - ip: logEntry.ip, + originalRequestURL: sanitizeString(logEntry.originalRequestURL) ?? "", + scheme: sanitizeString(logEntry.scheme) ?? "", + host: sanitizeString(logEntry.host) ?? "", + path: sanitizeString(logEntry.path) ?? "", + method: sanitizeString(logEntry.method) ?? "", + ip: sanitizeString(logEntry.ip), tls: logEntry.tls })); @@ -1884,7 +1886,7 @@ hybridRouter.post( const batchSize = 100; for (let i = 0; i < logEntries.length; i += batchSize) { const batch = logEntries.slice(i, i + batchSize); - await db.insert(requestAuditLog).values(batch); + await logsDb.insert(requestAuditLog).values(batch); } return response(res, { diff --git a/server/private/routers/orgIdp/createOrgOidcIdp.ts b/server/private/routers/orgIdp/createOrgOidcIdp.ts index 725e93c72..cc17d7cfc 100644 --- a/server/private/routers/orgIdp/createOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/createOrgOidcIdp.ts @@ -52,7 +52,7 @@ registry.registerPath({ method: "put", path: "/org/{orgId}/idp/oidc", description: "Create an OIDC IdP for a specific organization.", - tags: [OpenAPITags.Idp, OpenAPITags.Org], + tags: [OpenAPITags.OrgIdp], request: { params: paramsSchema, body: { diff --git a/server/private/routers/orgIdp/deleteOrgIdp.ts b/server/private/routers/orgIdp/deleteOrgIdp.ts index 2d6b0899b..7d201dd17 100644 --- a/server/private/routers/orgIdp/deleteOrgIdp.ts +++ b/server/private/routers/orgIdp/deleteOrgIdp.ts @@ -35,7 +35,7 @@ registry.registerPath({ method: "delete", path: "/org/{orgId}/idp/{idpId}", description: "Delete IDP for a specific organization.", - tags: [OpenAPITags.Idp, OpenAPITags.Org], + tags: [OpenAPITags.OrgIdp], request: { params: paramsSchema }, diff --git a/server/private/routers/orgIdp/getOrgIdp.ts b/server/private/routers/orgIdp/getOrgIdp.ts index dd987c443..6941fc0fc 100644 --- a/server/private/routers/orgIdp/getOrgIdp.ts +++ b/server/private/routers/orgIdp/getOrgIdp.ts @@ -50,9 +50,9 @@ async function query(idpId: number, orgId: string) { registry.registerPath({ method: "get", - path: "/org/:orgId/idp/:idpId", + path: "/org/{orgId}/idp/{idpId}", description: "Get an IDP by its IDP ID for a specific organization.", - tags: [OpenAPITags.Idp, OpenAPITags.Org], + tags: [OpenAPITags.OrgIdp], request: { params: paramsSchema }, diff --git a/server/private/routers/orgIdp/listOrgIdps.ts b/server/private/routers/orgIdp/listOrgIdps.ts index b6cf48ac0..fed8a0aab 100644 --- a/server/private/routers/orgIdp/listOrgIdps.ts +++ b/server/private/routers/orgIdp/listOrgIdps.ts @@ -67,7 +67,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/idp", description: "List all IDP for a specific organization.", - tags: [OpenAPITags.Idp, OpenAPITags.Org], + tags: [OpenAPITags.OrgIdp], request: { query: querySchema, params: paramsSchema diff --git a/server/private/routers/orgIdp/updateOrgOidcIdp.ts b/server/private/routers/orgIdp/updateOrgOidcIdp.ts index b8ee4dcb0..191f49068 100644 --- a/server/private/routers/orgIdp/updateOrgOidcIdp.ts +++ b/server/private/routers/orgIdp/updateOrgOidcIdp.ts @@ -59,7 +59,7 @@ registry.registerPath({ method: "post", path: "/org/{orgId}/idp/{idpId}/oidc", description: "Update an OIDC IdP for a specific organization.", - tags: [OpenAPITags.Idp, OpenAPITags.Org], + tags: [OpenAPITags.OrgIdp], request: { params: paramsSchema, body: { diff --git a/server/private/routers/remoteExitNode/handleRemoteExitNodePingMessage.ts b/server/private/routers/remoteExitNode/handleRemoteExitNodePingMessage.ts index dafc14121..9c2889a99 100644 --- a/server/private/routers/remoteExitNode/handleRemoteExitNodePingMessage.ts +++ b/server/private/routers/remoteExitNode/handleRemoteExitNodePingMessage.ts @@ -38,7 +38,7 @@ export const startRemoteExitNodeOfflineChecker = (): void => { ); // Find clients that haven't pinged in the last 2 minutes and mark them as offline - const newlyOfflineNodes = await db + const offlineNodes = await db .update(exitNodes) .set({ online: false }) .where( @@ -53,32 +53,15 @@ export const startRemoteExitNodeOfflineChecker = (): void => { ) .returning(); - // Update the sites to offline if they have not pinged either - const exitNodeIds = newlyOfflineNodes.map( - (node) => node.exitNodeId - ); - - const sitesOnNode = await db - .select() - .from(sites) - .where( - and( - eq(sites.online, true), - inArray(sites.exitNodeId, exitNodeIds) - ) + if (offlineNodes.length > 0) { + logger.info( + `checkRemoteExitNodeOffline: Marked ${offlineNodes.length} remoteExitNode client(s) offline due to inactivity` ); - // loop through the sites and process their lastBandwidthUpdate as an iso string and if its more than 1 minute old then mark the site offline - for (const site of sitesOnNode) { - if (!site.lastBandwidthUpdate) { - continue; - } - const lastBandwidthUpdate = new Date(site.lastBandwidthUpdate); - if (Date.now() - lastBandwidthUpdate.getTime() > 60 * 1000) { - await db - .update(sites) - .set({ online: false }) - .where(eq(sites.siteId, site.siteId)); + for (const offlineClient of offlineNodes) { + logger.debug( + `checkRemoteExitNodeOffline: Client ${offlineClient.exitNodeId} marked offline (lastPing: ${offlineClient.lastPing})` + ); } } } catch (error) { diff --git a/server/private/routers/resource/getMaintenanceInfo.ts b/server/private/routers/resource/getMaintenanceInfo.ts index cbba25194..e3e739c6e 100644 --- a/server/private/routers/resource/getMaintenanceInfo.ts +++ b/server/private/routers/resource/getMaintenanceInfo.ts @@ -52,7 +52,7 @@ registry.registerPath({ method: "get", path: "/maintenance/info", description: "Get maintenance information for a resource by domain.", - tags: [OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { query: z.object({ fullDomain: z.string() diff --git a/server/private/routers/ssh/signSshKey.ts b/server/private/routers/ssh/signSshKey.ts index f45db3c85..4a35bb89a 100644 --- a/server/private/routers/ssh/signSshKey.ts +++ b/server/private/routers/ssh/signSshKey.ts @@ -14,7 +14,9 @@ import { Request, Response, NextFunction } from "express"; import { z } from "zod"; import { + actionAuditLog, db, + logsDb, newts, roles, roundTripMessageTracker, @@ -29,12 +31,12 @@ import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; import logger from "@server/logger"; import { fromError } from "zod-validation-error"; -import { OpenAPITags, registry } from "@server/openApi"; import { and, eq, inArray, or } from "drizzle-orm"; import { canUserAccessSiteResource } from "@server/auth/canUserAccessSiteResource"; -import { signPublicKey, getOrgCAKeys } from "#private/lib/sshCA"; +import { signPublicKey, getOrgCAKeys } from "@server/lib/sshCA"; import config from "@server/lib/config"; import { sendToClient } from "#private/routers/ws"; +import { ActionsEnum } from "@server/auth/actions"; const paramsSchema = z.strictObject({ orgId: z.string().nonempty() @@ -64,6 +66,7 @@ export type SignSshKeyResponse = { sshUsername: string; sshHost: string; resourceId: number; + siteId: number; keyId: string; validPrincipals: string[]; validAfter: string; @@ -185,7 +188,7 @@ export async function signSshKey( } else if (req.user?.username) { usernameToUse = req.user.username; // We need to clean out any spaces or special characters from the username to ensure it's valid for SSH certificates - usernameToUse = usernameToUse.replace(/[^a-zA-Z0-9_-]/g, ""); + usernameToUse = usernameToUse.replace(/[^a-zA-Z0-9_-]/g, "-"); if (!usernameToUse) { return next( createHttpError( @@ -203,6 +206,9 @@ export async function signSshKey( ); } + // prefix with p- + usernameToUse = `p-${usernameToUse}`; + // check if we have a existing user in this org with the same const [existingUserWithSameName] = await db .select() @@ -248,6 +254,16 @@ export async function signSshKey( ); } } + + await db + .update(userOrgs) + .set({ pamUsername: usernameToUse }) + .where( + and( + eq(userOrgs.orgId, orgId), + eq(userOrgs.userId, userId) + ) + ); } else { usernameToUse = userOrg.pamUsername; } @@ -319,7 +335,16 @@ export async function signSshKey( ); } - // Check if the user has access to the resource (any of their roles) + if (resource.mode == "cidr") { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "SSHing is not supported for CIDR resources" + ) + ); + } + + // Check if the user has access to the resource const hasAccess = await canUserAccessSiteResource({ userId: userId, resourceId: resource.siteResourceId, @@ -444,6 +469,20 @@ export async function signSshKey( sshHost = resource.destination; } + await logsDb.insert(actionAuditLog).values({ + timestamp: Math.floor(Date.now() / 1000), + orgId: orgId, + actorType: "user", + actor: req.user?.username ?? "", + actorId: req.user?.userId ?? "", + action: ActionsEnum.signSshKey, + metadata: JSON.stringify({ + resourceId: resource.siteResourceId, + resource: resource.name, + siteId: resource.siteId, + }) + }); + return response(res, { data: { certificate: cert.certificate, @@ -451,6 +490,7 @@ export async function signSshKey( sshUsername: usernameToUse, sshHost: sshHost, resourceId: resource.siteResourceId, + siteId: resource.siteId, keyId: cert.keyId, validPrincipals: cert.validPrincipals, validAfter: cert.validAfter.toISOString(), diff --git a/server/private/routers/ws/messageHandlers.ts b/server/private/routers/ws/messageHandlers.ts index 5a6c85cff..d388ce40a 100644 --- a/server/private/routers/ws/messageHandlers.ts +++ b/server/private/routers/ws/messageHandlers.ts @@ -17,10 +17,13 @@ import { startRemoteExitNodeOfflineChecker } from "#private/routers/remoteExitNode"; import { MessageHandler } from "@server/routers/ws"; +import { build } from "@server/build"; export const messageHandlers: Record = { "remoteExitNode/register": handleRemoteExitNodeRegisterMessage, "remoteExitNode/ping": handleRemoteExitNodePingMessage }; -startRemoteExitNodeOfflineChecker(); // this is to handle the offline check for remote exit nodes +if (build != "saas") { + startRemoteExitNodeOfflineChecker(); // this is to handle the offline check for remote exit nodes +} diff --git a/server/private/routers/ws/ws.ts b/server/private/routers/ws/ws.ts index 342dba58c..4bfda5da8 100644 --- a/server/private/routers/ws/ws.ts +++ b/server/private/routers/ws/ws.ts @@ -12,6 +12,7 @@ */ import { Router, Request, Response } from "express"; +import zlib from "zlib"; import { Server as HttpServer } from "http"; import { WebSocket, WebSocketServer } from "ws"; import { Socket } from "net"; @@ -24,7 +25,8 @@ import { OlmSession, RemoteExitNode, RemoteExitNodeSession, - remoteExitNodes + remoteExitNodes, + sites } from "@server/db"; import { eq } from "drizzle-orm"; import { db } from "@server/db"; @@ -57,11 +59,13 @@ const MAX_PENDING_MESSAGES = 50; // Maximum messages to queue during connection const processMessage = async ( ws: AuthenticatedWebSocket, data: Buffer, + isBinary: boolean, clientId: string, clientType: ClientType ): Promise => { try { - const message: WSMessage = JSON.parse(data.toString()); + const messageBuffer = isBinary ? zlib.gunzipSync(data) : data; + const message: WSMessage = JSON.parse(messageBuffer.toString()); // logger.debug( // `Processing message from ${clientType.toUpperCase()} ID: ${clientId}, type: ${message.type}` @@ -76,7 +80,7 @@ const processMessage = async ( clientId, message.type, // Pass message type for granular limiting 100, // max requests per window - 20, // max requests per message type per window + 100, // max requests per message type per window 60 * 1000 // window in milliseconds ); if (rateLimitResult.isLimited) { @@ -163,8 +167,16 @@ const processPendingMessages = async ( ); const jobs = []; - for (const messageData of ws.pendingMessages) { - jobs.push(processMessage(ws, messageData, clientId, clientType)); + for (const pending of ws.pendingMessages) { + jobs.push( + processMessage( + ws, + pending.data, + pending.isBinary, + clientId, + clientType + ) + ); } await Promise.all(jobs); @@ -185,6 +197,12 @@ const connectedClients: Map = new Map(); // Config version tracking map (local to this node, resets on server restart) const clientConfigVersions: Map = new Map(); +// Tracks the last Unix timestamp (seconds) at which a ping was flushed to the +// DB for a given siteId. Resets on server restart which is fine – the first +// ping after startup will always write, re-establishing the online state. +const lastPingDbWrite: Map = new Map(); +const PING_DB_WRITE_INTERVAL = 45; // seconds + // Recovery tracking let isRedisRecoveryInProgress = false; @@ -325,7 +343,9 @@ const addClient = async ( // Check Redis first if enabled if (redisManager.isRedisEnabled()) { try { - const redisVersion = await redisManager.get(getConfigVersionKey(clientId)); + const redisVersion = await redisManager.get( + getConfigVersionKey(clientId) + ); if (redisVersion !== null) { configVersion = parseInt(redisVersion, 10); // Sync to local cache @@ -337,7 +357,10 @@ const addClient = async ( } else { // Use local cache version and sync to Redis configVersion = clientConfigVersions.get(clientId) || 0; - await redisManager.set(getConfigVersionKey(clientId), configVersion.toString()); + await redisManager.set( + getConfigVersionKey(clientId), + configVersion.toString() + ); } } catch (error) { logger.error("Failed to get/set config version in Redis:", error); @@ -432,7 +455,9 @@ const removeClient = async ( }; // Helper to get the current config version for a client -const getClientConfigVersion = async (clientId: string): Promise => { +const getClientConfigVersion = async ( + clientId: string +): Promise => { // Try Redis first if available if (redisManager.isRedisEnabled()) { try { @@ -502,11 +527,26 @@ const sendToClientLocal = async ( }; const messageString = JSON.stringify(messageWithVersion); - clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(messageString); - } - }); + if (options.compress) { + logger.debug( + `Message size before compression: ${messageString.length} bytes` + ); + const compressed = zlib.gzipSync(Buffer.from(messageString, "utf8")); + logger.debug( + `Message size after compression: ${compressed.length} bytes` + ); + clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(compressed); + } + }); + } else { + clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(messageString); + } + }); + } return true; }; @@ -532,11 +572,22 @@ const broadcastToAllExceptLocal = async ( configVersion }; - clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify(messageWithVersion)); - } - }); + if (options.compress) { + const compressed = zlib.gzipSync( + Buffer.from(JSON.stringify(messageWithVersion), "utf8") + ); + clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(compressed); + } + }); + } else { + clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(messageWithVersion)); + } + }); + } } } }; @@ -762,7 +813,7 @@ const setupConnection = async ( } // Set up message handler FIRST to prevent race condition - ws.on("message", async (data) => { + ws.on("message", async (data, isBinary) => { if (!ws.isFullyConnected) { // Queue message for later processing with limits ws.pendingMessages = ws.pendingMessages || []; @@ -777,11 +828,17 @@ const setupConnection = async ( logger.debug( `Queueing message from ${clientType.toUpperCase()} ID: ${clientId} (connection not fully established)` ); - ws.pendingMessages.push(data as Buffer); + ws.pendingMessages.push({ data: data as Buffer, isBinary }); return; } - await processMessage(ws, data as Buffer, clientId, clientType); + await processMessage( + ws, + data as Buffer, + isBinary, + clientId, + clientType + ); }); // Set up other event handlers before async operations @@ -796,6 +853,35 @@ const setupConnection = async ( ); }); + // Handle WebSocket protocol-level pings from older newt clients that do + // not send application-level "newt/ping" messages. Update the site's + // online state and lastPing timestamp so the offline checker treats them + // the same as modern newt clients. + if (clientType === "newt") { + const newtClient = client as Newt; + ws.on("ping", async () => { + if (!newtClient.siteId) return; + const now = Math.floor(Date.now() / 1000); + const lastWrite = lastPingDbWrite.get(newtClient.siteId) ?? 0; + if (now - lastWrite < PING_DB_WRITE_INTERVAL) return; + lastPingDbWrite.set(newtClient.siteId, now); + try { + await db + .update(sites) + .set({ + online: true, + lastPing: now + }) + .where(eq(sites.siteId, newtClient.siteId)); + } catch (error) { + logger.error( + "Error updating newt site online state on WS ping", + { error } + ); + } + }); + } + ws.on("error", (error: Error) => { logger.error( `WebSocket error for ${clientType.toUpperCase()} ID ${clientId}:`, diff --git a/server/routers/accessToken/generateAccessToken.ts b/server/routers/accessToken/generateAccessToken.ts index 35da6add3..9d0a7a7df 100644 --- a/server/routers/accessToken/generateAccessToken.ts +++ b/server/routers/accessToken/generateAccessToken.ts @@ -43,7 +43,7 @@ registry.registerPath({ method: "post", path: "/resource/{resourceId}/access-token", description: "Generate a new access token for a resource.", - tags: [OpenAPITags.Resource, OpenAPITags.AccessToken], + tags: [OpenAPITags.PublicResource, OpenAPITags.AccessToken], request: { params: generateAccssTokenParamsSchema, body: { diff --git a/server/routers/accessToken/listAccessTokens.ts b/server/routers/accessToken/listAccessTokens.ts index 495afeb3c..55751df81 100644 --- a/server/routers/accessToken/listAccessTokens.ts +++ b/server/routers/accessToken/listAccessTokens.ts @@ -122,7 +122,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/access-tokens", description: "List all access tokens in an organization.", - tags: [OpenAPITags.Org, OpenAPITags.AccessToken], + tags: [OpenAPITags.AccessToken], request: { params: z.object({ orgId: z.string() @@ -135,8 +135,8 @@ registry.registerPath({ registry.registerPath({ method: "get", path: "/resource/{resourceId}/access-tokens", - description: "List all access tokens in an organization.", - tags: [OpenAPITags.Resource, OpenAPITags.AccessToken], + description: "List all access tokens for a resource.", + tags: [OpenAPITags.PublicResource, OpenAPITags.AccessToken], request: { params: z.object({ resourceId: z.number() diff --git a/server/routers/apiKeys/createOrgApiKey.ts b/server/routers/apiKeys/createOrgApiKey.ts index d61a364b1..91ef72bc8 100644 --- a/server/routers/apiKeys/createOrgApiKey.ts +++ b/server/routers/apiKeys/createOrgApiKey.ts @@ -37,7 +37,7 @@ registry.registerPath({ method: "put", path: "/org/{orgId}/api-key", description: "Create a new API key scoped to the organization.", - tags: [OpenAPITags.Org, OpenAPITags.ApiKey], + tags: [OpenAPITags.ApiKey], request: { params: paramsSchema, body: { diff --git a/server/routers/apiKeys/deleteApiKey.ts b/server/routers/apiKeys/deleteApiKey.ts index 4b97b3530..2627fd636 100644 --- a/server/routers/apiKeys/deleteApiKey.ts +++ b/server/routers/apiKeys/deleteApiKey.ts @@ -18,7 +18,7 @@ registry.registerPath({ method: "delete", path: "/org/{orgId}/api-key/{apiKeyId}", description: "Delete an API key.", - tags: [OpenAPITags.Org, OpenAPITags.ApiKey], + tags: [OpenAPITags.ApiKey], request: { params: paramsSchema }, diff --git a/server/routers/apiKeys/listApiKeyActions.ts b/server/routers/apiKeys/listApiKeyActions.ts index 073a75831..d816d4b38 100644 --- a/server/routers/apiKeys/listApiKeyActions.ts +++ b/server/routers/apiKeys/listApiKeyActions.ts @@ -48,7 +48,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/api-key/{apiKeyId}/actions", description: "List all actions set for an API key.", - tags: [OpenAPITags.Org, OpenAPITags.ApiKey], + tags: [OpenAPITags.ApiKey], request: { params: paramsSchema, query: querySchema diff --git a/server/routers/apiKeys/listOrgApiKeys.ts b/server/routers/apiKeys/listOrgApiKeys.ts index 53191ba63..24370665d 100644 --- a/server/routers/apiKeys/listOrgApiKeys.ts +++ b/server/routers/apiKeys/listOrgApiKeys.ts @@ -52,7 +52,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/api-keys", description: "List all API keys for an organization", - tags: [OpenAPITags.Org, OpenAPITags.ApiKey], + tags: [OpenAPITags.ApiKey], request: { params: paramsSchema, query: querySchema diff --git a/server/routers/apiKeys/setApiKeyActions.ts b/server/routers/apiKeys/setApiKeyActions.ts index 629673886..55b3670ac 100644 --- a/server/routers/apiKeys/setApiKeyActions.ts +++ b/server/routers/apiKeys/setApiKeyActions.ts @@ -25,7 +25,7 @@ registry.registerPath({ path: "/org/{orgId}/api-key/{apiKeyId}/actions", description: "Set actions for an API key. This will replace any existing actions.", - tags: [OpenAPITags.Org, OpenAPITags.ApiKey], + tags: [OpenAPITags.ApiKey], request: { params: paramsSchema, body: { diff --git a/server/routers/auditLogs/exportRequestAuditLog.ts b/server/routers/auditLogs/exportRequestAuditLog.ts index 8b70ec5e1..14054a5c3 100644 --- a/server/routers/auditLogs/exportRequestAuditLog.ts +++ b/server/routers/auditLogs/exportRequestAuditLog.ts @@ -20,7 +20,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/logs/request", description: "Query the request audit log for an organization", - tags: [OpenAPITags.Org], + tags: [OpenAPITags.Logs], request: { query: queryAccessAuditLogsQuery.omit({ limit: true, diff --git a/server/routers/auditLogs/queryRequestAnalytics.ts b/server/routers/auditLogs/queryRequestAnalytics.ts index a6f9cb767..1e0f1f401 100644 --- a/server/routers/auditLogs/queryRequestAnalytics.ts +++ b/server/routers/auditLogs/queryRequestAnalytics.ts @@ -1,4 +1,4 @@ -import { db, requestAuditLog, driver, primaryDb } from "@server/db"; +import { logsDb, requestAuditLog, driver, primaryLogsDb } from "@server/db"; import { registry } from "@server/openApi"; import { NextFunction } from "express"; import { Request, Response } from "express"; @@ -74,12 +74,12 @@ async function query(query: Q) { ); } - const [all] = await primaryDb + const [all] = await primaryLogsDb .select({ total: count() }) .from(requestAuditLog) .where(baseConditions); - const [blocked] = await primaryDb + const [blocked] = await primaryLogsDb .select({ total: count() }) .from(requestAuditLog) .where(and(baseConditions, eq(requestAuditLog.action, false))); @@ -90,7 +90,7 @@ async function query(query: Q) { const DISTINCT_LIMIT = 500; - const requestsPerCountry = await primaryDb + const requestsPerCountry = await primaryLogsDb .selectDistinct({ code: requestAuditLog.location, count: totalQ @@ -118,7 +118,7 @@ async function query(query: Q) { const booleanTrue = driver === "pg" ? sql`true` : sql`1`; const booleanFalse = driver === "pg" ? sql`false` : sql`0`; - const requestsPerDay = await primaryDb + const requestsPerDay = await primaryLogsDb .select({ day: groupByDayFunction.as("day"), allowedCount: @@ -151,7 +151,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/logs/analytics", description: "Query the request audit analytics for an organization", - tags: [OpenAPITags.Org], + tags: [OpenAPITags.Logs], request: { query: queryAccessAuditLogsQuery, params: queryRequestAuditLogsParams diff --git a/server/routers/auditLogs/queryRequestAuditLog.ts b/server/routers/auditLogs/queryRequestAuditLog.ts index 98c237213..176a9e5d3 100644 --- a/server/routers/auditLogs/queryRequestAuditLog.ts +++ b/server/routers/auditLogs/queryRequestAuditLog.ts @@ -1,8 +1,8 @@ -import { db, primaryDb, requestAuditLog, resources } from "@server/db"; +import { logsDb, primaryLogsDb, requestAuditLog, resources, db, primaryDb } from "@server/db"; import { registry } from "@server/openApi"; import { NextFunction } from "express"; import { Request, Response } from "express"; -import { eq, gt, lt, and, count, desc } from "drizzle-orm"; +import { eq, gt, lt, and, count, desc, inArray } from "drizzle-orm"; import { OpenAPITags } from "@server/openApi"; import { z } from "zod"; import createHttpError from "http-errors"; @@ -107,7 +107,7 @@ function getWhere(data: Q) { } export function queryRequest(data: Q) { - return primaryDb + return primaryLogsDb .select({ id: requestAuditLog.id, timestamp: requestAuditLog.timestamp, @@ -129,21 +129,49 @@ export function queryRequest(data: Q) { host: requestAuditLog.host, path: requestAuditLog.path, method: requestAuditLog.method, - tls: requestAuditLog.tls, - resourceName: resources.name, - resourceNiceId: resources.niceId + tls: requestAuditLog.tls }) .from(requestAuditLog) - .leftJoin( - resources, - eq(requestAuditLog.resourceId, resources.resourceId) - ) // TODO: Is this efficient? .where(getWhere(data)) .orderBy(desc(requestAuditLog.timestamp)); } +async function enrichWithResourceDetails(logs: Awaited>) { + // If logs database is the same as main database, we can do a join + // Otherwise, we need to fetch resource details separately + const resourceIds = logs + .map(log => log.resourceId) + .filter((id): id is number => id !== null && id !== undefined); + + if (resourceIds.length === 0) { + return logs.map(log => ({ ...log, resourceName: null, resourceNiceId: null })); + } + + // Fetch resource details from main database + const resourceDetails = await primaryDb + .select({ + resourceId: resources.resourceId, + name: resources.name, + niceId: resources.niceId + }) + .from(resources) + .where(inArray(resources.resourceId, resourceIds)); + + // Create a map for quick lookup + const resourceMap = new Map( + resourceDetails.map(r => [r.resourceId, { name: r.name, niceId: r.niceId }]) + ); + + // Enrich logs with resource details + return logs.map(log => ({ + ...log, + resourceName: log.resourceId ? resourceMap.get(log.resourceId)?.name ?? null : null, + resourceNiceId: log.resourceId ? resourceMap.get(log.resourceId)?.niceId ?? null : null + })); +} + export function countRequestQuery(data: Q) { - const countQuery = primaryDb + const countQuery = primaryLogsDb .select({ count: count() }) .from(requestAuditLog) .where(getWhere(data)); @@ -154,7 +182,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/logs/request", description: "Query the request audit log for an organization", - tags: [OpenAPITags.Org], + tags: [OpenAPITags.Logs], request: { query: queryAccessAuditLogsQuery, params: queryRequestAuditLogsParams @@ -185,36 +213,31 @@ async function queryUniqueFilterAttributes( uniquePaths, uniqueResources ] = await Promise.all([ - primaryDb + primaryLogsDb .selectDistinct({ actor: requestAuditLog.actor }) .from(requestAuditLog) .where(baseConditions) .limit(DISTINCT_LIMIT + 1), - primaryDb + primaryLogsDb .selectDistinct({ locations: requestAuditLog.location }) .from(requestAuditLog) .where(baseConditions) .limit(DISTINCT_LIMIT + 1), - primaryDb + primaryLogsDb .selectDistinct({ hosts: requestAuditLog.host }) .from(requestAuditLog) .where(baseConditions) .limit(DISTINCT_LIMIT + 1), - primaryDb + primaryLogsDb .selectDistinct({ paths: requestAuditLog.path }) .from(requestAuditLog) .where(baseConditions) .limit(DISTINCT_LIMIT + 1), - primaryDb + primaryLogsDb .selectDistinct({ - id: requestAuditLog.resourceId, - name: resources.name + id: requestAuditLog.resourceId }) .from(requestAuditLog) - .leftJoin( - resources, - eq(requestAuditLog.resourceId, resources.resourceId) - ) .where(baseConditions) .limit(DISTINCT_LIMIT + 1) ]); @@ -231,13 +254,33 @@ async function queryUniqueFilterAttributes( // throw new Error("Too many distinct filter attributes to retrieve. Please refine your time range."); // } + // Fetch resource names from main database for the unique resource IDs + const resourceIds = uniqueResources + .map(row => row.id) + .filter((id): id is number => id !== null); + + let resourcesWithNames: Array<{ id: number; name: string | null }> = []; + + if (resourceIds.length > 0) { + const resourceDetails = await primaryDb + .select({ + resourceId: resources.resourceId, + name: resources.name + }) + .from(resources) + .where(inArray(resources.resourceId, resourceIds)); + + resourcesWithNames = resourceDetails.map(r => ({ + id: r.resourceId, + name: r.name + })); + } + return { actors: uniqueActors .map((row) => row.actor) .filter((actor): actor is string => actor !== null), - resources: uniqueResources.filter( - (row): row is { id: number; name: string | null } => row.id !== null - ), + resources: resourcesWithNames, locations: uniqueLocations .map((row) => row.locations) .filter((location): location is string => location !== null), @@ -280,7 +323,10 @@ export async function queryRequestAuditLogs( const baseQuery = queryRequest(data); - const log = await baseQuery.limit(data.limit).offset(data.offset); + const logsRaw = await baseQuery.limit(data.limit).offset(data.offset); + + // Enrich with resource details (handles cross-database scenario) + const log = await enrichWithResourceDetails(logsRaw); const totalCountResult = await countRequestQuery(data); const totalCount = totalCountResult[0].count; diff --git a/server/routers/auth/signup.ts b/server/routers/auth/signup.ts index cf8e41417..82d8c1515 100644 --- a/server/routers/auth/signup.ts +++ b/server/routers/auth/signup.ts @@ -1,5 +1,5 @@ import { NextFunction, Request, Response } from "express"; -import { db, users } from "@server/db"; +import { bannedEmails, bannedIps, db, users } from "@server/db"; import HttpCode from "@server/types/HttpCode"; import { email, z } from "zod"; import { fromError } from "zod-validation-error"; @@ -22,7 +22,6 @@ import { checkValidInvite } from "@server/auth/checkValidInvite"; import { passwordSchema } from "@server/auth/passwordSchema"; import { UserType } from "@server/types/UserTypes"; import { build } from "@server/build"; -import resend, { AudienceIds, moveEmailToAudience } from "#dynamic/lib/resend"; export const signupBodySchema = z.object({ email: z.email().toLowerCase(), @@ -66,6 +65,30 @@ export async function signup( skipVerificationEmail } = parsedBody.data; + const [bannedEmail] = await db + .select() + .from(bannedEmails) + .where(eq(bannedEmails.email, email)) + .limit(1); + if (bannedEmail) { + return next( + createHttpError(HttpCode.FORBIDDEN, "Signup blocked. Do not attempt to continue to use this service.") + ); + } + + if (req.ip) { + const [bannedIp] = await db + .select() + .from(bannedIps) + .where(eq(bannedIps.ip, req.ip)) + .limit(1); + if (bannedIp) { + return next( + createHttpError(HttpCode.FORBIDDEN, "Signup blocked. Do not attempt to continue to use this service.") + ); + } + } + const passwordHash = await hashPassword(password); const userId = generateId(15); @@ -189,6 +212,7 @@ export async function signup( dateCreated: moment().toISOString(), termsAcceptedTimestamp: termsAcceptedTimestamp || null, termsVersion: "1", + marketingEmailConsent: marketingEmailConsent ?? false, lastPasswordChange: new Date().getTime() }); @@ -212,7 +236,7 @@ export async function signup( logger.debug( `User ${email} opted in to marketing emails during signup.` ); - moveEmailToAudience(email, AudienceIds.SignUps); + // TODO: update user in Sendy } if (config.getRawConfig().flags?.require_email_verification) { diff --git a/server/routers/badger/logRequestAudit.ts b/server/routers/badger/logRequestAudit.ts index 5975d8f3c..92d01332e 100644 --- a/server/routers/badger/logRequestAudit.ts +++ b/server/routers/badger/logRequestAudit.ts @@ -1,10 +1,12 @@ -import { db, orgs, requestAuditLog } from "@server/db"; +import { logsDb, primaryLogsDb, db, orgs, requestAuditLog } from "@server/db"; import logger from "@server/logger"; import { and, eq, lt, sql } from "drizzle-orm"; -import cache from "@server/lib/cache"; +import cache from "#dynamic/lib/cache"; import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs"; import { stripPortFromHost } from "@server/lib/ip"; +import { sanitizeString } from "@server/lib/sanitize"; + /** Reasons: @@ -69,7 +71,7 @@ async function flushAuditLogs() { try { // Use a transaction to ensure all inserts succeed or fail together // This prevents index corruption from partial writes - await db.transaction(async (tx) => { + await logsDb.transaction(async (tx) => { // Batch insert logs in groups of 25 to avoid overwhelming the database const BATCH_DB_SIZE = 25; for (let i = 0; i < logsToWrite.length; i += BATCH_DB_SIZE) { @@ -130,7 +132,7 @@ export async function shutdownAuditLogger() { async function getRetentionDays(orgId: string): Promise { // check cache first - const cached = cache.get(`org_${orgId}_retentionDays`); + const cached = await cache.get(`org_${orgId}_retentionDays`); if (cached !== undefined) { return cached; } @@ -149,7 +151,7 @@ async function getRetentionDays(orgId: string): Promise { } // store the result in cache - cache.set( + await cache.set( `org_${orgId}_retentionDays`, org.settingsLogRetentionDaysRequest, 300 @@ -162,7 +164,7 @@ export async function cleanUpOldLogs(orgId: string, retentionDays: number) { const cutoffTimestamp = calculateCutoffTimestamp(retentionDays); try { - await db + await logsDb .delete(requestAuditLog) .where( and( @@ -253,24 +255,23 @@ export async function logRequestAudit( // Add to buffer instead of writing directly to DB auditLogBuffer.push({ timestamp, - orgId: data.orgId, - actorType, - actor, - actorId, - metadata, + orgId: sanitizeString(data.orgId), + actorType: sanitizeString(actorType), + actor: sanitizeString(actor), + actorId: sanitizeString(actorId), + metadata: sanitizeString(metadata), action: data.action, resourceId: data.resourceId, reason: data.reason, - location: data.location, - originalRequestURL: body.originalRequestURL, - scheme: body.scheme, - host: body.host, - path: body.path, - method: body.method, - ip: clientIp, + location: sanitizeString(data.location), + originalRequestURL: sanitizeString(body.originalRequestURL) ?? "", + scheme: sanitizeString(body.scheme) ?? "", + host: sanitizeString(body.host) ?? "", + path: sanitizeString(body.path) ?? "", + method: sanitizeString(body.method) ?? "", + ip: sanitizeString(clientIp), tls: body.tls }); - // Flush immediately if buffer is full, otherwise schedule a flush if (auditLogBuffer.length >= BATCH_SIZE) { // Fire and forget - don't block the caller diff --git a/server/routers/badger/verifySession.ts b/server/routers/badger/verifySession.ts index 2f6f7ac12..182b35dcb 100644 --- a/server/routers/badger/verifySession.ts +++ b/server/routers/badger/verifySession.ts @@ -38,7 +38,7 @@ import { enforceResourceSessionLength } from "#dynamic/lib/checkOrgAccessPolicy"; import { logRequestAudit } from "./logRequestAudit"; -import cache from "@server/lib/cache"; +import { localCache } from "#dynamic/lib/cache"; import { APP_VERSION } from "@server/lib/consts"; import { isSubscribed } from "#dynamic/lib/isSubscribed"; import { tierMatrix } from "@server/lib/billing/tierMatrix"; @@ -138,7 +138,7 @@ export async function verifyResourceSession( headerAuthExtendedCompatibility: ResourceHeaderAuthExtendedCompatibility | null; org: Org; } - | undefined = cache.get(resourceCacheKey); + | undefined = localCache.get(resourceCacheKey); if (!resourceData) { const result = await getResourceByDomain(cleanHost); @@ -162,7 +162,7 @@ export async function verifyResourceSession( } resourceData = result; - cache.set(resourceCacheKey, resourceData, 5); + localCache.set(resourceCacheKey, resourceData, 5); } const { @@ -406,7 +406,7 @@ export async function verifyResourceSession( // check for HTTP Basic Auth header const clientHeaderAuthKey = `headerAuth:${clientHeaderAuth}`; if (headerAuth && clientHeaderAuth) { - if (cache.get(clientHeaderAuthKey)) { + if (localCache.get(clientHeaderAuthKey)) { logger.debug( "Resource allowed because header auth is valid (cached)" ); @@ -429,7 +429,7 @@ export async function verifyResourceSession( headerAuth.headerAuthHash ) ) { - cache.set(clientHeaderAuthKey, clientHeaderAuth, 5); + localCache.set(clientHeaderAuthKey, clientHeaderAuth, 5); logger.debug("Resource allowed because header auth is valid"); logRequestAudit( @@ -521,7 +521,7 @@ export async function verifyResourceSession( if (resourceSessionToken) { const sessionCacheKey = `session:${resourceSessionToken}`; - let resourceSession: any = cache.get(sessionCacheKey); + let resourceSession: any = localCache.get(sessionCacheKey); if (!resourceSession) { const result = await validateResourceSessionToken( @@ -530,7 +530,7 @@ export async function verifyResourceSession( ); resourceSession = result?.resourceSession; - cache.set(sessionCacheKey, resourceSession, 5); + localCache.set(sessionCacheKey, resourceSession, 5); } if (resourceSession?.isRequestToken) { @@ -663,7 +663,7 @@ export async function verifyResourceSession( }:${resource.resourceId}`; let allowedUserData: BasicUserData | null | undefined = - cache.get(userAccessCacheKey); + localCache.get(userAccessCacheKey); if (allowedUserData === undefined) { allowedUserData = await isUserAllowedToAccessResource( @@ -672,7 +672,7 @@ export async function verifyResourceSession( resourceData.org ); - cache.set(userAccessCacheKey, allowedUserData, 5); + localCache.set(userAccessCacheKey, allowedUserData, 5); } if ( @@ -985,11 +985,11 @@ async function checkRules( ): Promise<"ACCEPT" | "DROP" | "PASS" | undefined> { const ruleCacheKey = `rules:${resourceId}`; - let rules: ResourceRule[] | undefined = cache.get(ruleCacheKey); + let rules: ResourceRule[] | undefined = localCache.get(ruleCacheKey); if (!rules) { rules = await getResourceRules(resourceId); - cache.set(ruleCacheKey, rules, 5); + localCache.set(ruleCacheKey, rules, 5); } if (rules.length === 0) { @@ -1219,13 +1219,13 @@ async function isIpInAsn( async function getAsnFromIp(ip: string): Promise { const asnCacheKey = `asn:${ip}`; - let cachedAsn: number | undefined = cache.get(asnCacheKey); + let cachedAsn: number | undefined = localCache.get(asnCacheKey); if (!cachedAsn) { cachedAsn = await getAsnForIp(ip); // do it locally // Cache for longer since IP ASN doesn't change frequently if (cachedAsn) { - cache.set(asnCacheKey, cachedAsn, 300); // 5 minutes + localCache.set(asnCacheKey, cachedAsn, 300); // 5 minutes } } @@ -1235,14 +1235,14 @@ async function getAsnFromIp(ip: string): Promise { async function getCountryCodeFromIp(ip: string): Promise { const geoIpCacheKey = `geoip:${ip}`; - let cachedCountryCode: string | undefined = cache.get(geoIpCacheKey); + let cachedCountryCode: string | undefined = localCache.get(geoIpCacheKey); if (!cachedCountryCode) { cachedCountryCode = await getCountryCodeForIp(ip); // do it locally // Only cache successful lookups to avoid filling cache with undefined values if (cachedCountryCode) { // Cache for longer since IP geolocation doesn't change frequently - cache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes + localCache.set(geoIpCacheKey, cachedCountryCode, 300); // 5 minutes } } diff --git a/server/routers/blueprints/applyJSONBlueprint.ts b/server/routers/blueprints/applyJSONBlueprint.ts index 7eee15bf1..fa7ed46ae 100644 --- a/server/routers/blueprints/applyJSONBlueprint.ts +++ b/server/routers/blueprints/applyJSONBlueprint.ts @@ -20,7 +20,7 @@ registry.registerPath({ method: "put", path: "/org/{orgId}/blueprint", description: "Apply a base64 encoded JSON blueprint to an organization", - tags: [OpenAPITags.Org, OpenAPITags.Blueprint], + tags: [OpenAPITags.Blueprint], request: { params: applyBlueprintParamsSchema, body: { diff --git a/server/routers/blueprints/applyYAMLBlueprint.ts b/server/routers/blueprints/applyYAMLBlueprint.ts index 19751e469..665943edd 100644 --- a/server/routers/blueprints/applyYAMLBlueprint.ts +++ b/server/routers/blueprints/applyYAMLBlueprint.ts @@ -43,7 +43,7 @@ registry.registerPath({ method: "put", path: "/org/{orgId}/blueprint", description: "Create and apply a YAML blueprint to an organization", - tags: [OpenAPITags.Org, OpenAPITags.Blueprint], + tags: [OpenAPITags.Blueprint], request: { params: applyBlueprintParamsSchema, body: { diff --git a/server/routers/blueprints/getBlueprint.ts b/server/routers/blueprints/getBlueprint.ts index 915e04814..ea2ac2d05 100644 --- a/server/routers/blueprints/getBlueprint.ts +++ b/server/routers/blueprints/getBlueprint.ts @@ -53,7 +53,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/blueprint/{blueprintId}", description: "Get a blueprint by its blueprint ID.", - tags: [OpenAPITags.Org, OpenAPITags.Blueprint], + tags: [OpenAPITags.Blueprint], request: { params: getBlueprintSchema }, diff --git a/server/routers/blueprints/listBlueprints.ts b/server/routers/blueprints/listBlueprints.ts index 2ece9e53d..0235e7a18 100644 --- a/server/routers/blueprints/listBlueprints.ts +++ b/server/routers/blueprints/listBlueprints.ts @@ -67,7 +67,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/blueprints", description: "List all blueprints for a organization.", - tags: [OpenAPITags.Org, OpenAPITags.Blueprint], + tags: [OpenAPITags.Blueprint], request: { params: z.object({ orgId: z.string() diff --git a/server/routers/client/createClient.ts b/server/routers/client/createClient.ts index 3e5ba4fa1..337d7e714 100644 --- a/server/routers/client/createClient.ts +++ b/server/routers/client/createClient.ts @@ -48,7 +48,7 @@ registry.registerPath({ method: "put", path: "/org/{orgId}/client", description: "Create a new client for an organization.", - tags: [OpenAPITags.Client, OpenAPITags.Org], + tags: [OpenAPITags.Client], request: { params: createClientParamsSchema, body: { diff --git a/server/routers/client/createUserClient.ts b/server/routers/client/createUserClient.ts index 5e9840f9d..d61eab15f 100644 --- a/server/routers/client/createUserClient.ts +++ b/server/routers/client/createUserClient.ts @@ -49,7 +49,7 @@ registry.registerPath({ path: "/org/{orgId}/user/{userId}/client", description: "Create a new client for a user and associate it with an existing olm.", - tags: [OpenAPITags.Client, OpenAPITags.Org, OpenAPITags.User], + tags: [OpenAPITags.Client], request: { params: paramsSchema, body: { diff --git a/server/routers/client/getClient.ts b/server/routers/client/getClient.ts index bb2ff8fda..375c027a7 100644 --- a/server/routers/client/getClient.ts +++ b/server/routers/client/getClient.ts @@ -243,7 +243,7 @@ registry.registerPath({ path: "/org/{orgId}/client/{niceId}", description: "Get a client by orgId and niceId. NiceId is a readable ID for the site and unique on a per org basis.", - tags: [OpenAPITags.Org, OpenAPITags.Site], + tags: [OpenAPITags.Site], request: { params: z.object({ orgId: z.string(), diff --git a/server/routers/client/listClients.ts b/server/routers/client/listClients.ts index 95d6281bf..0bf798509 100644 --- a/server/routers/client/listClients.ts +++ b/server/routers/client/listClients.ts @@ -70,7 +70,7 @@ async function getLatestOlmVersion(): Promise { tags = tags.filter((version) => !version.name.includes("rc")); const latestVersion = tags[0].name; - olmVersionCache.set("latestOlmVersion", latestVersion); + olmVersionCache.set("latestOlmVersion", latestVersion, 3600); return latestVersion; } catch (error: any) { @@ -119,12 +119,12 @@ const listClientsSchema = z.object({ }), query: z.string().optional(), sort_by: z - .enum(["megabytesIn", "megabytesOut"]) + .enum(["name", "megabytesIn", "megabytesOut"]) .optional() .catch(undefined) .openapi({ type: "string", - enum: ["megabytesIn", "megabytesOut"], + enum: ["name", "megabytesIn", "megabytesOut"], description: "Field to sort by" }), order: z @@ -237,7 +237,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/clients", description: "List all clients for an organization.", - tags: [OpenAPITags.Client, OpenAPITags.Org], + tags: [OpenAPITags.Client], request: { query: listClientsSchema, params: listClientsParamsSchema @@ -363,14 +363,14 @@ export async function listClients( const countQuery = db.$count(baseQuery.as("filtered_clients")); const listMachinesQuery = baseQuery - .limit(page) + .limit(pageSize) .offset(pageSize * (page - 1)) .orderBy( sort_by ? order === "asc" ? asc(clients[sort_by]) : desc(clients[sort_by]) - : asc(clients.clientId) + : asc(clients.name) ); const [clientsList, totalCount] = await Promise.all([ diff --git a/server/routers/client/listUserDevices.ts b/server/routers/client/listUserDevices.ts index 4d37dc440..0ae31165a 100644 --- a/server/routers/client/listUserDevices.ts +++ b/server/routers/client/listUserDevices.ts @@ -71,7 +71,7 @@ async function getLatestOlmVersion(): Promise { tags = tags.filter((version) => !version.name.includes("rc")); const latestVersion = tags[0].name; - olmVersionCache.set("latestOlmVersion", latestVersion); + olmVersionCache.set("latestOlmVersion", latestVersion, 3600); return latestVersion; } catch (error: any) { @@ -256,7 +256,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/user-devices", description: "List all user devices for an organization.", - tags: [OpenAPITags.Client, OpenAPITags.Org], + tags: [OpenAPITags.Client], request: { query: listUserDevicesSchema, params: listUserDevicesParamsSchema diff --git a/server/routers/client/pickClientDefaults.ts b/server/routers/client/pickClientDefaults.ts index fd31da127..5dffd77d7 100644 --- a/server/routers/client/pickClientDefaults.ts +++ b/server/routers/client/pickClientDefaults.ts @@ -23,7 +23,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/pick-client-defaults", description: "Return pre-requisite data for creating a client.", - tags: [OpenAPITags.Client, OpenAPITags.Site], + tags: [OpenAPITags.Client], request: { params: pickClientDefaultsSchema }, diff --git a/server/routers/client/targets.ts b/server/routers/client/targets.ts index bf612d352..94d41a4d1 100644 --- a/server/routers/client/targets.ts +++ b/server/routers/client/targets.ts @@ -1,51 +1,38 @@ import { sendToClient } from "#dynamic/routers/ws"; import { db, olms, Transaction } from "@server/db"; +import { canCompress } from "@server/lib/clientVersionChecks"; import { Alias, SubnetProxyTarget } from "@server/lib/ip"; import logger from "@server/logger"; import { eq } from "drizzle-orm"; -const BATCH_SIZE = 50; -const BATCH_DELAY_MS = 50; - -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} - -function chunkArray(array: T[], size: number): T[][] { - const chunks: T[][] = []; - for (let i = 0; i < array.length; i += size) { - chunks.push(array.slice(i, i + size)); - } - return chunks; -} - -export async function addTargets(newtId: string, targets: SubnetProxyTarget[]) { - const batches = chunkArray(targets, BATCH_SIZE); - for (let i = 0; i < batches.length; i++) { - if (i > 0) { - await sleep(BATCH_DELAY_MS); - } - await sendToClient(newtId, { +export async function addTargets( + newtId: string, + targets: SubnetProxyTarget[], + version?: string | null +) { + await sendToClient( + newtId, + { type: `newt/wg/targets/add`, - data: batches[i] - }, { incrementConfigVersion: true }); - } + data: targets + }, + { incrementConfigVersion: true, compress: canCompress(version, "newt") } + ); } export async function removeTargets( newtId: string, - targets: SubnetProxyTarget[] + targets: SubnetProxyTarget[], + version?: string | null ) { - const batches = chunkArray(targets, BATCH_SIZE); - for (let i = 0; i < batches.length; i++) { - if (i > 0) { - await sleep(BATCH_DELAY_MS); - } - await sendToClient(newtId, { + await sendToClient( + newtId, + { type: `newt/wg/targets/remove`, - data: batches[i] - },{ incrementConfigVersion: true }); - } + data: targets + }, + { incrementConfigVersion: true, compress: canCompress(version, "newt") } + ); } export async function updateTargets( @@ -53,26 +40,22 @@ export async function updateTargets( targets: { oldTargets: SubnetProxyTarget[]; newTargets: SubnetProxyTarget[]; - } + }, + version?: string | null ) { - const oldBatches = chunkArray(targets.oldTargets, BATCH_SIZE); - const newBatches = chunkArray(targets.newTargets, BATCH_SIZE); - const maxBatches = Math.max(oldBatches.length, newBatches.length); - - for (let i = 0; i < maxBatches; i++) { - if (i > 0) { - await sleep(BATCH_DELAY_MS); - } - await sendToClient(newtId, { + await sendToClient( + newtId, + { type: `newt/wg/targets/update`, data: { - oldTargets: oldBatches[i] || [], - newTargets: newBatches[i] || [] + oldTargets: targets.oldTargets, + newTargets: targets.newTargets } - }, { incrementConfigVersion: true }).catch((error) => { - logger.warn(`Error sending message:`, error); - }); - } + }, + { incrementConfigVersion: true, compress: canCompress(version, "newt") } + ).catch((error) => { + logger.warn(`Error sending message:`, error); + }); } export async function addPeerData( @@ -80,7 +63,8 @@ export async function addPeerData( siteId: number, remoteSubnets: string[], aliases: Alias[], - olmId?: string + olmId?: string, + version?: string | null ) { if (!olmId) { const [olm] = await db @@ -92,16 +76,21 @@ export async function addPeerData( return; // ignore this because an olm might not be associated with the client anymore } olmId = olm.olmId; + version = olm.version; } - await sendToClient(olmId, { - type: `olm/wg/peer/data/add`, - data: { - siteId: siteId, - remoteSubnets: remoteSubnets, - aliases: aliases - } - }, { incrementConfigVersion: true }).catch((error) => { + await sendToClient( + olmId, + { + type: `olm/wg/peer/data/add`, + data: { + siteId: siteId, + remoteSubnets: remoteSubnets, + aliases: aliases + } + }, + { incrementConfigVersion: true, compress: canCompress(version, "olm") } + ).catch((error) => { logger.warn(`Error sending message:`, error); }); } @@ -111,7 +100,8 @@ export async function removePeerData( siteId: number, remoteSubnets: string[], aliases: Alias[], - olmId?: string + olmId?: string, + version?: string | null ) { if (!olmId) { const [olm] = await db @@ -123,16 +113,21 @@ export async function removePeerData( return; } olmId = olm.olmId; + version = olm.version; } - await sendToClient(olmId, { - type: `olm/wg/peer/data/remove`, - data: { - siteId: siteId, - remoteSubnets: remoteSubnets, - aliases: aliases - } - }, { incrementConfigVersion: true }).catch((error) => { + await sendToClient( + olmId, + { + type: `olm/wg/peer/data/remove`, + data: { + siteId: siteId, + remoteSubnets: remoteSubnets, + aliases: aliases + } + }, + { incrementConfigVersion: true, compress: canCompress(version, "olm") } + ).catch((error) => { logger.warn(`Error sending message:`, error); }); } @@ -152,7 +147,8 @@ export async function updatePeerData( newAliases: Alias[]; } | undefined, - olmId?: string + olmId?: string, + version?: string | null ) { if (!olmId) { const [olm] = await db @@ -164,16 +160,21 @@ export async function updatePeerData( return; } olmId = olm.olmId; + version = olm.version; } - await sendToClient(olmId, { - type: `olm/wg/peer/data/update`, - data: { - siteId: siteId, - ...remoteSubnets, - ...aliases - } - }, { incrementConfigVersion: true }).catch((error) => { + await sendToClient( + olmId, + { + type: `olm/wg/peer/data/update`, + data: { + siteId: siteId, + ...remoteSubnets, + ...aliases + } + }, + { incrementConfigVersion: true, compress: canCompress(version, "olm") } + ).catch((error) => { logger.warn(`Error sending message:`, error); }); } diff --git a/server/routers/domain/listDomains.ts b/server/routers/domain/listDomains.ts index 20b236346..085acf0c6 100644 --- a/server/routers/domain/listDomains.ts +++ b/server/routers/domain/listDomains.ts @@ -40,7 +40,8 @@ async function queryDomains(orgId: string, limit: number, offset: number) { tries: domains.tries, configManaged: domains.configManaged, certResolver: domains.certResolver, - preferWildcardCert: domains.preferWildcardCert + preferWildcardCert: domains.preferWildcardCert, + errorMessage: domains.errorMessage }) .from(orgDomains) .where(eq(orgDomains.orgId, orgId)) @@ -59,7 +60,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/domains", description: "List all domains for a organization.", - tags: [OpenAPITags.Org], + tags: [OpenAPITags.Domain], request: { params: z.object({ orgId: z.string() diff --git a/server/routers/gerbil/getAllRelays.ts b/server/routers/gerbil/getAllRelays.ts index b7d33b955..bbe314b2a 100644 --- a/server/routers/gerbil/getAllRelays.ts +++ b/server/routers/gerbil/getAllRelays.ts @@ -125,7 +125,7 @@ export async function generateRelayMappings(exitNode: ExitNode) { // Add site as a destination for this client const destination: PeerDestination = { destinationIP: site.subnet.split("/")[0], - destinationPort: site.listenPort + destinationPort: site.listenPort || 1 // this satisfies gerbil for now but should be reevaluated }; // Check if this destination is already in the array to avoid duplicates @@ -165,7 +165,7 @@ export async function generateRelayMappings(exitNode: ExitNode) { const destination: PeerDestination = { destinationIP: peer.subnet.split("/")[0], - destinationPort: peer.listenPort + destinationPort: peer.listenPort || 1 // this satisfies gerbil for now but should be reevaluated }; // Check for duplicates diff --git a/server/routers/gerbil/receiveBandwidth.ts b/server/routers/gerbil/receiveBandwidth.ts index dbd687a15..b73ce986d 100644 --- a/server/routers/gerbil/receiveBandwidth.ts +++ b/server/routers/gerbil/receiveBandwidth.ts @@ -1,5 +1,5 @@ import { Request, Response, NextFunction } from "express"; -import { eq, and, lt, inArray, sql } from "drizzle-orm"; +import { eq, sql } from "drizzle-orm"; import { sites } from "@server/db"; import { db } from "@server/db"; import logger from "@server/logger"; @@ -11,19 +11,31 @@ import { FeatureId } from "@server/lib/billing/features"; import { checkExitNodeOrg } from "#dynamic/lib/exitNodes"; import { build } from "@server/build"; -// Track sites that are already offline to avoid unnecessary queries -const offlineSites = new Set(); - -// Retry configuration for deadlock handling -const MAX_RETRIES = 3; -const BASE_DELAY_MS = 50; - interface PeerBandwidth { publicKey: string; bytesIn: number; bytesOut: number; } +interface AccumulatorEntry { + bytesIn: number; + bytesOut: number; + /** Present when the update came through a remote exit node. */ + exitNodeId?: number; + /** Whether to record egress usage for billing purposes. */ + calcUsage: boolean; +} + +// Retry configuration for deadlock handling +const MAX_RETRIES = 3; +const BASE_DELAY_MS = 50; + +// How often to flush accumulated bandwidth data to the database +const FLUSH_INTERVAL_MS = 30_000; // 30 seconds + +// In-memory accumulator: publicKey -> AccumulatorEntry +let accumulator = new Map(); + /** * Check if an error is a deadlock error */ @@ -63,6 +75,220 @@ async function withDeadlockRetry( } } +/** + * Flush all accumulated site bandwidth data to the database. + * + * Swaps out the accumulator before writing so that any bandwidth messages + * received during the flush are captured in the new accumulator rather than + * being lost or causing contention. Entries that fail to write are re-queued + * back into the accumulator so they will be retried on the next flush. + * + * This function is exported so that the application's graceful-shutdown + * cleanup handler can call it before the process exits. + */ +export async function flushSiteBandwidthToDb(): Promise { + if (accumulator.size === 0) { + return; + } + + // Atomically swap out the accumulator so new data keeps flowing in + // while we write the snapshot to the database. + const snapshot = accumulator; + accumulator = new Map(); + + const currentTime = new Date().toISOString(); + + // Sort by publicKey for consistent lock ordering across concurrent + // writers — deadlock-prevention strategy. + const sortedEntries = [...snapshot.entries()].sort(([a], [b]) => + a.localeCompare(b) + ); + + logger.debug( + `Flushing accumulated bandwidth data for ${sortedEntries.length} site(s) to the database` + ); + + // Aggregate billing usage by org, collected during the DB update loop. + const orgUsageMap = new Map(); + + for (const [publicKey, { bytesIn, bytesOut, exitNodeId, calcUsage }] of sortedEntries) { + try { + const updatedSite = await withDeadlockRetry(async () => { + const [result] = await db + .update(sites) + .set({ + megabytesOut: sql`COALESCE(${sites.megabytesOut}, 0) + ${bytesIn}`, + megabytesIn: sql`COALESCE(${sites.megabytesIn}, 0) + ${bytesOut}`, + lastBandwidthUpdate: currentTime, + }) + .where(eq(sites.pubKey, publicKey)) + .returning({ + orgId: sites.orgId, + siteId: sites.siteId + }); + return result; + }, `flush bandwidth for site ${publicKey}`); + + if (updatedSite) { + if (exitNodeId) { + const notAllowed = await checkExitNodeOrg( + exitNodeId, + updatedSite.orgId + ); + if (notAllowed) { + logger.warn( + `Exit node ${exitNodeId} is not allowed for org ${updatedSite.orgId}` + ); + // Skip usage tracking for this site but continue + // processing the rest. + continue; + } + } + + if (calcUsage) { + const totalBandwidth = bytesIn + bytesOut; + const current = orgUsageMap.get(updatedSite.orgId) ?? 0; + orgUsageMap.set(updatedSite.orgId, current + totalBandwidth); + } + } + } catch (error) { + logger.error( + `Failed to flush bandwidth for site ${publicKey}:`, + error + ); + + // Re-queue the failed entry so it is retried on the next flush + // rather than silently dropped. + const existing = accumulator.get(publicKey); + if (existing) { + existing.bytesIn += bytesIn; + existing.bytesOut += bytesOut; + } else { + accumulator.set(publicKey, { + bytesIn, + bytesOut, + exitNodeId, + calcUsage + }); + } + } + } + + // Process billing usage updates outside the site-update loop to keep + // lock scope small and concerns separated. + if (orgUsageMap.size > 0) { + // Sort org IDs for consistent lock ordering. + const sortedOrgIds = [...orgUsageMap.keys()].sort(); + + for (const orgId of sortedOrgIds) { + try { + const totalBandwidth = orgUsageMap.get(orgId)!; + const bandwidthUsage = await usageService.add( + orgId, + FeatureId.EGRESS_DATA_MB, + totalBandwidth + ); + if (bandwidthUsage) { + // Fire-and-forget — don't block the flush on limit checking. + usageService + .checkLimitSet( + orgId, + FeatureId.EGRESS_DATA_MB, + bandwidthUsage + ) + .catch((error: any) => { + logger.error( + `Error checking bandwidth limits for org ${orgId}:`, + error + ); + }); + } + } catch (error) { + logger.error( + `Error processing usage for org ${orgId}:`, + error + ); + // Continue with other orgs. + } + } + } +} + +// --------------------------------------------------------------------------- +// Periodic flush timer +// --------------------------------------------------------------------------- + +const flushTimer = setInterval(async () => { + try { + await flushSiteBandwidthToDb(); + } catch (error) { + logger.error( + "Unexpected error during periodic site bandwidth flush:", + error + ); + } +}, FLUSH_INTERVAL_MS); + +// Allow the process to exit normally even while the timer is pending. +// The graceful-shutdown path (see server/cleanup.ts) will call +// flushSiteBandwidthToDb() explicitly before process.exit(), so no data +// is lost. +flushTimer.unref(); + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Accumulate bandwidth data reported by a gerbil or remote exit node. + * + * Only peers that actually transferred data (bytesIn > 0) are added to the + * accumulator; peers with no activity are silently ignored, which means the + * flush will only write rows that have genuinely changed. + * + * The function is intentionally synchronous in its fast path so that the + * HTTP handler can respond immediately without waiting for any I/O. + */ +export async function updateSiteBandwidth( + bandwidthData: PeerBandwidth[], + calcUsageAndLimits: boolean, + exitNodeId?: number +): Promise { + for (const { publicKey, bytesIn, bytesOut } of bandwidthData) { + // Skip peers that haven't transferred any data — writing zeros to the + // database would be a no-op anyway. + if (bytesIn <= 0 && bytesOut <= 0) { + continue; + } + + const existing = accumulator.get(publicKey); + if (existing) { + existing.bytesIn += bytesIn; + existing.bytesOut += bytesOut; + // Retain the most-recent exitNodeId for this peer. + if (exitNodeId !== undefined) { + existing.exitNodeId = exitNodeId; + } + // Once calcUsage has been requested for a peer, keep it set for + // the lifetime of this flush window. + if (calcUsageAndLimits) { + existing.calcUsage = true; + } + } else { + accumulator.set(publicKey, { + bytesIn, + bytesOut, + exitNodeId, + calcUsage: calcUsageAndLimits + }); + } + } +} + +// --------------------------------------------------------------------------- +// HTTP handler +// --------------------------------------------------------------------------- + export const receiveBandwidth = async ( req: Request, res: Response, @@ -75,7 +301,9 @@ export const receiveBandwidth = async ( throw new Error("Invalid bandwidth data"); } - await updateSiteBandwidth(bandwidthData, build == "saas"); // we are checking the usage on saas only + // Accumulate in memory; the periodic timer (and the shutdown hook) + // will write to the database. + await updateSiteBandwidth(bandwidthData, build == "saas"); return response(res, { data: {}, @@ -94,201 +322,3 @@ export const receiveBandwidth = async ( ); } }; - -export async function updateSiteBandwidth( - bandwidthData: PeerBandwidth[], - calcUsageAndLimits: boolean, - exitNodeId?: number -) { - const currentTime = new Date(); - const oneMinuteAgo = new Date(currentTime.getTime() - 60000); // 1 minute ago - - // Sort bandwidth data by publicKey to ensure consistent lock ordering across all instances - // This is critical for preventing deadlocks when multiple instances update the same sites - const sortedBandwidthData = [...bandwidthData].sort((a, b) => - a.publicKey.localeCompare(b.publicKey) - ); - - // First, handle sites that are actively reporting bandwidth - const activePeers = sortedBandwidthData.filter((peer) => peer.bytesIn > 0); - - // Aggregate usage data by organization (collected outside transaction) - const orgUsageMap = new Map(); - - if (activePeers.length > 0) { - // Remove any active peers from offline tracking since they're sending data - activePeers.forEach((peer) => offlineSites.delete(peer.publicKey)); - - // Update each active site individually with retry logic - // This reduces transaction scope and allows retries per-site - for (const peer of activePeers) { - try { - const updatedSite = await withDeadlockRetry(async () => { - const [result] = await db - .update(sites) - .set({ - megabytesOut: sql`${sites.megabytesOut} + ${peer.bytesIn}`, - megabytesIn: sql`${sites.megabytesIn} + ${peer.bytesOut}`, - lastBandwidthUpdate: currentTime.toISOString(), - online: true - }) - .where(eq(sites.pubKey, peer.publicKey)) - .returning({ - online: sites.online, - orgId: sites.orgId, - siteId: sites.siteId, - lastBandwidthUpdate: sites.lastBandwidthUpdate - }); - return result; - }, `update active site ${peer.publicKey}`); - - if (updatedSite) { - if (exitNodeId) { - const notAllowed = await checkExitNodeOrg( - exitNodeId, - updatedSite.orgId - ); - if (notAllowed) { - logger.warn( - `Exit node ${exitNodeId} is not allowed for org ${updatedSite.orgId}` - ); - // Skip this site but continue processing others - continue; - } - } - - // Aggregate bandwidth usage for the org - const totalBandwidth = peer.bytesIn + peer.bytesOut; - const currentOrgUsage = - orgUsageMap.get(updatedSite.orgId) || 0; - orgUsageMap.set( - updatedSite.orgId, - currentOrgUsage + totalBandwidth - ); - } - } catch (error) { - logger.error( - `Failed to update bandwidth for site ${peer.publicKey}:`, - error - ); - // Continue with other sites - } - } - } - - // Process usage updates outside of site update transactions - // This separates the concerns and reduces lock contention - if (calcUsageAndLimits && orgUsageMap.size > 0) { - // Sort org IDs to ensure consistent lock ordering - const allOrgIds = [...new Set([...orgUsageMap.keys()])].sort(); - - for (const orgId of allOrgIds) { - try { - // Process bandwidth usage for this org - const totalBandwidth = orgUsageMap.get(orgId); - if (totalBandwidth) { - const bandwidthUsage = await usageService.add( - orgId, - FeatureId.EGRESS_DATA_MB, - totalBandwidth - ); - if (bandwidthUsage) { - // Fire and forget - don't block on limit checking - usageService - .checkLimitSet( - orgId, - FeatureId.EGRESS_DATA_MB, - bandwidthUsage - ) - .catch((error: any) => { - logger.error( - `Error checking bandwidth limits for org ${orgId}:`, - error - ); - }); - } - } - } catch (error) { - logger.error(`Error processing usage for org ${orgId}:`, error); - // Continue with other orgs - } - } - } - - // Handle sites that reported zero bandwidth but need online status updated - const zeroBandwidthPeers = sortedBandwidthData.filter( - (peer) => peer.bytesIn === 0 && !offlineSites.has(peer.publicKey) - ); - - if (zeroBandwidthPeers.length > 0) { - // Fetch all zero bandwidth sites in one query - const zeroBandwidthSites = await db - .select() - .from(sites) - .where( - inArray( - sites.pubKey, - zeroBandwidthPeers.map((p) => p.publicKey) - ) - ); - - // Sort by siteId to ensure consistent lock ordering - const sortedZeroBandwidthSites = zeroBandwidthSites.sort( - (a, b) => a.siteId - b.siteId - ); - - for (const site of sortedZeroBandwidthSites) { - let newOnlineStatus = site.online; - - // Check if site should go offline based on last bandwidth update WITH DATA - if (site.lastBandwidthUpdate) { - const lastUpdateWithData = new Date(site.lastBandwidthUpdate); - if (lastUpdateWithData < oneMinuteAgo) { - newOnlineStatus = false; - } - } else { - // No previous data update recorded, set to offline - newOnlineStatus = false; - } - - // Only update online status if it changed - if (site.online !== newOnlineStatus) { - try { - const updatedSite = await withDeadlockRetry(async () => { - const [result] = await db - .update(sites) - .set({ - online: newOnlineStatus - }) - .where(eq(sites.siteId, site.siteId)) - .returning(); - return result; - }, `update offline status for site ${site.siteId}`); - - if (updatedSite && exitNodeId) { - const notAllowed = await checkExitNodeOrg( - exitNodeId, - updatedSite.orgId - ); - if (notAllowed) { - logger.warn( - `Exit node ${exitNodeId} is not allowed for org ${updatedSite.orgId}` - ); - } - } - - // If site went offline, add it to our tracking set - if (!newOnlineStatus && site.pubKey) { - offlineSites.add(site.pubKey); - } - } catch (error) { - logger.error( - `Failed to update offline status for site ${site.siteId}:`, - error - ); - // Continue with other sites - } - } - } - } -} diff --git a/server/routers/gerbil/updateHolePunch.ts b/server/routers/gerbil/updateHolePunch.ts index 3f24430bf..810c44ff7 100644 --- a/server/routers/gerbil/updateHolePunch.ts +++ b/server/routers/gerbil/updateHolePunch.ts @@ -112,7 +112,7 @@ export async function updateHolePunch( destinations: destinations }); } catch (error) { - // logger.error(error); // FIX THIS + logger.error(error); return next( createHttpError( HttpCode.INTERNAL_SERVER_ERROR, @@ -262,7 +262,7 @@ export async function updateAndGenerateEndpointDestinations( if (site.subnet && site.listenPort) { destinations.push({ destinationIP: site.subnet.split("/")[0], - destinationPort: site.listenPort + destinationPort: site.listenPort || 1 // this satisfies gerbil for now but should be reevaluated }); } } @@ -339,10 +339,10 @@ export async function updateAndGenerateEndpointDestinations( handleSiteEndpointChange(newt.siteId, updatedSite.endpoint!); } - if (!updatedSite || !updatedSite.subnet) { - logger.warn(`Site not found: ${newt.siteId}`); - throw new Error("Site not found"); - } + // if (!updatedSite || !updatedSite.subnet) { + // logger.warn(`Site not found: ${newt.siteId}`); + // throw new Error("Site not found"); + // } // Find all clients that connect to this site // const sitesClientPairs = await db diff --git a/server/routers/idp/createIdpOrgPolicy.ts b/server/routers/idp/createIdpOrgPolicy.ts index dc7af5377..da12bc12a 100644 --- a/server/routers/idp/createIdpOrgPolicy.ts +++ b/server/routers/idp/createIdpOrgPolicy.ts @@ -27,7 +27,7 @@ registry.registerPath({ method: "put", path: "/idp/{idpId}/org/{orgId}", description: "Create an IDP policy for an existing IDP on an organization.", - tags: [OpenAPITags.Idp], + tags: [OpenAPITags.GlobalIdp], request: { params: paramsSchema, body: { diff --git a/server/routers/idp/createOidcIdp.ts b/server/routers/idp/createOidcIdp.ts index 03626bfde..5b53f6820 100644 --- a/server/routers/idp/createOidcIdp.ts +++ b/server/routers/idp/createOidcIdp.ts @@ -37,7 +37,7 @@ registry.registerPath({ method: "put", path: "/idp/oidc", description: "Create an OIDC IdP.", - tags: [OpenAPITags.Idp], + tags: [OpenAPITags.GlobalIdp], request: { body: { content: { diff --git a/server/routers/idp/deleteIdp.ts b/server/routers/idp/deleteIdp.ts index f2b550993..8c15eada1 100644 --- a/server/routers/idp/deleteIdp.ts +++ b/server/routers/idp/deleteIdp.ts @@ -21,7 +21,7 @@ registry.registerPath({ method: "delete", path: "/idp/{idpId}", description: "Delete IDP.", - tags: [OpenAPITags.Idp], + tags: [OpenAPITags.GlobalIdp], request: { params: paramsSchema }, diff --git a/server/routers/idp/deleteIdpOrgPolicy.ts b/server/routers/idp/deleteIdpOrgPolicy.ts index b52a37df2..6793474a8 100644 --- a/server/routers/idp/deleteIdpOrgPolicy.ts +++ b/server/routers/idp/deleteIdpOrgPolicy.ts @@ -19,7 +19,7 @@ registry.registerPath({ method: "delete", path: "/idp/{idpId}/org/{orgId}", description: "Create an OIDC IdP for an organization.", - tags: [OpenAPITags.Idp], + tags: [OpenAPITags.GlobalIdp], request: { params: paramsSchema }, diff --git a/server/routers/idp/getIdp.ts b/server/routers/idp/getIdp.ts index 072537513..db199f2d6 100644 --- a/server/routers/idp/getIdp.ts +++ b/server/routers/idp/getIdp.ts @@ -34,7 +34,7 @@ registry.registerPath({ method: "get", path: "/idp/{idpId}", description: "Get an IDP by its IDP ID.", - tags: [OpenAPITags.Idp], + tags: [OpenAPITags.GlobalIdp], request: { params: paramsSchema }, diff --git a/server/routers/idp/listIdpOrgPolicies.ts b/server/routers/idp/listIdpOrgPolicies.ts index 9f7cdb42b..ecfd6f33a 100644 --- a/server/routers/idp/listIdpOrgPolicies.ts +++ b/server/routers/idp/listIdpOrgPolicies.ts @@ -48,7 +48,7 @@ registry.registerPath({ method: "get", path: "/idp/{idpId}/org", description: "List all org policies on an IDP.", - tags: [OpenAPITags.Idp], + tags: [OpenAPITags.GlobalIdp], request: { params: paramsSchema, query: querySchema diff --git a/server/routers/idp/listIdps.ts b/server/routers/idp/listIdps.ts index 9dda11bbf..ca0fd5fbf 100644 --- a/server/routers/idp/listIdps.ts +++ b/server/routers/idp/listIdps.ts @@ -58,7 +58,7 @@ registry.registerPath({ method: "get", path: "/idp", description: "List all IDP in the system.", - tags: [OpenAPITags.Idp], + tags: [OpenAPITags.GlobalIdp], request: { query: querySchema }, diff --git a/server/routers/idp/updateIdpOrgPolicy.ts b/server/routers/idp/updateIdpOrgPolicy.ts index ea08de420..5a9f882d4 100644 --- a/server/routers/idp/updateIdpOrgPolicy.ts +++ b/server/routers/idp/updateIdpOrgPolicy.ts @@ -26,7 +26,7 @@ registry.registerPath({ method: "post", path: "/idp/{idpId}/org/{orgId}", description: "Update an IDP org policy.", - tags: [OpenAPITags.Idp], + tags: [OpenAPITags.GlobalIdp], request: { params: paramsSchema, body: { diff --git a/server/routers/idp/updateOidcIdp.ts b/server/routers/idp/updateOidcIdp.ts index 82aed75ce..fe32a8b08 100644 --- a/server/routers/idp/updateOidcIdp.ts +++ b/server/routers/idp/updateOidcIdp.ts @@ -42,7 +42,7 @@ registry.registerPath({ method: "post", path: "/idp/{idpId}/oidc", description: "Update an OIDC IdP.", - tags: [OpenAPITags.Idp], + tags: [OpenAPITags.GlobalIdp], request: { params: paramsSchema, body: { diff --git a/server/routers/integration.ts b/server/routers/integration.ts index 56e44c661..e77f82c83 100644 --- a/server/routers/integration.ts +++ b/server/routers/integration.ts @@ -27,7 +27,8 @@ import { verifyApiKeyClientAccess, verifyApiKeySiteResourceAccess, verifyApiKeySetResourceClients, - verifyLimits + verifyLimits, + verifyApiKeyDomainAccess } from "@server/middlewares"; import HttpCode from "@server/types/HttpCode"; import { Router } from "express"; @@ -134,6 +135,13 @@ authenticated.post( logActionAudit(ActionsEnum.updateSite), site.updateSite ); +authenticated.post( + "/org/:orgId/reset-bandwidth", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.resetSiteBandwidth), + logActionAudit(ActionsEnum.resetSiteBandwidth), + org.resetOrgBandwidth +); authenticated.delete( "/site/:siteId", @@ -308,6 +316,14 @@ authenticated.post( siteResource.removeClientFromSiteResource ); +authenticated.post( + "/client/:clientId/site-resources", + verifyLimits, + verifyApiKeyHasAction(ActionsEnum.setResourceUsers), + logActionAudit(ActionsEnum.setResourceUsers), + siteResource.batchAddClientToSiteResources +); + authenticated.put( "/org/:orgId/resource", verifyApiKeyOrgAccess, @@ -347,6 +363,56 @@ authenticated.get( domain.listDomains ); +authenticated.get( + "/org/:orgId/domain/:domainId", + verifyApiKeyOrgAccess, + verifyApiKeyDomainAccess, + verifyApiKeyHasAction(ActionsEnum.getDomain), + domain.getDomain +); + +authenticated.put( + "/org/:orgId/domain", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.createOrgDomain), + logActionAudit(ActionsEnum.createOrgDomain), + domain.createOrgDomain +); + +authenticated.patch( + "/org/:orgId/domain/:domainId", + verifyApiKeyOrgAccess, + verifyApiKeyDomainAccess, + verifyApiKeyHasAction(ActionsEnum.updateOrgDomain), + domain.updateOrgDomain +); + +authenticated.delete( + "/org/:orgId/domain/:domainId", + verifyApiKeyOrgAccess, + verifyApiKeyDomainAccess, + verifyApiKeyHasAction(ActionsEnum.deleteOrgDomain), + logActionAudit(ActionsEnum.deleteOrgDomain), + domain.deleteAccountDomain +); + +authenticated.get( + "/org/:orgId/domain/:domainId/dns-records", + verifyApiKeyOrgAccess, + verifyApiKeyDomainAccess, + verifyApiKeyHasAction(ActionsEnum.getDNSRecords), + domain.getDNSRecords +); + +authenticated.post( + "/org/:orgId/domain/:domainId/restart", + verifyApiKeyOrgAccess, + verifyApiKeyDomainAccess, + verifyApiKeyHasAction(ActionsEnum.restartOrgDomain), + logActionAudit(ActionsEnum.restartOrgDomain), + domain.restartOrgDomain +); + authenticated.get( "/org/:orgId/invitations", verifyApiKeyOrgAccess, @@ -699,6 +765,13 @@ authenticated.get( user.getOrgUser ); +authenticated.get( + "/org/:orgId/user-by-username", + verifyApiKeyOrgAccess, + verifyApiKeyHasAction(ActionsEnum.getOrgUser), + user.getOrgUserByUsername +); + authenticated.post( "/user/:userId/2fa", verifyApiKeyIsRoot, diff --git a/server/routers/newt/buildConfiguration.ts b/server/routers/newt/buildConfiguration.ts index e349f24e8..c3a261f03 100644 --- a/server/routers/newt/buildConfiguration.ts +++ b/server/routers/newt/buildConfiguration.ts @@ -1,9 +1,24 @@ -import { clients, clientSiteResourcesAssociationsCache, clientSitesAssociationsCache, db, ExitNode, resources, Site, siteResources, targetHealthCheck, targets } from "@server/db"; +import { + clients, + clientSiteResourcesAssociationsCache, + clientSitesAssociationsCache, + db, + ExitNode, + resources, + Site, + siteResources, + targetHealthCheck, + targets +} from "@server/db"; import logger from "@server/logger"; import { initPeerAddHandshake, updatePeer } from "../olm/peers"; import { eq, and } from "drizzle-orm"; import config from "@server/lib/config"; -import { generateSubnetProxyTargets, SubnetProxyTarget } from "@server/lib/ip"; +import { + formatEndpoint, + generateSubnetProxyTargets, + SubnetProxyTarget +} from "@server/lib/ip"; export async function buildClientConfigurationForNewtClient( site: Site, @@ -69,40 +84,42 @@ export async function buildClientConfigurationForNewtClient( // ) // ); - // update the peer info on the olm - // if the peer has not been added yet this will be a no-op - await updatePeer(client.clients.clientId, { - siteId: site.siteId, - endpoint: site.endpoint!, - relayEndpoint: `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`, - publicKey: site.publicKey!, - serverIP: site.address, - serverPort: site.listenPort - // remoteSubnets: generateRemoteSubnets( - // allSiteResources.map( - // ({ siteResources }) => siteResources - // ) - // ), - // aliases: generateAliasConfig( - // allSiteResources.map( - // ({ siteResources }) => siteResources - // ) - // ) - }); + if (!client.clientSitesAssociationsCache.isJitMode) { // if we are adding sites through jit then dont add the site to the olm + // update the peer info on the olm + // if the peer has not been added yet this will be a no-op + await updatePeer(client.clients.clientId, { + siteId: site.siteId, + endpoint: site.endpoint!, + relayEndpoint: `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`, + publicKey: site.publicKey!, + serverIP: site.address, + serverPort: site.listenPort + // remoteSubnets: generateRemoteSubnets( + // allSiteResources.map( + // ({ siteResources }) => siteResources + // ) + // ), + // aliases: generateAliasConfig( + // allSiteResources.map( + // ({ siteResources }) => siteResources + // ) + // ) + }); - // also trigger the peer add handshake in case the peer was not already added to the olm and we need to hole punch - // if it has already been added this will be a no-op - await initPeerAddHandshake( - // this will kick off the add peer process for the client - client.clients.clientId, - { - siteId, - exitNode: { - publicKey: exitNode.publicKey, - endpoint: exitNode.endpoint + // also trigger the peer add handshake in case the peer was not already added to the olm and we need to hole punch + // if it has already been added this will be a no-op + await initPeerAddHandshake( + // this will kick off the add peer process for the client + client.clients.clientId, + { + siteId, + exitNode: { + publicKey: exitNode.publicKey, + endpoint: exitNode.endpoint + } } - } - ); + ); + } return { publicKey: client.clients.pubKey!, @@ -188,7 +205,8 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) { hcTimeout: targetHealthCheck.hcTimeout, hcHeaders: targetHealthCheck.hcHeaders, hcMethod: targetHealthCheck.hcMethod, - hcTlsServerName: targetHealthCheck.hcTlsServerName + hcTlsServerName: targetHealthCheck.hcTlsServerName, + hcStatus: targetHealthCheck.hcStatus }) .from(targets) .innerJoin(resources, eq(targets.resourceId, resources.resourceId)) @@ -205,8 +223,8 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) { return acc; } - // Format target into string - const formattedTarget = `${target.internalPort}:${target.ip}:${target.port}`; + // Format target into string (handles IPv6 bracketing) + const formattedTarget = `${target.internalPort}:${formatEndpoint(target.ip, target.port)}`; // Add to the appropriate protocol array if (target.protocol === "tcp") { @@ -229,9 +247,9 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) { !target.hcInterval || !target.hcMethod ) { - logger.debug( - `Skipping target ${target.targetId} due to missing health check fields` - ); + // logger.debug( + // `Skipping adding target health check ${target.targetId} due to missing health check fields` + // ); return null; // Skip targets with missing health check fields } @@ -261,7 +279,8 @@ export async function buildTargetConfigurationForNewtClient(siteId: number) { hcTimeout: target.hcTimeout, // in seconds hcHeaders: hcHeadersSend, hcMethod: target.hcMethod, - hcTlsServerName: target.hcTlsServerName + hcTlsServerName: target.hcTlsServerName, + hcStatus: target.hcStatus }; }); diff --git a/server/routers/newt/handleGetConfigMessage.ts b/server/routers/newt/handleGetConfigMessage.ts index 801c8b65a..d536e9828 100644 --- a/server/routers/newt/handleGetConfigMessage.ts +++ b/server/routers/newt/handleGetConfigMessage.ts @@ -6,6 +6,7 @@ import { db, ExitNode, exitNodes, Newt, sites } from "@server/db"; import { eq } from "drizzle-orm"; import { sendToExitNode } from "#dynamic/lib/exitNodes"; import { buildClientConfigurationForNewtClient } from "./buildConfiguration"; +import { canCompress } from "@server/lib/clientVersionChecks"; const inputSchema = z.object({ publicKey: z.string(), @@ -104,11 +105,11 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { const payload = { oldDestination: { destinationIP: existingSite.subnet?.split("/")[0], - destinationPort: existingSite.listenPort + destinationPort: existingSite.listenPort || 1 // this satisfies gerbil for now but should be reevaluated }, newDestination: { destinationIP: site.subnet?.split("/")[0], - destinationPort: site.listenPort + destinationPort: site.listenPort || 1 // this satisfies gerbil for now but should be reevaluated } }; @@ -135,6 +136,9 @@ export const handleGetConfigMessage: MessageHandler = async (context) => { targets } }, + options: { + compress: canCompress(newt.version, "newt") + }, broadcast: false, excludeSender: false }; diff --git a/server/routers/newt/handleNewtDisconnectingMessage.ts b/server/routers/newt/handleNewtDisconnectingMessage.ts new file mode 100644 index 000000000..02c5a95ac --- /dev/null +++ b/server/routers/newt/handleNewtDisconnectingMessage.ts @@ -0,0 +1,36 @@ +import { MessageHandler } from "@server/routers/ws"; +import { db, Newt, sites } from "@server/db"; +import { eq } from "drizzle-orm"; +import logger from "@server/logger"; + +/** + * Handles disconnecting messages from sites to show disconnected in the ui + */ +export const handleNewtDisconnectingMessage: MessageHandler = async ( + context +) => { + const { message, client: c, sendToClient } = context; + const newt = c as Newt; + + if (!newt) { + logger.warn("Newt not found"); + return; + } + + if (!newt.siteId) { + logger.warn("Newt has no client ID!"); + return; + } + + try { + // Update the client's last ping timestamp + await db + .update(sites) + .set({ + online: false + }) + .where(eq(sites.siteId, newt.siteId)); + } catch (error) { + logger.error("Error handling disconnecting message", { error }); + } +}; diff --git a/server/routers/newt/handleNewtPingMessage.ts b/server/routers/newt/handleNewtPingMessage.ts index dc9aacdd9..319647b83 100644 --- a/server/routers/newt/handleNewtPingMessage.ts +++ b/server/routers/newt/handleNewtPingMessage.ts @@ -1,105 +1,107 @@ -import { db, sites } from "@server/db"; -import { disconnectClient, getClientConfigVersion } from "#dynamic/routers/ws"; +import { db, newts, sites } from "@server/db"; +import { hasActiveConnections, getClientConfigVersion } from "#dynamic/routers/ws"; import { MessageHandler } from "@server/routers/ws"; -import { clients, Newt } from "@server/db"; +import { Newt } from "@server/db"; import { eq, lt, isNull, and, or } from "drizzle-orm"; import logger from "@server/logger"; -import { validateSessionToken } from "@server/auth/sessions/app"; -import { checkOrgAccessPolicy } from "#dynamic/lib/checkOrgAccessPolicy"; -import { sendTerminateClient } from "../client/terminate"; -import { encodeHexLowerCase } from "@oslojs/encoding"; -import { sha256 } from "@oslojs/crypto/sha2"; import { sendNewtSyncMessage } from "./sync"; // Track if the offline checker interval is running -// let offlineCheckerInterval: NodeJS.Timeout | null = null; -// const OFFLINE_CHECK_INTERVAL = 30 * 1000; // Check every 30 seconds -// const OFFLINE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes +let offlineCheckerInterval: NodeJS.Timeout | null = null; +const OFFLINE_CHECK_INTERVAL = 30 * 1000; // Check every 30 seconds +const OFFLINE_THRESHOLD_MS = 2 * 60 * 1000; // 2 minutes /** - * Starts the background interval that checks for clients that haven't pinged recently - * and marks them as offline + * Starts the background interval that checks for newt sites that haven't + * pinged recently and marks them as offline. For backward compatibility, + * a site is only marked offline when there is no active WebSocket connection + * either — so older newt versions that don't send pings but remain connected + * continue to be treated as online. */ -// export const startNewtOfflineChecker = (): void => { -// if (offlineCheckerInterval) { -// return; // Already running -// } +export const startNewtOfflineChecker = (): void => { + if (offlineCheckerInterval) { + return; // Already running + } -// offlineCheckerInterval = setInterval(async () => { -// try { -// const twoMinutesAgo = Math.floor( -// (Date.now() - OFFLINE_THRESHOLD_MS) / 1000 -// ); + offlineCheckerInterval = setInterval(async () => { + try { + const twoMinutesAgo = Math.floor( + (Date.now() - OFFLINE_THRESHOLD_MS) / 1000 + ); -// // TODO: WE NEED TO MAKE SURE THIS WORKS WITH DISTRIBUTED NODES ALL DOING THE SAME THING + // Find all online newt-type sites that haven't pinged recently + // (or have never pinged at all). Join newts to obtain the newtId + // needed for the WebSocket connection check. + const staleSites = await db + .select({ + siteId: sites.siteId, + newtId: newts.newtId, + lastPing: sites.lastPing + }) + .from(sites) + .innerJoin(newts, eq(newts.siteId, sites.siteId)) + .where( + and( + eq(sites.online, true), + eq(sites.type, "newt"), + or( + lt(sites.lastPing, twoMinutesAgo), + isNull(sites.lastPing) + ) + ) + ); -// // Find clients that haven't pinged in the last 2 minutes and mark them as offline -// const offlineClients = await db -// .update(clients) -// .set({ online: false }) -// .where( -// and( -// eq(clients.online, true), -// or( -// lt(clients.lastPing, twoMinutesAgo), -// isNull(clients.lastPing) -// ) -// ) -// ) -// .returning(); + for (const staleSite of staleSites) { + // Backward-compatibility check: if the newt still has an + // active WebSocket connection (older clients that don't send + // pings), keep the site online. + const isConnected = await hasActiveConnections(staleSite.newtId); + if (isConnected) { + logger.debug( + `Newt ${staleSite.newtId} has not pinged recently but is still connected via WebSocket — keeping site ${staleSite.siteId} online` + ); + continue; + } -// for (const offlineClient of offlineClients) { -// logger.info( -// `Kicking offline newt client ${offlineClient.clientId} due to inactivity` -// ); + logger.info( + `Marking site ${staleSite.siteId} offline: newt ${staleSite.newtId} has no recent ping and no active WebSocket connection` + ); -// if (!offlineClient.newtId) { -// logger.warn( -// `Offline client ${offlineClient.clientId} has no newtId, cannot disconnect` -// ); -// continue; -// } + await db + .update(sites) + .set({ online: false }) + .where(eq(sites.siteId, staleSite.siteId)); + } + } catch (error) { + logger.error("Error in newt offline checker interval", { error }); + } + }, OFFLINE_CHECK_INTERVAL); -// // Send a disconnect message to the client if connected -// try { -// await sendTerminateClient( -// offlineClient.clientId, -// offlineClient.newtId -// ); // terminate first -// // wait a moment to ensure the message is sent -// await new Promise((resolve) => setTimeout(resolve, 1000)); -// await disconnectClient(offlineClient.newtId); -// } catch (error) { -// logger.error( -// `Error sending disconnect to offline newt ${offlineClient.clientId}`, -// { error } -// ); -// } -// } -// } catch (error) { -// logger.error("Error in offline checker interval", { error }); -// } -// }, OFFLINE_CHECK_INTERVAL); - -// logger.debug("Started offline checker interval"); -// }; + logger.debug("Started newt offline checker interval"); +}; /** - * Stops the background interval that checks for offline clients + * Stops the background interval that checks for offline newt sites. */ -// export const stopNewtOfflineChecker = (): void => { -// if (offlineCheckerInterval) { -// clearInterval(offlineCheckerInterval); -// offlineCheckerInterval = null; -// logger.info("Stopped offline checker interval"); -// } -// }; +export const stopNewtOfflineChecker = (): void => { + if (offlineCheckerInterval) { + clearInterval(offlineCheckerInterval); + offlineCheckerInterval = null; + logger.info("Stopped newt offline checker interval"); + } +}; /** - * Handles ping messages from clients and responds with pong + * Handles ping messages from newt clients. + * + * On each ping: + * - Marks the associated site as online. + * - Records the current timestamp as the newt's last-ping time. + * - Triggers a config sync if the newt is running an outdated config version. + * - Responds with a pong message. */ export const handleNewtPingMessage: MessageHandler = async (context) => { - const { message, client: c, sendToClient } = context; + const { message, client: c } = context; const newt = c as Newt; if (!newt) { @@ -112,15 +114,31 @@ export const handleNewtPingMessage: MessageHandler = async (context) => { return; } - // get the version + try { + // Mark the site as online and record the ping timestamp. + await db + .update(sites) + .set({ + online: true, + lastPing: Math.floor(Date.now() / 1000) + }) + .where(eq(sites.siteId, newt.siteId)); + } catch (error) { + logger.error("Error updating online state on newt ping", { error }); + } + + // Check config version and sync if stale. const configVersion = await getClientConfigVersion(newt.newtId); - if (message.configVersion && configVersion != null && configVersion != message.configVersion) { + if ( + message.configVersion != null && + configVersion != null && + configVersion !== message.configVersion + ) { logger.warn( `Newt ping with outdated config version: ${message.configVersion} (current: ${configVersion})` ); - // get the site const [site] = await db .select() .from(sites) @@ -137,19 +155,6 @@ export const handleNewtPingMessage: MessageHandler = async (context) => { await sendNewtSyncMessage(newt, site); } - // try { - // // Update the client's last ping timestamp - // await db - // .update(clients) - // .set({ - // lastPing: Math.floor(Date.now() / 1000), - // online: true - // }) - // .where(eq(clients.clientId, newt.clientId)); - // } catch (error) { - // logger.error("Error handling ping message", { error }); - // } - return { message: { type: "pong", diff --git a/server/routers/newt/handleNewtRegisterMessage.ts b/server/routers/newt/handleNewtRegisterMessage.ts index 595430df5..90034cfbf 100644 --- a/server/routers/newt/handleNewtRegisterMessage.ts +++ b/server/routers/newt/handleNewtRegisterMessage.ts @@ -5,9 +5,7 @@ import { eq } from "drizzle-orm"; import { addPeer, deletePeer } from "../gerbil/peers"; import logger from "@server/logger"; import config from "@server/lib/config"; -import { - findNextAvailableCidr, -} from "@server/lib/ip"; +import { findNextAvailableCidr } from "@server/lib/ip"; import { selectBestExitNode, verifyExitNodeOrgAccess @@ -15,6 +13,7 @@ import { import { fetchContainers } from "./dockerSocket"; import { lockManager } from "#dynamic/lib/lock"; import { buildTargetConfigurationForNewtClient } from "./buildConfiguration"; +import { canCompress } from "@server/lib/clientVersionChecks"; export type ExitNodePingResult = { exitNodeId: number; @@ -215,6 +214,9 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => { healthCheckTargets: validHealthCheckTargets } }, + options: { + compress: canCompress(newt.version, "newt") + }, broadcast: false, // Send to all clients excludeSender: false // Include sender in broadcast }; diff --git a/server/routers/newt/handleReceiveBandwidthMessage.ts b/server/routers/newt/handleReceiveBandwidthMessage.ts index eb930e682..f086333e7 100644 --- a/server/routers/newt/handleReceiveBandwidthMessage.ts +++ b/server/routers/newt/handleReceiveBandwidthMessage.ts @@ -10,10 +10,21 @@ interface PeerBandwidth { bytesOut: number; } +interface BandwidthAccumulator { + bytesIn: number; + bytesOut: number; +} + // Retry configuration for deadlock handling const MAX_RETRIES = 3; const BASE_DELAY_MS = 50; +// How often to flush accumulated bandwidth data to the database +const FLUSH_INTERVAL_MS = 120_000; // 120 seconds + +// In-memory accumulator: publicKey -> { bytesIn, bytesOut } +let accumulator = new Map(); + /** * Check if an error is a deadlock error */ @@ -53,6 +64,90 @@ async function withDeadlockRetry( } } +/** + * Flush all accumulated bandwidth data to the database. + * + * Swaps out the accumulator before writing so that any bandwidth messages + * received during the flush are captured in the new accumulator rather than + * being lost or causing contention. Entries that fail to write are re-queued + * back into the accumulator so they will be retried on the next flush. + * + * This function is exported so that the application's graceful-shutdown + * cleanup handler can call it before the process exits. + */ +export async function flushBandwidthToDb(): Promise { + if (accumulator.size === 0) { + return; + } + + // Atomically swap out the accumulator so new data keeps flowing in + // while we write the snapshot to the database. + const snapshot = accumulator; + accumulator = new Map(); + + const currentTime = new Date().toISOString(); + + // Sort by publicKey for consistent lock ordering across concurrent + // writers — this is the same deadlock-prevention strategy used in the + // original per-message implementation. + const sortedEntries = [...snapshot.entries()].sort(([a], [b]) => + a.localeCompare(b) + ); + + logger.debug( + `Flushing accumulated bandwidth data for ${sortedEntries.length} client(s) to the database` + ); + + for (const [publicKey, { bytesIn, bytesOut }] of sortedEntries) { + try { + await withDeadlockRetry(async () => { + // Use atomic SQL increment to avoid the SELECT-then-UPDATE + // anti-pattern and the races it would introduce. + await db + .update(clients) + .set({ + // Note: bytesIn from peer goes to megabytesOut (data + // sent to client) and bytesOut from peer goes to + // megabytesIn (data received from client). + megabytesOut: sql`COALESCE(${clients.megabytesOut}, 0) + ${bytesIn}`, + megabytesIn: sql`COALESCE(${clients.megabytesIn}, 0) + ${bytesOut}`, + lastBandwidthUpdate: currentTime + }) + .where(eq(clients.pubKey, publicKey)); + }, `flush bandwidth for client ${publicKey}`); + } catch (error) { + logger.error( + `Failed to flush bandwidth for client ${publicKey}:`, + error + ); + + // Re-queue the failed entry so it is retried on the next flush + // rather than silently dropped. + const existing = accumulator.get(publicKey); + if (existing) { + existing.bytesIn += bytesIn; + existing.bytesOut += bytesOut; + } else { + accumulator.set(publicKey, { bytesIn, bytesOut }); + } + } + } +} + +const flushTimer = setInterval(async () => { + try { + await flushBandwidthToDb(); + } catch (error) { + logger.error("Unexpected error during periodic bandwidth flush:", error); + } +}, FLUSH_INTERVAL_MS); + +// Calling unref() means this timer will not keep the Node.js event loop alive +// on its own — the process can still exit normally when there is no other work +// left. The graceful-shutdown path (see server/cleanup.ts) will call +// flushBandwidthToDb() explicitly before process.exit(), so no data is lost. +flushTimer.unref(); + export const handleReceiveBandwidthMessage: MessageHandler = async ( context ) => { @@ -69,40 +164,21 @@ export const handleReceiveBandwidthMessage: MessageHandler = async ( throw new Error("Invalid bandwidth data"); } - // Sort bandwidth data by publicKey to ensure consistent lock ordering across all instances - // This is critical for preventing deadlocks when multiple instances update the same clients - const sortedBandwidthData = [...bandwidthData].sort((a, b) => - a.publicKey.localeCompare(b.publicKey) - ); + // Accumulate the incoming data in memory; the periodic timer (and the + // shutdown hook) will take care of writing it to the database. + for (const { publicKey, bytesIn, bytesOut } of bandwidthData) { + // Skip peers that haven't transferred any data — writing zeros to the + // database would be a no-op anyway. + if (bytesIn <= 0 && bytesOut <= 0) { + continue; + } - const currentTime = new Date().toISOString(); - - // Update each client individually with retry logic - // This reduces transaction scope and allows retries per-client - for (const peer of sortedBandwidthData) { - const { publicKey, bytesIn, bytesOut } = peer; - - try { - await withDeadlockRetry(async () => { - // Use atomic SQL increment to avoid SELECT then UPDATE pattern - // This eliminates the need to read the current value first - await db - .update(clients) - .set({ - // Note: bytesIn from peer goes to megabytesOut (data sent to client) - // and bytesOut from peer goes to megabytesIn (data received from client) - megabytesOut: sql`COALESCE(${clients.megabytesOut}, 0) + ${bytesIn}`, - megabytesIn: sql`COALESCE(${clients.megabytesIn}, 0) + ${bytesOut}`, - lastBandwidthUpdate: currentTime - }) - .where(eq(clients.pubKey, publicKey)); - }, `update client bandwidth ${publicKey}`); - } catch (error) { - logger.error( - `Failed to update bandwidth for client ${publicKey}:`, - error - ); - // Continue with other clients even if one fails + const existing = accumulator.get(publicKey); + if (existing) { + existing.bytesIn += bytesIn; + existing.bytesOut += bytesOut; + } else { + accumulator.set(publicKey, { bytesIn, bytesOut }); } } }; diff --git a/server/routers/newt/handleSocketMessages.ts b/server/routers/newt/handleSocketMessages.ts index f26f69c97..383ab5541 100644 --- a/server/routers/newt/handleSocketMessages.ts +++ b/server/routers/newt/handleSocketMessages.ts @@ -2,7 +2,7 @@ import { MessageHandler } from "@server/routers/ws"; import logger from "@server/logger"; import { Newt } from "@server/db"; import { applyNewtDockerBlueprint } from "@server/lib/blueprints/applyNewtDockerBlueprint"; -import cache from "@server/lib/cache"; +import cache from "#dynamic/lib/cache"; export const handleDockerStatusMessage: MessageHandler = async (context) => { const { message, client, sendToClient } = context; @@ -24,8 +24,8 @@ export const handleDockerStatusMessage: MessageHandler = async (context) => { if (available) { logger.info(`Newt ${newt.newtId} has Docker socket access`); - cache.set(`${newt.newtId}:socketPath`, socketPath, 0); - cache.set(`${newt.newtId}:isAvailable`, available, 0); + await cache.set(`${newt.newtId}:socketPath`, socketPath, 0); + await cache.set(`${newt.newtId}:isAvailable`, available, 0); } else { logger.warn(`Newt ${newt.newtId} does not have Docker socket access`); } @@ -54,7 +54,7 @@ export const handleDockerContainersMessage: MessageHandler = async ( ); if (containers && containers.length > 0) { - cache.set(`${newt.newtId}:dockerContainers`, containers, 0); + await cache.set(`${newt.newtId}:dockerContainers`, containers, 0); } else { logger.warn(`Newt ${newt.newtId} does not have Docker containers`); } diff --git a/server/routers/newt/index.ts b/server/routers/newt/index.ts index 8ff1b61ae..f31cd753b 100644 --- a/server/routers/newt/index.ts +++ b/server/routers/newt/index.ts @@ -7,3 +7,4 @@ export * from "./handleSocketMessages"; export * from "./handleNewtPingRequestMessage"; export * from "./handleApplyBlueprintMessage"; export * from "./handleNewtPingMessage"; +export * from "./handleNewtDisconnectingMessage"; diff --git a/server/routers/newt/sync.ts b/server/routers/newt/sync.ts index e6f465e55..6fce13ff3 100644 --- a/server/routers/newt/sync.ts +++ b/server/routers/newt/sync.ts @@ -6,6 +6,7 @@ import { buildClientConfigurationForNewtClient, buildTargetConfigurationForNewtClient } from "./buildConfiguration"; +import { canCompress } from "@server/lib/clientVersionChecks"; export async function sendNewtSyncMessage(newt: Newt, site: Site) { const { tcpTargets, udpTargets, validHealthCheckTargets } = @@ -24,18 +25,24 @@ export async function sendNewtSyncMessage(newt: Newt, site: Site) { exitNode ); - await sendToClient(newt.newtId, { - type: "newt/sync", - data: { - proxyTargets: { - udp: udpTargets, - tcp: tcpTargets - }, - healthCheckTargets: validHealthCheckTargets, - peers: peers, - clientTargets: targets + await sendToClient( + newt.newtId, + { + type: "newt/sync", + data: { + proxyTargets: { + udp: udpTargets, + tcp: tcpTargets + }, + healthCheckTargets: validHealthCheckTargets, + peers: peers, + clientTargets: targets + } + }, + { + compress: canCompress(newt.version, "newt") } - }).catch((error) => { + ).catch((error) => { logger.warn(`Error sending newt sync message:`, error); }); } diff --git a/server/routers/newt/targets.ts b/server/routers/newt/targets.ts index 6318861e4..6a523ebe9 100644 --- a/server/routers/newt/targets.ts +++ b/server/routers/newt/targets.ts @@ -2,13 +2,14 @@ import { Target, TargetHealthCheck, db, targetHealthCheck } from "@server/db"; import { sendToClient } from "#dynamic/routers/ws"; import logger from "@server/logger"; import { eq, inArray } from "drizzle-orm"; +import { canCompress } from "@server/lib/clientVersionChecks"; export async function addTargets( newtId: string, targets: Target[], healthCheckData: TargetHealthCheck[], protocol: string, - port: number | null = null + version?: string | null ) { //create a list of udp and tcp targets const payloadTargets = targets.map((target) => { @@ -22,7 +23,7 @@ export async function addTargets( data: { targets: payloadTargets } - }, { incrementConfigVersion: true }); + }, { incrementConfigVersion: true, compress: canCompress(version, "newt") }); // Create a map for quick lookup const healthCheckMap = new Map(); @@ -103,14 +104,14 @@ export async function addTargets( data: { targets: validHealthCheckTargets } - }, { incrementConfigVersion: true }); + }, { incrementConfigVersion: true, compress: canCompress(version, "newt") }); } export async function removeTargets( newtId: string, targets: Target[], protocol: string, - port: number | null = null + version?: string | null ) { //create a list of udp and tcp targets const payloadTargets = targets.map((target) => { @@ -135,5 +136,5 @@ export async function removeTargets( data: { ids: healthCheckTargets } - }, { incrementConfigVersion: true }); + }, { incrementConfigVersion: true, compress: canCompress(version, "newt") }); } diff --git a/server/routers/olm/buildConfiguration.ts b/server/routers/olm/buildConfiguration.ts index b506366bf..bc2611b1c 100644 --- a/server/routers/olm/buildConfiguration.ts +++ b/server/routers/olm/buildConfiguration.ts @@ -1,5 +1,17 @@ -import { Client, clientSiteResourcesAssociationsCache, clientSitesAssociationsCache, db, exitNodes, siteResources, sites } from "@server/db"; -import { generateAliasConfig, generateRemoteSubnets } from "@server/lib/ip"; +import { + Client, + clientSiteResourcesAssociationsCache, + clientSitesAssociationsCache, + db, + exitNodes, + siteResources, + sites +} from "@server/db"; +import { + Alias, + generateAliasConfig, + generateRemoteSubnets +} from "@server/lib/ip"; import logger from "@server/logger"; import { and, eq } from "drizzle-orm"; import { addPeer, deletePeer } from "../newt/peers"; @@ -8,9 +20,19 @@ import config from "@server/lib/config"; export async function buildSiteConfigurationForOlmClient( client: Client, publicKey: string | null, - relay: boolean + relay: boolean, + jitMode: boolean = false ) { - const siteConfigurations = []; + const siteConfigurations: { + siteId: number; + name?: string + endpoint?: string + publicKey?: string + serverIP?: string | null + serverPort?: number | null + remoteSubnets?: string[]; + aliases: Alias[]; + }[] = []; // Get all sites data const sitesData = await db @@ -27,6 +49,40 @@ export async function buildSiteConfigurationForOlmClient( sites: site, clientSitesAssociationsCache: association } of sitesData) { + const allSiteResources = await db // only get the site resources that this client has access to + .select() + .from(siteResources) + .innerJoin( + clientSiteResourcesAssociationsCache, + eq( + siteResources.siteResourceId, + clientSiteResourcesAssociationsCache.siteResourceId + ) + ) + .where( + and( + eq(siteResources.siteId, site.siteId), + eq( + clientSiteResourcesAssociationsCache.clientId, + client.clientId + ) + ) + ); + + if (jitMode) { + // Add site configuration to the array + siteConfigurations.push({ + siteId: site.siteId, + // remoteSubnets: generateRemoteSubnets( + // allSiteResources.map(({ siteResources }) => siteResources) + // ), + aliases: generateAliasConfig( + allSiteResources.map(({ siteResources }) => siteResources) + ) + }); + continue; + } + if (!site.exitNodeId) { logger.warn( `Site ${site.siteId} does not have exit node, skipping` @@ -42,6 +98,13 @@ export async function buildSiteConfigurationForOlmClient( continue; } + if (!site.publicKey || site.publicKey == "") { // the site is not ready to accept new peers + logger.warn( + `Site ${site.siteId} has no public key, skipping` + ); + continue; + } + // if (site.lastHolePunch && now - site.lastHolePunch > 6 && relay) { // logger.warn( // `Site ${site.siteId} last hole punch is too old, skipping` @@ -103,26 +166,6 @@ export async function buildSiteConfigurationForOlmClient( relayEndpoint = `${exitNode.endpoint}:${config.getRawConfig().gerbil.clients_start_port}`; } - const allSiteResources = await db // only get the site resources that this client has access to - .select() - .from(siteResources) - .innerJoin( - clientSiteResourcesAssociationsCache, - eq( - siteResources.siteResourceId, - clientSiteResourcesAssociationsCache.siteResourceId - ) - ) - .where( - and( - eq(siteResources.siteId, site.siteId), - eq( - clientSiteResourcesAssociationsCache.clientId, - client.clientId - ) - ) - ); - // Add site configuration to the array siteConfigurations.push({ siteId: site.siteId, diff --git a/server/routers/olm/getOlmToken.ts b/server/routers/olm/getOlmToken.ts index c8ede5186..2734a63bc 100644 --- a/server/routers/olm/getOlmToken.ts +++ b/server/routers/olm/getOlmToken.ts @@ -1,4 +1,7 @@ -import { generateSessionToken } from "@server/auth/sessions/app"; +import { + generateSessionToken, + validateSessionToken +} from "@server/auth/sessions/app"; import { clients, db, @@ -26,8 +29,9 @@ import { APP_VERSION } from "@server/lib/consts"; export const olmGetTokenBodySchema = z.object({ olmId: z.string(), - secret: z.string(), - token: z.string().optional(), + secret: z.string().optional(), + userToken: z.string().optional(), + token: z.string().optional(), // this is the olm token orgId: z.string().optional() }); @@ -49,7 +53,7 @@ export async function getOlmToken( ); } - const { olmId, secret, token, orgId } = parsedBody.data; + const { olmId, secret, token, orgId, userToken } = parsedBody.data; try { if (token) { @@ -84,19 +88,45 @@ export async function getOlmToken( ); } - const validSecret = await verifyPassword( - secret, - existingOlm.secretHash - ); - - if (!validSecret) { - if (config.getRawConfig().app.log_failed_attempts) { - logger.info( - `Olm id or secret is incorrect. Olm: ID ${olmId}. IP: ${req.ip}.` + if (userToken) { + const { session: userSession, user } = + await validateSessionToken(userToken); + if (!userSession || !user) { + return next( + createHttpError(HttpCode.BAD_REQUEST, "Invalid user token") ); } + if (user.userId !== existingOlm.userId) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "User token does not match olm" + ) + ); + } + } else if (secret) { + // this is for backward compatibility, we want to move towards userToken but some old clients may still be using secret so we will support both for now + const validSecret = await verifyPassword( + secret, + existingOlm.secretHash + ); + + if (!validSecret) { + if (config.getRawConfig().app.log_failed_attempts) { + logger.info( + `Olm id or secret is incorrect. Olm: ID ${olmId}. IP: ${req.ip}.` + ); + } + return next( + createHttpError(HttpCode.BAD_REQUEST, "Secret is incorrect") + ); + } + } else { return next( - createHttpError(HttpCode.BAD_REQUEST, "Secret is incorrect") + createHttpError( + HttpCode.BAD_REQUEST, + "Either secret or userToken is required" + ) ); } diff --git a/server/routers/olm/handleOlmDisconnectingMessage.ts b/server/routers/olm/handleOlmDisconnectingMessage.ts index 2ddd5e515..ecd101724 100644 --- a/server/routers/olm/handleOlmDisconnectingMessage.ts +++ b/server/routers/olm/handleOlmDisconnectingMessage.ts @@ -6,7 +6,7 @@ import logger from "@server/logger"; /** * Handles disconnecting messages from clients to show disconnected in the ui */ -export const handleOlmDisconnecingMessage: MessageHandler = async (context) => { +export const handleOlmDisconnectingMessage: MessageHandler = async (context) => { const { message, client: c, sendToClient } = context; const olm = c as Olm; diff --git a/server/routers/olm/handleOlmRegisterMessage.ts b/server/routers/olm/handleOlmRegisterMessage.ts index 7fa43c9cb..5439245c4 100644 --- a/server/routers/olm/handleOlmRegisterMessage.ts +++ b/server/routers/olm/handleOlmRegisterMessage.ts @@ -17,6 +17,9 @@ import { getUserDeviceName } from "@server/db/names"; import { buildSiteConfigurationForOlmClient } from "./buildConfiguration"; import { OlmErrorCodes, sendOlmError } from "./error"; import { handleFingerprintInsertion } from "./fingerprintingUtils"; +import { Alias } from "@server/lib/ip"; +import { build } from "@server/build"; +import { canCompress } from "@server/lib/clientVersionChecks"; export const handleOlmRegisterMessage: MessageHandler = async (context) => { logger.info("Handling register olm message!"); @@ -207,6 +210,32 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { } } + // Get all sites data + const sitesCountResult = await db + .select({ count: count() }) + .from(sites) + .innerJoin( + clientSitesAssociationsCache, + eq(sites.siteId, clientSitesAssociationsCache.siteId) + ) + .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); + + // Extract the count value from the result array + const sitesCount = + sitesCountResult.length > 0 ? sitesCountResult[0].count : 0; + + // Prepare an array to store site configurations + logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`); + + let jitMode = false; + if (sitesCount > 250 && build == "saas") { + // THIS IS THE MAX ON THE BUSINESS TIER + // we have too many sites + // If we have too many sites we need to drop into fully JIT mode by not sending any of the sites + logger.info("Too many sites (%d), dropping into JIT mode", sitesCount); + jitMode = true; + } + logger.debug( `Olm client ID: ${client.clientId}, Public Key: ${publicKey}, Relay: ${relay}` ); @@ -233,28 +262,12 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { await db .update(clientSitesAssociationsCache) .set({ - isRelayed: relay == true + isRelayed: relay == true, + isJitMode: jitMode }) .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); } - // Get all sites data - const sitesCountResult = await db - .select({ count: count() }) - .from(sites) - .innerJoin( - clientSitesAssociationsCache, - eq(sites.siteId, clientSitesAssociationsCache.siteId) - ) - .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); - - // Extract the count value from the result array - const sitesCount = - sitesCountResult.length > 0 ? sitesCountResult[0].count : 0; - - // Prepare an array to store site configurations - logger.debug(`Found ${sitesCount} sites for client ${client.clientId}`); - // this prevents us from accepting a register from an olm that has not hole punched yet. // the olm will pump the register so we can keep checking // TODO: I still think there is a better way to do this rather than locking it out here but ??? @@ -265,19 +278,14 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { return; } - // NOTE: its important that the client here is the old client and the public key is the new key + // NOTE: its important that the client here is the old client and the public key is the new key const siteConfigurations = await buildSiteConfigurationForOlmClient( client, publicKey, - relay + relay, + jitMode ); - // REMOVED THIS SO IT CREATES THE INTERFACE AND JUST WAITS FOR THE SITES - // if (siteConfigurations.length === 0) { - // logger.warn("No valid site configurations found"); - // return; - // } - // Return connect message with all site configurations return { message: { @@ -288,6 +296,9 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => { utilitySubnet: org.utilitySubnet } }, + options: { + compress: canCompress(olm.version, "olm") + }, broadcast: false, excludeSender: false }; diff --git a/server/routers/olm/handleOlmRelayMessage.ts b/server/routers/olm/handleOlmRelayMessage.ts index 88886cd15..7196824d2 100644 --- a/server/routers/olm/handleOlmRelayMessage.ts +++ b/server/routers/olm/handleOlmRelayMessage.ts @@ -18,7 +18,7 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => { } if (!olm.clientId) { - logger.warn("Olm has no site!"); // TODO: Maybe we create the site here? + logger.warn("Olm has no client!"); return; } @@ -41,7 +41,7 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => { return; } - const { siteId } = message.data; + const { siteId, chainId } = message.data; // Get the site const [site] = await db @@ -90,7 +90,8 @@ export const handleOlmRelayMessage: MessageHandler = async (context) => { data: { siteId: siteId, relayEndpoint: exitNode.endpoint, - relayPort: config.getRawConfig().gerbil.clients_start_port + relayPort: config.getRawConfig().gerbil.clients_start_port, + chainId } }, broadcast: false, diff --git a/server/routers/olm/handleOlmServerInitAddPeerHandshake.ts b/server/routers/olm/handleOlmServerInitAddPeerHandshake.ts new file mode 100644 index 000000000..54badb2dc --- /dev/null +++ b/server/routers/olm/handleOlmServerInitAddPeerHandshake.ts @@ -0,0 +1,241 @@ +import { + clientSiteResourcesAssociationsCache, + clientSitesAssociationsCache, + db, + exitNodes, + Site, + siteResources +} from "@server/db"; +import { MessageHandler } from "@server/routers/ws"; +import { clients, Olm, sites } from "@server/db"; +import { and, eq, or } from "drizzle-orm"; +import logger from "@server/logger"; +import { initPeerAddHandshake } from "./peers"; + +export const handleOlmServerInitAddPeerHandshake: MessageHandler = async ( + context +) => { + logger.info("Handling register olm message!"); + const { message, client: c, sendToClient } = context; + const olm = c as Olm; + + if (!olm) { + logger.warn("Olm not found"); + return; + } + + if (!olm.clientId) { + logger.warn("Olm has no client!"); // TODO: Maybe we create the site here? + return; + } + + const clientId = olm.clientId; + + const [client] = await db + .select() + .from(clients) + .where(eq(clients.clientId, clientId)) + .limit(1); + + if (!client) { + logger.warn("Client not found"); + return; + } + + const { siteId, resourceId, chainId } = message.data; + + let site: Site | null = null; + if (siteId) { + // get the site + const [siteRes] = await db + .select() + .from(sites) + .where(eq(sites.siteId, siteId)) + .limit(1); + if (siteRes) { + site = siteRes; + } + } + + if (resourceId && !site) { + const resources = await db + .select() + .from(siteResources) + .where( + and( + or( + eq(siteResources.niceId, resourceId), + eq(siteResources.alias, resourceId) + ), + eq(siteResources.orgId, client.orgId) + ) + ); + + if (!resources || resources.length === 0) { + logger.error(`handleOlmServerPeerAddMessage: Resource not found`); + // cancel the request from the olm side to not keep doing this + await sendToClient( + olm.olmId, + { + type: "olm/wg/peer/chain/cancel", + data: { + chainId + } + }, + { incrementConfigVersion: false } + ).catch((error) => { + logger.warn(`Error sending message:`, error); + }); + return; + } + + if (resources.length > 1) { + // error but this should not happen because the nice id cant contain a dot and the alias has to have a dot and both have to be unique within the org so there should never be multiple matches + logger.error( + `handleOlmServerPeerAddMessage: Multiple resources found matching the criteria` + ); + return; + } + + const resource = resources[0]; + + const currentResourceAssociationCaches = await db + .select() + .from(clientSiteResourcesAssociationsCache) + .where( + and( + eq( + clientSiteResourcesAssociationsCache.siteResourceId, + resource.siteResourceId + ), + eq( + clientSiteResourcesAssociationsCache.clientId, + client.clientId + ) + ) + ); + + if (currentResourceAssociationCaches.length === 0) { + logger.error( + `handleOlmServerPeerAddMessage: Client ${client.clientId} does not have access to resource ${resource.siteResourceId}` + ); + // cancel the request from the olm side to not keep doing this + await sendToClient( + olm.olmId, + { + type: "olm/wg/peer/chain/cancel", + data: { + chainId + } + }, + { incrementConfigVersion: false } + ).catch((error) => { + logger.warn(`Error sending message:`, error); + }); + return; + } + + const siteIdFromResource = resource.siteId; + + // get the site + const [siteRes] = await db + .select() + .from(sites) + .where(eq(sites.siteId, siteIdFromResource)); + if (!siteRes) { + logger.error( + `handleOlmServerPeerAddMessage: Site with ID ${site} not found` + ); + return; + } + + site = siteRes; + } + + if (!site) { + logger.error(`handleOlmServerPeerAddMessage: Site not found`); + return; + } + + // check if the client can access this site using the cache + const currentSiteAssociationCaches = await db + .select() + .from(clientSitesAssociationsCache) + .where( + and( + eq(clientSitesAssociationsCache.clientId, client.clientId), + eq(clientSitesAssociationsCache.siteId, site.siteId) + ) + ); + + if (currentSiteAssociationCaches.length === 0) { + logger.error( + `handleOlmServerPeerAddMessage: Client ${client.clientId} does not have access to site ${site.siteId}` + ); + // cancel the request from the olm side to not keep doing this + await sendToClient( + olm.olmId, + { + type: "olm/wg/peer/chain/cancel", + data: { + chainId + } + }, + { incrementConfigVersion: false } + ).catch((error) => { + logger.warn(`Error sending message:`, error); + }); + return; + } + + if (!site.exitNodeId) { + logger.error( + `handleOlmServerPeerAddMessage: Site with ID ${site.siteId} has no exit node` + ); + // cancel the request from the olm side to not keep doing this + await sendToClient( + olm.olmId, + { + type: "olm/wg/peer/chain/cancel", + data: { + chainId + } + }, + { incrementConfigVersion: false } + ).catch((error) => { + logger.warn(`Error sending message:`, error); + }); + return; + } + + // get the exit node from the side + const [exitNode] = await db + .select() + .from(exitNodes) + .where(eq(exitNodes.exitNodeId, site.exitNodeId)); + + if (!exitNode) { + logger.error( + `handleOlmServerPeerAddMessage: Site with ID ${site.siteId} has no exit node` + ); + return; + } + + // also trigger the peer add handshake in case the peer was not already added to the olm and we need to hole punch + // if it has already been added this will be a no-op + await initPeerAddHandshake( + // this will kick off the add peer process for the client + client.clientId, + { + siteId: site.siteId, + exitNode: { + publicKey: exitNode.publicKey, + endpoint: exitNode.endpoint + } + }, + olm.olmId, + chainId + ); + + return; +}; diff --git a/server/routers/olm/handleOlmServerPeerAddMessage.ts b/server/routers/olm/handleOlmServerPeerAddMessage.ts index 53f3474ce..64284f493 100644 --- a/server/routers/olm/handleOlmServerPeerAddMessage.ts +++ b/server/routers/olm/handleOlmServerPeerAddMessage.ts @@ -54,7 +54,7 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async ( return; } - const { siteId } = message.data; + const { siteId, chainId } = message.data; // get the site const [site] = await db @@ -179,7 +179,8 @@ export const handleOlmServerPeerAddMessage: MessageHandler = async ( ), aliases: generateAliasConfig( allSiteResources.map(({ siteResources }) => siteResources) - ) + ), + chainId: chainId, } }, broadcast: false, diff --git a/server/routers/olm/handleOlmUnRelayMessage.ts b/server/routers/olm/handleOlmUnRelayMessage.ts index 5f47a095e..a7b426023 100644 --- a/server/routers/olm/handleOlmUnRelayMessage.ts +++ b/server/routers/olm/handleOlmUnRelayMessage.ts @@ -17,7 +17,7 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => { } if (!olm.clientId) { - logger.warn("Olm has no site!"); // TODO: Maybe we create the site here? + logger.warn("Olm has no client!"); return; } @@ -40,7 +40,7 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => { return; } - const { siteId } = message.data; + const { siteId, chainId } = message.data; // Get the site const [site] = await db @@ -87,7 +87,8 @@ export const handleOlmUnRelayMessage: MessageHandler = async (context) => { type: "olm/wg/peer/unrelay", data: { siteId: siteId, - endpoint: site.endpoint + endpoint: site.endpoint, + chainId } }, broadcast: false, diff --git a/server/routers/olm/index.ts b/server/routers/olm/index.ts index f04ba0bee..322428572 100644 --- a/server/routers/olm/index.ts +++ b/server/routers/olm/index.ts @@ -11,3 +11,4 @@ export * from "./handleOlmServerPeerAddMessage"; export * from "./handleOlmUnRelayMessage"; export * from "./recoverOlmWithFingerprint"; export * from "./handleOlmDisconnectingMessage"; +export * from "./handleOlmServerInitAddPeerHandshake"; diff --git a/server/routers/olm/peers.ts b/server/routers/olm/peers.ts index 4ffeff736..05e153fea 100644 --- a/server/routers/olm/peers.ts +++ b/server/routers/olm/peers.ts @@ -1,8 +1,9 @@ import { sendToClient } from "#dynamic/routers/ws"; -import { db, olms } from "@server/db"; +import { clientSitesAssociationsCache, db, olms } from "@server/db"; +import { canCompress } from "@server/lib/clientVersionChecks"; import config from "@server/lib/config"; import logger from "@server/logger"; -import { eq } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import { Alias } from "yaml"; export async function addPeer( @@ -18,7 +19,8 @@ export async function addPeer( remoteSubnets: string[] | null; // optional, comma-separated list of subnets that this site can access aliases: Alias[]; }, - olmId?: string + olmId?: string, + version?: string | null ) { if (!olmId) { const [olm] = await db @@ -30,6 +32,7 @@ export async function addPeer( return; // ignore this because an olm might not be associated with the client anymore } olmId = olm.olmId; + version = olm.version; } await sendToClient( @@ -48,7 +51,7 @@ export async function addPeer( aliases: peer.aliases } }, - { incrementConfigVersion: true } + { incrementConfigVersion: true, compress: canCompress(version, "olm") } ).catch((error) => { logger.warn(`Error sending message:`, error); }); @@ -60,7 +63,8 @@ export async function deletePeer( clientId: number, siteId: number, publicKey: string, - olmId?: string + olmId?: string, + version?: string | null ) { if (!olmId) { const [olm] = await db @@ -72,6 +76,7 @@ export async function deletePeer( return; } olmId = olm.olmId; + version = olm.version; } await sendToClient( @@ -83,7 +88,7 @@ export async function deletePeer( siteId: siteId } }, - { incrementConfigVersion: true } + { incrementConfigVersion: true, compress: canCompress(version, "olm") } ).catch((error) => { logger.warn(`Error sending message:`, error); }); @@ -103,7 +108,8 @@ export async function updatePeer( remoteSubnets?: string[] | null; // optional, comma-separated list of subnets that aliases?: Alias[] | null; }, - olmId?: string + olmId?: string, + version?: string | null ) { if (!olmId) { const [olm] = await db @@ -115,6 +121,7 @@ export async function updatePeer( return; } olmId = olm.olmId; + version = olm.version; } await sendToClient( @@ -132,7 +139,7 @@ export async function updatePeer( aliases: peer.aliases } }, - { incrementConfigVersion: true } + { incrementConfigVersion: true, compress: canCompress(version, "olm") } ).catch((error) => { logger.warn(`Error sending message:`, error); }); @@ -149,7 +156,8 @@ export async function initPeerAddHandshake( endpoint: string; }; }, - olmId?: string + olmId?: string, + chainId?: string ) { if (!olmId) { const [olm] = await db @@ -173,7 +181,8 @@ export async function initPeerAddHandshake( publicKey: peer.exitNode.publicKey, relayPort: config.getRawConfig().gerbil.clients_start_port, endpoint: peer.exitNode.endpoint - } + }, + chainId } }, { incrementConfigVersion: true } @@ -181,6 +190,17 @@ export async function initPeerAddHandshake( logger.warn(`Error sending message:`, error); }); + // update the clientSiteAssociationsCache to make the isJitMode flag false so that JIT mode is disabled for this site if it restarts or something after the connection + await db + .update(clientSitesAssociationsCache) + .set({ isJitMode: false }) + .where( + and( + eq(clientSitesAssociationsCache.clientId, clientId), + eq(clientSitesAssociationsCache.siteId, peer.siteId) + ) + ); + logger.info( `Initiated peer add handshake for site ${peer.siteId} to olm ${olmId}` ); diff --git a/server/routers/olm/sync.ts b/server/routers/olm/sync.ts index d4ecd22c1..c994b2c73 100644 --- a/server/routers/olm/sync.ts +++ b/server/routers/olm/sync.ts @@ -1,9 +1,17 @@ -import { Client, db, exitNodes, Olm, sites, clientSitesAssociationsCache } from "@server/db"; +import { + Client, + db, + exitNodes, + Olm, + sites, + clientSitesAssociationsCache +} from "@server/db"; import { buildSiteConfigurationForOlmClient } from "./buildConfiguration"; import { sendToClient } from "#dynamic/routers/ws"; import logger from "@server/logger"; import { eq, inArray } from "drizzle-orm"; import config from "@server/lib/config"; +import { canCompress } from "@server/lib/clientVersionChecks"; export async function sendOlmSyncMessage(olm: Olm, client: Client) { // NOTE: WE ARE HARDCODING THE RELAY PARAMETER TO FALSE HERE BUT IN THE REGISTER MESSAGE ITS DEFINED BY THE CLIENT @@ -17,10 +25,7 @@ export async function sendOlmSyncMessage(olm: Olm, client: Client) { const clientSites = await db .select() .from(clientSitesAssociationsCache) - .innerJoin( - sites, - eq(sites.siteId, clientSitesAssociationsCache.siteId) - ) + .innerJoin(sites, eq(sites.siteId, clientSitesAssociationsCache.siteId)) .where(eq(clientSitesAssociationsCache.clientId, client.clientId)); // Extract unique exit node IDs @@ -68,13 +73,20 @@ export async function sendOlmSyncMessage(olm: Olm, client: Client) { logger.debug("sendOlmSyncMessage: sending sync message"); - await sendToClient(olm.olmId, { - type: "olm/sync", - data: { - sites: siteConfigurations, - exitNodes: exitNodesData + await sendToClient( + olm.olmId, + { + type: "olm/sync", + data: { + sites: siteConfigurations, + exitNodes: exitNodesData + } + }, + + { + compress: canCompress(olm.version, "olm") } - }).catch((error) => { + ).catch((error) => { logger.warn(`Error sending olm sync message:`, error); }); } diff --git a/server/routers/org/createOrg.ts b/server/routers/org/createOrg.ts index 22dc742fa..88f76c29c 100644 --- a/server/routers/org/createOrg.ts +++ b/server/routers/org/createOrg.ts @@ -29,7 +29,7 @@ import { FeatureId, limitsService, freeLimitSet } from "@server/lib/billing"; import { build } from "@server/build"; import { calculateUserClientsForOrgs } from "@server/lib/calculateUserClientsForOrgs"; import { doCidrsOverlap } from "@server/lib/ip"; -import { generateCA } from "@server/private/lib/sshCA"; +import { generateCA } from "@server/lib/sshCA"; import { encrypt } from "@server/lib/crypto"; const validOrgIdRegex = /^[a-z0-9_]+(-[a-z0-9_]+)*$/; diff --git a/server/routers/org/index.ts b/server/routers/org/index.ts index b0db28d14..c1aee7b33 100644 --- a/server/routers/org/index.ts +++ b/server/routers/org/index.ts @@ -8,3 +8,4 @@ export * from "./getOrgOverview"; export * from "./listOrgs"; export * from "./pickOrgDefaults"; export * from "./checkOrgUserAccess"; +export * from "./resetOrgBandwidth"; diff --git a/server/routers/org/resetOrgBandwidth.ts b/server/routers/org/resetOrgBandwidth.ts new file mode 100644 index 000000000..b98e2e406 --- /dev/null +++ b/server/routers/org/resetOrgBandwidth.ts @@ -0,0 +1,83 @@ +import { NextFunction, Request, Response } from "express"; +import { z } from "zod"; +import { db, sites } from "@server/db"; +import { eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; + +const resetOrgBandwidthParamsSchema = z.strictObject({ + orgId: z.string() +}); + +registry.registerPath({ + method: "post", + path: "/org/{orgId}/reset-bandwidth", + description: "Reset all sites in selected organization bandwidth counters.", + tags: [OpenAPITags.Org, OpenAPITags.Site], + request: { + params: resetOrgBandwidthParamsSchema + }, + responses: {} +}); + +export async function resetOrgBandwidth( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = resetOrgBandwidthParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + + const [site] = await db + .select({ siteId: sites.siteId }) + .from(sites) + .where(eq(sites.orgId, orgId)) + .limit(1); + + if (!site) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `No sites found in org ${orgId}` + ) + ); + } + + await db + .update(sites) + .set({ + megabytesIn: 0, + megabytesOut: 0 + }) + .where(eq(sites.orgId, orgId)); + + return response(res, { + data: {}, + success: true, + error: false, + message: "Sites bandwidth reset successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/org/updateOrg.ts b/server/routers/org/updateOrg.ts index e94be3a92..5049ac1fa 100644 --- a/server/routers/org/updateOrg.ts +++ b/server/routers/org/updateOrg.ts @@ -10,7 +10,7 @@ import logger from "@server/logger"; import { fromError } from "zod-validation-error"; import { OpenAPITags, registry } from "@server/openApi"; import { build } from "@server/build"; -import { cache } from "@server/lib/cache"; +import { cache } from "#dynamic/lib/cache"; import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed"; import { TierFeature, tierMatrix } from "@server/lib/billing/tierMatrix"; import { getOrgTierData } from "#dynamic/lib/billing"; @@ -194,9 +194,9 @@ export async function updateOrg( } // invalidate the cache for all of the orgs retention days - cache.del(`org_${orgId}_retentionDays`); - cache.del(`org_${orgId}_actionDays`); - cache.del(`org_${orgId}_accessDays`); + await cache.del(`org_${orgId}_retentionDays`); + await cache.del(`org_${orgId}_actionDays`); + await cache.del(`org_${orgId}_accessDays`); return response(res, { data: updatedOrg[0], diff --git a/server/routers/resource/addEmailToResourceWhitelist.ts b/server/routers/resource/addEmailToResourceWhitelist.ts index 53828b44c..27ba34699 100644 --- a/server/routers/resource/addEmailToResourceWhitelist.ts +++ b/server/routers/resource/addEmailToResourceWhitelist.ts @@ -29,7 +29,7 @@ registry.registerPath({ method: "post", path: "/resource/{resourceId}/whitelist/add", description: "Add a single email to the resource whitelist.", - tags: [OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { params: addEmailToResourceWhitelistParamsSchema, body: { diff --git a/server/routers/resource/addRoleToResource.ts b/server/routers/resource/addRoleToResource.ts index ba344c6c0..7a5c8fb63 100644 --- a/server/routers/resource/addRoleToResource.ts +++ b/server/routers/resource/addRoleToResource.ts @@ -29,7 +29,7 @@ registry.registerPath({ method: "post", path: "/resource/{resourceId}/roles/add", description: "Add a single role to a resource.", - tags: [OpenAPITags.Resource, OpenAPITags.Role], + tags: [OpenAPITags.PublicResource, OpenAPITags.Role], request: { params: addRoleToResourceParamsSchema, body: { diff --git a/server/routers/resource/addUserToResource.ts b/server/routers/resource/addUserToResource.ts index ee6081ff8..9880d9c27 100644 --- a/server/routers/resource/addUserToResource.ts +++ b/server/routers/resource/addUserToResource.ts @@ -29,7 +29,7 @@ registry.registerPath({ method: "post", path: "/resource/{resourceId}/users/add", description: "Add a single user to a resource.", - tags: [OpenAPITags.Resource, OpenAPITags.User], + tags: [OpenAPITags.PublicResource, OpenAPITags.User], request: { params: addUserToResourceParamsSchema, body: { diff --git a/server/routers/resource/createResource.ts b/server/routers/resource/createResource.ts index d2124d22e..6cff4d23a 100644 --- a/server/routers/resource/createResource.ts +++ b/server/routers/resource/createResource.ts @@ -79,7 +79,7 @@ registry.registerPath({ method: "put", path: "/org/{orgId}/resource", description: "Create a resource.", - tags: [OpenAPITags.Org, OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { params: createResourceParamsSchema, body: { @@ -223,6 +223,20 @@ async function createHttpResource( ); } + // Prevent creating resource with same domain as dashboard + const dashboardUrl = config.getRawConfig().app.dashboard_url; + if (dashboardUrl) { + const dashboardHost = new URL(dashboardUrl).hostname; + if (fullDomain === dashboardHost) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Resource domain cannot be the same as the dashboard domain" + ) + ); + } + } + if (build != "oss") { const existingLoginPages = await db .select() diff --git a/server/routers/resource/createResourceRule.ts b/server/routers/resource/createResourceRule.ts index a516d14af..b2ce2ee7c 100644 --- a/server/routers/resource/createResourceRule.ts +++ b/server/routers/resource/createResourceRule.ts @@ -31,7 +31,7 @@ registry.registerPath({ method: "put", path: "/resource/{resourceId}/rule", description: "Create a resource rule.", - tags: [OpenAPITags.Resource, OpenAPITags.Rule], + tags: [OpenAPITags.PublicResource, OpenAPITags.Rule], request: { params: createResourceRuleParamsSchema, body: { diff --git a/server/routers/resource/deleteResource.ts b/server/routers/resource/deleteResource.ts index d8891d75d..e63301867 100644 --- a/server/routers/resource/deleteResource.ts +++ b/server/routers/resource/deleteResource.ts @@ -22,7 +22,7 @@ registry.registerPath({ method: "delete", path: "/resource/{resourceId}", description: "Delete a resource.", - tags: [OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { params: deleteResourceSchema }, diff --git a/server/routers/resource/deleteResourceRule.ts b/server/routers/resource/deleteResourceRule.ts index 638f2e1de..0fe9007f8 100644 --- a/server/routers/resource/deleteResourceRule.ts +++ b/server/routers/resource/deleteResourceRule.ts @@ -19,7 +19,7 @@ registry.registerPath({ method: "delete", path: "/resource/{resourceId}/rule/{ruleId}", description: "Delete a resource rule.", - tags: [OpenAPITags.Resource, OpenAPITags.Rule], + tags: [OpenAPITags.PublicResource, OpenAPITags.Rule], request: { params: deleteResourceRuleSchema }, diff --git a/server/routers/resource/getResource.ts b/server/routers/resource/getResource.ts index 7f3e8a0ea..cd870dcbf 100644 --- a/server/routers/resource/getResource.ts +++ b/server/routers/resource/getResource.ts @@ -54,7 +54,7 @@ registry.registerPath({ path: "/org/{orgId}/resource/{niceId}", description: "Get a resource by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.", - tags: [OpenAPITags.Org, OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { params: z.object({ orgId: z.string(), @@ -68,7 +68,7 @@ registry.registerPath({ method: "get", path: "/resource/{resourceId}", description: "Get a resource by resourceId.", - tags: [OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { params: z.object({ resourceId: z.number() diff --git a/server/routers/resource/getResourceWhitelist.ts b/server/routers/resource/getResourceWhitelist.ts index 52cff0c72..5eb05184f 100644 --- a/server/routers/resource/getResourceWhitelist.ts +++ b/server/routers/resource/getResourceWhitelist.ts @@ -31,7 +31,7 @@ registry.registerPath({ method: "get", path: "/resource/{resourceId}/whitelist", description: "Get the whitelist of emails for a specific resource.", - tags: [OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { params: getResourceWhitelistSchema }, diff --git a/server/routers/resource/listAllResourceNames.ts b/server/routers/resource/listAllResourceNames.ts index df78e2640..37ae945fd 100644 --- a/server/routers/resource/listAllResourceNames.ts +++ b/server/routers/resource/listAllResourceNames.ts @@ -33,7 +33,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/resources-names", description: "List all resource names for an organization.", - tags: [OpenAPITags.Org, OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { params: z.object({ orgId: z.string() diff --git a/server/routers/resource/listResourceRoles.ts b/server/routers/resource/listResourceRoles.ts index 68dc58a21..4af631191 100644 --- a/server/routers/resource/listResourceRoles.ts +++ b/server/routers/resource/listResourceRoles.ts @@ -35,7 +35,7 @@ registry.registerPath({ method: "get", path: "/resource/{resourceId}/roles", description: "List all roles for a resource.", - tags: [OpenAPITags.Resource, OpenAPITags.Role], + tags: [OpenAPITags.PublicResource, OpenAPITags.Role], request: { params: listResourceRolesSchema }, diff --git a/server/routers/resource/listResourceRules.ts b/server/routers/resource/listResourceRules.ts index dae7922d9..92d738cbb 100644 --- a/server/routers/resource/listResourceRules.ts +++ b/server/routers/resource/listResourceRules.ts @@ -56,7 +56,7 @@ registry.registerPath({ method: "get", path: "/resource/{resourceId}/rules", description: "List rules for a resource.", - tags: [OpenAPITags.Resource, OpenAPITags.Rule], + tags: [OpenAPITags.PublicResource, OpenAPITags.Rule], request: { params: listResourceRulesParamsSchema, query: listResourceRulesSchema diff --git a/server/routers/resource/listResourceUsers.ts b/server/routers/resource/listResourceUsers.ts index e7f73287e..2802ac827 100644 --- a/server/routers/resource/listResourceUsers.ts +++ b/server/routers/resource/listResourceUsers.ts @@ -38,7 +38,7 @@ registry.registerPath({ method: "get", path: "/resource/{resourceId}/users", description: "List all users for a resource.", - tags: [OpenAPITags.Resource, OpenAPITags.User], + tags: [OpenAPITags.PublicResource, OpenAPITags.User], request: { params: listResourceUsersSchema }, diff --git a/server/routers/resource/listResources.ts b/server/routers/resource/listResources.ts index e6524a72e..fa7ec8a48 100644 --- a/server/routers/resource/listResources.ts +++ b/server/routers/resource/listResources.ts @@ -19,6 +19,7 @@ import { and, asc, count, + desc, eq, inArray, isNull, @@ -63,6 +64,26 @@ const listResourcesSchema = z.object({ description: "Page number to retrieve" }), query: z.string().optional(), + sort_by: z + .enum(["name"]) + .optional() + .catch(undefined) + .openapi({ + type: "string", + enum: ["name"], + description: "Field to sort by" + }), + order: z + .enum(["asc", "desc"]) + .optional() + .default("asc") + .catch("asc") + .openapi({ + type: "string", + enum: ["asc", "desc"], + default: "asc", + description: "Sort order" + }), enabled: z .enum(["true", "false"]) .transform((v) => v === "true") @@ -204,7 +225,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/resources", description: "List resources for an organization.", - tags: [OpenAPITags.Org, OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { params: z.object({ orgId: z.string() @@ -229,8 +250,16 @@ export async function listResources( ) ); } - const { page, pageSize, authState, enabled, query, healthStatus } = - parsedQuery.data; + const { + page, + pageSize, + authState, + enabled, + query, + healthStatus, + sort_by, + order + } = parsedQuery.data; const parsedParams = listResourcesParamsSchema.safeParse(req.params); if (!parsedParams.success) { @@ -395,7 +424,13 @@ export async function listResources( baseQuery .limit(pageSize) .offset(pageSize * (page - 1)) - .orderBy(asc(resources.resourceId)), + .orderBy( + sort_by + ? order === "asc" + ? asc(resources[sort_by]) + : desc(resources[sort_by]) + : asc(resources.name) + ), countQuery ]); diff --git a/server/routers/resource/removeEmailFromResourceWhitelist.ts b/server/routers/resource/removeEmailFromResourceWhitelist.ts index d60133b85..f419c4136 100644 --- a/server/routers/resource/removeEmailFromResourceWhitelist.ts +++ b/server/routers/resource/removeEmailFromResourceWhitelist.ts @@ -29,7 +29,7 @@ registry.registerPath({ method: "post", path: "/resource/{resourceId}/whitelist/remove", description: "Remove a single email from the resource whitelist.", - tags: [OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { params: removeEmailFromResourceWhitelistParamsSchema, body: { diff --git a/server/routers/resource/removeRoleFromResource.ts b/server/routers/resource/removeRoleFromResource.ts index eab7660c3..eef55277b 100644 --- a/server/routers/resource/removeRoleFromResource.ts +++ b/server/routers/resource/removeRoleFromResource.ts @@ -29,7 +29,7 @@ registry.registerPath({ method: "post", path: "/resource/{resourceId}/roles/remove", description: "Remove a single role from a resource.", - tags: [OpenAPITags.Resource, OpenAPITags.Role], + tags: [OpenAPITags.PublicResource, OpenAPITags.Role], request: { params: removeRoleFromResourceParamsSchema, body: { diff --git a/server/routers/resource/removeUserFromResource.ts b/server/routers/resource/removeUserFromResource.ts index 9da96d3c8..152316e62 100644 --- a/server/routers/resource/removeUserFromResource.ts +++ b/server/routers/resource/removeUserFromResource.ts @@ -29,7 +29,7 @@ registry.registerPath({ method: "post", path: "/resource/{resourceId}/users/remove", description: "Remove a single user from a resource.", - tags: [OpenAPITags.Resource, OpenAPITags.User], + tags: [OpenAPITags.PublicResource, OpenAPITags.User], request: { params: removeUserFromResourceParamsSchema, body: { diff --git a/server/routers/resource/setResourceHeaderAuth.ts b/server/routers/resource/setResourceHeaderAuth.ts index edd911cb8..9c28bf9f0 100644 --- a/server/routers/resource/setResourceHeaderAuth.ts +++ b/server/routers/resource/setResourceHeaderAuth.ts @@ -29,7 +29,7 @@ registry.registerPath({ path: "/resource/{resourceId}/header-auth", description: "Set or update the header authentication for a resource. If user and password is not provided, it will remove the header authentication.", - tags: [OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { params: setResourceAuthMethodsParamsSchema, body: { diff --git a/server/routers/resource/setResourcePassword.ts b/server/routers/resource/setResourcePassword.ts index 9bd845a4e..d9fcb5f0b 100644 --- a/server/routers/resource/setResourcePassword.ts +++ b/server/routers/resource/setResourcePassword.ts @@ -25,7 +25,7 @@ registry.registerPath({ path: "/resource/{resourceId}/password", description: "Set the password for a resource. Setting the password to null will remove it.", - tags: [OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { params: setResourceAuthMethodsParamsSchema, body: { diff --git a/server/routers/resource/setResourcePincode.ts b/server/routers/resource/setResourcePincode.ts index 0d5272731..54057ba08 100644 --- a/server/routers/resource/setResourcePincode.ts +++ b/server/routers/resource/setResourcePincode.ts @@ -29,7 +29,7 @@ registry.registerPath({ path: "/resource/{resourceId}/pincode", description: "Set the PIN code for a resource. Setting the PIN code to null will remove it.", - tags: [OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { params: setResourceAuthMethodsParamsSchema, body: { diff --git a/server/routers/resource/setResourceRoles.ts b/server/routers/resource/setResourceRoles.ts index 751fe4f91..ff3cd7377 100644 --- a/server/routers/resource/setResourceRoles.ts +++ b/server/routers/resource/setResourceRoles.ts @@ -23,7 +23,7 @@ registry.registerPath({ path: "/resource/{resourceId}/roles", description: "Set roles for a resource. This will replace all existing roles.", - tags: [OpenAPITags.Resource, OpenAPITags.Role], + tags: [OpenAPITags.PublicResource, OpenAPITags.Role], request: { params: setResourceRolesParamsSchema, body: { diff --git a/server/routers/resource/setResourceUsers.ts b/server/routers/resource/setResourceUsers.ts index 5ddceb8f0..46b5d1523 100644 --- a/server/routers/resource/setResourceUsers.ts +++ b/server/routers/resource/setResourceUsers.ts @@ -23,7 +23,7 @@ registry.registerPath({ path: "/resource/{resourceId}/users", description: "Set users for a resource. This will replace all existing users.", - tags: [OpenAPITags.Resource, OpenAPITags.User], + tags: [OpenAPITags.PublicResource, OpenAPITags.User], request: { params: setUserResourcesParamsSchema, body: { diff --git a/server/routers/resource/setResourceWhitelist.ts b/server/routers/resource/setResourceWhitelist.ts index 18f612f24..aa5dc8cdb 100644 --- a/server/routers/resource/setResourceWhitelist.ts +++ b/server/routers/resource/setResourceWhitelist.ts @@ -32,7 +32,7 @@ registry.registerPath({ path: "/resource/{resourceId}/whitelist", description: "Set email whitelist for a resource. This will replace all existing emails.", - tags: [OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { params: setResourceWhitelistParamsSchema, body: { diff --git a/server/routers/resource/updateResource.ts b/server/routers/resource/updateResource.ts index 4a3e65fa1..01f3e79ff 100644 --- a/server/routers/resource/updateResource.ts +++ b/server/routers/resource/updateResource.ts @@ -101,6 +101,49 @@ const updateHttpResourceBodySchema = z { error: "Invalid custom Host Header value. Use domain name format, or save empty to unset custom Host Header." } + ) + .refine( + (data) => { + if (data.headers) { + // HTTP header names must be valid token characters (RFC 7230) + const validHeaderName = /^[a-zA-Z0-9!#$%&'*+\-.^_`|~]+$/; + return data.headers.every((h) => validHeaderName.test(h.name)); + } + return true; + }, + { + error: "Header names may only contain valid HTTP token characters (letters, digits, and !#$%&'*+-.^_`|~)." + } + ) + .refine( + (data) => { + if (data.headers) { + // HTTP header values must be visible ASCII or horizontal whitespace, no control chars (RFC 7230) + const validHeaderValue = /^[\t\x20-\x7E]*$/; + return data.headers.every((h) => validHeaderValue.test(h.value)); + } + return true; + }, + { + error: "Header values may only contain printable ASCII characters and horizontal whitespace." + } + ) + .refine( + (data) => { + if (data.headers) { + // Reject Traefik template syntax {{word}} in names or values + const templatePattern = /\{\{[^}]+\}\}/; + return data.headers.every( + (h) => + !templatePattern.test(h.name) && + !templatePattern.test(h.value) + ); + } + return true; + }, + { + error: "Header names and values must not contain template expressions such as {{value}}." + } ); export type UpdateResourceResponse = Resource; @@ -136,7 +179,7 @@ registry.registerPath({ method: "post", path: "/resource/{resourceId}", description: "Update a resource.", - tags: [OpenAPITags.Resource], + tags: [OpenAPITags.PublicResource], request: { params: updateResourceParamsSchema, body: { @@ -310,6 +353,20 @@ async function updateHttpResource( ); } + // Prevent updating resource with same domain as dashboard + const dashboardUrl = config.getRawConfig().app.dashboard_url; + if (dashboardUrl) { + const dashboardHost = new URL(dashboardUrl).hostname; + if (fullDomain === dashboardHost) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Resource domain cannot be the same as the dashboard domain" + ) + ); + } + } + if (build != "oss") { const existingLoginPages = await db .select() diff --git a/server/routers/resource/updateResourceRule.ts b/server/routers/resource/updateResourceRule.ts index b443bd1c2..3e8f395dd 100644 --- a/server/routers/resource/updateResourceRule.ts +++ b/server/routers/resource/updateResourceRule.ts @@ -38,7 +38,7 @@ registry.registerPath({ method: "post", path: "/resource/{resourceId}/rule/{ruleId}", description: "Update a resource rule.", - tags: [OpenAPITags.Resource, OpenAPITags.Rule], + tags: [OpenAPITags.PublicResource, OpenAPITags.Rule], request: { params: updateResourceRuleParamsSchema, body: { diff --git a/server/routers/role/createRole.ts b/server/routers/role/createRole.ts index e732b4054..1fad18d72 100644 --- a/server/routers/role/createRole.ts +++ b/server/routers/role/createRole.ts @@ -45,7 +45,7 @@ registry.registerPath({ method: "put", path: "/org/{orgId}/role", description: "Create a role.", - tags: [OpenAPITags.Org, OpenAPITags.Role], + tags: [OpenAPITags.Role], request: { params: createRoleParamsSchema, body: { diff --git a/server/routers/role/listRoles.ts b/server/routers/role/listRoles.ts index d4cb580f2..f1b057a11 100644 --- a/server/routers/role/listRoles.ts +++ b/server/routers/role/listRoles.ts @@ -7,7 +7,7 @@ import { and, eq, inArray, sql } from "drizzle-orm"; import { ActionsEnum } from "@server/auth/actions"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; -import { z } from "zod"; +import { object, z } from "zod"; import { fromError } from "zod-validation-error"; const listRolesParamsSchema = z.strictObject({ @@ -64,7 +64,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/roles", description: "List roles.", - tags: [OpenAPITags.Org, OpenAPITags.Role], + tags: [OpenAPITags.Role], request: { params: listRolesParamsSchema, query: listRolesSchema diff --git a/server/routers/site/createSite.ts b/server/routers/site/createSite.ts index 57e963e56..4edebb080 100644 --- a/server/routers/site/createSite.ts +++ b/server/routers/site/createSite.ts @@ -58,7 +58,7 @@ registry.registerPath({ method: "put", path: "/org/{orgId}/site", description: "Create a new site.", - tags: [OpenAPITags.Site, OpenAPITags.Org], + tags: [OpenAPITags.Site], request: { params: createSiteParamsSchema, body: { @@ -292,7 +292,7 @@ export async function createSite( if (type == "newt") { [newSite] = await trx .insert(sites) - .values({ + .values({ // NOTE: NO SUBNET OR EXIT NODE ID PASSED IN HERE BECAUSE ITS NOW CHOSEN ON CONNECT orgId, name, niceId, diff --git a/server/routers/site/getSite.ts b/server/routers/site/getSite.ts index c82bf1999..45d49abe6 100644 --- a/server/routers/site/getSite.ts +++ b/server/routers/site/getSite.ts @@ -51,7 +51,7 @@ registry.registerPath({ path: "/org/{orgId}/site/{niceId}", description: "Get a site by orgId and niceId. NiceId is a readable ID for the site and unique on a per org basis.", - tags: [OpenAPITags.Org, OpenAPITags.Site], + tags: [OpenAPITags.Site], request: { params: z.object({ orgId: z.string(), diff --git a/server/routers/site/listSites.ts b/server/routers/site/listSites.ts index f2d460ff7..a244c650c 100644 --- a/server/routers/site/listSites.ts +++ b/server/routers/site/listSites.ts @@ -8,7 +8,7 @@ import { sites, userSites } from "@server/db"; -import cache from "@server/lib/cache"; +import cache from "#dynamic/lib/cache"; import response from "@server/lib/response"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; @@ -23,7 +23,7 @@ import { fromError } from "zod-validation-error"; async function getLatestNewtVersion(): Promise { try { - const cachedVersion = cache.get("latestNewtVersion"); + const cachedVersion = await cache.get("latestNewtVersion"); if (cachedVersion) { return cachedVersion; } @@ -55,7 +55,7 @@ async function getLatestNewtVersion(): Promise { tags = tags.filter((version) => !version.name.includes("rc")); const latestVersion = tags[0].name; - cache.set("latestNewtVersion", latestVersion); + await cache.set("latestNewtVersion", latestVersion, 3600); return latestVersion; } catch (error: any) { @@ -108,12 +108,12 @@ const listSitesSchema = z.object({ }), query: z.string().optional(), sort_by: z - .enum(["megabytesIn", "megabytesOut"]) + .enum(["name", "megabytesIn", "megabytesOut"]) .optional() .catch(undefined) .openapi({ type: "string", - enum: ["megabytesIn", "megabytesOut"], + enum: ["name", "megabytesIn", "megabytesOut"], description: "Field to sort by" }), order: z @@ -278,7 +278,7 @@ export async function listSites( // we need to add `as` so that drizzle filters the result as a subquery const countQuery = db.$count( - querySitesBase().where(and(...conditions)) + querySitesBase().where(and(...conditions)).as("filtered_sites") ); const siteListQuery = baseQuery @@ -289,7 +289,7 @@ export async function listSites( ? order === "asc" ? asc(sites[sort_by]) : desc(sites[sort_by]) - : asc(sites.siteId) + : asc(sites.name) ); const [totalCount, rows] = await Promise.all([ diff --git a/server/routers/site/pickSiteDefaults.ts b/server/routers/site/pickSiteDefaults.ts index 69ed76886..f5e95ca10 100644 --- a/server/routers/site/pickSiteDefaults.ts +++ b/server/routers/site/pickSiteDefaults.ts @@ -35,7 +35,7 @@ registry.registerPath({ path: "/org/{orgId}/pick-site-defaults", description: "Return pre-requisite data for creating a site, such as the exit node, subnet, Newt credentials, etc.", - tags: [OpenAPITags.Org, OpenAPITags.Site], + tags: [OpenAPITags.Site], request: { params: z.object({ orgId: z.string() diff --git a/server/routers/site/socketIntegration.ts b/server/routers/site/socketIntegration.ts index e0ad09d1e..fe6e7b95e 100644 --- a/server/routers/site/socketIntegration.ts +++ b/server/routers/site/socketIntegration.ts @@ -11,7 +11,7 @@ import { fromError } from "zod-validation-error"; import stoi from "@server/lib/stoi"; import { sendToClient } from "#dynamic/routers/ws"; import { fetchContainers, dockerSocket } from "../newt/dockerSocket"; -import cache from "@server/lib/cache"; +import cache from "#dynamic/lib/cache"; export interface ContainerNetwork { networkId: string; @@ -150,7 +150,7 @@ async function triggerFetch(siteId: number) { // clear the cache for this Newt ID so that the site has to keep asking for the containers // this is to ensure that the site always gets the latest data - cache.del(`${newt.newtId}:dockerContainers`); + await cache.del(`${newt.newtId}:dockerContainers`); return { siteId, newtId: newt.newtId }; } @@ -158,7 +158,7 @@ async function triggerFetch(siteId: number) { async function queryContainers(siteId: number) { const { newt } = await getSiteAndNewt(siteId); - const result = cache.get(`${newt.newtId}:dockerContainers`) as Container[]; + const result = await cache.get(`${newt.newtId}:dockerContainers`); if (!result) { throw createHttpError( HttpCode.TOO_EARLY, @@ -173,7 +173,7 @@ async function isDockerAvailable(siteId: number): Promise { const { newt } = await getSiteAndNewt(siteId); const key = `${newt.newtId}:isAvailable`; - const isAvailable = cache.get(key); + const isAvailable = await cache.get(key); return !!isAvailable; } @@ -186,9 +186,11 @@ async function getDockerStatus( const keys = ["isAvailable", "socketPath"]; const mappedKeys = keys.map((x) => `${newt.newtId}:${x}`); + const values = await cache.mget(mappedKeys); + const result = { - isAvailable: cache.get(mappedKeys[0]) as boolean, - socketPath: cache.get(mappedKeys[1]) as string | undefined + isAvailable: values[0] as boolean, + socketPath: values[1] as string | undefined }; return result; diff --git a/server/routers/siteResource/addClientToSiteResource.ts b/server/routers/siteResource/addClientToSiteResource.ts index 27d7f0573..4a67df94f 100644 --- a/server/routers/siteResource/addClientToSiteResource.ts +++ b/server/routers/siteResource/addClientToSiteResource.ts @@ -30,7 +30,7 @@ registry.registerPath({ path: "/site-resource/{siteResourceId}/clients/add", description: "Add a single client to a site resource. Clients with a userId cannot be added.", - tags: [OpenAPITags.Resource, OpenAPITags.Client], + tags: [OpenAPITags.PrivateResource, OpenAPITags.Client], request: { params: addClientToSiteResourceParamsSchema, body: { diff --git a/server/routers/siteResource/addRoleToSiteResource.ts b/server/routers/siteResource/addRoleToSiteResource.ts index abc2d221e..f6501463b 100644 --- a/server/routers/siteResource/addRoleToSiteResource.ts +++ b/server/routers/siteResource/addRoleToSiteResource.ts @@ -30,7 +30,7 @@ registry.registerPath({ method: "post", path: "/site-resource/{siteResourceId}/roles/add", description: "Add a single role to a site resource.", - tags: [OpenAPITags.Resource, OpenAPITags.Role], + tags: [OpenAPITags.PrivateResource, OpenAPITags.Role], request: { params: addRoleToSiteResourceParamsSchema, body: { diff --git a/server/routers/siteResource/addUserToSiteResource.ts b/server/routers/siteResource/addUserToSiteResource.ts index 4edf741cd..68151077c 100644 --- a/server/routers/siteResource/addUserToSiteResource.ts +++ b/server/routers/siteResource/addUserToSiteResource.ts @@ -30,7 +30,7 @@ registry.registerPath({ method: "post", path: "/site-resource/{siteResourceId}/users/add", description: "Add a single user to a site resource.", - tags: [OpenAPITags.Resource, OpenAPITags.User], + tags: [OpenAPITags.PrivateResource, OpenAPITags.User], request: { params: addUserToSiteResourceParamsSchema, body: { diff --git a/server/routers/siteResource/batchAddClientToSiteResources.ts b/server/routers/siteResource/batchAddClientToSiteResources.ts new file mode 100644 index 000000000..c3ad3859a --- /dev/null +++ b/server/routers/siteResource/batchAddClientToSiteResources.ts @@ -0,0 +1,247 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { + db, + clients, + clientSiteResources, + siteResources, + apiKeyOrg +} from "@server/db"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { eq, and, inArray } from "drizzle-orm"; +import { OpenAPITags, registry } from "@server/openApi"; +import { + rebuildClientAssociationsFromClient, + rebuildClientAssociationsFromSiteResource +} from "@server/lib/rebuildClientAssociations"; + +const batchAddClientToSiteResourcesParamsSchema = z + .object({ + clientId: z.string().transform(Number).pipe(z.number().int().positive()) + }) + .strict(); + +const batchAddClientToSiteResourcesBodySchema = z + .object({ + siteResourceIds: z + .array(z.number().int().positive()) + .min(1, "At least one siteResourceId is required") + }) + .strict(); + +registry.registerPath({ + method: "post", + path: "/client/{clientId}/site-resources", + description: "Add a machine client to multiple site resources at once.", + tags: [OpenAPITags.Client], + request: { + params: batchAddClientToSiteResourcesParamsSchema, + body: { + content: { + "application/json": { + schema: batchAddClientToSiteResourcesBodySchema + } + } + } + }, + responses: {} +}); + +export async function batchAddClientToSiteResources( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const apiKey = req.apiKey; + if (!apiKey) { + return next( + createHttpError(HttpCode.UNAUTHORIZED, "Key not authenticated") + ); + } + + const parsedParams = + batchAddClientToSiteResourcesParamsSchema.safeParse(req.params); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedBody = batchAddClientToSiteResourcesBodySchema.safeParse( + req.body + ); + if (!parsedBody.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedBody.error).toString() + ) + ); + } + + const { clientId } = parsedParams.data; + const { siteResourceIds } = parsedBody.data; + const uniqueSiteResourceIds = [...new Set(siteResourceIds)]; + + const batchSiteResources = await db + .select() + .from(siteResources) + .where( + inArray(siteResources.siteResourceId, uniqueSiteResourceIds) + ); + + if (batchSiteResources.length !== uniqueSiteResourceIds.length) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + "One or more site resources not found" + ) + ); + } + + if (!apiKey.isRoot) { + const orgIds = [ + ...new Set(batchSiteResources.map((sr) => sr.orgId)) + ]; + if (orgIds.length > 1) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "All site resources must belong to the same organization" + ) + ); + } + const orgId = orgIds[0]; + const [apiKeyOrgRow] = await db + .select() + .from(apiKeyOrg) + .where( + and( + eq(apiKeyOrg.apiKeyId, apiKey.apiKeyId), + eq(apiKeyOrg.orgId, orgId) + ) + ) + .limit(1); + + if (!apiKeyOrgRow) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to the organization of the specified site resources" + ) + ); + } + + const [clientInOrg] = await db + .select() + .from(clients) + .where( + and( + eq(clients.clientId, clientId), + eq(clients.orgId, orgId) + ) + ) + .limit(1); + + if (!clientInOrg) { + return next( + createHttpError( + HttpCode.FORBIDDEN, + "Key does not have access to the specified client" + ) + ); + } + } + + const [client] = await db + .select() + .from(clients) + .where(eq(clients.clientId, clientId)) + .limit(1); + + if (!client) { + return next( + createHttpError(HttpCode.NOT_FOUND, "Client not found") + ); + } + + if (client.userId !== null) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "This endpoint only supports machine (non-user) clients; the specified client is associated with a user" + ) + ); + } + + const existingEntries = await db + .select({ + siteResourceId: clientSiteResources.siteResourceId + }) + .from(clientSiteResources) + .where( + and( + eq(clientSiteResources.clientId, clientId), + inArray( + clientSiteResources.siteResourceId, + batchSiteResources.map((sr) => sr.siteResourceId) + ) + ) + ); + + const existingSiteResourceIds = new Set( + existingEntries.map((e) => e.siteResourceId) + ); + const siteResourcesToAdd = batchSiteResources.filter( + (sr) => !existingSiteResourceIds.has(sr.siteResourceId) + ); + + if (siteResourcesToAdd.length === 0) { + return next( + createHttpError( + HttpCode.CONFLICT, + "Client is already assigned to all specified site resources" + ) + ); + } + + await db.transaction(async (trx) => { + for (const siteResource of siteResourcesToAdd) { + await trx.insert(clientSiteResources).values({ + clientId, + siteResourceId: siteResource.siteResourceId + }); + } + + await rebuildClientAssociationsFromClient(client, trx); + }); + + return response(res, { + data: { + addedCount: siteResourcesToAdd.length, + skippedCount: + batchSiteResources.length - siteResourcesToAdd.length, + siteResourceIds: siteResourcesToAdd.map( + (sr) => sr.siteResourceId + ) + }, + success: true, + error: false, + message: `Client added to ${siteResourcesToAdd.length} site resource(s) successfully`, + status: HttpCode.CREATED + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/siteResource/createSiteResource.ts b/server/routers/siteResource/createSiteResource.ts index bbdc3638d..b9494776e 100644 --- a/server/routers/siteResource/createSiteResource.ts +++ b/server/routers/siteResource/createSiteResource.ts @@ -114,7 +114,7 @@ registry.registerPath({ method: "put", path: "/org/{orgId}/site-resource", description: "Create a new site resource.", - tags: [OpenAPITags.Client, OpenAPITags.Org], + tags: [OpenAPITags.PrivateResource], request: { params: createSiteResourceParamsSchema, body: { diff --git a/server/routers/siteResource/deleteSiteResource.ts b/server/routers/siteResource/deleteSiteResource.ts index 336c5080d..5b50b0ea3 100644 --- a/server/routers/siteResource/deleteSiteResource.ts +++ b/server/routers/siteResource/deleteSiteResource.ts @@ -23,7 +23,7 @@ registry.registerPath({ method: "delete", path: "/site-resource/{siteResourceId}", description: "Delete a site resource.", - tags: [OpenAPITags.Client, OpenAPITags.Org], + tags: [OpenAPITags.PrivateResource], request: { params: deleteSiteResourceParamsSchema }, diff --git a/server/routers/siteResource/getSiteResource.ts b/server/routers/siteResource/getSiteResource.ts index f97a5e22f..be28d36e4 100644 --- a/server/routers/siteResource/getSiteResource.ts +++ b/server/routers/siteResource/getSiteResource.ts @@ -65,7 +65,7 @@ registry.registerPath({ method: "get", path: "/site-resource/{siteResourceId}", description: "Get a specific site resource by siteResourceId.", - tags: [OpenAPITags.Client, OpenAPITags.Org], + tags: [OpenAPITags.PrivateResource], request: { params: z.object({ siteResourceId: z.number(), @@ -80,7 +80,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/site/{siteId}/resource/nice/{niceId}", description: "Get a specific site resource by niceId.", - tags: [OpenAPITags.Client, OpenAPITags.Org], + tags: [OpenAPITags.PrivateResource], request: { params: z.object({ niceId: z.string(), diff --git a/server/routers/siteResource/index.ts b/server/routers/siteResource/index.ts index 9494843bf..5c09d3883 100644 --- a/server/routers/siteResource/index.ts +++ b/server/routers/siteResource/index.ts @@ -15,4 +15,5 @@ export * from "./addUserToSiteResource"; export * from "./removeUserFromSiteResource"; export * from "./setSiteResourceClients"; export * from "./addClientToSiteResource"; +export * from "./batchAddClientToSiteResources"; export * from "./removeClientFromSiteResource"; diff --git a/server/routers/siteResource/listAllSiteResourcesByOrg.ts b/server/routers/siteResource/listAllSiteResourcesByOrg.ts index 5aec53c79..3320aa3b7 100644 --- a/server/routers/siteResource/listAllSiteResourcesByOrg.ts +++ b/server/routers/siteResource/listAllSiteResourcesByOrg.ts @@ -4,7 +4,7 @@ import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; import HttpCode from "@server/types/HttpCode"; import type { PaginatedResponse } from "@server/types/Pagination"; -import { and, asc, eq, like, or, sql } from "drizzle-orm"; +import { and, asc, desc, eq, like, or, sql } from "drizzle-orm"; import { NextFunction, Request, Response } from "express"; import createHttpError from "http-errors"; import { z } from "zod"; @@ -48,6 +48,26 @@ const listAllSiteResourcesByOrgQuerySchema = z.object({ type: "string", enum: ["host", "cidr"], description: "Filter site resources by mode" + }), + sort_by: z + .enum(["name"]) + .optional() + .catch(undefined) + .openapi({ + type: "string", + enum: ["name"], + description: "Field to sort by" + }), + order: z + .enum(["asc", "desc"]) + .optional() + .default("asc") + .catch("asc") + .openapi({ + type: "string", + enum: ["asc", "desc"], + default: "asc", + description: "Sort order" }) }); @@ -92,7 +112,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/site-resources", description: "List all site resources for an organization.", - tags: [OpenAPITags.Client, OpenAPITags.Org], + tags: [OpenAPITags.PrivateResource], request: { params: listAllSiteResourcesByOrgParamsSchema, query: listAllSiteResourcesByOrgQuerySchema @@ -131,7 +151,8 @@ export async function listAllSiteResourcesByOrg( } const { orgId } = parsedParams.data; - const { page, pageSize, query, mode } = parsedQuery.data; + const { page, pageSize, query, mode, sort_by, order } = + parsedQuery.data; const conditions = [and(eq(siteResources.orgId, orgId))]; if (query) { @@ -172,14 +193,20 @@ export async function listAllSiteResourcesByOrg( const baseQuery = querySiteResourcesBase().where(and(...conditions)); const countQuery = db.$count( - querySiteResourcesBase().where(and(...conditions)) + querySiteResourcesBase().where(and(...conditions)).as("filtered_site_resources") ); const [siteResourcesList, totalCount] = await Promise.all([ baseQuery .limit(pageSize) .offset(pageSize * (page - 1)) - .orderBy(asc(siteResources.siteResourceId)), + .orderBy( + sort_by + ? order === "asc" + ? asc(siteResources[sort_by]) + : desc(siteResources[sort_by]) + : asc(siteResources.name) + ), countQuery ]); diff --git a/server/routers/siteResource/listSiteResourceClients.ts b/server/routers/siteResource/listSiteResourceClients.ts index 772750d16..867e66b49 100644 --- a/server/routers/siteResource/listSiteResourceClients.ts +++ b/server/routers/siteResource/listSiteResourceClients.ts @@ -39,7 +39,7 @@ registry.registerPath({ method: "get", path: "/site-resource/{siteResourceId}/clients", description: "List all clients for a site resource.", - tags: [OpenAPITags.Resource, OpenAPITags.Client], + tags: [OpenAPITags.PrivateResource, OpenAPITags.Client], request: { params: listSiteResourceClientsSchema }, diff --git a/server/routers/siteResource/listSiteResourceRoles.ts b/server/routers/siteResource/listSiteResourceRoles.ts index 0dc5913b6..679a93f7e 100644 --- a/server/routers/siteResource/listSiteResourceRoles.ts +++ b/server/routers/siteResource/listSiteResourceRoles.ts @@ -40,7 +40,7 @@ registry.registerPath({ method: "get", path: "/site-resource/{siteResourceId}/roles", description: "List all roles for a site resource.", - tags: [OpenAPITags.Resource, OpenAPITags.Role], + tags: [OpenAPITags.PrivateResource, OpenAPITags.Role], request: { params: listSiteResourceRolesSchema }, diff --git a/server/routers/siteResource/listSiteResourceUsers.ts b/server/routers/siteResource/listSiteResourceUsers.ts index daf754801..e50d8684e 100644 --- a/server/routers/siteResource/listSiteResourceUsers.ts +++ b/server/routers/siteResource/listSiteResourceUsers.ts @@ -43,7 +43,7 @@ registry.registerPath({ method: "get", path: "/site-resource/{siteResourceId}/users", description: "List all users for a site resource.", - tags: [OpenAPITags.Resource, OpenAPITags.User], + tags: [OpenAPITags.PrivateResource, OpenAPITags.User], request: { params: listSiteResourceUsersSchema }, diff --git a/server/routers/siteResource/listSiteResources.ts b/server/routers/siteResource/listSiteResources.ts index 6ecda7c4c..358aa0497 100644 --- a/server/routers/siteResource/listSiteResources.ts +++ b/server/routers/siteResource/listSiteResources.ts @@ -5,7 +5,7 @@ import { siteResources, sites, SiteResource } from "@server/db"; import response from "@server/lib/response"; import HttpCode from "@server/types/HttpCode"; import createHttpError from "http-errors"; -import { eq, and } from "drizzle-orm"; +import { and, asc, desc, eq } from "drizzle-orm"; import { fromError } from "zod-validation-error"; import logger from "@server/logger"; import { OpenAPITags, registry } from "@server/openApi"; @@ -27,7 +27,27 @@ const listSiteResourcesQuerySchema = z.object({ .optional() .default("0") .transform(Number) - .pipe(z.int().nonnegative()) + .pipe(z.int().nonnegative()), + sort_by: z + .enum(["name"]) + .optional() + .catch(undefined) + .openapi({ + type: "string", + enum: ["name"], + description: "Field to sort by" + }), + order: z + .enum(["asc", "desc"]) + .optional() + .default("asc") + .catch("asc") + .openapi({ + type: "string", + enum: ["asc", "desc"], + default: "asc", + description: "Sort order" + }) }); export type ListSiteResourcesResponse = { @@ -38,7 +58,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/site/{siteId}/resources", description: "List site resources for a site.", - tags: [OpenAPITags.Client, OpenAPITags.Org], + tags: [OpenAPITags.PrivateResource], request: { params: listSiteResourcesParamsSchema, query: listSiteResourcesQuerySchema @@ -75,7 +95,7 @@ export async function listSiteResources( } const { siteId, orgId } = parsedParams.data; - const { limit, offset } = parsedQuery.data; + const { limit, offset, sort_by, order } = parsedQuery.data; // Verify the site exists and belongs to the org const site = await db @@ -98,6 +118,13 @@ export async function listSiteResources( eq(siteResources.orgId, orgId) ) ) + .orderBy( + sort_by + ? order === "asc" + ? asc(siteResources[sort_by]) + : desc(siteResources[sort_by]) + : asc(siteResources.name) + ) .limit(limit) .offset(offset); diff --git a/server/routers/siteResource/removeClientFromSiteResource.ts b/server/routers/siteResource/removeClientFromSiteResource.ts index 351128d18..51e54dd92 100644 --- a/server/routers/siteResource/removeClientFromSiteResource.ts +++ b/server/routers/siteResource/removeClientFromSiteResource.ts @@ -30,7 +30,7 @@ registry.registerPath({ path: "/site-resource/{siteResourceId}/clients/remove", description: "Remove a single client from a site resource. Clients with a userId cannot be removed.", - tags: [OpenAPITags.Resource, OpenAPITags.Client], + tags: [OpenAPITags.PrivateResource, OpenAPITags.Client], request: { params: removeClientFromSiteResourceParamsSchema, body: { diff --git a/server/routers/siteResource/removeRoleFromSiteResource.ts b/server/routers/siteResource/removeRoleFromSiteResource.ts index c9857e841..19478ed0f 100644 --- a/server/routers/siteResource/removeRoleFromSiteResource.ts +++ b/server/routers/siteResource/removeRoleFromSiteResource.ts @@ -30,7 +30,7 @@ registry.registerPath({ method: "post", path: "/site-resource/{siteResourceId}/roles/remove", description: "Remove a single role from a site resource.", - tags: [OpenAPITags.Resource, OpenAPITags.Role], + tags: [OpenAPITags.PrivateResource, OpenAPITags.Role], request: { params: removeRoleFromSiteResourceParamsSchema, body: { diff --git a/server/routers/siteResource/removeUserFromSiteResource.ts b/server/routers/siteResource/removeUserFromSiteResource.ts index 84347b2f6..70bb5e22e 100644 --- a/server/routers/siteResource/removeUserFromSiteResource.ts +++ b/server/routers/siteResource/removeUserFromSiteResource.ts @@ -30,7 +30,7 @@ registry.registerPath({ method: "post", path: "/site-resource/{siteResourceId}/users/remove", description: "Remove a single user from a site resource.", - tags: [OpenAPITags.Resource, OpenAPITags.User], + tags: [OpenAPITags.PrivateResource, OpenAPITags.User], request: { params: removeUserFromSiteResourceParamsSchema, body: { diff --git a/server/routers/siteResource/setSiteResourceClients.ts b/server/routers/siteResource/setSiteResourceClients.ts index 5a8acbcf5..7aff3875f 100644 --- a/server/routers/siteResource/setSiteResourceClients.ts +++ b/server/routers/siteResource/setSiteResourceClients.ts @@ -30,7 +30,7 @@ registry.registerPath({ path: "/site-resource/{siteResourceId}/clients", description: "Set clients for a site resource. This will replace all existing clients. Clients with a userId cannot be added.", - tags: [OpenAPITags.Resource, OpenAPITags.Client], + tags: [OpenAPITags.PrivateResource, OpenAPITags.Client], request: { params: setSiteResourceClientsParamsSchema, body: { diff --git a/server/routers/siteResource/setSiteResourceRoles.ts b/server/routers/siteResource/setSiteResourceRoles.ts index bb71a16b6..a1ee80b40 100644 --- a/server/routers/siteResource/setSiteResourceRoles.ts +++ b/server/routers/siteResource/setSiteResourceRoles.ts @@ -31,7 +31,7 @@ registry.registerPath({ path: "/site-resource/{siteResourceId}/roles", description: "Set roles for a site resource. This will replace all existing roles.", - tags: [OpenAPITags.Resource, OpenAPITags.Role], + tags: [OpenAPITags.PrivateResource, OpenAPITags.Role], request: { params: setSiteResourceRolesParamsSchema, body: { diff --git a/server/routers/siteResource/setSiteResourceUsers.ts b/server/routers/siteResource/setSiteResourceUsers.ts index eacd826cc..109e8c429 100644 --- a/server/routers/siteResource/setSiteResourceUsers.ts +++ b/server/routers/siteResource/setSiteResourceUsers.ts @@ -31,7 +31,7 @@ registry.registerPath({ path: "/site-resource/{siteResourceId}/users", description: "Set users for a site resource. This will replace all existing users.", - tags: [OpenAPITags.Resource, OpenAPITags.User], + tags: [OpenAPITags.PrivateResource, OpenAPITags.User], request: { params: setSiteResourceUsersParamsSchema, body: { diff --git a/server/routers/siteResource/updateSiteResource.ts b/server/routers/siteResource/updateSiteResource.ts index 242b92265..596ed9a3f 100644 --- a/server/routers/siteResource/updateSiteResource.ts +++ b/server/routers/siteResource/updateSiteResource.ts @@ -121,7 +121,7 @@ registry.registerPath({ method: "post", path: "/site-resource/{siteResourceId}", description: "Update a site resource.", - tags: [OpenAPITags.Client, OpenAPITags.Org], + tags: [OpenAPITags.PrivateResource], request: { params: updateSiteResourceParamsSchema, body: { @@ -620,7 +620,7 @@ export async function handleMessagingForUpdatedSiteResource( await updateTargets(newt.newtId, { oldTargets: oldTargets, newTargets: newTargets - }); + }, newt.version); } const olmJobs: Promise[] = []; diff --git a/server/routers/target/createTarget.ts b/server/routers/target/createTarget.ts index 5d37f6173..ba52d85a1 100644 --- a/server/routers/target/createTarget.ts +++ b/server/routers/target/createTarget.ts @@ -58,7 +58,7 @@ registry.registerPath({ method: "put", path: "/resource/{resourceId}/target", description: "Create a target for a resource.", - tags: [OpenAPITags.Resource, OpenAPITags.Target], + tags: [OpenAPITags.PublicResource, OpenAPITags.Target], request: { params: createTargetParamsSchema, body: { @@ -264,7 +264,7 @@ export async function createTarget( newTarget, healthCheck, resource.protocol, - resource.proxyPort + newt.version ); } } diff --git a/server/routers/target/listTargets.ts b/server/routers/target/listTargets.ts index 11a23f025..e4ef45f3b 100644 --- a/server/routers/target/listTargets.ts +++ b/server/routers/target/listTargets.ts @@ -88,7 +88,7 @@ registry.registerPath({ method: "get", path: "/resource/{resourceId}/targets", description: "List targets for a resource.", - tags: [OpenAPITags.Resource, OpenAPITags.Target], + tags: [OpenAPITags.PublicResource, OpenAPITags.Target], request: { params: listTargetsParamsSchema, query: listTargetsSchema diff --git a/server/routers/target/updateTarget.ts b/server/routers/target/updateTarget.ts index c5321e986..dd31f5f1b 100644 --- a/server/routers/target/updateTarget.ts +++ b/server/routers/target/updateTarget.ts @@ -262,7 +262,7 @@ export async function updateTarget( [updatedTarget], [updatedHc], resource.protocol, - resource.proxyPort + newt.version ); } } diff --git a/server/routers/traefik/traefikConfigProvider.ts b/server/routers/traefik/traefikConfigProvider.ts index e8ac1621e..fa76190ff 100644 --- a/server/routers/traefik/traefikConfigProvider.ts +++ b/server/routers/traefik/traefikConfigProvider.ts @@ -39,11 +39,18 @@ export async function traefikConfigProvider( userSessionCookieName: config.getRawConfig().server.session_cookie_name, - // deprecated accessTokenQueryParam: config.getRawConfig().server .resource_access_token_param, + accessTokenIdHeader: + config.getRawConfig().server + .resource_access_token_headers.id, + + accessTokenHeader: + config.getRawConfig().server + .resource_access_token_headers.token, + resourceSessionRequestParam: config.getRawConfig().server .resource_session_request_param diff --git a/server/routers/user/createOrgUser.ts b/server/routers/user/createOrgUser.ts index 891836651..237b7111e 100644 --- a/server/routers/user/createOrgUser.ts +++ b/server/routers/user/createOrgUser.ts @@ -37,7 +37,7 @@ registry.registerPath({ method: "put", path: "/org/{orgId}/user", description: "Create an organization user.", - tags: [OpenAPITags.User, OpenAPITags.Org], + tags: [OpenAPITags.User], request: { params: paramsSchema, body: { diff --git a/server/routers/user/getOrgUser.ts b/server/routers/user/getOrgUser.ts index 2cced3fc2..c415e186c 100644 --- a/server/routers/user/getOrgUser.ts +++ b/server/routers/user/getOrgUser.ts @@ -11,7 +11,7 @@ import { fromError } from "zod-validation-error"; import { ActionsEnum, checkUserActionPermission } from "@server/auth/actions"; import { OpenAPITags, registry } from "@server/openApi"; -async function queryUser(orgId: string, userId: string) { +export async function queryUser(orgId: string, userId: string) { const [userRow] = await db .select({ orgId: userOrgs.orgId, @@ -79,7 +79,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/user/{userId}", description: "Get a user in an organization.", - tags: [OpenAPITags.Org, OpenAPITags.User], + tags: [OpenAPITags.User], request: { params: getOrgUserParamsSchema }, diff --git a/server/routers/user/getOrgUserByUsername.ts b/server/routers/user/getOrgUserByUsername.ts new file mode 100644 index 000000000..a6a764a54 --- /dev/null +++ b/server/routers/user/getOrgUserByUsername.ts @@ -0,0 +1,136 @@ +import { Request, Response, NextFunction } from "express"; +import { z } from "zod"; +import { db } from "@server/db"; +import { userOrgs, users } from "@server/db"; +import { and, eq } from "drizzle-orm"; +import response from "@server/lib/response"; +import HttpCode from "@server/types/HttpCode"; +import createHttpError from "http-errors"; +import logger from "@server/logger"; +import { fromError } from "zod-validation-error"; +import { OpenAPITags, registry } from "@server/openApi"; +import { queryUser, type GetOrgUserResponse } from "./getOrgUser"; + +const getOrgUserByUsernameParamsSchema = z.strictObject({ + orgId: z.string() +}); + +const getOrgUserByUsernameQuerySchema = z.strictObject({ + username: z.string().min(1, "username is required"), + idpId: z + .string() + .optional() + .transform((v) => + v === undefined || v === "" ? undefined : parseInt(v, 10) + ) + .refine( + (v) => + v === undefined || (Number.isInteger(v) && (v as number) > 0), + { message: "idpId must be a positive integer" } + ) +}); + +registry.registerPath({ + method: "get", + path: "/org/{orgId}/user-by-username", + description: + "Get a user in an organization by username. When idpId is not passed, only internal users are searched (username is globally unique for them). For external (OIDC) users, pass idpId to search by username within that identity provider.", + tags: [OpenAPITags.User], + request: { + params: getOrgUserByUsernameParamsSchema, + query: getOrgUserByUsernameQuerySchema + }, + responses: {} +}); + +export async function getOrgUserByUsername( + req: Request, + res: Response, + next: NextFunction +): Promise { + try { + const parsedParams = getOrgUserByUsernameParamsSchema.safeParse( + req.params + ); + if (!parsedParams.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedParams.error).toString() + ) + ); + } + + const parsedQuery = getOrgUserByUsernameQuerySchema.safeParse( + req.query + ); + if (!parsedQuery.success) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + fromError(parsedQuery.error).toString() + ) + ); + } + + const { orgId } = parsedParams.data; + const { username, idpId } = parsedQuery.data; + + const conditions = [ + eq(userOrgs.orgId, orgId), + eq(users.username, username) + ]; + if (idpId !== undefined) { + conditions.push(eq(users.idpId, idpId)); + } else { + conditions.push(eq(users.type, "internal")); + } + + const candidates = await db + .select({ userId: users.userId }) + .from(userOrgs) + .innerJoin(users, eq(userOrgs.userId, users.userId)) + .where(and(...conditions)); + + if (candidates.length === 0) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `User with username '${username}' not found in organization` + ) + ); + } + + if (candidates.length > 1) { + return next( + createHttpError( + HttpCode.BAD_REQUEST, + "Multiple users with this username (external users from different identity providers). Specify idpId (identity provider ID) to disambiguate. When not specified, this searches for internal users only." + ) + ); + } + + const user = await queryUser(orgId, candidates[0].userId); + if (!user) { + return next( + createHttpError( + HttpCode.NOT_FOUND, + `User with username '${username}' not found in organization` + ) + ); + } + + return response(res, { + data: user, + success: true, + error: false, + message: "User retrieved successfully", + status: HttpCode.OK + }); + } catch (error) { + logger.error(error); + return next( + createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred") + ); + } +} diff --git a/server/routers/user/index.ts b/server/routers/user/index.ts index 2de44d8b1..a300e0092 100644 --- a/server/routers/user/index.ts +++ b/server/routers/user/index.ts @@ -6,6 +6,7 @@ export * from "./removeUserRole"; export * from "./inviteUser"; export * from "./acceptInvite"; export * from "./getOrgUser"; +export * from "./getOrgUserByUsername"; export * from "./adminListUsers"; export * from "./adminRemoveUser"; export * from "./adminGetUser"; diff --git a/server/routers/user/inviteUser.ts b/server/routers/user/inviteUser.ts index 693ef3b95..b0632da9e 100644 --- a/server/routers/user/inviteUser.ts +++ b/server/routers/user/inviteUser.ts @@ -19,7 +19,7 @@ import { UserType } from "@server/types/UserTypes"; import { usageService } from "@server/lib/billing/usageService"; import { FeatureId } from "@server/lib/billing"; import { build } from "@server/build"; -import cache from "@server/lib/cache"; +import cache from "#dynamic/lib/cache"; const inviteUserParamsSchema = z.strictObject({ orgId: z.string() @@ -44,7 +44,7 @@ registry.registerPath({ method: "post", path: "/org/{orgId}/create-invite", description: "Invite a user to join an organization.", - tags: [OpenAPITags.Org], + tags: [OpenAPITags.Invitation], request: { params: inviteUserParamsSchema, body: { @@ -191,7 +191,7 @@ export async function inviteUser( } if (existingInvite.length) { - const attempts = cache.get(email) || 0; + const attempts = (await cache.get(email)) || 0; if (attempts >= 3) { return next( createHttpError( @@ -201,7 +201,7 @@ export async function inviteUser( ); } - cache.set(email, attempts + 1); + await cache.set("regenerateInvite:" + email, attempts + 1, 3600); const inviteId = existingInvite[0].inviteId; // Retrieve the original inviteId const token = generateRandomString( diff --git a/server/routers/user/listInvitations.ts b/server/routers/user/listInvitations.ts index 4289b877f..2733c8395 100644 --- a/server/routers/user/listInvitations.ts +++ b/server/routers/user/listInvitations.ts @@ -54,7 +54,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/invitations", description: "List invitations in an organization.", - tags: [OpenAPITags.Org, OpenAPITags.Invitation], + tags: [OpenAPITags.Invitation], request: { params: listInvitationsParamsSchema, query: listInvitationsQuerySchema diff --git a/server/routers/user/listUsers.ts b/server/routers/user/listUsers.ts index aeced75b1..37567f24e 100644 --- a/server/routers/user/listUsers.ts +++ b/server/routers/user/listUsers.ts @@ -94,7 +94,7 @@ registry.registerPath({ method: "get", path: "/org/{orgId}/users", description: "List users in an organization.", - tags: [OpenAPITags.Org, OpenAPITags.User], + tags: [OpenAPITags.User], request: { params: listUsersParamsSchema, query: listUsersSchema diff --git a/server/routers/user/removeInvitation.ts b/server/routers/user/removeInvitation.ts index ab6a96d20..0f76fd30c 100644 --- a/server/routers/user/removeInvitation.ts +++ b/server/routers/user/removeInvitation.ts @@ -19,7 +19,7 @@ registry.registerPath({ method: "delete", path: "/org/{orgId}/invitations/{inviteId}", description: "Remove an open invitation from an organization", - tags: [OpenAPITags.Org], + tags: [OpenAPITags.Invitation], request: { params: removeInvitationParamsSchema }, diff --git a/server/routers/user/removeUserOrg.ts b/server/routers/user/removeUserOrg.ts index 4c321ad38..3c86a03c5 100644 --- a/server/routers/user/removeUserOrg.ts +++ b/server/routers/user/removeUserOrg.ts @@ -33,7 +33,7 @@ registry.registerPath({ method: "delete", path: "/org/{orgId}/user/{userId}", description: "Remove a user from an organization.", - tags: [OpenAPITags.Org, OpenAPITags.User], + tags: [OpenAPITags.User], request: { params: removeUserSchema }, diff --git a/server/routers/user/updateOrgUser.ts b/server/routers/user/updateOrgUser.ts index 97bedb5f9..a95c3fb5e 100644 --- a/server/routers/user/updateOrgUser.ts +++ b/server/routers/user/updateOrgUser.ts @@ -26,7 +26,7 @@ registry.registerPath({ method: "post", path: "/org/{orgId}/user/{userId}", description: "Update a user in an org.", - tags: [OpenAPITags.Org, OpenAPITags.User], + tags: [OpenAPITags.Org], request: { params: paramsSchema, body: { diff --git a/server/routers/ws/messageHandlers.ts b/server/routers/ws/messageHandlers.ts index 9a14344a5..628caafd5 100644 --- a/server/routers/ws/messageHandlers.ts +++ b/server/routers/ws/messageHandlers.ts @@ -1,3 +1,4 @@ +import { build } from "@server/build"; import { handleNewtRegisterMessage, handleReceiveBandwidthMessage, @@ -6,7 +7,9 @@ import { handleDockerContainersMessage, handleNewtPingRequestMessage, handleApplyBlueprintMessage, - handleNewtPingMessage + handleNewtPingMessage, + startNewtOfflineChecker, + handleNewtDisconnectingMessage } from "../newt"; import { handleOlmRegisterMessage, @@ -15,7 +18,8 @@ import { startOlmOfflineChecker, handleOlmServerPeerAddMessage, handleOlmUnRelayMessage, - handleOlmDisconnecingMessage + handleOlmDisconnectingMessage, + handleOlmServerInitAddPeerHandshake } from "../olm"; import { handleHealthcheckStatusMessage } from "../target"; import { handleRoundTripMessage } from "./handleRoundTripMessage"; @@ -23,11 +27,13 @@ import { MessageHandler } from "./types"; export const messageHandlers: Record = { "olm/wg/server/peer/add": handleOlmServerPeerAddMessage, + "olm/wg/server/peer/init": handleOlmServerInitAddPeerHandshake, "olm/wg/register": handleOlmRegisterMessage, "olm/wg/relay": handleOlmRelayMessage, "olm/wg/unrelay": handleOlmUnRelayMessage, "olm/ping": handleOlmPingMessage, - "olm/disconnecting": handleOlmDisconnecingMessage, + "olm/disconnecting": handleOlmDisconnectingMessage, + "newt/disconnecting": handleNewtDisconnectingMessage, "newt/ping": handleNewtPingMessage, "newt/wg/register": handleNewtRegisterMessage, "newt/wg/get-config": handleGetConfigMessage, @@ -40,4 +46,7 @@ export const messageHandlers: Record = { "ws/round-trip/complete": handleRoundTripMessage }; -startOlmOfflineChecker(); // this is to handle the offline check for olms +if (build != "saas") { + startOlmOfflineChecker(); // this is to handle the offline check for olms + startNewtOfflineChecker(); // this is to handle the offline check for newts +} diff --git a/server/routers/ws/types.ts b/server/routers/ws/types.ts index 4be68883e..e539954ce 100644 --- a/server/routers/ws/types.ts +++ b/server/routers/ws/types.ts @@ -24,7 +24,7 @@ export interface AuthenticatedWebSocket extends WebSocket { clientType?: ClientType; connectionId?: string; isFullyConnected?: boolean; - pendingMessages?: Buffer[]; + pendingMessages?: { data: Buffer; isBinary: boolean }[]; configVersion?: number; } @@ -73,6 +73,7 @@ export type MessageHandler = ( // Options for sending messages with config version tracking export interface SendMessageOptions { incrementConfigVersion?: boolean; + compress?: boolean; } // Redis message type for cross-node communication diff --git a/server/routers/ws/ws.ts b/server/routers/ws/ws.ts index 32432d997..08a7dbd4c 100644 --- a/server/routers/ws/ws.ts +++ b/server/routers/ws/ws.ts @@ -1,8 +1,9 @@ import { Router, Request, Response } from "express"; +import zlib from "zlib"; import { Server as HttpServer } from "http"; import { WebSocket, WebSocketServer } from "ws"; import { Socket } from "net"; -import { Newt, newts, NewtSession, olms, Olm, OlmSession } from "@server/db"; +import { Newt, newts, NewtSession, olms, Olm, OlmSession, sites } from "@server/db"; import { eq } from "drizzle-orm"; import { db } from "@server/db"; import { validateNewtSessionToken } from "@server/auth/sessions/newt"; @@ -116,11 +117,20 @@ const sendToClientLocal = async ( }; const messageString = JSON.stringify(messageWithVersion); - clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(messageString); - } - }); + if (options.compress) { + const compressed = zlib.gzipSync(Buffer.from(messageString, "utf8")); + clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(compressed); + } + }); + } else { + clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(messageString); + } + }); + } return true; }; @@ -147,11 +157,22 @@ const broadcastToAllExceptLocal = async ( ...message, configVersion }; - clients.forEach((client) => { - if (client.readyState === WebSocket.OPEN) { - client.send(JSON.stringify(messageWithVersion)); - } - }); + if (options.compress) { + const compressed = zlib.gzipSync( + Buffer.from(JSON.stringify(messageWithVersion), "utf8") + ); + clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(compressed); + } + }); + } else { + clients.forEach((client) => { + if (client.readyState === WebSocket.OPEN) { + client.send(JSON.stringify(messageWithVersion)); + } + }); + } } }); }; @@ -286,9 +307,12 @@ const setupConnection = async ( clientType === "newt" ? (client as Newt).newtId : (client as Olm).olmId; await addClient(clientType, clientId, ws); - ws.on("message", async (data) => { + ws.on("message", async (data, isBinary) => { try { - const message: WSMessage = JSON.parse(data.toString()); + const messageBuffer = isBinary + ? zlib.gunzipSync(data as Buffer) + : (data as Buffer); + const message: WSMessage = JSON.parse(messageBuffer.toString()); if (!message.type || typeof message.type !== "string") { throw new Error( @@ -356,6 +380,31 @@ const setupConnection = async ( ); }); + // Handle WebSocket protocol-level pings from older newt clients that do + // not send application-level "newt/ping" messages. Update the site's + // online state and lastPing timestamp so the offline checker treats them + // the same as modern newt clients. + if (clientType === "newt") { + const newtClient = client as Newt; + ws.on("ping", async () => { + if (!newtClient.siteId) return; + try { + await db + .update(sites) + .set({ + online: true, + lastPing: Math.floor(Date.now() / 1000) + }) + .where(eq(sites.siteId, newtClient.siteId)); + } catch (error) { + logger.error( + "Error updating newt site online state on WS ping", + { error } + ); + } + }); + } + ws.on("error", (error: Error) => { logger.error( `WebSocket error for ${clientType.toUpperCase()} ID ${clientId}:`, diff --git a/server/setup/migrationsPg.ts b/server/setup/migrationsPg.ts index 8d27435aa..1ace73474 100644 --- a/server/setup/migrationsPg.ts +++ b/server/setup/migrationsPg.ts @@ -5,6 +5,7 @@ import semver from "semver"; import { versionMigrations } from "../db/pg"; import { __DIRNAME, APP_VERSION } from "@server/lib/consts"; import path from "path"; +import { build } from "@server/build"; import m1 from "./scriptsPg/1.6.0"; import m2 from "./scriptsPg/1.7.0"; import m3 from "./scriptsPg/1.8.0"; @@ -19,7 +20,7 @@ import m11 from "./scriptsPg/1.14.0"; import m12 from "./scriptsPg/1.15.0"; import m13 from "./scriptsPg/1.15.3"; import m14 from "./scriptsPg/1.15.4"; -import { build } from "@server/build"; +import m15 from "./scriptsPg/1.16.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -39,7 +40,8 @@ const migrations = [ { version: "1.14.0", run: m11 }, { version: "1.15.0", run: m12 }, { version: "1.15.3", run: m13 }, - { version: "1.15.4", run: m14 } + { version: "1.15.4", run: m14 }, + { version: "1.16.0", run: m15 } // Add new migrations here as they are created ] as { version: string; diff --git a/server/setup/migrationsSqlite.ts b/server/setup/migrationsSqlite.ts index 17bc7f199..da7e6b6d1 100644 --- a/server/setup/migrationsSqlite.ts +++ b/server/setup/migrationsSqlite.ts @@ -7,6 +7,7 @@ import { versionMigrations } from "../db/sqlite"; import { __DIRNAME, APP_PATH, APP_VERSION } from "@server/lib/consts"; import { SqliteError } from "better-sqlite3"; import fs from "fs"; +import { build } from "@server/build"; import m1 from "./scriptsSqlite/1.0.0-beta1"; import m2 from "./scriptsSqlite/1.0.0-beta2"; import m3 from "./scriptsSqlite/1.0.0-beta3"; @@ -37,7 +38,7 @@ import m32 from "./scriptsSqlite/1.14.0"; import m33 from "./scriptsSqlite/1.15.0"; import m34 from "./scriptsSqlite/1.15.3"; import m35 from "./scriptsSqlite/1.15.4"; -import { build } from "@server/build"; +import m36 from "./scriptsSqlite/1.16.0"; // THIS CANNOT IMPORT ANYTHING FROM THE SERVER // EXCEPT FOR THE DATABASE AND THE SCHEMA @@ -73,7 +74,8 @@ const migrations = [ { version: "1.14.0", run: m32 }, { version: "1.15.0", run: m33 }, { version: "1.15.3", run: m34 }, - { version: "1.15.4", run: m35 } + { version: "1.15.4", run: m35 }, + { version: "1.16.0", run: m36 } // Add new migrations here as they are created ] as const; diff --git a/server/setup/scriptsPg/1.16.0.ts b/server/setup/scriptsPg/1.16.0.ts new file mode 100644 index 000000000..0bcfdc4a5 --- /dev/null +++ b/server/setup/scriptsPg/1.16.0.ts @@ -0,0 +1,179 @@ +import { db } from "@server/db/pg/driver"; +import { sql } from "drizzle-orm"; +import { configFilePath1, configFilePath2 } from "@server/lib/consts"; +import { encrypt } from "@server/lib/crypto"; +import { generateCA } from "@server/lib/sshCA"; +import fs from "fs"; +import yaml from "js-yaml"; + +const version = "1.16.0"; + +function getServerSecret(): string { + const envSecret = process.env.SERVER_SECRET; + + const configPath = fs.existsSync(configFilePath1) + ? configFilePath1 + : fs.existsSync(configFilePath2) + ? configFilePath2 + : null; + + // If no config file but an env secret is set, use the env secret directly + if (!configPath) { + if (envSecret && envSecret.length > 0) { + return envSecret; + } + + throw new Error( + "Cannot generate org CA keys: no config file found and SERVER_SECRET env var is not set. " + + "Expected config.yml or config.yaml in the config directory, or set SERVER_SECRET." + ); + } + + const configContent = fs.readFileSync(configPath, "utf8"); + const config = yaml.load(configContent) as { + server?: { secret?: string }; + }; + + let secret = config?.server?.secret; + if (!secret || secret.length === 0) { + // Fall back to SERVER_SECRET env var if config does not contain server.secret + if (envSecret && envSecret.length > 0) { + secret = envSecret; + } + } + + if (!secret || secret.length === 0) { + throw new Error( + "Cannot generate org CA keys: no server.secret in config and SERVER_SECRET env var is not set. " + + "Set server.secret in config.yml/config.yaml or set SERVER_SECRET." + ); + } + + return secret; +} + +export default async function migration() { + console.log(`Running setup script ${version}...`); + + // Ensure server secret exists before running migration (required for org CA key generation) + getServerSecret(); + + try { + await db.execute(sql`BEGIN`); + + // Schema changes + await db.execute(sql` + CREATE TABLE "roundTripMessageTracker" ( + "messageId" serial PRIMARY KEY NOT NULL, + "clientId" varchar, + "messageType" varchar, + "sentAt" bigint NOT NULL, + "receivedAt" bigint, + "error" text, + "complete" boolean DEFAULT false NOT NULL + ); + `); + + await db.execute( + sql`ALTER TABLE "orgs" ADD COLUMN "sshCaPrivateKey" text;` + ); + await db.execute( + sql`ALTER TABLE "orgs" ADD COLUMN "sshCaPublicKey" text;` + ); + await db.execute( + sql`ALTER TABLE "orgs" ADD COLUMN "isBillingOrg" boolean;` + ); + await db.execute( + sql`ALTER TABLE "orgs" ADD COLUMN "billingOrgId" varchar;` + ); + + await db.execute( + sql`ALTER TABLE "roles" ADD COLUMN "sshSudoMode" varchar(32) DEFAULT 'none';` + ); + await db.execute( + sql`ALTER TABLE "roles" ADD COLUMN "sshSudoCommands" text DEFAULT '[]';` + ); + await db.execute( + sql`ALTER TABLE "roles" ADD COLUMN "sshCreateHomeDir" boolean DEFAULT true;` + ); + await db.execute( + sql`ALTER TABLE "roles" ADD COLUMN "sshUnixGroups" text DEFAULT '[]';` + ); + + await db.execute( + sql`ALTER TABLE "siteResources" ADD COLUMN "authDaemonPort" integer DEFAULT 22123;` + ); + await db.execute( + sql`ALTER TABLE "siteResources" ADD COLUMN "authDaemonMode" varchar(32) DEFAULT 'site';` + ); + + await db.execute( + sql`ALTER TABLE "userOrgs" ADD COLUMN "pamUsername" varchar;` + ); + + // Set all admin role sudo to "full"; other roles keep default "none" + await db.execute( + sql`UPDATE "roles" SET "sshSudoMode" = 'full' WHERE "isAdmin" = true;` + ); + + await db.execute(sql`COMMIT`); + console.log("Migrated database"); + } catch (e) { + await db.execute(sql`ROLLBACK`); + console.log("Unable to migrate database"); + console.log(e); + throw e; + } + + // Generate and store encrypted SSH CA keys for all orgs + try { + const secret = getServerSecret(); + + const orgQuery = await db.execute(sql`SELECT "orgId" FROM "orgs"`); + const orgRows = orgQuery.rows as { orgId: string }[]; + + const failedOrgIds: string[] = []; + + for (const row of orgRows) { + try { + const ca = generateCA(`pangolin-ssh-ca-${row.orgId}`); + const encryptedPrivateKey = encrypt(ca.privateKeyPem, secret); + + await db.execute(sql` + UPDATE "orgs" + SET "sshCaPrivateKey" = ${encryptedPrivateKey}, + "sshCaPublicKey" = ${ca.publicKeyOpenSSH} + WHERE "orgId" = ${row.orgId}; + `); + } catch (err) { + failedOrgIds.push(row.orgId); + console.error( + `Error: No CA was generated for organization "${row.orgId}".`, + err instanceof Error ? err.message : err + ); + } + } + + if (orgRows.length > 0) { + const succeeded = orgRows.length - failedOrgIds.length; + console.log( + `Generated and stored SSH CA keys for ${succeeded} org(s).` + ); + } + + if (failedOrgIds.length > 0) { + console.error( + `No CA was generated for ${failedOrgIds.length} organization(s): ${failedOrgIds.join( + ", " + )}` + ); + } + } catch (e) { + console.error( + "Error while generating SSH CA keys for orgs after migration:", + e + ); + } + + console.log(`${version} migration complete`); +} diff --git a/server/setup/scriptsSqlite/1.16.0.ts b/server/setup/scriptsSqlite/1.16.0.ts index 459367339..67f87c288 100644 --- a/server/setup/scriptsSqlite/1.16.0.ts +++ b/server/setup/scriptsSqlite/1.16.0.ts @@ -1,23 +1,167 @@ -import { __DIRNAME, APP_PATH } from "@server/lib/consts"; +import { APP_PATH, configFilePath1, configFilePath2 } from "@server/lib/consts"; +import { encrypt } from "@server/lib/crypto"; +import { generateCA } from "@server/lib/sshCA"; import Database from "better-sqlite3"; +import fs from "fs"; import path from "path"; +import yaml from "js-yaml"; const version = "1.16.0"; +function getServerSecret(): string { + const envSecret = process.env.SERVER_SECRET; + + const configPath = fs.existsSync(configFilePath1) + ? configFilePath1 + : fs.existsSync(configFilePath2) + ? configFilePath2 + : null; + + // If no config file but an env secret is set, use the env secret directly + if (!configPath) { + if (envSecret && envSecret.length > 0) { + return envSecret; + } + + throw new Error( + "Cannot generate org CA keys: no config file found and SERVER_SECRET env var is not set. " + + "Expected config.yml or config.yaml in the config directory, or set SERVER_SECRET." + ); + } + + const configContent = fs.readFileSync(configPath, "utf8"); + const config = yaml.load(configContent) as { + server?: { secret?: string }; + }; + + let secret = config?.server?.secret; + if (!secret || secret.length === 0) { + // Fall back to SERVER_SECRET env var if config does not contain server.secret + if (envSecret && envSecret.length > 0) { + secret = envSecret; + } + } + + if (!secret || secret.length === 0) { + throw new Error( + "Cannot generate org CA keys: no server.secret in config and SERVER_SECRET env var is not set. " + + "Set server.secret in config.yml/config.yaml or set SERVER_SECRET." + ); + } + + return secret; +} + export default async function migration() { console.log(`Running setup script ${version}...`); + // Ensure server secret exists before running migration (required for org CA key generation) + getServerSecret(); + const location = path.join(APP_PATH, "db", "db.sqlite"); const db = new Database(location); - // set all admin role sudo to "full"; all other roles to "none" - // all roles set hoemdir to true - - // generate ca certs for all orgs? - // set authDaemonMode to "site" for all site-resources - try { - db.transaction(() => {})(); + db.pragma("foreign_keys = OFF"); + + db.transaction(() => { + // Create roundTripMessageTracker table for tracking message round-trips + db.prepare( + ` + CREATE TABLE 'roundTripMessageTracker' ( + 'messageId' integer PRIMARY KEY AUTOINCREMENT NOT NULL, + 'clientId' text, + 'messageType' text, + 'sentAt' integer NOT NULL, + 'receivedAt' integer, + 'error' text, + 'complete' integer DEFAULT 0 NOT NULL + ); + ` + ).run(); + + // Org SSH CA and billing columns + db.prepare(`ALTER TABLE 'orgs' ADD 'sshCaPrivateKey' text;`).run(); + db.prepare(`ALTER TABLE 'orgs' ADD 'sshCaPublicKey' text;`).run(); + db.prepare(`ALTER TABLE 'orgs' ADD 'isBillingOrg' integer;`).run(); + db.prepare(`ALTER TABLE 'orgs' ADD 'billingOrgId' text;`).run(); + + // Role SSH sudo and unix group columns + db.prepare( + `ALTER TABLE 'roles' ADD 'sshSudoMode' text DEFAULT 'none';` + ).run(); + db.prepare( + `ALTER TABLE 'roles' ADD 'sshSudoCommands' text DEFAULT '[]';` + ).run(); + db.prepare( + `ALTER TABLE 'roles' ADD 'sshCreateHomeDir' integer DEFAULT 1;` + ).run(); + db.prepare( + `ALTER TABLE 'roles' ADD 'sshUnixGroups' text DEFAULT '[]';` + ).run(); + + // Site resource auth daemon columns + db.prepare( + `ALTER TABLE 'siteResources' ADD 'authDaemonPort' integer DEFAULT 22123;` + ).run(); + db.prepare( + `ALTER TABLE 'siteResources' ADD 'authDaemonMode' text DEFAULT 'site';` + ).run(); + + // UserOrg PAM username for SSH + db.prepare(`ALTER TABLE 'userOrgs' ADD 'pamUsername' text;`).run(); + + // Set all admin role sudo to "full"; other roles keep default "none" + db.prepare( + `UPDATE 'roles' SET 'sshSudoMode' = 'full' WHERE isAdmin = 1;` + ).run(); + })(); + + db.pragma("foreign_keys = ON"); + + const orgRows = db.prepare("SELECT orgId FROM orgs").all() as { + orgId: string; + }[]; + + // Generate and store encrypted SSH CA keys for all orgs + const secret = getServerSecret(); + + const updateOrgCaKeys = db.prepare( + "UPDATE orgs SET sshCaPrivateKey = ?, sshCaPublicKey = ? WHERE orgId = ?" + ); + + const failedOrgIds: string[] = []; + + for (const row of orgRows) { + try { + const ca = generateCA(`pangolin-ssh-ca-${row.orgId}`); + const encryptedPrivateKey = encrypt(ca.privateKeyPem, secret); + updateOrgCaKeys.run( + encryptedPrivateKey, + ca.publicKeyOpenSSH, + row.orgId + ); + } catch (err) { + failedOrgIds.push(row.orgId); + console.error( + `Error: No CA was generated for organization "${row.orgId}".`, + err instanceof Error ? err.message : err + ); + } + } + + if (orgRows.length > 0) { + const succeeded = orgRows.length - failedOrgIds.length; + console.log( + `Generated and stored SSH CA keys for ${succeeded} org(s).` + ); + } + + if (failedOrgIds.length > 0) { + console.error( + `No CA was generated for ${failedOrgIds.length} organization(s): ${failedOrgIds.join(", ")}` + ); + } console.log(`Migrated database`); } catch (e) { diff --git a/src/app/[orgId]/settings/(private)/billing/page.tsx b/src/app/[orgId]/settings/(private)/billing/page.tsx index 24fa74805..ba08f6022 100644 --- a/src/app/[orgId]/settings/(private)/billing/page.tsx +++ b/src/app/[orgId]/settings/(private)/billing/page.tsx @@ -35,11 +35,7 @@ import { } from "@app/components/Credenza"; import { cn } from "@app/lib/cn"; import { CreditCard, ExternalLink, Check, AlertTriangle } from "lucide-react"; -import { - Alert, - AlertTitle, - AlertDescription -} from "@app/components/ui/alert"; +import { Alert, AlertTitle, AlertDescription } from "@app/components/ui/alert"; import { Tooltip, TooltipTrigger, @@ -69,6 +65,7 @@ type PlanOption = { price: string; priceDetail?: string; tierType: Tier | null; + features: string[]; }; const planOptions: PlanOption[] = [ @@ -76,41 +73,87 @@ const planOptions: PlanOption[] = [ id: "basic", name: "Basic", price: "Free", - tierType: null + tierType: null, + features: [ + "Basic Pangolin features", + "Free provided domains", + "Web-based proxy resources", + "Private resources and clients", + "Peer-to-peer connections" + ] }, { id: "home", name: "Home", price: "$12.50", priceDetail: "/ month", - tierType: "tier1" + tierType: "tier1", + features: [ + "Everything in Basic", + "OAuth2/OIDC, Google, & Azure SSO", + "Bring your own identity provider", + "Pangolin SSH", + "Custom branding", + "Device admin approvals" + ] }, { id: "team", name: "Team", price: "$4", priceDetail: "per user / month", - tierType: "tier2" + tierType: "tier2", + features: [ + "Everything in Basic", + "Custom domains", + "OAuth2/OIDC, Google, & Azure SSO", + "Access and action audit logs", + "Device posture information" + ] }, { id: "business", name: "Business", price: "$9", priceDetail: "per user / month", - tierType: "tier3" + tierType: "tier3", + features: [ + "Everything in Team", + "Multiple organizations (multi-tenancy)", + "Auto-provisioning via IdP", + "Pangolin SSH", + "Device approvals", + "Custom branding", + "Business support" + ] }, { id: "enterprise", name: "Enterprise", price: "Custom", - tierType: null + tierType: null, + features: [ + "Everything in Business", + "Custom limits", + "Priority support and SLA", + "Log push and export", + "Private and Gov-Cloud deployment options", + "Dedicated, premium relay/exit nodes", + "Pay by invoice " + ] } ]; // Tier limits mapping derived from limit sets const tierLimits: Record< Tier | "basic", - { users: number; sites: number; domains: number; remoteNodes: number; organizations: number } + { + users: number; + sites: number; + domains: number; + remoteNodes: number; + organizations: number; + } > = { basic: { users: freeLimitSet[FeatureId.USERS]?.value ?? 0, @@ -210,7 +253,8 @@ export default function BillingPage() { ({ subscription }) => subscription?.type === "tier1" || subscription?.type === "tier2" || - subscription?.type === "tier3" + subscription?.type === "tier3" || + subscription?.type === "enterprise" ); setTierSubscription(tierSub || null); @@ -439,6 +483,8 @@ export default function BillingPage() { // Get current plan ID from tier const getCurrentPlanId = (): PlanId => { if (!hasSubscription || !currentTier) return "basic"; + // Handle enterprise subscription type directly + if (currentTier === "enterprise") return "enterprise"; const plan = planOptions.find((p) => p.tierType === currentTier); return plan?.id || "basic"; }; @@ -460,31 +506,43 @@ export default function BillingPage() { const isProblematicState = hasProblematicSubscription(); // Get user-friendly subscription status message - const getSubscriptionStatusMessage = (): { title: string; description: string } | null => { + const getSubscriptionStatusMessage = (): { + title: string; + description: string; + } | null => { if (!tierSubscription?.subscription || !isProblematicState) return null; - + const status = tierSubscription.subscription.status; - + switch (status) { case "past_due": return { title: t("billingPastDueTitle") || "Payment Past Due", - description: t("billingPastDueDescription") || "Your payment is past due. Please update your payment method to continue using your current plan features. If not resolved, your subscription will be canceled and you'll be reverted to the free tier." + description: + t("billingPastDueDescription") || + "Your payment is past due. Please update your payment method to continue using your current plan features. If not resolved, your subscription will be canceled and you'll be reverted to the free tier." }; case "unpaid": return { title: t("billingUnpaidTitle") || "Subscription Unpaid", - description: t("billingUnpaidDescription") || "Your subscription is unpaid and you have been reverted to the free tier. Please update your payment method to restore your subscription." + description: + t("billingUnpaidDescription") || + "Your subscription is unpaid and you have been reverted to the free tier. Please update your payment method to restore your subscription." }; case "incomplete": return { title: t("billingIncompleteTitle") || "Payment Incomplete", - description: t("billingIncompleteDescription") || "Your payment is incomplete. Please complete the payment process to activate your subscription." + description: + t("billingIncompleteDescription") || + "Your payment is incomplete. Please complete the payment process to activate your subscription." }; case "incomplete_expired": return { - title: t("billingIncompleteExpiredTitle") || "Payment Expired", - description: t("billingIncompleteExpiredDescription") || "Your payment was never completed and has expired. You have been reverted to the free tier. Please subscribe again to restore access to paid features." + title: + t("billingIncompleteExpiredTitle") || "Payment Expired", + description: + t("billingIncompleteExpiredDescription") || + "Your payment was never completed and has expired. You have been reverted to the free tier. Please subscribe again to restore access to paid features." }; default: return null; @@ -506,7 +564,11 @@ export default function BillingPage() { if (plan.id === currentPlanId) { // If it's the basic plan (basic with no subscription), show as current but disabled - if (plan.id === "basic" && !hasSubscription && !isProblematicState) { + if ( + plan.id === "basic" && + !hasSubscription && + !isProblematicState + ) { return { label: "Current Plan", action: () => {}, @@ -629,7 +691,9 @@ export default function BillingPage() { }; // Check if downgrading to a tier would violate current usage limits - const checkLimitViolations = (targetTier: Tier | "basic"): Array<{ + const checkLimitViolations = ( + targetTier: Tier | "basic" + ): Array<{ feature: string; currentUsage: number; newLimit: number; @@ -684,7 +748,10 @@ export default function BillingPage() { // Check organizations const organizationsUsage = getUsageValue(ORGINIZATIONS); - if (limits.organizations > 0 && organizationsUsage > limits.organizations) { + if ( + limits.organizations > 0 && + organizationsUsage > limits.organizations + ) { violations.push({ feature: "Organizations", currentUsage: organizationsUsage, @@ -709,17 +776,15 @@ export default function BillingPage() { {isProblematicState && statusMessage && ( - - {statusMessage.title} - + {statusMessage.title} - {statusMessage.description} - {" "} + {statusMessage.description}{" "} @@ -769,7 +834,10 @@ export default function BillingPage() {
- {isProblematicState && planAction.disabled && !isCurrentPlan && plan.id !== "enterprise" ? ( + {isProblematicState && + planAction.disabled && + !isCurrentPlan && + plan.id !== "enterprise" ? (
@@ -781,18 +849,29 @@ export default function BillingPage() { } size="sm" className="w-full" - onClick={planAction.action} - disabled={ - isLoading || planAction.disabled + onClick={ + planAction.action + } + disabled={ + isLoading || + planAction.disabled + } + loading={ + isLoading && + isCurrentPlan } - loading={isLoading && isCurrentPlan} > {planAction.label}
-

{t("billingResolvePaymentIssue") || "Please resolve your payment issue before upgrading or downgrading"}

+

+ {t( + "billingResolvePaymentIssue" + ) || + "Please resolve your payment issue before upgrading or downgrading"} +

) : ( @@ -806,9 +885,12 @@ export default function BillingPage() { className="w-full" onClick={planAction.action} disabled={ - isLoading || planAction.disabled + isLoading || + planAction.disabled + } + loading={ + isLoading && isCurrentPlan } - loading={isLoading && isCurrentPlan} > {planAction.label} @@ -883,18 +965,38 @@ export default function BillingPage() { - + {getLimitValue(USERS) ?? - t("billingUnlimited") ?? + t( + "billingUnlimited" + ) ?? "∞"}{" "} - {getLimitValue(USERS) !== null && - "users"} + {getLimitValue( + USERS + ) !== null && "users"} -

{t("billingUsageExceedsLimit", { current: getUsageValue(USERS), limit: getLimitValue(USERS) ?? 0 }) || `Current usage (${getUsageValue(USERS)}) exceeds limit (${getLimitValue(USERS)})`}

+

+ {t( + "billingUsageExceedsLimit", + { + current: + getUsageValue( + USERS + ), + limit: + getLimitValue( + USERS + ) ?? 0 + } + ) || + `Current usage (${getUsageValue(USERS)}) exceeds limit (${getLimitValue(USERS)})`} +

) : ( @@ -902,8 +1004,8 @@ export default function BillingPage() { {getLimitValue(USERS) ?? t("billingUnlimited") ?? "∞"}{" "} - {getLimitValue(USERS) !== null && - "users"} + {getLimitValue(USERS) !== + null && "users"} )} @@ -917,18 +1019,38 @@ export default function BillingPage() { - + {getLimitValue(SITES) ?? - t("billingUnlimited") ?? + t( + "billingUnlimited" + ) ?? "∞"}{" "} - {getLimitValue(SITES) !== null && - "sites"} + {getLimitValue( + SITES + ) !== null && "sites"} -

{t("billingUsageExceedsLimit", { current: getUsageValue(SITES), limit: getLimitValue(SITES) ?? 0 }) || `Current usage (${getUsageValue(SITES)}) exceeds limit (${getLimitValue(SITES)})`}

+

+ {t( + "billingUsageExceedsLimit", + { + current: + getUsageValue( + SITES + ), + limit: + getLimitValue( + SITES + ) ?? 0 + } + ) || + `Current usage (${getUsageValue(SITES)}) exceeds limit (${getLimitValue(SITES)})`} +

) : ( @@ -936,8 +1058,8 @@ export default function BillingPage() { {getLimitValue(SITES) ?? t("billingUnlimited") ?? "∞"}{" "} - {getLimitValue(SITES) !== null && - "sites"} + {getLimitValue(SITES) !== + null && "sites"} )} @@ -951,18 +1073,40 @@ export default function BillingPage() { - - {getLimitValue(DOMAINS) ?? - t("billingUnlimited") ?? + + {getLimitValue( + DOMAINS + ) ?? + t( + "billingUnlimited" + ) ?? "∞"}{" "} - {getLimitValue(DOMAINS) !== null && - "domains"} + {getLimitValue( + DOMAINS + ) !== null && "domains"} -

{t("billingUsageExceedsLimit", { current: getUsageValue(DOMAINS), limit: getLimitValue(DOMAINS) ?? 0 }) || `Current usage (${getUsageValue(DOMAINS)}) exceeds limit (${getLimitValue(DOMAINS)})`}

+

+ {t( + "billingUsageExceedsLimit", + { + current: + getUsageValue( + DOMAINS + ), + limit: + getLimitValue( + DOMAINS + ) ?? 0 + } + ) || + `Current usage (${getUsageValue(DOMAINS)}) exceeds limit (${getLimitValue(DOMAINS)})`} +

) : ( @@ -970,8 +1114,8 @@ export default function BillingPage() { {getLimitValue(DOMAINS) ?? t("billingUnlimited") ?? "∞"}{" "} - {getLimitValue(DOMAINS) !== null && - "domains"} + {getLimitValue(DOMAINS) !== + null && "domains"} )} @@ -986,18 +1130,40 @@ export default function BillingPage() { - - {getLimitValue(ORGINIZATIONS) ?? - t("billingUnlimited") ?? + + {getLimitValue( + ORGINIZATIONS + ) ?? + t( + "billingUnlimited" + ) ?? "∞"}{" "} - {getLimitValue(ORGINIZATIONS) !== - null && "orgs"} + {getLimitValue( + ORGINIZATIONS + ) !== null && "orgs"} -

{t("billingUsageExceedsLimit", { current: getUsageValue(ORGINIZATIONS), limit: getLimitValue(ORGINIZATIONS) ?? 0 }) || `Current usage (${getUsageValue(ORGINIZATIONS)}) exceeds limit (${getLimitValue(ORGINIZATIONS)})`}

+

+ {t( + "billingUsageExceedsLimit", + { + current: + getUsageValue( + ORGINIZATIONS + ), + limit: + getLimitValue( + ORGINIZATIONS + ) ?? 0 + } + ) || + `Current usage (${getUsageValue(ORGINIZATIONS)}) exceeds limit (${getLimitValue(ORGINIZATIONS)})`} +

) : ( @@ -1005,8 +1171,9 @@ export default function BillingPage() { {getLimitValue(ORGINIZATIONS) ?? t("billingUnlimited") ?? "∞"}{" "} - {getLimitValue(ORGINIZATIONS) !== - null && "orgs"} + {getLimitValue( + ORGINIZATIONS + ) !== null && "orgs"} )} @@ -1021,27 +1188,52 @@ export default function BillingPage() { - - {getLimitValue(REMOTE_EXIT_NODES) ?? - t("billingUnlimited") ?? + + {getLimitValue( + REMOTE_EXIT_NODES + ) ?? + t( + "billingUnlimited" + ) ?? "∞"}{" "} - {getLimitValue(REMOTE_EXIT_NODES) !== - null && "nodes"} + {getLimitValue( + REMOTE_EXIT_NODES + ) !== null && "nodes"} -

{t("billingUsageExceedsLimit", { current: getUsageValue(REMOTE_EXIT_NODES), limit: getLimitValue(REMOTE_EXIT_NODES) ?? 0 }) || `Current usage (${getUsageValue(REMOTE_EXIT_NODES)}) exceeds limit (${getLimitValue(REMOTE_EXIT_NODES)})`}

+

+ {t( + "billingUsageExceedsLimit", + { + current: + getUsageValue( + REMOTE_EXIT_NODES + ), + limit: + getLimitValue( + REMOTE_EXIT_NODES + ) ?? 0 + } + ) || + `Current usage (${getUsageValue(REMOTE_EXIT_NODES)}) exceeds limit (${getLimitValue(REMOTE_EXIT_NODES)})`} +

) : ( <> - {getLimitValue(REMOTE_EXIT_NODES) ?? + {getLimitValue( + REMOTE_EXIT_NODES + ) ?? t("billingUnlimited") ?? "∞"}{" "} - {getLimitValue(REMOTE_EXIT_NODES) !== - null && "nodes"} + {getLimitValue( + REMOTE_EXIT_NODES + ) !== null && "nodes"} )} @@ -1069,7 +1261,8 @@ export default function BillingPage() {
- {t("billingCurrentKeys") || "Current Keys"} + {t("billingCurrentKeys") || + "Current Keys"}
@@ -1134,61 +1327,101 @@ export default function BillingPage() {
+ {/* Features with check marks */} + {(() => { + const plan = planOptions.find( + (p) => + p.tierType === pendingTier.tier || + (pendingTier.tier === "basic" && + p.id === "basic") + ); + return plan?.features?.length ? ( +
+

+ {"What's included:"} +

+
+ {plan.features.map( + (feature, i) => ( +
+ + + {feature} + +
+ ) + )} +
+
+ ) : null; + })()} + + {/* Limits without check marks */} {tierLimits[pendingTier.tier] && (

- {t("billingPlanIncludes") || - "Plan Includes:"} + {"Up to:"}

-
+
- + { - tierLimits[pendingTier.tier] - .users + tierLimits[ + pendingTier.tier + ].users }{" "} - {t("billingUsers") || "Users"} + {t("billingUsers") || + "Users"}
- + { - tierLimits[pendingTier.tier] - .sites + tierLimits[ + pendingTier.tier + ].sites }{" "} - {t("billingSites") || "Sites"} + {t("billingSites") || + "Sites"}
- + { - tierLimits[pendingTier.tier] - .domains + tierLimits[ + pendingTier.tier + ].domains }{" "} {t("billingDomains") || "Domains"}
- + { - tierLimits[pendingTier.tier] - .organizations + tierLimits[ + pendingTier.tier + ].organizations }{" "} - {t("billingOrganizations") || - "Organizations"} + {t( + "billingOrganizations" + ) || "Organizations"}
- + { - tierLimits[pendingTier.tier] - .remoteNodes + tierLimits[ + pendingTier.tier + ].remoteNodes }{" "} {t("billingRemoteNodes") || "Remote Nodes"} @@ -1199,43 +1432,84 @@ export default function BillingPage() { )} {/* Warning for limit violations when downgrading */} - {pendingTier.action === "downgrade" && (() => { - const violations = checkLimitViolations(pendingTier.tier); - if (violations.length > 0) { - return ( - - - - {t("billingLimitViolationWarning") || "Usage Exceeds New Plan Limits"} - - -

- {t("billingLimitViolationDescription") || "Your current usage exceeds the limits of this plan. The following features will be disabled until you reduce usage:"} -

-
    - {violations.map((violation, index) => ( -
  • - {violation.feature}: - Currently using {violation.currentUsage}, new limit is {violation.newLimit} -
  • - ))} -
-
-
+ {pendingTier.action === "downgrade" && + (() => { + const violations = checkLimitViolations( + pendingTier.tier ); - } - return null; - })()} + if (violations.length > 0) { + return ( + + + + {t( + "billingLimitViolationWarning" + ) || + "Usage Exceeds New Plan Limits"} + + +

+ {t( + "billingLimitViolationDescription" + ) || + "Your current usage exceeds the limits of this plan. The following features will be disabled until you reduce usage:"} +

+
    + {violations.map( + ( + violation, + index + ) => ( +
  • + + { + violation.feature + } + : + + + Currently + using{" "} + { + violation.currentUsage + } + , + new + limit + is{" "} + { + violation.newLimit + } + +
  • + ) + )} +
+
+
+ ); + } + return null; + })()} {/* Warning for feature loss when downgrading */} {pendingTier.action === "downgrade" && ( - {t("billingFeatureLossWarning") || "Feature Availability Notice"} + {t("billingFeatureLossWarning") || + "Feature Availability Notice"} - {t("billingFeatureLossDescription") || "By downgrading, features not available in the new plan will be automatically disabled. Some settings and configurations may be lost. Please review the pricing matrix to understand which features will no longer be available."} + {t( + "billingFeatureLossDescription" + ) || + "By downgrading, features not available in the new plan will be automatically disabled. Some settings and configurations may be lost. Please review the pricing matrix to understand which features will no longer be available."} )} diff --git a/src/app/[orgId]/settings/domains/[domainId]/page.tsx b/src/app/[orgId]/settings/domains/[domainId]/page.tsx index 39ad02db2..cf23e81be 100644 --- a/src/app/[orgId]/settings/domains/[domainId]/page.tsx +++ b/src/app/[orgId]/settings/domains/[domainId]/page.tsx @@ -69,6 +69,7 @@ export default async function DomainSettingsPage({ failed={domain.failed} verified={domain.verified} type={domain.type} + errorMessage={domain.errorMessage} /> diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx index b00ce1eeb..a533fb6c3 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/authentication/page.tsx @@ -187,7 +187,11 @@ export default function ResourceAuthenticationPage() { number | null >(null); - const [ssoEnabled, setSsoEnabled] = useState(resource.sso); + const [ssoEnabled, setSsoEnabled] = useState(resource.sso ?? false); + + useEffect(() => { + setSsoEnabled(resource.sso ?? false); + }, [resource.sso]); const [selectedIdpId, setSelectedIdpId] = useState( resource.skipToIdpId || null @@ -472,7 +476,7 @@ export default function ResourceAuthenticationPage() { setSsoEnabled(val)} /> @@ -800,8 +804,13 @@ function OneTimePasswordFormSection({ }: OneTimePasswordFormSectionProps) { const { env } = useEnvContext(); const [whitelistEnabled, setWhitelistEnabled] = useState( - resource.emailWhitelistEnabled + resource.emailWhitelistEnabled ?? false ); + + useEffect(() => { + setWhitelistEnabled(resource.emailWhitelistEnabled); + }, [resource.emailWhitelistEnabled]); + const queryClient = useQueryClient(); const [loadingSaveWhitelist, startTransition] = useTransition(); @@ -894,7 +903,7 @@ function OneTimePasswordFormSection({ diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx index e7e64ae98..51f11a2c3 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/proxy/page.tsx @@ -89,7 +89,14 @@ import { } from "lucide-react"; import { useTranslations } from "next-intl"; import { useRouter } from "next/navigation"; -import { use, useActionState, useCallback, useEffect, useMemo, useState } from "react"; +import { + use, + useActionState, + useCallback, + useEffect, + useMemo, + useState +} from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -184,29 +191,35 @@ function ProxyResourceTargetsForm({ setDockerStates((prev) => new Map(prev.set(siteId, dockerState))); }; - const refreshContainersForSite = useCallback(async (siteId: number) => { - const dockerManager = new DockerManager(api, siteId); - const containers = await dockerManager.fetchContainers(); + const refreshContainersForSite = useCallback( + async (siteId: number) => { + const dockerManager = new DockerManager(api, siteId); + const containers = await dockerManager.fetchContainers(); - setDockerStates((prev) => { - const newMap = new Map(prev); - const existingState = newMap.get(siteId); - if (existingState) { - newMap.set(siteId, { ...existingState, containers }); - } - return newMap; - }); - }, [api]); + setDockerStates((prev) => { + const newMap = new Map(prev); + const existingState = newMap.get(siteId); + if (existingState) { + newMap.set(siteId, { ...existingState, containers }); + } + return newMap; + }); + }, + [api] + ); - const getDockerStateForSite = useCallback((siteId: number): DockerState => { - return ( - dockerStates.get(siteId) || { - isEnabled: false, - isAvailable: false, - containers: [] - } - ); - }, [dockerStates]); + const getDockerStateForSite = useCallback( + (siteId: number): DockerState => { + return ( + dockerStates.get(siteId) || { + isEnabled: false, + isAvailable: false, + containers: [] + } + ); + }, + [dockerStates] + ); const [isAdvancedMode, setIsAdvancedMode] = useState(() => { if (typeof window !== "undefined") { @@ -220,7 +233,9 @@ function ProxyResourceTargetsForm({ const removeTarget = useCallback((targetId: number) => { setTargets((prevTargets) => { - const targetToRemove = prevTargets.find((target) => target.targetId === targetId); + const targetToRemove = prevTargets.find( + (target) => target.targetId === targetId + ); if (targetToRemove && !targetToRemove.new) { setTargetsToRemove((prev) => [...prev, targetId]); } @@ -228,21 +243,24 @@ function ProxyResourceTargetsForm({ }); }, []); - const updateTarget = useCallback((targetId: number, data: Partial) => { - setTargets((prevTargets) => { - const site = sites.find((site) => site.siteId === data.siteId); - return prevTargets.map((target) => - target.targetId === targetId - ? { - ...target, - ...data, - updated: true, - siteType: site ? site.type : target.siteType - } - : target - ); - }); - }, [sites]); + const updateTarget = useCallback( + (targetId: number, data: Partial) => { + setTargets((prevTargets) => { + const site = sites.find((site) => site.siteId === data.siteId); + return prevTargets.map((target) => + target.targetId === targetId + ? { + ...target, + ...data, + updated: true, + siteType: site ? site.type : target.siteType + } + : target + ); + }); + }, + [sites] + ); const openHealthCheckDialog = useCallback((target: LocalTarget) => { setSelectedTargetForHealthCheck(target); @@ -250,7 +268,6 @@ function ProxyResourceTargetsForm({ }, []); const columns = useMemo((): ColumnDef[] => { - const priorityColumn: ColumnDef = { id: "priority", header: () => ( @@ -581,7 +598,17 @@ function ProxyResourceTargetsForm({ actionsColumn ]; } - }, [isAdvancedMode, isHttp, sites, updateTarget, getDockerStateForSite, refreshContainersForSite, openHealthCheckDialog, removeTarget, t]); + }, [ + isAdvancedMode, + isHttp, + sites, + updateTarget, + getDockerStateForSite, + refreshContainersForSite, + openHealthCheckDialog, + removeTarget, + t + ]); function addNewTarget() { const isHttp = resource.http; diff --git a/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx b/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx index f0ac0e1a7..2b6a16870 100644 --- a/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/[niceId]/rules/page.tsx @@ -113,7 +113,12 @@ export default function ResourceRules(props: { const [rulesToRemove, setRulesToRemove] = useState([]); const [loading, setLoading] = useState(false); const [pageLoading, setPageLoading] = useState(true); - const [rulesEnabled, setRulesEnabled] = useState(resource.applyRules); + const [rulesEnabled, setRulesEnabled] = useState(resource.applyRules ?? false); + + useEffect(() => { + setRulesEnabled(resource.applyRules); + }, [resource.applyRules]); + const [openCountrySelect, setOpenCountrySelect] = useState(false); const [countrySelectValue, setCountrySelectValue] = useState(""); const [openAddRuleCountrySelect, setOpenAddRuleCountrySelect] = @@ -836,7 +841,7 @@ export default function ResourceRules(props: { setRulesEnabled(val)} />
diff --git a/src/app/[orgId]/settings/resources/proxy/create/page.tsx b/src/app/[orgId]/settings/resources/proxy/create/page.tsx index 47f24cd9b..fbc916479 100644 --- a/src/app/[orgId]/settings/resources/proxy/create/page.tsx +++ b/src/app/[orgId]/settings/resources/proxy/create/page.tsx @@ -54,6 +54,7 @@ import { TooltipProvider, TooltipTrigger } from "@app/components/ui/tooltip"; +import { Alert, AlertDescription, AlertTitle } from "@app/components/ui/alert"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; @@ -61,9 +62,11 @@ import { DockerManager, DockerState } from "@app/lib/docker"; import { orgQueries } from "@app/lib/queries"; import { finalizeSubdomainSanitize } from "@app/lib/subdomain-utils"; import { zodResolver } from "@hookform/resolvers/zod"; +import { build } from "@server/build"; import { Resource } from "@server/db"; import { isTargetValid } from "@server/lib/validators"; import { ListTargetsResponse } from "@server/routers/target"; +import { ListRemoteExitNodesResponse } from "@server/routers/remoteExitNode/types"; import { ArrayElement } from "@server/types/ArrayElement"; import { useQuery } from "@tanstack/react-query"; import { @@ -80,6 +83,7 @@ import { CircleCheck, CircleX, Info, + InfoIcon, Plus, Settings, SquareArrowOutUpRight @@ -209,6 +213,13 @@ export default function Page() { orgQueries.sites({ orgId: orgId as string }) ); + const [remoteExitNodes, setRemoteExitNodes] = useState< + ListRemoteExitNodesResponse["remoteExitNodes"] + >([]); + const [loadingExitNodes, setLoadingExitNodes] = useState( + build === "saas" + ); + const [createLoading, setCreateLoading] = useState(false); const [showSnippets, setShowSnippets] = useState(false); const [niceId, setNiceId] = useState(""); @@ -223,6 +234,27 @@ export default function Page() { useState(null); const [healthCheckDialogOpen, setHealthCheckDialogOpen] = useState(false); + useEffect(() => { + if (build !== "saas") return; + + const fetchExitNodes = async () => { + try { + const res = await api.get< + AxiosResponse + >(`/org/${orgId}/remote-exit-nodes`); + if (res && res.status === 200) { + setRemoteExitNodes(res.data.data.remoteExitNodes); + } + } catch (e) { + console.error("Failed to fetch remote exit nodes:", e); + } finally { + setLoadingExitNodes(false); + } + }; + + fetchExitNodes(); + }, [orgId]); + const [isAdvancedMode, setIsAdvancedMode] = useState(() => { if (typeof window !== "undefined") { const saved = localStorage.getItem("create-advanced-mode"); @@ -288,15 +320,25 @@ export default function Page() { }, ...(!env.flags.allowRawResources ? [] - : [ - { - id: "raw" as ResourceType, - title: t("resourceRaw"), - description: t("resourceRawDescription") - } - ]) + : build === "saas" && remoteExitNodes.length === 0 + ? [] + : [ + { + id: "raw" as ResourceType, + title: t("resourceRaw"), + description: + build == "saas" + ? t("resourceRawDescriptionCloud") + : t("resourceRawDescription") + } + ]) ]; + // In saas mode with no exit nodes, force HTTP + const showTypeSelector = + build !== "saas" || + (!loadingExitNodes && remoteExitNodes.length > 0); + const baseForm = useForm({ resolver: zodResolver(baseResourceFormSchema), defaultValues: { @@ -558,7 +600,7 @@ export default function Page() { toast({ variant: "destructive", title: t("resourceErrorCreate"), - description: t("resourceErrorCreateMessageDescription") + description: formatAxiosError(e, t("resourceErrorCreateMessageDescription")) }); } @@ -983,34 +1025,35 @@ export default function Page() { - {resourceTypes.length > 1 && ( - <> -
- - {t("type")} - -
+ {showTypeSelector && + resourceTypes.length > 1 && ( + <> +
+ + {t("type")} + +
- { - baseForm.setValue( - "http", - value === "http" - ); - // Update method default when switching resource type - addTargetForm.setValue( - "method", - value === "http" - ? "http" - : null - ); - }} - cols={2} - /> - - )} + { + baseForm.setValue( + "http", + value === "http" + ); + // Update method default when switching resource type + addTargetForm.setValue( + "method", + value === "http" + ? "http" + : null + ); + }} + cols={2} + /> + + )}
@@ -1066,6 +1109,9 @@ export default function Page() { = 1 + } onDomainChange={(res) => { if (!res) return; diff --git a/src/app/auth/login/device/page.tsx b/src/app/auth/login/device/page.tsx index 7d2ed4e30..01c23c999 100644 --- a/src/app/auth/login/device/page.tsx +++ b/src/app/auth/login/device/page.tsx @@ -3,11 +3,12 @@ import { redirect } from "next/navigation"; import DeviceLoginForm from "@/components/DeviceLoginForm"; import { getUserDisplayName } from "@app/lib/getUserDisplayName"; import { cache } from "react"; +import { cleanRedirect } from "@app/lib/cleanRedirect"; export const dynamic = "force-dynamic"; type Props = { - searchParams: Promise<{ code?: string; user?: string }>; + searchParams: Promise<{ code?: string; user?: string; authPath?: string }>; }; function deviceRedirectSearchParams(params: { @@ -30,11 +31,11 @@ export default async function DeviceLoginPage({ searchParams }: Props) { if (!user) { const redirectDestination = `/auth/login/device${deviceRedirectSearchParams({ code, user: params.user })}`; - const loginUrl = new URL("/auth/login", "http://x"); + const authPath = cleanRedirect(params.authPath || "/auth/login"); + const loginUrl = new URL(authPath, "http://x"); loginUrl.searchParams.set("forceLogin", "true"); loginUrl.searchParams.set("redirect", redirectDestination); if (defaultUser) loginUrl.searchParams.set("user", defaultUser); - console.log("loginUrl", loginUrl.pathname + loginUrl.search); redirect(loginUrl.pathname + loginUrl.search); } diff --git a/src/app/navigation.tsx b/src/app/navigation.tsx index 915e5f04a..0066721db 100644 --- a/src/app/navigation.tsx +++ b/src/app/navigation.tsx @@ -107,7 +107,7 @@ export const orgNavSections = ( ] }, { - heading: "access", + heading: "accessControl", items: [ { title: "sidebarTeam", diff --git a/src/components/ClientResourcesTable.tsx b/src/components/ClientResourcesTable.tsx index 68c72b9ed..5066f273d 100644 --- a/src/components/ClientResourcesTable.tsx +++ b/src/components/ClientResourcesTable.tsx @@ -15,7 +15,15 @@ import { InfoPopup } from "@app/components/ui/info-popup"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; -import { ArrowUpDown, ArrowUpRight, MoreHorizontal } from "lucide-react"; +import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; +import { + ArrowDown01Icon, + ArrowUp10Icon, + ArrowUpDown, + ArrowUpRight, + ChevronsUpDownIcon, + MoreHorizontal +} from "lucide-react"; import { useTranslations } from "next-intl"; import Link from "next/link"; import { useRouter } from "next/navigation"; @@ -133,7 +141,26 @@ export default function ClientResourcesTable({ accessorKey: "name", enableHiding: false, friendlyName: t("name"), - header: () => {t("name")} + header: () => { + const nameOrder = getSortDirection("name", searchParams); + const Icon = + nameOrder === "asc" + ? ArrowDown01Icon + : nameOrder === "desc" + ? ArrowUp10Icon + : ChevronsUpDownIcon; + + return ( + + ); + } }, { id: "niceId", @@ -329,6 +356,14 @@ export default function ClientResourcesTable({ }); } + function toggleSort(column: string) { + const newSearch = getNextSortOrder(column, searchParams); + + filter({ + searchParams: newSearch + }); + } + const handlePaginationChange = (newPage: PaginationState) => { searchParams.set("page", (newPage.pageIndex + 1).toString()); searchParams.set("pageSize", newPage.pageSize.toString()); diff --git a/src/components/ContainersSelector.tsx b/src/components/ContainersSelector.tsx index 5a0f49f99..d4ba6c437 100644 --- a/src/components/ContainersSelector.tsx +++ b/src/components/ContainersSelector.tsx @@ -171,8 +171,7 @@ const DockerContainersTable: FC<{ ...Object.values(container.networks) .map((n) => n.ipAddress) .filter(Boolean), - ...getExposedPorts(container).map((p) => p.toString()), - ...Object.entries(container.labels).flat() + ...getExposedPorts(container).map((p) => p.toString()) ]; return searchableFields.some((field) => diff --git a/src/components/CopyToClipboard.tsx b/src/components/CopyToClipboard.tsx index b755f9a59..dca14728c 100644 --- a/src/components/CopyToClipboard.tsx +++ b/src/components/CopyToClipboard.tsx @@ -31,6 +31,18 @@ const CopyToClipboard = ({ return (
+ {isLink ? ( )} -
); }; diff --git a/src/components/CreateShareLinkForm.tsx b/src/components/CreateShareLinkForm.tsx index 361bfe7d0..2f6f9aff2 100644 --- a/src/components/CreateShareLinkForm.tsx +++ b/src/components/CreateShareLinkForm.tsx @@ -20,7 +20,7 @@ import { import { toast } from "@app/hooks/useToast"; import { zodResolver } from "@hookform/resolvers/zod"; import { AxiosResponse } from "axios"; -import { useEffect, useState } from "react"; +import { useState, useMemo } from "react"; import { useForm } from "react-hook-form"; import { z } from "zod"; import CopyTextBox from "@app/components/CopyTextBox"; @@ -39,7 +39,8 @@ import { formatAxiosError } from "@app/lib/api"; import { cn } from "@app/lib/cn"; import { createApiClient } from "@app/lib/api"; import { useEnvContext } from "@app/hooks/useEnvContext"; -import { ListResourcesResponse } from "@server/routers/resource"; +import { useQuery } from "@tanstack/react-query"; +import { orgQueries } from "@app/lib/queries"; import { Popover, PopoverContent, @@ -94,14 +95,22 @@ export default function CreateShareLinkForm({ const [isOpen, setIsOpen] = useState(false); const t = useTranslations(); - const [resources, setResources] = useState< - { - resourceId: number; - name: string; - niceId: string; - resourceUrl: string; - }[] - >([]); + const { data: allResources = [] } = useQuery( + orgQueries.resources({ orgId: org?.org.orgId ?? "" }) + ); + + const resources = useMemo( + () => + allResources + .filter((r) => r.http) + .map((r) => ({ + resourceId: r.resourceId, + name: r.name, + niceId: r.niceId, + resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/` + })), + [allResources] + ); const formSchema = z.object({ resourceId: z.number({ message: t("shareErrorSelectResource") }), @@ -130,47 +139,6 @@ export default function CreateShareLinkForm({ } }); - useEffect(() => { - if (!open) { - return; - } - - async function fetchResources() { - const res = await api - .get< - AxiosResponse - >(`/org/${org?.org.orgId}/resources`) - .catch((e) => { - console.error(e); - toast({ - variant: "destructive", - title: t("shareErrorFetchResource"), - description: formatAxiosError( - e, - t("shareErrorFetchResourceDescription") - ) - }); - }); - - if (res?.status === 200) { - setResources( - res.data.data.resources - .filter((r) => { - return r.http; - }) - .map((r) => ({ - resourceId: r.resourceId, - name: r.name, - niceId: r.niceId, - resourceUrl: `${r.ssl ? "https://" : "http://"}${toUnicode(r.fullDomain || "")}/` - })) - ); - } - } - - fetchResources(); - }, [open]); - async function onSubmit(values: z.infer) { setLoading(true); diff --git a/src/components/Credenza.tsx b/src/components/Credenza.tsx index 63b847462..63b98c671 100644 --- a/src/components/Credenza.tsx +++ b/src/components/Credenza.tsx @@ -84,7 +84,7 @@ const CredenzaContent = ({ className, children, ...props }: CredenzaProps) => { return ( @@ -79,5 +83,19 @@ export default function DomainInfoCard({ + {errorMessage && (failed || !verified) && ( + + + + {failed + ? t("domainErrorTitle", { fallback: "Domain Error" }) + : t("domainPendingErrorTitle", { fallback: "Verification Issue" })} + + + {errorMessage} + + + )} +
); } diff --git a/src/components/DomainPicker.tsx b/src/components/DomainPicker.tsx index 82861d798..44446763b 100644 --- a/src/components/DomainPicker.tsx +++ b/src/components/DomainPicker.tsx @@ -79,6 +79,7 @@ interface DomainPickerProps { defaultFullDomain?: string | null; defaultSubdomain?: string | null; defaultDomainId?: string | null; + warnOnProvidedDomain?: boolean; } export default function DomainPicker({ @@ -88,7 +89,8 @@ export default function DomainPicker({ hideFreeDomain = false, defaultSubdomain, defaultFullDomain, - defaultDomainId + defaultDomainId, + warnOnProvidedDomain = false }: DomainPickerProps) { const { env } = useEnvContext(); const api = createApiClient({ env }); @@ -689,6 +691,14 @@ export default function DomainPicker({ {showProvidedDomainSearch && (
+ {warnOnProvidedDomain && ( + + + + {t("domainPickerRemoteExitNodeWarning")} + + + )} {isChecking && (
diff --git a/src/components/DomainsTable.tsx b/src/components/DomainsTable.tsx index ff23df67b..f5cb1ae74 100644 --- a/src/components/DomainsTable.tsx +++ b/src/components/DomainsTable.tsx @@ -27,6 +27,12 @@ import { DropdownMenuItem, DropdownMenuTrigger } from "./ui/dropdown-menu"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger +} from "./ui/tooltip"; import Link from "next/link"; export type DomainRow = { @@ -39,6 +45,7 @@ export type DomainRow = { configManaged: boolean; certResolver: string; preferWildcardCert: boolean; + errorMessage?: string | null; }; type Props = { @@ -175,7 +182,7 @@ export default function DomainsTable({ domains, orgId }: Props) { ); }, cell: ({ row }) => { - const { verified, failed, type } = row.original; + const { verified, failed, type, errorMessage } = row.original; if (verified) { return type == "wildcard" ? ( {t("manual")} @@ -183,12 +190,44 @@ export default function DomainsTable({ domains, orgId }: Props) { {t("verified")} ); } else if (failed) { + if (errorMessage) { + return ( + + + + + {t("failed", { fallback: "Failed" })} + + + +

{errorMessage}

+
+
+
+ ); + } return ( {t("failed", { fallback: "Failed" })} ); } else { + if (errorMessage) { + return ( + + + + + {t("pending")} + + + +

{errorMessage}

+
+
+
+ ); + } return {t("pending")}; } } diff --git a/src/components/InternalResourceForm.tsx b/src/components/InternalResourceForm.tsx index 08eb5a24d..6df1aceb7 100644 --- a/src/components/InternalResourceForm.tsx +++ b/src/components/InternalResourceForm.tsx @@ -640,7 +640,7 @@ export function InternalResourceForm({ title: t("editInternalResourceDialogAccessPolicy"), href: "#" }, - ...(disableEnterpriseFeatures + ...(disableEnterpriseFeatures || mode === "cidr" ? [] : [{ title: t("sshAccess"), href: "#" }]) ]} @@ -1188,138 +1188,152 @@ export function InternalResourceForm({
{/* SSH Access tab */} - {!disableEnterpriseFeatures && ( -
- -
- -
- {t.rich( - "internalResourceAuthDaemonDescription", - { - docsLink: (chunks) => ( - - {chunks} - - - ) - } - )} + {!disableEnterpriseFeatures && mode !== "cidr" && ( +
+ +
+ +
+ {t.rich( + "internalResourceAuthDaemonDescription", + { + docsLink: (chunks) => ( + + {chunks} + + + ) + } + )} +
-
-
- ( - - - {t( - "internalResourceAuthDaemonStrategyLabel" - )} - - - - value={field.value ?? undefined} - options={[ - { - id: "site", - title: t( - "internalResourceAuthDaemonSite" - ), - description: t( - "internalResourceAuthDaemonSiteDescription" - ), - disabled: sshSectionDisabled - }, - { - id: "remote", - title: t( - "internalResourceAuthDaemonRemote" - ), - description: t( - "internalResourceAuthDaemonRemoteDescription" - ), - disabled: sshSectionDisabled - } - ]} - onChange={(v) => { - if (sshSectionDisabled) return; - field.onChange(v); - if (v === "site") { - form.setValue( - "authDaemonPort", - null - ); - } - }} - cols={2} - /> - - - - )} - /> - {authDaemonMode === "remote" && ( +
( {t( - "internalResourceAuthDaemonPort" + "internalResourceAuthDaemonStrategyLabel" )} - { - if (sshSectionDisabled) return; - const v = - e.target.value; - if (v === "") { - field.onChange( + + value={ + field.value ?? undefined + } + options={[ + { + id: "site", + title: t( + "internalResourceAuthDaemonSite" + ), + description: t( + "internalResourceAuthDaemonSiteDescription" + ), + disabled: + sshSectionDisabled + }, + { + id: "remote", + title: t( + "internalResourceAuthDaemonRemote" + ), + description: t( + "internalResourceAuthDaemonRemoteDescription" + ), + disabled: + sshSectionDisabled + } + ]} + onChange={(v) => { + if (sshSectionDisabled) + return; + field.onChange(v); + if (v === "site") { + form.setValue( + "authDaemonPort", null ); - return; } - const num = parseInt( - v, - 10 - ); - field.onChange( - Number.isNaN(num) - ? null - : num - ); }} + cols={2} /> )} /> - )} + {authDaemonMode === "remote" && ( + ( + + + {t( + "internalResourceAuthDaemonPort" + )} + + + { + if ( + sshSectionDisabled + ) + return; + const v = + e.target.value; + if (v === "") { + field.onChange( + null + ); + return; + } + const num = + parseInt(v, 10); + field.onChange( + Number.isNaN( + num + ) + ? null + : num + ); + }} + /> + + + + )} + /> + )} +
-
)} diff --git a/src/components/LayoutMobileMenu.tsx b/src/components/LayoutMobileMenu.tsx index b661d780a..e1c883a2b 100644 --- a/src/components/LayoutMobileMenu.tsx +++ b/src/components/LayoutMobileMenu.tsx @@ -69,15 +69,16 @@ export function LayoutMobileMenu({ {t("navbarDescription")} -
-
+
+
-
-
+
+
+
{!isAdminPage && user.serverAdmin && (
diff --git a/src/components/MachineClientsTable.tsx b/src/components/MachineClientsTable.tsx index 97de41130..bd5a8e00d 100644 --- a/src/components/MachineClientsTable.tsx +++ b/src/components/MachineClientsTable.tsx @@ -204,7 +204,26 @@ export default function MachineClientsTable({ accessorKey: "name", enableHiding: false, friendlyName: t("name"), - header: () => {t("name")}, + header: () => { + const nameOrder = getSortDirection("name", searchParams); + const Icon = + nameOrder === "asc" + ? ArrowDown01Icon + : nameOrder === "desc" + ? ArrowUp10Icon + : ChevronsUpDownIcon; + + return ( + + ); + }, cell: ({ row }) => { const r = row.original; return ( diff --git a/src/components/MemberResourcesPortal.tsx b/src/components/MemberResourcesPortal.tsx index 93456b126..8ce721c88 100644 --- a/src/components/MemberResourcesPortal.tsx +++ b/src/components/MemberResourcesPortal.tsx @@ -129,6 +129,11 @@ const ResourceInfo = ({ resource }: { resource: Resource }) => { resource.pincode || resource.whitelist; + const hasAnyInfo = + Boolean(resource.siteName) || Boolean(hasAuthMethods) || !resource.enabled; + + if (!hasAnyInfo) return null; + const infoContent = (
{/* Site Information */} @@ -828,6 +833,12 @@ export default function MemberResourcesPortal({
)} +
+ Destination: + + {siteResource.destination} + +
{siteResource.alias && (
Alias: @@ -836,14 +847,6 @@ export default function MemberResourcesPortal({
)} - {siteResource.aliasAddress && ( -
- Alias Address: - - {siteResource.aliasAddress} - -
- )}
Status: diff --git a/src/components/OrgSelector.tsx b/src/components/OrgSelector.tsx index db43b1e65..fcbc700a2 100644 --- a/src/components/OrgSelector.tsx +++ b/src/components/OrgSelector.tsx @@ -29,6 +29,7 @@ import { usePathname, useRouter } from "next/navigation"; import { useMemo, useState } from "react"; import { useUserContext } from "@app/hooks/useUserContext"; import { useTranslations } from "next-intl"; +import { build } from "@server/build"; interface OrgSelectorProps { orgId?: string; @@ -50,6 +51,11 @@ export function OrgSelector({ const selectedOrg = orgs?.find((org) => org.orgId === orgId); + let canCreateOrg = !env.flags.disableUserCreateOrg || user.serverAdmin; + if (build === "saas" && user.type !== "internal") { + canCreateOrg = false; + } + const sortedOrgs = useMemo(() => { if (!orgs?.length) return orgs ?? []; return [...orgs].sort((a, b) => { @@ -161,7 +167,7 @@ export function OrgSelector({ - {(!env.flags.disableUserCreateOrg || user.serverAdmin) && ( + {canCreateOrg && (
@@ -174,7 +193,8 @@ export function PaidFeaturesAlert({ tiers }: Props) { {t.rich("ossEnterpriseEditionRequired", { enterpriseEditionLink: enterpriseDocsLinkRenderer, - pangolinCloudLink: pangolinCloudLinkRenderer + pangolinCloudLink: pangolinCloudLinkRenderer, + bookADemoLink: bookADemoLinkRenderer })}
diff --git a/src/components/PermissionsSelectBox.tsx b/src/components/PermissionsSelectBox.tsx index b11c635a6..1f7c5279d 100644 --- a/src/components/PermissionsSelectBox.tsx +++ b/src/components/PermissionsSelectBox.tsx @@ -26,12 +26,12 @@ function getActionsCategories(root: boolean) { [t("actionGetOrg")]: "getOrg", [t("actionUpdateOrg")]: "updateOrg", [t("actionGetOrgUser")]: "getOrgUser", + [t("actionResetSiteBandwidth")]: "resetSiteBandwidth", [t("actionInviteUser")]: "inviteUser", [t("actionRemoveInvitation")]: "removeInvitation", [t("actionListInvitations")]: "listInvitations", [t("actionRemoveUser")]: "removeUser", [t("actionListUsers")]: "listUsers", - [t("actionListOrgDomains")]: "listOrgDomains", [t("updateOrgUser")]: "updateOrgUser", [t("createOrgUser")]: "createOrgUser", [t("actionApplyBlueprint")]: "applyBlueprint", @@ -39,6 +39,16 @@ function getActionsCategories(root: boolean) { [t("actionGetBlueprint")]: "getBlueprint" }, + Domain: { + [t("actionListOrgDomains")]: "listOrgDomains", + [t("actionGetDomain")]: "getDomain", + [t("actionCreateOrgDomain")]: "createOrgDomain", + [t("actionUpdateOrgDomain")]: "updateOrgDomain", + [t("actionDeleteOrgDomain")]: "deleteOrgDomain", + [t("actionGetDNSRecords")]: "getDNSRecords", + [t("actionRestartOrgDomain")]: "restartOrgDomain" + }, + Site: { [t("actionCreateSite")]: "createSite", [t("actionDeleteSite")]: "deleteSite", diff --git a/src/components/ProxyResourcesTable.tsx b/src/components/ProxyResourcesTable.tsx index 490904c71..353eddb50 100644 --- a/src/components/ProxyResourcesTable.tsx +++ b/src/components/ProxyResourcesTable.tsx @@ -14,15 +14,19 @@ import { InfoPopup } from "@app/components/ui/info-popup"; import { Switch } from "@app/components/ui/switch"; import { useEnvContext } from "@app/hooks/useEnvContext"; import { useNavigationContext } from "@app/hooks/useNavigationContext"; +import { getNextSortOrder, getSortDirection } from "@app/lib/sortColumn"; import { toast } from "@app/hooks/useToast"; import { createApiClient, formatAxiosError } from "@app/lib/api"; import { UpdateResourceResponse } from "@server/routers/resource"; import type { PaginationState } from "@tanstack/react-table"; import { AxiosResponse } from "axios"; import { + ArrowDown01Icon, ArrowRight, + ArrowUp10Icon, CheckCircle2, ChevronDown, + ChevronsUpDownIcon, Clock, MoreHorizontal, ShieldCheck, @@ -318,7 +322,26 @@ export default function ProxyResourcesTable({ accessorKey: "name", enableHiding: false, friendlyName: t("name"), - header: () => {t("name")} + header: () => { + const nameOrder = getSortDirection("name", searchParams); + const Icon = + nameOrder === "asc" + ? ArrowDown01Icon + : nameOrder === "desc" + ? ArrowUp10Icon + : ChevronsUpDownIcon; + + return ( + + ); + } }, { id: "niceId", @@ -563,6 +586,14 @@ export default function ProxyResourcesTable({ }); } + function toggleSort(column: string) { + const newSearch = getNextSortOrder(column, searchParams); + + filter({ + searchParams: newSearch + }); + } + const handlePaginationChange = (newPage: PaginationState) => { searchParams.set("page", (newPage.pageIndex + 1).toString()); searchParams.set("pageSize", newPage.pageSize.toString()); diff --git a/src/components/SitesTable.tsx b/src/components/SitesTable.tsx index c78577731..cc02e5d37 100644 --- a/src/components/SitesTable.tsx +++ b/src/components/SitesTable.tsx @@ -141,7 +141,24 @@ export default function SitesTable({ accessorKey: "name", enableHiding: false, header: () => { - return {t("name")}; + const nameOrder = getSortDirection("name", searchParams); + const Icon = + nameOrder === "asc" + ? ArrowDown01Icon + : nameOrder === "desc" + ? ArrowUp10Icon + : ChevronsUpDownIcon; + + return ( + + ); } }, { diff --git a/src/components/newt-install-commands.tsx b/src/components/newt-install-commands.tsx index 5a252f0d5..ba0363e3b 100644 --- a/src/components/newt-install-commands.tsx +++ b/src/components/newt-install-commands.tsx @@ -101,6 +101,7 @@ export function NewtSiteInstallCommands({ `helm install newt fossorial/newt \\ --create-namespace \\ --set newtInstances[0].name="main-tunnel" \\ + --set newtInstances[0].enabled=true \\ --set-string newtInstances[0].auth.keys.endpointKey="${endpoint}" \\ --set-string newtInstances[0].auth.keys.idKey="${id}" \\ --set-string newtInstances[0].auth.keys.secretKey="${secret}"` @@ -185,59 +186,72 @@ WantedBy=default.target` className="mt-4" /> -
-

- {t("siteConfiguration")} -

-
- { - const value = checked as boolean; - setAcceptClients(value); - }} - label={t("siteAcceptClientConnections")} - /> -
-

- {t("siteAcceptClientConnectionsDescription")} -

+
+

{t("siteConfiguration")}

+
+ { + const value = checked as boolean; + setAcceptClients(value); + }} + label={t("siteAcceptClientConnections")} + />
+

+ {t("siteAcceptClientConnectionsDescription")} +

+
-
-

{t("commands")}

-
- {commands.map((item, index) => { - const commandText = - typeof item === "string" - ? item - : item.command; - const title = - typeof item === "string" - ? undefined - : item.title; +
+

{t("commands")}

+ {platform === "kubernetes" && ( +

+ For more and up to date Kubernetes installation + information, see{" "} + + docs.pangolin.net/manage/sites/install-kubernetes + + . +

+ )} +
+ {commands.map((item, index) => { + const commandText = + typeof item === "string" ? item : item.command; + const title = + typeof item === "string" + ? undefined + : item.title; - return ( -
- {title && ( -

- {title} -

- )} - -
- ); - })} -
+ const key = `${title ?? ""}::${commandText}`; + + return ( +
+ {title && ( +

+ {title} +

+ )} + +
+ ); + })}
+
); diff --git a/src/lib/api/cookies.ts b/src/lib/api/cookies.ts index fe3c00907..c4c395c4f 100644 --- a/src/lib/api/cookies.ts +++ b/src/lib/api/cookies.ts @@ -2,31 +2,20 @@ import { headers } from "next/headers"; export async function authCookieHeader() { const otherHeaders = await headers(); - const otherHeadersObject = Object.fromEntries(otherHeaders.entries()); + const otherHeadersObject = Object.fromEntries( + Array.from(otherHeaders.entries()).map(([k, v]) => [k.toLowerCase(), v]) + ); return { headers: { - cookie: - otherHeadersObject["cookie"] || otherHeadersObject["Cookie"], - host: otherHeadersObject["host"] || otherHeadersObject["Host"], - "user-agent": - otherHeadersObject["user-agent"] || - otherHeadersObject["User-Agent"], - "x-forwarded-for": - otherHeadersObject["x-forwarded-for"] || - otherHeadersObject["X-Forwarded-For"], - "x-forwarded-host": - otherHeadersObject["fx-forwarded-host"] || - otherHeadersObject["Fx-Forwarded-Host"], - "x-forwarded-port": - otherHeadersObject["x-forwarded-port"] || - otherHeadersObject["X-Forwarded-Port"], - "x-forwarded-proto": - otherHeadersObject["x-forwarded-proto"] || - otherHeadersObject["X-Forwarded-Proto"], - "x-real-ip": - otherHeadersObject["x-real-ip"] || - otherHeadersObject["X-Real-IP"] + cookie: otherHeadersObject["cookie"], + host: otherHeadersObject["host"], + "user-agent": otherHeadersObject["user-agent"], + "x-forwarded-for": otherHeadersObject["x-forwarded-for"], + "x-forwarded-host": otherHeadersObject["x-forwarded-host"], + "x-forwarded-port": otherHeadersObject["x-forwarded-port"], + "x-forwarded-proto": otherHeadersObject["x-forwarded-proto"], + "x-real-ip": otherHeadersObject["x-real-ip"] } }; } diff --git a/src/lib/queries.ts b/src/lib/queries.ts index fe5350ff9..d3e962d74 100644 --- a/src/lib/queries.ts +++ b/src/lib/queries.ts @@ -4,7 +4,8 @@ import type { ListClientsResponse } from "@server/routers/client"; import type { ListDomainsResponse } from "@server/routers/domain"; import type { GetResourceWhitelistResponse, - ListResourceNamesResponse + ListResourceNamesResponse, + ListResourcesResponse } from "@server/routers/resource"; import type { ListRolesResponse } from "@server/routers/role"; import type { ListSitesResponse } from "@server/routers/site"; @@ -90,23 +91,13 @@ export const productUpdatesQueries = { }) }; -export const clientFilterSchema = z.object({ - pageSize: z.int().prefault(1000).optional() -}); - export const orgQueries = { - clients: ({ - orgId, - filters - }: { - orgId: string; - filters?: z.infer; - }) => + clients: ({ orgId }: { orgId: string }) => queryOptions({ - queryKey: ["ORG", orgId, "CLIENTS", filters] as const, + queryKey: ["ORG", orgId, "CLIENTS"] as const, queryFn: async ({ signal, meta }) => { const sp = new URLSearchParams({ - pageSize: (filters?.pageSize ?? 1000).toString() + pageSize: "10000" }); const res = await meta!.api.get< @@ -143,9 +134,13 @@ export const orgQueries = { queryOptions({ queryKey: ["ORG", orgId, "SITES"] as const, queryFn: async ({ signal, meta }) => { + const sp = new URLSearchParams({ + pageSize: "10000" + }); + const res = await meta!.api.get< AxiosResponse - >(`/org/${orgId}/sites`, { signal }); + >(`/org/${orgId}/sites?${sp.toString()}`, { signal }); return res.data.data.sites; } }), @@ -182,6 +177,22 @@ export const orgQueries = { ); return res.data.data.idps; } + }), + + resources: ({ orgId }: { orgId: string }) => + queryOptions({ + queryKey: ["ORG", orgId, "RESOURCES"] as const, + queryFn: async ({ signal, meta }) => { + const sp = new URLSearchParams({ + pageSize: "10000" + }); + + const res = await meta!.api.get< + AxiosResponse + >(`/org/${orgId}/resources?${sp.toString()}`, { signal }); + + return res.data.data.resources; + } }) };