mirror of
https://github.com/fosrl/pangolin.git
synced 2026-07-31 17:50:47 +02:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 83df1cc57c | |||
| d3324f4f28 | |||
| 97a1bc3697 | |||
| 45623ce36b | |||
| d68f123e13 | |||
| 9469fea88f | |||
| 915e8d5b1f | |||
| 719baa201e | |||
| b0e274f5a9 |
@@ -62,7 +62,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Monitor storage space
|
- name: Monitor storage space
|
||||||
run: |
|
run: |
|
||||||
@@ -77,7 +77,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
@@ -134,7 +134,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Monitor storage space
|
- name: Monitor storage space
|
||||||
run: |
|
run: |
|
||||||
@@ -149,7 +149,7 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
@@ -201,10 +201,10 @@ jobs:
|
|||||||
timeout-minutes: 30
|
timeout-minutes: 30
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
@@ -256,7 +256,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Extract tag name
|
- name: Extract tag name
|
||||||
id: get-tag
|
id: get-tag
|
||||||
@@ -264,7 +264,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Install Go
|
- name: Install Go
|
||||||
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: 1.25
|
go-version: 1.25
|
||||||
|
|
||||||
@@ -407,7 +407,7 @@ jobs:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry (for cosign)
|
- name: Login to GitHub Container Registry (for cosign)
|
||||||
uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
|
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
|
|||||||
@@ -21,10 +21,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
node-version: '24'
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ jobs:
|
|||||||
stale:
|
stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
|
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
|
||||||
with:
|
with:
|
||||||
days-before-stale: 14
|
days-before-stale: 14
|
||||||
days-before-close: 14
|
days-before-close: 14
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Install Node
|
- name: Install Node
|
||||||
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: '24'
|
node-version: '24'
|
||||||
|
|
||||||
@@ -62,7 +62,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Build Docker image sqlite
|
- name: Build Docker image sqlite
|
||||||
run: make dev-build-sqlite
|
run: make dev-build-sqlite
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
|
|
||||||
- name: Build Docker image pg
|
- name: Build Docker image pg
|
||||||
run: make dev-build-pg
|
run: make dev-build-pg
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
# FROM node:24-slim AS base
|
# FROM node:24-slim AS base
|
||||||
FROM public.ecr.aws/docker/library/node:26-slim AS base
|
FROM public.ecr.aws/docker/library/node:24-slim AS base
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ FROM base AS builder
|
|||||||
RUN npm ci --omit=dev
|
RUN npm ci --omit=dev
|
||||||
|
|
||||||
# FROM node:24-slim AS runner
|
# FROM node:24-slim AS runner
|
||||||
FROM public.ecr.aws/docker/library/node:26-slim AS runner
|
FROM public.ecr.aws/docker/library/node:24-slim AS runner
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
FROM node:26-alpine
|
FROM node:24-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -5,7 +5,7 @@ go 1.25.0
|
|||||||
require (
|
require (
|
||||||
github.com/charmbracelet/huh v1.0.0
|
github.com/charmbracelet/huh v1.0.0
|
||||||
github.com/charmbracelet/lipgloss v1.1.0
|
github.com/charmbracelet/lipgloss v1.1.0
|
||||||
golang.org/x/term v0.45.0
|
golang.org/x/term v0.44.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -33,6 +33,6 @@ require (
|
|||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||||
golang.org/x/sync v0.15.0 // indirect
|
golang.org/x/sync v0.15.0 // indirect
|
||||||
golang.org/x/sys v0.47.0 // indirect
|
golang.org/x/sys v0.46.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
+4
-4
@@ -69,10 +69,10 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
|
|||||||
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
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.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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
|
||||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
|
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
|
||||||
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
|
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
|
||||||
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
|||||||
Generated
+60
-304
@@ -51,7 +51,7 @@
|
|||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"arctic": "3.7.0",
|
"arctic": "3.7.0",
|
||||||
"axios": "1.18.0",
|
"axios": "1.16.1",
|
||||||
"better-sqlite3": "11.9.1",
|
"better-sqlite3": "11.9.1",
|
||||||
"canvas-confetti": "1.9.4",
|
"canvas-confetti": "1.9.4",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
@@ -70,12 +70,12 @@
|
|||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"ioredis": "5.11.0",
|
"ioredis": "5.11.0",
|
||||||
"jmespath": "0.16.0",
|
"jmespath": "0.16.0",
|
||||||
"js-yaml": "4.3.0",
|
"js-yaml": "4.2.0",
|
||||||
"jsonwebtoken": "9.0.3",
|
"jsonwebtoken": "9.0.3",
|
||||||
"lucide-react": "1.17.0",
|
"lucide-react": "1.17.0",
|
||||||
"maxmind": "5.0.6",
|
"maxmind": "5.0.6",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "16.2.11",
|
"next": "16.2.6",
|
||||||
"next-intl": "4.13.0",
|
"next-intl": "4.13.0",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"nextjs-toploader": "3.9.17",
|
"nextjs-toploader": "3.9.17",
|
||||||
@@ -2574,9 +2574,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/env": {
|
"node_modules/@next/env": {
|
||||||
"version": "16.2.11",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz",
|
||||||
"integrity": "sha512-0do5A3BJ2gxWr0ZCMcD6BhW+e595jyxdTl3rXTS6lOtD8ektMiW6CO+EPwt1Eca1DBnm90r/7GdiKWBKxH++DA==",
|
"integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@next/eslint-plugin-next": {
|
"node_modules/@next/eslint-plugin-next": {
|
||||||
@@ -2590,9 +2590,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-arm64": {
|
"node_modules/@next/swc-darwin-arm64": {
|
||||||
"version": "16.2.11",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz",
|
||||||
"integrity": "sha512-wryL4pjKmDwGv2ox6+GZDFxvmtSRLqApBR8kL1j4+vhB7Z5vJC/zAnXpiR9Xkfzl0AS8WLMnsuGV/UKI67/rrw==",
|
"integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2606,9 +2606,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-darwin-x64": {
|
"node_modules/@next/swc-darwin-x64": {
|
||||||
"version": "16.2.11",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz",
|
||||||
"integrity": "sha512-aZl2j4f/fLyjQvOhv0Oe9UaMAQHolYpKhctsoYzplSumKJKPUmgjcf6545aBtysLTcu994TREd0+pSgNE4ohmg==",
|
"integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -2622,15 +2622,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-gnu": {
|
"node_modules/@next/swc-linux-arm64-gnu": {
|
||||||
"version": "16.2.11",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz",
|
||||||
"integrity": "sha512-5jEriyEnH/LWFy27L2ZG0XaLlyEJIjhsImEsiS9P563PKEVp2BVups/xfOucIrsvVntp11oNcZwjHvaDPYVB5g==",
|
"integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2641,15 +2638,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-arm64-musl": {
|
"node_modules/@next/swc-linux-arm64-musl": {
|
||||||
"version": "16.2.11",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz",
|
||||||
"integrity": "sha512-eIjcpx2fnnFSSkZDbTxy74KnokUXDjfoLClpWelfgHLf621aTqswhwXQ7GkD5K5rplrS6LZ/Bj+mVuvzluBOEg==",
|
"integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2660,15 +2654,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-gnu": {
|
"node_modules/@next/swc-linux-x64-gnu": {
|
||||||
"version": "16.2.11",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz",
|
||||||
"integrity": "sha512-8WgzpaWMs46qJT9kiV47cje86L0x/Mu9t8/Gwj+pnbgW3rETVfCnaScPjlYUwNScpOozdcIMHWmAvuZJUonR2w==",
|
"integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2679,15 +2670,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-linux-x64-musl": {
|
"node_modules/@next/swc-linux-x64-musl": {
|
||||||
"version": "16.2.11",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz",
|
||||||
"integrity": "sha512-I3UgPds7G4ZYnTb/H+5GBGuUT2DhAk6j0mL6A4s63RjFs74wB2hOWP0vaxsK+3NJraExt3eYEPQ/UtT0x/64Nw==",
|
"integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -2698,9 +2686,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-arm64-msvc": {
|
"node_modules/@next/swc-win32-arm64-msvc": {
|
||||||
"version": "16.2.11",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz",
|
||||||
"integrity": "sha512-n89CjtcThnjrwgJMAiI5xbqwLY51zvwC9tSlArmVndAJLYVl9T9UAdlkXTmZvE++idoXe8KdglQlhNRdUp1c6g==",
|
"integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
@@ -2714,9 +2702,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@next/swc-win32-x64-msvc": {
|
"node_modules/@next/swc-win32-x64-msvc": {
|
||||||
"version": "16.2.11",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz",
|
||||||
"integrity": "sha512-md8CLNggS1Dx9pUgApzps5uAf+N8GN9xywzmNx9vHAWo94HtBwCCqkSnhIrdfQe83Dhz8Lfo/20Nb1Zxal092w==",
|
"integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
@@ -6472,225 +6460,6 @@
|
|||||||
"next": "16.2.6"
|
"next": "16.2.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-email/ui/node_modules/@next/env": {
|
|
||||||
"version": "16.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz",
|
|
||||||
"integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/@react-email/ui/node_modules/@next/swc-darwin-arm64": {
|
|
||||||
"version": "16.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz",
|
|
||||||
"integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-email/ui/node_modules/@next/swc-darwin-x64": {
|
|
||||||
"version": "16.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz",
|
|
||||||
"integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"darwin"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-email/ui/node_modules/@next/swc-linux-arm64-gnu": {
|
|
||||||
"version": "16.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz",
|
|
||||||
"integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-email/ui/node_modules/@next/swc-linux-arm64-musl": {
|
|
||||||
"version": "16.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz",
|
|
||||||
"integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-email/ui/node_modules/@next/swc-linux-x64-gnu": {
|
|
||||||
"version": "16.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz",
|
|
||||||
"integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-email/ui/node_modules/@next/swc-linux-x64-musl": {
|
|
||||||
"version": "16.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz",
|
|
||||||
"integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"linux"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-email/ui/node_modules/@next/swc-win32-arm64-msvc": {
|
|
||||||
"version": "16.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz",
|
|
||||||
"integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==",
|
|
||||||
"cpu": [
|
|
||||||
"arm64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-email/ui/node_modules/@next/swc-win32-x64-msvc": {
|
|
||||||
"version": "16.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz",
|
|
||||||
"integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==",
|
|
||||||
"cpu": [
|
|
||||||
"x64"
|
|
||||||
],
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"os": [
|
|
||||||
"win32"
|
|
||||||
],
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 10"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-email/ui/node_modules/@swc/helpers": {
|
|
||||||
"version": "0.5.15",
|
|
||||||
"resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz",
|
|
||||||
"integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-email/ui/node_modules/next": {
|
|
||||||
"version": "16.2.6",
|
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz",
|
|
||||||
"integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@next/env": "16.2.6",
|
|
||||||
"@swc/helpers": "0.5.15",
|
|
||||||
"baseline-browser-mapping": "^2.9.19",
|
|
||||||
"caniuse-lite": "^1.0.30001579",
|
|
||||||
"postcss": "8.4.31",
|
|
||||||
"styled-jsx": "5.1.6"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"next": "dist/bin/next"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=20.9.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@next/swc-darwin-arm64": "16.2.6",
|
|
||||||
"@next/swc-darwin-x64": "16.2.6",
|
|
||||||
"@next/swc-linux-arm64-gnu": "16.2.6",
|
|
||||||
"@next/swc-linux-arm64-musl": "16.2.6",
|
|
||||||
"@next/swc-linux-x64-gnu": "16.2.6",
|
|
||||||
"@next/swc-linux-x64-musl": "16.2.6",
|
|
||||||
"@next/swc-win32-arm64-msvc": "16.2.6",
|
|
||||||
"@next/swc-win32-x64-msvc": "16.2.6",
|
|
||||||
"sharp": "^0.34.5"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"@opentelemetry/api": "^1.1.0",
|
|
||||||
"@playwright/test": "^1.51.1",
|
|
||||||
"babel-plugin-react-compiler": "*",
|
|
||||||
"react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
|
|
||||||
"react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0",
|
|
||||||
"sass": "^1.3.0"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"@opentelemetry/api": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"@playwright/test": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"babel-plugin-react-compiler": {
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"sass": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@react-types/shared": {
|
"node_modules/@react-types/shared": {
|
||||||
"version": "3.34.0",
|
"version": "3.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.34.0.tgz",
|
"resolved": "https://registry.npmjs.org/@react-types/shared/-/shared-3.34.0.tgz",
|
||||||
@@ -9309,9 +9078,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axios": {
|
"node_modules/axios": {
|
||||||
"version": "1.18.0",
|
"version": "1.16.1",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz",
|
||||||
"integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==",
|
"integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"follow-redirects": "^1.16.0",
|
"follow-redirects": "^1.16.0",
|
||||||
@@ -9445,20 +9214,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/body-parser": {
|
"node_modules/body-parser": {
|
||||||
"version": "2.3.0",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz",
|
||||||
"integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
|
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bytes": "^3.1.2",
|
"bytes": "^3.1.2",
|
||||||
"content-type": "^2.0.0",
|
"content-type": "^1.0.5",
|
||||||
"debug": "^4.4.3",
|
"debug": "^4.4.3",
|
||||||
"http-errors": "^2.0.1",
|
"http-errors": "^2.0.0",
|
||||||
"iconv-lite": "^0.7.2",
|
"iconv-lite": "^0.7.0",
|
||||||
"on-finished": "^2.4.1",
|
"on-finished": "^2.4.1",
|
||||||
"qs": "^6.15.2",
|
"qs": "^6.14.1",
|
||||||
"raw-body": "^3.0.2",
|
"raw-body": "^3.0.1",
|
||||||
"type-is": "^2.1.0"
|
"type-is": "^2.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
@@ -9468,19 +9237,6 @@
|
|||||||
"url": "https://opencollective.com/express"
|
"url": "https://opencollective.com/express"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/body-parser/node_modules/content-type": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/content-type/-/content-type-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-j/O/d7GcZCyNl7/hwZAb606rzqkyvaDctLmckbxLzHvFBzTJHuGEdodATcP3yIRoDrLHkIATJuvzbFlp/ki2cQ==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "opencollective",
|
|
||||||
"url": "https://opencollective.com/express"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/body-parser/node_modules/iconv-lite": {
|
"node_modules/body-parser/node_modules/iconv-lite": {
|
||||||
"version": "0.7.2",
|
"version": "0.7.2",
|
||||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
|
||||||
@@ -12169,9 +11925,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-uri": {
|
"node_modules/fast-uri": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz",
|
||||||
"integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
|
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -13633,9 +13389,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.3.0",
|
"version": "4.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz",
|
||||||
"integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
|
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "github",
|
"type": "github",
|
||||||
@@ -14593,12 +14349,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/next": {
|
"node_modules/next": {
|
||||||
"version": "16.2.11",
|
"version": "16.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/next/-/next-16.2.11.tgz",
|
"resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz",
|
||||||
"integrity": "sha512-B339zaqbyK8cmxhoAvLrcwoabwCP1wz21zSzfqxqXAemTu2BXnH7tQnfcglKv1vnMUIDBc+Hth7XODQriTZiRQ==",
|
"integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next/env": "16.2.11",
|
"@next/env": "16.2.6",
|
||||||
"@swc/helpers": "0.5.15",
|
"@swc/helpers": "0.5.15",
|
||||||
"baseline-browser-mapping": "^2.9.19",
|
"baseline-browser-mapping": "^2.9.19",
|
||||||
"caniuse-lite": "^1.0.30001579",
|
"caniuse-lite": "^1.0.30001579",
|
||||||
@@ -14612,14 +14368,14 @@
|
|||||||
"node": ">=20.9.0"
|
"node": ">=20.9.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@next/swc-darwin-arm64": "16.2.11",
|
"@next/swc-darwin-arm64": "16.2.6",
|
||||||
"@next/swc-darwin-x64": "16.2.11",
|
"@next/swc-darwin-x64": "16.2.6",
|
||||||
"@next/swc-linux-arm64-gnu": "16.2.11",
|
"@next/swc-linux-arm64-gnu": "16.2.6",
|
||||||
"@next/swc-linux-arm64-musl": "16.2.11",
|
"@next/swc-linux-arm64-musl": "16.2.6",
|
||||||
"@next/swc-linux-x64-gnu": "16.2.11",
|
"@next/swc-linux-x64-gnu": "16.2.6",
|
||||||
"@next/swc-linux-x64-musl": "16.2.11",
|
"@next/swc-linux-x64-musl": "16.2.6",
|
||||||
"@next/swc-win32-arm64-msvc": "16.2.11",
|
"@next/swc-win32-arm64-msvc": "16.2.6",
|
||||||
"@next/swc-win32-x64-msvc": "16.2.11",
|
"@next/swc-win32-x64-msvc": "16.2.6",
|
||||||
"sharp": "^0.34.5"
|
"sharp": "^0.34.5"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|||||||
+3
-3
@@ -74,7 +74,7 @@
|
|||||||
"@xterm/addon-web-links": "^0.12.0",
|
"@xterm/addon-web-links": "^0.12.0",
|
||||||
"@xterm/xterm": "^6.0.0",
|
"@xterm/xterm": "^6.0.0",
|
||||||
"arctic": "3.7.0",
|
"arctic": "3.7.0",
|
||||||
"axios": "1.18.0",
|
"axios": "1.16.1",
|
||||||
"better-sqlite3": "11.9.1",
|
"better-sqlite3": "11.9.1",
|
||||||
"canvas-confetti": "1.9.4",
|
"canvas-confetti": "1.9.4",
|
||||||
"class-variance-authority": "0.7.1",
|
"class-variance-authority": "0.7.1",
|
||||||
@@ -93,12 +93,12 @@
|
|||||||
"input-otp": "1.4.2",
|
"input-otp": "1.4.2",
|
||||||
"ioredis": "5.11.0",
|
"ioredis": "5.11.0",
|
||||||
"jmespath": "0.16.0",
|
"jmespath": "0.16.0",
|
||||||
"js-yaml": "4.3.0",
|
"js-yaml": "4.2.0",
|
||||||
"jsonwebtoken": "9.0.3",
|
"jsonwebtoken": "9.0.3",
|
||||||
"lucide-react": "1.17.0",
|
"lucide-react": "1.17.0",
|
||||||
"maxmind": "5.0.6",
|
"maxmind": "5.0.6",
|
||||||
"moment": "2.30.1",
|
"moment": "2.30.1",
|
||||||
"next": "16.2.11",
|
"next": "16.2.6",
|
||||||
"next-intl": "4.13.0",
|
"next-intl": "4.13.0",
|
||||||
"next-themes": "0.4.6",
|
"next-themes": "0.4.6",
|
||||||
"nextjs-toploader": "3.9.17",
|
"nextjs-toploader": "3.9.17",
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ export const sites = pgTable(
|
|||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
pubKey: varchar("pubKey"),
|
pubKey: varchar("pubKey"),
|
||||||
subnet: varchar("subnet"),
|
subnet: varchar("subnet"),
|
||||||
|
exitNodeSubnet: text("exitNodeSubnet"), // this is the subnet when connecting to an exit node
|
||||||
megabytesIn: real("bytesIn").default(0),
|
megabytesIn: real("bytesIn").default(0),
|
||||||
megabytesOut: real("bytesOut").default(0),
|
megabytesOut: real("bytesOut").default(0),
|
||||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||||
@@ -194,7 +195,10 @@ export const resources = pgTable(
|
|||||||
postAuthPath: text("postAuthPath"),
|
postAuthPath: text("postAuthPath"),
|
||||||
health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown"
|
health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown"
|
||||||
wildcard: boolean("wildcard").notNull().default(false),
|
wildcard: boolean("wildcard").notNull().default(false),
|
||||||
mode: text("mode").default("http").notNull(), // rdp, ssh, http, vnc
|
mode: text("mode")
|
||||||
|
.default("http")
|
||||||
|
.$type<"rdp" | "ssh" | "http" | "vnc" | "inference">()
|
||||||
|
.notNull(),
|
||||||
pamMode: varchar("pamMode", { length: 32 })
|
pamMode: varchar("pamMode", { length: 32 })
|
||||||
.$type<"passthrough" | "push">()
|
.$type<"passthrough" | "push">()
|
||||||
.default("passthrough"),
|
.default("passthrough"),
|
||||||
@@ -424,11 +428,14 @@ export const siteResources = pgTable(
|
|||||||
onDelete: "restrict"
|
onDelete: "restrict"
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
requiresExitNodeConnection: boolean("requiresExitNodeConnection")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
niceId: varchar("niceId").notNull(),
|
niceId: varchar("niceId").notNull(),
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
ssl: boolean("ssl").notNull().default(false),
|
ssl: boolean("ssl").notNull().default(false),
|
||||||
mode: varchar("mode")
|
mode: varchar("mode")
|
||||||
.$type<"host" | "cidr" | "http" | "ssh">()
|
.$type<"host" | "cidr" | "http" | "ssh" | "inference">()
|
||||||
.notNull(), // "host" | "cidr" | "http"
|
.notNull(), // "host" | "cidr" | "http"
|
||||||
scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
|
scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
|
||||||
proxyPort: integer("proxyPort"), // only for port mode
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
@@ -1181,7 +1188,7 @@ export const clients = pgTable(
|
|||||||
olmId: text("olmId"), // to lock it to a specific olm optionally
|
olmId: text("olmId"), // to lock it to a specific olm optionally
|
||||||
name: varchar("name").notNull(),
|
name: varchar("name").notNull(),
|
||||||
pubKey: varchar("pubKey"),
|
pubKey: varchar("pubKey"),
|
||||||
subnet: varchar("subnet").notNull(),
|
exitNodeSubnet: varchar("exitNodeSubnet").notNull(),
|
||||||
megabytesIn: real("bytesIn"),
|
megabytesIn: real("bytesIn"),
|
||||||
megabytesOut: real("bytesOut"),
|
megabytesOut: real("bytesOut"),
|
||||||
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export const sites = sqliteTable("sites", {
|
|||||||
}),
|
}),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
pubKey: text("pubKey"),
|
pubKey: text("pubKey"),
|
||||||
subnet: text("subnet"),
|
exitNodeSubnet: text("exitNodeSubnet"),
|
||||||
megabytesIn: integer("bytesIn").default(0),
|
megabytesIn: integer("bytesIn").default(0),
|
||||||
megabytesOut: integer("bytesOut").default(0),
|
megabytesOut: integer("bytesOut").default(0),
|
||||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||||
@@ -203,7 +203,10 @@ export const resources = sqliteTable("resources", {
|
|||||||
postAuthPath: text("postAuthPath"),
|
postAuthPath: text("postAuthPath"),
|
||||||
health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown"
|
health: text("health").default("unknown"), // "healthy", "unhealthy", "unknown"
|
||||||
wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false),
|
wildcard: integer("wildcard", { mode: "boolean" }).notNull().default(false),
|
||||||
mode: text("mode").default("http").notNull(), // rdp, ssh, http, vnc
|
mode: text("mode")
|
||||||
|
.default("http")
|
||||||
|
.$type<"rdp" | "ssh" | "http" | "vnc" | "inference">()
|
||||||
|
.notNull(), // rdp, ssh, http, vnc, inference
|
||||||
pamMode: text("pamMode")
|
pamMode: text("pamMode")
|
||||||
.$type<"passthrough" | "push">()
|
.$type<"passthrough" | "push">()
|
||||||
.default("passthrough"),
|
.default("passthrough"),
|
||||||
@@ -422,10 +425,17 @@ export const siteResources = sqliteTable("siteResources", {
|
|||||||
() => networks.networkId,
|
() => networks.networkId,
|
||||||
{ onDelete: "restrict" }
|
{ onDelete: "restrict" }
|
||||||
),
|
),
|
||||||
|
requiresExitNodeConnection: integer("requiresExitNodeConnection", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
niceId: text("niceId").notNull(),
|
niceId: text("niceId").notNull(),
|
||||||
name: text("name").notNull(),
|
name: text("name").notNull(),
|
||||||
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
|
ssl: integer("ssl", { mode: "boolean" }).notNull().default(false),
|
||||||
mode: text("mode").$type<"host" | "cidr" | "http" | "ssh">().notNull(), // "host" | "cidr" | "http"
|
mode: text("mode")
|
||||||
|
.$type<"host" | "cidr" | "http" | "ssh" | "inference">()
|
||||||
|
.notNull(), // "host" | "cidr" | "http"
|
||||||
scheme: text("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
|
scheme: text("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
|
||||||
proxyPort: integer("proxyPort"), // only for port mode
|
proxyPort: integer("proxyPort"), // only for port mode
|
||||||
destinationPort: integer("destinationPort"), // only for port mode
|
destinationPort: integer("destinationPort"), // only for port mode
|
||||||
@@ -599,6 +609,7 @@ export const clients = sqliteTable("clients", {
|
|||||||
pubKey: text("pubKey"),
|
pubKey: text("pubKey"),
|
||||||
olmId: text("olmId"), // to lock it to a specific olm optionally
|
olmId: text("olmId"), // to lock it to a specific olm optionally
|
||||||
subnet: text("subnet").notNull(),
|
subnet: text("subnet").notNull(),
|
||||||
|
exitNodeSubnet: text("exitNodeSubnet"), // this is the subnet when connecting to an exit node
|
||||||
megabytesIn: integer("bytesIn"),
|
megabytesIn: integer("bytesIn"),
|
||||||
megabytesOut: integer("bytesOut"),
|
megabytesOut: integer("bytesOut"),
|
||||||
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
lastBandwidthUpdate: text("lastBandwidthUpdate"),
|
||||||
|
|||||||
@@ -339,19 +339,6 @@ export async function calculateUserClientsForOrgs(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get exit nodes for this org
|
|
||||||
const exitNodesList = await getExitNodes(orgId);
|
|
||||||
|
|
||||||
if (exitNodesList.length === 0) {
|
|
||||||
logger.warn(
|
|
||||||
`Skipping org ${orgId} for OLM ${olm.olmId} (user ${userId}): no exit nodes found`
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const randomExitNode =
|
|
||||||
exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
|
|
||||||
|
|
||||||
// Get next available subnet
|
// Get next available subnet
|
||||||
const { value: newSubnet, release: releaseSubnetLock } =
|
const { value: newSubnet, release: releaseSubnetLock } =
|
||||||
await getNextAvailableClientSubnet(orgId, trx);
|
await getNextAvailableClientSubnet(orgId, trx);
|
||||||
@@ -370,7 +357,6 @@ export async function calculateUserClientsForOrgs(
|
|||||||
const newClientData: InferInsertModel<typeof clients> = {
|
const newClientData: InferInsertModel<typeof clients> = {
|
||||||
userId,
|
userId,
|
||||||
orgId: userOrg.orgId,
|
orgId: userOrg.orgId,
|
||||||
exitNodeId: randomExitNode.exitNodeId,
|
|
||||||
name: olm.name || "User Client",
|
name: olm.name || "User Client",
|
||||||
subnet: updatedSubnet,
|
subnet: updatedSubnet,
|
||||||
olmId: olm.olmId,
|
olmId: olm.olmId,
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { db, sites, clients } from "@server/db";
|
||||||
|
import { and, eq, count } from "drizzle-orm";
|
||||||
|
|
||||||
|
// (MAX_CONNECTIONS - current_connections) / MAX_CONNECTIONS)
|
||||||
|
// higher = more desirable
|
||||||
|
// like saying, this node has x% of its capacity left
|
||||||
|
export async function calculateExitNodeWeight(
|
||||||
|
exitNodeId: number,
|
||||||
|
maxConnections: number | null | undefined
|
||||||
|
): Promise<number | null> {
|
||||||
|
if (maxConnections === null || maxConnections === undefined) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [[siteConnections], [clientConnections]] = await Promise.all([
|
||||||
|
db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(sites)
|
||||||
|
.where(
|
||||||
|
and(eq(sites.exitNodeId, exitNodeId), eq(sites.online, true))
|
||||||
|
),
|
||||||
|
db
|
||||||
|
.select({ count: count() })
|
||||||
|
.from(clients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(clients.exitNodeId, exitNodeId),
|
||||||
|
eq(clients.online, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
|
||||||
|
const currentConnections = siteConnections.count + clientConnections.count;
|
||||||
|
|
||||||
|
if (currentConnections >= maxConnections) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (maxConnections - currentConnections) / maxConnections;
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { db, exitNodes, Transaction } from "@server/db";
|
import { db, exitNodes, Transaction } from "@server/db";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { ExitNodePingResult } from "@server/routers/newt";
|
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
|
||||||
export async function verifyExitNodeOrgAccess(
|
export async function verifyExitNodeOrgAccess(
|
||||||
@@ -52,6 +51,16 @@ export async function listExitNodes(
|
|||||||
return allExitNodes;
|
return allExitNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ExitNodePingResult = {
|
||||||
|
exitNodeId: number;
|
||||||
|
latencyMs: number;
|
||||||
|
weight: number;
|
||||||
|
error?: string;
|
||||||
|
exitNodeName: string;
|
||||||
|
endpoint: string;
|
||||||
|
wasPreviouslyConnected: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export function selectBestExitNode(
|
export function selectBestExitNode(
|
||||||
pingResults: ExitNodePingResult[]
|
pingResults: ExitNodePingResult[]
|
||||||
): ExitNodePingResult | null {
|
): ExitNodePingResult | null {
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { db, ExitNode, Transaction, sites, clients } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
import { findNextAvailableCidr } from "@server/lib/ip";
|
||||||
|
import { lockManager } from "#dynamic/lib/lock";
|
||||||
|
|
||||||
|
export async function getUniqueSubnetForExitNode(
|
||||||
|
exitNode: ExitNode,
|
||||||
|
trx: Transaction | typeof db = db
|
||||||
|
): Promise<string | null> {
|
||||||
|
const lockKey = `subnet-allocation:${exitNode.exitNodeId}`;
|
||||||
|
|
||||||
|
return await lockManager.withLock(
|
||||||
|
lockKey,
|
||||||
|
async () => {
|
||||||
|
const [sitesQuery, clientsQuery] = await Promise.all([
|
||||||
|
trx
|
||||||
|
.select({ subnet: sites.exitNodeSubnet })
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.exitNodeId, exitNode.exitNodeId)),
|
||||||
|
trx
|
||||||
|
.select({ subnet: clients.exitNodeSubnet })
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.exitNodeId, exitNode.exitNodeId))
|
||||||
|
]);
|
||||||
|
|
||||||
|
const blockSize = config.getRawConfig().gerbil.site_block_size;
|
||||||
|
const subnets = [...sitesQuery, ...clientsQuery]
|
||||||
|
.map((row) => row.subnet)
|
||||||
|
.filter(
|
||||||
|
(subnet): subnet is string =>
|
||||||
|
!!subnet &&
|
||||||
|
/^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(subnet)
|
||||||
|
);
|
||||||
|
subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`));
|
||||||
|
|
||||||
|
return findNextAvailableCidr(subnets, blockSize, exitNode.address);
|
||||||
|
},
|
||||||
|
5000 // 5 second lock TTL - subnet allocation should be quick
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -2,3 +2,5 @@ export * from "./exitNodes";
|
|||||||
export * from "./exitNodeComms";
|
export * from "./exitNodeComms";
|
||||||
export * from "./subnet";
|
export * from "./subnet";
|
||||||
export * from "./getCurrentExitNodeId";
|
export * from "./getCurrentExitNodeId";
|
||||||
|
export * from "./calculateExitNodeWeight";
|
||||||
|
export * from "./getUniqueSubnetForExitNode";
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
userOrgRoles,
|
userOrgRoles,
|
||||||
userSiteResources
|
userSiteResources
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import { and, count, eq, inArray, ne } from "drizzle-orm";
|
import { and, count, eq, inArray, isNotNull, ne } from "drizzle-orm";
|
||||||
|
|
||||||
import { deletePeersBatch as newtDeletePeersBatch } from "@server/routers/newt/peers";
|
import { deletePeersBatch as newtDeletePeersBatch } from "@server/routers/newt/peers";
|
||||||
import {
|
import {
|
||||||
@@ -27,6 +27,9 @@ import {
|
|||||||
deletePeersBatch as olmDeletePeersBatch
|
deletePeersBatch as olmDeletePeersBatch
|
||||||
} from "@server/routers/olm/peers";
|
} from "@server/routers/olm/peers";
|
||||||
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
import { sendToExitNode } from "#dynamic/lib/exitNodes";
|
||||||
|
import { sendToClientsBatch } from "#dynamic/routers/ws";
|
||||||
|
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||||
|
import config from "@server/lib/config";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import {
|
import {
|
||||||
generateAliasConfig,
|
generateAliasConfig,
|
||||||
@@ -187,7 +190,12 @@ export async function getClientSiteResourceAccess(
|
|||||||
`rebuildClientAssociations: [getClientSiteResourceAccess] siteResourceId=${siteResource.siteResourceId} networkId=${siteResource.networkId} siteCount=${sitesList.length} siteIds=[${sitesList.map((s) => s.siteId).join(", ")}]`
|
`rebuildClientAssociations: [getClientSiteResourceAccess] siteResourceId=${siteResource.siteResourceId} networkId=${siteResource.networkId} siteCount=${sitesList.length} siteIds=[${sitesList.map((s) => s.siteId).join(", ")}]`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (sitesList.length === 0) {
|
if (sitesList.length === 0 && siteResource.networkId !== null) {
|
||||||
|
// A site resource with a networkId is expected to have at least one
|
||||||
|
// site attached via siteNetworks. Resources with no networkId (e.g.
|
||||||
|
// inference-mode resources, which connect clients directly to the
|
||||||
|
// exit node instead of any site) are expected to have no sites, so
|
||||||
|
// don't warn for those.
|
||||||
logger.warn(
|
logger.warn(
|
||||||
`No sites found for siteResource ${siteResource.siteResourceId} with networkId ${siteResource.networkId}`
|
`No sites found for siteResource ${siteResource.siteResourceId} with networkId ${siteResource.networkId}`
|
||||||
);
|
);
|
||||||
@@ -687,6 +695,22 @@ async function rebuildClientAssociationsFromSiteResourceImpl(
|
|||||||
clientSiteResourcesToRemove,
|
clientSiteResourcesToRemove,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// If this resource requires clients to be connected to the exit node
|
||||||
|
// (e.g. an inference resource), re-sync the connect/disconnect state for
|
||||||
|
// every client whose access to it may have changed - both those who
|
||||||
|
// currently have access and those who just lost it.
|
||||||
|
if (siteResource.requiresExitNodeConnection) {
|
||||||
|
await syncClientExitNodeConnections(
|
||||||
|
Array.from(
|
||||||
|
new Set([
|
||||||
|
...mergedAllClientIds,
|
||||||
|
...existingClientSiteResourceIds
|
||||||
|
])
|
||||||
|
),
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMessagesForSiteClients(
|
async function handleMessagesForSiteClients(
|
||||||
@@ -966,7 +990,7 @@ export async function updateClientSiteDestinations(
|
|||||||
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
||||||
|
|
||||||
for (const site of sitesData) {
|
for (const site of sitesData) {
|
||||||
if (!site.sites.subnet) {
|
if (!site.sites.exitNodeSubnet) {
|
||||||
logger.debug(`Site ${site.sites.siteId} has no subnet, skipping`);
|
logger.debug(`Site ${site.sites.siteId} has no subnet, skipping`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1002,7 +1026,7 @@ export async function updateClientSiteDestinations(
|
|||||||
sourcePort: parsedEndpoint.port,
|
sourcePort: parsedEndpoint.port,
|
||||||
destinations: [
|
destinations: [
|
||||||
{
|
{
|
||||||
destinationIP: site.sites.subnet.split("/")[0],
|
destinationIP: site.sites.exitNodeSubnet.split("/")[0],
|
||||||
destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -1010,7 +1034,7 @@ export async function updateClientSiteDestinations(
|
|||||||
} else {
|
} else {
|
||||||
// add to the existing destinations
|
// add to the existing destinations
|
||||||
destinations.destinations.push({
|
destinations.destinations.push({
|
||||||
destinationIP: site.sites.subnet.split("/")[0],
|
destinationIP: site.sites.exitNodeSubnet.split("/")[0],
|
||||||
destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
destinationPort: site.sites.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1052,6 +1076,179 @@ export async function updateClientSiteDestinations(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determines, for each of the given clients, whether they currently have
|
||||||
|
// access to any enabled site resource with requiresExitNodeConnection set
|
||||||
|
// (e.g. an inference-mode resource) and tells the client's olm to connect to
|
||||||
|
// or disconnect from its assigned exit node accordingly. Site resources with
|
||||||
|
// requiresExitNodeConnection don't belong to any site/network, so this can't
|
||||||
|
// be derived from the per-site peer logic above - it has to be recomputed
|
||||||
|
// from the client's full current resource access every time that access
|
||||||
|
// changes.
|
||||||
|
async function syncClientExitNodeConnections(
|
||||||
|
clientIds: number[],
|
||||||
|
trx: Transaction | typeof db = db
|
||||||
|
): Promise<void> {
|
||||||
|
const uniqueClientIds = Array.from(new Set(clientIds));
|
||||||
|
if (uniqueClientIds.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only clients with an exit node assigned can be told to connect/disconnect.
|
||||||
|
const clientsData = await trx
|
||||||
|
.select({
|
||||||
|
clientId: clients.clientId,
|
||||||
|
exitNodeId: clients.exitNodeId,
|
||||||
|
exitNodeSubnet: clients.exitNodeSubnet
|
||||||
|
})
|
||||||
|
.from(clients)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(clients.clientId, uniqueClientIds),
|
||||||
|
isNotNull(clients.exitNodeId)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (clientsData.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientIdsWithExitNode = clientsData.map((c) => c.clientId);
|
||||||
|
|
||||||
|
const requiresExitNodeRows = await trx
|
||||||
|
.select({
|
||||||
|
clientId: clientSiteResourcesAssociationsCache.clientId
|
||||||
|
})
|
||||||
|
.from(clientSiteResourcesAssociationsCache)
|
||||||
|
.innerJoin(
|
||||||
|
siteResources,
|
||||||
|
eq(
|
||||||
|
clientSiteResourcesAssociationsCache.siteResourceId,
|
||||||
|
siteResources.siteResourceId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
inArray(
|
||||||
|
clientSiteResourcesAssociationsCache.clientId,
|
||||||
|
clientIdsWithExitNode
|
||||||
|
),
|
||||||
|
eq(siteResources.enabled, true),
|
||||||
|
eq(siteResources.requiresExitNodeConnection, true)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const needsConnectSet = new Set(requiresExitNodeRows.map((r) => r.clientId));
|
||||||
|
|
||||||
|
const exitNodeIds = Array.from(
|
||||||
|
new Set(
|
||||||
|
clientsData
|
||||||
|
.map((c) => c.exitNodeId)
|
||||||
|
.filter((id): id is number => id !== null)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const exitNodeRows =
|
||||||
|
exitNodeIds.length > 0
|
||||||
|
? await trx
|
||||||
|
.select()
|
||||||
|
.from(exitNodes)
|
||||||
|
.where(inArray(exitNodes.exitNodeId, exitNodeIds))
|
||||||
|
: [];
|
||||||
|
const exitNodeById = new Map(exitNodeRows.map((n) => [n.exitNodeId, n]));
|
||||||
|
|
||||||
|
const olmRows = await trx
|
||||||
|
.select({
|
||||||
|
clientId: olms.clientId,
|
||||||
|
olmId: olms.olmId,
|
||||||
|
version: olms.version
|
||||||
|
})
|
||||||
|
.from(olms)
|
||||||
|
.where(inArray(olms.clientId, clientIdsWithExitNode));
|
||||||
|
const olmByClientId = new Map(
|
||||||
|
olmRows
|
||||||
|
.filter((r) => r.clientId !== null)
|
||||||
|
.map((r) => [r.clientId as number, r])
|
||||||
|
);
|
||||||
|
|
||||||
|
const relayPort = config.getRawConfig().gerbil.clients_start_port;
|
||||||
|
|
||||||
|
const connectPayloads: {
|
||||||
|
clientId: string;
|
||||||
|
message: { type: string; data: any };
|
||||||
|
options: { compress: boolean };
|
||||||
|
}[] = [];
|
||||||
|
const disconnectPayloads: {
|
||||||
|
clientId: string;
|
||||||
|
message: { type: string; data: any };
|
||||||
|
options: { compress: boolean };
|
||||||
|
}[] = [];
|
||||||
|
|
||||||
|
for (const client of clientsData) {
|
||||||
|
const olm = olmByClientId.get(client.clientId);
|
||||||
|
if (!olm) {
|
||||||
|
// No olm registered for this client yet/anymore, nothing to send.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsConnect = needsConnectSet.has(client.clientId);
|
||||||
|
|
||||||
|
if (needsConnect) {
|
||||||
|
const exitNode = client.exitNodeId
|
||||||
|
? exitNodeById.get(client.exitNodeId)
|
||||||
|
: undefined;
|
||||||
|
if (!exitNode || !client.exitNodeSubnet) {
|
||||||
|
logger.warn(
|
||||||
|
`rebuildClientAssociations: [syncClientExitNodeConnections] client ${client.clientId} needs an exit node connection but has no exit node or subnet assigned`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectPayloads.push({
|
||||||
|
clientId: olm.olmId,
|
||||||
|
message: {
|
||||||
|
type: "olm/wg/exitnode/connect",
|
||||||
|
data: {
|
||||||
|
connect: true,
|
||||||
|
endpoint: `${exitNode.endpoint}:${exitNode.listenPort}`,
|
||||||
|
relayPort,
|
||||||
|
publicKey: exitNode.publicKey,
|
||||||
|
serverIP: exitNode.address.split("/")[0],
|
||||||
|
tunnelIP: client.exitNodeSubnet.split("/")[0]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
options: { compress: canCompress(olm.version, "olm") }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
disconnectPayloads.push({
|
||||||
|
clientId: olm.olmId,
|
||||||
|
message: {
|
||||||
|
type: "olm/wg/exitnode/disconnect",
|
||||||
|
data: {}
|
||||||
|
},
|
||||||
|
options: { compress: canCompress(olm.version, "olm") }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectPayloads.length > 0) {
|
||||||
|
await sendToClientsBatch(connectPayloads).catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
`rebuildClientAssociations: Error sending exit node connect messages:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (disconnectPayloads.length > 0) {
|
||||||
|
await sendToClientsBatch(disconnectPayloads).catch((error) => {
|
||||||
|
logger.error(
|
||||||
|
`rebuildClientAssociations: Error sending exit node disconnect messages:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function handleSubnetProxyTargetUpdates(
|
async function handleSubnetProxyTargetUpdates(
|
||||||
siteResource: SiteResource,
|
siteResource: SiteResource,
|
||||||
sitesList: Site[],
|
sitesList: Site[],
|
||||||
@@ -1709,6 +1906,20 @@ export async function handleMessagingForUpdatedSiteResource(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If this resource requires (or required) clients to be connected to the
|
||||||
|
// exit node (e.g. an inference resource), re-sync connect/disconnect
|
||||||
|
// state for every client currently associated with it - covers toggling
|
||||||
|
// requiresExitNodeConnection on update as well as enabling/disabling it.
|
||||||
|
if (
|
||||||
|
updatedSiteResource.requiresExitNodeConnection ||
|
||||||
|
existingSiteResource?.requiresExitNodeConnection
|
||||||
|
) {
|
||||||
|
await syncClientExitNodeConnections(
|
||||||
|
mergedAllClients.map((c) => c.clientId),
|
||||||
|
trx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`handleMessagingForUpdatedSiteResource: DONE siteResourceId=${updatedSiteResource.siteResourceId}`
|
`handleMessagingForUpdatedSiteResource: DONE siteResourceId=${updatedSiteResource.siteResourceId}`
|
||||||
);
|
);
|
||||||
@@ -1990,6 +2201,10 @@ async function rebuildClientAssociationsFromClientImpl(
|
|||||||
resourcesToRemove,
|
resourcesToRemove,
|
||||||
trx
|
trx
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Re-sync exit node connect/disconnect state based on this client's
|
||||||
|
// current full set of resource access (e.g. inference resources).
|
||||||
|
await syncClientExitNodeConnections([client.clientId], trx);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleMessagesForClientSites(
|
async function handleMessagesForClientSites(
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export async function getTraefikConfig(
|
|||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
siteType: sites.type,
|
siteType: sites.type,
|
||||||
siteOnline: sites.online,
|
siteOnline: sites.online,
|
||||||
subnet: sites.subnet,
|
subnet: sites.exitNodeSubnet,
|
||||||
exitNodeId: sites.exitNodeId,
|
exitNodeId: sites.exitNodeId,
|
||||||
// Domain cert resolver fields
|
// Domain cert resolver fields
|
||||||
domainCertResolver: domains.certResolver,
|
domainCertResolver: domains.certResolver,
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
Transaction
|
Transaction
|
||||||
} from "@server/db";
|
} from "@server/db";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { ExitNodePingResult } from "@server/routers/newt";
|
|
||||||
import { eq, and, or, ne, isNull, inArray } from "drizzle-orm";
|
import { eq, and, or, ne, isNull, inArray } from "drizzle-orm";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
@@ -330,6 +329,16 @@ export async function listExitNodes(
|
|||||||
return exitNodesList;
|
return exitNodesList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ExitNodePingResult = {
|
||||||
|
exitNodeId: number;
|
||||||
|
latencyMs: number;
|
||||||
|
weight: number;
|
||||||
|
error?: string;
|
||||||
|
exitNodeName: string;
|
||||||
|
endpoint: string;
|
||||||
|
wasPreviouslyConnected: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Selects the most suitable exit node from a list of ping results.
|
* Selects the most suitable exit node from a list of ping results.
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ export async function getTraefikConfig(
|
|||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
siteType: sites.type,
|
siteType: sites.type,
|
||||||
siteOnline: sites.online,
|
siteOnline: sites.online,
|
||||||
subnet: sites.subnet,
|
subnet: sites.exitNodeSubnet,
|
||||||
exitNodeId: sites.exitNodeId,
|
exitNodeId: sites.exitNodeId,
|
||||||
// Namespace
|
// Namespace
|
||||||
domainNamespaceId: domainNamespaces.domainNamespaceId,
|
domainNamespaceId: domainNamespaces.domainNamespaceId,
|
||||||
|
|||||||
@@ -178,7 +178,7 @@ export async function reGenerateSiteSecret(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (site.exitNodeId && site.subnet) {
|
if (site.exitNodeId && site.exitNodeSubnet) {
|
||||||
await deletePeer(site.exitNodeId, site.pubKey!); // the old pubkey
|
await deletePeer(site.exitNodeId, site.pubKey!); // the old pubkey
|
||||||
await addPeer(site.exitNodeId, {
|
await addPeer(site.exitNodeId, {
|
||||||
publicKey: pubKey,
|
publicKey: pubKey,
|
||||||
|
|||||||
@@ -255,20 +255,6 @@ export async function createClient(
|
|||||||
|
|
||||||
let newClient: Client | null = null;
|
let newClient: Client | null = null;
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
// TODO: more intelligent way to pick the exit node
|
|
||||||
const exitNodesList = await listExitNodes(orgId);
|
|
||||||
const randomExitNode =
|
|
||||||
exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
|
|
||||||
|
|
||||||
if (!randomExitNode) {
|
|
||||||
return next(
|
|
||||||
createHttpError(
|
|
||||||
HttpCode.NOT_FOUND,
|
|
||||||
`No exit nodes available. ${build == "saas" ? "Please contact support." : "You need to install gerbil to use the clients."}`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const [adminRole] = await trx
|
const [adminRole] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(roles)
|
.from(roles)
|
||||||
@@ -287,7 +273,6 @@ export async function createClient(
|
|||||||
.insert(clients)
|
.insert(clients)
|
||||||
.values({
|
.values({
|
||||||
niceId,
|
niceId,
|
||||||
exitNodeId: randomExitNode.exitNodeId,
|
|
||||||
orgId,
|
orgId,
|
||||||
name,
|
name,
|
||||||
subnet: updatedSubnet,
|
subnet: updatedSubnet,
|
||||||
|
|||||||
@@ -222,11 +222,6 @@ export async function createUserClient(
|
|||||||
|
|
||||||
let newClient: Client | null = null;
|
let newClient: Client | null = null;
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
// TODO: more intelligent way to pick the exit node
|
|
||||||
const exitNodesList = await listExitNodes(orgId);
|
|
||||||
const randomExitNode =
|
|
||||||
exitNodesList[Math.floor(Math.random() * exitNodesList.length)];
|
|
||||||
|
|
||||||
const [adminRole] = await trx
|
const [adminRole] = await trx
|
||||||
.select()
|
.select()
|
||||||
.from(roles)
|
.from(roles)
|
||||||
@@ -244,7 +239,6 @@ export async function createUserClient(
|
|||||||
[newClient] = await trx
|
[newClient] = await trx
|
||||||
.insert(clients)
|
.insert(clients)
|
||||||
.values({
|
.values({
|
||||||
exitNodeId: randomExitNode.exitNodeId,
|
|
||||||
orgId,
|
orgId,
|
||||||
niceId,
|
niceId,
|
||||||
name,
|
name,
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ export async function generateRelayMappings(exitNode: ExitNode) {
|
|||||||
// Filter to sites with the required fields up front so the rest of the
|
// Filter to sites with the required fields up front so the rest of the
|
||||||
// function can safely treat endpoint/subnet/listenPort as defined.
|
// function can safely treat endpoint/subnet/listenPort as defined.
|
||||||
const validSites = sitesRes.filter(
|
const validSites = sitesRes.filter(
|
||||||
(s) => s.endpoint && s.subnet && s.listenPort
|
(s) => s.endpoint && s.exitNodeSubnet && s.listenPort
|
||||||
);
|
);
|
||||||
|
|
||||||
if (validSites.length === 0) {
|
if (validSites.length === 0) {
|
||||||
@@ -136,7 +136,7 @@ export async function generateRelayMappings(exitNode: ExitNode) {
|
|||||||
if (
|
if (
|
||||||
peer.orgId == null ||
|
peer.orgId == null ||
|
||||||
!peer.endpoint ||
|
!peer.endpoint ||
|
||||||
!peer.subnet ||
|
!peer.exitNodeSubnet ||
|
||||||
!peer.listenPort
|
!peer.listenPort
|
||||||
) {
|
) {
|
||||||
continue;
|
continue;
|
||||||
@@ -183,7 +183,7 @@ export async function generateRelayMappings(exitNode: ExitNode) {
|
|||||||
// Process each site using the pre-fetched data.
|
// Process each site using the pre-fetched data.
|
||||||
for (const site of validSites) {
|
for (const site of validSites) {
|
||||||
const siteDestination: PeerDestination = {
|
const siteDestination: PeerDestination = {
|
||||||
destinationIP: site.subnet!.split("/")[0],
|
destinationIP: site.exitNodeSubnet!.split("/")[0],
|
||||||
destinationPort: site.listenPort! || 1 // this satisfies gerbil for now but should be reevaluated
|
destinationPort: site.listenPort! || 1 // this satisfies gerbil for now but should be reevaluated
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -207,7 +207,7 @@ export async function generateRelayMappings(exitNode: ExitNode) {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
addDestination(site.endpoint!, {
|
addDestination(site.endpoint!, {
|
||||||
destinationIP: peer.subnet!.split("/")[0],
|
destinationIP: peer.exitNodeSubnet!.split("/")[0],
|
||||||
destinationPort: peer.listenPort! || 1 // this satisfies gerbil for now but should be reevaluated
|
destinationPort: peer.listenPort! || 1 // this satisfies gerbil for now but should be reevaluated
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export async function generateGerbilConfig(exitNode: ExitNode) {
|
|||||||
and(
|
and(
|
||||||
eq(sites.exitNodeId, exitNode.exitNodeId),
|
eq(sites.exitNodeId, exitNode.exitNodeId),
|
||||||
isNotNull(sites.pubKey),
|
isNotNull(sites.pubKey),
|
||||||
isNotNull(sites.subnet)
|
isNotNull(sites.exitNodeSubnet)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ export async function generateGerbilConfig(exitNode: ExitNode) {
|
|||||||
} else if (site.type === "newt") {
|
} else if (site.type === "newt") {
|
||||||
return {
|
return {
|
||||||
publicKey: site.pubKey,
|
publicKey: site.pubKey,
|
||||||
allowedIps: [site.subnet!]
|
allowedIps: [site.exitNodeSubnet!]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ export async function updateAndGenerateEndpointDestinations(
|
|||||||
.select({
|
.select({
|
||||||
siteId: sites.siteId,
|
siteId: sites.siteId,
|
||||||
newtId: newts.newtId,
|
newtId: newts.newtId,
|
||||||
subnet: sites.subnet,
|
subnet: sites.exitNodeSubnet,
|
||||||
listenPort: sites.listenPort,
|
listenPort: sites.listenPort,
|
||||||
publicKey: sites.publicKey,
|
publicKey: sites.publicKey,
|
||||||
endpoint: clientSitesAssociationsCache.endpoint,
|
endpoint: clientSitesAssociationsCache.endpoint,
|
||||||
|
|||||||
+10
-26
@@ -2,14 +2,17 @@ import { db, sites } from "@server/db";
|
|||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import { exitNodes, Newt } from "@server/db";
|
import { exitNodes, Newt } from "@server/db";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { ne, eq, or, and, count } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
||||||
|
import { calculateExitNodeWeight } from "@server/lib/exitNodes";
|
||||||
|
|
||||||
export const handleNewtPingRequestMessage: MessageHandler = async (context) => {
|
export const handleNewtExitNodesRequestMessage: MessageHandler = async (
|
||||||
|
context
|
||||||
|
) => {
|
||||||
const { message, client, sendToClient } = context;
|
const { message, client, sendToClient } = context;
|
||||||
const newt = client as Newt;
|
const newt = client as Newt;
|
||||||
|
|
||||||
logger.info("Handling ping request newt message!");
|
logger.info("Handling exit nodes request newt message!");
|
||||||
|
|
||||||
if (!newt) {
|
if (!newt) {
|
||||||
logger.warn("Newt not found");
|
logger.warn("Newt not found");
|
||||||
@@ -54,34 +57,15 @@ export const handleNewtPingRequestMessage: MessageHandler = async (context) => {
|
|||||||
|
|
||||||
const exitNodesPayload = await Promise.all(
|
const exitNodesPayload = await Promise.all(
|
||||||
exitNodesList.map(async (node) => {
|
exitNodesList.map(async (node) => {
|
||||||
// (MAX_CONNECTIONS - current_connections) / MAX_CONNECTIONS)
|
const weight = await calculateExitNodeWeight(
|
||||||
// higher = more desirable
|
node.exitNodeId,
|
||||||
// like saying, this node has x% of its capacity left
|
node.maxConnections
|
||||||
|
|
||||||
let weight = 1;
|
|
||||||
const maxConnections = node.maxConnections;
|
|
||||||
if (maxConnections !== null && maxConnections !== undefined) {
|
|
||||||
const [currentConnections] = await db
|
|
||||||
.select({
|
|
||||||
count: count()
|
|
||||||
})
|
|
||||||
.from(sites)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(sites.exitNodeId, node.exitNodeId),
|
|
||||||
eq(sites.online, true)
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (currentConnections.count >= maxConnections) {
|
if (weight === null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
weight =
|
|
||||||
(maxConnections - currentConnections.count) /
|
|
||||||
maxConnections;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
exitNodeId: node.exitNodeId,
|
exitNodeId: node.exitNodeId,
|
||||||
exitNodeName: node.name,
|
exitNodeName: node.name,
|
||||||
@@ -95,16 +95,16 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
.limit(1);
|
.limit(1);
|
||||||
if (
|
if (
|
||||||
exitNode.reachableAt &&
|
exitNode.reachableAt &&
|
||||||
existingSite.subnet &&
|
existingSite.exitNodeSubnet &&
|
||||||
existingSite.listenPort
|
existingSite.listenPort
|
||||||
) {
|
) {
|
||||||
const payload = {
|
const payload = {
|
||||||
oldDestination: {
|
oldDestination: {
|
||||||
destinationIP: existingSite.subnet?.split("/")[0],
|
destinationIP: existingSite.exitNodeSubnet?.split("/")[0],
|
||||||
destinationPort: existingSite.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
destinationPort: existingSite.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
||||||
},
|
},
|
||||||
newDestination: {
|
newDestination: {
|
||||||
destinationIP: site.subnet?.split("/")[0],
|
destinationIP: site.exitNodeSubnet?.split("/")[0],
|
||||||
destinationPort: site.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
destinationPort: site.listenPort || 1 // this satisfies gerbil for now but should be reevaluated
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -132,7 +132,10 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
|
|||||||
({ targets: dedupedTargets, certs } = dedupeCertsForTargets(targets));
|
({ targets: dedupedTargets, certs } = dedupeCertsForTargets(targets));
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetsToSend = await convertTargetsIfNecessary(newt.newtId, dedupedTargets); // for backward compatibility with old newt versions that don't support the new target format
|
const targetsToSend = await convertTargetsIfNecessary(
|
||||||
|
newt.newtId,
|
||||||
|
dedupedTargets
|
||||||
|
); // for backward compatibility with old newt versions that don't support the new target format
|
||||||
|
|
||||||
return {
|
return {
|
||||||
message: {
|
message: {
|
||||||
|
|||||||
@@ -1,30 +1,20 @@
|
|||||||
import { db, ExitNode, newts, remoteExitNodes, Transaction } from "@server/db";
|
import { db, newts, remoteExitNodes } from "@server/db";
|
||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import { exitNodes, Newt, sites } from "@server/db";
|
import { exitNodes, Newt, sites } from "@server/db";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import { addPeer, deletePeer } from "../gerbil/peers";
|
import { addPeer, deletePeer } from "../gerbil/peers";
|
||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import { findNextAvailableCidr } from "@server/lib/ip";
|
|
||||||
import {
|
import {
|
||||||
|
ExitNodePingResult,
|
||||||
selectBestExitNode,
|
selectBestExitNode,
|
||||||
verifyExitNodeOrgAccess
|
verifyExitNodeOrgAccess
|
||||||
} from "#dynamic/lib/exitNodes";
|
} from "#dynamic/lib/exitNodes";
|
||||||
|
import { getUniqueSubnetForExitNode } from "@server/lib/exitNodes";
|
||||||
import { fetchContainers } from "./dockerSocket";
|
import { fetchContainers } from "./dockerSocket";
|
||||||
import { lockManager } from "#dynamic/lib/lock";
|
|
||||||
import { buildTargetConfigurationForNewtClient } from "./buildConfiguration";
|
import { buildTargetConfigurationForNewtClient } from "./buildConfiguration";
|
||||||
import { canCompress } from "@server/lib/clientVersionChecks";
|
import { canCompress } from "@server/lib/clientVersionChecks";
|
||||||
|
|
||||||
export type ExitNodePingResult = {
|
|
||||||
exitNodeId: number;
|
|
||||||
latencyMs: number;
|
|
||||||
weight: number;
|
|
||||||
error?: string;
|
|
||||||
exitNodeName: string;
|
|
||||||
endpoint: string;
|
|
||||||
wasPreviouslyConnected: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
||||||
const { message, client, sendToClient } = context;
|
const { message, client, sendToClient } = context;
|
||||||
const newt = client as Newt;
|
const newt = client as Newt;
|
||||||
@@ -94,9 +84,12 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
fetchContainers(newt.newtId);
|
fetchContainers(newt.newtId);
|
||||||
}
|
}
|
||||||
|
|
||||||
let siteSubnet = oldSite.subnet;
|
let siteSubnet = oldSite.exitNodeSubnet;
|
||||||
let exitNodeIdToQuery = oldSite.exitNodeId;
|
let exitNodeIdToQuery = oldSite.exitNodeId;
|
||||||
if (exitNodeId && (oldSite.exitNodeId !== exitNodeId || !oldSite.subnet)) {
|
if (
|
||||||
|
exitNodeId &&
|
||||||
|
(oldSite.exitNodeId !== exitNodeId || !oldSite.exitNodeSubnet)
|
||||||
|
) {
|
||||||
// This effectively moves the exit node to the new one
|
// This effectively moves the exit node to the new one
|
||||||
exitNodeIdToQuery = exitNodeId; // Use the provided exitNodeId if it differs from the site's exitNodeId
|
exitNodeIdToQuery = exitNodeId; // Use the provided exitNodeId if it differs from the site's exitNodeId
|
||||||
|
|
||||||
@@ -115,7 +108,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newSubnet = await getUniqueSubnetForSite(exitNode);
|
const newSubnet = await getUniqueSubnetForExitNode(exitNode);
|
||||||
|
|
||||||
if (!newSubnet) {
|
if (!newSubnet) {
|
||||||
logger.error(
|
logger.error(
|
||||||
@@ -131,7 +124,7 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
.set({
|
.set({
|
||||||
pubKey: publicKey,
|
pubKey: publicKey,
|
||||||
exitNodeId: exitNodeId,
|
exitNodeId: exitNodeId,
|
||||||
subnet: newSubnet
|
exitNodeSubnet: newSubnet
|
||||||
})
|
})
|
||||||
.where(eq(sites.siteId, siteId))
|
.where(eq(sites.siteId, siteId))
|
||||||
.returning();
|
.returning();
|
||||||
@@ -250,40 +243,3 @@ export const handleNewtRegisterMessage: MessageHandler = async (context) => {
|
|||||||
excludeSender: false // Include sender in broadcast
|
excludeSender: false // Include sender in broadcast
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getUniqueSubnetForSite(
|
|
||||||
exitNode: ExitNode,
|
|
||||||
trx: Transaction | typeof db = db
|
|
||||||
): Promise<string | null> {
|
|
||||||
const lockKey = `subnet-allocation:${exitNode.exitNodeId}`;
|
|
||||||
|
|
||||||
return await lockManager.withLock(
|
|
||||||
lockKey,
|
|
||||||
async () => {
|
|
||||||
const sitesQuery = await trx
|
|
||||||
.select({
|
|
||||||
subnet: sites.subnet
|
|
||||||
})
|
|
||||||
.from(sites)
|
|
||||||
.where(eq(sites.exitNodeId, exitNode.exitNodeId));
|
|
||||||
|
|
||||||
const blockSize = config.getRawConfig().gerbil.site_block_size;
|
|
||||||
const subnets = sitesQuery
|
|
||||||
.map((site) => site.subnet)
|
|
||||||
.filter(
|
|
||||||
(subnet) =>
|
|
||||||
subnet &&
|
|
||||||
/^(\d{1,3}\.){3}\d{1,3}\/\d{1,2}$/.test(subnet)
|
|
||||||
)
|
|
||||||
.filter((subnet) => subnet !== null);
|
|
||||||
subnets.push(exitNode.address.replace(/\/\d+$/, `/${blockSize}`));
|
|
||||||
const newSubnet = findNextAvailableCidr(
|
|
||||||
subnets,
|
|
||||||
blockSize,
|
|
||||||
exitNode.address
|
|
||||||
);
|
|
||||||
return newSubnet;
|
|
||||||
},
|
|
||||||
5000 // 5 second lock TTL - subnet allocation should be quick
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export * from "./handleNewtRegisterMessage";
|
|||||||
export * from "./handleReceiveBandwidthMessage";
|
export * from "./handleReceiveBandwidthMessage";
|
||||||
export * from "./handleNewtGetConfigMessage";
|
export * from "./handleNewtGetConfigMessage";
|
||||||
export * from "./handleSocketMessages";
|
export * from "./handleSocketMessages";
|
||||||
export * from "./handleNewtPingRequestMessage";
|
export * from "./handleNewtExitNodesRequestMessage";
|
||||||
export * from "./handleApplyBlueprintMessage";
|
export * from "./handleApplyBlueprintMessage";
|
||||||
export * from "./handleNewtPingMessage";
|
export * from "./handleNewtPingMessage";
|
||||||
export * from "./handleNewtDisconnectingMessage";
|
export * from "./handleNewtDisconnectingMessage";
|
||||||
|
|||||||
@@ -48,10 +48,6 @@ export async function buildSiteConfigurationForOlmClient(
|
|||||||
)
|
)
|
||||||
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
.where(eq(clientSitesAssociationsCache.clientId, client.clientId));
|
||||||
|
|
||||||
if (sitesData.length === 0) {
|
|
||||||
return siteConfigurations;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Batch-fetch every site resource this client has access to across ALL sites
|
// Batch-fetch every site resource this client has access to across ALL sites
|
||||||
// in a single query, then group by siteId in memory. This avoids issuing one
|
// in a single query, then group by siteId in memory. This avoids issuing one
|
||||||
// query per site (which would be N round-trips for N sites).
|
// query per site (which would be N round-trips for N sites).
|
||||||
@@ -68,8 +64,8 @@ export async function buildSiteConfigurationForOlmClient(
|
|||||||
clientSiteResourcesAssociationsCache.siteResourceId
|
clientSiteResourcesAssociationsCache.siteResourceId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.innerJoin(networks, eq(siteResources.networkId, networks.networkId))
|
.leftJoin(networks, eq(siteResources.networkId, networks.networkId))
|
||||||
.innerJoin(siteNetworks, eq(networks.networkId, siteNetworks.networkId))
|
.leftJoin(siteNetworks, eq(networks.networkId, siteNetworks.networkId))
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(
|
eq(
|
||||||
@@ -80,8 +76,20 @@ export async function buildSiteConfigurationForOlmClient(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const haveInferenceResources = allClientSiteResources.some(
|
||||||
|
(row) => row.siteResource.requiresExitNodeConnection === true
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sitesData.length === 0) {
|
||||||
|
return { siteConfigurations, haveInferenceResources };
|
||||||
|
}
|
||||||
|
|
||||||
const siteResourcesBySiteId = new Map<number, SiteResource[]>();
|
const siteResourcesBySiteId = new Map<number, SiteResource[]>();
|
||||||
for (const row of allClientSiteResources) {
|
for (const row of allClientSiteResources) {
|
||||||
|
if (!row.siteId) {
|
||||||
|
// because we are doing a leftJoin above to get the inference resources without a network / sites
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const arr = siteResourcesBySiteId.get(row.siteId);
|
const arr = siteResourcesBySiteId.get(row.siteId);
|
||||||
if (arr) {
|
if (arr) {
|
||||||
arr.push(row.siteResource);
|
arr.push(row.siteResource);
|
||||||
@@ -167,7 +175,7 @@ export async function buildSiteConfigurationForOlmClient(
|
|||||||
peerOps.push(deletePeer(site.siteId, client.pubKey!));
|
peerOps.push(deletePeer(site.siteId, client.pubKey!));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!site.subnet) {
|
if (!site.exitNodeSubnet) {
|
||||||
logger.debug(`Site ${site.siteId} has no subnet, skipping`);
|
logger.debug(`Site ${site.siteId} has no subnet, skipping`);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -226,5 +234,5 @@ export async function buildSiteConfigurationForOlmClient(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return siteConfigurations;
|
return { siteConfigurations, haveInferenceResources };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import { db, clients } from "@server/db";
|
||||||
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
|
import { exitNodes, Olm } from "@server/db";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { listExitNodes } from "#dynamic/lib/exitNodes";
|
||||||
|
import { calculateExitNodeWeight } from "@server/lib/exitNodes";
|
||||||
|
|
||||||
|
export const handleOlmExitNodesRequestMessage: MessageHandler = async (
|
||||||
|
context
|
||||||
|
) => {
|
||||||
|
const { message, client: olmClient, sendToClient } = context;
|
||||||
|
const olm = olmClient as Olm;
|
||||||
|
|
||||||
|
logger.info("Handling exit nodes request olm message!");
|
||||||
|
|
||||||
|
if (!olm) {
|
||||||
|
logger.warn("olm not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the olm's orgId through the client relationship
|
||||||
|
if (!olm.clientId) {
|
||||||
|
logger.warn("olm clientId not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [client] = await db
|
||||||
|
.select({ orgId: clients.orgId })
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.clientId, olm.clientId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!client || !client.orgId) {
|
||||||
|
logger.warn("client not found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { noCloud, chainId } = message.data;
|
||||||
|
|
||||||
|
const exitNodesList = await listExitNodes(
|
||||||
|
client.orgId,
|
||||||
|
true,
|
||||||
|
noCloud || false,
|
||||||
|
olm.clientId
|
||||||
|
); // filter for only the online ones
|
||||||
|
|
||||||
|
let lastExitNodeId = null;
|
||||||
|
if (olm.clientId) {
|
||||||
|
const [lastExitNode] = await db
|
||||||
|
.select()
|
||||||
|
.from(clients)
|
||||||
|
.where(eq(clients.clientId, olm.clientId))
|
||||||
|
.limit(1);
|
||||||
|
lastExitNodeId = lastExitNode?.exitNodeId || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exitNodesPayload = await Promise.all(
|
||||||
|
exitNodesList.map(async (node) => {
|
||||||
|
const weight = await calculateExitNodeWeight(
|
||||||
|
node.exitNodeId,
|
||||||
|
node.maxConnections
|
||||||
|
);
|
||||||
|
|
||||||
|
if (weight === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
exitNodeId: node.exitNodeId,
|
||||||
|
exitNodeName: node.name,
|
||||||
|
endpoint: node.endpoint,
|
||||||
|
weight,
|
||||||
|
wasPreviouslyConnected: node.exitNodeId === lastExitNodeId
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// filter out null values
|
||||||
|
const filteredExitNodes = exitNodesPayload.filter((node) => node !== null);
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: {
|
||||||
|
type: "olm/ping/exitNodes",
|
||||||
|
data: {
|
||||||
|
exitNodes: filteredExitNodes,
|
||||||
|
chainId: chainId
|
||||||
|
}
|
||||||
|
},
|
||||||
|
broadcast: false, // Send to all clients
|
||||||
|
excludeSender: false // Include sender in broadcast
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { db, orgs, primaryDb } from "@server/db";
|
import { db, ExitNode, orgs, primaryDb } from "@server/db";
|
||||||
import { MessageHandler } from "@server/routers/ws";
|
import { MessageHandler } from "@server/routers/ws";
|
||||||
import {
|
import {
|
||||||
clients,
|
clients,
|
||||||
@@ -22,6 +22,12 @@ import { canCompress } from "@server/lib/clientVersionChecks";
|
|||||||
import config from "@server/lib/config";
|
import config from "@server/lib/config";
|
||||||
import cache from "#dynamic/lib/cache"; // not using regional here because we need this in the register message handler before we know where the client is
|
import cache from "#dynamic/lib/cache"; // not using regional here because we need this in the register message handler before we know where the client is
|
||||||
import { waitForClientRebuildIdle } from "@server/lib/rebuildClientAssociations";
|
import { waitForClientRebuildIdle } from "@server/lib/rebuildClientAssociations";
|
||||||
|
import {
|
||||||
|
ExitNodePingResult,
|
||||||
|
selectBestExitNode,
|
||||||
|
verifyExitNodeOrgAccess
|
||||||
|
} from "#dynamic/lib/exitNodes";
|
||||||
|
import { getUniqueSubnetForExitNode } from "@server/lib/exitNodes";
|
||||||
|
|
||||||
const HOLEPUNCH_STALE_CHAIN_THRESHOLD = 18;
|
const HOLEPUNCH_STALE_CHAIN_THRESHOLD = 18;
|
||||||
const HOLEPUNCH_STALE_CHAIN_TTL_SECONDS = 1800;
|
const HOLEPUNCH_STALE_CHAIN_TTL_SECONDS = 1800;
|
||||||
@@ -49,11 +55,20 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
olmAgent,
|
olmAgent,
|
||||||
orgId,
|
orgId,
|
||||||
userToken,
|
userToken,
|
||||||
|
pingResults,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
postures,
|
postures,
|
||||||
|
backwardsCompatible,
|
||||||
chainId
|
chainId
|
||||||
} = message.data;
|
} = message.data;
|
||||||
|
|
||||||
|
if (backwardsCompatible) {
|
||||||
|
logger.debug(
|
||||||
|
"[handleOlmRegisterMessage] Backwards compatible mode detected - not sending connect message and waiting for ping response."
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!olm.clientId) {
|
if (!olm.clientId) {
|
||||||
logger.warn("[handleOlmRegisterMessage] Olm client ID not found");
|
logger.warn("[handleOlmRegisterMessage] Olm client ID not found");
|
||||||
sendOlmError(OlmErrorCodes.CLIENT_ID_NOT_FOUND, olm.olmId);
|
sendOlmError(OlmErrorCodes.CLIENT_ID_NOT_FOUND, olm.olmId);
|
||||||
@@ -284,7 +299,65 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (client.pubKey !== publicKey || client.archived) {
|
let exitNodeId: number | undefined;
|
||||||
|
if (pingResults) {
|
||||||
|
const bestPingResult = selectBestExitNode(
|
||||||
|
pingResults as ExitNodePingResult[]
|
||||||
|
);
|
||||||
|
if (!bestPingResult) {
|
||||||
|
logger.warn("No suitable exit node found based on ping results");
|
||||||
|
}
|
||||||
|
exitNodeId = bestPingResult?.exitNodeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
let clientSubnet = client.exitNodeSubnet;
|
||||||
|
let exitNode: ExitNode | null = null;
|
||||||
|
if (
|
||||||
|
exitNodeId &&
|
||||||
|
(client.exitNodeId !== exitNodeId || !client.exitNodeSubnet)
|
||||||
|
) {
|
||||||
|
const { exitNode: exitNodeResult, hasAccess } =
|
||||||
|
await verifyExitNodeOrgAccess(exitNodeId, client.orgId);
|
||||||
|
|
||||||
|
exitNode = exitNodeResult;
|
||||||
|
|
||||||
|
if (!exitNode) {
|
||||||
|
logger.warn("[handleOlmRegisterMessage] Exit node not found", {
|
||||||
|
orgId: client.orgId,
|
||||||
|
clientId: client.clientId
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAccess) {
|
||||||
|
logger.warn(
|
||||||
|
"[handleOlmRegisterMessage] Not authorized to use this exit node",
|
||||||
|
{ orgId: client.orgId, clientId: client.clientId }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: IF WE DO NOT HAVE AN INFERENCE RESOURCE DO WE NEED TO BE HOLDING A SUBNET ON THE CLIENT?
|
||||||
|
|
||||||
|
const newSubnet = await getUniqueSubnetForExitNode(exitNode);
|
||||||
|
|
||||||
|
if (!newSubnet) {
|
||||||
|
logger.error(
|
||||||
|
`[handleOlmRegisterMessage] No available subnets found for exit node id ${exitNodeId} and client id ${client.clientId}`,
|
||||||
|
{ orgId: client.orgId, clientId: client.clientId }
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clientSubnet = newSubnet;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
client.pubKey !== publicKey ||
|
||||||
|
client.archived ||
|
||||||
|
client.exitNodeId !== exitNodeId ||
|
||||||
|
client.exitNodeSubnet !== clientSubnet
|
||||||
|
) {
|
||||||
logger.info(
|
logger.info(
|
||||||
"[handleOlmRegisterMessage] Public key mismatch. Updating public key and clearing session info...",
|
"[handleOlmRegisterMessage] Public key mismatch. Updating public key and clearing session info...",
|
||||||
{ orgId: client.orgId, clientId: client.clientId }
|
{ orgId: client.orgId, clientId: client.clientId }
|
||||||
@@ -294,7 +367,9 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
.update(clients)
|
.update(clients)
|
||||||
.set({
|
.set({
|
||||||
pubKey: publicKey,
|
pubKey: publicKey,
|
||||||
archived: false
|
archived: false,
|
||||||
|
exitNodeId: exitNodeId, // this can be undefined if no exit node was selected, which is fine just means we cant talk to the node or connect to it
|
||||||
|
exitNodeSubnet: clientSubnet
|
||||||
})
|
})
|
||||||
.where(eq(clients.clientId, client.clientId));
|
.where(eq(clients.clientId, client.clientId));
|
||||||
|
|
||||||
@@ -379,7 +454,8 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
// 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
|
||||||
await waitForClientRebuildIdle(olm.clientId);
|
await waitForClientRebuildIdle(olm.clientId);
|
||||||
|
|
||||||
const siteConfigurations = await buildSiteConfigurationForOlmClient(
|
const { siteConfigurations, haveInferenceResources } =
|
||||||
|
await buildSiteConfigurationForOlmClient(
|
||||||
client,
|
client,
|
||||||
publicKey,
|
publicKey,
|
||||||
relay,
|
relay,
|
||||||
@@ -394,6 +470,19 @@ export const handleOlmRegisterMessage: MessageHandler = async (context) => {
|
|||||||
sites: siteConfigurations,
|
sites: siteConfigurations,
|
||||||
tunnelIP: client.subnet,
|
tunnelIP: client.subnet,
|
||||||
utilitySubnet: org.utilitySubnet,
|
utilitySubnet: org.utilitySubnet,
|
||||||
|
exitNode:
|
||||||
|
exitNode && client.exitNodeSubnet
|
||||||
|
? {
|
||||||
|
connect: haveInferenceResources, // we do not need to connect to the exit node if we do not have inference resources
|
||||||
|
endpoint: `${exitNode.endpoint}:${exitNode.listenPort}`,
|
||||||
|
relayPort:
|
||||||
|
config.getRawConfig().gerbil
|
||||||
|
.clients_start_port,
|
||||||
|
publicKey: exitNode.publicKey,
|
||||||
|
serverIP: exitNode.address.split("/")[0],
|
||||||
|
tunnelIP: client.exitNodeSubnet.split("/")[0]
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
chainId: chainId
|
chainId: chainId
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -15,3 +15,4 @@ export * from "./handleOlmServerInitAddPeerHandshake";
|
|||||||
export * from "./offlineChecker";
|
export * from "./offlineChecker";
|
||||||
export * from "./handleOlmUnLocalMessage";
|
export * from "./handleOlmUnLocalMessage";
|
||||||
export * from "./handleOlmLocalMessage";
|
export * from "./handleOlmLocalMessage";
|
||||||
|
export * from "./handleOlmExitNodesRequestMessage";
|
||||||
|
|||||||
@@ -311,13 +311,13 @@ export async function createSite(
|
|||||||
// lets also make sure there is no overlap with other sites on the exit node
|
// lets also make sure there is no overlap with other sites on the exit node
|
||||||
const sitesQuery = await db
|
const sitesQuery = await db
|
||||||
.select({
|
.select({
|
||||||
subnet: sites.subnet
|
subnet: sites.exitNodeSubnet
|
||||||
})
|
})
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(
|
.where(
|
||||||
and(
|
and(
|
||||||
eq(sites.exitNodeId, exitNodeId),
|
eq(sites.exitNodeId, exitNodeId),
|
||||||
eq(sites.subnet, subnet)
|
eq(sites.exitNodeSubnet, subnet)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -427,7 +427,7 @@ export async function createSite(
|
|||||||
exitNodeId,
|
exitNodeId,
|
||||||
name,
|
name,
|
||||||
niceId: updatedNiceId!,
|
niceId: updatedNiceId!,
|
||||||
subnet,
|
exitNodeSubnet: subnet,
|
||||||
type,
|
type,
|
||||||
pubKey: pubKey || null,
|
pubKey: pubKey || null,
|
||||||
status: "approved"
|
status: "approved"
|
||||||
@@ -444,7 +444,7 @@ export async function createSite(
|
|||||||
type,
|
type,
|
||||||
dockerSocketEnabled: false,
|
dockerSocketEnabled: false,
|
||||||
online: true,
|
online: true,
|
||||||
subnet: "0.0.0.0/32",
|
exitNodeSubnet: "0.0.0.0/32",
|
||||||
status: "approved"
|
status: "approved"
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ function querySitesBase() {
|
|||||||
niceId: sites.niceId,
|
niceId: sites.niceId,
|
||||||
name: sites.name,
|
name: sites.name,
|
||||||
pubKey: sites.pubKey,
|
pubKey: sites.pubKey,
|
||||||
subnet: sites.subnet,
|
subnet: sites.exitNodeSubnet,
|
||||||
megabytesIn: sites.megabytesIn,
|
megabytesIn: sites.megabytesIn,
|
||||||
megabytesOut: sites.megabytesOut,
|
megabytesOut: sites.megabytesOut,
|
||||||
orgName: orgs.name,
|
orgName: orgs.name,
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ const PickSiteDefaultsResponseDataSchema = z.object({
|
|||||||
clientAddress: z.string().optional()
|
clientAddress: z.string().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
registry.registerPath({
|
registry.registerPath({
|
||||||
method: "get",
|
method: "get",
|
||||||
path: "/org/{orgId}/pick-site-defaults",
|
path: "/org/{orgId}/pick-site-defaults",
|
||||||
@@ -60,7 +59,9 @@ registry.registerPath({
|
|||||||
description: "Successful response",
|
description: "Successful response",
|
||||||
content: {
|
content: {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
schema: createApiResponseSchema(PickSiteDefaultsResponseDataSchema)
|
schema: createApiResponseSchema(
|
||||||
|
PickSiteDefaultsResponseDataSchema
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -108,7 +109,7 @@ export async function pickSiteDefaults(
|
|||||||
// list all of the sites on that exit node
|
// list all of the sites on that exit node
|
||||||
const sitesQuery = await db
|
const sitesQuery = await db
|
||||||
.select({
|
.select({
|
||||||
subnet: sites.subnet
|
subnet: sites.exitNodeSubnet
|
||||||
})
|
})
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(eq(sites.exitNodeId, randomExitNode.exitNodeId));
|
.where(eq(sites.exitNodeId, randomExitNode.exitNodeId));
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ const createSiteResourceSchema = z
|
|||||||
name: z.string().min(1).max(255),
|
name: z.string().min(1).max(255),
|
||||||
niceId: z.string().optional(),
|
niceId: z.string().optional(),
|
||||||
// protocol: z.enum(["tcp", "udp"]).optional(),
|
// protocol: z.enum(["tcp", "udp"]).optional(),
|
||||||
mode: z.enum(["host", "cidr", "http", "ssh"]),
|
mode: z.enum(["host", "cidr", "http", "ssh", "inference"]),
|
||||||
ssl: z.boolean().optional(), // only used for http mode
|
ssl: z.boolean().optional(), // only used for http mode
|
||||||
scheme: z.enum(["http", "https"]).optional(),
|
scheme: z.enum(["http", "https"]).optional(),
|
||||||
siteIds: z.array(z.int()).optional(),
|
siteIds: z.array(z.int()).optional(),
|
||||||
@@ -171,6 +171,9 @@ const createSiteResourceSchema = z
|
|||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
|
if (data.mode == "inference") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
(data.siteIds !== undefined && data.siteIds.length > 0) ||
|
(data.siteIds !== undefined && data.siteIds.length > 0) ||
|
||||||
data.siteId !== undefined
|
data.siteId !== undefined
|
||||||
@@ -533,7 +536,9 @@ export async function createSiteResource(
|
|||||||
let newSiteResource: SiteResource | undefined;
|
let newSiteResource: SiteResource | undefined;
|
||||||
try {
|
try {
|
||||||
await db.transaction(async (trx) => {
|
await db.transaction(async (trx) => {
|
||||||
const [network] = await trx
|
let network: typeof networks.$inferSelect | undefined;
|
||||||
|
if (mode !== "inference") {
|
||||||
|
[network] = await trx
|
||||||
.insert(networks)
|
.insert(networks)
|
||||||
.values({
|
.values({
|
||||||
scope: "resource",
|
scope: "resource",
|
||||||
@@ -549,6 +554,7 @@ export async function createSiteResource(
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let tcpPortRangeStringAdjusted = tcpPortRangeString;
|
let tcpPortRangeStringAdjusted = tcpPortRangeString;
|
||||||
if (mode === "http") {
|
if (mode === "http") {
|
||||||
@@ -566,7 +572,7 @@ export async function createSiteResource(
|
|||||||
name,
|
name,
|
||||||
mode,
|
mode,
|
||||||
ssl,
|
ssl,
|
||||||
networkId: network.networkId,
|
networkId: network ? network.networkId : null,
|
||||||
destination: destination, // the ssh can be null
|
destination: destination, // the ssh can be null
|
||||||
scheme,
|
scheme,
|
||||||
destinationPort,
|
destinationPort,
|
||||||
@@ -582,7 +588,8 @@ export async function createSiteResource(
|
|||||||
(mode == "http" || mode == "ssh" ? true : false), // default to true for http resources, otherwise false
|
(mode == "http" || mode == "ssh" ? true : false), // default to true for http resources, otherwise false
|
||||||
domainId,
|
domainId,
|
||||||
subdomain: finalSubdomain,
|
subdomain: finalSubdomain,
|
||||||
fullDomain
|
fullDomain,
|
||||||
|
requiresExitNodeConnection: mode === "inference" // in the future we might want to have different modes that do this
|
||||||
};
|
};
|
||||||
if (isLicensedSshPam) {
|
if (isLicensedSshPam) {
|
||||||
if (authDaemonPort !== undefined)
|
if (authDaemonPort !== undefined)
|
||||||
@@ -600,12 +607,14 @@ export async function createSiteResource(
|
|||||||
|
|
||||||
//////////////////// update the associations ////////////////////
|
//////////////////// update the associations ////////////////////
|
||||||
|
|
||||||
|
if (network) {
|
||||||
for (const siteId of siteIds) {
|
for (const siteId of siteIds) {
|
||||||
await trx.insert(siteNetworks).values({
|
await trx.insert(siteNetworks).values({
|
||||||
siteId: siteId,
|
siteId: siteId,
|
||||||
networkId: network.networkId
|
networkId: network.networkId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const [adminRole] = await trx
|
const [adminRole] = await trx
|
||||||
.select()
|
.select()
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ const updateSiteResourceSchema = z
|
|||||||
)
|
)
|
||||||
.optional(),
|
.optional(),
|
||||||
// mode: z.enum(["host", "cidr", "port"]).optional(),
|
// mode: z.enum(["host", "cidr", "port"]).optional(),
|
||||||
mode: z.enum(["host", "cidr", "http", "ssh"]).optional(),
|
mode: z.enum(["host", "cidr", "http", "ssh", "inference"]).optional(),
|
||||||
ssl: z.boolean().optional(),
|
ssl: z.boolean().optional(),
|
||||||
scheme: z.enum(["http", "https"]).nullish(),
|
scheme: z.enum(["http", "https"]).nullish(),
|
||||||
destinationPort: z.int().positive().nullish(),
|
destinationPort: z.int().positive().nullish(),
|
||||||
@@ -173,6 +173,9 @@ const updateSiteResourceSchema = z
|
|||||||
)
|
)
|
||||||
.refine(
|
.refine(
|
||||||
(data) => {
|
(data) => {
|
||||||
|
if (data.mode == "inference") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// if neither is provided, the existing site associations are left unchanged
|
// if neither is provided, the existing site associations are left unchanged
|
||||||
if (data.siteIds === undefined && data.siteId === undefined) {
|
if (data.siteIds === undefined && data.siteId === undefined) {
|
||||||
return true;
|
return true;
|
||||||
@@ -579,13 +582,15 @@ export async function updateSiteResource(
|
|||||||
disableIcmp:
|
disableIcmp:
|
||||||
mode !== undefined
|
mode !== undefined
|
||||||
? disableIcmp ||
|
? disableIcmp ||
|
||||||
(mode == "http" || mode == "ssh"
|
(mode == "http" || mode == "ssh" ? true : false)
|
||||||
? true
|
|
||||||
: false)
|
|
||||||
: disableIcmp,
|
: disableIcmp,
|
||||||
domainId,
|
domainId,
|
||||||
subdomain: finalSubdomain,
|
subdomain: finalSubdomain,
|
||||||
fullDomain,
|
fullDomain,
|
||||||
|
networkId:
|
||||||
|
mode === "inference" ? null : undefined,
|
||||||
|
requiresExitNodeConnection:
|
||||||
|
mode !== undefined ? mode === "inference" : undefined,
|
||||||
...sshPamSet
|
...sshPamSet
|
||||||
})
|
})
|
||||||
.where(and(eq(siteResources.siteResourceId, siteResourceId)))
|
.where(and(eq(siteResources.siteResourceId, siteResourceId)))
|
||||||
@@ -593,7 +598,20 @@ export async function updateSiteResource(
|
|||||||
|
|
||||||
//////////////////// update the associations ////////////////////
|
//////////////////// update the associations ////////////////////
|
||||||
|
|
||||||
if (siteIds !== undefined) {
|
if (mode === "inference") {
|
||||||
|
// inference resources are not attached to any site network
|
||||||
|
if (existingSiteResource.networkId) {
|
||||||
|
await trx
|
||||||
|
.delete(siteNetworks)
|
||||||
|
.where(
|
||||||
|
eq(
|
||||||
|
siteNetworks.networkId,
|
||||||
|
existingSiteResource.networkId
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
updatedSiteIds = [];
|
||||||
|
} else if (siteIds !== undefined) {
|
||||||
// delete the site - site resources associations
|
// delete the site - site resources associations
|
||||||
await trx
|
await trx
|
||||||
.delete(siteNetworks)
|
.delete(siteNetworks)
|
||||||
|
|||||||
@@ -263,7 +263,7 @@ export async function createTarget(
|
|||||||
// make sure the target is within the site subnet
|
// make sure the target is within the site subnet
|
||||||
if (
|
if (
|
||||||
site.type == "wireguard" &&
|
site.type == "wireguard" &&
|
||||||
!isIpInCidr(targetData.ip, site.subnet!)
|
!isIpInCidr(targetData.ip, site.exitNodeSubnet!)
|
||||||
) {
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
handleNewtGetConfigMessage,
|
handleNewtGetConfigMessage,
|
||||||
handleDockerStatusMessage,
|
handleDockerStatusMessage,
|
||||||
handleDockerContainersMessage,
|
handleDockerContainersMessage,
|
||||||
handleNewtPingRequestMessage,
|
handleNewtExitNodesRequestMessage,
|
||||||
handleApplyBlueprintMessage,
|
handleApplyBlueprintMessage,
|
||||||
handleNewtPingMessage,
|
handleNewtPingMessage,
|
||||||
startNewtOfflineChecker,
|
startNewtOfflineChecker,
|
||||||
@@ -22,7 +22,8 @@ import {
|
|||||||
handleOlmDisconnectingMessage,
|
handleOlmDisconnectingMessage,
|
||||||
handleOlmServerInitAddPeerHandshake,
|
handleOlmServerInitAddPeerHandshake,
|
||||||
handleOlmLocalMessage,
|
handleOlmLocalMessage,
|
||||||
handleOlmUnLocalMessage
|
handleOlmUnLocalMessage,
|
||||||
|
handleOlmExitNodesRequestMessage
|
||||||
} from "../olm";
|
} from "../olm";
|
||||||
import { handleHealthcheckStatusMessage } from "../target";
|
import { handleHealthcheckStatusMessage } from "../target";
|
||||||
import { handleRoundTripMessage } from "./handleRoundTripMessage";
|
import { handleRoundTripMessage } from "./handleRoundTripMessage";
|
||||||
@@ -37,6 +38,7 @@ export const messageHandlers: Record<string, MessageHandler> = {
|
|||||||
"olm/wg/local": handleOlmLocalMessage,
|
"olm/wg/local": handleOlmLocalMessage,
|
||||||
"olm/wg/unlocal": handleOlmUnLocalMessage,
|
"olm/wg/unlocal": handleOlmUnLocalMessage,
|
||||||
"olm/ping": handleOlmPingMessage,
|
"olm/ping": handleOlmPingMessage,
|
||||||
|
"olm/ping/request": handleOlmExitNodesRequestMessage,
|
||||||
"olm/disconnecting": handleOlmDisconnectingMessage,
|
"olm/disconnecting": handleOlmDisconnectingMessage,
|
||||||
"newt/disconnecting": handleNewtDisconnectingMessage,
|
"newt/disconnecting": handleNewtDisconnectingMessage,
|
||||||
"newt/ping": handleNewtPingMessage,
|
"newt/ping": handleNewtPingMessage,
|
||||||
@@ -45,7 +47,7 @@ export const messageHandlers: Record<string, MessageHandler> = {
|
|||||||
"newt/receive-bandwidth": handleReceiveBandwidthMessage,
|
"newt/receive-bandwidth": handleReceiveBandwidthMessage,
|
||||||
"newt/socket/status": handleDockerStatusMessage,
|
"newt/socket/status": handleDockerStatusMessage,
|
||||||
"newt/socket/containers": handleDockerContainersMessage,
|
"newt/socket/containers": handleDockerContainersMessage,
|
||||||
"newt/ping/request": handleNewtPingRequestMessage,
|
"newt/ping/request": handleNewtExitNodesRequestMessage,
|
||||||
"newt/blueprint/apply": handleApplyBlueprintMessage,
|
"newt/blueprint/apply": handleApplyBlueprintMessage,
|
||||||
"newt/healthcheck/status": handleHealthcheckStatusMessage,
|
"newt/healthcheck/status": handleHealthcheckStatusMessage,
|
||||||
"ws/round-trip/complete": handleRoundTripMessage
|
"ws/round-trip/complete": handleRoundTripMessage
|
||||||
|
|||||||
@@ -72,8 +72,7 @@ export default function CredentialsPage() {
|
|||||||
const { data: latestVersions } = useQuery(
|
const { data: latestVersions } = useQuery(
|
||||||
productUpdatesQueries.latestVersion(true)
|
productUpdatesQueries.latestVersion(true)
|
||||||
);
|
);
|
||||||
const newtVersion =
|
const newtVersion = latestVersions?.data?.newt?.latestVersion ?? "latest";
|
||||||
latestVersions?.data?.newt?.latestVersion ?? "latest";
|
|
||||||
|
|
||||||
// Fetch site defaults for wireguard sites to show in obfuscated config
|
// Fetch site defaults for wireguard sites to show in obfuscated config
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -354,7 +353,7 @@ export default function CredentialsPage() {
|
|||||||
text={generateObfuscatedWireGuardConfig(
|
text={generateObfuscatedWireGuardConfig(
|
||||||
{
|
{
|
||||||
subnet:
|
subnet:
|
||||||
site?.subnet ||
|
site?.exitNodeSubnet ||
|
||||||
siteDefaults?.subnet ||
|
siteDefaults?.subnet ||
|
||||||
null,
|
null,
|
||||||
address:
|
address:
|
||||||
|
|||||||
@@ -134,7 +134,9 @@ export default function UptimeBar({
|
|||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
const allNoData = data.days.every((d) => d.status === "no_data");
|
const allNoData = data.days.every(
|
||||||
|
(d) => d.status === "no_data" || d.status === "unknown"
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("space-y-3", className)}>
|
<div className={cn("space-y-3", className)}>
|
||||||
|
|||||||
@@ -124,7 +124,9 @@ export function UptimeMiniBar({
|
|||||||
|
|
||||||
if (!data) return null;
|
if (!data) return null;
|
||||||
|
|
||||||
const allNoData = data.days.every((d) => d.status === "no_data");
|
const allNoData = data.days.every(
|
||||||
|
(d) => d.status === "no_data" || d.status === "unknown"
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user