Compare commits

..

72 Commits

Author SHA1 Message Date
Owen Schwartz 0e4cd3a5ba Merge pull request #3414 from fosrl/dependabot/go_modules/install/go-install-dependencies-3804ca7238
Bump golang.org/x/term from 0.44.0 to 0.45.0 in /install in the go-install-dependencies group across 1 directory
2026-07-29 18:00:54 -04:00
Owen Schwartz aeed271f3e Merge pull request #3494 from fosrl/dependabot/npm_and_yarn/next-16.2.11
Bump next from 16.2.6 to 16.2.11
2026-07-29 17:58:09 -04:00
Owen Schwartz d41e9bbe68 Merge pull request #3472 from fosrl/dependabot/npm_and_yarn/axios-1.18.0
Bump axios from 1.16.1 to 1.18.0
2026-07-29 17:57:46 -04:00
Owen Schwartz 904a5520b8 Merge pull request #3476 from fosrl/dependabot/npm_and_yarn/body-parser-2.3.0
Bump body-parser from 2.2.2 to 2.3.0
2026-07-29 17:57:34 -04:00
Owen Schwartz 0f43ae4871 Merge pull request #3486 from fosrl/dependabot/npm_and_yarn/fast-uri-3.1.4
Bump fast-uri from 3.1.2 to 3.1.4
2026-07-29 17:57:23 -04:00
Owen Schwartz bd0cf98319 Merge pull request #3489 from fosrl/dependabot/github_actions/github-actions-dependencies-3a33fbbccd
Bump the github-actions-dependencies group across 1 directory with 5 updates
2026-07-29 17:57:07 -04:00
Owen Schwartz fc7d6bbaf9 Merge pull request #3496 from fosrl/dependabot/npm_and_yarn/js-yaml-4.3.0
Bump js-yaml from 4.2.0 to 4.3.0
2026-07-29 17:56:37 -04:00
Owen Schwartz a69b310109 Merge pull request #3460 from shubhamsinnh/fix/idn-punycode-domain-validation
Fix Punycode domain validation
2026-07-29 17:29:25 -04:00
Milo Schwartz 1882d4df88 Merge pull request #3469 from fosrl/refactor/batch-status-requests
refactor: batch status histories
2026-07-29 17:27:11 -04:00
Milo Schwartz 9981c4c035 Merge pull request #3410 from Adityakk9031/#3408
fix: redirect to /auth/initial-setup after hitting auth rate limit (#…
2026-07-29 17:09:40 -04:00
Fred KISSIE bc267b7107 ♻️ refactor 2026-07-29 19:11:16 +01:00
Fred KISSIE 5cbb767e5f Set default certificates for private resources 2026-07-29 19:11:03 +01:00
Fred KISSIE ac79621cae ♻️ Adjust the polling interval to 30s for valid certs 2026-07-29 19:05:33 +01:00
Fred KISSIE 10773432bb ♻️ Batch certificate queries 2026-07-28 20:59:42 +01:00
dependabot[bot] d5a56fb71d Bump js-yaml from 4.2.0 to 4.3.0
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.2.0 to 4.3.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...4.3.0)

---
updated-dependencies:
- dependency-name: js-yaml
  dependency-version: 4.3.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 12:24:11 +00:00
dependabot[bot] b5518d029e Bump next from 16.2.6 to 16.2.11
Bumps [next](https://github.com/vercel/next.js) from 16.2.6 to 16.2.11.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.2.6...v16.2.11)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.2.11
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 07:10:39 +00:00
dependabot[bot] 0da96b06ea Bump the github-actions-dependencies group across 1 directory with 5 updates
Bumps the github-actions-dependencies group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [actions/checkout](https://github.com/actions/checkout) | `7.0.0` | `7.0.1` |
| [docker/login-action](https://github.com/docker/login-action) | `4.2.0` | `4.5.1` |
| [actions/setup-go](https://github.com/actions/setup-go) | `6.4.0` | `7.0.0` |
| [actions/setup-node](https://github.com/actions/setup-node) | `6.4.0` | `7.0.0` |
| [actions/stale](https://github.com/actions/stale) | `10.3.0` | `10.4.0` |



Updates `actions/checkout` from 7.0.0 to 7.0.1
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0...3d3c42e5aac5ba805825da76410c181273ba90b1)

Updates `docker/login-action` from 4.2.0 to 4.5.1
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/650006c6eb7dba73a995cc03b0b2d7f5ca915bee...abd2ef45e78c5afb21d64d4ca52ee8550d9572c7)

Updates `actions/setup-go` from 6.4.0 to 7.0.0
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/4a3601121dd01d1626a1e23e37211e3254c1c06c...b7ad1dad31e06c5925ef5d2fc7ad053ef454303e)

Updates `actions/setup-node` from 6.4.0 to 7.0.0
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e...820762786026740c76f36085b0efc47a31fe5020)

Updates `actions/stale` from 10.3.0 to 10.4.0
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899...1e223db275d687790206a7acac4d1a11bd6fe629)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: 7.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: github-actions-dependencies
- dependency-name: docker/login-action
  dependency-version: 4.5.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
- dependency-name: actions/setup-go
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions-dependencies
- dependency-name: actions/setup-node
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions-dependencies
- dependency-name: actions/stale
  dependency-version: 10.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 01:34:06 +00:00
dependabot[bot] 6649f15498 Bump fast-uri from 3.1.2 to 3.1.4
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.2 to 3.1.4.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.2...v3.1.4)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-25 12:40:05 +00:00
Fred KISSIE 49854f31a5 🚧 WIP: refactor useCertificate to use tanstack qeury 2026-07-24 21:03:52 +01:00
Fred KISSIE 762c79511b 🚧 Add batchedCertificate queries 2026-07-24 20:59:29 +01:00
Fred KISSIE 6e29ebd649 ♻️ refactor getbatchedcertificate 2026-07-24 20:58:47 +01:00
Fred KISSIE e128b8c282 get batched certificates endpoint 2026-07-24 19:07:37 +01:00
Fred KISSIE 832e382888 🚧 WIP: batch certificates 2026-07-23 22:00:31 +01:00
dependabot[bot] 8a68b899f5 Bump golang.org/x/term
Bumps the go-install-dependencies group with 1 update in the /install directory: [golang.org/x/term](https://github.com/golang/term).


Updates `golang.org/x/term` from 0.44.0 to 0.45.0
- [Commits](https://github.com/golang/term/compare/v0.44.0...v0.45.0)

---
updated-dependencies:
- dependency-name: golang.org/x/term
  dependency-version: 0.45.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: go-install-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-23 01:33:10 +00:00
Owen Schwartz 71f45b8a80 Merge pull request #3479 from fosrl/dev
1.21.0-s.1
2026-07-22 17:27:34 -04:00
Owen ecf008a8d9 Improve cert retreival and new newt combined certs 2026-07-22 17:22:51 -04:00
Fred KISSIE b3880e5c02 ♻️ refactor 2026-07-22 21:34:13 +01:00
Fred KISSIE 58004a8ec9 ♻️ little refactor 2026-07-22 21:25:31 +01:00
Fred KISSIE b0ff7d2707 ♻️ batch healthcheck status history 2026-07-22 21:06:40 +01:00
Fred KISSIE 262f7bd090 resource status histories 2026-07-22 20:57:32 +01:00
Fred KISSIE b1558b09b1 ♻️ refactor imports 2026-07-22 19:33:00 +01:00
Fred KISSIE a33de8268b 🐛 Get last known events working with sqlite 2026-07-22 19:32:53 +01:00
dependabot[bot] c92d5096c4 Bump body-parser from 2.2.2 to 2.3.0
Bumps [body-parser](https://github.com/expressjs/body-parser) from 2.2.2 to 2.3.0.
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/v2.2.2...v2.3.0)

---
updated-dependencies:
- dependency-name: body-parser
  dependency-version: 2.3.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-21 23:15:07 +00:00
Fred KISSIE 9867d3c876 ♻️ const instead of let 2026-07-21 20:31:57 +01:00
Fred KISSIE 70bddba55b Make batched status history work 2026-07-21 20:29:16 +01:00
Fred KISSIE 23181f4019 🚧 WIP: batched site status histories 2026-07-21 19:57:09 +01:00
Owen Schwartz 19c1c2042b Merge pull request #3468 from fosrl/fix/labels-dropdown-flicker
fix labels dropdown flicker if no changes applied
2026-07-20 20:28:37 -04:00
Fred KISSIE 9cc3190e3a 🚧 WIP: batched status 2026-07-20 20:37:52 +01:00
dependabot[bot] 72f179578b Bump axios from 1.16.1 to 1.18.0
Bumps [axios](https://github.com/axios/axios) from 1.16.1 to 1.18.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.16.1...v1.18.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.18.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-20 19:06:46 +00:00
Fred KISSIE 4c873e7c48 Merge branch 'dev' into refactor/batch-status-requests 2026-07-20 18:38:00 +01:00
Owen Schwartz 41c68148a9 Merge pull request #3467 from fosrl/dev
1.21.0
2026-07-20 11:46:48 -04:00
Owen Schwartz 5d38059b95 Merge pull request #3466 from fosrl/api-improvements
api improvements
2026-07-20 11:44:25 -04:00
Owen df0198df9c Update internal -> private 2026-07-20 11:44:07 -04:00
Owen 26713b53f6 Fix #3462 2026-07-20 11:44:07 -04:00
Owen 2bc6b28978 Unify put vs post 2026-07-20 11:44:07 -04:00
Owen f1ed4da8a4 Move to legacy 2026-07-20 11:44:07 -04:00
Owen adeefb9dbd Add missing endpoints to api 2026-07-20 11:44:05 -04:00
Owen bf1cc705a5 Update tags 2026-07-20 11:43:10 -04:00
Owen 0b82dae01e More route cleanup 2026-07-20 11:43:09 -04:00
Owen 75afe6ece2 Order more api routes 2026-07-20 11:43:09 -04:00
Owen 70e2fe1e4e Just add ordering 2026-07-20 11:43:06 -04:00
Owen 17e03457e1 Recategorize 2026-07-20 11:42:37 -04:00
Owen 4d8cb7e231 Add aliaes for public/private resources 2026-07-20 11:42:35 -04:00
Shubham Singh 0275f44056 Fix Punycode domain validation 2026-07-19 13:45:44 +05:30
Milo Schwartz c30fe5b574 Merge pull request #3457 from fosrl/dev
1.21.0
2026-07-17 17:17:35 -04:00
Owen Schwartz 113b7a0b84 Merge pull request #3453 from fosrl/dev
1.20.0-s.3
2026-07-16 21:39:00 -04:00
Owen Schwartz 5d20956a0e Merge pull request #3450 from fosrl/dev
1.20.0-s.2
2026-07-15 20:55:40 -04:00
Fred KISSIE 1580b7abff 🚧 wip: batch status histories 2026-07-10 07:02:22 +02:00
Milo Schwartz e5398d441e Merge pull request #3425 from fosrl/dev
Dev
2026-07-09 21:33:17 -04:00
Milo Schwartz d4138e2141 Merge pull request #3421 from fosrl/dev
1.20.0
2026-07-09 10:40:35 -04:00
Milo Schwartz 09d5d9082e Merge pull request #3413 from fosrl/dev
1.20.0
2026-07-08 15:30:03 -04:00
Aditya kumar singh bb5547a157 fix: redirect to /auth/initial-setup after hitting auth rate limit (#3408) 2026-07-08 23:33:57 +05:30
Owen Schwartz 5cb316f4e9 Merge pull request #3404 from fosrl/dev
Add ? to support undefined
2026-07-07 10:59:56 -04:00
Owen Schwartz 6b6c9cf4d8 Merge pull request #3403 from fosrl/dev
Move jit mode to a config param
2026-07-07 10:41:59 -04:00
Owen Schwartz 05617c63c0 Merge pull request #3402 from fosrl/dev
1.19.4-s.5
2026-07-07 10:21:57 -04:00
Owen Schwartz d4c52bbf2f Merge pull request #3396 from fosrl/dev
1.19.4-s.5
2026-07-03 16:15:18 -04:00
Owen Schwartz 87f50bf0cc Merge pull request #3389 from fosrl/dev
1.19.4-s.3
2026-07-03 10:29:15 -04:00
Owen Schwartz 2c66da1b19 Merge pull request #3386 from v1rusnl/main
Upgrade Traefik image to version 3.7
2026-07-03 10:18:44 -04:00
v1rusnl ab19955502 Upgrade Traefik image to version 3.7 2026-07-03 08:22:13 +02:00
v1rusnl 1db9dcec81 Update Traefik image version to v3.7 2026-07-03 08:21:12 +02:00
Owen Schwartz 49c2d3163e Merge pull request #3381 from fosrl/dev
dev
2026-07-02 10:56:39 -04:00
Owen Schwartz 45b9e13a13 Merge pull request #3378 from fosrl/dev
1.19.4-s.1
2026-07-01 21:48:01 -04:00
129 changed files with 4196 additions and 545 deletions
+9 -9
View File
@@ -62,7 +62,7 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- 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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- 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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Log in to Docker Hub - name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- 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@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.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@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 uses: docker/login-action@abd2ef45e78c5afb21d64d4ca52ee8550d9572c7 # v4.5.1
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
+2 -2
View File
@@ -21,10 +21,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Node.js - name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with: with:
node-version: '24' node-version: '24'
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
stale: stale:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0 - uses: actions/stale@1e223db275d687790206a7acac4d1a11bd6fe629 # v10.4.0
with: with:
days-before-stale: 14 days-before-stale: 14
days-before-close: 14 days-before-close: 14
+4 -4
View File
@@ -14,10 +14,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Node - name: Install Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- 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@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build Docker image pg - name: Build Docker image pg
run: make dev-build-pg run: make dev-build-pg
+1 -1
View File
@@ -41,7 +41,7 @@ services:
- 80:80 # Port for traefik because of the network_mode - 80:80 # Port for traefik because of the network_mode
traefik: traefik:
image: traefik:v3.6 image: traefik:v3.7
container_name: traefik container_name: traefik
restart: unless-stopped restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service network_mode: service:gerbil # Ports appear on the gerbil service
+1 -1
View File
@@ -50,7 +50,7 @@ services:
- 80:80{{end}} - 80:80{{end}}
traefik: traefik:
image: docker.io/traefik:v3.6 image: docker.io/traefik:v3.7
container_name: traefik container_name: traefik
restart: unless-stopped restart: unless-stopped
{{if .InstallGerbil}}network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}} {{if .InstallGerbil}}network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}}
+2 -2
View File
@@ -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.44.0 golang.org/x/term v0.45.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.46.0 // indirect golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.23.0 // indirect golang.org/x/text v0.23.0 // indirect
) )
+4 -4
View File
@@ -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.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
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=
+7 -7
View File
@@ -934,7 +934,7 @@
"newtVersion": "Version", "newtVersion": "Version",
"architecture": "Architecture", "architecture": "Architecture",
"sites": "Sites", "sites": "Sites",
"siteWgAnyClients": "Use any WireGuard client to connect. You will have to address internal resources using the peer IP.", "siteWgAnyClients": "Use any WireGuard client to connect. You will have to address private resources using the peer IP.",
"siteWgCompatibleAllClients": "Compatible with all WireGuard clients", "siteWgCompatibleAllClients": "Compatible with all WireGuard clients",
"siteWgManualConfigurationRequired": "Manual configuration required", "siteWgManualConfigurationRequired": "Manual configuration required",
"userErrorNotAdminOrOwner": "User is not an admin or owner", "userErrorNotAdminOrOwner": "User is not an admin or owner",
@@ -2361,7 +2361,7 @@
"resourcesTableProxyResources": "Public", "resourcesTableProxyResources": "Public",
"resourcesTableClientResources": "Private", "resourcesTableClientResources": "Private",
"resourcesTableNoProxyResourcesFound": "No proxy resources found.", "resourcesTableNoProxyResourcesFound": "No proxy resources found.",
"resourcesTableNoInternalResourcesFound": "No internal resources found.", "resourcesTableNoInternalResourcesFound": "No private resources found.",
"resourcesTableDestination": "Destination", "resourcesTableDestination": "Destination",
"resourcesTableAlias": "Alias", "resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Alias Address", "resourcesTableAliasAddress": "Alias Address",
@@ -2384,9 +2384,9 @@
"editInternalResourceDialogCancel": "Cancel", "editInternalResourceDialogCancel": "Cancel",
"editInternalResourceDialogSaveResource": "Save Resource", "editInternalResourceDialogSaveResource": "Save Resource",
"editInternalResourceDialogSuccess": "Success", "editInternalResourceDialogSuccess": "Success",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully", "editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Private resource updated successfully",
"editInternalResourceDialogError": "Error", "editInternalResourceDialogError": "Error",
"editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource", "editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update private resource",
"editInternalResourceDialogNameRequired": "Name is required", "editInternalResourceDialogNameRequired": "Name is required",
"editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", "editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters",
"editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1", "editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1",
@@ -2412,7 +2412,7 @@
"editInternalResourceDialogAlias": "Alias", "editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.", "editInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
"createInternalResourceDialogNoSitesAvailable": "No Sites Available", "createInternalResourceDialogNoSitesAvailable": "No Sites Available",
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.", "createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create private resources.",
"createInternalResourceDialogClose": "Close", "createInternalResourceDialogClose": "Close",
"createInternalResourceDialogCreateClientResource": "Create Private Resource", "createInternalResourceDialogCreateClientResource": "Create Private Resource",
"createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will only be accessible to clients connected to the organization", "createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will only be accessible to clients connected to the organization",
@@ -2441,9 +2441,9 @@
"createInternalResourceDialogCancel": "Cancel", "createInternalResourceDialogCancel": "Cancel",
"createInternalResourceDialogCreateResource": "Create Resource", "createInternalResourceDialogCreateResource": "Create Resource",
"createInternalResourceDialogSuccess": "Success", "createInternalResourceDialogSuccess": "Success",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully", "createInternalResourceDialogInternalResourceCreatedSuccessfully": "Private resource created successfully",
"createInternalResourceDialogError": "Error", "createInternalResourceDialogError": "Error",
"createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource", "createInternalResourceDialogFailedToCreateInternalResource": "Failed to create private resource",
"createInternalResourceDialogNameRequired": "Name is required", "createInternalResourceDialogNameRequired": "Name is required",
"createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters", "createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters",
"createInternalResourceDialogPleaseSelectSite": "Please select a site", "createInternalResourceDialogPleaseSelectSite": "Please select a site",
+304 -60
View File
@@ -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.16.1", "axios": "1.18.0",
"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.2.0", "js-yaml": "4.3.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.6", "next": "16.2.11",
"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.6", "version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.11.tgz",
"integrity": "sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==", "integrity": "sha512-0do5A3BJ2gxWr0ZCMcD6BhW+e595jyxdTl3rXTS6lOtD8ektMiW6CO+EPwt1Eca1DBnm90r/7GdiKWBKxH++DA==",
"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.6", "version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.11.tgz",
"integrity": "sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==", "integrity": "sha512-wryL4pjKmDwGv2ox6+GZDFxvmtSRLqApBR8kL1j4+vhB7Z5vJC/zAnXpiR9Xkfzl0AS8WLMnsuGV/UKI67/rrw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2606,9 +2606,9 @@
} }
}, },
"node_modules/@next/swc-darwin-x64": { "node_modules/@next/swc-darwin-x64": {
"version": "16.2.6", "version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.11.tgz",
"integrity": "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==", "integrity": "sha512-aZl2j4f/fLyjQvOhv0Oe9UaMAQHolYpKhctsoYzplSumKJKPUmgjcf6545aBtysLTcu994TREd0+pSgNE4ohmg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -2622,12 +2622,15 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-gnu": { "node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.2.6", "version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.11.tgz",
"integrity": "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==", "integrity": "sha512-5jEriyEnH/LWFy27L2ZG0XaLlyEJIjhsImEsiS9P563PKEVp2BVups/xfOucIrsvVntp11oNcZwjHvaDPYVB5g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2638,12 +2641,15 @@
} }
}, },
"node_modules/@next/swc-linux-arm64-musl": { "node_modules/@next/swc-linux-arm64-musl": {
"version": "16.2.6", "version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.11.tgz",
"integrity": "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==", "integrity": "sha512-eIjcpx2fnnFSSkZDbTxy74KnokUXDjfoLClpWelfgHLf621aTqswhwXQ7GkD5K5rplrS6LZ/Bj+mVuvzluBOEg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2654,12 +2660,15 @@
} }
}, },
"node_modules/@next/swc-linux-x64-gnu": { "node_modules/@next/swc-linux-x64-gnu": {
"version": "16.2.6", "version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.11.tgz",
"integrity": "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==", "integrity": "sha512-8WgzpaWMs46qJT9kiV47cje86L0x/Mu9t8/Gwj+pnbgW3rETVfCnaScPjlYUwNScpOozdcIMHWmAvuZJUonR2w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"glibc"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2670,12 +2679,15 @@
} }
}, },
"node_modules/@next/swc-linux-x64-musl": { "node_modules/@next/swc-linux-x64-musl": {
"version": "16.2.6", "version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.11.tgz",
"integrity": "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==", "integrity": "sha512-I3UgPds7G4ZYnTb/H+5GBGuUT2DhAk6j0mL6A4s63RjFs74wB2hOWP0vaxsK+3NJraExt3eYEPQ/UtT0x/64Nw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"libc": [
"musl"
],
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2686,9 +2698,9 @@
} }
}, },
"node_modules/@next/swc-win32-arm64-msvc": { "node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.2.6", "version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.11.tgz",
"integrity": "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==", "integrity": "sha512-n89CjtcThnjrwgJMAiI5xbqwLY51zvwC9tSlArmVndAJLYVl9T9UAdlkXTmZvE++idoXe8KdglQlhNRdUp1c6g==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@@ -2702,9 +2714,9 @@
} }
}, },
"node_modules/@next/swc-win32-x64-msvc": { "node_modules/@next/swc-win32-x64-msvc": {
"version": "16.2.6", "version": "16.2.11",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.6.tgz", "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.11.tgz",
"integrity": "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==", "integrity": "sha512-md8CLNggS1Dx9pUgApzps5uAf+N8GN9xywzmNx9vHAWo94HtBwCCqkSnhIrdfQe83Dhz8Lfo/20Nb1Zxal092w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@@ -6460,6 +6472,225 @@
"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",
@@ -9078,9 +9309,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.16.1", "version": "1.18.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.16.1.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.18.0.tgz",
"integrity": "sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==", "integrity": "sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.16.0", "follow-redirects": "^1.16.0",
@@ -9214,20 +9445,20 @@
} }
}, },
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.2.2", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.3.0.tgz",
"integrity": "sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==", "integrity": "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"bytes": "^3.1.2", "bytes": "^3.1.2",
"content-type": "^1.0.5", "content-type": "^2.0.0",
"debug": "^4.4.3", "debug": "^4.4.3",
"http-errors": "^2.0.0", "http-errors": "^2.0.1",
"iconv-lite": "^0.7.0", "iconv-lite": "^0.7.2",
"on-finished": "^2.4.1", "on-finished": "^2.4.1",
"qs": "^6.14.1", "qs": "^6.15.2",
"raw-body": "^3.0.1", "raw-body": "^3.0.2",
"type-is": "^2.0.1" "type-is": "^2.1.0"
}, },
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -9237,6 +9468,19 @@
"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",
@@ -11925,9 +12169,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/fast-uri": { "node_modules/fast-uri": {
"version": "3.1.2", "version": "3.1.4",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz",
"integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "integrity": "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -13389,9 +13633,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.2.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz",
"integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -14349,12 +14593,12 @@
} }
}, },
"node_modules/next": { "node_modules/next": {
"version": "16.2.6", "version": "16.2.11",
"resolved": "https://registry.npmjs.org/next/-/next-16.2.6.tgz", "resolved": "https://registry.npmjs.org/next/-/next-16.2.11.tgz",
"integrity": "sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==", "integrity": "sha512-B339zaqbyK8cmxhoAvLrcwoabwCP1wz21zSzfqxqXAemTu2BXnH7tQnfcglKv1vnMUIDBc+Hth7XODQriTZiRQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@next/env": "16.2.6", "@next/env": "16.2.11",
"@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",
@@ -14368,14 +14612,14 @@
"node": ">=20.9.0" "node": ">=20.9.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-arm64": "16.2.11",
"@next/swc-darwin-x64": "16.2.6", "@next/swc-darwin-x64": "16.2.11",
"@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.11",
"@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.11",
"@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.11",
"@next/swc-linux-x64-musl": "16.2.6", "@next/swc-linux-x64-musl": "16.2.11",
"@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.11",
"@next/swc-win32-x64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.11",
"sharp": "^0.34.5" "sharp": "^0.34.5"
}, },
"peerDependencies": { "peerDependencies": {
+3 -3
View File
@@ -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.16.1", "axios": "1.18.0",
"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.2.0", "js-yaml": "4.3.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.6", "next": "16.2.11",
"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",
+1 -1
View File
@@ -18,7 +18,7 @@ export const domains = pgTable("domains", {
domainId: varchar("domainId").primaryKey(), domainId: varchar("domainId").primaryKey(),
baseDomain: varchar("baseDomain").notNull(), baseDomain: varchar("baseDomain").notNull(),
configManaged: boolean("configManaged").notNull().default(false), configManaged: boolean("configManaged").notNull().default(false),
type: varchar("type"), // "ns", "cname", "wildcard" type: varchar("type").$type<"ns" | "cname" | "wildcard">(),
verified: boolean("verified").notNull().default(false), verified: boolean("verified").notNull().default(false),
failed: boolean("failed").notNull().default(false), failed: boolean("failed").notNull().default(false),
tries: integer("tries").notNull().default(0), tries: integer("tries").notNull().default(0),
+1 -1
View File
@@ -15,7 +15,7 @@ export const domains = sqliteTable("domains", {
configManaged: integer("configManaged", { mode: "boolean" }) configManaged: integer("configManaged", { mode: "boolean" })
.notNull() .notNull()
.default(false), .default(false),
type: text("type"), // "ns", "cname", "wildcard" type: text("type").$type<"ns" | "cname" | "wildcard">(),
verified: integer("verified", { mode: "boolean" }).notNull().default(false), verified: integer("verified", { mode: "boolean" }).notNull().default(false),
failed: integer("failed", { mode: "boolean" }).notNull().default(false), failed: integer("failed", { mode: "boolean" }).notNull().default(false),
tries: integer("tries").notNull().default(0), tries: integer("tries").notNull().default(0),
+3 -2
View File
@@ -12,7 +12,7 @@ import { logIncomingMiddleware } from "./middlewares/logIncoming";
import helmet from "helmet"; import helmet from "helmet";
import swaggerUi from "swagger-ui-express"; import swaggerUi from "swagger-ui-express";
import { OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi"; import { OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi";
import { registry } from "./openApi"; import { registry, openApiTags } from "./openApi";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { APP_PATH } from "./lib/consts"; import { APP_PATH } from "./lib/consts";
@@ -181,7 +181,8 @@ function getOpenApiDocumentation() {
version: "v1", version: "v1",
title: "Pangolin Integration API" title: "Pangolin Integration API"
}, },
servers: [{ url: "/v1" }] servers: [{ url: "/v1" }],
tags: openApiTags
}); });
if (!process.env.DISABLE_GEN_OPENAPI) { if (!process.env.DISABLE_GEN_OPENAPI) {
+1
View File
@@ -5,6 +5,7 @@ export async function getValidCertificatesForDomains(
Array<{ Array<{
id: number; id: number;
domain: string; domain: string;
queriedDomain: string;
wildcard: boolean | null; wildcard: boolean | null;
certFile: string | null; certFile: string | null;
keyFile: string | null; keyFile: string | null;
+16
View File
@@ -18,3 +18,19 @@ export function canCompress(
return false; return false;
} }
} }
// Whether this newt client understands `tlsCertId` references into the
// sync message's `certs` array, instead of requiring each target to carry
// its own inline `tlsCert`/`tlsKey` PEM data. Bump the version floor here to
// match whatever release first ships the newt-side support.
export function supportsCertReferences(
clientVersion: string | null | undefined
): boolean {
try {
if (!clientVersion) return false;
if (!semver.valid(clientVersion)) return false;
return semver.gte(clientVersion, "1.16.0");
} catch {
return false;
}
}
+99 -1
View File
@@ -5,6 +5,7 @@ import config from "@server/lib/config";
import z from "zod"; import z from "zod";
import logger from "@server/logger"; import logger from "@server/logger";
import semver from "semver"; import semver from "semver";
import { createHash } from "crypto";
import { getValidCertificatesForDomains } from "#dynamic/lib/certificates"; import { getValidCertificatesForDomains } from "#dynamic/lib/certificates";
import { lockManager } from "#dynamic/lib/lock"; import { lockManager } from "#dynamic/lib/lock";
@@ -648,21 +649,101 @@ export type SubnetProxyTargetV2 = {
httpTargets?: HTTPTarget[]; httpTargets?: HTTPTarget[];
tlsCert?: string; tlsCert?: string;
tlsKey?: string; tlsKey?: string;
tlsCertId?: string; // references an entry in the sync message's top-level `certs` array instead of inlining tlsCert/tlsKey
}; };
export type CertRef = { id: string; cert: string; key: string };
/**
* Replaces each target's inline tlsCert/tlsKey with a tlsCertId reference
* into a deduplicated certs array, so that many targets sharing the same
* certificate (e.g. a wildcard cert used by thousands of site resources)
* only need that certificate sent once per sync message.
*/
export function dedupeCertsForTargets(
targetsV2: SubnetProxyTargetV2[]
): { targets: SubnetProxyTargetV2[]; certs: CertRef[] } {
const idByContent = new Map<string, string>();
const certs: CertRef[] = [];
const targets = targetsV2.map((target) => {
if (!target.tlsCert || !target.tlsKey) {
return target;
}
const contentKey = `${target.tlsCert}|${target.tlsKey}`;
let id = idByContent.get(contentKey);
if (!id) {
id = createHash("sha1").update(contentKey).digest("hex").slice(0, 16);
idByContent.set(contentKey, id);
certs.push({ id, cert: target.tlsCert, key: target.tlsKey });
}
const { tlsCert, tlsKey, ...rest } = target;
return { ...rest, tlsCertId: id };
});
return { targets, certs };
}
export type HTTPTarget = { export type HTTPTarget = {
destAddr: string; // must be an IP or hostname destAddr: string; // must be an IP or hostname
destPort: number; destPort: number;
scheme: "http" | "https"; scheme: "http" | "https";
}; };
export type CertByDomain = Map<string, { certFile: string; keyFile: string }>;
/**
* Fetches the TLS certificates for every enabled, SSL-enabled HTTP site
* resource's fullDomain in a single batched call, instead of one call per
* resource. Many resources commonly resolve to the very same certificate
* (e.g. a wildcard covering the org's domain), so batching turns what would
* be N concurrent DB/cache round-trips into one, and a lookup failure fails
* loudly for the whole batch rather than silently dropping the cert on a
* random subset of otherwise-identical resources under load.
*/
export async function batchFetchCertsForSiteResources(
allSiteResources: SiteResource[]
): Promise<CertByDomain> {
const domains = new Set(
allSiteResources
.filter((r) => r.enabled && r.mode === "http" && r.ssl && r.fullDomain)
.map((r) => r.fullDomain as string)
);
const certByDomain: CertByDomain = new Map();
if (domains.size === 0) {
return certByDomain;
}
try {
const certResults = await getValidCertificatesForDomains(domains, true);
for (const cert of certResults) {
if (cert.certFile && cert.keyFile) {
certByDomain.set(cert.queriedDomain, {
certFile: cert.certFile,
keyFile: cert.keyFile
});
}
}
} catch (err) {
logger.error(
`Failed to batch-retrieve certificates for ${domains.size} domain(s): ${err}`
);
}
return certByDomain;
}
export async function generateSubnetProxyTargetV2( export async function generateSubnetProxyTargetV2(
siteResource: SiteResource, siteResource: SiteResource,
clients: { clients: {
clientId: number; clientId: number;
pubKey: string | null; pubKey: string | null;
subnet: string | null; subnet: string | null;
}[] }[],
certByDomain?: CertByDomain
): Promise<SubnetProxyTargetV2[] | undefined> { ): Promise<SubnetProxyTargetV2[] | undefined> {
if (!siteResource.enabled) { if (!siteResource.enabled) {
logger.debug( logger.debug(
@@ -750,6 +831,22 @@ export async function generateSubnetProxyTargetV2(
let tlsKey: string | undefined; let tlsKey: string | undefined;
if (siteResource.ssl && siteResource.fullDomain) { if (siteResource.ssl && siteResource.fullDomain) {
if (certByDomain) {
// Caller batch-fetched certs for all resources up front (the
// common, high-scale path) — just look up this resource's
// domain rather than issuing its own DB/cache round-trip.
const cert = certByDomain.get(siteResource.fullDomain);
if (cert) {
tlsCert = cert.certFile;
tlsKey = cert.keyFile;
} else {
logger.warn(
`No valid certificate found for SSL site resource ${siteResource.siteResourceId} with domain ${siteResource.fullDomain}`
);
}
} else {
// No batched map supplied by the caller — fall back to a
// single-domain lookup for this resource alone.
try { try {
const certs = await getValidCertificatesForDomains( const certs = await getValidCertificatesForDomains(
new Set([siteResource.fullDomain]), new Set([siteResource.fullDomain]),
@@ -769,6 +866,7 @@ export async function generateSubnetProxyTargetV2(
); );
} }
} }
}
targets.push({ targets.push({
sourcePrefixes: [], sourcePrefixes: [],
+134 -5
View File
@@ -1,6 +1,6 @@
import { z } from "zod"; import { z } from "zod";
import { db, logsDb, statusHistory } from "@server/db"; import { db, logsDb, statusHistory } from "@server/db";
import { and, eq, gte, lt, asc, desc } from "drizzle-orm"; import { and, eq, gte, lt, asc, desc, inArray, max, sql } from "drizzle-orm";
import { regionalCache as cache } from "#dynamic/lib/cache"; import { regionalCache as cache } from "#dynamic/lib/cache";
const STATUS_HISTORY_CACHE_TTL = 60; // seconds const STATUS_HISTORY_CACHE_TTL = 60; // seconds
@@ -41,6 +41,7 @@ export async function getCachedStatusHistory(
return cached; return cached;
} }
console.time(`[getCachedStatusHistory/${entityType}=${entityId}]`);
// Anchor to local midnight (UTC when tzOffsetMinutes is 0) so the query // Anchor to local midnight (UTC when tzOffsetMinutes is 0) so the query
// window aligns with stable calendar days for the requesting client // window aligns with stable calendar days for the requesting client
const todayMidnightSec = localMidnightSec(tzOffsetMinutes); const todayMidnightSec = localMidnightSec(tzOffsetMinutes);
@@ -76,12 +77,14 @@ export async function getCachedStatusHistory(
const priorStatus = lastKnownEvent?.status ?? null; const priorStatus = lastKnownEvent?.status ?? null;
console.time(`[computeBuckets/${entityType}=${entityId}]`);
const { buckets, totalDowntime } = computeBuckets( const { buckets, totalDowntime } = computeBuckets(
events, events,
days, days,
priorStatus, priorStatus,
tzOffsetMinutes tzOffsetMinutes
); );
console.timeEnd(`[computeBuckets/${entityType}=${entityId}]`);
const totalWindow = days * 86400; const totalWindow = days * 86400;
const overallUptime = const overallUptime =
totalWindow > 0 totalWindow > 0
@@ -96,6 +99,7 @@ export async function getCachedStatusHistory(
totalDowntimeSeconds: totalDowntime totalDowntimeSeconds: totalDowntime
}; };
console.timeEnd(`[getCachedStatusHistory/${entityType}=${entityId}]`);
await cache.set(cacheKey, result, STATUS_HISTORY_CACHE_TTL); await cache.set(cacheKey, result, STATUS_HISTORY_CACHE_TTL);
return result; return result;
} }
@@ -264,9 +268,7 @@ export function computeBuckets(
// Shift by the client's offset before formatting so the label reflects // Shift by the client's offset before formatting so the label reflects
// their local calendar date rather than the UTC date of dayStartSec // their local calendar date rather than the UTC date of dayStartSec
const dateStr = new Date( const dateStr = new Date((dayStartSec + tzOffsetMinutes * 60) * 1000)
(dayStartSec + tzOffsetMinutes * 60) * 1000
)
.toISOString() .toISOString()
.slice(0, 10); .slice(0, 10);
@@ -301,6 +303,133 @@ export function computeBuckets(
status status
}); });
} }
return { buckets, totalDowntime }; return { buckets, totalDowntime };
} }
export type BatchedStatusHistoryResponse = Record<
string,
StatusHistoryResponse
>;
export async function getBatchedStatusHistory(
entityType: string,
entityIds: number[],
days: number,
tzOffsetMinutes: number = 0
): Promise<BatchedStatusHistoryResponse> {
console.time(
`[getBatchedStatusHistory/${entityType}=(${entityIds.join(" ,")})]`
);
// Anchor to local midnight (UTC when tzOffsetMinutes is 0) so the query
// window aligns with stable calendar days for the requesting client
const todayMidnightSec = localMidnightSec(tzOffsetMinutes);
const startSec = todayMidnightSec - days * 86400;
const events = await logsDb
.select()
.from(statusHistory)
.where(
and(
eq(statusHistory.entityType, entityType),
inArray(statusHistory.entityId, entityIds),
gte(statusHistory.timestamp, startSec)
)
)
.orderBy(asc(statusHistory.timestamp));
// Fetch the last known state before the window so that entities that
// haven't changed status recently still show the correct status rather
// than appearing as "no_data".
/**
* If we used only postgres, we would have used `SELECT DISTINCT ON` to get the
* latest event for each `entityId`,
* but it doesn't work on SQLite, so instead we use a subquery,
* the `ROW_NUMBER() OVER PARTITION` allows to assign a number
* to each row ordered by the timestamp, the number 1 is the first one appearing in
* the specified order, then the next and more, we only want the highest timestamp,
* so we get for `row_number=1`
*/
const lastKnowEventsSub = logsDb
.select({
entityId: statusHistory.entityId,
status: statusHistory.status,
timestamp: statusHistory.timestamp,
row_number:
sql<number>`ROW_NUMBER() OVER (PARTITION BY ${statusHistory.entityId} ORDER BY ${statusHistory.timestamp} DESC)`.as(
"row_number"
)
})
.from(statusHistory)
.where(
and(
eq(statusHistory.entityType, entityType),
inArray(statusHistory.entityId, entityIds),
lt(statusHistory.timestamp, startSec)
)
)
.as("sub");
const lastKnownEvents = await logsDb
.select({
entityId: lastKnowEventsSub.entityId,
status: lastKnowEventsSub.status,
timestamp: lastKnowEventsSub.timestamp
})
.from(lastKnowEventsSub)
.where(eq(lastKnowEventsSub.row_number, 1));
const eventStatusMap: Record<
number,
{
events: typeof events;
lastKnownEvent: (typeof lastKnownEvents)[number] | null;
}
> = {};
for (const entityId of entityIds) {
eventStatusMap[entityId] = {
events: events.filter((ev) => ev.entityId === entityId),
lastKnownEvent:
lastKnownEvents.find((ev) => ev.entityId === entityId) ?? null
};
}
const result: BatchedStatusHistoryResponse = {};
console.time(`[computeBuckets/${entityType}=(${entityIds.join(" ,")})]`);
for (const entityId in eventStatusMap) {
const event = eventStatusMap[Number(entityId)];
const priorStatus = event.lastKnownEvent?.status ?? null;
const { buckets, totalDowntime } = computeBuckets(
event.events,
days,
priorStatus,
tzOffsetMinutes
);
const totalWindow = days * 86400;
const overallUptime =
totalWindow > 0
? Math.max(
0,
((totalWindow - totalDowntime) / totalWindow) * 100
)
: 100;
result[entityId] = {
entityType,
entityId: Number(entityId),
days: buckets,
overallUptimePercent: Math.round(overallUptime * 100) / 100,
totalDowntimeSeconds: totalDowntime
};
}
console.timeEnd(`[computeBuckets/${entityType}=(${entityIds.join(" ,")})]`);
console.timeEnd(
`[getBatchedStatusHistory/${entityType}=(${entityIds.join(" ,")})]`
);
return result;
}
+19
View File
@@ -1,10 +1,29 @@
import { import {
getResourceRuleValueValidationError, getResourceRuleValueValidationError,
isValidDomain,
isValidUrlGlobPattern isValidUrlGlobPattern
} from "./validators"; } from "./validators";
import { assertEquals } from "@test/assert"; import { assertEquals } from "@test/assert";
function runTests() { function runTests() {
console.log("Running domain validation tests...");
assertEquals(
isValidDomain("example.com"),
true,
"Standard ASCII domain should be valid"
);
assertEquals(
isValidDomain("xn--e1afmkfd.xn--p1ai"),
true,
"Punycode IDN domain should be valid"
);
assertEquals(
isValidDomain("example.invalid-tld"),
false,
"Domain with unknown TLD should be invalid"
);
console.log("Running URL pattern validation tests..."); console.log("Running URL pattern validation tests...");
// Test valid patterns // Test valid patterns
+3 -2
View File
@@ -171,9 +171,10 @@ export function isValidDomain(domain: string): boolean {
if (!/^[a-zA-Z0-9-]+$/.test(label)) return false; if (!/^[a-zA-Z0-9-]+$/.test(label)) return false;
} }
// TLD should be at least 2 characters and contain only letters // TLD should be at least 2 characters. Punycode TLDs can contain digits
// and hyphens, so validity is ultimately enforced by the TLD allowlist.
const tld = labels[labels.length - 1]; const tld = labels[labels.length - 1];
if (tld.length < 2 || !/^[a-zA-Z]+$/.test(tld)) return false; if (tld.length < 2) return false;
// Check if TLD is in the list of valid TLDs // Check if TLD is in the list of valid TLDs
if (!validTlds.includes(tld.toUpperCase())) return false; if (!validTlds.includes(tld.toUpperCase())) return false;
+18 -7
View File
@@ -4,23 +4,34 @@ export const registry = new OpenAPIRegistry();
export enum OpenAPITags { export enum OpenAPITags {
Site = "Site", Site = "Site",
Org = "Organization",
PublicResource = "Public Resource", PublicResource = "Public Resource",
Target = "Resource Target",
PrivateResource = "Private Resource", PrivateResource = "Private Resource",
Policy = "Policy", Client = "Client",
Org = "Organization",
Domain = "Domain",
PublicResourcePolicy = "Public Resource Policy",
Role = "Role", Role = "Role",
User = "User", User = "User",
Invitation = "User Invitation",
Target = "Resource Target",
Rule = "Rule", Rule = "Rule",
Invitation = "User Invitation",
AccessToken = "Access Token", AccessToken = "Access Token",
GlobalIdp = "Identity Provider (Global)", GlobalIdp = "Identity Provider (Global)",
OrgIdp = "Identity Provider (Organization Only)", OrgIdp = "Identity Provider (Organization Only)",
Client = "Client",
ApiKey = "API Key", ApiKey = "API Key",
SiteProvisioningKey = "Site Provisioning Key", SiteProvisioningKey = "Site Provisioning Key",
Domain = "Domain",
Blueprint = "Blueprint", Blueprint = "Blueprint",
Ssh = "SSH", Ssh = "SSH",
Logs = "Logs" Logs = "Logs",
EventStreamingDestination = "Event Streaming Destination",
AlertRule = "Alert Rule",
HealthCheck = "Health Check",
PublicResourcePolicyLegacy = "Public Resource Policy (Legacy)",
PublicResourceLegacy = "Public Resource (Legacy)",
PrivateResourceLegacy = "Private Resource (Legacy)"
} }
// Order here controls the order tags are displayed in Swagger UI
export const openApiTags = Object.values(OpenAPITags).map((name) => ({
name
}));
@@ -191,7 +191,7 @@ registry.registerPath({
method: "put", method: "put",
path: "/org/{orgId}/alert-rule", path: "/org/{orgId}/alert-rule",
description: "Create an alert rule for a specific organization.", description: "Create an alert rule for a specific organization.",
tags: [OpenAPITags.Org], tags: [OpenAPITags.AlertRule],
request: { request: {
params: paramsSchema, params: paramsSchema,
body: { body: {
@@ -34,7 +34,7 @@ registry.registerPath({
method: "delete", method: "delete",
path: "/org/{orgId}/alert-rule/{alertRuleId}", path: "/org/{orgId}/alert-rule/{alertRuleId}",
description: "Delete an alert rule for a specific organization.", description: "Delete an alert rule for a specific organization.",
tags: [OpenAPITags.Org], tags: [OpenAPITags.AlertRule],
request: { request: {
params: paramsSchema params: paramsSchema
}, },
@@ -48,7 +48,7 @@ registry.registerPath({
method: "get", method: "get",
path: "/org/{orgId}/alert-rule/{alertRuleId}", path: "/org/{orgId}/alert-rule/{alertRuleId}",
description: "Get a specific alert rule for an organization.", description: "Get a specific alert rule for an organization.",
tags: [OpenAPITags.Org], tags: [OpenAPITags.AlertRule],
request: { request: {
params: paramsSchema params: paramsSchema
}, },
@@ -90,7 +90,7 @@ registry.registerPath({
method: "get", method: "get",
path: "/org/{orgId}/alert-rules", path: "/org/{orgId}/alert-rules",
description: "List all alert rules for a specific organization.", description: "List all alert rules for a specific organization.",
tags: [OpenAPITags.Org], tags: [OpenAPITags.AlertRule],
request: { request: {
query: querySchema, query: querySchema,
params: paramsSchema params: paramsSchema
@@ -158,7 +158,7 @@ registry.registerPath({
method: "post", method: "post",
path: "/org/{orgId}/alert-rule/{alertRuleId}", path: "/org/{orgId}/alert-rule/{alertRuleId}",
description: "Update an alert rule for a specific organization.", description: "Update an alert rule for a specific organization.",
tags: [OpenAPITags.Org], tags: [OpenAPITags.AlertRule],
request: { request: {
params: paramsSchema, params: paramsSchema,
body: { body: {
@@ -0,0 +1,190 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import { certificates, db, domains, orgDomains } from "@server/db";
import response from "@server/lib/response";
import logger from "@server/logger";
import { type GetBatchedCertificateResponse } from "@server/routers/certificates/types";
import HttpCode from "@server/types/HttpCode";
import { and, eq, inArray, or } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const getCertificateParamSchema = z.strictObject({
orgId: z.string()
});
const getCertificateQuerySchema = z.object({
domains: z.preprocess(
(val) => {
if (val === undefined || val === null || val === "") {
return undefined;
}
if (Array.isArray(val)) {
return val;
}
// the array is returned as this
if (typeof val === "string") {
return val.split(",");
}
return undefined;
},
z.array(z.string().min(1).max(255))
)
});
async function query(orgId: string, domainList: string[]) {
// Try to get CNAME certificates first
let existingCertificates = await db
.select({
certId: certificates.certId,
domain: certificates.domain,
wildcard: certificates.wildcard,
status: certificates.status,
expiresAt: certificates.expiresAt,
lastRenewalAttempt: certificates.lastRenewalAttempt,
createdAt: certificates.createdAt,
updatedAt: certificates.updatedAt,
errorMessage: certificates.errorMessage,
renewalCount: certificates.renewalCount,
domainId: domains.domainId,
domainType: domains.type
})
.from(certificates)
.innerJoin(domains, eq(certificates.domainId, domains.domainId))
.innerJoin(
orgDomains,
and(
eq(domains.domainId, orgDomains.domainId),
eq(orgDomains.orgId, orgId)
)
)
.where(and(inArray(certificates.domain, domainList)));
// All non resolved domain certificates might be `ns` or `wildcard`,
// which means exact domain certificates do not
const nonAvailableCertificates = existingCertificates
.filter((cert) => !domainList.includes(cert.domain))
.map((cert) => cert.domain);
if (nonAvailableCertificates.length > 0) {
const domainLevelDownSet = new Set<string>();
const wildcardDomainSet = new Set<string>();
for (const domain of nonAvailableCertificates) {
const domainLevelDown = domain.split(".").slice(1).join(".");
const wildcardPrefixed = `*.${domainLevelDown}`;
domainLevelDownSet.add(domainLevelDown);
wildcardDomainSet.add(wildcardPrefixed);
}
// Need to map the certificates to each domain
const wildcardCertificates = await db
.select({
certId: certificates.certId,
domain: certificates.domain,
wildcard: certificates.wildcard,
status: certificates.status,
expiresAt: certificates.expiresAt,
lastRenewalAttempt: certificates.lastRenewalAttempt,
createdAt: certificates.createdAt,
updatedAt: certificates.updatedAt,
errorMessage: certificates.errorMessage,
renewalCount: certificates.renewalCount,
domainId: domains.domainId,
domainType: domains.type
})
.from(certificates)
.innerJoin(domains, eq(certificates.domainId, domains.domainId))
.innerJoin(
orgDomains,
and(
eq(domains.domainId, orgDomains.domainId),
eq(orgDomains.orgId, orgId)
)
)
.where(
and(
eq(certificates.wildcard, true),
or(
inArray(certificates.domain, [...domainLevelDownSet]),
inArray(certificates.domain, [...wildcardDomainSet])
)
)
);
existingCertificates.push(...wildcardCertificates);
}
const certificateMap: Record<string, any> = {};
for (const domain of domainList) {
const domainLevelDown = domain.split(".").slice(1).join(".");
const wildcardPrefixed = `*.${domainLevelDown}`;
certificateMap[domain] =
existingCertificates.find(
(cert) =>
cert.domain === domain ||
cert.domain === domainLevelDown ||
cert.domain === wildcardPrefixed
) ?? null;
}
return certificateMap;
}
export async function getBatchedCertificates(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedParams = getCertificateParamSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error).toString()
)
);
}
const { orgId } = parsedParams.data;
const parsedQuery = getCertificateQuerySchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error).toString()
)
);
}
const { domains } = parsedQuery.data;
const cert = await query(orgId, domains);
return response<GetBatchedCertificateResponse>(res, {
data: cert,
success: true,
error: false,
message: "Certificates retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
@@ -14,3 +14,4 @@
export * from "./getCertificate"; export * from "./getCertificate";
export * from "./restartCertificate"; export * from "./restartCertificate";
export * from "./syncCertToNewts"; export * from "./syncCertToNewts";
export * from "./getBatchedCertificates";
@@ -11,18 +11,16 @@
* This file is not licensed under the AGPLv3. * This file is not licensed under the AGPLv3.
*/ */
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { certificates, db } from "@server/db"; import { certificates, db } from "@server/db";
import { sites } from "@server/db";
import { eq, and } from "drizzle-orm";
import response from "@server/lib/response"; import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import stoi from "@server/lib/stoi"; import { registry } from "@server/openApi";
import HttpCode from "@server/types/HttpCode";
import { eq } from "drizzle-orm";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
const restartCertificateParamsSchema = z.strictObject({ const restartCertificateParamsSchema = z.strictObject({
certId: z.coerce.number().int().positive(), certId: z.coerce.number().int().positive(),
@@ -52,7 +52,7 @@ registry.registerPath({
method: "put", method: "put",
path: "/org/{orgId}/event-streaming-destination", path: "/org/{orgId}/event-streaming-destination",
description: "Create an event streaming destination for a specific organization.", description: "Create an event streaming destination for a specific organization.",
tags: [OpenAPITags.Org], tags: [OpenAPITags.EventStreamingDestination],
request: { request: {
params: paramsSchema, params: paramsSchema,
body: { body: {
@@ -35,7 +35,7 @@ registry.registerPath({
path: "/org/{orgId}/event-streaming-destination/{destinationId}", path: "/org/{orgId}/event-streaming-destination/{destinationId}",
description: description:
"Delete an event streaming destination for a specific organization.", "Delete an event streaming destination for a specific organization.",
tags: [OpenAPITags.Org], tags: [OpenAPITags.EventStreamingDestination],
request: { request: {
params: paramsSchema params: paramsSchema
}, },
@@ -109,7 +109,7 @@ registry.registerPath({
path: "/org/{orgId}/event-streaming-destination", path: "/org/{orgId}/event-streaming-destination",
description: description:
"List all event streaming destinations for a specific organization.", "List all event streaming destinations for a specific organization.",
tags: [OpenAPITags.Org], tags: [OpenAPITags.EventStreamingDestination],
request: { request: {
query: querySchema, query: querySchema,
params: paramsSchema params: paramsSchema
@@ -55,7 +55,7 @@ registry.registerPath({
method: "post", method: "post",
path: "/org/{orgId}/event-streaming-destination/{destinationId}", path: "/org/{orgId}/event-streaming-destination/{destinationId}",
description: "Update an event streaming destination for a specific organization.", description: "Update an event streaming destination for a specific organization.",
tags: [OpenAPITags.Org], tags: [OpenAPITags.EventStreamingDestination],
request: { request: {
params: paramsSchema, params: paramsSchema,
body: { body: {
+15
View File
@@ -175,6 +175,13 @@ authenticated.get(
certificates.getCertificate certificates.getCertificate
); );
authenticated.get(
"/org/:orgId/batched-certificates",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.getCertificate),
certificates.getBatchedCertificates
);
authenticated.post( authenticated.post(
"/org/:orgId/certificate/:certId/restart", "/org/:orgId/certificate/:certId/restart",
verifyValidLicense, verifyValidLicense,
@@ -853,6 +860,14 @@ authenticated.get(
healthChecks.getHealthCheckStatusHistory healthChecks.getHealthCheckStatusHistory
); );
authenticated.get(
"/org/:orgId/health-check-status-histories",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.getTarget),
healthChecks.getBatchedHealthCheckStatusHistory
);
authenticated.get( authenticated.get(
"/client/:clientId/verify-associations-cache", "/client/:clientId/verify-associations-cache",
verifyClientAccess, verifyClientAccess,
@@ -75,7 +75,7 @@ registry.registerPath({
method: "put", method: "put",
path: "/org/{orgId}/health-check", path: "/org/{orgId}/health-check",
description: "Create a health check for a specific organization.", description: "Create a health check for a specific organization.",
tags: [OpenAPITags.Org], tags: [OpenAPITags.HealthCheck],
request: { request: {
params: paramsSchema, params: paramsSchema,
body: { body: {
@@ -37,7 +37,7 @@ registry.registerPath({
method: "delete", method: "delete",
path: "/org/{orgId}/health-check/{healthCheckId}", path: "/org/{orgId}/health-check/{healthCheckId}",
description: "Delete a health check for a specific organization.", description: "Delete a health check for a specific organization.",
tags: [OpenAPITags.Org], tags: [OpenAPITags.HealthCheck],
request: { request: {
params: paramsSchema params: paramsSchema
}, },
@@ -0,0 +1,96 @@
/*
* This file is part of a proprietary work.
*
* Copyright (c) 2025-2026 Fossorial, Inc.
* All rights reserved.
*
* This file is licensed under the Fossorial Commercial License.
* You may not use this file except in compliance with the License.
* Unauthorized use, copying, modification, or distribution is strictly prohibited.
*
* This file is not licensed under the AGPLv3.
*/
import response from "@server/lib/response";
import {
getBatchedStatusHistory,
type BatchedStatusHistoryResponse
} from "@server/lib/statusHistory";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const healthCheckIdParamsSchema = z.object({
days: z
.string()
.optional()
.transform((v) => (v ? parseInt(v, 10) : 90)),
// Minutes to add to UTC to get the requesting client's local time
// (e.g. Australia/Sydney standard time is 600). Optional and
// defaults to 0 (UTC) so older clients keep the prior behavior.
tzOffsetMinutes: z
.string()
.optional()
.transform((v) => (v ? parseInt(v, 10) : 0)),
healthCheckIds: z
.preprocess((val) => {
if (val === undefined || val === null || val === "") {
return undefined;
}
const raw = Array.isArray(val) ? val : [val];
const nums = raw
.map((v) =>
typeof v === "string" ? parseInt(v, 10) : Number(v)
)
.filter((n) => Number.isInteger(n) && n > 0);
const unique = [...new Set(nums)];
return unique.length ? unique : undefined;
}, z.array(z.number().int().positive()))
.openapi({
description: "Filter by healthCheckIds (repeat query param)"
})
});
export async function getBatchedHealthCheckStatusHistory(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedQuery = healthCheckIdParamsSchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error).toString()
)
);
}
const entityType = "health_check";
const { days, healthCheckIds, tzOffsetMinutes } = parsedQuery.data;
const data = await getBatchedStatusHistory(
entityType,
healthCheckIds,
days,
tzOffsetMinutes
);
return response<BatchedStatusHistoryResponse>(res, {
data,
success: true,
error: false,
message: "Status history retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
@@ -16,3 +16,4 @@ export * from "./createHealthCheck";
export * from "./updateHealthCheck"; export * from "./updateHealthCheck";
export * from "./deleteHealthCheck"; export * from "./deleteHealthCheck";
export * from "./getStatusHistory"; export * from "./getStatusHistory";
export * from "./getBatchedStatusHistory";
@@ -63,7 +63,7 @@ registry.registerPath({
method: "get", method: "get",
path: "/org/{orgId}/health-checks", path: "/org/{orgId}/health-checks",
description: "List health checks for an organization.", description: "List health checks for an organization.",
tags: [OpenAPITags.Org], tags: [OpenAPITags.HealthCheck],
request: { request: {
params: paramsSchema, params: paramsSchema,
query: querySchema query: querySchema
@@ -109,7 +109,7 @@ registry.registerPath({
method: "post", method: "post",
path: "/org/{orgId}/health-check/{healthCheckId}", path: "/org/{orgId}/health-check/{healthCheckId}",
description: "Update a health check for a specific organization.", description: "Update a health check for a specific organization.",
tags: [OpenAPITags.Org], tags: [OpenAPITags.HealthCheck],
request: { request: {
params: paramsSchema, params: paramsSchema,
body: { body: {
+152
View File
@@ -17,6 +17,10 @@ import * as logs from "#private/routers/auditLogs";
import * as alertEvents from "#private/routers/alertEvents"; import * as alertEvents from "#private/routers/alertEvents";
import * as certificates from "#private/routers/certificates"; import * as certificates from "#private/routers/certificates";
import * as siteProvisioning from "#private/routers/siteProvisioning"; import * as siteProvisioning from "#private/routers/siteProvisioning";
import * as policy from "#private/routers/policy";
import * as eventStreamingDestination from "#private/routers/eventStreamingDestination";
import * as alertRule from "#private/routers/alertRule";
import * as healthChecks from "#private/routers/healthChecks";
import { import {
verifyApiKeyHasAction, verifyApiKeyHasAction,
@@ -26,6 +30,7 @@ import {
verifyApiKeyRoleAccess, verifyApiKeyRoleAccess,
verifyApiKeyUserAccess, verifyApiKeyUserAccess,
verifyApiKeySiteProvisioningKeyAccess, verifyApiKeySiteProvisioningKeyAccess,
verifyApiKeyResourcePolicyAccess,
verifyLimits verifyLimits
} from "@server/middlewares"; } from "@server/middlewares";
import * as user from "#private/routers/user"; import * as user from "#private/routers/user";
@@ -259,3 +264,150 @@ authenticated.patch(
logActionAudit(ActionsEnum.updateSiteProvisioningKey), logActionAudit(ActionsEnum.updateSiteProvisioningKey),
siteProvisioning.updateSiteProvisioningKey siteProvisioning.updateSiteProvisioningKey
); );
authenticated.get(
["/org/:orgId/resource-policies", "/org/:orgId/public-resource-policies"],
verifyValidLicense,
verifyValidSubscription(tierMatrix.resourcePolicies),
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.listResourcePolicies),
logActionAudit(ActionsEnum.listResourcePolicies),
policy.listResourcePolicies
);
authenticated.post(
["/org/:orgId/resource-policy", "/org/:orgId/public-resource-policy"],
verifyValidLicense,
verifyValidSubscription(tierMatrix.resourcePolicies),
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResourcePolicy),
logActionAudit(ActionsEnum.createResourcePolicy),
policy.createResourcePolicy
);
authenticated.delete(
[
"/resource-policy/:resourcePolicyId",
"/public-resource-policy/:resourcePolicyId"
],
verifyApiKeyResourcePolicyAccess,
verifyValidLicense,
verifyValidSubscription(tierMatrix.resourcePolicies),
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.deleteResourcePolicy),
logActionAudit(ActionsEnum.deleteResourcePolicy),
policy.deleteResourcePolicy
);
authenticated.put(
"/org/:orgId/event-streaming-destination",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createEventStreamingDestination),
logActionAudit(ActionsEnum.createEventStreamingDestination),
eventStreamingDestination.createEventStreamingDestination
);
authenticated.post(
"/org/:orgId/event-streaming-destination/:destinationId",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateEventStreamingDestination),
logActionAudit(ActionsEnum.updateEventStreamingDestination),
eventStreamingDestination.updateEventStreamingDestination
);
authenticated.delete(
"/org/:orgId/event-streaming-destination/:destinationId",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.deleteEventStreamingDestination),
logActionAudit(ActionsEnum.deleteEventStreamingDestination),
eventStreamingDestination.deleteEventStreamingDestination
);
authenticated.get(
"/org/:orgId/event-streaming-destinations",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listEventStreamingDestinations),
eventStreamingDestination.listEventStreamingDestinations
);
authenticated.put(
"/org/:orgId/alert-rule",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createAlertRule),
logActionAudit(ActionsEnum.createAlertRule),
alertRule.createAlertRule
);
authenticated.post(
"/org/:orgId/alert-rule/:alertRuleId",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.updateAlertRule),
logActionAudit(ActionsEnum.updateAlertRule),
alertRule.updateAlertRule
);
authenticated.delete(
"/org/:orgId/alert-rule/:alertRuleId",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.deleteAlertRule),
logActionAudit(ActionsEnum.deleteAlertRule),
alertRule.deleteAlertRule
);
authenticated.get(
"/org/:orgId/alert-rules",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listAlertRules),
alertRule.listAlertRules
);
authenticated.get(
"/org/:orgId/alert-rule/:alertRuleId",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.getAlertRule),
alertRule.getAlertRule
);
authenticated.get(
"/org/:orgId/health-checks",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listHealthChecks),
healthChecks.listHealthChecks
);
authenticated.put(
"/org/:orgId/health-check",
verifyApiKeyOrgAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createHealthCheck),
logActionAudit(ActionsEnum.createHealthCheck),
healthChecks.createHealthCheck
);
authenticated.post(
"/org/:orgId/health-check/:healthCheckId",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.updateHealthCheck),
logActionAudit(ActionsEnum.updateHealthCheck),
healthChecks.updateHealthCheck
);
authenticated.delete(
"/org/:orgId/health-check/:healthCheckId",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.deleteHealthCheck),
logActionAudit(ActionsEnum.deleteHealthCheck),
healthChecks.deleteHealthCheck
);
authenticated.get(
"/org/:orgId/health-check/:healthCheckId/status-history",
verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.getTarget),
healthChecks.getHealthCheckStatusHistory
);
@@ -121,7 +121,7 @@ registry.registerPath({
method: "post", method: "post",
path: "/org/{orgId}/resource-policy", path: "/org/{orgId}/resource-policy",
description: "Create a resource policy.", description: "Create a resource policy.",
tags: [OpenAPITags.Org, OpenAPITags.Policy], tags: [OpenAPITags.PublicResourcePolicy],
request: { request: {
params: createResourcePolicyParamsSchema, params: createResourcePolicyParamsSchema,
body: { body: {
@@ -31,7 +31,7 @@ registry.registerPath({
method: "delete", method: "delete",
path: "/resource-policy/{resourcePolicyId}", path: "/resource-policy/{resourcePolicyId}",
description: "Delete a resource policy.", description: "Delete a resource policy.",
tags: [OpenAPITags.Policy], tags: [OpenAPITags.PublicResourcePolicy],
request: { request: {
params: deleteResourcePolicySchema params: deleteResourcePolicySchema
}, },
@@ -79,7 +79,7 @@ registry.registerPath({
method: "get", method: "get",
path: "/org/{orgId}/resource-policies", path: "/org/{orgId}/resource-policies",
description: "List resource policies for an organization.", description: "List resource policies for an organization.",
tags: [OpenAPITags.Org, OpenAPITags.Policy], tags: [OpenAPITags.PublicResourcePolicy],
request: { request: {
params: z.object({ params: z.object({
orgId: z.string() orgId: z.string()
@@ -45,6 +45,39 @@ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/access-token", path: "/resource/{resourceId}/access-token",
description: "Generate a new access token for a resource.", description: "Generate a new access token for a resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: generateAccssTokenParamsSchema,
body: {
content: {
"application/json": {
schema: generateAccessTokenBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/access-token",
description: "Generate a new access token for a resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.AccessToken], tags: [OpenAPITags.PublicResource, OpenAPITags.AccessToken],
request: { request: {
params: generateAccssTokenParamsSchema, params: generateAccssTokenParamsSchema,
@@ -159,6 +159,35 @@ registry.registerPath({
method: "get", method: "get",
path: "/resource/{resourceId}/access-tokens", path: "/resource/{resourceId}/access-tokens",
description: "List all access tokens for a resource.", description: "List all access tokens for a resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: z.object({
resourceId: z.number()
}),
query: listAccessTokensSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "get",
path: "/public-resource/{resourceId}/access-tokens",
description: "List all access tokens for a resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.AccessToken], tags: [OpenAPITags.PublicResource, OpenAPITags.AccessToken],
request: { request: {
params: z.object({ params: z.object({
+8 -1
View File
@@ -1,9 +1,11 @@
import type { Domain } from "@server/db";
export type GetCertificateResponse = { export type GetCertificateResponse = {
certId: number; certId: number;
domain: string; domain: string;
domainId: string; domainId: string;
wildcard: boolean; wildcard: boolean;
domainType: string; domainType: Domain["type"];
status: string; // pending, requested, valid, expired, failed status: string; // pending, requested, valid, expired, failed
expiresAt: string | null; expiresAt: string | null;
lastRenewalAttempt: Date | null; lastRenewalAttempt: Date | null;
@@ -12,3 +14,8 @@ export type GetCertificateResponse = {
errorMessage?: string | null; errorMessage?: string | null;
renewalCount: number; renewalCount: number;
}; };
export type GetBatchedCertificateResponse = Record<
string,
GetCertificateResponse | null
>;
+26 -1
View File
@@ -33,7 +33,7 @@ const UpdateDomainResponseDataSchema = z.object({
registry.registerPath({ registry.registerPath({
method: "patch", method: "post",
path: "/org/{orgId}/domain/{domainId}", path: "/org/{orgId}/domain/{domainId}",
description: "Update a domain by domainId.", description: "Update a domain by domainId.",
tags: [OpenAPITags.Domain], tags: [OpenAPITags.Domain],
@@ -55,6 +55,31 @@ registry.registerPath({
} }
}); });
registry.registerPath({
method: "patch",
path: "/org/{orgId}/domain/{domainId}",
description:
"Update a domain by domainId. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.Domain],
request: {
params: z.object({
domainId: z.string(),
orgId: z.string()
})
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: createApiResponseSchema(UpdateDomainResponseDataSchema)
}
}
}
}
});
export async function updateOrgDomain( export async function updateOrgDomain(
req: Request, req: Request,
res: Response, res: Response,
+20 -1
View File
@@ -319,6 +319,13 @@ authenticated.get(
site.getSiteStatusHistory site.getSiteStatusHistory
); );
authenticated.get(
"/org/:orgId/site-status-histories",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listSites),
site.getBatchedSiteStatusHistory
);
// Site Resource endpoints // Site Resource endpoints
authenticated.put( authenticated.put(
"/org/:orgId/site-resource", "/org/:orgId/site-resource",
@@ -469,6 +476,13 @@ authenticated.get(
resource.getResourceStatusHistory resource.getResourceStatusHistory
); );
authenticated.get(
"/org/:orgId/resource-status-histories",
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listResources),
resource.getBatchedResourceStatusHistory
);
authenticated.get( authenticated.get(
"/org/:orgId/resources", "/org/:orgId/resources",
verifyOrgAccess, verifyOrgAccess,
@@ -1397,6 +1411,12 @@ authenticated.put(
// Auth routes // Auth routes
export const authRouter = Router(); export const authRouter = Router();
unauthenticated.use("/auth", authRouter); unauthenticated.use("/auth", authRouter);
// Register setup-check BEFORE the global auth rate limiter.
// This endpoint is called on every dashboard root page load (pure boolean
// read, no secrets) and must not consume the auth rate-limit budget.
authRouter.get("/initial-setup-complete", auth.initialSetupComplete);
authRouter.use( authRouter.use(
rateLimit({ rateLimit({
windowMs: windowMs:
@@ -1701,7 +1721,6 @@ authRouter.post("/idp/:idpId/oidc/generate-url", idp.generateOidcUrl);
authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback); authRouter.post("/idp/:idpId/oidc/validate-callback", idp.validateOidcCallback);
authRouter.put("/set-server-admin", auth.setServerAdmin); authRouter.put("/set-server-admin", auth.setServerAdmin);
authRouter.get("/initial-setup-complete", auth.initialSetupComplete);
authRouter.post("/validate-setup-token", auth.validateSetupToken); authRouter.post("/validate-setup-token", auth.validateSetupToken);
// Security Key routes // Security Key routes
+232 -26
View File
@@ -163,7 +163,7 @@ authenticated.get(
// Site Resource endpoints // Site Resource endpoints
authenticated.put( authenticated.put(
"/org/:orgId/site-resource", ["/org/:orgId/site-resource", "/org/:orgId/private-resource"],
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createSiteResource), verifyApiKeyHasAction(ActionsEnum.createSiteResource),
@@ -172,7 +172,10 @@ authenticated.put(
); );
authenticated.get( authenticated.get(
[
"/org/:orgId/site/:siteId/resources", "/org/:orgId/site/:siteId/resources",
"/org/:orgId/site/:siteId/private-resources"
],
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
verifyApiKeySiteAccess, verifyApiKeySiteAccess,
verifyApiKeyHasAction(ActionsEnum.listSiteResources), verifyApiKeyHasAction(ActionsEnum.listSiteResources),
@@ -180,21 +183,21 @@ authenticated.get(
); );
authenticated.get( authenticated.get(
"/org/:orgId/site-resources", ["/org/:orgId/site-resources", "/org/:orgId/private-resources"],
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listSiteResources), verifyApiKeyHasAction(ActionsEnum.listSiteResources),
siteResource.listAllSiteResourcesByOrg siteResource.listAllSiteResourcesByOrg
); );
authenticated.get( authenticated.get(
"/site-resource/:siteResourceId", ["/site-resource/:siteResourceId", "/private-resource/:siteResourceId"],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyApiKeyHasAction(ActionsEnum.getSiteResource), verifyApiKeyHasAction(ActionsEnum.getSiteResource),
siteResource.getSiteResource siteResource.getSiteResource
); );
authenticated.post( authenticated.post(
"/site-resource/:siteResourceId", ["/site-resource/:siteResourceId", "/private-resource/:siteResourceId"],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateSiteResource), verifyApiKeyHasAction(ActionsEnum.updateSiteResource),
@@ -203,7 +206,7 @@ authenticated.post(
); );
authenticated.delete( authenticated.delete(
"/site-resource/:siteResourceId", ["/site-resource/:siteResourceId", "/private-resource/:siteResourceId"],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyApiKeyHasAction(ActionsEnum.deleteSiteResource), verifyApiKeyHasAction(ActionsEnum.deleteSiteResource),
logActionAudit(ActionsEnum.deleteSiteResource), logActionAudit(ActionsEnum.deleteSiteResource),
@@ -211,28 +214,40 @@ authenticated.delete(
); );
authenticated.get( authenticated.get(
[
"/site-resource/:siteResourceId/roles", "/site-resource/:siteResourceId/roles",
"/private-resource/:siteResourceId/roles"
],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listResourceRoles), verifyApiKeyHasAction(ActionsEnum.listResourceRoles),
siteResource.listSiteResourceRoles siteResource.listSiteResourceRoles
); );
authenticated.get( authenticated.get(
[
"/site-resource/:siteResourceId/users", "/site-resource/:siteResourceId/users",
"/private-resource/:siteResourceId/users"
],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listResourceUsers), verifyApiKeyHasAction(ActionsEnum.listResourceUsers),
siteResource.listSiteResourceUsers siteResource.listSiteResourceUsers
); );
authenticated.get( authenticated.get(
[
"/site-resource/:siteResourceId/clients", "/site-resource/:siteResourceId/clients",
"/private-resource/:siteResourceId/clients"
],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listResourceUsers), verifyApiKeyHasAction(ActionsEnum.listResourceUsers),
siteResource.listSiteResourceClients siteResource.listSiteResourceClients
); );
authenticated.post( authenticated.post(
[
"/site-resource/:siteResourceId/roles", "/site-resource/:siteResourceId/roles",
"/private-resource/:siteResourceId/roles"
],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyApiKeyRoleAccess, verifyApiKeyRoleAccess,
verifyLimits, verifyLimits,
@@ -242,7 +257,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
"/site-resource/:siteResourceId/users", "/site-resource/:siteResourceId/users",
"/private-resource/:siteResourceId/users"
],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceUsers, verifyApiKeySetResourceUsers,
verifyLimits, verifyLimits,
@@ -252,7 +270,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
"/site-resource/:siteResourceId/roles/add", "/site-resource/:siteResourceId/roles/add",
"/private-resource/:siteResourceId/roles/add"
],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyApiKeyRoleAccess, verifyApiKeyRoleAccess,
verifyLimits, verifyLimits,
@@ -262,7 +283,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
"/site-resource/:siteResourceId/roles/remove", "/site-resource/:siteResourceId/roles/remove",
"/private-resource/:siteResourceId/roles/remove"
],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyApiKeyRoleAccess, verifyApiKeyRoleAccess,
verifyLimits, verifyLimits,
@@ -272,7 +296,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
"/site-resource/:siteResourceId/users/add", "/site-resource/:siteResourceId/users/add",
"/private-resource/:siteResourceId/users/add"
],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceUsers, verifyApiKeySetResourceUsers,
verifyLimits, verifyLimits,
@@ -282,7 +309,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
"/site-resource/:siteResourceId/users/remove", "/site-resource/:siteResourceId/users/remove",
"/private-resource/:siteResourceId/users/remove"
],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceUsers, verifyApiKeySetResourceUsers,
verifyLimits, verifyLimits,
@@ -292,7 +322,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
"/site-resource/:siteResourceId/clients", "/site-resource/:siteResourceId/clients",
"/private-resource/:siteResourceId/clients"
],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceClients, verifyApiKeySetResourceClients,
verifyLimits, verifyLimits,
@@ -302,7 +335,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
"/site-resource/:siteResourceId/clients/add", "/site-resource/:siteResourceId/clients/add",
"/private-resource/:siteResourceId/clients/add"
],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceClients, verifyApiKeySetResourceClients,
verifyLimits, verifyLimits,
@@ -312,7 +348,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
"/site-resource/:siteResourceId/clients/remove", "/site-resource/:siteResourceId/clients/remove",
"/private-resource/:siteResourceId/clients/remove"
],
verifyApiKeySiteResourceAccess, verifyApiKeySiteResourceAccess,
verifyApiKeySetResourceClients, verifyApiKeySetResourceClients,
verifyLimits, verifyLimits,
@@ -322,7 +361,7 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
"/client/:clientId/site-resources", ["/client/:clientId/site-resources", "/client/:clientId/private-resources"],
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceUsers), verifyApiKeyHasAction(ActionsEnum.setResourceUsers),
logActionAudit(ActionsEnum.setResourceUsers), logActionAudit(ActionsEnum.setResourceUsers),
@@ -330,7 +369,7 @@ authenticated.post(
); );
authenticated.put( authenticated.put(
"/org/:orgId/resource", ["/org/:orgId/resource", "/org/:orgId/public-resource"],
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResource), verifyApiKeyHasAction(ActionsEnum.createResource),
@@ -339,7 +378,10 @@ authenticated.put(
); );
authenticated.put( authenticated.put(
[
"/org/:orgId/site/:siteId/resource", "/org/:orgId/site/:siteId/resource",
"/org/:orgId/site/:siteId/public-resource"
],
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResource), verifyApiKeyHasAction(ActionsEnum.createResource),
@@ -348,14 +390,14 @@ authenticated.put(
); );
authenticated.get( authenticated.get(
"/site/:siteId/resources", ["/site/:siteId/resources", "/site/:siteId/public-resources"],
verifyApiKeySiteAccess, verifyApiKeySiteAccess,
verifyApiKeyHasAction(ActionsEnum.listResources), verifyApiKeyHasAction(ActionsEnum.listResources),
resource.listResources resource.listResources
); );
authenticated.get( authenticated.get(
"/org/:orgId/resources", ["/org/:orgId/resources", "/org/:orgId/public-resources"],
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listResources), verifyApiKeyHasAction(ActionsEnum.listResources),
resource.listResources resource.listResources
@@ -384,6 +426,15 @@ authenticated.put(
domain.createOrgDomain domain.createOrgDomain
); );
authenticated.post(
"/org/:orgId/domain/:domainId",
verifyApiKeyOrgAccess,
verifyApiKeyDomainAccess,
verifyApiKeyHasAction(ActionsEnum.updateOrgDomain),
domain.updateOrgDomain
);
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.patch( authenticated.patch(
"/org/:orgId/domain/:domainId", "/org/:orgId/domain/:domainId",
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
@@ -443,42 +494,45 @@ authenticated.delete(
); );
authenticated.get( authenticated.get(
"/resource/:resourceId/roles", ["/resource/:resourceId/roles", "/public-resource/:resourceId/roles"],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listResourceRoles), verifyApiKeyHasAction(ActionsEnum.listResourceRoles),
resource.listResourceRoles resource.listResourceRoles
); );
authenticated.get( authenticated.get(
"/resource/:resourceId/users", ["/resource/:resourceId/users", "/public-resource/:resourceId/users"],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listResourceUsers), verifyApiKeyHasAction(ActionsEnum.listResourceUsers),
resource.listResourceUsers resource.listResourceUsers
); );
authenticated.get( authenticated.get(
"/resource/:resourceId", ["/resource/:resourceId", "/public-resource/:resourceId"],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.getResource), verifyApiKeyHasAction(ActionsEnum.getResource),
resource.getResource resource.getResource
); );
authenticated.get( authenticated.get(
[
"/resource-policy/:resourcePolicyId", "/resource-policy/:resourcePolicyId",
"/public-resource-policy/:resourcePolicyId"
],
verifyApiKeyResourcePolicyAccess, verifyApiKeyResourcePolicyAccess,
verifyApiKeyHasAction(ActionsEnum.getResourcePolicy), verifyApiKeyHasAction(ActionsEnum.getResourcePolicy),
policy.getResourcePolicy policy.getResourcePolicy
); );
authenticated.get( authenticated.get(
"/resource/:resourceId/policies", ["/resource/:resourceId/policies", "/public-resource/:resourceId/policies"],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.getResourcePolicy), verifyApiKeyHasAction(ActionsEnum.getResourcePolicy),
resource.getResourcePolicies resource.getResourcePolicies
); );
authenticated.post( authenticated.post(
"/resource/:resourceId", ["/resource/:resourceId", "/public-resource/:resourceId"],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateResource), verifyApiKeyHasAction(ActionsEnum.updateResource),
@@ -486,15 +540,29 @@ authenticated.post(
resource.updateResource resource.updateResource
); );
authenticated.put( authenticated.post(
[
"/resource-policy/:resourcePolicyId", "/resource-policy/:resourcePolicyId",
"/public-resource-policy/:resourcePolicyId"
],
verifyApiKeyResourcePolicyAccess,
verifyApiKeyHasAction(ActionsEnum.updateResourcePolicy),
policy.updateResourcePolicy
);
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.put(
[
"/resource-policy/:resourcePolicyId",
"/public-resource-policy/:resourcePolicyId"
],
verifyApiKeyResourcePolicyAccess, verifyApiKeyResourcePolicyAccess,
verifyApiKeyHasAction(ActionsEnum.updateResourcePolicy), verifyApiKeyHasAction(ActionsEnum.updateResourcePolicy),
policy.updateResourcePolicy policy.updateResourcePolicy
); );
authenticated.delete( authenticated.delete(
"/resource/:resourceId", ["/resource/:resourceId", "/public-resource/:resourceId"],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.deleteResource), verifyApiKeyHasAction(ActionsEnum.deleteResource),
logActionAudit(ActionsEnum.deleteResource), logActionAudit(ActionsEnum.deleteResource),
@@ -502,7 +570,7 @@ authenticated.delete(
); );
authenticated.put( authenticated.put(
"/resource/:resourceId/target", ["/resource/:resourceId/target", "/public-resource/:resourceId/target"],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createTarget), verifyApiKeyHasAction(ActionsEnum.createTarget),
@@ -511,14 +579,14 @@ authenticated.put(
); );
authenticated.get( authenticated.get(
"/resource/:resourceId/targets", ["/resource/:resourceId/targets", "/public-resource/:resourceId/targets"],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listTargets), verifyApiKeyHasAction(ActionsEnum.listTargets),
target.listTargets target.listTargets
); );
authenticated.put( authenticated.put(
"/resource/:resourceId/rule", ["/resource/:resourceId/rule", "/public-resource/:resourceId/rule"],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.createResourceRule), verifyApiKeyHasAction(ActionsEnum.createResourceRule),
@@ -527,14 +595,17 @@ authenticated.put(
); );
authenticated.get( authenticated.get(
"/resource/:resourceId/rules", ["/resource/:resourceId/rules", "/public-resource/:resourceId/rules"],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listResourceRules), verifyApiKeyHasAction(ActionsEnum.listResourceRules),
resource.listResourceRules resource.listResourceRules
); );
authenticated.post( authenticated.post(
[
"/resource/:resourceId/rule/:ruleId", "/resource/:resourceId/rule/:ruleId",
"/public-resource/:resourceId/rule/:ruleId"
],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.updateResourceRule), verifyApiKeyHasAction(ActionsEnum.updateResourceRule),
@@ -543,7 +614,10 @@ authenticated.post(
); );
authenticated.delete( authenticated.delete(
[
"/resource/:resourceId/rule/:ruleId", "/resource/:resourceId/rule/:ruleId",
"/public-resource/:resourceId/rule/:ruleId"
],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.deleteResourceRule), verifyApiKeyHasAction(ActionsEnum.deleteResourceRule),
logActionAudit(ActionsEnum.deleteResourceRule), logActionAudit(ActionsEnum.deleteResourceRule),
@@ -625,7 +699,7 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
"/resource/:resourceId/roles", ["/resource/:resourceId/roles", "/public-resource/:resourceId/roles"],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeyRoleAccess, verifyApiKeyRoleAccess,
verifyLimits, verifyLimits,
@@ -635,7 +709,7 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
"/resource/:resourceId/users", ["/resource/:resourceId/users", "/public-resource/:resourceId/users"],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeySetResourceUsers, verifyApiKeySetResourceUsers,
verifyLimits, verifyLimits,
@@ -644,8 +718,11 @@ authenticated.post(
resource.setResourceUsers resource.setResourceUsers
); );
authenticated.put( authenticated.post(
[
"/resource-policy/:resourcePolicyId/access-control", "/resource-policy/:resourcePolicyId/access-control",
"/public-resource-policy/:resourcePolicyId/access-control"
],
verifyApiKeyResourcePolicyAccess, verifyApiKeyResourcePolicyAccess,
verifyApiKeyRoleAccess, verifyApiKeyRoleAccess,
verifyLimits, verifyLimits,
@@ -656,8 +733,27 @@ authenticated.put(
policy.setResourcePolicyAccessControl policy.setResourcePolicyAccessControl
); );
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.put( authenticated.put(
[
"/resource-policy/:resourcePolicyId/access-control",
"/public-resource-policy/:resourcePolicyId/access-control"
],
verifyApiKeyResourcePolicyAccess,
verifyApiKeyRoleAccess,
verifyLimits,
verifyUserHasAction(ActionsEnum.setResourcePolicyUsers),
verifyUserHasAction(ActionsEnum.setResourcePolicyRoles),
logActionAudit(ActionsEnum.setResourcePolicyUsers),
logActionAudit(ActionsEnum.setResourcePolicyRoles),
policy.setResourcePolicyAccessControl
);
authenticated.post(
[
"/resource-policy/:resourcePolicyId/password", "/resource-policy/:resourcePolicyId/password",
"/public-resource-policy/:resourcePolicyId/password"
],
verifyApiKeyResourcePolicyAccess, verifyApiKeyResourcePolicyAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPassword), verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPassword),
@@ -665,8 +761,24 @@ authenticated.put(
policy.setResourcePolicyPassword policy.setResourcePolicyPassword
); );
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.put( authenticated.put(
[
"/resource-policy/:resourcePolicyId/password",
"/public-resource-policy/:resourcePolicyId/password"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPassword),
logActionAudit(ActionsEnum.setResourcePolicyPassword),
policy.setResourcePolicyPassword
);
authenticated.post(
[
"/resource-policy/:resourcePolicyId/pincode", "/resource-policy/:resourcePolicyId/pincode",
"/public-resource-policy/:resourcePolicyId/pincode"
],
verifyApiKeyResourcePolicyAccess, verifyApiKeyResourcePolicyAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPincode), verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPincode),
@@ -674,8 +786,24 @@ authenticated.put(
policy.setResourcePolicyPincode policy.setResourcePolicyPincode
); );
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.put( authenticated.put(
[
"/resource-policy/:resourcePolicyId/pincode",
"/public-resource-policy/:resourcePolicyId/pincode"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyPincode),
logActionAudit(ActionsEnum.setResourcePolicyPincode),
policy.setResourcePolicyPincode
);
authenticated.post(
[
"/resource-policy/:resourcePolicyId/header-auth", "/resource-policy/:resourcePolicyId/header-auth",
"/public-resource-policy/:resourcePolicyId/header-auth"
],
verifyApiKeyResourcePolicyAccess, verifyApiKeyResourcePolicyAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyHeaderAuth), verifyApiKeyHasAction(ActionsEnum.setResourcePolicyHeaderAuth),
@@ -683,8 +811,24 @@ authenticated.put(
policy.setResourcePolicyHeaderAuth policy.setResourcePolicyHeaderAuth
); );
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.put( authenticated.put(
[
"/resource-policy/:resourcePolicyId/header-auth",
"/public-resource-policy/:resourcePolicyId/header-auth"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyHeaderAuth),
logActionAudit(ActionsEnum.setResourcePolicyHeaderAuth),
policy.setResourcePolicyHeaderAuth
);
authenticated.post(
[
"/resource-policy/:resourcePolicyId/whitelist", "/resource-policy/:resourcePolicyId/whitelist",
"/public-resource-policy/:resourcePolicyId/whitelist"
],
verifyApiKeyResourcePolicyAccess, verifyApiKeyResourcePolicyAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyWhitelist), verifyApiKeyHasAction(ActionsEnum.setResourcePolicyWhitelist),
@@ -692,8 +836,37 @@ authenticated.put(
policy.setResourcePolicyWhitelist policy.setResourcePolicyWhitelist
); );
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.put( authenticated.put(
[
"/resource-policy/:resourcePolicyId/whitelist",
"/public-resource-policy/:resourcePolicyId/whitelist"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyWhitelist),
logActionAudit(ActionsEnum.setResourcePolicyWhitelist),
policy.setResourcePolicyWhitelist
);
authenticated.post(
[
"/resource-policy/:resourcePolicyId/rules", "/resource-policy/:resourcePolicyId/rules",
"/public-resource-policy/:resourcePolicyId/rules"
],
verifyApiKeyResourcePolicyAccess,
verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyRules),
logActionAudit(ActionsEnum.setResourcePolicyRules),
policy.setResourcePolicyRules
);
// Deprecated: use POST instead. Kept for backward compatibility.
authenticated.put(
[
"/resource-policy/:resourcePolicyId/rules",
"/public-resource-policy/:resourcePolicyId/rules"
],
verifyApiKeyResourcePolicyAccess, verifyApiKeyResourcePolicyAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePolicyRules), verifyApiKeyHasAction(ActionsEnum.setResourcePolicyRules),
@@ -702,7 +875,10 @@ authenticated.put(
); );
authenticated.post( authenticated.post(
[
"/resource/:resourceId/roles/add", "/resource/:resourceId/roles/add",
"/public-resource/:resourceId/roles/add"
],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeyRoleAccess, verifyApiKeyRoleAccess,
verifyLimits, verifyLimits,
@@ -712,7 +888,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
"/resource/:resourceId/roles/remove", "/resource/:resourceId/roles/remove",
"/public-resource/:resourceId/roles/remove"
],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeyRoleAccess, verifyApiKeyRoleAccess,
verifyLimits, verifyLimits,
@@ -722,7 +901,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
"/resource/:resourceId/users/add", "/resource/:resourceId/users/add",
"/public-resource/:resourceId/users/add"
],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeySetResourceUsers, verifyApiKeySetResourceUsers,
verifyLimits, verifyLimits,
@@ -732,7 +914,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
"/resource/:resourceId/users/remove", "/resource/:resourceId/users/remove",
"/public-resource/:resourceId/users/remove"
],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeySetResourceUsers, verifyApiKeySetResourceUsers,
verifyLimits, verifyLimits,
@@ -742,7 +927,7 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
`/resource/:resourceId/password`, [`/resource/:resourceId/password`, `/public-resource/:resourceId/password`],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePassword), verifyApiKeyHasAction(ActionsEnum.setResourcePassword),
@@ -751,7 +936,7 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
`/resource/:resourceId/pincode`, [`/resource/:resourceId/pincode`, `/public-resource/:resourceId/pincode`],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourcePincode), verifyApiKeyHasAction(ActionsEnum.setResourcePincode),
@@ -760,7 +945,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
`/resource/:resourceId/header-auth`, `/resource/:resourceId/header-auth`,
`/public-resource/:resourceId/header-auth`
],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceHeaderAuth), verifyApiKeyHasAction(ActionsEnum.setResourceHeaderAuth),
@@ -769,7 +957,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
`/resource/:resourceId/whitelist`, `/resource/:resourceId/whitelist`,
`/public-resource/:resourceId/whitelist`
],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist), verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
@@ -778,7 +969,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
`/resource/:resourceId/whitelist/add`, `/resource/:resourceId/whitelist/add`,
`/public-resource/:resourceId/whitelist/add`
],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist), verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
@@ -786,7 +980,10 @@ authenticated.post(
); );
authenticated.post( authenticated.post(
[
`/resource/:resourceId/whitelist/remove`, `/resource/:resourceId/whitelist/remove`,
`/public-resource/:resourceId/whitelist/remove`
],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist), verifyApiKeyHasAction(ActionsEnum.setResourceWhitelist),
@@ -794,14 +991,20 @@ authenticated.post(
); );
authenticated.get( authenticated.get(
[
`/resource/:resourceId/whitelist`, `/resource/:resourceId/whitelist`,
`/public-resource/:resourceId/whitelist`
],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.getResourceWhitelist), verifyApiKeyHasAction(ActionsEnum.getResourceWhitelist),
resource.getResourceWhitelist resource.getResourceWhitelist
); );
authenticated.post( authenticated.post(
[
`/resource/:resourceId/access-token`, `/resource/:resourceId/access-token`,
`/public-resource/:resourceId/access-token`
],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyLimits, verifyLimits,
verifyApiKeyHasAction(ActionsEnum.generateAccessToken), verifyApiKeyHasAction(ActionsEnum.generateAccessToken),
@@ -825,7 +1028,10 @@ authenticated.get(
); );
authenticated.get( authenticated.get(
[
`/resource/:resourceId/access-tokens`, `/resource/:resourceId/access-tokens`,
`/public-resource/:resourceId/access-tokens`
],
verifyApiKeyResourceAccess, verifyApiKeyResourceAccess,
verifyApiKeyHasAction(ActionsEnum.listAccessTokens), verifyApiKeyHasAction(ActionsEnum.listAccessTokens),
accessToken.listAccessTokens accessToken.listAccessTokens
@@ -1155,7 +1361,7 @@ authenticated.get(
); );
authenticated.get( authenticated.get(
"/org/:orgId/resource-names", ["/org/:orgId/resource-names", "/org/:orgId/public-resource-names"],
verifyApiKeyOrgAccess, verifyApiKeyOrgAccess,
verifyApiKeyHasAction(ActionsEnum.listResources), verifyApiKeyHasAction(ActionsEnum.listResources),
resource.listAllResourceNames resource.listAllResourceNames
+9 -1
View File
@@ -19,6 +19,7 @@ import { eq, and, inArray } from "drizzle-orm";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { decrypt } from "@server/lib/crypto"; import { decrypt } from "@server/lib/crypto";
import { import {
batchFetchCertsForSiteResources,
formatEndpoint, formatEndpoint,
generateSubnetProxyTargetV2, generateSubnetProxyTargetV2,
SubnetProxyTargetV2 SubnetProxyTargetV2
@@ -206,11 +207,18 @@ export async function buildClientConfigurationForNewtClient(
}); });
} }
// Batch-fetch certs for every SSL-enabled HTTP resource's domain in one
// call rather than letting each resource fetch its own — with thousands
// of resources this avoids a concurrent DB/cache stampede for what is
// often the very same (e.g. wildcard) certificate.
const certByDomain = await batchFetchCertsForSiteResources(allSiteResources);
const resourceTargetsArr = await Promise.all( const resourceTargetsArr = await Promise.all(
allSiteResources.map((resource) => allSiteResources.map((resource) =>
generateSubnetProxyTargetV2( generateSubnetProxyTargetV2(
resource, resource,
clientsByResourceId.get(resource.siteResourceId) ?? [] clientsByResourceId.get(resource.siteResourceId) ?? [],
certByDomain
) )
) )
); );
+75
View File
@@ -0,0 +1,75 @@
import { sendToClient } from "#dynamic/routers/ws";
import logger from "@server/logger";
import {
canCompress,
supportsCertReferences
} from "@server/lib/clientVersionChecks";
import { CertRef } from "@server/lib/ip";
/**
* Pushes an incremental set of certs to a newt client outside of a full
* newt/sync or newt/wg/receive-config, e.g. after a certificate renewal so
* that every target referencing it (by tlsCertId) picks up the new material
* without waiting for the next full resync.
*/
export async function sendCertsAdd(
newtId: string,
certs: CertRef[],
version?: string | null
) {
if (certs.length === 0) {
return;
}
if (!supportsCertReferences(version)) {
logger.debug(
`Newt ${newtId} (version ${version}) does not support cert references, skipping certs/add`
);
return;
}
await sendToClient(
newtId,
{
type: "newt/certs/add",
data: certs
},
{
incrementConfigVersion: true,
compress: canCompress(version, "newt")
}
);
}
/**
* Tells a newt client to drop the given cert IDs, e.g. once the server knows
* no target references them anymore.
*/
export async function sendCertsRemove(
newtId: string,
certIds: string[],
version?: string | null
) {
if (certIds.length === 0) {
return;
}
if (!supportsCertReferences(version)) {
logger.debug(
`Newt ${newtId} (version ${version}) does not support cert references, skipping certs/remove`
);
return;
}
await sendToClient(
newtId,
{
type: "newt/certs/remove",
data: { ids: certIds }
},
{
incrementConfigVersion: true,
compress: canCompress(version, "newt")
}
);
}
@@ -7,7 +7,11 @@ import { eq } from "drizzle-orm";
import { sendToExitNode } from "#dynamic/lib/exitNodes"; import { sendToExitNode } from "#dynamic/lib/exitNodes";
import { buildClientConfigurationForNewtClient } from "./buildConfiguration"; import { buildClientConfigurationForNewtClient } from "./buildConfiguration";
import { convertTargetsIfNecessary } from "../client/targets"; import { convertTargetsIfNecessary } from "../client/targets";
import { canCompress } from "@server/lib/clientVersionChecks"; import {
canCompress,
supportsCertReferences
} from "@server/lib/clientVersionChecks";
import { dedupeCertsForTargets } from "@server/lib/ip";
import config from "@server/lib/config"; import config from "@server/lib/config";
import { waitForSiteRebuildIdle } from "@server/lib/rebuildClientAssociations"; import { waitForSiteRebuildIdle } from "@server/lib/rebuildClientAssociations";
@@ -119,7 +123,16 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
exitNode exitNode
); );
const targetsToSend = await convertTargetsIfNecessary(newt.newtId, targets); // for backward compatibility with old newt versions that don't support the new target format // Older newt clients only understand inline tlsCert/tlsKey on each
// target, so only switch to certId references once we know the client
// can resolve them.
let dedupedTargets = targets;
let certs: { id: string; cert: string; key: string }[] = [];
if (supportsCertReferences(newt.version)) {
({ 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
return { return {
message: { message: {
@@ -128,6 +141,7 @@ export const handleNewtGetConfigMessage: MessageHandler = async (context) => {
ipAddress: site.address, ipAddress: site.address,
peers, peers,
targets: targetsToSend, targets: targetsToSend,
certs,
chainId: chainId chainId: chainId
} }
}, },
+17 -2
View File
@@ -6,7 +6,11 @@ import {
buildClientConfigurationForNewtClient, buildClientConfigurationForNewtClient,
buildTargetConfigurationForNewtClient buildTargetConfigurationForNewtClient
} from "./buildConfiguration"; } from "./buildConfiguration";
import { canCompress } from "@server/lib/clientVersionChecks"; import {
canCompress,
supportsCertReferences
} from "@server/lib/clientVersionChecks";
import { dedupeCertsForTargets } from "@server/lib/ip";
export async function sendNewtSyncMessage(newt: Newt, site: Site) { export async function sendNewtSyncMessage(newt: Newt, site: Site) {
const { const {
@@ -28,6 +32,16 @@ export async function sendNewtSyncMessage(newt: Newt, site: Site) {
site, site,
exitNode exitNode
); );
// Older newt clients only understand inline tlsCert/tlsKey on each
// target, so only switch to certId references once we know the client
// can resolve them.
let clientTargets = targets;
let certs: { id: string; cert: string; key: string }[] = [];
if (supportsCertReferences(newt.version)) {
({ targets: clientTargets, certs } = dedupeCertsForTargets(targets));
}
await sendToClient( await sendToClient(
newt.newtId, newt.newtId,
{ {
@@ -39,7 +53,8 @@ export async function sendNewtSyncMessage(newt: Newt, site: Site) {
}, },
healthCheckTargets: validHealthCheckTargets, healthCheckTargets: validHealthCheckTargets,
peers: peers, peers: peers,
clientTargets: targets, clientTargets: clientTargets,
certs: certs,
browserGatewayTargets: browserGatewayTargets, browserGatewayTargets: browserGatewayTargets,
remoteExitNodeSubnets: remoteExitNodeSubnets remoteExitNodeSubnets: remoteExitNodeSubnets
} }
+15 -2
View File
@@ -168,7 +168,7 @@ registry.registerPath({
path: "/org/{orgId}/resource-policy/{niceId}", path: "/org/{orgId}/resource-policy/{niceId}",
description: description:
"Get a resource policy by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.", "Get a resource policy by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.",
tags: [OpenAPITags.Org, OpenAPITags.Policy], tags: [OpenAPITags.PublicResourcePolicy],
request: { request: {
params: z.object({ params: z.object({
orgId: z.string(), orgId: z.string(),
@@ -182,7 +182,20 @@ registry.registerPath({
method: "get", method: "get",
path: "/resource-policy/{resourcePolicyId}", path: "/resource-policy/{resourcePolicyId}",
description: "Get a resource policy by its resourcePolicyId.", description: "Get a resource policy by its resourcePolicyId.",
tags: [OpenAPITags.Policy], tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: z.object({
resourcePolicyId: z.number()
})
},
responses: {}
});
registry.registerPath({
method: "get",
path: "/public-resource-policy/{resourcePolicyId}",
description: "Get a resource policy by its resourcePolicyId.",
tags: [OpenAPITags.PublicResourcePolicy],
request: { request: {
params: z.object({ params: z.object({
resourcePolicyId: z.number() resourcePolicyId: z.number()
@@ -40,7 +40,66 @@ registry.registerPath({
path: "/resource-policy/{resourceId}/access-control", path: "/resource-policy/{resourceId}/access-control",
description: description:
"Set access control users for a resource policy, including SSO, users, roles, Identity provider.", "Set access control users for a resource policy, including SSO, users, roles, Identity provider.",
tags: [OpenAPITags.Policy, OpenAPITags.User], tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyAccessControlParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyAcccessControlBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "post",
path: "/public-resource-policy/{resourceId}/access-control",
description:
"Set access control users for a resource policy, including SSO, users, roles, Identity provider.",
tags: [OpenAPITags.PublicResourcePolicy, OpenAPITags.User],
request: {
params: setResourcePolicyAccessControlParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyAcccessControlBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/resource-policy/{resourceId}/access-control",
description:
"Set access control users for a resource policy, including SSO, users, roles, Identity provider. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyAccessControlParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyAcccessControlBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/public-resource-policy/{resourceId}/access-control",
description:
"Set access control users for a resource policy, including SSO, users, roles, Identity provider. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicy, OpenAPITags.User],
request: { request: {
params: setResourcePolicyAccessControlParamsSchema, params: setResourcePolicyAccessControlParamsSchema,
body: { body: {
@@ -25,11 +25,70 @@ const setResourcePolicyHeaderAuthBodySchema = z.strictObject({
}); });
registry.registerPath({ registry.registerPath({
method: "put", method: "post",
path: "/resource-policy/{resourcePolicyId}/header-auth", path: "/resource-policy/{resourcePolicyId}/header-auth",
description: description:
"Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication.", "Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication.",
tags: [OpenAPITags.Policy], tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyHeaderAuthParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyHeaderAuthBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "post",
path: "/public-resource-policy/{resourcePolicyId}/header-auth",
description:
"Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication.",
tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: setResourcePolicyHeaderAuthParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyHeaderAuthBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/resource-policy/{resourcePolicyId}/header-auth",
description:
"Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyHeaderAuthParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyHeaderAuthBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/public-resource-policy/{resourcePolicyId}/header-auth",
description:
"Set or update the header authentication for a resource policy. If user and password is not provided, it will remove the header authentication. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicy],
request: { request: {
params: setResourcePolicyHeaderAuthParamsSchema, params: setResourcePolicyHeaderAuthParamsSchema,
body: { body: {
@@ -20,11 +20,70 @@ const setResourcePolicyPasswordBodySchema = z.strictObject({
}); });
registry.registerPath({ registry.registerPath({
method: "put", method: "post",
path: "/resource-policy/{resourcePolicyId}/password", path: "/resource-policy/{resourcePolicyId}/password",
description: description:
"Set the password for a resource policy. Setting the password to null will remove it.", "Set the password for a resource policy. Setting the password to null will remove it.",
tags: [OpenAPITags.Policy], tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyPasswordParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyPasswordBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "post",
path: "/public-resource-policy/{resourcePolicyId}/password",
description:
"Set the password for a resource policy. Setting the password to null will remove it.",
tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: setResourcePolicyPasswordParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyPasswordBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/resource-policy/{resourcePolicyId}/password",
description:
"Set the password for a resource policy. Setting the password to null will remove it. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyPasswordParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyPasswordBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/public-resource-policy/{resourcePolicyId}/password",
description:
"Set the password for a resource policy. Setting the password to null will remove it. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicy],
request: { request: {
params: setResourcePolicyPasswordParamsSchema, params: setResourcePolicyPasswordParamsSchema,
body: { body: {
@@ -23,11 +23,70 @@ const setResourcePolicyPincodeBodySchema = z.strictObject({
}); });
registry.registerPath({ registry.registerPath({
method: "put", method: "post",
path: "/resource-policy/{resourcePolicyId}/pincode", path: "/resource-policy/{resourcePolicyId}/pincode",
description: description:
"Set the PIN code for a resource policy. Setting the PIN code to null will remove it.", "Set the PIN code for a resource policy. Setting the PIN code to null will remove it.",
tags: [OpenAPITags.Policy], tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyPincodeParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyPincodeBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "post",
path: "/public-resource-policy/{resourcePolicyId}/pincode",
description:
"Set the PIN code for a resource policy. Setting the PIN code to null will remove it.",
tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: setResourcePolicyPincodeParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyPincodeBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/resource-policy/{resourcePolicyId}/pincode",
description:
"Set the PIN code for a resource policy. Setting the PIN code to null will remove it. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyPincodeParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyPincodeBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/public-resource-policy/{resourcePolicyId}/pincode",
description:
"Set the PIN code for a resource policy. Setting the PIN code to null will remove it. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicy],
request: { request: {
params: setResourcePolicyPincodeParamsSchema, params: setResourcePolicyPincodeParamsSchema,
body: { body: {
@@ -43,11 +43,70 @@ const setResourcePolicyRulesParamsSchema = z.strictObject({
}); });
registry.registerPath({ registry.registerPath({
method: "put", method: "post",
path: "/resource-policy/{resourcePolicyId}/rules", path: "/resource-policy/{resourcePolicyId}/rules",
description: description:
"Set all rules for a resource policy at once. This will replace all existing rules.", "Set all rules for a resource policy at once. This will replace all existing rules.",
tags: [OpenAPITags.Policy], tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyRulesParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyRulesBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "post",
path: "/public-resource-policy/{resourcePolicyId}/rules",
description:
"Set all rules for a resource policy at once. This will replace all existing rules.",
tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: setResourcePolicyRulesParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyRulesBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/resource-policy/{resourcePolicyId}/rules",
description:
"Set all rules for a resource policy at once. This will replace all existing rules. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyRulesParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyRulesBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/public-resource-policy/{resourcePolicyId}/rules",
description:
"Set all rules for a resource policy at once. This will replace all existing rules. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicy],
request: { request: {
params: setResourcePolicyRulesParamsSchema, params: setResourcePolicyRulesParamsSchema,
body: { body: {
@@ -28,11 +28,70 @@ const setResourcePolicyWhitelistParamsSchema = z.strictObject({
}); });
registry.registerPath({ registry.registerPath({
method: "put", method: "post",
path: "/resource-policy/{resourcePolicyId}/whitelist", path: "/resource-policy/{resourcePolicyId}/whitelist",
description: description:
"Set email whitelist for a resource policy. This will replace all existing emails.", "Set email whitelist for a resource policy. This will replace all existing emails.",
tags: [OpenAPITags.Policy], tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyWhitelistParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyWhitelistBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "post",
path: "/public-resource-policy/{resourcePolicyId}/whitelist",
description:
"Set email whitelist for a resource policy. This will replace all existing emails.",
tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: setResourcePolicyWhitelistParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyWhitelistBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/resource-policy/{resourcePolicyId}/whitelist",
description:
"Set email whitelist for a resource policy. This will replace all existing emails. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicyLegacy],
request: {
params: setResourcePolicyWhitelistParamsSchema,
body: {
content: {
"application/json": {
schema: setResourcePolicyWhitelistBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/public-resource-policy/{resourcePolicyId}/whitelist",
description:
"Set email whitelist for a resource policy. This will replace all existing emails. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicy],
request: { request: {
params: setResourcePolicyWhitelistParamsSchema, params: setResourcePolicyWhitelistParamsSchema,
body: { body: {
+58 -2
View File
@@ -19,10 +19,66 @@ const updateResourcePolicyBodySchema = z.strictObject({
}); });
registry.registerPath({ registry.registerPath({
method: "put", method: "post",
path: "/resource-policy/{resourcePolicyId}", path: "/resource-policy/{resourcePolicyId}",
description: "Update a resource policy.", description: "Update a resource policy.",
tags: [OpenAPITags.Org, OpenAPITags.Policy], tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: updateResourcePolicyParamsSchema,
body: {
content: {
"application/json": {
schema: updateResourcePolicyBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "post",
path: "/public-resource-policy/{resourcePolicyId}",
description: "Update a resource policy.",
tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: updateResourcePolicyParamsSchema,
body: {
content: {
"application/json": {
schema: updateResourcePolicyBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/resource-policy/{resourcePolicyId}",
description: "Update a resource policy. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicy],
request: {
params: updateResourcePolicyParamsSchema,
body: {
content: {
"application/json": {
schema: updateResourcePolicyBodySchema
}
}
}
},
responses: {}
});
registry.registerPath({
method: "put",
path: "/public-resource-policy/{resourcePolicyId}",
description: "Update a resource policy. Deprecated: use POST instead.",
deprecated: true,
tags: [OpenAPITags.PublicResourcePolicy],
request: { request: {
params: updateResourcePolicyParamsSchema, params: updateResourcePolicyParamsSchema,
body: { body: {
@@ -34,6 +34,39 @@ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/whitelist/add", path: "/resource/{resourceId}/whitelist/add",
description: "Add a single email to the resource whitelist.", description: "Add a single email to the resource whitelist.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: addEmailToResourceWhitelistParamsSchema,
body: {
content: {
"application/json": {
schema: addEmailToResourceWhitelistBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/whitelist/add",
description: "Add a single email to the resource whitelist.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResource],
request: { request: {
params: addEmailToResourceWhitelistParamsSchema, params: addEmailToResourceWhitelistParamsSchema,
@@ -144,10 +177,7 @@ export async function addEmailToResourceWhitelist(
.from(resourcePolicyWhiteList) .from(resourcePolicyWhiteList)
.where( .where(
and( and(
eq( eq(resourcePolicyWhiteList.resourcePolicyId, policyId),
resourcePolicyWhiteList.resourcePolicyId,
policyId
),
eq(resourcePolicyWhiteList.email, email) eq(resourcePolicyWhiteList.email, email)
) )
); );
@@ -28,6 +28,40 @@ const addRoleToResourceParamsSchema = z
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/roles/add", path: "/resource/{resourceId}/roles/add",
description:
"Add a single role to a resource. When the resource has an inline policy defined (no shared resource policy assigned), the role is added to the inline policy instead of directly to the resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: addRoleToResourceParamsSchema,
body: {
content: {
"application/json": {
schema: addRoleToResourceBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/roles/add",
description: description:
"Add a single role to a resource. When the resource has an inline policy defined (no shared resource policy assigned), the role is added to the inline policy instead of directly to the resource.", "Add a single role to a resource. When the resource has an inline policy defined (no shared resource policy assigned), the role is added to the inline policy instead of directly to the resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.Role], tags: [OpenAPITags.PublicResource, OpenAPITags.Role],
@@ -28,6 +28,40 @@ const addUserToResourceParamsSchema = z
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/users/add", path: "/resource/{resourceId}/users/add",
description:
"Add a single user to a resource. When the resource has an inline policy defined (no shared resource policy assigned), the user is added to the inline policy instead of directly to the resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: addUserToResourceParamsSchema,
body: {
content: {
"application/json": {
schema: addUserToResourceBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/users/add",
description: description:
"Add a single user to a resource. When the resource has an inline policy defined (no shared resource policy assigned), the user is added to the inline policy instead of directly to the resource.", "Add a single user to a resource. When the resource has an inline policy defined (no shared resource policy assigned), the user is added to the inline policy instead of directly to the resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.User], tags: [OpenAPITags.PublicResource, OpenAPITags.User],
+33
View File
@@ -153,6 +153,39 @@ registry.registerPath({
method: "put", method: "put",
path: "/org/{orgId}/resource", path: "/org/{orgId}/resource",
description: "Create a resource.", description: "Create a resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: createResourceParamsSchema,
body: {
content: {
"application/json": {
schema: createHttpResourceSchema.or(createRawResourceSchema)
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "put",
path: "/org/{orgId}/public-resource",
description: "Create a resource.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResource],
request: { request: {
params: createResourceParamsSchema, params: createResourceParamsSchema,
+41 -35
View File
@@ -9,16 +9,14 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { import {
isValidCIDR, RESOURCE_RULE_MATCH_TYPES,
isValidIP, getResourceRuleValueValidationError
isValidUrlGlobPattern
} from "@server/lib/validators"; } from "@server/lib/validators";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { isValidRegionId } from "@server/db/regions";
const createResourceRuleSchema = z.strictObject({ const createResourceRuleSchema = z.strictObject({
action: z.enum(["ACCEPT", "DROP", "PASS"]), action: z.enum(["ACCEPT", "DROP", "PASS"]),
match: z.enum(["CIDR", "IP", "PATH", "COUNTRY", "ASN", "REGION"]), match: z.enum(RESOURCE_RULE_MATCH_TYPES),
value: z.string().min(1), value: z.string().min(1),
priority: z.int(), priority: z.int(),
enabled: z.boolean().optional() enabled: z.boolean().optional()
@@ -32,6 +30,39 @@ registry.registerPath({
method: "put", method: "put",
path: "/resource/{resourceId}/rule", path: "/resource/{resourceId}/rule",
description: "Create a resource rule.", description: "Create a resource rule.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: createResourceRuleParamsSchema,
body: {
content: {
"application/json": {
schema: createResourceRuleSchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "put",
path: "/public-resource/{resourceId}/rule",
description: "Create a resource rule.",
tags: [OpenAPITags.PublicResource, OpenAPITags.Rule], tags: [OpenAPITags.PublicResource, OpenAPITags.Rule],
request: { request: {
params: createResourceRuleParamsSchema, params: createResourceRuleParamsSchema,
@@ -118,40 +149,15 @@ export async function createResourceRule(
); );
} }
if (match === "CIDR") { const valueValidationError = getResourceRuleValueValidationError(
if (!isValidCIDR(value)) { match,
return next( value
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid CIDR provided"
)
); );
} if (valueValidationError) {
} else if (match === "IP") {
if (!isValidIP(value)) {
return next( return next(
createHttpError(HttpCode.BAD_REQUEST, "Invalid IP provided") createHttpError(HttpCode.BAD_REQUEST, valueValidationError)
); );
} }
} else if (match === "PATH") {
if (!isValidUrlGlobPattern(value)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid URL glob pattern provided"
)
);
}
} else if (match === "REGION") {
if (!isValidRegionId(value)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid region ID provided"
)
);
}
}
// Create the new resource rule // Create the new resource rule
const isInlinePolicy = const isInlinePolicy =
+26
View File
@@ -22,6 +22,32 @@ registry.registerPath({
method: "delete", method: "delete",
path: "/resource/{resourceId}", path: "/resource/{resourceId}",
description: "Delete a resource.", description: "Delete a resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: deleteResourceSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "delete",
path: "/public-resource/{resourceId}",
description: "Delete a resource.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResource],
request: { request: {
params: deleteResourceSchema params: deleteResourceSchema
@@ -19,6 +19,32 @@ registry.registerPath({
method: "delete", method: "delete",
path: "/resource/{resourceId}/rule/{ruleId}", path: "/resource/{resourceId}/rule/{ruleId}",
description: "Delete a resource rule.", description: "Delete a resource rule.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: deleteResourceRuleSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "delete",
path: "/public-resource/{resourceId}/rule/{ruleId}",
description: "Delete a resource rule.",
tags: [OpenAPITags.PublicResource, OpenAPITags.Rule], tags: [OpenAPITags.PublicResource, OpenAPITags.Rule],
request: { request: {
params: deleteResourceRuleSchema params: deleteResourceRuleSchema
@@ -0,0 +1,83 @@
import response from "@server/lib/response";
import {
getBatchedStatusHistory,
type BatchedStatusHistoryResponse
} from "@server/lib/statusHistory";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const resourceIdParamsSchema = z.object({
days: z
.string()
.optional()
.transform((v) => (v ? parseInt(v, 10) : 90)),
// Minutes to add to UTC to get the requesting client's local time
// (e.g. Australia/Sydney standard time is 600). Optional and
// defaults to 0 (UTC) so older clients keep the prior behavior.
tzOffsetMinutes: z
.string()
.optional()
.transform((v) => (v ? parseInt(v, 10) : 0)),
resourceIds: z
.preprocess((val) => {
if (val === undefined || val === null || val === "") {
return undefined;
}
const raw = Array.isArray(val) ? val : [val];
const nums = raw
.map((v) =>
typeof v === "string" ? parseInt(v, 10) : Number(v)
)
.filter((n) => Number.isInteger(n) && n > 0);
const unique = [...new Set(nums)];
return unique.length ? unique : undefined;
}, z.array(z.number().int().positive()))
.openapi({
description: "Filter by resourceIds (repeat query param)"
})
});
export async function getBatchedResourceStatusHistory(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedQuery = resourceIdParamsSchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error).toString()
)
);
}
const entityType = "resource";
const { days, resourceIds, tzOffsetMinutes } = parsedQuery.data;
const data = await getBatchedStatusHistory(
entityType,
resourceIds,
days,
tzOffsetMinutes
);
return response<BatchedStatusHistoryResponse>(res, {
data,
success: true,
error: false,
message: "Status history retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
+29 -1
View File
@@ -63,7 +63,7 @@ registry.registerPath({
path: "/org/{orgId}/resource/{niceId}", path: "/org/{orgId}/resource/{niceId}",
description: description:
"Get a resource by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.", "Get a resource by orgId and niceId. NiceId is a readable ID for the resource and unique on a per org basis.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResourceLegacy],
request: { request: {
params: z.object({ params: z.object({
orgId: z.string(), orgId: z.string(),
@@ -92,6 +92,34 @@ registry.registerPath({
method: "get", method: "get",
path: "/resource/{resourceId}", path: "/resource/{resourceId}",
description: "Get a resource by resourceId.", description: "Get a resource by resourceId.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: z.object({
resourceId: z.number()
})
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "get",
path: "/public-resource/{resourceId}",
description: "Get a resource by resourceId.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResource],
request: { request: {
params: z.object({ params: z.object({
+15 -2
View File
@@ -25,8 +25,21 @@ export type GetResourcePoliciesResponse = {
registry.registerPath({ registry.registerPath({
method: "get", method: "get",
path: "/resource/{resourceId}/policies", path: "/resource/{resourceId}/policies",
description: "Get the inline and shared policies associated with a resource.", description:
tags: [OpenAPITags.PublicResource, OpenAPITags.Policy], "Get the inline and shared policies associated with a resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: getResourcePoliciesParamsSchema
},
responses: {}
});
registry.registerPath({
method: "get",
path: "/public-resource/{resourceId}/policies",
description:
"Get the inline and shared policies associated with a resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.PublicResourcePolicy],
request: { request: {
params: getResourcePoliciesParamsSchema params: getResourcePoliciesParamsSchema
}, },
@@ -44,6 +44,32 @@ registry.registerPath({
method: "get", method: "get",
path: "/resource/{resourceId}/whitelist", path: "/resource/{resourceId}/whitelist",
description: "Get the whitelist of emails for a specific resource.", description: "Get the whitelist of emails for a specific resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: getResourceWhitelistSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "get",
path: "/public-resource/{resourceId}/whitelist",
description: "Get the whitelist of emails for a specific resource.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResource],
request: { request: {
params: getResourceWhitelistSchema params: getResourceWhitelistSchema
+1
View File
@@ -33,4 +33,5 @@ export * from "./removeUserFromResource";
export * from "./listAllResourceNames"; export * from "./listAllResourceNames";
export * from "./removeEmailFromResourceWhitelist"; export * from "./removeEmailFromResourceWhitelist";
export * from "./getStatusHistory"; export * from "./getStatusHistory";
export * from "./getBatchedStatusHistory";
export * from "./getResourcePolicies"; export * from "./getResourcePolicies";
@@ -33,6 +33,34 @@ registry.registerPath({
method: "get", method: "get",
path: "/org/{orgId}/resources-names", path: "/org/{orgId}/resources-names",
description: "List all resource names for an organization.", description: "List all resource names for an organization.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: z.object({
orgId: z.string()
})
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "get",
path: "/org/{orgId}/public-resource-names",
description: "List all resource names for an organization.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResource],
request: { request: {
params: z.object({ params: z.object({
@@ -48,6 +48,32 @@ registry.registerPath({
method: "get", method: "get",
path: "/resource/{resourceId}/roles", path: "/resource/{resourceId}/roles",
description: "List all roles for a resource.", description: "List all roles for a resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: listResourceRolesSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "get",
path: "/public-resource/{resourceId}/roles",
description: "List all roles for a resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.Role], tags: [OpenAPITags.PublicResource, OpenAPITags.Role],
request: { request: {
params: listResourceRolesSchema params: listResourceRolesSchema
@@ -71,6 +71,33 @@ registry.registerPath({
method: "get", method: "get",
path: "/resource/{resourceId}/rules", path: "/resource/{resourceId}/rules",
description: "List rules for a resource.", description: "List rules for a resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: listResourceRulesParamsSchema,
query: listResourceRulesSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "get",
path: "/public-resource/{resourceId}/rules",
description: "List rules for a resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.Rule], tags: [OpenAPITags.PublicResource, OpenAPITags.Rule],
request: { request: {
params: listResourceRulesParamsSchema, params: listResourceRulesParamsSchema,
@@ -38,6 +38,32 @@ registry.registerPath({
method: "get", method: "get",
path: "/resource/{resourceId}/users", path: "/resource/{resourceId}/users",
description: "List all users for a resource.", description: "List all users for a resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: listResourceUsersSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "get",
path: "/public-resource/{resourceId}/users",
description: "List all users for a resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.User], tags: [OpenAPITags.PublicResource, OpenAPITags.User],
request: { request: {
params: listResourceUsersSchema params: listResourceUsersSchema
+29
View File
@@ -409,6 +409,35 @@ registry.registerPath({
method: "get", method: "get",
path: "/org/{orgId}/resources", path: "/org/{orgId}/resources",
description: "List resources for an organization.", description: "List resources for an organization.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: z.object({
orgId: z.string()
}),
query: listResourcesSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "get",
path: "/org/{orgId}/public-resources",
description: "List resources for an organization.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResource],
request: { request: {
params: z.object({ params: z.object({
@@ -34,6 +34,39 @@ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/whitelist/remove", path: "/resource/{resourceId}/whitelist/remove",
description: "Remove a single email from the resource whitelist.", description: "Remove a single email from the resource whitelist.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: removeEmailFromResourceWhitelistParamsSchema,
body: {
content: {
"application/json": {
schema: removeEmailFromResourceWhitelistBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/whitelist/remove",
description: "Remove a single email from the resource whitelist.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResource],
request: { request: {
params: removeEmailFromResourceWhitelistParamsSchema, params: removeEmailFromResourceWhitelistParamsSchema,
@@ -143,10 +176,7 @@ export async function removeEmailFromResourceWhitelist(
.from(resourcePolicyWhiteList) .from(resourcePolicyWhiteList)
.where( .where(
and( and(
eq( eq(resourcePolicyWhiteList.resourcePolicyId, policyId),
resourcePolicyWhiteList.resourcePolicyId,
policyId
),
eq(resourcePolicyWhiteList.email, email) eq(resourcePolicyWhiteList.email, email)
) )
); );
@@ -164,10 +194,7 @@ export async function removeEmailFromResourceWhitelist(
.delete(resourcePolicyWhiteList) .delete(resourcePolicyWhiteList)
.where( .where(
and( and(
eq( eq(resourcePolicyWhiteList.resourcePolicyId, policyId),
resourcePolicyWhiteList.resourcePolicyId,
policyId
),
eq(resourcePolicyWhiteList.email, email) eq(resourcePolicyWhiteList.email, email)
) )
); );
@@ -29,6 +29,39 @@ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/roles/remove", path: "/resource/{resourceId}/roles/remove",
description: "Remove a single role from a resource.", description: "Remove a single role from a resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: removeRoleFromResourceParamsSchema,
body: {
content: {
"application/json": {
schema: removeRoleFromResourceBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/roles/remove",
description: "Remove a single role from a resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.Role], tags: [OpenAPITags.PublicResource, OpenAPITags.Role],
request: { request: {
params: removeRoleFromResourceParamsSchema, params: removeRoleFromResourceParamsSchema,
@@ -29,6 +29,39 @@ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/users/remove", path: "/resource/{resourceId}/users/remove",
description: "Remove a single user from a resource.", description: "Remove a single user from a resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: removeUserFromResourceParamsSchema,
body: {
content: {
"application/json": {
schema: removeUserFromResourceBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/users/remove",
description: "Remove a single user from a resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.User], tags: [OpenAPITags.PublicResource, OpenAPITags.User],
request: { request: {
params: removeUserFromResourceParamsSchema, params: removeUserFromResourceParamsSchema,
@@ -29,6 +29,40 @@ const setResourceAuthMethodsBodySchema = z.strictObject({
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/header-auth", path: "/resource/{resourceId}/header-auth",
description:
"Set or update the header authentication for a resource. If user and password is not provided, it will remove the header authentication.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: setResourceAuthMethodsParamsSchema,
body: {
content: {
"application/json": {
schema: setResourceAuthMethodsBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/header-auth",
description: description:
"Set or update the header authentication for a resource. If user and password is not provided, it will remove the header authentication.", "Set or update the header authentication for a resource. If user and password is not provided, it will remove the header authentication.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResource],
@@ -27,6 +27,40 @@ const setResourceAuthMethodsBodySchema = z.strictObject({
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/password", path: "/resource/{resourceId}/password",
description:
"Set the password for a resource. Setting the password to null will remove it.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: setResourceAuthMethodsParamsSchema,
body: {
content: {
"application/json": {
schema: setResourceAuthMethodsBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/password",
description: description:
"Set the password for a resource. Setting the password to null will remove it.", "Set the password for a resource. Setting the password to null will remove it.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResource],
@@ -27,6 +27,40 @@ const setResourceAuthMethodsBodySchema = z.strictObject({
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/pincode", path: "/resource/{resourceId}/pincode",
description:
"Set the PIN code for a resource. Setting the PIN code to null will remove it.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: setResourceAuthMethodsParamsSchema,
body: {
content: {
"application/json": {
schema: setResourceAuthMethodsBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/pincode",
description: description:
"Set the PIN code for a resource. Setting the PIN code to null will remove it.", "Set the PIN code for a resource. Setting the PIN code to null will remove it.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResource],
@@ -21,6 +21,40 @@ const setResourceRolesParamsSchema = z.strictObject({
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/roles", path: "/resource/{resourceId}/roles",
description:
"Set roles for a resource. This will replace all existing roles. When the resource has an inline policy defined (no shared resource policy assigned), roles are set on the inline policy instead of directly on the resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: setResourceRolesParamsSchema,
body: {
content: {
"application/json": {
schema: setResourceRolesBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/roles",
description: description:
"Set roles for a resource. This will replace all existing roles. When the resource has an inline policy defined (no shared resource policy assigned), roles are set on the inline policy instead of directly on the resource.", "Set roles for a resource. This will replace all existing roles. When the resource has an inline policy defined (no shared resource policy assigned), roles are set on the inline policy instead of directly on the resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.Role], tags: [OpenAPITags.PublicResource, OpenAPITags.Role],
@@ -21,6 +21,40 @@ const setUserResourcesParamsSchema = z.strictObject({
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/users", path: "/resource/{resourceId}/users",
description:
"Set users for a resource. This will replace all existing users. When the resource has an inline policy defined (no shared resource policy assigned), users are set on the inline policy instead of directly on the resource.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: setUserResourcesParamsSchema,
body: {
content: {
"application/json": {
schema: setUserResourcesBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/users",
description: description:
"Set users for a resource. This will replace all existing users. When the resource has an inline policy defined (no shared resource policy assigned), users are set on the inline policy instead of directly on the resource.", "Set users for a resource. This will replace all existing users. When the resource has an inline policy defined (no shared resource policy assigned), users are set on the inline policy instead of directly on the resource.",
tags: [OpenAPITags.PublicResource, OpenAPITags.User], tags: [OpenAPITags.PublicResource, OpenAPITags.User],
@@ -35,6 +35,40 @@ const setResourceWhitelistParamsSchema = z.strictObject({
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/whitelist", path: "/resource/{resourceId}/whitelist",
description:
"Set email whitelist for a resource. This will replace all existing emails.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: setResourceWhitelistParamsSchema,
body: {
content: {
"application/json": {
schema: setResourceWhitelistBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/whitelist",
description: description:
"Set email whitelist for a resource. This will replace all existing emails.", "Set email whitelist for a resource. This will replace all existing emails.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResource],
+36
View File
@@ -240,6 +240,42 @@ const updateRawResourceBodySchema = z
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}", path: "/resource/{resourceId}",
description:
"Update a resource. Policy fields (sso, mfa, pincode, password, whitelist) update the inline policy when no shared resource policy is assigned; when a shared policy is assigned those fields override the shared policy for this resource only.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: updateResourceParamsSchema,
body: {
content: {
"application/json": {
schema: updateHttpResourceBodySchema.and(
updateRawResourceBodySchema
)
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}",
description: description:
"Update a resource. Policy fields (sso, mfa, pincode, password, whitelist) update the inline policy when no shared resource policy is assigned; when a shared policy is assigned those fields override the shared policy for this resource only.", "Update a resource. Policy fields (sso, mfa, pincode, password, whitelist) update the inline policy when no shared resource policy is assigned; when a shared policy is assigned those fields override the shared policy for this resource only.",
tags: [OpenAPITags.PublicResource], tags: [OpenAPITags.PublicResource],
+43 -54
View File
@@ -9,12 +9,11 @@ import createHttpError from "http-errors";
import logger from "@server/logger"; import logger from "@server/logger";
import { fromError } from "zod-validation-error"; import { fromError } from "zod-validation-error";
import { import {
isValidCIDR, RESOURCE_RULE_MATCH_TYPES,
isValidIP, getResourceRuleValueValidationError,
isValidUrlGlobPattern ResourceRuleMatchType
} from "@server/lib/validators"; } from "@server/lib/validators";
import { OpenAPITags, registry } from "@server/openApi"; import { OpenAPITags, registry } from "@server/openApi";
import { isValidRegionId } from "@server/db/regions";
// Define Zod schema for request parameters validation // Define Zod schema for request parameters validation
const updateResourceRuleParamsSchema = z.strictObject({ const updateResourceRuleParamsSchema = z.strictObject({
@@ -22,15 +21,7 @@ const updateResourceRuleParamsSchema = z.strictObject({
resourceId: z.coerce.number().int().positive() resourceId: z.coerce.number().int().positive()
}); });
const resourceRuleMatchSchema = z.enum([ const resourceRuleMatchSchema = z.enum(RESOURCE_RULE_MATCH_TYPES);
"CIDR",
"IP",
"PATH",
"COUNTRY",
"COUNTRY_IS_NOT",
"ASN",
"REGION"
]);
// Define Zod schema for request body validation // Define Zod schema for request body validation
const updateResourceRuleSchema = z const updateResourceRuleSchema = z
@@ -49,6 +40,39 @@ registry.registerPath({
method: "post", method: "post",
path: "/resource/{resourceId}/rule/{ruleId}", path: "/resource/{resourceId}/rule/{ruleId}",
description: "Update a resource rule.", description: "Update a resource rule.",
tags: [OpenAPITags.PublicResourceLegacy],
request: {
params: updateResourceRuleParamsSchema,
body: {
content: {
"application/json": {
schema: updateResourceRuleSchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/public-resource/{resourceId}/rule/{ruleId}",
description: "Update a resource rule.",
tags: [OpenAPITags.PublicResource, OpenAPITags.Rule], tags: [OpenAPITags.PublicResource, OpenAPITags.Rule],
request: { request: {
params: updateResourceRuleParamsSchema, params: updateResourceRuleParamsSchema,
@@ -140,14 +164,7 @@ export async function updateResourceRule(
resource.resourcePolicyId === null && resource.resourcePolicyId === null &&
resource.defaultResourcePolicyId !== null; resource.defaultResourcePolicyId !== null;
let existingMatch: let existingMatch: ResourceRuleMatchType;
| "CIDR"
| "IP"
| "PATH"
| "COUNTRY"
| "COUNTRY_IS_NOT"
| "ASN"
| "REGION";
if (isInlinePolicy) { if (isInlinePolicy) {
const policyId = resource.defaultResourcePolicyId!; const policyId = resource.defaultResourcePolicyId!;
@@ -231,43 +248,15 @@ export async function updateResourceRule(
const { value } = updateData; const { value } = updateData;
if (value !== undefined) { if (value !== undefined) {
if (match === "CIDR") { const valueValidationError = getResourceRuleValueValidationError(
if (!isValidCIDR(value)) { match,
return next( value
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid CIDR provided"
)
); );
} if (valueValidationError) {
} else if (match === "IP") {
if (!isValidIP(value)) {
return next( return next(
createHttpError( createHttpError(HttpCode.BAD_REQUEST, valueValidationError)
HttpCode.BAD_REQUEST,
"Invalid IP provided"
)
); );
} }
} else if (match === "PATH") {
if (!isValidUrlGlobPattern(value)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid URL glob pattern provided"
)
);
}
} else if (match === "REGION") {
if (!isValidRegionId(value)) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
"Invalid region ID provided"
)
);
}
}
} }
// Update the rule // Update the rule
@@ -0,0 +1,83 @@
import response from "@server/lib/response";
import {
getBatchedStatusHistory,
type BatchedStatusHistoryResponse
} from "@server/lib/statusHistory";
import logger from "@server/logger";
import HttpCode from "@server/types/HttpCode";
import { NextFunction, Request, Response } from "express";
import createHttpError from "http-errors";
import { z } from "zod";
import { fromError } from "zod-validation-error";
const siteIdParamsSchema = z.object({
days: z
.string()
.optional()
.transform((v) => (v ? parseInt(v, 10) : 90)),
// Minutes to add to UTC to get the requesting client's local time
// (e.g. Australia/Sydney standard time is 600). Optional and
// defaults to 0 (UTC) so older clients keep the prior behavior.
tzOffsetMinutes: z
.string()
.optional()
.transform((v) => (v ? parseInt(v, 10) : 0)),
siteIds: z
.preprocess((val) => {
if (val === undefined || val === null || val === "") {
return undefined;
}
const raw = Array.isArray(val) ? val : [val];
const nums = raw
.map((v) =>
typeof v === "string" ? parseInt(v, 10) : Number(v)
)
.filter((n) => Number.isInteger(n) && n > 0);
const unique = [...new Set(nums)];
return unique.length ? unique : undefined;
}, z.array(z.number().int().positive()))
.openapi({
description: "Filter by siteIds (repeat query param)"
})
});
export async function getBatchedSiteStatusHistory(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedQuery = siteIdParamsSchema.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error).toString()
)
);
}
const entityType = "site";
const { days, siteIds, tzOffsetMinutes } = parsedQuery.data;
const data = await getBatchedStatusHistory(
entityType,
siteIds,
days,
tzOffsetMinutes
);
return response<BatchedStatusHistoryResponse>(res, {
data,
success: true,
error: false,
message: "Status history retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
+1
View File
@@ -1,5 +1,6 @@
export * from "./getSite"; export * from "./getSite";
export * from "./getStatusHistory"; export * from "./getStatusHistory";
export * from "./getBatchedStatusHistory";
export * from "./createSite"; export * from "./createSite";
export * from "./deleteSite"; export * from "./deleteSite";
export * from "./updateSite"; export * from "./updateSite";
+1 -1
View File
@@ -177,7 +177,7 @@ registry.registerPath({
method: "get", method: "get",
path: "/org/{orgId}/sites", path: "/org/{orgId}/sites",
description: "List all sites in an organization", description: "List all sites in an organization",
tags: [OpenAPITags.Org, OpenAPITags.Site], tags: [OpenAPITags.Site],
request: { request: {
params: listSitesParamsSchema, params: listSitesParamsSchema,
query: listSitesSchema query: listSitesSchema
@@ -31,6 +31,40 @@ const addClientToSiteResourceParamsSchema = z
registry.registerPath({ registry.registerPath({
method: "post", method: "post",
path: "/site-resource/{siteResourceId}/clients/add", path: "/site-resource/{siteResourceId}/clients/add",
description:
"Add a single client to a site resource. Clients with a userId cannot be added.",
tags: [OpenAPITags.PrivateResourceLegacy],
request: {
params: addClientToSiteResourceParamsSchema,
body: {
content: {
"application/json": {
schema: addClientToSiteResourceBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/private-resource/{siteResourceId}/clients/add",
description: description:
"Add a single client to a site resource. Clients with a userId cannot be added.", "Add a single client to a site resource. Clients with a userId cannot be added.",
tags: [OpenAPITags.PrivateResource, OpenAPITags.Client], tags: [OpenAPITags.PrivateResource, OpenAPITags.Client],
@@ -33,6 +33,39 @@ registry.registerPath({
method: "post", method: "post",
path: "/site-resource/{siteResourceId}/roles/add", path: "/site-resource/{siteResourceId}/roles/add",
description: "Add a single role to a site resource.", description: "Add a single role to a site resource.",
tags: [OpenAPITags.PrivateResourceLegacy],
request: {
params: addRoleToSiteResourceParamsSchema,
body: {
content: {
"application/json": {
schema: addRoleToSiteResourceBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/private-resource/{siteResourceId}/roles/add",
description: "Add a single role to a site resource.",
tags: [OpenAPITags.PrivateResource, OpenAPITags.Role], tags: [OpenAPITags.PrivateResource, OpenAPITags.Role],
request: { request: {
params: addRoleToSiteResourceParamsSchema, params: addRoleToSiteResourceParamsSchema,
@@ -33,6 +33,39 @@ registry.registerPath({
method: "post", method: "post",
path: "/site-resource/{siteResourceId}/users/add", path: "/site-resource/{siteResourceId}/users/add",
description: "Add a single user to a site resource.", description: "Add a single user to a site resource.",
tags: [OpenAPITags.PrivateResourceLegacy],
request: {
params: addUserToSiteResourceParamsSchema,
body: {
content: {
"application/json": {
schema: addUserToSiteResourceBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/private-resource/{siteResourceId}/users/add",
description: "Add a single user to a site resource.",
tags: [OpenAPITags.PrivateResource, OpenAPITags.User], tags: [OpenAPITags.PrivateResource, OpenAPITags.User],
request: { request: {
params: addUserToSiteResourceParamsSchema, params: addUserToSiteResourceParamsSchema,
@@ -38,6 +38,39 @@ registry.registerPath({
method: "post", method: "post",
path: "/client/{clientId}/site-resources", path: "/client/{clientId}/site-resources",
description: "Add a machine client to multiple site resources at once.", description: "Add a machine client to multiple site resources at once.",
tags: [OpenAPITags.PrivateResourceLegacy],
request: {
params: batchAddClientToSiteResourcesParamsSchema,
body: {
content: {
"application/json": {
schema: batchAddClientToSiteResourcesBodySchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "post",
path: "/client/{clientId}/private-resources",
description: "Add a machine client to multiple site resources at once.",
tags: [OpenAPITags.Client], tags: [OpenAPITags.Client],
request: { request: {
params: batchAddClientToSiteResourcesParamsSchema, params: batchAddClientToSiteResourcesParamsSchema,
@@ -207,6 +207,39 @@ registry.registerPath({
method: "put", method: "put",
path: "/org/{orgId}/site-resource", path: "/org/{orgId}/site-resource",
description: "Create a new site resource.", description: "Create a new site resource.",
tags: [OpenAPITags.PrivateResourceLegacy],
request: {
params: createSiteResourceParamsSchema,
body: {
content: {
"application/json": {
schema: createSiteResourceSchema
}
}
}
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "put",
path: "/org/{orgId}/private-resource",
description: "Create a new site resource.",
tags: [OpenAPITags.PrivateResource], tags: [OpenAPITags.PrivateResource],
request: { request: {
params: createSiteResourceParamsSchema, params: createSiteResourceParamsSchema,
@@ -27,6 +27,32 @@ registry.registerPath({
method: "delete", method: "delete",
path: "/site-resource/{siteResourceId}", path: "/site-resource/{siteResourceId}",
description: "Delete a site resource.", description: "Delete a site resource.",
tags: [OpenAPITags.PrivateResourceLegacy],
request: {
params: deleteSiteResourceParamsSchema
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "delete",
path: "/private-resource/{siteResourceId}",
description: "Delete a site resource.",
tags: [OpenAPITags.PrivateResource], tags: [OpenAPITags.PrivateResource],
request: { request: {
params: deleteSiteResourceParamsSchema params: deleteSiteResourceParamsSchema
@@ -57,6 +57,36 @@ registry.registerPath({
method: "get", method: "get",
path: "/site-resource/{siteResourceId}", path: "/site-resource/{siteResourceId}",
description: "Get a specific site resource by siteResourceId.", description: "Get a specific site resource by siteResourceId.",
tags: [OpenAPITags.PrivateResourceLegacy],
request: {
params: z.object({
siteResourceId: z.number(),
siteId: z.number(),
orgId: z.string()
})
},
responses: {
200: {
description: "Successful response",
content: {
"application/json": {
schema: z.object({
data: z.record(z.string(), z.any()).nullable(),
success: z.boolean(),
error: z.boolean(),
message: z.string(),
status: z.number()
})
}
}
}
}
});
registry.registerPath({
method: "get",
path: "/private-resource/{siteResourceId}",
description: "Get a specific site resource by siteResourceId.",
tags: [OpenAPITags.PrivateResource], tags: [OpenAPITags.PrivateResource],
request: { request: {
params: z.object({ params: z.object({

Some files were not shown because too many files have changed in this diff Show More