visual improvements

This commit is contained in:
miloschwartz committed 2026-11-03 14:34:16 -05:00
1 parent 2c70c01f5e
commit 5a47f25b38
304 files changed
+17970 -3740

No files matched your search

+4 -4
View File
@@ -77,7 +77,7 @@ jobs:
fi
- name: Log in to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -149,7 +149,7 @@ jobs:
fi
- name: Log in to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -204,7 +204,7 @@ jobs:
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Log in to Docker Hub
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -407,7 +407,7 @@ jobs:
shell: bash
- name: Login to GitHub Container Registry (for cosign)
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0
with:
registry: ghcr.io
username: ${{ github.actor }}
+2 -2
View File
@@ -34,9 +34,9 @@ jobs:
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
# Match the version the app ships on (Dockerfile /
# Dockerfile.dev both use 24.18.1) rather than floating
# Dockerfile.dev both use 24.22.1) rather than floating
# to the latest 24.x.
node-version: '24.18.1'
node-version: '24.22.1'
- name: Install dependencies
run: npm ci
+138
View File
@@ -0,0 +1,138 @@
name: Mirror Node Base Image (Docker Hub to GHCR)
# Keeps a copy of the official node images our Dockerfiles build FROM in GHCR.
# Anonymous pulls from Docker Hub / ECR Public are rate limited per source IP,
# and GitHub-hosted runners share IPs, so CI builds were failing with 429s.
# Public GHCR packages pull without credentials, so PR builds (including from
# forks) need no secrets.
#
# The tags to mirror are read from the Dockerfiles themselves: any reference to
# ghcr.io/<owner>/node:<tag> is copied from docker.io/library/node:<tag>. Bumping
# the Node version in a Dockerfile and pushing it mirrors the new tag
# automatically. The daily run picks up upstream rebuilds (OS security patches)
# of the same tag.
#
# NOTE: the first push creates the ghcr.io/<owner>/node package as private.
# Make it public once in the org's package settings so builds can pull it
# anonymously.
permissions:
contents: read
packages: write
on:
schedule:
- cron: "17 4 * * *"
push:
paths:
- "Dockerfile*"
- ".github/workflows/mirror-node.yml"
workflow_dispatch:
inputs:
tags:
description: "Extra node tags to mirror, space separated (e.g. 24.23.0-slim)"
required: false
default: ""
concurrency:
group: mirror-node
cancel-in-progress: false
env:
SOURCE_IMAGE: docker.io/library/node
DEST_IMAGE: ghcr.io/${{ github.repository_owner }}/node
jobs:
mirror:
# Don't run (and push to their own GHCR) in forks
if: github.repository_owner == 'fosrl'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install skopeo
run: |
sudo apt-get update -y
sudo apt-get install -y skopeo
skopeo --version
- name: Log in to registries
env:
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
DOCKER_HUB_ACCESS_TOKEN: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }}
run: |
set -euo pipefail
# runner.temp isn't available in job-level env, so export it here
# for this and later steps.
export REGISTRY_AUTH_FILE="${RUNNER_TEMP}/containers/auth.json"
echo "REGISTRY_AUTH_FILE=${REGISTRY_AUTH_FILE}" >> "$GITHUB_ENV"
mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")"
echo "$GHCR_TOKEN" | skopeo login ghcr.io -u "${{ github.actor }}" --password-stdin
# Authenticated Docker Hub pulls avoid the shared-IP anonymous limit.
# This only runs on trusted refs (schedule/push/dispatch), never PRs.
if [ -n "$DOCKER_HUB_USERNAME" ] && [ -n "$DOCKER_HUB_ACCESS_TOKEN" ]; then
echo "$DOCKER_HUB_ACCESS_TOKEN" | skopeo login docker.io -u "$DOCKER_HUB_USERNAME" --password-stdin
fi
- name: Collect tags
env:
EXTRA_TAGS: ${{ inputs.tags }}
run: |
set -euo pipefail
{
grep -hoE "${DEST_IMAGE}:[A-Za-z0-9._-]+" Dockerfile* | sed "s|^${DEST_IMAGE}:||" || true
for t in ${EXTRA_TAGS:-}; do echo "$t"; done
} | sort -u > tags.txt
if [ ! -s tags.txt ]; then
echo "::error ::No ${DEST_IMAGE}:<tag> references found in Dockerfiles"
exit 1
fi
echo "Tags to mirror:"
cat tags.txt
- name: Mirror tags
run: |
set -euo pipefail
errs=0
while read -r tag; do
if ! [[ "$tag" =~ ^[A-Za-z0-9._-]+$ ]]; then
echo "::warning ::Skipping invalid tag '${tag}'"
errs=$((errs+1)); continue
fi
src="${SOURCE_IMAGE}:${tag}"
dst="${DEST_IMAGE}:${tag}"
if ! skopeo inspect --raw --retry-times 3 "docker://${src}" > src.json; then
echo "::warning title=Inspect failed::${src}"
errs=$((errs+1)); continue
fi
src_digest="$(skopeo manifest-digest src.json)"
dst_digest=""
if skopeo inspect --raw --retry-times 3 "docker://${dst}" > dst.json 2>/dev/null; then
dst_digest="$(skopeo manifest-digest dst.json)"
fi
if [ "$src_digest" = "$dst_digest" ]; then
echo "Up to date: ${dst} (${src_digest})"
continue
fi
echo "==> Copy ${src} (${src_digest}) -> ${dst}"
# --all keeps every architecture; --preserve-digests keeps the
# mirror byte-identical so it can be pinned by the upstream digest.
if ! skopeo copy --all --preserve-digests --retry-times 3 \
"docker://${src}" "docker://${dst}"; then
echo "::warning title=Copy failed::${src}"
errs=$((errs+1))
fi
done < tags.txt
if [ "$errs" -gt 0 ]; then
echo "::error ::${errs} tag(s) failed to mirror"
exit 1
fi
+5 -2
View File
@@ -20,12 +20,12 @@ jobs:
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
# Pin to the version the app actually ships on (Dockerfile /
# Dockerfile.dev both use 24.18.1; .nvmrc pins the 24 major).
# Dockerfile.dev both use 24.22.1; .nvmrc pins the 24 major).
# A bare '24' floats to the latest 24.x; Node >= 24.19.0 added
# node::ObjectWrap cleanup hooks that crash better-sqlite3 on teardown
# ("Assertion failed: (env) != nullptr" in RemoveEnvironmentCleanupHook),
# which intermittently kills `npm run dev` in the step below.
node-version: '24.18.1'
node-version: '24.22.1'
- name: Copy config file
run: cp config/config.example.yml config/config.yml
@@ -48,6 +48,9 @@ jobs:
- name: Test with tsc
run: npx tsc --noEmit
- name: Run unit tests
run: npm test
- name: Start app in background
run: |
nohup npm run dev > app.log 2>&1 &
+6 -4
View File
@@ -1,5 +1,8 @@
# FROM node:24.18.1-slim AS base
FROM public.ecr.aws/docker/library/node:24.18.1-slim AS base
# Mirror of docker.io/library/node, kept in sync by
# .github/workflows/mirror-node.yml (avoids Docker Hub / ECR pull rate limits)
ARG NODE_IMAGE=ghcr.io/fosrl/node:24.22.1-slim
FROM ${NODE_IMAGE} AS base
WORKDIR /app
@@ -35,8 +38,7 @@ RUN npm prune --omit=dev && \
# Real files should be present for saas builds, placeholders for oss builds
RUN touch /app/GeoLite2-Country.mmdb /app/GeoLite2-ASN.mmdb
# FROM node:24.18.1-slim AS runner
FROM public.ecr.aws/docker/library/node:24.18.1-slim AS runner
FROM ${NODE_IMAGE} AS runner
WORKDIR /app
+3 -1
View File
@@ -1,4 +1,6 @@
FROM node:24.18.1-alpine
# Mirror of docker.io/library/node, kept in sync by
# .github/workflows/mirror-node.yml
FROM ghcr.io/fosrl/node:24.22.1-alpine
WORKDIR /app
+90
View File
@@ -0,0 +1,90 @@
# Standalone image that runs only the authoritative DNS server
# (server/private/dnsServer.ts) against an existing Pangolin database.
#
# Build: docker build -f Dockerfile.dns -t pangolin-dns .
# Run: docker run -v ./config:/app/config -p 53:53/udp pangolin-dns
#
# The mounted config directory needs the same config.yml (database
# connection) and privateConfig.yml (dns section with
# enabled: true) as the Pangolin server whose database it reads.
# Mirror of docker.io/library/node, kept in sync by
# .github/workflows/mirror-node.yml (avoids Docker Hub / ECR pull rate limits)
ARG NODE_IMAGE=ghcr.io/fosrl/node:24.22.1-slim
FROM ${NODE_IMAGE} AS builder
WORKDIR /app
COPY package.json ./
# esbuild.mjs marks every npm package external, so bundling needs only the
# bundler's own tooling (at the versions package.json pins), not the full
# dependency tree. Installed outside /app so the root package.json's
# dependencies aren't pulled in with it.
RUN mkdir /tools && cd /tools && echo '{"private": true}' > package.json && \
npm install --no-audit --no-fund --no-package-lock $(node -p ' \
const p = require("/app/package.json"); \
const all = { ...p.dependencies, ...p.devDependencies }; \
["esbuild", "esbuild-node-externals", "yargs"] \
.map((n) => `${n}@${all[n]}`).join(" ")') && \
ln -s /tools/node_modules /app/node_modules
COPY . .
# The DNS server is private code, so only saas/enterprise builds include it
ARG BUILD=enterprise
ARG DATABASE=pg
# Bundle, then write a package.json listing only the packages the bundle
# imports at runtime
RUN npm run set:$DATABASE && \
npm run set:$BUILD && \
mkdir -p dist && \
node esbuild.mjs -e server/private/dnsServer.ts -o dist/dns.mjs -m dist/dns.meta.json && \
node scripts/bundle-package-json.mjs dist/dns.meta.json /out/package.json
FROM ${NODE_IMAGE} AS deps
WORKDIR /app
# Installing the trimmed package.json against the root lockfile keeps every
# transitive version pinned to what the main Pangolin image ships
COPY --from=builder /out/package.json ./package.json
COPY package-lock.json .npmrc ./
RUN npm install --omit=dev --no-audit --no-fund
FROM ${NODE_IMAGE} AS runner
WORKDIR /app
RUN apt-get update && apt-get install -y tzdata && rm -rf /var/lib/apt/lists/*
COPY --from=deps /app/node_modules ./node_modules
COPY --from=deps /app/package.json ./package.json
COPY --from=builder /app/dist/dns.mjs ./dist/dns.mjs
ENV ENVIRONMENT=prod
ENV NODE_ENV=production
EXPOSE 53/udp
# HTTP health check (GET /healthz)
EXPOSE 8053/tcp
ARG VERSION="dev"
ARG REVISION=""
ARG CREATED=""
LABEL org.opencontainers.image.source="https://github.com/fosrl/pangolin" \
org.opencontainers.image.url="https://github.com/fosrl/pangolin" \
org.opencontainers.image.documentation="https://docs.pangolin.net" \
org.opencontainers.image.vendor="Fossorial" \
org.opencontainers.image.licenses="Fossorial Commercial License" \
org.opencontainers.image.title="Pangolin DNS" \
org.opencontainers.image.description="Standalone Pangolin authoritative DNS server" \
org.opencontainers.image.version="${VERSION}" \
org.opencontainers.image.revision="${REVISION}" \
org.opencontainers.image.created="${CREATED}"
CMD ["node", "--disable-warning=ExperimentalWarning", "--enable-source-maps", "dist/dns.mjs"]
+3 -1
View File
@@ -58,7 +58,9 @@ Legacy SASE platforms got the idea right: connectivity and security belong toget
- Get started for free with [Pangolin Cloud](https://app.pangolin.net/).
- Or, check out the [quick install guide](https://docs.pangolin.net/self-host/quick-install) for how to self-host Pangolin.
- Install from the [DigitalOcean marketplace](https://marketplace.digitalocean.com/apps/pangolin-ce-1?refcode=edf0480eeb81) for a one-click pre-configured installer.
- Or, deploy in minutes from a prebuilt image on [AWS or DigitalOcean](https://docs.pangolin.net/self-host/one-click-deploy):
- **AWS:** Community and Enterprise AMIs for x86 and Graviton. Configure at launch with user-data or CloudFormation.
- **DigitalOcean:** install from the [DigitalOcean marketplace](https://marketplace.digitalocean.com/apps/pangolin-ce-1?refcode=edf0480eeb81) for a one-click pre-configured installer.
<img src="public/screenshots/hero.png" alt="Pangolin" width="100%" />
+1 -1
View File
@@ -11,7 +11,7 @@ experimental:
plugins:
badger:
moduleName: github.com/fosrl/badger
version: v1.8.0
version: v1.7.0
log:
level: INFO
format: common
+10
View File
@@ -45,6 +45,11 @@ const argv = yargs(hideBin(process.argv))
choices: ["oss", "saas", "enterprise"],
default: build
})
.option("metafile", {
alias: "m",
describe: "Write the esbuild metafile (inputs/imports) to this path",
type: "string"
})
.help()
.alias("help", "h").argv;
@@ -281,6 +286,7 @@ esbuild
})
],
sourcemap: "inline",
metafile: Boolean(argv.metafile),
target: "node24"
})
.then((result) => {
@@ -306,6 +312,10 @@ esbuild
process.exit(1);
}
if (argv.metafile) {
fs.writeFileSync(argv.metafile, JSON.stringify(result.metafile));
}
console.log("Build completed successfully");
})
.catch((error) => {
+5 -1
View File
@@ -1,5 +1,9 @@
listen_addr: 0.0.0.0:7422
appsec_config: crowdsecurity/appsec-default
appsec_configs:
- crowdsecurity/appsec-default
{{- if .EnableBotDetection}}
- crowdsecurity/appsec-bot-*
{{- end}}
name: myAppSecComponent
source: appsec
labels:
+1 -1
View File
@@ -4,7 +4,7 @@ services:
container_name: crowdsec
environment:
GID: "1000"
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules
COLLECTIONS: crowdsecurity/traefik crowdsecurity/appsec-virtual-patching crowdsecurity/appsec-generic-rules{{if .EnableBotDetection}} crowdsecurity/appsec-bot-challenge{{end}}
ENROLL_INSTANCE_NAME: "pangolin-crowdsec"
PARSERS: crowdsecurity/whitelists
ENROLL_TAGS: docker
+1 -1
View File
@@ -50,7 +50,7 @@ services:
- 80:80{{end}}
traefik:
image: docker.io/traefik:v3.7
image: docker.io/traefik:v3.7.16
container_name: traefik
restart: unless-stopped
{{if .InstallGerbil}}network_mode: service:gerbil # Ports appear on the gerbil service{{end}}{{if not .InstallGerbil}}
+120
View File
@@ -0,0 +1,120 @@
package main
import (
"flag"
"fmt"
"os"
)
// Command line flags that pre-answer installer prompts. Any flag that is set
// explicitly skips its prompt; with --non-interactive every remaining prompt
// takes its default value (or fails if it has none), so the installer can be
// driven from scripts such as cloud-init or an AMI first-boot service.
var (
crowdsecFlag *bool
redisFlag *bool
nonInteractiveFlag *bool
installDirFlag *string
containerTypeFlag *string
startContainersFlag *bool
enterpriseFlag *bool
postgresFlag *bool
postgresPasswordFlag *string
redisPasswordFlag *string
baseDomainFlag *string
dashboardDomainFlag *string
letsEncryptEmailFlag *string
gerbilFlag *bool
ipv6Flag *bool
maxmindFlag *bool
emailFlag *bool
smtpHostFlag *string
smtpPortFlag *int
smtpUserFlag *string
smtpPassFlag *string
smtpNoReplyFlag *string
)
// setFlags records which flags were passed explicitly on the command line.
var setFlags = map[string]bool{}
func parseFlags() {
crowdsecFlag = flag.Bool("crowdsec", false, "Enable the CrowdSec installation prompt")
redisFlag = flag.Bool("redis", false, "Install Redis as caching solution. Required for HA. Not required for the Enterprise version.")
nonInteractiveFlag = flag.Bool("non-interactive", false, "Never prompt; use flag values and defaults. Fails if a required value is missing.")
installDirFlag = flag.String("install-dir", "", "Installation directory (default /opt/pangolin)")
containerTypeFlag = flag.String("container-type", "docker", "Container runtime: docker or podman")
startContainersFlag = flag.Bool("start-containers", true, "Install and start the containers after generating the configuration")
enterpriseFlag = flag.Bool("enterprise", false, "Install the Enterprise Edition")
postgresFlag = flag.Bool("postgres", false, "Use PostgreSQL instead of SQLite")
postgresPasswordFlag = flag.String("postgres-password", "", "Password for the PostgreSQL pangolin user")
redisPasswordFlag = flag.String("redis-password", "", "Password for the Redis service (requires --redis)")
baseDomainFlag = flag.String("base-domain", "", "Base domain without subdomain (e.g. example.com)")
dashboardDomainFlag = flag.String("dashboard-domain", "", "Domain for the Pangolin dashboard (default pangolin.<base-domain>)")
letsEncryptEmailFlag = flag.String("letsencrypt-email", "", "Email for Let's Encrypt certificates")
gerbilFlag = flag.Bool("gerbil", true, "Install Gerbil to allow tunneled connections")
ipv6Flag = flag.Bool("ipv6", true, "Enable IPv6 on the Docker network")
maxmindFlag = flag.Bool("maxmind", true, "Download the MaxMind GeoLite2 Country and ASN databases")
emailFlag = flag.Bool("email", false, "Enable email functionality (SMTP)")
smtpHostFlag = flag.String("smtp-host", "", "SMTP host")
smtpPortFlag = flag.Int("smtp-port", 587, "SMTP port")
smtpUserFlag = flag.String("smtp-user", "", "SMTP username")
smtpPassFlag = flag.String("smtp-pass", "", "SMTP password")
smtpNoReplyFlag = flag.String("smtp-no-reply", "", "No-reply email address")
flag.Parse()
flag.Visit(func(f *flag.Flag) {
setFlags[f.Name] = true
})
}
func isNonInteractive() bool {
return nonInteractiveFlag != nil && *nonInteractiveFlag
}
// boolAnswer returns the flag value if it was set, otherwise prompts.
func boolAnswer(name string, value bool, prompt string, defaultValue bool) bool {
if setFlags[name] {
return value
}
return readBool(prompt, defaultValue)
}
// stringAnswer returns the flag value if it was set, otherwise prompts.
func stringAnswer(name string, value string, prompt string, defaultValue string) string {
if setFlags[name] && value != "" {
return value
}
if isNonInteractive() && defaultValue == "" {
fmt.Printf("Error: --%s is required in non-interactive mode\n", name)
os.Exit(1)
}
return readString(prompt, defaultValue)
}
// passwordAnswer returns the flag value if it was set, otherwise prompts.
func passwordAnswer(name string, value string, prompt string) string {
if setFlags[name] && value != "" {
return value
}
if isNonInteractive() {
fmt.Printf("Error: --%s is required in non-interactive mode\n", name)
os.Exit(1)
}
return readPassword(prompt)
}
// intAnswer returns the flag value if it was set, otherwise prompts.
func intAnswer(name string, value int, prompt string, defaultValue int) int {
if setFlags[name] {
return value
}
return readInt(prompt, defaultValue)
}
+35
View File
@@ -48,7 +48,22 @@ func runField(field huh.Field) error {
return form.Run()
}
// nonInteractiveDefault reports the value used for a prompt when running with
// --non-interactive, so the decision stays visible in the install log.
func nonInteractiveDefault(prompt string, value string) {
fmt.Printf("%s: %s (non-interactive)\n", prompt, value)
}
func readString(prompt string, defaultValue string) string {
if isNonInteractive() {
if defaultValue == "" {
fmt.Printf("Error: no value for %q in non-interactive mode\n", prompt)
os.Exit(1)
}
nonInteractiveDefault(prompt, defaultValue)
return defaultValue
}
var value string
title := prompt
@@ -86,6 +101,11 @@ func readString(prompt string, defaultValue string) string {
}
func readPassword(prompt string) string {
if isNonInteractive() {
fmt.Printf("Error: no value for %q in non-interactive mode\n", prompt)
os.Exit(1)
}
var value string
for {
@@ -114,6 +134,11 @@ func readPassword(prompt string) string {
}
func readBool(prompt string, defaultValue bool) bool {
if isNonInteractive() {
nonInteractiveDefault(prompt, strconv.FormatBool(defaultValue))
return defaultValue
}
var value = defaultValue
confirm := huh.NewConfirm().
@@ -138,6 +163,11 @@ func readBool(prompt string, defaultValue bool) bool {
}
func readBoolNoDefault(prompt string) bool {
if isNonInteractive() {
nonInteractiveDefault(prompt, "false")
return false
}
var value bool
confirm := huh.NewConfirm().
@@ -162,6 +192,11 @@ func readBoolNoDefault(prompt string) bool {
}
func readInt(prompt string, defaultValue int) int {
if isNonInteractive() {
nonInteractiveDefault(prompt, strconv.Itoa(defaultValue))
return defaultValue
}
var value string
title := fmt.Sprintf("%s (default: %d)", prompt, defaultValue)
+41 -24
View File
@@ -4,7 +4,6 @@ import (
"crypto/rand"
"embed"
"encoding/base64"
"flag"
"fmt"
"io"
"io/fs"
@@ -54,6 +53,7 @@ type Config struct {
InstallGerbil bool
TraefikBouncerKey string
DoCrowdsecInstall bool
EnableBotDetection bool
EnableMaxMind bool
Secret string
IsEnterprise bool
@@ -71,13 +71,9 @@ const (
Undefined SupportedContainer = "undefined"
)
var redisFlag *bool
func main() {
crowdsecFlag := flag.Bool("crowdsec", false, "Enable the CrowdSec installation prompt")
redisFlag = flag.Bool("redis", false, "Install Redis as caching solution. Required for HA. Not required for the Enterprise version.")
flag.Parse()
parseFlags()
// print a banner about prerequisites - opening port 80, 443, 51820, and 21820 on the VPS and firewall and pointing your domain to the VPS IP with a records. Docs are at http://localhost:3000/Getting%20Started/dns-networking
@@ -141,7 +137,7 @@ func main() {
fmt.Println("\n=== Starting installation ===")
if readBool("Would you like to install and start the containers?", true) {
if boolAnswer("start-containers", *startContainersFlag, "Would you like to install and start the containers?", true) {
config.InstallationContainerType = podmanOrDocker()
@@ -271,6 +267,8 @@ func main() {
fmt.Printf("Detected container type: %s\n", config.InstallationContainerType)
}
config.EnableBotDetection = readBool("Would you like to enable CrowdSec bot detection (alpha)? Browsers must solve a challenge before reaching any resource; non-browser clients such as mobile apps and API tools may be blocked.", false)
config.DoCrowdsecInstall = true
err := installCrowdsec(config, installDir)
if err != nil {
@@ -323,6 +321,21 @@ func findOrSelectInstallDirectory() string {
os.Exit(1)
}
// An explicit --install-dir skips detection and prompts
if setFlags["install-dir"] && *installDirFlag != "" {
installDir, err := filepath.Abs(*installDirFlag)
if err != nil {
fmt.Printf("Error resolving path: %v\n", err)
os.Exit(1)
}
if err := os.MkdirAll(installDir, 0755); err != nil {
fmt.Printf("Error creating directory: %v\n", err)
os.Exit(1)
}
fmt.Printf("Installation directory: %s\n", installDir)
return installDir
}
// 1. Check current directory for existing install
if hasExistingInstall(cwd) {
fmt.Printf("Found existing Pangolin installation in current directory: %s\n", cwd)
@@ -419,7 +432,7 @@ func changeDirectoryOwnership(dir string) {
}
func podmanOrDocker() SupportedContainer {
inputContainer := readString("Would you like to run Pangolin as Docker or Podman containers?", "docker")
inputContainer := stringAnswer("container-type", *containerTypeFlag, "Would you like to run Pangolin as Docker or Podman containers?", "docker")
chosenContainer := Docker
if strings.EqualFold(inputContainer, "docker") {
@@ -492,40 +505,44 @@ func collectUserInput() Config {
// Basic configuration
fmt.Println("\n=== Basic Configuration ===")
config.IsEnterprise = readBoolNoDefault("Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.")
if setFlags["enterprise"] {
config.IsEnterprise = *enterpriseFlag
} else {
config.IsEnterprise = readBoolNoDefault("Do you want to install the Enterprise version of Pangolin? The EE is free for personal use or for businesses making less than 100k USD annually.")
}
if config.IsEnterprise {
if *redisFlag {
config.IsRedis = true
config.IsRedisPass = readPassword("Enter a unique password for the Redis service.")
config.IsRedisPass = passwordAnswer("redis-password", *redisPasswordFlag, "Enter a unique password for the Redis service.")
}
}
config.IsPostgreSQL = readBool("Do you want to use PostgreSQL (not recommended for most users)?", false)
config.IsPostgreSQL = boolAnswer("postgres", *postgresFlag, "Do you want to use PostgreSQL (not recommended for most users)?", false)
if config.IsPostgreSQL {
config.IsPostgreSQLPass = readPassword("Enter a unique password for the PostgreSQL pangolin user.")
config.IsPostgreSQLPass = passwordAnswer("postgres-password", *postgresPasswordFlag, "Enter a unique password for the PostgreSQL pangolin user.")
}
config.BaseDomain = readString("Enter your base domain (no subdomain e.g. example.com)", "")
config.BaseDomain = stringAnswer("base-domain", *baseDomainFlag, "Enter your base domain (no subdomain e.g. example.com)", "")
// Set default dashboard domain after base domain is collected
defaultDashboardDomain := ""
if config.BaseDomain != "" {
defaultDashboardDomain = "pangolin." + config.BaseDomain
}
config.DashboardDomain = readString("Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
config.LetsEncryptEmail = readString("Enter email for Let's Encrypt certificates", "")
config.InstallGerbil = readBool("Do you want to use Gerbil to allow tunneled connections", true)
config.DashboardDomain = stringAnswer("dashboard-domain", *dashboardDomainFlag, "Enter the domain for the Pangolin dashboard", defaultDashboardDomain)
config.LetsEncryptEmail = stringAnswer("letsencrypt-email", *letsEncryptEmailFlag, "Enter email for Let's Encrypt certificates", "")
config.InstallGerbil = boolAnswer("gerbil", *gerbilFlag, "Do you want to use Gerbil to allow tunneled connections", true)
// Email configuration
fmt.Println("\n=== Email Configuration ===")
config.EnableEmail = readBool("Enable email functionality (SMTP)", false)
config.EnableEmail = boolAnswer("email", *emailFlag, "Enable email functionality (SMTP)", false)
if config.EnableEmail {
config.EmailSMTPHost = readString("Enter SMTP host", "")
config.EmailSMTPPort = readInt("Enter SMTP port (default 587)", 587)
config.EmailSMTPUser = readString("Enter SMTP username", "")
config.EmailSMTPPass = readPassword("Enter SMTP password")
config.EmailNoReply = readString("Enter no-reply email address (often the same as SMTP username)", "")
config.EmailSMTPHost = stringAnswer("smtp-host", *smtpHostFlag, "Enter SMTP host", "")
config.EmailSMTPPort = intAnswer("smtp-port", *smtpPortFlag, "Enter SMTP port (default 587)", 587)
config.EmailSMTPUser = stringAnswer("smtp-user", *smtpUserFlag, "Enter SMTP username", "")
config.EmailSMTPPass = passwordAnswer("smtp-pass", *smtpPassFlag, "Enter SMTP password")
config.EmailNoReply = stringAnswer("smtp-no-reply", *smtpNoReplyFlag, "Enter no-reply email address (often the same as SMTP username)", "")
}
// Validate required fields
@@ -546,8 +563,8 @@ func collectUserInput() Config {
fmt.Println("\n=== Advanced Configuration ===")
config.EnableIPv6 = readBool("Is your server IPv6 capable?", true)
config.EnableMaxMind = readBool("Do you want to download the MaxMind GeoLite2 Country and ASN databases for blocking functionality?", true)
config.EnableIPv6 = boolAnswer("ipv6", *ipv6Flag, "Is your server IPv6 capable?", true)
config.EnableMaxMind = boolAnswer("maxmind", *maxmindFlag, "Do you want to download the MaxMind GeoLite2 Country and ASN databases for blocking functionality?", true)
if config.DashboardDomain == "" {
fmt.Println("Error: Dashboard Domain name is required")
+128 -4
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "Изпратете целия интернет трафик да излезе през мрежата на сайта.",
"resourceTypeGatewayDescription": "Изпратете целия интернет трафик да излезе през мрежата на сайта.",
"resourceDomainDescription": "Ресурсът ще се обслужва на това напълно квалифицирано име на домейн.",
"resourceMatchBaseDomain": "Също съвпадение {host}",
"resourceMatchBaseDomainDescription": "Универсален символ съвпада само с имена на хостове под {host}. Разрешете това, за да маршрутизира {host} също чрез този ресурс.",
"resourceHTTPSSettings": "HTTPS настройки",
"resourceHTTPSSettingsDescription": "Конфигурирайте как ресурсът ще бъде достъпен по HTTPS",
"resourcePortDescription": "Външен порт на инстанцията или възела на Панголиин, където ресурсът ще бъде достъпен.",
@@ -425,6 +427,31 @@
"accessRolesAdd": "Добавете роля",
"accessRoleDelete": "Изтриване на роля",
"accessApprovalsManage": "Управление на одобрения",
"resourceSessionsManage": "Управление на ресурсни сесии",
"resourceSessionsDescription": "Вижте кой в момента има активна сесия на вашите публични ресурси и отменете достъпа",
"resourceSessionsSearch": "Търсене по потребител, имейл, връзка или ресурс...",
"resourceSessionsMethod": "Метод",
"resourceSessionsIssued": "Издаден",
"resourceSessionsAnonymous": "Анонимен",
"resourceSessionsFilterAnyUser": "Всички потребители",
"resourceSessionsFilteredToSession": "Показва една сесия",
"resourceSessionsMethodSso": "SSO",
"resourceSessionsMethodPassword": "Парола",
"resourceSessionsMethodPincode": "ПИН код",
"resourceSessionsMethodWhitelist": "Имейл",
"resourceSessionsMethodAccessToken": "Споделима връзка",
"resourceSessionsRevoke": "Отмяна",
"resourceSessionsRevokeTitle": "Отмяна на сесия",
"resourceSessionsRevokeQuestion": "Отменете сесията за {identity} на {resource}? Те ще трябва да се автентифицират отново, за да имат достъп до ресурса.",
"resourceSessionsRevokeSsoNote": "Този потребител е влязъл с SSO. Всички техни сесии за този ресурс от същия вход ще бъдат прекратени, и те ще трябва да влязат отново, за да имат достъп до него, дори ако все още са влезли в Pangolin.",
"resourceSessionsRevoked": "Сесията е отменена",
"resourceSessionsRevokedDescription": "Сесията беше отменена. Може да отнеме няколко секунди да влезе в сила.",
"resourceSessionsErrorRevoke": "Неуспешно отменяне на сесията",
"resourceSessionsErrorRevokeDescription": "Възникна грешка при отмяната на сесията",
"resourceSessionReauthRequired": "Вашата сесия за този ресурс беше прекратена от администратор. Моля, влезте отново, за да продължите.",
"logLinks": "Линкове",
"logLinkUser": "Потребител",
"logLinkResourceSession": "Ресурсна сесия",
"accessApprovalsDescription": "Прегледайте и управлявайте чакащи одобрения за достъп до тази организация",
"description": "Описание",
"inviteTitle": "Отворени покани",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "Няма лимит за броя на потребителите за този хост.",
"licenseUsersUsedMax": "{usedUsers} от {maxUsers} използвани потребители",
"licenseUsersUsed": "{count, plural, =0 {# потребители} one {# потребител} other {# потребители}}",
"licenseUsageMachines": "Машинни клиенти",
"licenseNoMachineLimit": "Няма ограничение за броя на машинните клиенти за този хост.",
"licenseMachinesUsedMax": "{usedMachines} от {maxMachines} машинни клиенти използвани",
"licenseMachinesUsed": "{count, plural, =0 {# машинни клиенти} one {# машинен клиент} other {# машинни клиенти}}",
"licenseUnlimited": "Неограничено",
"licenseTierLabel": "Ниво",
"licensePurchaseDescription": "Изберете колко сайтове искате да {selectedMode, select, license {закупите лиценз за. Можете винаги да добавите повече сайтове по-късно.} other {добавите към съществуващия си лиценз.}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "Потребителят беше актуализиран.",
"autoProvisioned": "Автоматично предоставено",
"autoProvisionSettings": "Настройки за автоматично осигуряване",
"autoProvisionedDescription": "Позволете този потребител да бъде автоматично управляван от доставчик на идентификационни данни",
"autoProvisionedDescription": "Ако доставчикът на идентичност е активирал автоматичното осигуряване, позволявайте на този потребител да бъде автоматично управляван.",
"accessControlsDescription": "Управлявайте какво може да достъпва и прави този потребител в организацията",
"accessControlsSubmit": "Запазване на контролите за достъп",
"singleRolePerUserPlanNotice": "Вашият план поддържа само една роля на потребител.",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "Разпределете заявките равномерно по цели в ротация.",
"loadBalancingStrategy_p2c": "Избор на две случайни цели",
"loadBalancingStrategy_p2c_description": "Изберете две случайни цели и изпратете заявката към тази с по-малко активни връзки.",
"loadBalancingStrategy_hrw": "Клиентски IP хеш",
"loadBalancingStrategy_hrw": "Най-високата случайна тежест",
"loadBalancingStrategy_hrw_description": "Хаширане на клиентски IP, така че същият клиент последователно да се маршрутира към същата цел.",
"loadBalancingStrategy_leasttime": "Най-малко време за отговор",
"loadBalancingStrategy_leasttime_description": "Изпратете заявки към целта с най-бързо време за отговор и най-малко активни връзки.",
@@ -829,6 +860,8 @@
"save": "Запази",
"proxyAdditional": "Допълнителни настройки на прокси",
"proxyAdditionalDescription": "Конфигурирайте как ресурсът обработва настройките на прокси",
"proxyForwardIdentityHeaders": "Пренасочване на идентификационни заглавки",
"proxyForwardIdentityHeadersDescription": "Изпращане на автентифицирания идентификатор на потребителя към бекенда в заглавките Remote-User, Remote-Email, Remote-Name, Remote-Role и Remote-User-Id. Деактивирайте за бекенди, които отхвърлят заявки с неразпознати заглавки.",
"proxyCustomHeader": "Персонализиран хост заглавие",
"proxyCustomHeaderDescription": "Хост заглавието, което да зададете при прокси заявките. Оставете празно, за да използвате подразбиране.",
"proxyAdditionalSubmit": "Запазване на прокси настройките",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "Базово удостоверяване чрез заглавие",
"policyAuthHeaderAuthDescription": "Валидирайте собствено HTTP заглавие и стойност при всяка заявка",
"policyAuthHeaderAuthSummary": "Конфигурирано заглавие",
"policyAuthMtlsTitle": "Клиентски сертификат (mTLS)",
"policyAuthMtlsDescription": "Изискване на клиентски сертификат, подписан от доверен CA по време на TLS ръкостискане",
"policyAuthMtlsSummary": "{count} доверени CA сертификати",
"policyAuthHeaderName": "Потребителско име",
"policyAuthHeaderValue": "Парола",
"policyAuthSetPasscode": "Задайте парола",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "Автоматично потребителско създаване",
"idpAutoProvisionUsersDescription": "Когато е активирано, потребителите ще бъдат автоматично създадени в системата при първо влизане с възможност за свързване на потребителите с роли и организации.",
"idpAutoProvisionConfigureAfterCreate": "Можете да конфигурирате настройките за автоматично предоставяне, след като дистрибуторът на самоличност бъде създаден.",
"idpSessionRevalidation": "Непрекъсната повторна проверка на сесията",
"idpSessionRevalidationDescription": "Когато е активирано, периодично се извършва повторна проверка на сесиите с този идентификатор. Ако доставчикът отнеме достъп, свързаните сесии се прекратяват.",
"idpSessionRevalidationConfigureAfterCreate": "Можете да конфигурирате непрекъсната повторна проверка на сесията, след като идентификаторът бъде създаден.",
"licenseBadge": "ЕЕ",
"idpType": "Тип доставчик",
"idpTypeDescription": "Изберете типа доставчик на идентичност, който искате да конфигурирате",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "Изтрий домейн",
"actionGetDNSRecords": "Вземи DNS записи",
"actionRestartOrgDomain": "Рестартирай домейн",
"actionListCustomDnsRecords": "Списък с персонализирани DNS записи",
"actionCreateCustomDnsRecord": "Създаване на персонализиран DNS запис",
"actionUpdateCustomDnsRecord": "Актуализиране на персонализиран DNS запис",
"actionDeleteCustomDnsRecord": "Изтриване на персонализиран DNS запис",
"actionCreateSite": "Създаване на сайт",
"actionDeleteSite": "Изтриване на сайта",
"actionGetSite": "Вземете сайт",
"actionListSites": "Изброяване на сайтове",
"actionReGenerateSecret": "Генериране на нови удостоверителни данни",
"actionCreateAiProvider": "Създаване на AI Доставчик",
"actionDeleteAiProvider": "Изтриване на AI Доставчик",
"actionGetAiProvider": "Получаване на AI Доставчик",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "Генериране на токен за достъп",
"actionDeleteAccessToken": "Изтриване на токен за достъп",
"actionListAccessTokens": "Изброяване на токени за достъп",
"actionListResourceSessions": "Изброяване на ресурсни сесии",
"actionDeleteResourceSession": "Отмяна на ресурсна сесия",
"actionCreateResourceSessionToken": "Създаване на токен за сесия на ресурс",
"actionCreateResourceRule": "Създаване на правило за ресурс",
"actionDeleteResourceRule": "Изтрийте правило за ресурс",
"actionListResourceRules": "Изброяване на правила за ресурс",
"actionUpdateResourceRule": "Актуализиране на правило за ресурс",
"actionCreateResourceMtlsCertificate": "Създаване на ресурсен mTLS сертификат",
"actionDeleteResourceMtlsCertificate": "Изтриване на ресурсен mTLS сертификат",
"actionListResourceMtlsCertificates": "Изброяване на ресурсни mTLS сертификати",
"actionListOrgLabels": "Изброяване на етикети",
"actionCreateOrgLabel": "Създаване на етикет",
"actionUpdateOrgLabel": "Актуализиране на етикет",
"actionDeleteOrgLabel": "Изтриване на етикет",
"actionAttachLabelToItem": "Прикачване на етикет",
"actionDetachLabelFromItem": "Отцепване на етикет",
"actionCreateEventStreamingDestination": "Създаване на дестинация за събитийно предаване",
"actionUpdateEventStreamingDestination": "Актуализиране на дестинация за събитийно предаване",
"actionDeleteEventStreamingDestination": "Изтриване на дестинация за събитийно предаване",
"actionListEventStreamingDestinations": "Изброяване на дестинации за събитийно предаване",
"actionCreateAlertRule": "Създаване на правило за предупреждение",
"actionUpdateAlertRule": "Актуализиране на правило за предупреждение",
"actionDeleteAlertRule": "Изтриване на правило за предупреждение",
"actionGetAlertRule": "Вземане на правило за предупреждение",
"actionListAlertRules": "Изброяване на правила за предупреждение",
"actionCreateHealthCheck": "Създаване на проверка на състоянието",
"actionUpdateHealthCheck": "Актуализиране на проверка на състоянието",
"actionDeleteHealthCheck": "Изтриване на проверка на състоянието",
"actionListHealthChecks": "Изброяване на проверки на състоянието",
"actionListOrgs": "Изброяване на организации",
"actionCheckOrgId": "Проверка на ID на организацията",
"actionCreateOrg": "Създаване на организация",
@@ -1726,6 +1794,7 @@
"sidebarHome": "Начало",
"sidebarSites": "Сайтове",
"sidebarApprovals": "Заявки за одобрение",
"sidebarResourceSessions": "Ресурсни сесии",
"sidebarResources": "Ресурси",
"sidebarProxyResources": "Публично",
"sidebarClientResources": "Частно",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "Политики за обществени ресурси",
"commandIdentityProviders": "Доставчици на идентичност",
"commandApprovals": "Заявки за потвърждение",
"commandResourceSessions": "Ресурсни сесии",
"commandShareableLinks": "Споделени връзки",
"commandOrganization": "Организация",
"commandLogsAndAnalytics": "Логове и Анализи",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "Логове за достъп",
"commandLogsAction": "Логове на администратори",
"commandLogsConnection": "Логове на връзките",
"commandLogsSsh": "SSH сесийни дневници",
"commandLogsStreaming": "Течения на събития",
"commandManagement": "Управление",
"commandAlerting": "Предупреждение",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "Колко дълго да се съхраняват дневниците на връзките",
"logRetentionAISessionsLabel": "Съхранение на Журнали на AI Портал Сесиите",
"logRetentionAISessionsDescription": "Колко време да съхраняваме подканящите/отговоряващите журнали на AI портал сесии",
"logRetentionSshLabel": "Съхранение на SSH сесийни дневници",
"logRetentionSshDescription": "Колко дълго да се съхраняват SSH сесиите и дневниците на команди",
"logRetentionDisabled": "Деактивирано",
"logRetention3Days": "3 дни",
"logRetention7Days": "7 дни",
@@ -3707,6 +3780,27 @@
"connectionLogs": "Логове на връзката",
"connectionLogsDescription": "Вижте логовете на връзките за тунелите в тази организация",
"sidebarLogsConnection": "Логове на връзката",
"sidebarLogsSsh": "SSH сесийни дневници",
"sshSessionLogs": "SSH сесийни дневници",
"sshSessionLogsDescription": "Преглед на SSH сесии и командите, стартирани в тях на сайтовете в тази организация",
"sshSystemUser": "Системен потребител",
"sshCommands": "Команди",
"sshSessionId": "ID на сесията",
"sshAuthMethod": "Метод за идентификация",
"sshTransport": "Връзка",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "Браузър",
"sshKeyFingerprint": "Отпечатък на ключа",
"sshCertKeyId": "ID на сертификата",
"sshStartedAt": "Започнато в",
"sshEndedAt": "Завършено в",
"sshSessionLoadingCommands": "Зареждане на команди...",
"sshSessionCommandsError": "Неуспешно зареждане на команди за тази сесия",
"sshSessionNoCommands": "Няма записани команди за тази сесия",
"sshSessionRecalledCommand": "(възстановено от историята на черупката)",
"sshSessionInexactNotice": "Някои команди бяха редактирани чрез извикване на история или попълване на табулацията и може да не съответстват точно на това, което е изпълнено от черупката.",
"sshSessionInexactTooltip": "Редактирано с извикване на история или попълване на табулацията; може да не съответства точно на това, което е изпълнено от черупката",
"sshSessionCommandsTruncated": "Показване на първите {shown} от {total} команди. Експортирайте журналите, за да видите всички тях.",
"sidebarLogsStreaming": "Потоци",
"sourceAddress": "Източен адрес",
"destinationAddress": "Адрес на дестинация",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "Как да добавите записи",
"dnsRecord": "DNS записи",
"domainTabSetup": "Настройка",
"domainTabRecords": "Записи",
"domainSetupRecordsDescription": "Създайте тези записи при вашия регистратор на домейни или текущия DNS доставчик. Pangolin ги използва, за да провери домейна и да насочи трафика му.",
"domainRecordsInfoTitle": "Записи, обслужвани от Pangolin",
"domainRecordsInfoDescription": "Именните сървъри на Pangolin отговарят на DNS заявките за {domain}. Записите, които добавяте тук, се обслужват директно от Pangolin, така че не е нужно да ги създавате при вашия регистратор.",
"customDnsRecords": "Персонализирани DNS записи",
"customDnsRecordsAdd": "Добавяне на запис",
"customDnsRecordsSearch": "Търсене на записи...",
"customDnsRecordsUnverifiedTitle": "Записите все още не се обслужват",
"customDnsRecordsUnverifiedDescription": "Този домейн не е потвърден. Можете да добавяте записи сега, но сървърите на имената на Pangolin няма да ги обслужват, докато сървърите за имена на домейна не сочат към Pangolin и потвърждението не завърши.",
"customDnsRecordCreate": "Добавяне на DNS запис",
"customDnsRecordEdit": "Редактиране на DNS запис",
"customDnsRecordFormDescription": "Този запис ще се обслужва за {domain}. Промените могат да отнемат няколко минути, за да се разпространят.",
"customDnsRecordNameDescription": "Използвайте @ за корена на домейна или * като първи етикет за масов запис.",
"customDnsRecordValueDescription": "Въведете една стойност на ред, напр. и двата сървъра на имена на NS запис.",
"customDnsRecordPriority": "Приоритет",
"customDnsRecordWeight": "Тегло",
"customDnsRecordPort": "Порт",
"customDnsRecordCreated": "DNS запис създаден",
"customDnsRecordUpdated": "DNS запис актуализиран",
"customDnsRecordSaveError": "Неуспешно запазване на DNS запис",
"customDnsRecordDeleted": "DNS запис изтрит",
"customDnsRecordDeleteError": "Неуспешно изтриване на DNS запис",
"customDnsRecordDeleteTitle": "Изтриване на DNS запис",
"customDnsRecordDeleteQuestion": "Сигурни ли сте, че искате да изтриете записа от тип {type} за {name}? Той ще спре да разрешава след изтичане на кешираните отговори.",
"customDnsRecordDeleteConfirm": "Изтриване на запис",
"required": "Задължително",
"domainSettingsUpdated": "Настройките на домейна са успешно актуализирани",
"orgOrDomainIdMissing": "Липсва идентификатор на организация или домейн",
@@ -3938,7 +4058,7 @@
"backToHome": "Връщане към началната страница.",
"needToSignInToOrg": "Трябва ли да използвате доставчика на идентичност на организацията си?",
"mtls": "mTLS",
"mtlsCaCertificates": "Доверени CA сертификати",
"mtlsCaCertificates": "Задаване на клиентски сертификати",
"mtlsCaCertificatesDescription": "Клиентите трябва да представят сертификат, подписан от един от тези органи за издаване на сертификати, за да се свържат. Качете само публични CA сертификати, никога частен ключ.",
"mtlsCertificateName": "Име (по избор)",
"mtlsCertificateNamePlaceholder": "Корпоративно устройство CA",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "Сертификатът не може да бъде премахнат.",
"mtlsLastCertificateRemoved": "mTLS беше изключен, защото последният CA сертификат беше премахнат.",
"mtlsNoCertificates": "Все още няма добавени CA сертификати",
"mtlsPendingCertificate": "CA сертификат {number}",
"mtlsCertificateInvalidPem": "Не е намерен PEM-кодиран сертификат. Очаква се блок, започващ с -----BEGIN CERTIFICATE-----.",
"mtlsCertificatePrivateKey": "Това изглежда като частен ключ, а не сертификат. Качете само обществен сертификат на CA.",
"mtlsCertificateDuplicate": "Този CA сертификат вече е добавен.",
"mtlsRemoveCertificate": "Премахване на CA сертификат",
"mtlsExpired": "Изтекло",
"mtlsExpiresSoon": "Скоро ще изтече",
@@ -4488,7 +4612,7 @@
"rdpFull": "Пълен",
"rdpReal": "Реален",
"rdpMeta": "Мета",
"rdpUploadFiles": "Качване на файловете",
"rdpUploadFiles": "Качване на файлове",
"rdpFilesReadyToPaste": "Файлове готови за поставяне",
"rdpFilesReadyToPasteDescription": "{count} файл(ове) копирани в отдалечения клипборд — натиснете Ctrl+V на отдалечения работен плот за поставяне.",
"rdpUploadFailed": "Качването неуспешно",
+128 -4
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "Odesílejte veškerý internetový provoz k výstupu přes síť lokality.",
"resourceTypeGatewayDescription": "Odesílejte veškerý internetový provoz k výstupu přes síť lokality.",
"resourceDomainDescription": "Zdroji bude obsluhován tento plně kvalifikovaný doménový název.",
"resourceMatchBaseDomain": "Také porovnejte {host}",
"resourceMatchBaseDomainDescription": "Zástupný znak odpovídá pouze názvům hostitelů pod {host}. Povolit směrování {host} přes tento prostředek také.",
"resourceHTTPSSettings": "Nastavení HTTPS",
"resourceHTTPSSettingsDescription": "Nakonfigurujte, jak bude dokument přístupný přes HTTPS",
"resourcePortDescription": "Externí port na instanci nebo uzlu Pangolin, kde bude zdroj dostupný.",
@@ -425,6 +427,31 @@
"accessRolesAdd": "Přidat roli",
"accessRoleDelete": "Odstranit roli",
"accessApprovalsManage": "Spravovat schválení",
"resourceSessionsManage": "Spravovat relace zdrojů",
"resourceSessionsDescription": "Zobrazit, kdo má aktuálně aktivní relaci na vašich veřejných zdrojích a zbavit ho přístupu",
"resourceSessionsSearch": "Hledat podle uživatele, emailu, odkazu nebo zdroje...",
"resourceSessionsMethod": "Způsob",
"resourceSessionsIssued": "Vydáno",
"resourceSessionsAnonymous": "Anonymní",
"resourceSessionsFilterAnyUser": "Všichni uživatelé",
"resourceSessionsFilteredToSession": "Zobrazuje jednu relaci",
"resourceSessionsMethodSso": "SSO",
"resourceSessionsMethodPassword": "Heslo",
"resourceSessionsMethodPincode": "PIN kód",
"resourceSessionsMethodWhitelist": "E-mail",
"resourceSessionsMethodAccessToken": "Sdílný odkaz",
"resourceSessionsRevoke": "Odebrat",
"resourceSessionsRevokeTitle": "Zrušit relaci",
"resourceSessionsRevokeQuestion": "Odebrat relaci pro {identity} na {resource}? Budou se muset znovu ověřit, aby měli přístup ke zdroji.",
"resourceSessionsRevokeSsoNote": "Tento uživatel je přihlášen přes SSO. Všechny jejich relace pro tento zdroj ze stejného přihlášení budou odebrány a budou se muset znovu přihlásit, i když jsou stále přihlášeni ke službě Pangolin.",
"resourceSessionsRevoked": "Relace byla odebrána",
"resourceSessionsRevokedDescription": "Relace byla odebrána. Mohlo by to trvat několik sekund, než se projeví.",
"resourceSessionsErrorRevoke": "Nepodařilo se zrušit relaci",
"resourceSessionsErrorRevokeDescription": "Při zrušení relace došlo k chybě",
"resourceSessionReauthRequired": "Vaši relaci pro tento zdroj ukončil administrátor. Prosím znovu se přihlaste, abyste mohli pokračovat.",
"logLinks": "Odkazy",
"logLinkUser": "Uživatel",
"logLinkResourceSession": "Podrobnosti o zdroji",
"accessApprovalsDescription": "Zobrazit a spravovat čekající oprávnění pro přístup k této organizaci",
"description": "L 343, 22.12.2009, s. 1).",
"inviteTitle": "Otevřít pozvánky",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "Neexistuje žádný limit na počet uživatelů pro tohoto hostitele.",
"licenseUsersUsedMax": "{usedUsers} z {maxUsers} použitých uživatelů",
"licenseUsersUsed": "{count, plural, =0 {# uživatelů} one {# uživatel} few {# uživatelé} many {# uživatelů} other {# uživatelů}}",
"licenseUsageMachines": "Strojoví klienti",
"licenseNoMachineLimit": "Neexistuje žádný limit na počet strojových klientů pro tohoto hostitele.",
"licenseMachinesUsedMax": "{usedMachines} z {maxMachines} strojových klientů použito",
"licenseMachinesUsed": "{count, plural, =0 {# strojových klientů} one {# strojový klient} few {# strojové klienty} many {# strojových klientů} other {# strojových klientů}}",
"licenseUnlimited": "Bez omezení",
"licenseTierLabel": "Úroveň",
"licensePurchaseDescription": "Vyberte kolik stránek chcete {selectedMode, select, license {Zakupte si licenci. Vždy můžete přidat více webů později.} other {Přidejte k vaší existující licenci.}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "Uživatel byl aktualizován.",
"autoProvisioned": "Automaticky poskytnuto",
"autoProvisionSettings": "Automatická nastavení",
"autoProvisionedDescription": "Povolit tomuto uživateli automaticky spravovat poskytovatel identity",
"autoProvisionedDescription": "Pokud má poskytovatel identity povolenou automatizovanou správu, povolit tomuto uživateli být správa automaticky.",
"accessControlsDescription": "Spravovat co může tento uživatel přistupovat a dělat v organizaci",
"accessControlsSubmit": "Uložit kontroly přístupu",
"singleRolePerUserPlanNotice": "Váš plán podporuje pouze jednu roli na uživatele.",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "Rozdělte požadavky rovnoměrně mezi cíle.",
"loadBalancingStrategy_p2c": "Síla dvou volby",
"loadBalancingStrategy_p2c_description": "Vyberte náhodně dva cíle a odeslat požadavek tomu s menším počtem aktivních spojení.",
"loadBalancingStrategy_hrw": "Hash klientské IP",
"loadBalancingStrategy_hrw": "Nejvyšší náhodná váha",
"loadBalancingStrategy_hrw_description": "Hash klientské IP tedy stejný klient je konzistentně směrován na stejný cíl.",
"loadBalancingStrategy_leasttime": "Nejmenší doba odezvy",
"loadBalancingStrategy_leasttime_description": "Odeslání požadavků na cíl s nejrychlejší dobou odezvy a nejméně aktivními spojeními.",
@@ -829,6 +860,8 @@
"save": "Uložit",
"proxyAdditional": "Další nastavení proxy",
"proxyAdditionalDescription": "Konfigurovat nastavení proxy",
"proxyForwardIdentityHeaders": "Předávat identifikační hlavičky",
"proxyForwardIdentityHeadersDescription": "Odeslat identitu autentizovaného uživatele na backend v hlavičkách Remote-User, Remote-Email, Remote-Name, Remote-Role a Remote-User-Id. Zakázat pro backendy, které odmítají požadavky s neznámými hlavičkami.",
"proxyCustomHeader": "Vlastní hlavička hostitele",
"proxyCustomHeaderDescription": "Hlavička hostitele bude nastavena při proxování požadavků. Nechte prázdné pro použití výchozího nastavení.",
"proxyAdditionalSubmit": "Uložit nastavení proxy",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "Základní Ověření Záhlaví",
"policyAuthHeaderAuthDescription": "Ověřit vlastní HTTP hlavičku názvu a hodnoty při každém požadavku",
"policyAuthHeaderAuthSummary": "Nastaveno hlavička",
"policyAuthMtlsTitle": "Klientský certifikát (mTLS)",
"policyAuthMtlsDescription": "Vyžadovat klientský certifikát podepsaný důvěryhodnou CA během TLS handshake",
"policyAuthMtlsSummary": "{count} důvěryhodných certifikátů CA",
"policyAuthHeaderName": "Uživatelské jméno",
"policyAuthHeaderValue": "Heslo",
"policyAuthSetPasscode": "Nastavit přístupový kód",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "Automatická úprava uživatelů",
"idpAutoProvisionUsersDescription": "Pokud je povoleno, uživatelé budou automaticky vytvářeni v systému při prvním přihlášení, s možností namapovat uživatele na role a organizace.",
"idpAutoProvisionConfigureAfterCreate": "Nastavení automatického poskytování lze nakonfigurovat, jakmile je vytvořen poskytovatel identity.",
"idpSessionRevalidation": "Kontinuální opětovné ověřování relace",
"idpSessionRevalidationDescription": "Když je povoleno, pravidelně znovu ověřuje relace s tímto poskytovatelem identity. Pokud poskytovatel odejme přístup, propojené relace jsou ukončeny.",
"idpSessionRevalidationConfigureAfterCreate": "Nepřetržité ověřování relací lze nakonfigurovat, jakmile je vytvořen poskytovatel identity.",
"licenseBadge": "PE",
"idpType": "Typ poskytovatele",
"idpTypeDescription": "Vyberte typ poskytovatele identity, který chcete nakonfigurovat",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "Odstranit doménu",
"actionGetDNSRecords": "Získat záznamy DNS",
"actionRestartOrgDomain": "Restartovat doménu",
"actionListCustomDnsRecords": "Seznam vlastních záznamů DNS",
"actionCreateCustomDnsRecord": "Vytvořit vlastní záznam DNS",
"actionUpdateCustomDnsRecord": "Aktualizovat vlastní záznam DNS",
"actionDeleteCustomDnsRecord": "Smazat vlastní záznam DNS",
"actionCreateSite": "Vytvořit lokalitu",
"actionDeleteSite": "Odstranění lokality",
"actionGetSite": "Získat web",
"actionListSites": "Seznam stránek",
"actionReGenerateSecret": "Obnovit přihlašovací údaje",
"actionCreateAiProvider": "Vytvořit poskytovatele AI",
"actionDeleteAiProvider": "Smazat poskytovatele AI",
"actionGetAiProvider": "Získat poskytovatele AI",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "Generovat přístupový token",
"actionDeleteAccessToken": "Odstranit přístupový token",
"actionListAccessTokens": "Seznam přístupových tokenů",
"actionListResourceSessions": "Seznam relací zdrojů",
"actionDeleteResourceSession": "Zrušit relaci zdroje",
"actionCreateResourceSessionToken": "Vytvořit token relace prostředku",
"actionCreateResourceRule": "Vytvořit pravidlo pro zdroj",
"actionDeleteResourceRule": "Odstranit pravidlo pro dokument",
"actionListResourceRules": "Seznam pravidel zdrojů",
"actionUpdateResourceRule": "Aktualizovat pravidlo pro dokument",
"actionCreateResourceMtlsCertificate": "Vytvořit mTLS certifikát zdroje",
"actionDeleteResourceMtlsCertificate": "Smazat mTLS certifikát zdroje",
"actionListResourceMtlsCertificates": "Seznam mTLS certifikátů zdrojů",
"actionListOrgLabels": "Seznam štítků",
"actionCreateOrgLabel": "Vytvořit štítek",
"actionUpdateOrgLabel": "Aktualizovat štítek",
"actionDeleteOrgLabel": "Smazat štítek",
"actionAttachLabelToItem": "Přiřadit štítek",
"actionDetachLabelFromItem": "Odstranit štítek",
"actionCreateEventStreamingDestination": "Vytvořit cíl pro streamování událostí",
"actionUpdateEventStreamingDestination": "Aktualizovat cíl pro streamování událostí",
"actionDeleteEventStreamingDestination": "Smazat cíl pro streamování událostí",
"actionListEventStreamingDestinations": "Seznam cílů pro streamování událostí",
"actionCreateAlertRule": "Vytvořit pravidlo upozornění",
"actionUpdateAlertRule": "Aktualizovat pravidlo upozornění",
"actionDeleteAlertRule": "Smazat pravidlo upozornění",
"actionGetAlertRule": "Získat pravidlo upozornění",
"actionListAlertRules": "Seznam pravidel upozornění",
"actionCreateHealthCheck": "Vytvořit kontrolu stavu",
"actionUpdateHealthCheck": "Aktualizovat kontrolu stavu",
"actionDeleteHealthCheck": "Smazat kontrolu stavu",
"actionListHealthChecks": "Seznam kontrol stavu",
"actionListOrgs": "Seznam organizací",
"actionCheckOrgId": "ID kontroly",
"actionCreateOrg": "Vytvořit organizaci",
@@ -1726,6 +1794,7 @@
"sidebarHome": "Domů",
"sidebarSites": "Stránky",
"sidebarApprovals": "Žádosti o schválení",
"sidebarResourceSessions": "Relace zdrojů",
"sidebarResources": "Zdroje",
"sidebarProxyResources": "Veřejnost",
"sidebarClientResources": "Soukromé",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "Zásady veřejných zdrojů",
"commandIdentityProviders": "Poskytovatelé identity",
"commandApprovals": "Žádosti o schválení",
"commandResourceSessions": "Relace zdrojů",
"commandShareableLinks": "Sdílené odkazy",
"commandOrganization": "Organizace",
"commandLogsAndAnalytics": "Protokoly a analytika",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "Protokoly autentizace",
"commandLogsAction": "Protokoly akcí administrátora",
"commandLogsConnection": "Protokoly připojení",
"commandLogsSsh": "Logy SSH relací",
"commandLogsStreaming": "Streamování událostí",
"commandManagement": "Řízení",
"commandAlerting": "Upozorňování",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "Jak dlouho uchovávat protokoly připojení",
"logRetentionAISessionsLabel": "Retence protokolu AI Gateway Session",
"logRetentionAISessionsDescription": "Jak dlouho zachovávat protokoly relací výzev/odpovědí AI brány",
"logRetentionSshLabel": "Doba uchování logů SSH relací",
"logRetentionSshDescription": "Jak dlouho uchovávat logy SSH relací a příkazů",
"logRetentionDisabled": "Zakázáno",
"logRetention3Days": "3 dny",
"logRetention7Days": "7 dní",
@@ -3707,6 +3780,27 @@
"connectionLogs": "Protokoly připojení",
"connectionLogsDescription": "Zobrazit protokoly připojení pro tunely v této organizaci",
"sidebarLogsConnection": "Protokoly připojení",
"sidebarLogsSsh": "Logy SSH relací",
"sshSessionLogs": "Logy SSH relací",
"sshSessionLogsDescription": "Zobrazit SSH relace a příkazy provedené v nich na stránkách v této organizaci",
"sshSystemUser": "Systémový uživatel",
"sshCommands": "Příkazy",
"sshSessionId": "ID relace",
"sshAuthMethod": "Metoda ověření",
"sshTransport": "Připojení",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "Prohlížeč",
"sshKeyFingerprint": "Fingerprint klíče",
"sshCertKeyId": "ID klíče certifikátu",
"sshStartedAt": "Začátek",
"sshEndedAt": "Ukončen v",
"sshSessionLoadingCommands": "Načítám příkazy...",
"sshSessionCommandsError": "Nepodařilo se načíst příkazy pro tuto relaci",
"sshSessionNoCommands": "Pro tuto relaci nejsou záznamy o příkazech",
"sshSessionRecalledCommand": "(vyvoláno z historie shellu)",
"sshSessionInexactNotice": "Některé příkazy byly upraveny s vyvoláním z historie nebo dokončením pomocí tabulátoru a nemusí přesně odpovídat tomu, co shell spustil.",
"sshSessionInexactTooltip": "Editováno z historie nebo dokončení pomocí tabulátoru; nemusí přesně odpovídat tomu, co shell spustil",
"sshSessionCommandsTruncated": "Zobrazuje se prvních {shown} z {total} příkazů. K exportování logů pro zobrazení všech použijte možnost exportu.",
"sidebarLogsStreaming": "Streamování",
"sourceAddress": "Zdrojová adresa",
"destinationAddress": "Cílová adresa",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "Jak přidat záznamy",
"dnsRecord": "Záznamy DNS",
"domainTabSetup": "Nastavení",
"domainTabRecords": "Záznamy",
"domainSetupRecordsDescription": "Vytvořte tyto záznamy u registrátora vaší domény nebo aktuálního poskytovatele DNS. Pangolin je používá k ověření domény a směrování jejího provozu.",
"domainRecordsInfoTitle": "Záznamy poskytované Pangolinem",
"domainRecordsInfoDescription": "Jmenné servery Pangolinu odpovídají na DNS dotazy pro {domain}. Záznamy, které zde přidáte, jsou poskytovány přímo z Pangolinu, takže je nemusíte vytvářet u svého registrátora.",
"customDnsRecords": "Vlastní záznamy DNS",
"customDnsRecordsAdd": "Přidat záznam",
"customDnsRecordsSearch": "Prohledat záznamy...",
"customDnsRecordsUnverifiedTitle": "Záznamy zatím nejsou poskytovány",
"customDnsRecordsUnverifiedDescription": "Tato doména nebyla ověřena. Nyní můžete přidat záznamy, ale jmenné servery Pangolin je neposkytnou, dokud jmenné servery domény nebudou ukazovat na Pangolin a ověření nebude dokončeno.",
"customDnsRecordCreate": "Přidat DNS záznam",
"customDnsRecordEdit": "Upravit DNS záznam",
"customDnsRecordFormDescription": "Tento záznam bude poskytován pro {domain}. Změny se mohou začít projevovat během několika minut.",
"customDnsRecordNameDescription": "Použijte @ pro kořen domény nebo * jako první label pro záznam všech jmen.",
"customDnsRecordValueDescription": "Zadejte jednu hodnotu na řádek, např. oba jmenné servery záznamu NS.",
"customDnsRecordPriority": "Priorita",
"customDnsRecordWeight": "Váha",
"customDnsRecordPort": "Port (v kontekstu: customDnsRecordPort)",
"customDnsRecordCreated": "DNS záznam byl vytvořen",
"customDnsRecordUpdated": "DNS záznam byl aktualizován",
"customDnsRecordSaveError": "Nepodařilo se uložit DNS záznam",
"customDnsRecordDeleted": "DNS záznam byl smazán",
"customDnsRecordDeleteError": "Nepodařilo se smazat DNS záznam",
"customDnsRecordDeleteTitle": "Smazat DNS záznam",
"customDnsRecordDeleteQuestion": "Opravdu chcete smazat {type} záznam pro {name}? Přestane se zobrazovat, jakmile vyprší odpovědi v cache.",
"customDnsRecordDeleteConfirm": "Smazat záznam",
"required": "Požadováno",
"domainSettingsUpdated": "Nastavení domény bylo úspěšně aktualizováno",
"orgOrDomainIdMissing": "Chybí ID organizace nebo domény",
@@ -3938,14 +4058,14 @@
"backToHome": "Zpět na domovskou stránku",
"needToSignInToOrg": "Potřebujete použít identitního poskytovatele vaší organizace?",
"mtls": "mTLS",
"mtlsCaCertificates": "Důvěryhodné certifikáty CA",
"mtlsCaCertificates": "Nastavit klientské certifikáty",
"mtlsCaCertificatesDescription": "Klienti musí předložit certifikát podepsaný jedním z těchto certifikačních autorit, aby se připojili. Nahrávejte pouze veřejné certifikáty CA, nikdy soukromý klíč.",
"mtlsCertificateName": "Jméno (nepovinné)",
"mtlsCertificateNamePlaceholder": "CA certifikát podnikového zařízení",
"mtlsCertificatePem": "CA certifikát (PEM)",
"mtlsCertificatePemDescription": "Vložte certifikát kódovaný v PEM nebo svazek certifikátů, nebo nahrajte soubor .pem, .crt nebo .cer.",
"mtlsUploadFile": "Nahrát soubor",
"mtlsAddCertificate": "Přidat CA certifikát",
"mtlsAddCertificate": "Přidat certifikát CA",
"mtlsCertificateAdded": "CA certifikát přidán",
"mtlsCertificateAddedDescription": "Klienti s certifikáty podepsanými touto CA mohou nyní být ověřeni.",
"mtlsCertificateErrorAdd": "Nepodařilo se přidat CA certifikát",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "Certifikát nebylo možné odstranit.",
"mtlsLastCertificateRemoved": "mTLS byl vypnut, protože byl odstraněn poslední certifikát CA.",
"mtlsNoCertificates": "Zatím nebyly přidány žádné certifikáty CA",
"mtlsPendingCertificate": "CA certifikát {number}",
"mtlsCertificateInvalidPem": "Nebyly nalezeny žádné certifikáty ve formátu PEM. Očekávaný blok začínající příkazem -----BEGIN CERTIFICATE-----.",
"mtlsCertificatePrivateKey": "Vypadá to jako soukromý klíč, ne certifikát. Nahrajte pouze veřejný certifikát CA.",
"mtlsCertificateDuplicate": "Tento certifikát CA byl již přidán.",
"mtlsRemoveCertificate": "Odstranit CA certifikát",
"mtlsExpired": "Vypršelo",
"mtlsExpiresSoon": "Brzy vyprší",
+128 -4
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "Send al internettrafik for at gå ud igennem site-netværket.",
"resourceTypeGatewayDescription": "Send al internettrafik for at gå ud igennem site-netværket.",
"resourceDomainDescription": "Ressourcen vil blive serveret på dette fuldstændigt kvalificerede domænenavnet.",
"resourceMatchBaseDomain": "Match også {host}",
"resourceMatchBaseDomainDescription": "Et jokertegn matcher kun værtsnavne under {host}. Aktiver dette for også at køre {host} gennem denne ressource.",
"resourceHTTPSSettings": "HTTPS-indstillinger",
"resourceHTTPSSettingsDescription": "Konfigurer hvordan ressourcen skal nås over HTTPS",
"resourcePortDescription": "Den eksterne port på Pangolin-instansen eller noden, hvor ressourcen vil være tilgængelig.",
@@ -425,6 +427,31 @@
"accessRolesAdd": "Tilføj rolle",
"accessRoleDelete": "Slet rolle",
"accessApprovalsManage": "Administrer godkendelser",
"resourceSessionsManage": "Administrer Resource Sessioner",
"resourceSessionsDescription": "Se, hvem der aktuelt har en aktiv session på dine offentlige ressourcer, og tilbagekald adgang",
"resourceSessionsSearch": "Søg efter bruger, e-mail, link eller ressource...",
"resourceSessionsMethod": "Metode",
"resourceSessionsIssued": "Udstedt",
"resourceSessionsAnonymous": "Anonym",
"resourceSessionsFilterAnyUser": "Alle brugere",
"resourceSessionsFilteredToSession": "Viser én session",
"resourceSessionsMethodSso": "SSO",
"resourceSessionsMethodPassword": "Adgangskode",
"resourceSessionsMethodPincode": "PIN-kode",
"resourceSessionsMethodWhitelist": "E-mail",
"resourceSessionsMethodAccessToken": "Delbart link",
"resourceSessionsRevoke": "Tilbagekald",
"resourceSessionsRevokeTitle": "Tilbagekald session",
"resourceSessionsRevokeQuestion": "Tilbagekald sessionen for {identity} på {resource}? De skal godkende igen for at få adgang til ressourcen.",
"resourceSessionsRevokeSsoNote": "Denne bruger er logget ind med SSO. Alle deres sessioner for denne ressource fra samme login vil blive tilbagekaldt, og de skal logge ind igen for at få adgang til det, selvom de stadig er logget ind på Pangolin.",
"resourceSessionsRevoked": "Session tilbagekaldt",
"resourceSessionsRevokedDescription": "Sessionen er blevet tilbagekaldt. Det kan tage et par sekunder, før det træder i kraft.",
"resourceSessionsErrorRevoke": "Kunne ikke tilbagekalde session",
"resourceSessionsErrorRevokeDescription": "Der opstod en fejl under tilbagekaldelse af sessionen",
"resourceSessionReauthRequired": "Din session for denne ressource blev afsluttet af en administrator. Log ind igen for at fortsætte.",
"logLinks": "Links",
"logLinkUser": "Bruger",
"logLinkResourceSession": "Ressource Session",
"accessApprovalsDescription": "Se og administrer ventende godkendelser for adgang til denne organisation",
"description": "Beskrivelse",
"inviteTitle": "Åbne invitationer",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "Der er ingen grænse for antallet af brugere for denne host.",
"licenseUsersUsedMax": "{usedUsers} af {maxUsers} brugere brugt",
"licenseUsersUsed": "{count, plural, =0 {ingen brugere} one {én bruger} other {# brugere}}",
"licenseUsageMachines": "Maskinklienter",
"licenseNoMachineLimit": "Der er ingen grænse for antallet af maskinklienter til denne host.",
"licenseMachinesUsedMax": "{usedMachines} af {maxMachines} maskinklienter brugt",
"licenseMachinesUsed": "{count, plural, =0 {# maskinklienter} one {# maskinklient} other {# maskinklienter}}",
"licenseUnlimited": "Ubegrænset",
"licenseTierLabel": "Niveau",
"licensePurchaseDescription": "Vælg hvor mange sites du vil {selectedMode, select, license {købe en licens for. Du kan altid tilføje flere sites senere.} other {tilføje din eksisterende licens.}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "Brugeren er blevet opdateret.",
"autoProvisioned": "Automatisk provisioneret",
"autoProvisionSettings": "Autoprovisioneringsindstillinger",
"autoProvisionedDescription": "Tillad denne bruger at blive automatisk administrert af en identitetsudbyder",
"autoProvisionedDescription": "Hvis identitetsudbyderen har auto-provisionering aktiveret, tillad brugeren at blive administreret automatisk.",
"accessControlsDescription": "Administrer, hvad denne bruger kan få adgang til og gøre i organisationen",
"accessControlsSubmit": "Gem adgangskontroller",
"singleRolePerUserPlanNotice": "Din plan understøtter kun én rolle per bruger.",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "Fordel forespørgsler jævnt på tværs af målene i rotation.",
"loadBalancingStrategy_p2c": "Power of Two Choices",
"loadBalancingStrategy_p2c_description": "Vælg to tilfældige mål og send forespørgslen til det med færre aktive forbindelser.",
"loadBalancingStrategy_hrw": "Klient IP Hash",
"loadBalancingStrategy_hrw": "Højeste tilfældige vægt",
"loadBalancingStrategy_hrw_description": "Hash klientens IP, så den samme klient konsekvent dirigeres til det samme mål.",
"loadBalancingStrategy_leasttime": "Mindste svarstid",
"loadBalancingStrategy_leasttime_description": "Send forespørgsler til målet med den hurtigste svartid og færrest aktive forbindelser.",
@@ -829,6 +860,8 @@
"save": "Gem",
"proxyAdditional": "Yderligere Proxyindstillinger",
"proxyAdditionalDescription": "Konfigurer hvordan ressourcen håndterer proxy-indstillingerne",
"proxyForwardIdentityHeaders": "Videresend identitetshoveder",
"proxyForwardIdentityHeadersDescription": "Send den autentificerede brugers identitet til backend i Remote-User, Remote-Email, Remote-Name, Remote-Role og Remote-User-Id hoveder. Deaktiver for backends, der afviser forespørgsler med ugenkendte hoveder.",
"proxyCustomHeader": "Tilpasset host-header",
"proxyCustomHeaderDescription": "Host-header som skal settes ved videresendelse af forespørgsler. Lad stå tom for at bruge standardinnstillingen.",
"proxyAdditionalSubmit": "Gem proxyindstillinger",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "Grunnleggende Header Autentificering",
"policyAuthHeaderAuthDescription": "Bekræft et tilpasset HTTP-headernavn og værdi ved hver forespørgsel",
"policyAuthHeaderAuthSummary": "Header konfigureret",
"policyAuthMtlsTitle": "Kundecertifikat (mTLS)",
"policyAuthMtlsDescription": "Kræv et kundecertifikat underskrevet af en betroet CA under TLS-håndtrykket",
"policyAuthMtlsSummary": "{count} betroede CA-certifikater",
"policyAuthHeaderName": "Brugernavn",
"policyAuthHeaderValue": "Adgangskode",
"policyAuthSetPasscode": "Angiv adgangskode",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "Automatisk brugerprovisionering",
"idpAutoProvisionUsersDescription": "Når aktiveret, oprettes brugere automatisk i systemet ved første login, med mulighed til at tildele brugere til roller og organisationer.",
"idpAutoProvisionConfigureAfterCreate": "Du kan konfigurere autoprovisioneringsindstillingerne når identitetsudbyderen er oprettet.",
"idpSessionRevalidation": "Kontinuerlig Session Revalidering",
"idpSessionRevalidationDescription": "Når aktiveret, revaliderer periodisk sessioner med denne identitetsudbyder. Hvis udbyderen tilbagekalder adgang, afsluttes knyttede sessioner.",
"idpSessionRevalidationConfigureAfterCreate": "Du kan konfigurere kontinuerlig session revalidering, når identitetsudbyderen er oprettet.",
"licenseBadge": "EE",
"idpType": "Udbydertype",
"idpTypeDescription": "Vælg typen identitetsudbyder du ønsker at konfigurere",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "Slet domæne",
"actionGetDNSRecords": "Hent DNS-poster",
"actionRestartOrgDomain": "Genstart Domæne",
"actionListCustomDnsRecords": "Liste over tilpassede DNS-poster",
"actionCreateCustomDnsRecord": "Opret tilpasset DNS-post",
"actionUpdateCustomDnsRecord": "Opdater tilpasset DNS-post",
"actionDeleteCustomDnsRecord": "Slet tilpasset DNS-post",
"actionCreateSite": "Opret site",
"actionDeleteSite": "Slet site",
"actionGetSite": "Hent site",
"actionListSites": "Vis sites",
"actionReGenerateSecret": "Regenerer brugeroplysninger",
"actionCreateAiProvider": "Opret AI-udbyder",
"actionDeleteAiProvider": "Slet AI-udbyder",
"actionGetAiProvider": "Få AI-udbyder",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "Generer adgangstoken",
"actionDeleteAccessToken": "Slet adgangstoken",
"actionListAccessTokens": "Vis adgangstokens",
"actionListResourceSessions": "Liste Resource Sessioner",
"actionDeleteResourceSession": "Tilbagekald Resource Session",
"actionCreateResourceSessionToken": "Opret ressourcessession token",
"actionCreateResourceRule": "Opret ressourceregel",
"actionDeleteResourceRule": "Slet ressourceregel",
"actionListResourceRules": "Vis ressourceregler",
"actionUpdateResourceRule": "Opdater ressourceregel",
"actionCreateResourceMtlsCertificate": "Opret Resource mTLS Certifikat",
"actionDeleteResourceMtlsCertificate": "Slet Resource mTLS Certifikat",
"actionListResourceMtlsCertificates": "Liste Resource mTLS Certifikater",
"actionListOrgLabels": "Liste Etiketter",
"actionCreateOrgLabel": "Opret etiket",
"actionUpdateOrgLabel": "Opdater etiket",
"actionDeleteOrgLabel": "Slet etiket",
"actionAttachLabelToItem": "Vedhæft etikett",
"actionDetachLabelFromItem": "Fjern etikett",
"actionCreateEventStreamingDestination": "Opret Event Streaming Destination",
"actionUpdateEventStreamingDestination": "Opdater Event Streaming Destination",
"actionDeleteEventStreamingDestination": "Slet Event Streaming Destination",
"actionListEventStreamingDestinations": "Liste Event Streaming Destinationer",
"actionCreateAlertRule": "Opret varslingsregel",
"actionUpdateAlertRule": "Opdater varslingsregel",
"actionDeleteAlertRule": "Slet varslingsregel",
"actionGetAlertRule": "Hent varslingsregel",
"actionListAlertRules": "Liste varslingsregler",
"actionCreateHealthCheck": "Opret sundhedstjek",
"actionUpdateHealthCheck": "Opdater sundhedstjek",
"actionDeleteHealthCheck": "Slet sundhedstjek",
"actionListHealthChecks": "Liste sundhedstjek",
"actionListOrgs": "Vis organisationer",
"actionCheckOrgId": "Tjek ID",
"actionCreateOrg": "Opret organisation",
@@ -1726,6 +1794,7 @@
"sidebarHome": "Hjem",
"sidebarSites": "Websteder",
"sidebarApprovals": "Godkendelsesanmodninger",
"sidebarResourceSessions": "Ressource Sessioner",
"sidebarResources": "Ressourcer",
"sidebarProxyResources": "Offentlig",
"sidebarClientResources": "Privat",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "Offentlige ressourcepolitikker",
"commandIdentityProviders": "Identitetsudbydere",
"commandApprovals": "Godkendelsesanmodninger",
"commandResourceSessions": "Ressource Sessioner",
"commandShareableLinks": "Delbare links",
"commandOrganization": "Organisation",
"commandLogsAndAnalytics": "Logs og analyser",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "Adgangslogs",
"commandLogsAction": "Handlingsloger",
"commandLogsConnection": "Forbindelseslogs",
"commandLogsSsh": "SSH-session Logs",
"commandLogsStreaming": "Hændelsesstreaming",
"commandManagement": "Administration",
"commandAlerting": "Varsling",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "Hvor længe du vil beholde forbindelseslogs",
"logRetentionAISessionsLabel": "AI Gateway Økt Log Opbevaring",
"logRetentionAISessionsDescription": "Hvor længe at opbevare AI gateway prompt/svar øktprotokoller",
"logRetentionSshLabel": "SSH Session Log Opbevaring",
"logRetentionSshDescription": "Hvor længe du vil beholde SSH-session og kommando logs",
"logRetentionDisabled": "Deaktiveret",
"logRetention3Days": "3 dage",
"logRetention7Days": "7 dage",
@@ -3707,6 +3780,27 @@
"connectionLogs": "Forbindelseslogs",
"connectionLogsDescription": "Vis forbindelseslogs for tunneler i denne organisation",
"sidebarLogsConnection": "Forbindelseslogs",
"sidebarLogsSsh": "SSH-session Logs",
"sshSessionLogs": "SSH-session Logs",
"sshSessionLogsDescription": "Vis SSH-sessioner og de kommandoer, der blev kørt i dem på sider i denne organisation",
"sshSystemUser": "Systembruger",
"sshCommands": "Kommandoer",
"sshSessionId": "Sessions-ID",
"sshAuthMethod": "Godkendelsesmetode",
"sshTransport": "Forbindelse",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "Browser",
"sshKeyFingerprint": "Nøglefingeraftryk",
"sshCertKeyId": "Certifikat Nøgle-ID",
"sshStartedAt": "Startet kl",
"sshEndedAt": "Sluttede kl",
"sshSessionLoadingCommands": "Indlæser kommandoer...",
"sshSessionCommandsError": "Kunne ikke indlæse kommandoer for denne session",
"sshSessionNoCommands": "Ingen kommandoer registreret for denne session",
"sshSessionRecalledCommand": "(erindret fra shell-historik)",
"sshSessionInexactNotice": "Nogle kommandoer blev redigeret med historik-rekald eller tab-fuldførelse og matcher muligvis ikke præcis, hvad shell'en kørte.",
"sshSessionInexactTooltip": "Redigeret med historik-rekald eller tab-fuldførelse; matcher muligvis ikke præcis, hvad shell'en kørte",
"sshSessionCommandsTruncated": "Viser de første {shown} af {total} kommandoer. Eksporter logs for at se dem alle.",
"sidebarLogsStreaming": "Streaming",
"sourceAddress": "Kildeadresse",
"destinationAddress": "Måladresse ",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "Hvordan tilføje poster",
"dnsRecord": "DNS registre",
"domainTabSetup": "Opsætning",
"domainTabRecords": "Poster",
"domainSetupRecordsDescription": "Opret disse poster hos din domæneregistrator eller nuværende DNS-udbyder. Pangolin bruger dem til at verificere domænet og rute trafikken.",
"domainRecordsInfoTitle": "Poster leveret af Pangolin",
"domainRecordsInfoDescription": "Pangolins navneservere besvarer DNS-forespørgsler for {domain}. Posterne, du tilføjer her, leveres direkte fra Pangolin, så du behøver ikke at oprette dem hos din registrator.",
"customDnsRecords": "Tilpassede DNS-poster",
"customDnsRecordsAdd": "Tilføj post",
"customDnsRecordsSearch": "Søg poster...",
"customDnsRecordsUnverifiedTitle": "Poster serveres endnu ikke",
"customDnsRecordsUnverifiedDescription": "Dette domæne er ikke verificeret. Du kan tilføje poster nu, men Pangolin's navneservere vil ikke servere dem, før domænenavneservere peger mod Pangolin, og verifikationen er færdig.",
"customDnsRecordCreate": "Tilføj DNS-post",
"customDnsRecordEdit": "Rediger DNS-post",
"customDnsRecordFormDescription": "Denne post vil blive serveret for {domain}. Ændringer kan tage et par minutter at blive opdateret.",
"customDnsRecordNameDescription": "Brug @ for rodens domæne eller * som første etiket for et wildcard.",
"customDnsRecordValueDescription": "Indtast én værdi per linje, f.eks. begge navneservere for en NS-post.",
"customDnsRecordPriority": "Prioritet",
"customDnsRecordWeight": "Vægt",
"customDnsRecordPort": "Porten",
"customDnsRecordCreated": "DNS-post oprettet",
"customDnsRecordUpdated": "DNS-post opdateret",
"customDnsRecordSaveError": "Kunne ikke gemme DNS-post",
"customDnsRecordDeleted": "DNS-post slettet",
"customDnsRecordDeleteError": "Kunne ikke slette DNS-post",
"customDnsRecordDeleteTitle": "Slet DNS-post",
"customDnsRecordDeleteQuestion": "Er du sikker på, at du vil slette {type}-posten for {name}? Den vil stoppe med at blive løst, når cachede svar udløber.",
"customDnsRecordDeleteConfirm": "Slet post",
"required": "Påkrævet",
"domainSettingsUpdated": "Domæne indstillinger blev opdateret",
"orgOrDomainIdMissing": "ID for organisation eller domæne mangler",
@@ -3938,7 +4058,7 @@
"backToHome": "Gå tilbage til start",
"needToSignInToOrg": "Behøver du at bruge organisationens identitetsudbyder?",
"mtls": "mTLS",
"mtlsCaCertificates": "Betroede CA-certifikater",
"mtlsCaCertificates": "Indstil kundecertifikater",
"mtlsCaCertificatesDescription": "Klienter skal præsentere et certifikat underskrevet af en af disse certifikatautoriteter for at oprette forbindelse. Kun upload offentlige CA-certifikater, aldrig en privat nøgle.",
"mtlsCertificateName": "Navn (valgfrit)",
"mtlsCertificateNamePlaceholder": "Virksomhedsenhed CA",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "Certifikatet kunne ikke fjernes.",
"mtlsLastCertificateRemoved": "mTLS blev slået fra fordi det sidste CA-certifikat blev fjernet.",
"mtlsNoCertificates": "Ingen CA-certifikater tilføjet endnu",
"mtlsPendingCertificate": "CA-certifikat {number}",
"mtlsCertificateInvalidPem": "Ingen PEM-kodet certifikat fundet. Forventet en blok der starter med -----BEGIN CERTIFICATE-----.",
"mtlsCertificatePrivateKey": "Dette ligner en privat nøgle, ikke et certifikat. Upload kun CA's offentlige certifikat.",
"mtlsCertificateDuplicate": "Dette CA-certifikat er allerede tilføjet.",
"mtlsRemoveCertificate": "Fjern CA-certifikat",
"mtlsExpired": "Udløbet",
"mtlsExpiresSoon": "Udløber snart",
@@ -4488,7 +4612,7 @@
"rdpFull": "Fuldt",
"rdpReal": "Ekte",
"rdpMeta": "Meta",
"rdpUploadFiles": "Upload Filer",
"rdpUploadFiles": "Upload filer",
"rdpFilesReadyToPaste": "Filer klare til at limes ind",
"rdpFilesReadyToPasteDescription": "{count} fil(er) kopieret til fjernudklipsholderen — trykk Ctrl+V på fjernskrivebordet for at indsætte.",
"rdpUploadFailed": "Uploaden mislykkedes",
+127 -3
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "Senden Sie den gesamten Internetverkehr über das Seitennetzwerk.",
"resourceTypeGatewayDescription": "Senden Sie den gesamten Internetverkehr über das Seitennetzwerk.",
"resourceDomainDescription": "Die Ressource wird unter diesem vollständig qualifizierten Domainnamen bereitgestellt.",
"resourceMatchBaseDomain": "Ebenfalls mit {host} übereinstimmen",
"resourceMatchBaseDomainDescription": "Ein Platzhalter stimmt nur mit Hostnamen unter {host} überein. Aktivieren Sie diese Option, um {host} ebenfalls über diese Ressource zu leiten.",
"resourceHTTPSSettings": "HTTPS-Einstellungen",
"resourceHTTPSSettingsDescription": "Konfigurieren Sie den Zugriff auf die Ressource über HTTPS",
"resourcePortDescription": "Der externe Port auf der Pangolin-Instanz oder dem Knoten, an dem die Ressource zugänglich ist.",
@@ -425,6 +427,31 @@
"accessRolesAdd": "Rolle hinzufügen",
"accessRoleDelete": "Rolle löschen",
"accessApprovalsManage": "Genehmigungen verwalten",
"resourceSessionsManage": "Ressourcensitzungen verwalten",
"resourceSessionsDescription": "Anzeigen, wer derzeit eine aktive Sitzung auf Ihren öffentlichen Ressourcen hat und den Zugang widerrufen",
"resourceSessionsSearch": "Nach Benutzer, E-Mail, Link oder Ressource suchen...",
"resourceSessionsMethod": "Methode",
"resourceSessionsIssued": "Ausgestellt",
"resourceSessionsAnonymous": "Anonym",
"resourceSessionsFilterAnyUser": "Alle Benutzer",
"resourceSessionsFilteredToSession": "Eine Sitzung anzeigen",
"resourceSessionsMethodSso": "SSO",
"resourceSessionsMethodPassword": "Passwort",
"resourceSessionsMethodPincode": "PIN-Code",
"resourceSessionsMethodWhitelist": "E-Mail",
"resourceSessionsMethodAccessToken": "Freigabelink",
"resourceSessionsRevoke": "Widerrufen",
"resourceSessionsRevokeTitle": "Sitzung widerrufen",
"resourceSessionsRevokeQuestion": "Möchten Sie die Sitzung für {identity} auf {resource} widerrufen? Sie müssen sich erneut authentifizieren, um auf die Ressource zuzugreifen.",
"resourceSessionsRevokeSsoNote": "Dieser Benutzer ist mit SSO angemeldet. Alle ihre Sitzungen für diese Ressource aus derselben Anmeldung werden widerrufen und sie müssen sich erneut anmelden, um darauf zuzugreifen, auch wenn sie noch bei Pangolin angemeldet sind.",
"resourceSessionsRevoked": "Sitzung widerrufen",
"resourceSessionsRevokedDescription": "Die Sitzung wurde widerrufen. Es kann einige Sekunden dauern, bis dies wirksam wird.",
"resourceSessionsErrorRevoke": "Sitzung konnte nicht widerrufen werden",
"resourceSessionsErrorRevokeDescription": "Beim Widerrufen der Sitzung ist ein Fehler aufgetreten",
"resourceSessionReauthRequired": "Ihre Sitzung für diese Ressource wurde von einem Administrator beendet. Bitte melden Sie sich erneut an, um fortzufahren.",
"logLinks": "Links",
"logLinkUser": "Benutzer",
"logLinkResourceSession": "Ressourcensitzung",
"accessApprovalsDescription": "Zeige und verwalte ausstehende Genehmigungen für den Zugriff auf diese Organisation",
"description": "Beschreibung",
"inviteTitle": "Offene Einladungen",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "Es gibt keine Begrenzung hinsichtlich der Anzahl der Benutzer für diesen Host.",
"licenseUsersUsedMax": "{usedUsers} von {maxUsers} Benutzern verwendet",
"licenseUsersUsed": "{count, plural, =0 {# Benutzer} one {# Benutzer} other {# Benutzer}}",
"licenseUsageMachines": "Maschinen-Clients",
"licenseNoMachineLimit": "Die Anzahl der Maschinen-Clients für diesen Host ist unbegrenzt.",
"licenseMachinesUsedMax": "{usedMachines} von {maxMachines} Maschinen-Clients verwendet",
"licenseMachinesUsed": "{count,plural,=0 {<b>#</b> Maschinen-Clients} one {<b>#</b> Maschinen-Client} other {<b>#</b> Maschinen-Clients}}",
"licenseUnlimited": "Unbegrenzt",
"licenseTierLabel": "Stufe",
"licensePurchaseDescription": "Wähle aus, für wie viele Standorte du möchtest {selectedMode, select, license {kaufe eine Lizenz. Du kannst später immer weitere Standorte hinzufügen.} other {Füge zu deiner bestehenden Lizenz hinzu.}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "Der Benutzer wurde aktualisiert.",
"autoProvisioned": "Automatisch bereitgestellt",
"autoProvisionSettings": "Auto-Bereitstellungseinstellungen",
"autoProvisionedDescription": "Erlaube diesem Benutzer die automatische Verwaltung durch Identitätsanbieter",
"autoProvisionedDescription": "Wenn der Identitätsanbieter die automatische Bereitstellung aktiviert hat, erlauben Sie, dass dieser Benutzer automatisch verwaltet wird.",
"accessControlsDescription": "Verwalten Sie, worauf dieser Benutzer in der Organisation zugreifen und was er tun kann",
"accessControlsSubmit": "Zugriffskontrollen speichern",
"singleRolePerUserPlanNotice": "Ihr Plan unterstützt nur eine Rolle pro Benutzer.",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "Verteilen Sie Anfragen gleichmäßig über Ziele in Rotation.",
"loadBalancingStrategy_p2c": "Macht der Zwei Wahlmöglichkeiten",
"loadBalancingStrategy_p2c_description": "Wählen Sie zwei zufällige Ziele und senden Sie die Anfrage an dasjenige mit weniger aktiven Verbindungen.",
"loadBalancingStrategy_hrw": "Client-IP-Hash",
"loadBalancingStrategy_hrw": "Höchstes Zufallsgewicht",
"loadBalancingStrategy_hrw_description": "Hashen Sie die Client-IP, damit derselbe Client konsistent zum selben Ziel geleitet wird.",
"loadBalancingStrategy_leasttime": "Wenigste Antwortzeit",
"loadBalancingStrategy_leasttime_description": "Senden Sie Anfragen an das Ziel mit der schnellsten Antwortzeit und den wenigsten aktiven Verbindungen.",
@@ -829,6 +860,8 @@
"save": "Speichern",
"proxyAdditional": "Zusätzliche Proxy-Einstellungen",
"proxyAdditionalDescription": "Konfigurieren Sie die Proxy-Einstellungen der Ressource",
"proxyForwardIdentityHeaders": "Identitäts-Header weiterleiten",
"proxyForwardIdentityHeadersDescription": "Die Identität des authentifizierten Benutzers an das Backend in den Remote-User, Remote-Email, Remote-Name, Remote-Role und Remote-User-Id Headern senden. Deaktivieren für Backends, die Anfragen mit unbekannten Headern ablehnen.",
"proxyCustomHeader": "Benutzerdefinierter Host-Header",
"proxyCustomHeaderDescription": "Der Host-Header, der beim Weiterleiten von Anfragen gesetzt werden soll. Leer lassen, um den Standard zu verwenden.",
"proxyAdditionalSubmit": "Proxy-Einstellungen speichern",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "Grundlegende Header-Authentifizierung",
"policyAuthHeaderAuthDescription": "Überprüfen Sie einen benutzerdefinierten HTTP-Headernamen und -wert bei jeder Anfrage",
"policyAuthHeaderAuthSummary": "Header konfiguriert",
"policyAuthMtlsTitle": "Client-Zertifikat (mTLS)",
"policyAuthMtlsDescription": "Ein Client-Zertifikat von einer vertrauenswürdigen CA während des TLS-Handshakes verlangen",
"policyAuthMtlsSummary": "{count,plural,one {<b>#</b> vertrauenswürdiges CA-Zertifikat} other {<b>#</b> vertrauenswürdige CA-Zertifikate}}",
"policyAuthHeaderName": "Benutzername",
"policyAuthHeaderValue": "Passwort",
"policyAuthSetPasscode": "Passcode setzen",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "Automatische Benutzerbereitstellung",
"idpAutoProvisionUsersDescription": "Wenn aktiviert, werden Benutzer beim ersten Login automatisch im System erstellt, mit der Möglichkeit, Benutzer Rollen und Organisationen zuzuordnen.",
"idpAutoProvisionConfigureAfterCreate": "Sie können die automatische Bereitstellung einstellen, sobald der Identitätsanbieter erstellt ist.",
"idpSessionRevalidation": "Kontinuierliche Sitzungs-Revalidierung",
"idpSessionRevalidationDescription": "Wenn aktiviert, werden Sitzungen periodisch mit diesem Identitätsanbieter revalidiert. Wenn der Anbieter den Zugang widerruft, werden verknüpfte Sitzungen beendet.",
"idpSessionRevalidationConfigureAfterCreate": "Sie können die kontinuierliche Sitzungs-Revalidierung konfigurieren, sobald der Identitätsanbieter erstellt ist.",
"licenseBadge": "EE",
"idpType": "Anbietertyp",
"idpTypeDescription": "Wählen Sie den Typ des Identitätsanbieters, den Sie konfigurieren möchten",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "Domain löschen",
"actionGetDNSRecords": "DNS-Einträge abrufen",
"actionRestartOrgDomain": "Domain neu starten",
"actionListCustomDnsRecords": "Benutzerdefinierte DNS-Einträge auflisten",
"actionCreateCustomDnsRecord": "Benutzerdefinierten DNS-Eintrag erstellen",
"actionUpdateCustomDnsRecord": "Benutzerdefinierten DNS-Eintrag aktualisieren",
"actionDeleteCustomDnsRecord": "Benutzerdefinierten DNS-Eintrag löschen",
"actionCreateSite": "Standort erstellen",
"actionDeleteSite": "Standort löschen",
"actionGetSite": "Standort abrufen",
"actionListSites": "Standorte auflisten",
"actionReGenerateSecret": "Zugangsdaten neu generieren",
"actionCreateAiProvider": "KI-Anbieter erstellen",
"actionDeleteAiProvider": "KI-Anbieter löschen",
"actionGetAiProvider": "KI-Anbieter abrufen",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "Zugriffstoken generieren",
"actionDeleteAccessToken": "Zugriffstoken löschen",
"actionListAccessTokens": "Zugriffstoken auflisten",
"actionListResourceSessions": "Ressourcensitzungen auflisten",
"actionDeleteResourceSession": "Ressourcensitzung widerrufen",
"actionCreateResourceSessionToken": "Ressourcensitzungstoken erstellen",
"actionCreateResourceRule": "Ressourcenregel erstellen",
"actionDeleteResourceRule": "Ressourcenregel löschen",
"actionListResourceRules": "Ressourcenregeln auflisten",
"actionUpdateResourceRule": "Ressourcenregel aktualisieren",
"actionCreateResourceMtlsCertificate": "Ressourcen-mTLS-Zertifikat erstellen",
"actionDeleteResourceMtlsCertificate": "Ressourcen-mTLS-Zertifikat löschen",
"actionListResourceMtlsCertificates": "Ressourcen-mTLS-Zertifikate auflisten",
"actionListOrgLabels": "Labels auflisten",
"actionCreateOrgLabel": "Label erstellen",
"actionUpdateOrgLabel": "Label aktualisieren",
"actionDeleteOrgLabel": "Label löschen",
"actionAttachLabelToItem": "Label anhängen",
"actionDetachLabelFromItem": "Label abtrennen",
"actionCreateEventStreamingDestination": "Event-Streaming-Ziel erstellen",
"actionUpdateEventStreamingDestination": "Event-Streaming-Ziel aktualisieren",
"actionDeleteEventStreamingDestination": "Event-Streaming-Ziel löschen",
"actionListEventStreamingDestinations": "Event-Streaming-Ziele auflisten",
"actionCreateAlertRule": "Benachrichtigungsregel erstellen",
"actionUpdateAlertRule": "Benachrichtigungsregel aktualisieren",
"actionDeleteAlertRule": "Benachrichtigungsregel löschen",
"actionGetAlertRule": "Benachrichtigungsregel abrufen",
"actionListAlertRules": "Benachrichtigungsregeln auflisten",
"actionCreateHealthCheck": "Gesundheits-Check erstellen",
"actionUpdateHealthCheck": "Gesundheits-Check aktualisieren",
"actionDeleteHealthCheck": "Gesundheits-Check löschen",
"actionListHealthChecks": "Gesundheits-Checks auflisten",
"actionListOrgs": "Organisationen auflisten",
"actionCheckOrgId": "ID prüfen",
"actionCreateOrg": "Organisation erstellen",
@@ -1726,6 +1794,7 @@
"sidebarHome": "Zuhause",
"sidebarSites": "Standorte",
"sidebarApprovals": "Genehmigungsanfragen",
"sidebarResourceSessions": "Ressourcensitzungen",
"sidebarResources": "Ressourcen",
"sidebarProxyResources": "Öffentlich",
"sidebarClientResources": "Privat",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "Öffentliche Ressourcenrichtlinien",
"commandIdentityProviders": "Identitätsanbieter",
"commandApprovals": "Genehmigungsanfragen",
"commandResourceSessions": "Ressourcensitzungen",
"commandShareableLinks": "Teilbare Links",
"commandOrganization": "Organisation",
"commandLogsAndAnalytics": "Logs & Analysen",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "Zugriffsprotokolle",
"commandLogsAction": "Administrator-Aktionsprotokolle",
"commandLogsConnection": "Netzwerkprotokolle",
"commandLogsSsh": "SSH-Sitzungsprotokolle",
"commandLogsStreaming": "Ereignis-Streaming",
"commandManagement": "Verwaltung",
"commandAlerting": "Alarmierung",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "Wie lange Verbindungsprotokolle gespeichert werden sollen",
"logRetentionAISessionsLabel": "Aufbewahrung von Sitzungsprotokollen des KI-Gateways",
"logRetentionAISessionsDescription": "Wie lange Sitzunglogs von Aufforderung / Antwort des KI-Gateways aufbewahrt werden",
"logRetentionSshLabel": "SSH-Sitzungsprotokolle aufbewahren",
"logRetentionSshDescription": "Wie lange sollen SSH-Sitzungs- und Befehlsprotokolle aufbewahrt werden",
"logRetentionDisabled": "Deaktiviert",
"logRetention3Days": "3 Tage",
"logRetention7Days": "7 Tage",
@@ -3707,6 +3780,27 @@
"connectionLogs": "Verbindungsprotokolle",
"connectionLogsDescription": "Verbindungsprotokolle für Tunnel in dieser Organisation anzeigen",
"sidebarLogsConnection": "Verbindungsprotokolle",
"sidebarLogsSsh": "SSH-Sitzungsprotokolle",
"sshSessionLogs": "SSH-Sitzungsprotokolle",
"sshSessionLogsDescription": "SSH-Sitzungen anzeigen und die darin ausgeführten Befehle auf Standorten in dieser Organisation",
"sshSystemUser": "Systembenutzer",
"sshCommands": "Befehle",
"sshSessionId": "Sitzungs-ID",
"sshAuthMethod": "Auth-Methode",
"sshTransport": "Verbindung",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "Browser",
"sshKeyFingerprint": "Schlüsselfingerabdruck",
"sshCertKeyId": "Zertifikatsschlüssel-ID",
"sshStartedAt": "Gestartet um",
"sshEndedAt": "Beendet um",
"sshSessionLoadingCommands": "Befehle werden geladen...",
"sshSessionCommandsError": "Fehler beim Laden der Befehle für diese Sitzung",
"sshSessionNoCommands": "Keine Befehle für diese Sitzung aufgezeichnet",
"sshSessionRecalledCommand": "(aus der Shell-Historie zurückgerufen)",
"sshSessionInexactNotice": "Einige Befehle wurden mit Verlaufserinnerung oder Tab-Vervollständigung bearbeitet und stimmen möglicherweise nicht genau mit dem überein, was die Shell ausgeführt hat.",
"sshSessionInexactTooltip": "Bearbeitet mit Verlaufserinnerung oder Tab-Vervollständigung; möglicherweise nicht exakt mit dem übereinstimmend, was die Shell ausgeführt hat",
"sshSessionCommandsTruncated": "Die ersten {shown} von {total} Befehlen werden angezeigt. Exportieren Sie die Protokolle, um alle Befehle zu sehen.",
"sidebarLogsStreaming": "Streaming",
"sourceAddress": "Quelladresse",
"destinationAddress": "Zieladresse",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "So kann man Datensätze hinzufügen",
"dnsRecord": "DNS-Einträge",
"domainTabSetup": "Einrichtung",
"domainTabRecords": "Einträge",
"domainSetupRecordsDescription": "Erstellen Sie diese Einträge bei Ihrem Domain-Registrar oder aktuellen DNS-Anbieter. Pangolin nutzt sie, um die Domain zu verifizieren und deren Traffic zu leiten.",
"domainRecordsInfoTitle": "Einträge bereitgestellt von Pangolin",
"domainRecordsInfoDescription": "Die Nameserver von Pangolin beantworten DNS-Abfragen für {domain}. Die hier hinzugefügten Einträge werden direkt von Pangolin bereitgestellt, sodass Sie diese nicht bei Ihrem Registrar erstellen müssen.",
"customDnsRecords": "Benutzerdefinierte DNS-Einträge",
"customDnsRecordsAdd": "Eintrag hinzufügen",
"customDnsRecordsSearch": "Einträge durchsuchen...",
"customDnsRecordsUnverifiedTitle": "Einträge werden noch nicht bereitgestellt",
"customDnsRecordsUnverifiedDescription": "Diese Domain wurde nicht verifiziert. Sie können jetzt Einträge hinzufügen, aber die Nameserver von Pangolin werden sie nicht bereitstellen, bis die Nameserver der Domain auf Pangolin zeigen und die Verifizierung abgeschlossen ist.",
"customDnsRecordCreate": "DNS-Eintrag hinzufügen",
"customDnsRecordEdit": "DNS-Eintrag bearbeiten",
"customDnsRecordFormDescription": "Dieser Eintrag wird für {domain} bereitgestellt. Änderungen können einige Minuten dauern, um sich zu verbreiten.",
"customDnsRecordNameDescription": "Verwende @ für die Wurzel der Domain oder * als erstes Label für eine Platzhalterfunktion.",
"customDnsRecordValueDescription": "Geben Sie einen Wert pro Zeile ein, z.B. beide Nameserver eines NS-Eintrags.",
"customDnsRecordPriority": "Priorität",
"customDnsRecordWeight": "Gewicht",
"customDnsRecordPort": "Port",
"customDnsRecordCreated": "DNS-Eintrag erstellt",
"customDnsRecordUpdated": "DNS-Eintrag aktualisiert",
"customDnsRecordSaveError": "Fehler beim Speichern des DNS-Eintrags",
"customDnsRecordDeleted": "DNS-Eintrag gelöscht",
"customDnsRecordDeleteError": "Fehler beim Löschen des DNS-Eintrags",
"customDnsRecordDeleteTitle": "DNS-Eintrag löschen",
"customDnsRecordDeleteQuestion": "Sind Sie sicher, dass Sie den {type}-Eintrag für {name} löschen möchten? Er wird aufhören, aufgelöst zu werden, sobald die zwischengespeicherten Antworten ablaufen.",
"customDnsRecordDeleteConfirm": "Eintrag löschen",
"required": "Benötigt",
"domainSettingsUpdated": "Domain-Einstellungen erfolgreich aktualisiert",
"orgOrDomainIdMissing": "Organisation oder Domänen-ID fehlt",
@@ -3938,7 +4058,7 @@
"backToHome": "Zurück zur Startseite",
"needToSignInToOrg": "Benötigen Sie den Identitätsanbieter Ihres Unternehmens?",
"mtls": "mTLS",
"mtlsCaCertificates": "Vertrauenswürdige CA-Zertifikate",
"mtlsCaCertificates": "Client-Zertifikate festlegen",
"mtlsCaCertificatesDescription": "Clients müssen ein Zertifikat vorlegen, das von einer dieser Zertifizierungsstellen signiert wurde, um eine Verbindung herzustellen. Laden Sie nur öffentliche CA-Zertifikate hoch, niemals einen privaten Schlüssel.",
"mtlsCertificateName": "Name (optional)",
"mtlsCertificateNamePlaceholder": "Unternehmensgerät-CA",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "Das Zertifikat konnte nicht entfernt werden.",
"mtlsLastCertificateRemoved": "mTLS wurde deaktiviert, da das letzte CA-Zertifikat entfernt wurde.",
"mtlsNoCertificates": "Noch keine CA-Zertifikate hinzugefügt",
"mtlsPendingCertificate": "CA-Zertifikat {number}",
"mtlsCertificateInvalidPem": "Kein PEM-kodiertes Zertifikat gefunden. Erwarte einen Block, der mit -----BEGIN CERTIFICATE----- beginnt.",
"mtlsCertificatePrivateKey": "Das sieht wie ein privater Schlüssel aus, nicht wie ein Zertifikat. Laden Sie nur das öffentliche Zertifikat der CA hoch.",
"mtlsCertificateDuplicate": "Dieses CA-Zertifikat wurde bereits hinzugefügt.",
"mtlsRemoveCertificate": "CA-Zertifikat entfernen",
"mtlsExpired": "Abgelaufen",
"mtlsExpiresSoon": "Läuft bald ab",
+182 -30
View File
@@ -170,7 +170,9 @@
"siteInfo": "Site Information",
"status": "Status",
"shareTitle": "Manage Shareable Links",
"shareDescription": "Create shareable links to grant temporary or permanent access to proxy resources",
"shareDescription": "Create shareable links to grant temporary or permanent access to public resources",
"shareLinksBannerTitle": "Share Access Without Accounts",
"shareLinksBannerDescription": "Shareable links let you grant temporary or permanent access to a public resource without creating a user. Set an expiration when you create a link, and revoke it anytime from this list.",
"shareSearch": "Search shareable links...",
"shareCreate": "Create Shareable Link",
"shareErrorDelete": "Failed to delete link",
@@ -243,7 +245,7 @@
"resourcePoliciesBannerTitle": "Re-use Authentication and Access Rules",
"resourcePoliciesBannerDescription": "Shared resource policies let you define authentication methods and access rules once, then attach them to multiple public resources. When you update a policy, every linked resource inherits the change automatically.",
"resourcePoliciesBannerButtonText": "Learn More",
"resourcePoliciesTitle": "Manage Public Resource Policies",
"resourcePoliciesTitle": "Manage Resource Policies",
"resourcePoliciesAttachedResourcesColumnTitle": "Resources",
"resourcePoliciesAttachedResources": "{count} resource(s)",
"resourcePoliciesAttachedResourcesCount": "{count, plural, one {# resource} other {# resources}}",
@@ -311,6 +313,8 @@
"privateResourceTypeGatewayDescription": "Send all internet traffic to exit through the site network.",
"resourceTypeGatewayDescription": "Send all internet traffic to exit through the site network.",
"resourceDomainDescription": "The resource will be served at this fully qualified domain name.",
"resourceMatchBaseDomain": "Also match {host}",
"resourceMatchBaseDomainDescription": "A wildcard only matches hostnames under {host}. Enable this to route {host} through this resource as well.",
"resourceHTTPSSettings": "HTTPS Settings",
"resourceHTTPSSettingsDescription": "Configure how the resource will be accessed over HTTPS",
"resourcePortDescription": "The external port on the Pangolin instance or node where the resource will be accessible.",
@@ -372,7 +376,7 @@
"alwaysAllow": "Bypass Auth",
"alwaysDeny": "Block Access",
"passToAuth": "Pass to Auth",
"orgSettingsDescription": "Configure the organization's settings",
"orgSettingsDescription": "Configure settings that apply to the entire organization",
"orgGeneralSettings": "Organization Settings",
"orgGeneralSettingsDescription": "Manage the organization's details and configuration",
"saveGeneralSettings": "Save General Settings",
@@ -410,6 +414,9 @@
"accessUsersManage": "Manage Users",
"accessUserManage": "Manage User",
"accessUsersDescription": "Invite and manage users with access to this organization",
"usersBannerTitle": "Organization Security Policies",
"usersBannerDescription": "Enforce two-factor authentication, session length, and password expiry for users in this organization from security settings.",
"usersBannerButtonText": "Go to Security Settings",
"accessUsersSearch": "Search users...",
"accessUsersRoleFilterCount": "{count, plural, one {# role} other {# roles}}",
"accessUsersRoleFilterClear": "Clear role filters",
@@ -425,6 +432,51 @@
"accessRolesAdd": "Add Role",
"accessRoleDelete": "Delete Role",
"accessApprovalsManage": "Manage Approvals",
"resourceSessionsManage": "Manage Sessions",
"resourceSessionsDescription": "View who currently has an active session on your public resources and revoke access",
"resourceSessionsBannerTitle": "See Who Is Connected",
"resourceSessionsBannerDescription": "Sessions show who currently has access to your public resources, including users and shareable link visitors. Revoke a session here to cut off access immediately and require re-authentication.",
"resourceSessionsSearch": "Search resource sessions...",
"resourceSessionsMethod": "Method",
"resourceSessionsIssued": "Issued",
"resourceSessionsAnonymous": "Anonymous",
"resourceSessionsFilterAnyUser": "All users",
"resourceSessionsFilterAnyActor": "All actors",
"resourceSessionsFilteredToSession": "Showing one session",
"shareLinksNotFound": "No shareable links found",
"resourceSessionsMethodSso": "SSO",
"resourceSessionsMethodPassword": "Password",
"resourceSessionsMethodPincode": "PIN Code",
"resourceSessionsMethodWhitelist": "Email",
"resourceSessionsMethodAccessToken": "Shareable Link",
"resourceSessionsRevoke": "Revoke",
"resourceSessionsRevokeTitle": "Revoke Session",
"resourceSessionsRevokeQuestion": "Revoke the session for {identity} on {resource}? They will need to authenticate again to access the resource.",
"resourceSessionsRevokeSsoNote": "This user is signed in with SSO. All of their sessions for this resource from the same login will be revoked, and they will have to sign in again to access it, even if they are still logged in to the dashboard.",
"resourceSessionsRevokeConfirmPhrase": "revoke",
"resourceSessionsRevoked": "Session revoked",
"resourceSessionsRevokedDescription": "The session has been revoked. It may take a few seconds to take effect.",
"resourceSessionsErrorRevoke": "Failed to revoke session",
"resourceSessionsErrorRevokeDescription": "An error occurred while revoking the session",
"resourceSessionsDetailIp": "IP Address",
"resourceSessionsDetailNetwork": "Network",
"resourceSessionsDetailBrowser": "Browser",
"resourceSessionsDetailDevice": "Device",
"resourceSessionsDetailDeviceDesktop": "Desktop",
"resourceSessionsDetailDeviceMobile": "Mobile",
"resourceSessionsDetailDeviceTablet": "Tablet",
"resourceSessionsDetailDeviceBot": "Bot / Script",
"resourceSessionsDetailUserAgent": "User Agent",
"resourceSessionsDetailSessionId": "Session ID",
"resourceSessionsDetailNotRecorded": "Not recorded",
"resourceSessionsDetailNotRecordedDescription": "Client details are only recorded for sessions created after this feature was added.",
"toggleRowDetails": "Toggle row details",
"timestampLocal": "Local",
"timestampUtc": "UTC",
"timestampRelative": "Relative",
"logLinks": "Links",
"logLinkUser": "User",
"logLinkResourceSession": "Resource Session",
"accessApprovalsDescription": "View and manage pending approvals for access to this organization",
"description": "Description",
"inviteTitle": "Open Invitations",
@@ -477,8 +529,8 @@
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
"provisioningManage": "Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval.",
"provisioningManage": "Manage Provisioning",
"provisioningDescription": "Manage provisioning keys and review pending sites awaiting approval",
"pendingSites": "Pending Sites",
"siteApproveSuccess": "Site and associated resources approved successfully",
"siteApproveError": "Error approving site",
@@ -511,8 +563,8 @@
"provisioningKeysValidUntil": "Valid Until",
"provisioningKeysValidUntilHint": "Leave empty for no expiration.",
"provisioningKeysValidUntilInvalid": "Enter a valid date and time.",
"provisioningKeysNumUsed": "Times used",
"provisioningKeysLastUsed": "Last used",
"provisioningKeysNumUsed": "Times Used",
"provisioningKeysLastUsed": "Last Used",
"provisioningKeysNoExpiry": "No expiration",
"provisioningKeysNeverUsed": "Never",
"provisioningKeysEdit": "Edit Provisioning Key",
@@ -749,7 +801,7 @@
"userSavedDescription": "The user has been updated.",
"autoProvisioned": "Auto Provisioned",
"autoProvisionSettings": "Auto Provision Settings",
"autoProvisionedDescription": "Allow this user to be automatically managed by identity provider",
"autoProvisionedDescription": "If the identity provider has auto-provisioning enabled, allow this user to be automatically managed.",
"accessControlsDescription": "Manage what this user can access and do in the organization",
"accessControlsSubmit": "Save Access Controls",
"singleRolePerUserPlanNotice": "Your plan only supports one role per user.",
@@ -1170,6 +1222,8 @@
"sitestCountIncrease": "Increase site count",
"idpManage": "Manage Identity Providers",
"idpManageDescription": "View and manage identity providers in the system",
"idpBannerTitle": "Sign In With Your Own SSO",
"idpBannerDescription": "Connect an OIDC identity provider so users in this organization can sign in with your existing SSO. Map roles and organizations during login, or require users to authenticate through the provider you configure here.",
"idpGlobalModeBanner": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To enable IdPs per organization, edit the server config and set IdP mode to org. <configDocsLink>See the docs</configDocsLink>. If you want to continue using global IdPs and make this disappear from the organization settings, explicitly set the mode to global in the config.",
"idpGlobalModeBannerUpgradeRequired": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To use identity providers per organization, you must upgrade to the Enterprise edition.",
"idpGlobalModeBannerLicenseRequired": "Identity providers (IdPs) per organization are disabled on this server. It is using global IdPs (shared across all organizations). Manage global IdPs in the <adminPanelLink>admin panel</adminPanelLink>. To use identity providers per organization, an Enterprise license is required.",
@@ -1199,6 +1253,9 @@
"idpAutoProvisionUsers": "Auto Provision Users",
"idpAutoProvisionUsersDescription": "When enabled, users will be automatically created in the system upon first login with the ability to map users to roles and organizations.",
"idpAutoProvisionConfigureAfterCreate": "You can configure auto provision settings once the identity provider is created.",
"idpSessionRevalidation": "Continuous Session Revalidation",
"idpSessionRevalidationDescription": "When enabled, periodically revalidate sessions with this identity provider. If the provider revokes access, linked sessions are ended.",
"idpSessionRevalidationConfigureAfterCreate": "You can configure continuous session revalidation once the identity provider is created.",
"licenseBadge": "EE",
"idpType": "Provider Type",
"idpTypeDescription": "Select the type of identity provider you want to configure",
@@ -1392,6 +1449,7 @@
"idpErrorNotFound": "IdP not found",
"inviteInvalid": "Invalid Invite",
"labels": "Labels",
"manageLabels": "Manage Labels",
"orgLabelsDescription": "Manage labels in this organization.",
"addLabels": "Add labels",
"siteLabelsTab": "Labels",
@@ -1509,6 +1567,10 @@
"actionDeleteOrgDomain": "Delete Domain",
"actionGetDNSRecords": "Get DNS Records",
"actionRestartOrgDomain": "Restart Domain",
"actionListCustomDnsRecords": "List Custom DNS Records",
"actionCreateCustomDnsRecord": "Create Custom DNS Record",
"actionUpdateCustomDnsRecord": "Update Custom DNS Record",
"actionDeleteCustomDnsRecord": "Delete Custom DNS Record",
"actionCreateSite": "Create Site",
"actionDeleteSite": "Delete Site",
"actionGetSite": "Get Site",
@@ -1600,11 +1662,35 @@
"actionGenerateAccessToken": "Generate Access Token",
"actionDeleteAccessToken": "Delete Access Token",
"actionListAccessTokens": "List Access Tokens",
"actionListResourceSessions": "List Resource Sessions",
"actionDeleteResourceSession": "Revoke Resource Session",
"actionCreateResourceSessionToken": "Create Resource Session Token",
"actionCreateResourceRule": "Create Resource Rule",
"actionDeleteResourceRule": "Delete Resource Rule",
"actionListResourceRules": "List Resource Rules",
"actionUpdateResourceRule": "Update Resource Rule",
"actionCreateResourceMtlsCertificate": "Create Resource mTLS Certificate",
"actionDeleteResourceMtlsCertificate": "Delete Resource mTLS Certificate",
"actionListResourceMtlsCertificates": "List Resource mTLS Certificates",
"actionListOrgLabels": "List Labels",
"actionCreateOrgLabel": "Create Label",
"actionUpdateOrgLabel": "Update Label",
"actionDeleteOrgLabel": "Delete Label",
"actionAttachLabelToItem": "Attach Label",
"actionDetachLabelFromItem": "Detach Label",
"actionCreateEventStreamingDestination": "Create Event Streaming Destination",
"actionUpdateEventStreamingDestination": "Update Event Streaming Destination",
"actionDeleteEventStreamingDestination": "Delete Event Streaming Destination",
"actionListEventStreamingDestinations": "List Event Streaming Destinations",
"actionCreateAlertRule": "Create Alert Rule",
"actionUpdateAlertRule": "Update Alert Rule",
"actionDeleteAlertRule": "Delete Alert Rule",
"actionGetAlertRule": "Get Alert Rule",
"actionListAlertRules": "List Alert Rules",
"actionCreateHealthCheck": "Create Health Check",
"actionUpdateHealthCheck": "Update Health Check",
"actionDeleteHealthCheck": "Delete Health Check",
"actionListHealthChecks": "List Health Checks",
"actionListOrgs": "List Organizations",
"actionCheckOrgId": "Check ID",
"actionCreateOrg": "Create Organization",
@@ -1735,8 +1821,14 @@
"sidebarOverview": "Overview",
"sidebarHome": "Home",
"sidebarSites": "Sites",
"sidebarAllSites": "All Sites",
"sidebarAllOrgUsers": "All Users",
"sidebarApprovals": "Approval Requests",
"sidebarResourceSessions": "Sessions",
"sidebarResources": "Resources",
"sidebarAllResources": "All Resources",
"sidebarPrivateResources": "Private Resources",
"sidebarPublicResources": "Public Resources",
"sidebarProxyResources": "Public",
"sidebarClientResources": "Private",
"sidebarPolicies": "Shared Policies",
@@ -1752,10 +1844,10 @@
"sidebarAiGateway": "AI Gateway",
"sidebarAiProviders": "Providers",
"commandAiProviders": "AI Providers",
"sidebarVirtualApiKeys": "Virtual API Keys",
"sidebarVirtualApiKeys": "Virtual Keys",
"sidebarMyApiKeys": "Your API Keys",
"sidebarAccount": "Launcher",
"commandVirtualApiKeys": "Virtual API Keys",
"commandVirtualApiKeys": "Virtual Keys",
"virtualApiKeysTitle": "Manage Virtual API Keys",
"virtualApiKeysDescription": "Create and manage manual API keys for AI Gateway access to public AI gateways",
"virtualApiKeysTabIdentity": "Identity Keys",
@@ -1874,7 +1966,7 @@
"aiClientConfigRevealError": "Could not load your API key.",
"aiClientConfigRevealRetry": "Try again",
"resourceGeneralAiClientConfigDescription": "This resource can be used from common clients like Claude Code and OpenCode. <configLink>Learn more</configLink>",
"aiProvidersTitle": "AI Providers",
"aiProvidersTitle": "Manage AI Providers",
"aiProvidersDescription": "Connect model providers for AI workloads in this organization",
"aiProvidersBannerTitle": "Connect Model Providers",
"aiProvidersBannerDescription": "Providers are the model backends Pangolin uses for AI workloads. Connect OpenAI, Anthropic, and other providers here, then attach them to public AI gateways so users can call models with identity-aware access, budgets, and logging.",
@@ -2186,7 +2278,8 @@
"sidebarOrganization": "Organization",
"sidebarManagement": "Management",
"sidebarBillingAndLicenses": "Billing & Licenses",
"sidebarLogsAnalytics": "Analytics",
"sidebarAnalytics": "Analytics",
"sidebarLogsAnalytics": "HTTP Requests",
"commandSites": "Sites",
"commandActionModeInfo": "Type \">\" To Open Action Mode",
"commandResources": "Resources",
@@ -2205,14 +2298,16 @@
"commandResourcePolicies": "Public Resources Policies",
"commandIdentityProviders": "Identity Providers",
"commandApprovals": "Approval Requests",
"commandResourceSessions": "Resource Sessions",
"commandShareableLinks": "Shareable Links",
"commandOrganization": "Organization",
"commandLogsAndAnalytics": "Logs & Analytics",
"commandLogsAnalytics": "Analytics",
"commandLogsRequest": "HTTP Request Logs",
"commandLogsAccess": "Authentication Logs",
"commandLogsAction": "Admin Action Logs",
"commandLogsAnalytics": "HTTP Requests",
"commandLogsRequest": "HTTP Requests",
"commandLogsAccess": "Authentications",
"commandLogsAction": "Admin Actions",
"commandLogsConnection": "Network Logs",
"commandLogsSsh": "SSH Sessions",
"commandLogsStreaming": "Event Streaming",
"commandManagement": "Management",
"commandAlerting": "Alerting",
@@ -2226,7 +2321,7 @@
"commandLauncher": "Launcher",
"commandResourceLauncher": "Resource Launcher",
"commandSearchResults": "Search Results",
"alertingTitle": "Alerting",
"alertingTitle": "Manage Alerting",
"alertingDescription": "Define sources, triggers, and actions for notifications",
"alertingRules": "Alert rules",
"alertingSearchRules": "Search rules…",
@@ -2391,6 +2486,7 @@
"standaloneHcFilterResourceIdFallback": "Resource {id}",
"blueprints": "Blueprints",
"blueprintsLog": "Blueprints Log",
"manageBlueprints": "Manage Blueprints",
"blueprintsDescription": "View past blueprint applications and their results or apply a new blueprint",
"blueprintAdd": "Add Blueprint",
"blueprintGoBack": "See all Blueprints",
@@ -2421,6 +2517,7 @@
"siteAutoUpdate": "Site Auto-Update",
"siteAutoUpdateLabel": "Enable Auto-Update",
"siteAutoUpdateDescription": "When enabled, this site's connector will automatically download the latest version and restart itself.",
"siteAutoUpdateOrgSettingsHelp": "To enable auto-update for all sites, toggle it in <orgSettingsLink>organization general settings</orgSettingsLink>.",
"siteAutoUpdateOrgDefault": "Organization default: {state}",
"siteAutoUpdateOverriding": "Overriding organization setting",
"siteAutoUpdateResetToOrg": "Reset to Organization Default",
@@ -2474,7 +2571,10 @@
"loadingAnalytics": "Loading Analytics",
"restart": "Restart",
"domains": "Domains",
"manageDomains": "Manage Domains",
"domainsDescription": "Create and manage domains available in the organization",
"domainsBannerTitle": "Bring Your Own Domains",
"domainsBannerDescription": "Add custom domains so Pangolin can issue certificates and route resources. After creating a domain, configure the required DNS records at your registrar to verify ownership.",
"domainsSearch": "Search domains...",
"domainsNotFound": "No domains found",
"domainAdd": "Add Domain",
@@ -3435,7 +3535,7 @@
"enterpriseEdition": "Enterprise Edition",
"unlicensed": "Unlicensed",
"beta": "Beta",
"manageUserDevices": "User Devices",
"manageUserDevices": "Manage User Devices",
"manageUserDevicesDescription": "View and manage devices that users use to privately connect to resources",
"downloadClientBannerTitle": "Download Pangolin Client",
"downloadClientBannerDescription": "Download the Pangolin client for your system to connect to the Pangolin network and access resources privately.",
@@ -3618,6 +3718,7 @@
"requests": "Requests",
"logs": "Logs",
"logsSettingsDescription": "Monitor logs collected from this organization",
"logsEmptyState": "No logs found. Try adjusting your filters or date range.",
"searchLogs": "Search logs...",
"action": "Action",
"actor": "Actor",
@@ -3648,25 +3749,25 @@
"ip": "IP",
"reason": "Reason",
"requestLogs": "HTTP Request Logs",
"requestAnalytics": "Request Analytics",
"requestAnalytics": "HTTP Analytics",
"host": "Host",
"location": "Location",
"actionLogs": "Admin Action Logs",
"sidebarLogsRequest": "HTTP Request Logs",
"sidebarLogsAccess": "Authentication Logs",
"sidebarLogsAction": "Admin Action Logs",
"sidebarLogsRequest": "HTTP Requests",
"sidebarLogsAccess": "Authentications",
"sidebarLogsAction": "Admin Actions",
"logRetention": "Log Retention",
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
"logRetentionDisabledWarningTitle": "Log Retention Disabled",
"logRetentionDisabledWarningDescription": "{logType} are not being retained for this organization, so new activity will not appear here. Enable retention in security settings to start collecting these logs.",
"logRetentionDisabledWarningButton": "Go to Security Settings",
"logRetentionDisabledWarningDescription": "{logType} are not being retained for this organization, so new activity will not appear here. Enable retention in log settings to start collecting these logs.",
"logRetentionDisabledWarningButton": "Go to Log Settings",
"requestLogsDescription": "View detailed request logs for HTTPS resources in this organization",
"aiSessionLogs": "AI Gateway Session Logs",
"aiSessionLogsDescription": "View prompt and response transcripts for AI gateway requests in this organization",
"sidebarLogsAi": "Session Logs",
"commandLogsAi": "Session Logs",
"sidebarLogsAiUsage": "Usage Analytics",
"commandLogsAiUsage": "Usage Analytics",
"sidebarLogsAi": "AI Sessions",
"commandLogsAi": "AI Sessions",
"sidebarLogsAiUsage": "AI Usage",
"commandLogsAiUsage": "AI Usage",
"provider": "Provider",
"capability": "Capability",
"model": "Model",
@@ -3704,6 +3805,8 @@
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionAISessionsLabel": "AI Gateway Session Log Retention",
"logRetentionAISessionsDescription": "How long to retain AI gateway prompt/response session logs",
"logRetentionSshLabel": "SSH Session Log Retention",
"logRetentionSshDescription": "How long to retain SSH session and command logs",
"logRetentionDisabled": "Disabled",
"logRetention3Days": "3 days",
"logRetention7Days": "7 days",
@@ -3717,6 +3820,27 @@
"connectionLogs": "Network Logs",
"connectionLogsDescription": "View network session logs handled by sites in this organization",
"sidebarLogsConnection": "Network Logs",
"sidebarLogsSsh": "SSH Sessions",
"sshSessionLogs": "SSH Session Logs",
"sshSessionLogsDescription": "View SSH session history and command transcripts",
"sshSystemUser": "System User",
"sshCommands": "Commands",
"sshSessionId": "Session ID",
"sshAuthMethod": "Auth Method",
"sshTransport": "Connection",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "Browser",
"sshKeyFingerprint": "Key Fingerprint",
"sshCertKeyId": "Certificate Key ID",
"sshStartedAt": "Started At",
"sshEndedAt": "Ended At",
"sshSessionLoadingCommands": "Loading commands...",
"sshSessionCommandsError": "Failed to load commands for this session",
"sshSessionNoCommands": "No commands recorded for this session",
"sshSessionRecalledCommand": "(recalled from shell history)",
"sshSessionInexactNotice": "Some commands were edited with history recall or tab completion and may not exactly match what the shell ran.",
"sshSessionInexactTooltip": "Edited with history recall or tab completion; may not exactly match what the shell ran",
"sshSessionCommandsTruncated": "Showing the first {shown} of {total} commands. Export the logs to see all of them.",
"sidebarLogsStreaming": "Event Streaming",
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
@@ -3737,6 +3861,32 @@
"TTL": "TTL",
"howToAddRecords": "How to Add Records",
"dnsRecord": "DNS Records",
"domainTabSetup": "Setup",
"domainTabRecords": "Records",
"domainSetupRecordsDescription": "Create these records at your domain registrar or current DNS provider. Pangolin uses them to verify the domain and route its traffic.",
"domainRecordsInfoTitle": "Records served by Pangolin",
"domainRecordsInfoDescription": "Pangolin's nameservers answer DNS queries for {domain}. Records you add here are served directly from Pangolin, so you don't need to create them at your registrar.",
"customDnsRecords": "Custom DNS Records",
"customDnsRecordsAdd": "Add Record",
"customDnsRecordsSearch": "Search records...",
"customDnsRecordsUnverifiedTitle": "Records are not being served yet",
"customDnsRecordsUnverifiedDescription": "This domain hasn't been verified. You can add records now, but Pangolin's nameservers won't serve them until the domain's nameservers point to Pangolin and verification completes.",
"customDnsRecordCreate": "Add DNS Record",
"customDnsRecordEdit": "Edit DNS Record",
"customDnsRecordFormDescription": "This record will be served for {domain}. Changes may take a few minutes to propagate.",
"customDnsRecordNameDescription": "Use @ for the root of the domain or * as the first label for a wildcard.",
"customDnsRecordValueDescription": "Enter one value per line, e.g. both nameservers of an NS record.",
"customDnsRecordPriority": "Priority",
"customDnsRecordWeight": "Weight",
"customDnsRecordPort": "Port",
"customDnsRecordCreated": "DNS record created",
"customDnsRecordUpdated": "DNS record updated",
"customDnsRecordSaveError": "Failed to save DNS record",
"customDnsRecordDeleted": "DNS record deleted",
"customDnsRecordDeleteError": "Failed to delete DNS record",
"customDnsRecordDeleteTitle": "Delete DNS Record",
"customDnsRecordDeleteQuestion": "Are you sure you want to delete the {type} record for {name}? It will stop resolving once cached answers expire.",
"customDnsRecordDeleteConfirm": "Delete Record",
"required": "Required",
"domainSettingsUpdated": "Domain settings updated successfully",
"orgOrDomainIdMissing": "Organization or Domain ID is missing",
@@ -4121,7 +4271,7 @@
"domainErrorTitle": "We are having trouble verifying your domain",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab.",
"streamingTitle": "Event Streaming",
"streamingDescription": "Stream events from your organization to external destinations in real time.",
"streamingDescription": "Stream events from your organization to external destinations in real time",
"streamingUnnamedDestination": "Unnamed destination",
"streamingNoUrlConfigured": "No URL configured",
"streamingAddDestination": "Add Destination",
@@ -4512,7 +4662,9 @@
"actionUpdateSiteApprovals": "Update Site Approvals",
"check": "Check",
"redirectsTitle": "Manage Redirects",
"redirectsDescription": "Forward requests from a path on your domains or resources to another URL.",
"redirectsDescription": "Forward requests from a path on your domains or resources to another URL",
"redirectsBannerTitle": "Forward Traffic Anywhere",
"redirectsBannerDescription": "Redirects send visitors from a path on your domains or public resources to another URL. Use them for vanity links, legacy path migrations, or pointing traffic to an external site.",
"redirectsSearch": "Search redirects...",
"redirectAdd": "Add Redirect",
"redirectSource": "Source",
+127 -3
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "Enviar todo el tráfico de internet para salir a través de la red del sitio.",
"resourceTypeGatewayDescription": "Envía todo el tráfico de internet para salir a través de la red del sitio.",
"resourceDomainDescription": "El recurso se servirá en este nombre de dominio completamente calificado.",
"resourceMatchBaseDomain": "Coincidir también {host}",
"resourceMatchBaseDomainDescription": "Un comodín solo coincide con los nombres de host bajo {host}. Habilita esto para enrutar {host} a través de este recurso también.",
"resourceHTTPSSettings": "Configuración HTTPS",
"resourceHTTPSSettingsDescription": "Configurar cómo se accederá al recurso a través de HTTPS",
"resourcePortDescription": "El puerto externo en la instancia o nodo de Pangolin donde el recurso será accesible.",
@@ -425,6 +427,31 @@
"accessRolesAdd": "Añadir rol",
"accessRoleDelete": "Eliminar rol",
"accessApprovalsManage": "Administrar aprobaciones",
"resourceSessionsManage": "Administrar sesiones de recursos",
"resourceSessionsDescription": "Ver quién tiene actualmente una sesión activa en sus recursos públicos y revocar el acceso",
"resourceSessionsSearch": "Buscar por usuario, correo electrónico, enlace o recurso...",
"resourceSessionsMethod": "Método",
"resourceSessionsIssued": "Emitido",
"resourceSessionsAnonymous": "Anónimo",
"resourceSessionsFilterAnyUser": "Todos los usuarios",
"resourceSessionsFilteredToSession": "Mostrando una sesión",
"resourceSessionsMethodSso": "SSO",
"resourceSessionsMethodPassword": "Contraseña",
"resourceSessionsMethodPincode": "Código PIN",
"resourceSessionsMethodWhitelist": "E-mail",
"resourceSessionsMethodAccessToken": "Enlace Compartible",
"resourceSessionsRevoke": "Revocar",
"resourceSessionsRevokeTitle": "Revocar Sesión",
"resourceSessionsRevokeQuestion": "¿Revocar la sesión para {identity} en {resource}? Necesitarán autenticarse nuevamente para acceder al recurso.",
"resourceSessionsRevokeSsoNote": "Este usuario ha iniciado sesión con SSO. Todas sus sesiones para este recurso desde el mismo inicio de sesión serán revocadas, y deberán iniciar sesión de nuevo para acceder, incluso si aún están conectados a Pangolin.",
"resourceSessionsRevoked": "Sesión revocada",
"resourceSessionsRevokedDescription": "La sesión ha sido revocada. Puede tardar unos segundos en aplicarse.",
"resourceSessionsErrorRevoke": "Error al revocar sesión",
"resourceSessionsErrorRevokeDescription": "Se produjo un error al revocar la sesión",
"resourceSessionReauthRequired": "Su sesión para este recurso fue terminada por un administrador. Por favor inicie sesión nuevamente para continuar.",
"logLinks": "Enlaces",
"logLinkUser": "Usuario",
"logLinkResourceSession": "Sesión de recurso",
"accessApprovalsDescription": "Ver y administrar aprobaciones pendientes para el acceso a esta organización",
"description": "Descripción",
"inviteTitle": "Invitaciones abiertas",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "No hay límite en el número de usuarios para este host.",
"licenseUsersUsedMax": "{usedUsers} de {maxUsers} usuarios usados",
"licenseUsersUsed": "{count, plural, =0 {# usuarios} one {# usuario} other {# usuarios}}",
"licenseUsageMachines": "Clientes de la máquina",
"licenseNoMachineLimit": "No hay límite en el número de clientes de máquina para este host.",
"licenseMachinesUsedMax": "{usedMachines} de {maxMachines} clientes de máquina usados",
"licenseMachinesUsed": "{count, plural, =0 {# clientes de máquina} one {# cliente de máquina} other {# clientes de máquina}}",
"licenseUnlimited": "Ilimitado",
"licenseTierLabel": "Categoría",
"licensePurchaseDescription": "Elige cuántos sitios quieres {selectedMode, select, license {compra una licencia para. Siempre puedes añadir más sitios más tarde.} other {añadir a tu licencia existente.}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "El usuario ha sido actualizado.",
"autoProvisioned": "Auto asegurado",
"autoProvisionSettings": "Configuración de Auto Provision",
"autoProvisionedDescription": "Permitir a este usuario ser administrado automáticamente por el proveedor de identidad",
"autoProvisionedDescription": "Si el proveedor de identidad tiene habilitada la autoaprovisionamiento, permite que este usuario sea gestionado automáticamente.",
"accessControlsDescription": "Administrar lo que este usuario puede acceder y hacer en la organización",
"accessControlsSubmit": "Guardar controles de acceso",
"singleRolePerUserPlanNotice": "Tu plan sólo soporta un rol por usuario.",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "Distribuir solicitudes uniformemente entre los objetivos en rotación.",
"loadBalancingStrategy_p2c": "Poder de Dos Opciones",
"loadBalancingStrategy_p2c_description": "Escoge dos objetivos al azar y envía la solicitud al que tenga menos conexiones activas.",
"loadBalancingStrategy_hrw": "Hash de IP del Cliente",
"loadBalancingStrategy_hrw": "Mayor peso aleatorio",
"loadBalancingStrategy_hrw_description": "Hashea la IP del cliente para que el mismo cliente sea dirigido constantemente al mismo objetivo.",
"loadBalancingStrategy_leasttime": "Menor Tiempo de Respuesta",
"loadBalancingStrategy_leasttime_description": "Envía solicitudes al objetivo con el tiempo de respuesta más rápido y menos conexiones activas.",
@@ -829,6 +860,8 @@
"save": "Guardar",
"proxyAdditional": "Ajustes adicionales del proxy",
"proxyAdditionalDescription": "Configurar cómo maneja el recurso la configuración del proxy",
"proxyForwardIdentityHeaders": "Encabezados de identidad enviados al proxy",
"proxyForwardIdentityHeadersDescription": "Enviar la identidad del usuario autenticado al backend en los encabezados Remote-User, Remote-Email, Remote-Name, Remote-Role y Remote-User-Id. Desactivar para backends que rechazan solicitudes con encabezados no reconocidos.",
"proxyCustomHeader": "Cabecera de host personalizada",
"proxyCustomHeaderDescription": "La cabecera del host a establecer cuando se realizan peticiones de reemplazo. Deje en blanco para usar el valor predeterminado.",
"proxyAdditionalSubmit": "Guardar ajustes de proxy",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "Autenticación Básica del Encabezado",
"policyAuthHeaderAuthDescription": "Valida un nombre y valor de encabezado HTTP personalizado en cada petición",
"policyAuthHeaderAuthSummary": "Encabezado configurado",
"policyAuthMtlsTitle": "Certificado de cliente (mTLS)",
"policyAuthMtlsDescription": "Requiere un certificado de cliente firmado por una CA de confianza durante el intercambio TLS",
"policyAuthMtlsSummary": "{count} certificados CA de confianza",
"policyAuthHeaderName": "Usuario",
"policyAuthHeaderValue": "Contraseña",
"policyAuthSetPasscode": "Establecer Código de Acceso",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "Auto-Provisión de Usuarios",
"idpAutoProvisionUsersDescription": "Cuando está habilitado, los usuarios serán creados automáticamente en el sistema al iniciar sesión con la capacidad de asignar a los usuarios a roles y organizaciones.",
"idpAutoProvisionConfigureAfterCreate": "Puede configurar las configuraciones de provisión automática una vez que se haya creado el proveedor de identidad.",
"idpSessionRevalidation": "Revalidación continua de sesión",
"idpSessionRevalidationDescription": "Cuando está habilitado, se revalidan periódicamente las sesiones con este proveedor de identidad. Si el proveedor revoca el acceso, las sesiones vinculadas son finalizadas.",
"idpSessionRevalidationConfigureAfterCreate": "Puede configurar la revalidación continua de sesión una vez creado el proveedor de identidad.",
"licenseBadge": "EE",
"idpType": "Tipo de proveedor",
"idpTypeDescription": "Seleccione el tipo de proveedor de identidad que desea configurar",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "Eliminar dominio",
"actionGetDNSRecords": "Obtener registros DNS",
"actionRestartOrgDomain": "Reiniciar dominio",
"actionListCustomDnsRecords": "Listar registros DNS personalizados",
"actionCreateCustomDnsRecord": "Crear registro DNS personalizado",
"actionUpdateCustomDnsRecord": "Actualizar registro DNS personalizado",
"actionDeleteCustomDnsRecord": "Eliminar registro DNS personalizado",
"actionCreateSite": "Crear sitio",
"actionDeleteSite": "Eliminar sitio",
"actionGetSite": "Obtener sitio",
"actionListSites": "Listar sitios",
"actionReGenerateSecret": "Regenerar credenciales",
"actionCreateAiProvider": "Crear Proveedor de IA",
"actionDeleteAiProvider": "Eliminar Proveedor de IA",
"actionGetAiProvider": "Obtener Proveedor de IA",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "Generar token de acceso",
"actionDeleteAccessToken": "Eliminar token de acceso",
"actionListAccessTokens": "Lista de Tokens de Acceso",
"actionListResourceSessions": "Listar sesiones de recursos",
"actionDeleteResourceSession": "Revocar sesión de recurso",
"actionCreateResourceSessionToken": "Crear Token de Sesión de Recurso",
"actionCreateResourceRule": "Crear Regla de Recursos",
"actionDeleteResourceRule": "Eliminar Regla de Recurso",
"actionListResourceRules": "Lista de Reglas de Recursos",
"actionUpdateResourceRule": "Actualizar regla de recursos",
"actionCreateResourceMtlsCertificate": "Crear certificado mTLS de recurso",
"actionDeleteResourceMtlsCertificate": "Eliminar certificado mTLS de recurso",
"actionListResourceMtlsCertificates": "Listar certificados mTLS de recurso",
"actionListOrgLabels": "Listar etiquetas",
"actionCreateOrgLabel": "Crear etiqueta",
"actionUpdateOrgLabel": "Actualizar etiqueta",
"actionDeleteOrgLabel": "Eliminar etiqueta",
"actionAttachLabelToItem": "Adjuntar etiqueta",
"actionDetachLabelFromItem": "Desvincular etiqueta",
"actionCreateEventStreamingDestination": "Crear destino de transmisión de eventos",
"actionUpdateEventStreamingDestination": "Actualizar destino de transmisión de eventos",
"actionDeleteEventStreamingDestination": "Eliminar destino de transmisión de eventos",
"actionListEventStreamingDestinations": "Listar destinos de transmisión de eventos",
"actionCreateAlertRule": "Crear regla de alerta",
"actionUpdateAlertRule": "Actualizar regla de alerta",
"actionDeleteAlertRule": "Eliminar regla de alerta",
"actionGetAlertRule": "Obtener regla de alerta",
"actionListAlertRules": "Listar reglas de alerta",
"actionCreateHealthCheck": "Crear chequeo de salud",
"actionUpdateHealthCheck": "Actualizar chequeo de salud",
"actionDeleteHealthCheck": "Eliminar chequeo de salud",
"actionListHealthChecks": "Listar chequeos de salud",
"actionListOrgs": "Listar organizaciones",
"actionCheckOrgId": "Comprobar ID",
"actionCreateOrg": "Crear organización",
@@ -1726,6 +1794,7 @@
"sidebarHome": "Inicio",
"sidebarSites": "Sitios",
"sidebarApprovals": "Solicitudes de aprobación",
"sidebarResourceSessions": "Sesiones de recurso",
"sidebarResources": "Recursos",
"sidebarProxyResources": "Público",
"sidebarClientResources": "Privado",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "Políticas de Recursos Públicos",
"commandIdentityProviders": "Proveedores de identidad",
"commandApprovals": "Solicitudes de Aprobación",
"commandResourceSessions": "Sesiones de recurso",
"commandShareableLinks": "Enlaces compartibles",
"commandOrganization": "Organización",
"commandLogsAndAnalytics": "Registros y Análisis",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "Registros de acceso",
"commandLogsAction": "Registros de acción de administrador",
"commandLogsConnection": "Registros de conexión",
"commandLogsSsh": "Registros de sesión SSH",
"commandLogsStreaming": "Transmisión de Eventos",
"commandManagement": "Gestión",
"commandAlerting": "Alertas",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "Cuánto tiempo conservar los registros de conexión",
"logRetentionAISessionsLabel": "Retención de Registro de Sesiones del Portal de IA",
"logRetentionAISessionsDescription": "Cuánto tiempo retener los registros de sesión de solicitud/respuesta del portal de IA",
"logRetentionSshLabel": "Retención de registros de sesión SSH",
"logRetentionSshDescription": "Cuánto tiempo conservar registros de sesión y comandos de SSH",
"logRetentionDisabled": "Deshabilitado",
"logRetention3Days": "3 días",
"logRetention7Days": "7 días",
@@ -3707,6 +3780,27 @@
"connectionLogs": "Registros de conexión",
"connectionLogsDescription": "Ver registros de conexión para túneles en esta organización",
"sidebarLogsConnection": "Registros de conexión",
"sidebarLogsSsh": "Registros de sesión SSH",
"sshSessionLogs": "Registros de sesión SSH",
"sshSessionLogsDescription": "Ver sesiones SSH y los comandos ejecutados en ellas en sitios de esta organización",
"sshSystemUser": "Usuario del sistema",
"sshCommands": "Comandos",
"sshSessionId": "ID de Sesión",
"sshAuthMethod": "Método de autenticación",
"sshTransport": "Conexión",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "Navegador",
"sshKeyFingerprint": "Huella digital de clave",
"sshCertKeyId": "ID de Clave de Certificado",
"sshStartedAt": "Iniciado el",
"sshEndedAt": "Finalizado el",
"sshSessionLoadingCommands": "Cargando comandos...",
"sshSessionCommandsError": "Error al cargar comandos para esta sesión",
"sshSessionNoCommands": "No se grabaron comandos para esta sesión",
"sshSessionRecalledCommand": "(recordado desde el historial del shell)",
"sshSessionInexactNotice": "Algunos comandos fueron editados con el recuerdo del historial o la finalización automática y pueden no coincidir exactamente con lo que ejecutó el shell.",
"sshSessionInexactTooltip": "Editado con recuerdo del historial o finalización automática; puede que no coincida exactamente con lo que ejecutó el shell",
"sshSessionCommandsTruncated": "Mostrándose los primeros {shown} de {total} comandos. Exporta los registros para ver todos ellos.",
"sidebarLogsStreaming": "Transmisión",
"sourceAddress": "Dirección de origen",
"destinationAddress": "Dirección de destino",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "Cómo añadir registros",
"dnsRecord": "Registros DNS",
"domainTabSetup": "Configuración",
"domainTabRecords": "Registros",
"domainSetupRecordsDescription": "Cree estos registros en su registrador de dominios o proveedor de DNS actual. Pangolin los utiliza para verificar el dominio y dirigir su tráfico.",
"domainRecordsInfoTitle": "Registros servidos por Pangolin",
"domainRecordsInfoDescription": "Los servidores de nombres de Pangolin responden consultas DNS para {domain}. Los registros que añada aquí se sirven directamente desde Pangolin, por lo que no necesita crearlos en su registrador.",
"customDnsRecords": "Registros DNS personalizados",
"customDnsRecordsAdd": "Agregar registro",
"customDnsRecordsSearch": "Buscar registros...",
"customDnsRecordsUnverifiedTitle": "Los registros aún no están siendo servidos",
"customDnsRecordsUnverifiedDescription": "Este dominio no ha sido verificado. Puede agregar registros ahora, pero los servidores de nombres de Pangolin no los servirán hasta que los servidores de nombres del dominio apunten a Pangolin y la verificación se complete.",
"customDnsRecordCreate": "Agregar registro DNS",
"customDnsRecordEdit": "Editar registro DNS",
"customDnsRecordFormDescription": "Este registro se servirá para {domain}. Los cambios pueden tardar unos minutos en propagarse.",
"customDnsRecordNameDescription": "Use @ para la raíz del dominio o * como la primera etiqueta para un comodín.",
"customDnsRecordValueDescription": "Ingrese un valor por línea, por ejemplo, ambos servidores de nombres de un registro NS.",
"customDnsRecordPriority": "Prioridad",
"customDnsRecordWeight": "Peso",
"customDnsRecordPort": "Puerto",
"customDnsRecordCreated": "Registro DNS creado",
"customDnsRecordUpdated": "Registro DNS actualizado",
"customDnsRecordSaveError": "Error al guardar el registro DNS",
"customDnsRecordDeleted": "Registro DNS eliminado",
"customDnsRecordDeleteError": "Error al eliminar el registro DNS",
"customDnsRecordDeleteTitle": "Eliminar registro DNS",
"customDnsRecordDeleteQuestion": "¿Está seguro de que desea eliminar el registro {type} para {name}? Dejará de resolverse una vez que expiren las respuestas en caché.",
"customDnsRecordDeleteConfirm": "Eliminar registro",
"required": "Requerido",
"domainSettingsUpdated": "Configuración de dominio actualizada correctamente",
"orgOrDomainIdMissing": "Falta el ID de organización o dominio",
@@ -3938,7 +4058,7 @@
"backToHome": "Volver a inicio",
"needToSignInToOrg": "¿Necesita usar el proveedor de identidad de su organización?",
"mtls": "mTLS",
"mtlsCaCertificates": "Certificados de CA de confianza",
"mtlsCaCertificates": "Establecer certificados de cliente",
"mtlsCaCertificatesDescription": "Los clientes deben presentar un certificado firmado por una de estas autoridades de certificación para conectarse. Sube solo certificados públicos de CA, nunca una clave privada.",
"mtlsCertificateName": "Nombre (opcional)",
"mtlsCertificateNamePlaceholder": "CA del dispositivo corporativo",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "El certificado no pudo ser eliminado.",
"mtlsLastCertificateRemoved": "mTLS fue desactivado porque se eliminó el último certificado CA.",
"mtlsNoCertificates": "Aún no se han agregado certificados CA",
"mtlsPendingCertificate": "Certificado CA {number}",
"mtlsCertificateInvalidPem": "No se encontró un certificado codificado PEM. Se esperaba un bloque que comenzara con -----BEGIN CERTIFICATE-----.",
"mtlsCertificatePrivateKey": "Esto parece una clave privada, no un certificado. Suba solo el certificado público del CA.",
"mtlsCertificateDuplicate": "Este certificado CA ya ha sido añadido.",
"mtlsRemoveCertificate": "Eliminar certificado CA",
"mtlsExpired": "Caducado",
"mtlsExpiresSoon": "Caducará pronto",
+127 -3
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "Envoyez tout le trafic Internet pour sortir par le réseau du site.",
"resourceTypeGatewayDescription": "Envoyez tout le trafic Internet pour sortir par le réseau du site.",
"resourceDomainDescription": "La ressource sera servie à ce nom de domaine pleinement qualifié.",
"resourceMatchBaseDomain": "Faire correspondre également {host}",
"resourceMatchBaseDomainDescription": "Un caractère générique ne correspond qu'aux noms d'hôte sous {host}. Activez ceci pour router également {host} à travers cette ressource.",
"resourceHTTPSSettings": "Paramètres HTTPS",
"resourceHTTPSSettingsDescription": "Configurer comment la ressource sera accédée via HTTPS",
"resourcePortDescription": "Le port externe sur l'instance ou nœud Pangolin où la ressource sera accessible.",
@@ -425,6 +427,31 @@
"accessRolesAdd": "Ajouter un rôle",
"accessRoleDelete": "Supprimer le rôle",
"accessApprovalsManage": "Gérer les approbations",
"resourceSessionsManage": "Gérer les sessions de ressources",
"resourceSessionsDescription": "Voir qui a actuellement une session active sur vos ressources publiques et révoquer l'accès",
"resourceSessionsSearch": "Recherche par utilisateur, email, lien ou ressource...",
"resourceSessionsMethod": "Méthode",
"resourceSessionsIssued": "Émis",
"resourceSessionsAnonymous": "Anonyme",
"resourceSessionsFilterAnyUser": "Tous les Utilisateurs",
"resourceSessionsFilteredToSession": "Affichage d'une session",
"resourceSessionsMethodSso": "SSO",
"resourceSessionsMethodPassword": "Mot de passe",
"resourceSessionsMethodPincode": "Code PIN",
"resourceSessionsMethodWhitelist": "Adresse mail",
"resourceSessionsMethodAccessToken": "Lien partageable",
"resourceSessionsRevoke": "Révoquer",
"resourceSessionsRevokeTitle": "Révoquer la session",
"resourceSessionsRevokeQuestion": "Révoquer la session pour {identity} sur {resource}? Ils devront s'authentifier à nouveau pour accéder à la ressource.",
"resourceSessionsRevokeSsoNote": "Cet utilisateur est connecté avec SSO. Toutes leurs sessions pour cette ressource depuis la même connexion seront révoquées, et ils devront se reconnecter pour y accéder, même s'ils sont encore connectés à Pangolin.",
"resourceSessionsRevoked": "Session révoquée",
"resourceSessionsRevokedDescription": "La session a été révoquée. Cela peut prendre quelques secondes pour prendre effet.",
"resourceSessionsErrorRevoke": "Échec de la révocation de la session",
"resourceSessionsErrorRevokeDescription": "Une erreur s'est produite lors de la révocation de la session",
"resourceSessionReauthRequired": "Votre session pour cette ressource a été terminée par un administrateur. Veuillez vous reconnecter pour continuer.",
"logLinks": "Liens",
"logLinkUser": "Utilisateur",
"logLinkResourceSession": "Session de ressource",
"accessApprovalsDescription": "Voir et gérer les approbations en attente pour accéder à cette organisation",
"description": "Libellé",
"inviteTitle": "Invitations actives",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "Il n'y a pas de limite sur le nombre d'utilisateurs pour cet hôte.",
"licenseUsersUsedMax": "{usedUsers} des {maxUsers} utilisateurs utilisés",
"licenseUsersUsed": "{count, plural, =0 {# utilisateurs} one {# utilisateur} other {# utilisateurs}}",
"licenseUsageMachines": "Clients Machines",
"licenseNoMachineLimit": "Il n'y a pas de limite sur le nombre de clients machines pour cet hôte.",
"licenseMachinesUsedMax": "{usedMachines} de {maxMachines} clients machines utilisés",
"licenseMachinesUsed": "{count, plural, =0 {# clients machine} one {# client machine} other {# clients machine}}",
"licenseUnlimited": "Illimité",
"licenseTierLabel": "Niveau",
"licensePurchaseDescription": "Choisissez le nombre de sites que vous voulez {selectedMode, select, license {achetez une licence. Vous pouvez toujours ajouter plus de sites plus tard.} other {ajouter à votre licence existante.}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "L'utilisateur a été mis à jour.",
"autoProvisioned": "Auto-provisionné",
"autoProvisionSettings": "Paramètres de la fourniture automatique",
"autoProvisionedDescription": "Permettre à cet utilisateur d'être géré automatiquement par le fournisseur d'identité",
"autoProvisionedDescription": "Si le fournisseur d'identité a activé l'approvisionnement automatique, permettez à cet utilisateur d'être géré automatiquement.",
"accessControlsDescription": "Gérer ce que cet utilisateur peut accéder et faire dans l'organisation",
"accessControlsSubmit": "Enregistrer les contrôles d'accès",
"singleRolePerUserPlanNotice": "Votre plan ne prend en charge qu'un seul rôle par utilisateur.",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "Distribuez les requêtes uniformément entre les cibles en rotation.",
"loadBalancingStrategy_p2c": "Puissance de deux choix",
"loadBalancingStrategy_p2c_description": "Choisissez deux cibles au hasard et envoyez la demande à celle qui a le moins de connexions actives.",
"loadBalancingStrategy_hrw": "Hachage de l'adresse IP du client",
"loadBalancingStrategy_hrw": "Poids aléatoire le plus élevé",
"loadBalancingStrategy_hrw_description": "Hachez l'IP du client pour que le même client soit systématiquement routé vers la même cible.",
"loadBalancingStrategy_leasttime": "Temps de réponse le plus court",
"loadBalancingStrategy_leasttime_description": "Envoyer les demandes à la cible avec le temps de réponse le plus rapide et le moins de connexions actives.",
@@ -829,6 +860,8 @@
"save": "Enregistrer",
"proxyAdditional": "Paramètres de proxy supplémentaires",
"proxyAdditionalDescription": "Configurer comment la ressource gère les paramètres du proxy",
"proxyForwardIdentityHeaders": "Transférer les en-têtes d'identité",
"proxyForwardIdentityHeadersDescription": "Envoyez l'identité de l'utilisateur authentifié au backend dans les en-têtes Remote-User, Remote-Email, Remote-Name, Remote-Role et Remote-User-Id. Désactiver pour les backends qui rejettent les requêtes avec des en-têtes non reconnus.",
"proxyCustomHeader": "En-tête Host personnalisé",
"proxyCustomHeaderDescription": "L'en-tête host à définir lors du proxy des requêtes. Laissez vide pour utiliser la valeur par défaut.",
"proxyAdditionalSubmit": "Enregistrer les paramètres de proxy",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "Authentification de l'en-tête de base",
"policyAuthHeaderAuthDescription": "Validez un nom et une valeur d'en-tête HTTP personnalisé à chaque requête",
"policyAuthHeaderAuthSummary": "En-tête configuré",
"policyAuthMtlsTitle": "Certificat Client (mTLS)",
"policyAuthMtlsDescription": "Exiger un certificat client signé par une CA de confiance lors de l'échange TLS",
"policyAuthMtlsSummary": "{count} certificats CA de confiance",
"policyAuthHeaderName": "Nom d'utilisateur",
"policyAuthHeaderValue": "Mot de passe",
"policyAuthSetPasscode": "Définir le code confidentiel",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "Approvisionnement automatique des utilisateurs",
"idpAutoProvisionUsersDescription": "Lorsque cette option est activée, les utilisateurs seront automatiquement créés dans le système lors de leur première connexion avec la possibilité de mapper les utilisateurs aux rôles et aux organisations.",
"idpAutoProvisionConfigureAfterCreate": "Vous pouvez configurer les paramètres de provisionnement automatique une fois le fournisseur d'identités créé.",
"idpSessionRevalidation": "Revalidation continue de la session",
"idpSessionRevalidationDescription": "Lorsqu'elle est activée, elle revalide périodiquement les sessions avec ce fournisseur d'identités. Si le fournisseur révoque l'accès, les sessions associées sont terminées.",
"idpSessionRevalidationConfigureAfterCreate": "Vous pouvez configurer la revalidation continue de la session une fois le fournisseur d'identités créé.",
"licenseBadge": "EE",
"idpType": "Type de fournisseur",
"idpTypeDescription": "Sélectionnez le type de fournisseur d'identité que vous souhaitez configurer",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "Supprimer le domaine",
"actionGetDNSRecords": "Récupérer les enregistrements DNS",
"actionRestartOrgDomain": "Redémarrer le domaine",
"actionListCustomDnsRecords": "Lister les enregistrements DNS personnalisés",
"actionCreateCustomDnsRecord": "Créer un enregistrement DNS personnalisé",
"actionUpdateCustomDnsRecord": "Mettre à jour l'enregistrement DNS personnalisé",
"actionDeleteCustomDnsRecord": "Supprimer l'enregistrement DNS personnalisé",
"actionCreateSite": "Créer un site",
"actionDeleteSite": "Supprimer un site",
"actionGetSite": "Obtenir un site",
"actionListSites": "Lister les sites",
"actionReGenerateSecret": "Régénérer les identifiants",
"actionCreateAiProvider": "Créer un fournisseur AI",
"actionDeleteAiProvider": "Supprimer un fournisseur AI",
"actionGetAiProvider": "Obtenir un fournisseur AI",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "Générer un jeton d'accès",
"actionDeleteAccessToken": "Supprimer un jeton d'accès",
"actionListAccessTokens": "Lister les jetons d'accès",
"actionListResourceSessions": "Lister les sessions de ressource",
"actionDeleteResourceSession": "Révoquer la session de ressource",
"actionCreateResourceSessionToken": "Créer un jeton de session de ressource",
"actionCreateResourceRule": "Créer une règle de ressource",
"actionDeleteResourceRule": "Supprimer une règle de ressource",
"actionListResourceRules": "Lister les règles de ressource",
"actionUpdateResourceRule": "Mettre à jour une règle de ressource",
"actionCreateResourceMtlsCertificate": "Créer un certificat mTLS pour la ressource",
"actionDeleteResourceMtlsCertificate": "Supprimer le certificat mTLS pour la ressource",
"actionListResourceMtlsCertificates": "Lister les certificats mTLS pour les ressources",
"actionListOrgLabels": "Lister les étiquettes",
"actionCreateOrgLabel": "Créer Étiquette",
"actionUpdateOrgLabel": "Mettre à jour Étiquette",
"actionDeleteOrgLabel": "Supprimer Étiquette",
"actionAttachLabelToItem": "Attacher Étiquette",
"actionDetachLabelFromItem": "Détacher Étiquette",
"actionCreateEventStreamingDestination": "Créer une destination pour le streaming d'événements",
"actionUpdateEventStreamingDestination": "Mettre à jour la destination du streaming d'événements",
"actionDeleteEventStreamingDestination": "Supprimer la destination du streaming d'événements",
"actionListEventStreamingDestinations": "Lister les destinations de streaming d'événements",
"actionCreateAlertRule": "Créer une règle d'alerte",
"actionUpdateAlertRule": "Mettre à jour une règle d'alerte",
"actionDeleteAlertRule": "Supprimer la règle d'alerte",
"actionGetAlertRule": "Obtenir la règle d'alerte",
"actionListAlertRules": "Lister les règles d'alerte",
"actionCreateHealthCheck": "Créer une vérification de l'état de santé",
"actionUpdateHealthCheck": "Mettre à jour la vérification de l'état de santé",
"actionDeleteHealthCheck": "Supprimer la vérification de l'état de santé",
"actionListHealthChecks": "Lister les vérifications de l'état de santé",
"actionListOrgs": "Lister les organisations",
"actionCheckOrgId": "Vérifier l'ID",
"actionCreateOrg": "Créer une organisation",
@@ -1726,6 +1794,7 @@
"sidebarHome": "Domicile",
"sidebarSites": "Nœuds",
"sidebarApprovals": "Demandes d'approbation",
"sidebarResourceSessions": "Sessions des ressources",
"sidebarResources": "Ressource",
"sidebarProxyResources": "Publique",
"sidebarClientResources": "Privé",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "Politiques de ressources publiques",
"commandIdentityProviders": "Fournisseurs d'identité",
"commandApprovals": "Demandes d'approbation",
"commandResourceSessions": "Sessions des ressources",
"commandShareableLinks": "Liens partageables",
"commandOrganization": "Organisation",
"commandLogsAndAnalytics": "Journaux & Analyses",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "Journaux d'authentification",
"commandLogsAction": "Journaux des actions administratives",
"commandLogsConnection": "Journaux de connexion",
"commandLogsSsh": "Journaux de session SSH",
"commandLogsStreaming": "Diffusion d'événements",
"commandManagement": "Gestion",
"commandAlerting": "Alerte",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "Durée de conservation des logs de connexion",
"logRetentionAISessionsLabel": "Rétention des Journaux des Sessions du Portail AI",
"logRetentionAISessionsDescription": "Combien de temps conserver les journaux des sessions d'invite/réponse du portail AI",
"logRetentionSshLabel": "Rétention des Journaux des Sessions SSH",
"logRetentionSshDescription": "Durée de conservation des journaux de session SSH et des commandes",
"logRetentionDisabled": "Désactivé",
"logRetention3Days": "3 jours",
"logRetention7Days": "7 jours",
@@ -3707,6 +3780,27 @@
"connectionLogs": "Journaux de connexion",
"connectionLogsDescription": "Voir les journaux de connexion pour les tunnels de cette organisation",
"sidebarLogsConnection": "Journaux de connexion",
"sidebarLogsSsh": "Journaux de session SSH",
"sshSessionLogs": "Journaux de session SSH",
"sshSessionLogsDescription": "Voir des sessions SSH et les commandes exécutées sur des sites dans cette organisation",
"sshSystemUser": "Utilisateur Système",
"sshCommands": "Commandes",
"sshSessionId": "ID de Session",
"sshAuthMethod": "Méthode d'authentification",
"sshTransport": "Connexion",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "Navigateur",
"sshKeyFingerprint": "Empreinte de clé",
"sshCertKeyId": "ID de Clé de Certificat",
"sshStartedAt": "Commencé le",
"sshEndedAt": "Terminé le",
"sshSessionLoadingCommands": "Chargement des commandes...",
"sshSessionCommandsError": "Échec du chargement des commandes pour cette session",
"sshSessionNoCommands": "Aucune commande enregistrée pour cette session",
"sshSessionRecalledCommand": "(rappelé de l'historique de l'interpréteur de commandes)",
"sshSessionInexactNotice": "Certaines commandes ont été modifiées avec le rappel de l'historique ou l'achèvement par tabulation et peuvent ne pas correspondre exactement à celles exécutées par l'interpréteur de commandes.",
"sshSessionInexactTooltip": "Édité avec rappel de l'historique ou achèvement par tabulation; peut ne pas exactement correspondre à ce que l'interpréteur de commandes a exécuté",
"sshSessionCommandsTruncated": "Affichage des {shown} premières parmi {total} commandes. Exportez les journaux pour les voir toutes.",
"sidebarLogsStreaming": "Streaming en cours",
"sourceAddress": "Adresse source",
"destinationAddress": "Adresse de destination",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "Comment ajouter des enregistrements",
"dnsRecord": "Enregistrements DNS",
"domainTabSetup": "Configuration",
"domainTabRecords": "Enregistrements",
"domainSetupRecordsDescription": "Créez ces enregistrements chez votre registraire de domaine ou votre fournisseur DNS actuel. Pangolin les utilise pour vérifier le domaine et gérer son trafic.",
"domainRecordsInfoTitle": "Enregistrements servis par le Pangolin",
"domainRecordsInfoDescription": "Les serveurs de noms de Pangolin répondent aux requêtes DNS pour {domain}. Les enregistrements que vous ajoutez ici sont servis directement par Pangolin, vous n'avez donc pas besoin de les créer chez votre registraire.",
"customDnsRecords": "Enregistrements DNS personnalisés",
"customDnsRecordsAdd": "Ajouter un enregistrement",
"customDnsRecordsSearch": "Rechercher des enregistrements...",
"customDnsRecordsUnverifiedTitle": "Les enregistrements ne sont pas encore servis",
"customDnsRecordsUnverifiedDescription": "Ce domaine n'a pas été vérifié. Vous pouvez ajouter des enregistrements maintenant, mais les serveurs de noms de Pangolin ne les serviront pas tant que les serveurs de noms du domaine ne pointent pas vers Pangolin et que la vérification n'est pas terminée.",
"customDnsRecordCreate": "Ajouter un enregistrement DNS",
"customDnsRecordEdit": "Modifier l'enregistrement DNS",
"customDnsRecordFormDescription": "Cet enregistrement sera servi pour {domain}. Les modifications peuvent prendre quelques minutes pour se propager.",
"customDnsRecordNameDescription": "Utilisez @ pour la racine du domaine ou * comme premier label pour un joker.",
"customDnsRecordValueDescription": "Entrez une valeur par ligne, par exemple les deux serveurs de noms d'un enregistrement NS.",
"customDnsRecordPriority": "Priorité",
"customDnsRecordWeight": "Poids",
"customDnsRecordPort": "Port",
"customDnsRecordCreated": "Enregistrement DNS créé",
"customDnsRecordUpdated": "Enregistrement DNS mis à jour",
"customDnsRecordSaveError": "Échec de l'enregistrement de l'enregistrement DNS",
"customDnsRecordDeleted": "Enregistrement DNS supprimé",
"customDnsRecordDeleteError": "Impossible de supprimer l'enregistrement DNS",
"customDnsRecordDeleteTitle": "Supprimer l'enregistrement DNS",
"customDnsRecordDeleteQuestion": "Êtes-vous sûr de vouloir supprimer l'enregistrement {type} pour {name} ? Il cessera d'être résolu une fois que les réponses mises en cache auront expiré.",
"customDnsRecordDeleteConfirm": "Supprimer l'enregistrement",
"required": "Requis",
"domainSettingsUpdated": "Paramètres de domaine mis à jour avec succès",
"orgOrDomainIdMissing": "L'organisation ou l'identifiant de domaine est manquant",
@@ -3938,7 +4058,7 @@
"backToHome": "Retour à l'accueil",
"needToSignInToOrg": "Besoin d'utiliser le fournisseur d'identité de votre organisation ?",
"mtls": "mTLS",
"mtlsCaCertificates": "Certificats CA de confiance",
"mtlsCaCertificates": "Configurer les certificats clients",
"mtlsCaCertificatesDescription": "Les clients doivent présenter un certificat signé par l'une de ces autorités de certification pour se connecter. Téléchargez uniquement des certificats CA publics, jamais une clé privée.",
"mtlsCertificateName": "Nom (optionnel)",
"mtlsCertificateNamePlaceholder": "Dispositif d'entreprise CA",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "Le certificat n'a pas pu être supprimé.",
"mtlsLastCertificateRemoved": "mTLS a été désactivé car le dernier certificat CA a été supprimé.",
"mtlsNoCertificates": "Aucun certificat CA ajouté pour le moment",
"mtlsPendingCertificate": "Certificat CA {number}",
"mtlsCertificateInvalidPem": "Aucun certificat encodé en PEM trouvé. Un bloc commençant par -----BEGIN CERTIFICATE----- était attendu.",
"mtlsCertificatePrivateKey": "Cela ressemble à une clé privée, pas à un certificat. Chargez uniquement le certificat public de la CA.",
"mtlsCertificateDuplicate": "Ce certificat CA a déjà été ajouté.",
"mtlsRemoveCertificate": "Supprimer le certificat CA",
"mtlsExpired": "Expiré",
"mtlsExpiresSoon": "Expire bientôt",
+130 -6
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "Invia tutto il traffico Internet per uscire attraverso la rete del sito.",
"resourceTypeGatewayDescription": "Invia tutto il traffico Internet per uscire attraverso la rete del sito.",
"resourceDomainDescription": "La risorsa sarà servita su questo dominio completamente qualificato.",
"resourceMatchBaseDomain": "Corrispondenza anche con {host}",
"resourceMatchBaseDomainDescription": "Un carattere jolly corrisponde solo ai nomi host sotto {host}. Abilita questa opzione per instradare {host} attraverso questa risorsa.",
"resourceHTTPSSettings": "Impostazioni HTTPS",
"resourceHTTPSSettingsDescription": "Configura come sarà possibile accedere alla risorsa su HTTPS",
"resourcePortDescription": "La porta esterna sull'istanza o nodo Pangolin dove la risorsa sarà accessibile.",
@@ -425,6 +427,31 @@
"accessRolesAdd": "Aggiungi Ruolo",
"accessRoleDelete": "Elimina Ruolo",
"accessApprovalsManage": "Gestisci Approvazioni",
"resourceSessionsManage": "Gestisci Sessioni Risorsa",
"resourceSessionsDescription": "Visualizza chi ha attualmente una sessione attiva sulle tue risorse pubbliche e revoca l'accesso",
"resourceSessionsSearch": "Cerca per utente, email, link o risorsa...",
"resourceSessionsMethod": "Metodo",
"resourceSessionsIssued": "Emesso",
"resourceSessionsAnonymous": "Anonimo",
"resourceSessionsFilterAnyUser": "Tutti gli Utenti",
"resourceSessionsFilteredToSession": "Mostra una sessione",
"resourceSessionsMethodSso": "SSO",
"resourceSessionsMethodPassword": "Password",
"resourceSessionsMethodPincode": "Codice PIN",
"resourceSessionsMethodWhitelist": "Email",
"resourceSessionsMethodAccessToken": "Collegamento Condivisibile",
"resourceSessionsRevoke": "Revoca",
"resourceSessionsRevokeTitle": "Revoca Sessione",
"resourceSessionsRevokeQuestion": "Revocare la sessione per {identity} su {resource}? Dovranno autenticarsi di nuovo per accedere alla risorsa.",
"resourceSessionsRevokeSsoNote": "Questo utente è connesso con SSO. Tutte le loro sessioni per questa risorsa dallo stesso login verranno revocate, e dovranno accedere nuovamente per accedervi, anche se sono ancora collegati a Pangolin.",
"resourceSessionsRevoked": "Sessione revocata",
"resourceSessionsRevokedDescription": "La sessione è stata revocata. Potrebbero essere necessari alcuni secondi per applicare la modifica.",
"resourceSessionsErrorRevoke": "Impossibile revocare la sessione",
"resourceSessionsErrorRevokeDescription": "Si è verificato un errore durante la revoca della sessione",
"resourceSessionReauthRequired": "La tua sessione per questa risorsa è stata chiusa da un amministratore. Si prega di effettuare nuovamente l'accesso per continuare.",
"logLinks": "Collegamenti",
"logLinkUser": "Utente",
"logLinkResourceSession": "Sessione Risorsa",
"accessApprovalsDescription": "Visualizza e gestisci le approvazioni in attesa per accedere a questa organizzazione",
"description": "Descrizione",
"inviteTitle": "Inviti Aperti",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "Non c'è alcun limite al numero di utenti per questo host.",
"licenseUsersUsedMax": "{usedUsers} di {maxUsers} utenti utilizzati",
"licenseUsersUsed": "{count, plural, =0 {# utenti} one {# utente} other {# utenti}}",
"licenseUsageMachines": "Client Macchina",
"licenseNoMachineLimit": "Non c'è alcun limite al numero di client macchina per questo host.",
"licenseMachinesUsedMax": "{usedMachines} di {maxMachines} client macchina utilizzati",
"licenseMachinesUsed": "{count, plural, =0 {# client macchina} one {# client macchina} other {# client macchina}}",
"licenseUnlimited": "Illimitato",
"licenseTierLabel": "Livello",
"licensePurchaseDescription": "Scegli quanti siti vuoi {selectedMode, select, license {acquista una licenza. Puoi sempre aggiungere altri siti più tardi.} other {aggiungi alla tua licenza esistente.}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "L'utente è stato aggiornato.",
"autoProvisioned": "Provisioning Automatico",
"autoProvisionSettings": "Impostazioni Automatiche di provisioning",
"autoProvisionedDescription": "Permetti a questo utente di essere gestito automaticamente dal provider di identità",
"autoProvisionedDescription": "Se il provider di identità ha l'auto-provisioning abilitato, consenti che questo utente venga gestito automaticamente.",
"accessControlsDescription": "Gestisci cosa questo utente può accedere e fare nell'organizzazione",
"accessControlsSubmit": "Salva Controlli di Accesso",
"singleRolePerUserPlanNotice": "Il tuo piano supporta solo un ruolo per utente.",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "Distribuisci le richieste uniformemente tra i bersagli in rotazione.",
"loadBalancingStrategy_p2c": "Power of Two Choices",
"loadBalancingStrategy_p2c_description": "Scegli due destinatari a caso e invia la richiesta a quello con meno connessioni attive.",
"loadBalancingStrategy_hrw": "Hash IP Client",
"loadBalancingStrategy_hrw": "Peso Casuale Massimo",
"loadBalancingStrategy_hrw_description": "Hashera l'IP del client in modo che lo stesso client sia instradato costantemente allo stesso target.",
"loadBalancingStrategy_leasttime": "Tempo di Risposta Minimo",
"loadBalancingStrategy_leasttime_description": "Invia richieste al target con il tempo di risposta più veloce e meno connessioni attive.",
@@ -829,6 +860,8 @@
"save": "Salva",
"proxyAdditional": "Impostazioni Proxy Aggiuntive",
"proxyAdditionalDescription": "Configura come la risorsa gestisce le impostazioni del proxy",
"proxyForwardIdentityHeaders": "Inoltra Intestazioni di Identità",
"proxyForwardIdentityHeadersDescription": "Invia l'identità dell'utente autenticato al backend nelle intestazioni Remote-User, Remote-Email, Remote-Name, Remote-Role e Remote-User-Id. Disabilita per i backend che rigettano richieste con intestazioni non riconosciute.",
"proxyCustomHeader": "Intestazione Host Personalizzata",
"proxyCustomHeaderDescription": "L'intestazione host da impostare durante il proxy delle richieste. Lascia vuoto per usare quella predefinita.",
"proxyAdditionalSubmit": "Salva Impostazioni Proxy",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "Autenticazione Header Base",
"policyAuthHeaderAuthDescription": "Convalida un nome e un valore di intestazione HTTP personalizzato su ogni richiesta",
"policyAuthHeaderAuthSummary": "Intestazione configurata",
"policyAuthMtlsTitle": "Certificato Client (mTLS)",
"policyAuthMtlsDescription": "Richiede un certificato client firmato da una CA fidata durante il handshake TLS",
"policyAuthMtlsSummary": "{count} certificati CA fidati",
"policyAuthHeaderName": "Nome utente",
"policyAuthHeaderValue": "Password",
"policyAuthSetPasscode": "Imposta Codice di Accesso",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "Provisioning Automatico Utenti",
"idpAutoProvisionUsersDescription": "Quando abilitato, gli utenti verranno creati automaticamente nel sistema al primo accesso con la possibilità di mappare gli utenti a ruoli e organizzazioni.",
"idpAutoProvisionConfigureAfterCreate": "Puoi configurare le impostazioni di auto fornitura una volta creato il provider di identità.",
"idpSessionRevalidation": "Ri-validazione Continua della Sessione",
"idpSessionRevalidationDescription": "Quando abilitata, ri-valida periodicamente le sessioni con questo provider di identità. Se il provider revoca l'accesso, le sessioni collegate vengono chiuse.",
"idpSessionRevalidationConfigureAfterCreate": "Puoi configurare la ri-validazione continua della sessione una volta creato il provider di identità.",
"licenseBadge": "EE",
"idpType": "Tipo di Provider",
"idpTypeDescription": "Seleziona il tipo di provider di identità che desideri configurare",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "Elimina Dominio",
"actionGetDNSRecords": "Ottieni Record DNS",
"actionRestartOrgDomain": "Riavvia Dominio",
"actionListCustomDnsRecords": "Elenca Record DNS Personalizzati",
"actionCreateCustomDnsRecord": "Crea Record DNS Personalizzato",
"actionUpdateCustomDnsRecord": "Aggiorna Record DNS Personalizzato",
"actionDeleteCustomDnsRecord": "Elimina Record DNS Personalizzato",
"actionCreateSite": "Crea Sito",
"actionDeleteSite": "Elimina Sito",
"actionGetSite": "Ottieni Sito",
"actionListSites": "Elenca Siti",
"actionReGenerateSecret": "Rigenera Credenziali",
"actionCreateAiProvider": "Crea Provider AI",
"actionDeleteAiProvider": "Elimina Provider AI",
"actionGetAiProvider": "Ottieni Provider AI",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "Genera Token di Accesso",
"actionDeleteAccessToken": "Elimina Token di Accesso",
"actionListAccessTokens": "Elenca Token di Accesso",
"actionListResourceSessions": "Elenca Sessioni Risorsa",
"actionDeleteResourceSession": "Revoca Sessione Risorsa",
"actionCreateResourceSessionToken": "Crea Token di Sessione della Risorsa",
"actionCreateResourceRule": "Crea Regola Risorsa",
"actionDeleteResourceRule": "Elimina Regola Risorsa",
"actionListResourceRules": "Elenca Regole Risorsa",
"actionUpdateResourceRule": "Aggiorna Regola Risorsa",
"actionCreateResourceMtlsCertificate": "Crea Certificato mtls della Risorsa",
"actionDeleteResourceMtlsCertificate": "Elimina Certificato mtls della Risorsa",
"actionListResourceMtlsCertificates": "Elenca Certificati mtls della Risorsa",
"actionListOrgLabels": "Elenca Etichette",
"actionCreateOrgLabel": "Crea Etichetta",
"actionUpdateOrgLabel": "Aggiorna Etichetta",
"actionDeleteOrgLabel": "Elimina Etichetta",
"actionAttachLabelToItem": "Assegna Etichetta",
"actionDetachLabelFromItem": "Rimuovi Etichetta",
"actionCreateEventStreamingDestination": "Crea Destinazione Streaming Evento",
"actionUpdateEventStreamingDestination": "Aggiorna Destinazione Streaming Evento",
"actionDeleteEventStreamingDestination": "Elimina Destinazione Streaming Evento",
"actionListEventStreamingDestinations": "Elenca Destinazioni Streaming Evento",
"actionCreateAlertRule": "Crea Regola di Allerta",
"actionUpdateAlertRule": "Aggiorna Regola di Allerta",
"actionDeleteAlertRule": "Elimina Regola di Allerta",
"actionGetAlertRule": "Ottieni Regola di Allerta",
"actionListAlertRules": "Elenca Regole di Allerta",
"actionCreateHealthCheck": "Crea Controllo di Salute",
"actionUpdateHealthCheck": "Aggiorna Controllo di Salute",
"actionDeleteHealthCheck": "Elimina Controllo di Salute",
"actionListHealthChecks": "Elenca Controlli di Salute",
"actionListOrgs": "Elenca Organizzazioni",
"actionCheckOrgId": "Controlla ID",
"actionCreateOrg": "Crea Organizzazione",
@@ -1726,6 +1794,7 @@
"sidebarHome": "Pagina Iniziale",
"sidebarSites": "Siti",
"sidebarApprovals": "Richieste Di Approvazione",
"sidebarResourceSessions": "Sessioni Risorse",
"sidebarResources": "Risorse",
"sidebarProxyResources": "Pubblico",
"sidebarClientResources": "Privato",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "Politiche delle Risorse Pubbliche",
"commandIdentityProviders": "Fornitori di Identità",
"commandApprovals": "Richieste di Approvazione",
"commandResourceSessions": "Sessioni Risorse",
"commandShareableLinks": "Link Condivisibili",
"commandOrganization": "Organizzazione",
"commandLogsAndAnalytics": "Log & Analisi",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "Log di Autenticazione",
"commandLogsAction": "Log delle Azioni Amministrative",
"commandLogsConnection": "Log di Connessione",
"commandLogsSsh": "Log di Sessioni SSH",
"commandLogsStreaming": "Streaming di Eventi",
"commandManagement": "Gestione",
"commandAlerting": "Avvisi",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "Per quanto tempo conservare i log di connessione",
"logRetentionAISessionsLabel": "Conservazione Log delle Sessioni AI Gateway",
"logRetentionAISessionsDescription": "Per quanto tempo mantenere i log delle sessioni prompt/risposta del gateway AI",
"logRetentionSshLabel": "Conservazione Log di Sessioni SSH",
"logRetentionSshDescription": "Per quanto tempo mantenere i log della sessione ssh e dei comandi",
"logRetentionDisabled": "Disabilitato",
"logRetention3Days": "3 giorni",
"logRetention7Days": "7 giorni",
@@ -3707,6 +3780,27 @@
"connectionLogs": "Log Di Connessione",
"connectionLogsDescription": "Visualizza i log di connessione per i tunnel in questa organizzazione",
"sidebarLogsConnection": "Log Di Connessione",
"sidebarLogsSsh": "Log di Sessioni SSH",
"sshSessionLogs": "Log di Sessioni SSH",
"sshSessionLogsDescription": "Visualizza sessioni SSH e i comandi eseguiti su siti in questa organizzazione",
"sshSystemUser": "Utente di Sistema",
"sshCommands": "Comandi",
"sshSessionId": "ID Sessione",
"sshAuthMethod": "Metodo di Autenticazione",
"sshTransport": "Connessione",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "Browser",
"sshKeyFingerprint": "Impronta Certificato",
"sshCertKeyId": "ID Chiave Certificato",
"sshStartedAt": "Iniziato Il",
"sshEndedAt": "Terminato Il",
"sshSessionLoadingCommands": "Caricamento comandi...",
"sshSessionCommandsError": "Errore nel caricamento dei comandi per questa sessione",
"sshSessionNoCommands": "Nessun comando registrato per questa sessione",
"sshSessionRecalledCommand": "(richiamato dalla cronologia della shell)",
"sshSessionInexactNotice": "Alcuni comandi sono stati modificati con il richiamo della cronologia o completamento tab e potrebbero non corrispondere esattamente a ciò che la shell ha eseguito.",
"sshSessionInexactTooltip": "Modificato con richiamo della cronologia o completamento tab; potrebbe non corrispondere esattamente a ciò che la shell ha eseguito",
"sshSessionCommandsTruncated": "Mostra i primi {shown} di {total} comandi. Esporta i log per vedere tutte.",
"sidebarLogsStreaming": "Streaming",
"sourceAddress": "Indirizzo Di Origine",
"destinationAddress": "Indirizzo Di Destinazione",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "Come aggiungere record",
"dnsRecord": "Record DNS",
"domainTabSetup": "Impostazione",
"domainTabRecords": "Record",
"domainSetupRecordsDescription": "Crea questi record presso il tuo registratore di dominio o provider DNS attuale. Pangolin li utilizza per verificare il dominio e instradare il suo traffico.",
"domainRecordsInfoTitle": "Record serviti da Pangolin",
"domainRecordsInfoDescription": "I nameserver di Pangolin rispondono alle query DNS per {domain}. I record che aggiungi qui sono serviti direttamente da Pangolin, quindi non è necessario crearli presso il tuo registratore.",
"customDnsRecords": "Record DNS Personalizzati",
"customDnsRecordsAdd": "Aggiungi Record",
"customDnsRecordsSearch": "Cerca record...",
"customDnsRecordsUnverifiedTitle": "I record non sono ancora serviti",
"customDnsRecordsUnverifiedDescription": "Questo dominio non è stato verificato. Puoi aggiungere i record ora, ma i nameserver di Pangolin non li serviranno fino a quando i nameserver del dominio non punteranno a Pangolin e la verifica non sarà completata.",
"customDnsRecordCreate": "Aggiungi Record DNS",
"customDnsRecordEdit": "Modifica Record DNS",
"customDnsRecordFormDescription": "Questo record verrà servito per {domain}. Le modifiche potrebbero richiedere alcuni minuti per propagarsi.",
"customDnsRecordNameDescription": "Usa @ per la radice del dominio o * come primo etichetta per un jolly.",
"customDnsRecordValueDescription": "Inserisci un valore per riga, ad es. entrambi i nameserver di un record NS.",
"customDnsRecordPriority": "Priorità",
"customDnsRecordWeight": "Peso",
"customDnsRecordPort": "Porta",
"customDnsRecordCreated": "Record DNS creato",
"customDnsRecordUpdated": "Record DNS aggiornato",
"customDnsRecordSaveError": "Impossibile salvare il record DNS",
"customDnsRecordDeleted": "Record DNS eliminato",
"customDnsRecordDeleteError": "Impossibile eliminare il record DNS",
"customDnsRecordDeleteTitle": "Elimina Record DNS",
"customDnsRecordDeleteQuestion": "Sei sicuro di voler eliminare il record {type} per {name}? Smetterà di risolversi una volta che le risposte memorizzate nella cache scadranno.",
"customDnsRecordDeleteConfirm": "Elimina Record",
"required": "Richiesto",
"domainSettingsUpdated": "Impostazioni dominio aggiornate con successo",
"orgOrDomainIdMissing": "Manca l'ID dell'organizzazione o del dominio",
@@ -3938,14 +4058,14 @@
"backToHome": "Torna alla home",
"needToSignInToOrg": "Hai bisogno di utilizzare il provider di identità della tua organizzazione?",
"mtls": "mTLS",
"mtlsCaCertificates": "Certificati CA fidati",
"mtlsCaCertificates": "Imposta Certificati Client",
"mtlsCaCertificatesDescription": "I client devono presentare un certificato firmato da una di queste autorità di certificazione per connettersi. Carica solo certificati CA pubblici, mai una chiave privata.",
"mtlsCertificateName": "Nome (Opzionale)",
"mtlsCertificateNamePlaceholder": "Dispositivo aziendale CA",
"mtlsCertificatePem": "Certificato CA (PEM)",
"mtlsCertificatePemDescription": "Incolla un certificato codificato PEM o un insieme di certificati, o carica un file .pem, .crt o .cer.",
"mtlsUploadFile": "Carica file",
"mtlsAddCertificate": "Aggiungi certificato CA",
"mtlsUploadFile": "Carica File",
"mtlsAddCertificate": "Aggiungi Certificato CA",
"mtlsCertificateAdded": "Certificato CA aggiunto",
"mtlsCertificateAddedDescription": "I client con certificati firmati da questa CA possono ora essere verificati.",
"mtlsCertificateErrorAdd": "Impossibile aggiungere il certificato CA",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "Il certificato non può essere rimosso.",
"mtlsLastCertificateRemoved": "mTLS è stato disattivato perché l'ultimo certificato CA è stato rimosso.",
"mtlsNoCertificates": "Nessun certificato CA aggiunto ancora",
"mtlsPendingCertificate": "Certificato CA {number}",
"mtlsCertificateInvalidPem": "Nessun certificato codificato in PEM trovato. Previsto un blocco che inizia con -----BEGIN CERTIFICATE-----.",
"mtlsCertificatePrivateKey": "Questo sembra essere una chiave privata, non un certificato. Carica solo il certificato pubblico della CA.",
"mtlsCertificateDuplicate": "Questo certificato CA è già stato aggiunto.",
"mtlsRemoveCertificate": "Rimuovi certificato CA",
"mtlsExpired": "Scaduto",
"mtlsExpiresSoon": "Scade presto",
@@ -4488,7 +4612,7 @@
"rdpFull": "Completo",
"rdpReal": "Reale",
"rdpMeta": "Meta",
"rdpUploadFiles": "Carica file",
"rdpUploadFiles": "Carica File",
"rdpFilesReadyToPaste": "File pronti per essere incollati",
"rdpFilesReadyToPasteDescription": "{count} file(s) copiati negli appunti remoti — premi Ctrl+V sul desktop remoto per incollare.",
"rdpUploadFailed": "Caricamento fallito",
+127 -3
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "모든 인터넷 트래픽을 사이트 네트워크를 통해 종료하도록 전송합니다.",
"resourceTypeGatewayDescription": "모든 인터넷 트래픽을 사이트 네트워크를 통해 종료하도록 전송합니다.",
"resourceDomainDescription": "리소스는 숙주 네임에서 제공됩니다.",
"resourceMatchBaseDomain": "{host}도 일치시킵니다",
"resourceMatchBaseDomainDescription": "와일드카드는 {host} 하위의 호스트명과만 일치합니다. 이 기능을 활성화하여 {host}를 이 리소스로 라우팅하십시오.",
"resourceHTTPSSettings": "HTTPS 설정",
"resourceHTTPSSettingsDescription": "리소스가 HTTPS로 접근할 수 있는 방식을 구성합니다.",
"resourcePortDescription": "리소스에 접근할 수 있는 Pangolin 인스턴스나 노드의 외부 포트입니다.",
@@ -425,6 +427,31 @@
"accessRolesAdd": "역할 추가",
"accessRoleDelete": "역할 삭제",
"accessApprovalsManage": "승인 관리",
"resourceSessionsManage": "리소스 세션 관리",
"resourceSessionsDescription": "현재 귀하의 공용 리소스에 활동 중인 세션이 있는 사용자를 보고 액세스를 철회합니다",
"resourceSessionsSearch": "사용자, 이메일, 링크 또는 리소스로 검색...",
"resourceSessionsMethod": "방법",
"resourceSessionsIssued": "발행됨",
"resourceSessionsAnonymous": "익명",
"resourceSessionsFilterAnyUser": "모든 사용자",
"resourceSessionsFilteredToSession": "하나의 세션 표시",
"resourceSessionsMethodSso": "SSO",
"resourceSessionsMethodPassword": "비밀번호",
"resourceSessionsMethodPincode": "PIN 코드",
"resourceSessionsMethodWhitelist": "이메일",
"resourceSessionsMethodAccessToken": "공유 가능한 링크",
"resourceSessionsRevoke": "철회",
"resourceSessionsRevokeTitle": "세션 철회",
"resourceSessionsRevokeQuestion": "{identity}의 {resource}에 대한 세션을 철회하시겠습니까? 리소스에 액세스하기 위해서는 다시 인증해야 합니다.",
"resourceSessionsRevokeSsoNote": "이 사용자는 SSO로 로그인했습니다. 동일한 로그인에서 이 리소스에 대한 모든 세션이 철회되며, Pangolin에 계속 로그인되어 있어도 다시 로그인하여 액세스해야 합니다.",
"resourceSessionsRevoked": "세션이 철회되었습니다",
"resourceSessionsRevokedDescription": "세션이 철회되었습니다. 효과가 나타나기까지 몇 초가 걸릴 수 있습니다.",
"resourceSessionsErrorRevoke": "세션을 철회하지 못했습니다",
"resourceSessionsErrorRevokeDescription": "세션을 철회하는 동안 오류가 발생했습니다",
"resourceSessionReauthRequired": "관리자가 이 리소스에 대한 귀하의 세션을 종료했습니다. 계속하려면 다시 로그인하세요.",
"logLinks": "링크",
"logLinkUser": "사용자",
"logLinkResourceSession": "리소스 세션",
"accessApprovalsDescription": "이 조직의 접근 승인 대기를 보고 관리하세요.",
"description": "설명",
"inviteTitle": "열린 초대",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "이 호스트에 대한 사용자 수에 제한이 없습니다.",
"licenseUsersUsedMax": "{maxUsers}명의 사용자 중 {usedUsers}명 사용 중",
"licenseUsersUsed": "{count, plural, =0 {# 사용자} other {# 사용자}}",
"licenseUsageMachines": "기계 클라이언트",
"licenseNoMachineLimit": "이 호스트의 기계 클라이언트 수에는 제한이 없습니다.",
"licenseMachinesUsedMax": "{usedMachines} / {maxMachines} 기계 클라이언트 사용됨",
"licenseMachinesUsed": "{count, plural, =0 {기계 클라이언트 #개} one {기계 클라이언트 #개} other {기계 클라이언트 #개}}",
"licenseUnlimited": "무제한",
"licenseTierLabel": "등급",
"licensePurchaseDescription": "구매할 사이트 수를 선택하세요 {selectedMode, select, license {라이센스를 구매합니다. 나중에 더 많은 사이트를 추가할 수 있습니다.} other {기존 라이센스에 추가합니다.}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "사용자가 업데이트되었습니다.",
"autoProvisioned": "자동 프로비저닝됨",
"autoProvisionSettings": "자동 프로비저닝 설정",
"autoProvisionedDescription": "이 사용자가 ID 공급자에 의해 자동으로 관리될 수 있도록 허용합니다",
"autoProvisionedDescription": "ID 제공자가 자동 프로비저닝을 활성화한 경우, 이 사용자를 자동으로 관리하도록 허용합니다.",
"accessControlsDescription": "이 사용자가 조직에서 접근하고 수행할 수 있는 작업을 관리하세요",
"accessControlsSubmit": "접근 제어 저장",
"singleRolePerUserPlanNotice": "계획에는 사용자당 한 가지 역할만 지원됩니다.",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "대상 사이에서 요청을 균등하게 회전시킵니다.",
"loadBalancingStrategy_p2c": "두 가지 선택의 힘",
"loadBalancingStrategy_p2c_description": "두 개의 무작위 대상을 선택하여 더 적은 활성 연결을 가진 대상에 요청을 보냅니다.",
"loadBalancingStrategy_hrw": "클라이언트 IP 해시",
"loadBalancingStrategy_hrw": "가장 높은 무작위 가중치",
"loadBalancingStrategy_hrw_description": "클라이언트 IP를 해시하여 동일한 클라이언트가 일관되게 동일한 대상으로 라우팅되도록 합니다.",
"loadBalancingStrategy_leasttime": "최소 응답 시간",
"loadBalancingStrategy_leasttime_description": "가장 빠른 응답 시간과 가장 적은 활성 연결을 가진 대상으로 요청을 보냅니다.",
@@ -829,6 +860,8 @@
"save": "저장",
"proxyAdditional": "추가 프록시 설정",
"proxyAdditionalDescription": "리소스가 프록시 설정을 처리하는 방법 구성",
"proxyForwardIdentityHeaders": "아이덴티티 헤더 포워딩",
"proxyForwardIdentityHeadersDescription": "인증된 사용자의 아이덴티티를 Remote-User, Remote-Email, Remote-Name, Remote-Role, 및 Remote-User-Id 헤더로 백엔드에 전송합니다. 인식되지 않는 헤더로 요청을 거부하는 백엔드에는 비활성화하십시오.",
"proxyCustomHeader": "사용자 정의 호스트 헤더",
"proxyCustomHeaderDescription": "요청을 프록시할 때 설정할 호스트 헤더입니다. 기본값을 사용하려면 비워 두십시오.",
"proxyAdditionalSubmit": "프록시 설정 저장",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "기본 헤더 인증",
"policyAuthHeaderAuthDescription": "각 요청에서 맞춤 HTTP 헤더 이름 및 값을 검증",
"policyAuthHeaderAuthSummary": "헤더 구성됨",
"policyAuthMtlsTitle": "클라이언트 인증서 (mTLS)",
"policyAuthMtlsDescription": "TLS 핸드셰이크 동안 신뢰할 수 있는 CA가 서명한 클라이언트 인증서가 필요합니다",
"policyAuthMtlsSummary": "{count}개의 신뢰할 수 있는 CA 인증서",
"policyAuthHeaderName": "사용자 이름",
"policyAuthHeaderValue": "비밀번호",
"policyAuthSetPasscode": "패스코드 설정",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "사용자 자동 프로비저닝",
"idpAutoProvisionUsersDescription": "활성화되면 사용자가 첫 로그인 시 시스템에 자동으로 생성되며, 사용자와 역할 및 조직을 매핑할 수 있습니다.",
"idpAutoProvisionConfigureAfterCreate": "아이덴티티 공급자가 생성되면 자동 프로비저닝 설정을 구성할 수 있습니다.",
"idpSessionRevalidation": "연속 세션 재검증",
"idpSessionRevalidationDescription": "활성화되면 이 ID 공급업체와 주기적으로 세션을 재검증합니다. 공급업체가 액세스를 철회하면 연결된 세션이 종료됩니다.",
"idpSessionRevalidationConfigureAfterCreate": "아이덴티티 공급자가 생성되면 연속 세션 재검증을 구성할 수 있습니다.",
"licenseBadge": "EE",
"idpType": "제공자 유형",
"idpTypeDescription": "구성할 ID 공급자의 유형을 선택하십시오.",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "도메인 삭제",
"actionGetDNSRecords": "DNS 레코드 가져오기",
"actionRestartOrgDomain": "도메인 재시작",
"actionListCustomDnsRecords": "사용자 정의 DNS 레코드 목록 보기",
"actionCreateCustomDnsRecord": "사용자 정의 DNS 레코드 생성하기",
"actionUpdateCustomDnsRecord": "사용자 정의 DNS 레코드 업데이트하기",
"actionDeleteCustomDnsRecord": "사용자 정의 DNS 레코드 삭제하기",
"actionCreateSite": "사이트 생성",
"actionDeleteSite": "사이트 삭제",
"actionGetSite": "사이트 가져오기",
"actionListSites": "사이트 목록",
"actionReGenerateSecret": "자격 증명 다시 생성",
"actionCreateAiProvider": "AI 공급자 생성",
"actionDeleteAiProvider": "AI 공급자 삭제",
"actionGetAiProvider": "AI 공급자 가져오기",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "액세스 토큰 생성",
"actionDeleteAccessToken": "액세스 토큰 삭제",
"actionListAccessTokens": "액세스 토큰 목록",
"actionListResourceSessions": "리소스 세션 목록",
"actionDeleteResourceSession": "리소스 세션 철회",
"actionCreateResourceSessionToken": "리소스 세션 토큰 생성",
"actionCreateResourceRule": "리소스 규칙 생성",
"actionDeleteResourceRule": "리소스 규칙 삭제",
"actionListResourceRules": "리소스 규칙 목록",
"actionUpdateResourceRule": "리소스 규칙 업데이트",
"actionCreateResourceMtlsCertificate": "리소스 mTLS 인증서 만들기",
"actionDeleteResourceMtlsCertificate": "리소스 mTLS 인증서 삭제",
"actionListResourceMtlsCertificates": "리소스 mTLS 인증서 목록",
"actionListOrgLabels": "레이블 목록",
"actionCreateOrgLabel": "레이블 만들기",
"actionUpdateOrgLabel": "레이블 업데이트",
"actionDeleteOrgLabel": "레이블 삭제",
"actionAttachLabelToItem": "레이블 연결",
"actionDetachLabelFromItem": "레이블 분리",
"actionCreateEventStreamingDestination": "이벤트 스트리밍 목적지 생성",
"actionUpdateEventStreamingDestination": "이벤트 스트리밍 목적지 업데이트",
"actionDeleteEventStreamingDestination": "이벤트 스트리밍 목적지 삭제",
"actionListEventStreamingDestinations": "이벤트 스트리밍 목적지 목록",
"actionCreateAlertRule": "알림 규칙 생성",
"actionUpdateAlertRule": "알림 규칙 업데이트",
"actionDeleteAlertRule": "알림 규칙 삭제",
"actionGetAlertRule": "알림 규칙 확인",
"actionListAlertRules": "알림 규칙 목록",
"actionCreateHealthCheck": "상태 확인 생성",
"actionUpdateHealthCheck": "상태 확인 업데이트",
"actionDeleteHealthCheck": "상태 확인 삭제",
"actionListHealthChecks": "상태 확인 목록",
"actionListOrgs": "조직 목록",
"actionCheckOrgId": "ID 확인",
"actionCreateOrg": "조직 생성",
@@ -1726,6 +1794,7 @@
"sidebarHome": "홈",
"sidebarSites": "사이트",
"sidebarApprovals": "승인 요청",
"sidebarResourceSessions": "리소스 세션",
"sidebarResources": "리소스",
"sidebarProxyResources": "공유",
"sidebarClientResources": "비공개",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "공용 리소스 정책",
"commandIdentityProviders": "신원 공급자",
"commandApprovals": "승인 요청",
"commandResourceSessions": "리소스 세션",
"commandShareableLinks": "공유 가능한 링크",
"commandOrganization": "조직",
"commandLogsAndAnalytics": "로그 및 분석",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "인증 로그",
"commandLogsAction": "관리자 작업 로그",
"commandLogsConnection": "네트워크 로그",
"commandLogsSsh": "SSH 세션 로그",
"commandLogsStreaming": "이벤트 스트리밍",
"commandManagement": "관리",
"commandAlerting": "경보",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "연결 로그를 얼마나 오래 보유할지",
"logRetentionAISessionsLabel": "AI 게이트웨이 세션 로그 보관",
"logRetentionAISessionsDescription": "AI 게이트웨이 프롬프트/응답 세션 로그를 보관할 기간",
"logRetentionSshLabel": "SSH 세션 로그 보관",
"logRetentionSshDescription": "SSH 세션과 명령 로그를 얼마나 오래 보유할지 설정",
"logRetentionDisabled": "비활성화됨",
"logRetention3Days": "3 일",
"logRetention7Days": "7 일",
@@ -3707,6 +3780,27 @@
"connectionLogs": "연결 로그",
"connectionLogsDescription": "이 조직의 터널 연결 로그 보기",
"sidebarLogsConnection": "연결 로그",
"sidebarLogsSsh": "SSH 세션 로그",
"sshSessionLogs": "SSH 세션 로그",
"sshSessionLogsDescription": "이 조직 내 사이트에서 SSH 세션과 실행된 명령을 봅니다",
"sshSystemUser": "시스템 사용자",
"sshCommands": "명령",
"sshSessionId": "세션 ID",
"sshAuthMethod": "인증 방법",
"sshTransport": "연결",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "브라우저",
"sshKeyFingerprint": "키 지문",
"sshCertKeyId": "인증서 키 ID",
"sshStartedAt": "시작 시간",
"sshEndedAt": "종료 시간",
"sshSessionLoadingCommands": "명령을 로드하는 중...",
"sshSessionCommandsError": "이 세션의 명령을 로드하지 못했습니다",
"sshSessionNoCommands": "이 세션에 기록된 명령이 없습니다",
"sshSessionRecalledCommand": "(쉘 기록에서 회상됨)",
"sshSessionInexactNotice": "일부 명령은 기록 회상 또는 탭 완료로 편집되었을 수 있으며 실행된 명령과 정확히 일치하지 않을 수 있습니다.",
"sshSessionInexactTooltip": "기록 회상 또는 탭 완료를 사용하여 편집됨; 실행된 명령과 정확히 일치하지 않을 수 있음",
"sshSessionCommandsTruncated": "{total} 명령 중 처음 {shown}개 명령 표시 중. 모든 명령을 보려면 로그를 내보내십시오.",
"sidebarLogsStreaming": "스트리밍",
"sourceAddress": "소스 주소",
"destinationAddress": "대상 주소",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "레코드 추가 방법",
"dnsRecord": "DNS 레코드",
"domainTabSetup": "설정",
"domainTabRecords": "레코드",
"domainSetupRecordsDescription": "귀하의 도메인 등록기관 또는 현재 DNS 제공업체에서 이러한 레코드를 생성하십시오. Pangolin은 이를 사용하여 도메인을 검증하고 트래픽을 라우팅합니다.",
"domainRecordsInfoTitle": "Pangolin이 제공하는 레코드",
"domainRecordsInfoDescription": "Pangolin의 네임서버는 {domain}의 DNS 쿼리에 응답합니다. 여기에서 추가한 레코드는 Pangolin에서 직접 제공되므로 등록기관에서 생성할 필요가 없습니다.",
"customDnsRecords": "사용자 정의 DNS 레코드",
"customDnsRecordsAdd": "레코드 추가",
"customDnsRecordsSearch": "레코드 검색...",
"customDnsRecordsUnverifiedTitle": "레코드가 아직 제공되지 않았습니다",
"customDnsRecordsUnverifiedDescription": "이 도메인은 아직 확인되지 않았습니다. 지금 레코드를 추가할 수 있지만, 도메인의 네임서버가 Pangolin으로 설정되며 검증이 완료될 때까지 Pangolin의 네임서버는 이를 제공하지 않습니다.",
"customDnsRecordCreate": "DNS 레코드 추가",
"customDnsRecordEdit": "DNS 레코드 편집",
"customDnsRecordFormDescription": "이 레코드는 {domain}에 대해 제공됩니다. 변경 사항이 반영되기까지 몇 분 정도 걸릴 수 있습니다.",
"customDnsRecordNameDescription": "도메인의 루트에는 @를 사용하고, 와일드카드의 첫 라벨에는 * 을 사용합니다.",
"customDnsRecordValueDescription": "라인당 하나의 값을 입력하세요, 예를 들어 NS 레코드의 두 네임서버 모두를 입력하세요.",
"customDnsRecordPriority": "우선순위",
"customDnsRecordWeight": "가중치",
"customDnsRecordPort": "포트",
"customDnsRecordCreated": "DNS 레코드 생성 완료",
"customDnsRecordUpdated": "DNS 레코드 업데이트 완료",
"customDnsRecordSaveError": "DNS 레코드 저장 실패",
"customDnsRecordDeleted": "DNS 레코드 삭제 완료",
"customDnsRecordDeleteError": "DNS 레코드 삭제 실패",
"customDnsRecordDeleteTitle": "DNS 레코드 삭제",
"customDnsRecordDeleteQuestion": "{name}의 {type} 레코드를 삭제하시겠습니까? 만료된 캐시 응답이 없으면 더 이상 해결되지 않습니다.",
"customDnsRecordDeleteConfirm": "레코드 삭제",
"required": "필수",
"domainSettingsUpdated": "도메인 설정이 성공적으로 업데이트되었습니다",
"orgOrDomainIdMissing": "조직 ID 또는 도메인 ID가 누락되었습니다",
@@ -3938,7 +4058,7 @@
"backToHome": "홈으로 돌아가기",
"needToSignInToOrg": "조직의 아이덴티티 공급자를 사용해야 합니까?",
"mtls": "mTLS",
"mtlsCaCertificates": "신뢰할 수 있는 CA 인증서",
"mtlsCaCertificates": "클라이언트 인증서 설정",
"mtlsCaCertificatesDescription": "클라이언트는 이러한 인증 기관 중 하나가 서명한 인증서를 제시해야 연결할 수 있습니다. 공개 CA 인증서만 업로드하고, 비공개 키는 절대 업로드하지 마세요.",
"mtlsCertificateName": "이름 (선택 사항)",
"mtlsCertificateNamePlaceholder": "회사 장치 CA",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "인증서를 제거할 수 없습니다.",
"mtlsLastCertificateRemoved": "마지막 CA 인증서가 제거되어 mTLS가 꺼졌습니다.",
"mtlsNoCertificates": "아직 CA 인증서가 추가되지 않았습니다",
"mtlsPendingCertificate": "CA 인증서 {number}",
"mtlsCertificateInvalidPem": "PEM으로 인코딩된 인증서를 찾을 수 없습니다. -----BEGIN CERTIFICATE-----로 시작하는 블록을 예상합니다.",
"mtlsCertificatePrivateKey": "이는 인증서가 아니라 개인 키처럼 보입니다. CA의 공용 인증서만 업로드하십시오.",
"mtlsCertificateDuplicate": "이 CA 인증서는 이미 추가되었습니다.",
"mtlsRemoveCertificate": "CA 인증서 제거하기",
"mtlsExpired": "만료됨",
"mtlsExpiresSoon": "곧 만료됨",
+127 -3
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "Send all internett trafikk for å gå ut gjennom områdenettverket.",
"resourceTypeGatewayDescription": "Send all internett trafikk for å gå ut gjennom områdenettverket.",
"resourceDomainDescription": "Ressursen vil bli servert på dette fullstendig kvalifiserte domenenavnet.",
"resourceMatchBaseDomain": "Samsvar også med {host}",
"resourceMatchBaseDomainDescription": "Et jokertegn samsvarer bare med vertsnavn under {host}. Aktiver dette for å rute {host} gjennom denne ressursen også.",
"resourceHTTPSSettings": "HTTPS-innstillinger",
"resourceHTTPSSettingsDescription": "Konfigurer hvordan ressursen skal nås over HTTPS",
"resourcePortDescription": "Den eksterne porten på Pangolin-instansen eller noden der ressursen vil være tilgjengelig.",
@@ -425,6 +427,31 @@
"accessRolesAdd": "Legg til rolle",
"accessRoleDelete": "Slett rolle",
"accessApprovalsManage": "Behandle godkjenninger",
"resourceSessionsManage": "Administrer ressursøkter",
"resourceSessionsDescription": "Se hvem som for øyeblikket har en aktiv økt på dine offentlige ressurser og tilbakekall tilgang",
"resourceSessionsSearch": "Søk etter bruker, e-post, lenke eller ressurs...",
"resourceSessionsMethod": "Metode",
"resourceSessionsIssued": "Utsendt",
"resourceSessionsAnonymous": "Anonym",
"resourceSessionsFilterAnyUser": "Alle brukere",
"resourceSessionsFilteredToSession": "Viser én økt",
"resourceSessionsMethodSso": "SSO",
"resourceSessionsMethodPassword": "Passord",
"resourceSessionsMethodPincode": "PIN-kode",
"resourceSessionsMethodWhitelist": "E-post",
"resourceSessionsMethodAccessToken": "Delbar lenke",
"resourceSessionsRevoke": "Tilbakekall",
"resourceSessionsRevokeTitle": "Tilbakekall økt",
"resourceSessionsRevokeQuestion": "Tilbakekall økten for {identity} på {resource}? De må autentisere seg igjen for å få tilgang til ressursen.",
"resourceSessionsRevokeSsoNote": "Denne brukeren er logget inn med SSO. Alle deres økter for denne ressursen fra samme pålogging vil bli tilbakekalt, og de må logge inn igjen for å få tilgang til den, selv om de fortsatt er logget inn på Pangolin.",
"resourceSessionsRevoked": "Økt tilbakekalt",
"resourceSessionsRevokedDescription": "Økten har blitt tilbakekalt. Det kan ta noen sekunder før den trer i kraft.",
"resourceSessionsErrorRevoke": "Mislyktes i å tilbakekalle økt",
"resourceSessionsErrorRevokeDescription": "Det oppstod en feil mens økten ble tilbakekalt",
"resourceSessionReauthRequired": "Din økt for denne ressursen ble avsluttet av en administrator. Vennligst logg inn igjen for å fortsette.",
"logLinks": "Lenker",
"logLinkUser": "Bruker",
"logLinkResourceSession": "Ressursøkt",
"accessApprovalsDescription": "Se og administrer ventende godkjenninger for tilgang til denne organisasjonen",
"description": "Beskrivelse",
"inviteTitle": "Åpne invitasjoner",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "Det er ingen begrensning på antall brukere for denne verten.",
"licenseUsersUsedMax": "{usedUsers} av {maxUsers} brukere brukt",
"licenseUsersUsed": "{count, plural, =0 {ingen brukere} one {en bruker} other {# brukere}}",
"licenseUsageMachines": "Maskinklienter",
"licenseNoMachineLimit": "Det er ingen grense på antall maskinklienter for denne verten.",
"licenseMachinesUsedMax": "{usedMachines} av {maxMachines} maskinklienter brukt",
"licenseMachinesUsed": "{count, plural, =0 {# maskinklienter} one {# maskinklient} other {# maskinklienter}}",
"licenseUnlimited": "Ubegrenset",
"licenseTierLabel": "Nivå",
"licensePurchaseDescription": "Velg hvor mange områder du vil {selectedMode, select, license {kjøpe en lisens for. Du kan alltid legge til flere områder senere.} other {legge til din eksisterende lisens.}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "Brukeren har blitt oppdatert.",
"autoProvisioned": "Auto avlyst",
"autoProvisionSettings": "Auto leveringsinnstillinger",
"autoProvisionedDescription": "Tillat denne brukeren å bli automatisk administrert av en identitetsleverandør",
"autoProvisionedDescription": "Hvis identitetsleverandøren har aktivert automatisk klargjøring, tillat at denne brukeren administreres automatisk.",
"accessControlsDescription": "Administrer hva denne brukeren kan få tilgang til og gjøre i organisasjonen",
"accessControlsSubmit": "Lagre tilgangskontroller",
"singleRolePerUserPlanNotice": "Din plan støtter bare én rolle per bruker.",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "Distribuer forespørsler jevnt over mål i rotasjon.",
"loadBalancingStrategy_p2c": "To tilfeldig valg",
"loadBalancingStrategy_p2c_description": "Velg to tilfeldige mål og send forespørselen til den med færre aktive forbindelser.",
"loadBalancingStrategy_hrw": "Klient IP-hash",
"loadBalancingStrategy_hrw": "Høyeste tilfeldige vekt",
"loadBalancingStrategy_hrw_description": "Hash klient-IP-en slik at samme klient konsekvent rutes til samme mål.",
"loadBalancingStrategy_leasttime": "Raskest svartid",
"loadBalancingStrategy_leasttime_description": "Send forespørsler til målet med raskest svartid og færrest aktive forbindelser.",
@@ -829,6 +860,8 @@
"save": "Lagre",
"proxyAdditional": "Ytterligere Proxy-innstillinger",
"proxyAdditionalDescription": "Konfigurer hvordan ressursen håndterer proxy-innstillingene",
"proxyForwardIdentityHeaders": "Videresend identitetsoverskrifter",
"proxyForwardIdentityHeadersDescription": "Send den autentiserte brukerens identitet til backend i overskriftene Remote-User, Remote-Email, Remote-Name, Remote-Role og Remote-User-Id. Deaktiver for backend-systemer som avviser forespørsler med ukjente overskrifter.",
"proxyCustomHeader": "Tilpasset verts-header",
"proxyCustomHeaderDescription": "Verts-header som skal settes ved videresending av forespørsler. La stå tom for å bruke standardinnstillingen.",
"proxyAdditionalSubmit": "Lagre proxy-innstillinger",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "Grunnleggende Header Autentisering",
"policyAuthHeaderAuthDescription": "Bekreft et tilpasset HTTP-headernavn og verdi ved hver forespørsel",
"policyAuthHeaderAuthSummary": "Header konfigurert",
"policyAuthMtlsTitle": "Klientsertifikat (mTLS)",
"policyAuthMtlsDescription": "Krev et klientsertifikat signert av en betrodd CA under TLS-håndtrykket",
"policyAuthMtlsSummary": "{count} betrodde CA-sertifikater",
"policyAuthHeaderName": "Brukernavn",
"policyAuthHeaderValue": "Passord",
"policyAuthSetPasscode": "Angi passordkode",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "Automatisk brukerklargjøring",
"idpAutoProvisionUsersDescription": "Når aktivert, opprettes brukere automatisk i systemet ved første innlogging, med mulighet til å tilordne brukere til roller og organisasjoner.",
"idpAutoProvisionConfigureAfterCreate": "Du kan konfigurere autoprovisjonsinnstillingene når identitetsleverandøren er opprettet.",
"idpSessionRevalidation": "Kontinuerlig øktsesjonsrevalidering",
"idpSessionRevalidationDescription": "Når aktivert, revaliderer sesjoner periodisk med denne identitetsleverandøren. Hvis leverandøren tilbakekaller tilgang, avsluttes tilknyttede sesjoner.",
"idpSessionRevalidationConfigureAfterCreate": "Du kan konfigurere kontinuerlig øktsesjonsrevalidering når identitetsleverandøren er opprettet.",
"licenseBadge": "EE",
"idpType": "Leverandørtype",
"idpTypeDescription": "Velg typen identitetsleverandør du ønsker å konfigurere",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "Slett domene",
"actionGetDNSRecords": "Hent DNS-oppføringer",
"actionRestartOrgDomain": "Omstart Domene",
"actionListCustomDnsRecords": "List tilpassede DNS-oppføringer",
"actionCreateCustomDnsRecord": "Opprett tilpasset DNS-oppføring",
"actionUpdateCustomDnsRecord": "Oppdater tilpasset DNS-oppføring",
"actionDeleteCustomDnsRecord": "Slett tilpasset DNS-oppføring",
"actionCreateSite": "Opprett område",
"actionDeleteSite": "Slett område",
"actionGetSite": "Hent område",
"actionListSites": "List opp områder",
"actionReGenerateSecret": "Regenerer brukeropplysninger",
"actionCreateAiProvider": "Opprett AI-leverandør",
"actionDeleteAiProvider": "Slett AI-leverandør",
"actionGetAiProvider": "Hent AI-leverandør",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "Generer tilgangstoken",
"actionDeleteAccessToken": "Slett tilgangstoken",
"actionListAccessTokens": "List opp tilgangstokener",
"actionListResourceSessions": "List opp ressursøkter",
"actionDeleteResourceSession": "Tilbakekall ressursøkt",
"actionCreateResourceSessionToken": "Opprett ressurs økt-token",
"actionCreateResourceRule": "Opprett ressursregel",
"actionDeleteResourceRule": "Slett ressursregel",
"actionListResourceRules": "List opp ressursregler",
"actionUpdateResourceRule": "Oppdater ressursregel",
"actionCreateResourceMtlsCertificate": "Opprett mTLS-sertifikat for ressurs",
"actionDeleteResourceMtlsCertificate": "Slett mTLS-sertifikat for ressurs",
"actionListResourceMtlsCertificates": "List opp mTLS-sertifikater for ressurser",
"actionListOrgLabels": "List opp etiketter",
"actionCreateOrgLabel": "Opprett etikett",
"actionUpdateOrgLabel": "Oppdater etikett",
"actionDeleteOrgLabel": "Slett etikett",
"actionAttachLabelToItem": "Legg til etikett",
"actionDetachLabelFromItem": "Fjern etikett",
"actionCreateEventStreamingDestination": "Opprett mål for strømmeavvikling",
"actionUpdateEventStreamingDestination": "Oppdater mål for strømmeavvikling",
"actionDeleteEventStreamingDestination": "Slett mål for strømmeavvikling",
"actionListEventStreamingDestinations": "List opp mål for strømmeavvikling",
"actionCreateAlertRule": "Opprett varslingsregel",
"actionUpdateAlertRule": "Oppdater varslingsregel",
"actionDeleteAlertRule": "Slett varslingsregel",
"actionGetAlertRule": "Hent varslingsregel",
"actionListAlertRules": "List opp varslingsregler",
"actionCreateHealthCheck": "Opprett helsekontroll",
"actionUpdateHealthCheck": "Oppdater helsekontroll",
"actionDeleteHealthCheck": "Slett helsekontroll",
"actionListHealthChecks": "List opp helsekontroller",
"actionListOrgs": "List opp organisasjoner",
"actionCheckOrgId": "Sjekk ID",
"actionCreateOrg": "Opprett organisasjon",
@@ -1726,6 +1794,7 @@
"sidebarHome": "Hjem",
"sidebarSites": "Områder",
"sidebarApprovals": "Godkjenningsforespørsler",
"sidebarResourceSessions": "Ressursøkter",
"sidebarResources": "Ressurser",
"sidebarProxyResources": "Offentlig",
"sidebarClientResources": "Privat",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "Offentlige ressurser regler",
"commandIdentityProviders": "Identitetsleverandører",
"commandApprovals": "Godkjenningsforespørsler",
"commandResourceSessions": "Ressursøkter",
"commandShareableLinks": "Delbare lenker",
"commandOrganization": "Organisasjon",
"commandLogsAndAnalytics": "Logg & Analyse",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "Logger for autentisering",
"commandLogsAction": "Handlingslogger",
"commandLogsConnection": "Loggfiler for tilkobling",
"commandLogsSsh": "SSH-øktlogger",
"commandLogsStreaming": "Strømming",
"commandManagement": "Administrasjon",
"commandAlerting": "Varsler",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "Hvor lenge du vil beholde tilkoblingslogger",
"logRetentionAISessionsLabel": "Bevaringsinnstillinger AI-portal sesjonslogger",
"logRetentionAISessionsDescription": "Hvor lenge man skal bevare stikkord/response sesjonslogger for AI-portaler",
"logRetentionSshLabel": "Bevaringsinnstillinger for SSH-øktlogger",
"logRetentionSshDescription": "Hvor lenge du vil beholde SSH-økt- og kommando logger",
"logRetentionDisabled": "Deaktivert",
"logRetention3Days": "3 dager",
"logRetention7Days": "7 dager",
@@ -3707,6 +3780,27 @@
"connectionLogs": "Loggfiler for tilkobling",
"connectionLogsDescription": "Vis tilkoblingslogger for tunneler i denne organisasjonen",
"sidebarLogsConnection": "Loggfiler for tilkobling",
"sidebarLogsSsh": "SSH-øktlogger",
"sshSessionLogs": "SSH-øktlogger",
"sshSessionLogsDescription": "Se SSH-økter og kommandoene som ble kjørt i dem på nettsteder i denne organisasjonen",
"sshSystemUser": "Systembruker",
"sshCommands": "Kommandoer",
"sshSessionId": "Sesjons-ID",
"sshAuthMethod": "Autentiseringsmetode",
"sshTransport": "Tilkobling",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "Nettleser",
"sshKeyFingerprint": "Nøkkelfingeravtrykk",
"sshCertKeyId": "Sertifikat Nøkkel-ID",
"sshStartedAt": "Startet",
"sshEndedAt": "Avsluttet",
"sshSessionLoadingCommands": "Laster kommandoer...",
"sshSessionCommandsError": "Mislyktes å laste kommandoer for denne økten",
"sshSessionNoCommands": "Ingen kommandoer registrert for denne økten",
"sshSessionRecalledCommand": "(husket fra skallhistorikken)",
"sshSessionInexactNotice": "Noen kommandoer ble redigert med historikkgjenkalling eller tabulatorfullføring og kan ikke nøyaktig samsvare med det skallet kjørte.",
"sshSessionInexactTooltip": "Redigert med historikkgjenkalling eller tabulatorfullføring; kan ikke nøyaktig samsvare med det skallet kjørte",
"sshSessionCommandsTruncated": "Viser de første {shown} av {total} kommandoene. Eksporter loggene for å se alle.",
"sidebarLogsStreaming": "Strømming",
"sourceAddress": "Kilde adresse",
"destinationAddress": "Måladresse (Automatic Translation)",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "Hvordan legge til poster",
"dnsRecord": "DNS registre",
"domainTabSetup": "Oppsett",
"domainTabRecords": "Oppføringer",
"domainSetupRecordsDescription": "Opprett disse oppføringene hos domeneregistratoren eller nåværende DNS-tilbyder. Pangolin bruker dem til å verifisere domenet og rute trafikken.",
"domainRecordsInfoTitle": "Oppføringer betjent av Pangolin",
"domainRecordsInfoDescription": "Pangolins navneservere besvarer DNS-forespørsler for {domain}. Oppføringene du legger til her betjenes direkte fra Pangolin, så du trenger ikke å opprette dem hos registratoren.",
"customDnsRecords": "Tilpassede DNS-oppføringer",
"customDnsRecordsAdd": "Legg til oppføring",
"customDnsRecordsSearch": "Søk i oppføringer...",
"customDnsRecordsUnverifiedTitle": "Oppføringer serveres ikke ennå",
"customDnsRecordsUnverifiedDescription": "Dette domenet er ikke verifisert. Du kan legge til oppføringer nå, men Pangolins navneservere vil ikke tjene dem før domenets navneservere peker til Pangolin og verifiseringen er fullført.",
"customDnsRecordCreate": "Legg til DNS-oppføring",
"customDnsRecordEdit": "Rediger DNS-oppføring",
"customDnsRecordFormDescription": "Denne oppføringen vil bli servert for {domain}. Endringer kan ta noen minutter å oppdatere.",
"customDnsRecordNameDescription": "Bruk @ for roten av domenet eller * som den første etiketten for et jokertegn.",
"customDnsRecordValueDescription": "Skriv inn én verdi per linje, f.eks. begge navneservere av en NS-oppføring.",
"customDnsRecordPriority": "Prioritet",
"customDnsRecordWeight": "Vekt",
"customDnsRecordPort": "Port",
"customDnsRecordCreated": "DNS-oppføring opprettet",
"customDnsRecordUpdated": "DNS-oppføring oppdatert",
"customDnsRecordSaveError": "Klarte ikke å lagre DNS-oppføring",
"customDnsRecordDeleted": "DNS-oppføring slettet",
"customDnsRecordDeleteError": "Klarte ikke å slette DNS-oppføring",
"customDnsRecordDeleteTitle": "Slett DNS-oppføring",
"customDnsRecordDeleteQuestion": "Er du sikker på at du vil slette {type}-oppføringen for {name}? Den vil slutte å løse seg opp når hurtigbufrede svar utløper.",
"customDnsRecordDeleteConfirm": "Slett oppføring",
"required": "Påkrevd",
"domainSettingsUpdated": "Domene innstillinger ble oppdatert",
"orgOrDomainIdMissing": "ID for organisasjon eller domene mangler",
@@ -3938,7 +4058,7 @@
"backToHome": "Gå tilbake til start",
"needToSignInToOrg": "Trenger du å bruke organisasjonens identitetsleverandør?",
"mtls": "mTLS",
"mtlsCaCertificates": "Pålitelige CA-sertifikater",
"mtlsCaCertificates": "Angi klientsertifikater",
"mtlsCaCertificatesDescription": "Klienter må presentere et sertifikat signert av en av disse sertifikatmyndighetene for å koble til. Last kun opp offentlige CA-sertifikater, aldri en privat nøkkel.",
"mtlsCertificateName": "Navn (valgfritt)",
"mtlsCertificateNamePlaceholder": "Bedriftsenhet CA",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "Sertifikatet kunne ikke fjernes.",
"mtlsLastCertificateRemoved": "mTLS ble slått av fordi det siste CA-sertifikatet ble fjernet.",
"mtlsNoCertificates": "Ingen CA-sertifikater er lagt til ennå",
"mtlsPendingCertificate": "CA-sertifikat {number}",
"mtlsCertificateInvalidPem": "Fant ingen sertifikat kodet i PEM-format. Forventet en blokk som starter med -----BEGIN CERTIFICATE-----.",
"mtlsCertificatePrivateKey": "Dette ser ut som en privat nøkkel, ikke et sertifikat. Last opp bare CAs offentlige sertifikat.",
"mtlsCertificateDuplicate": "Dette CA-sertifikatet er allerede lagt til.",
"mtlsRemoveCertificate": "Fjern CA-sertifikat",
"mtlsExpired": "Utløpt",
"mtlsExpiresSoon": "Utløper snart",
+127 -3
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "Stuur al het internetverkeer om via het sitenetwerk.",
"resourceTypeGatewayDescription": "Stuur al het internetverkeer om via het sitenetwerk.",
"resourceDomainDescription": "De bron zal worden bediend onder deze volledig gekwalificeerde domeinnaam.",
"resourceMatchBaseDomain": "Ook overeenkomen met {host}",
"resourceMatchBaseDomainDescription": "Een wildcard komt alleen overeen met hostnamen onder {host}. Schakel dit in om {host} ook via deze bron te routeren.",
"resourceHTTPSSettings": "HTTPS instellingen",
"resourceHTTPSSettingsDescription": "Stel in hoe de bron wordt benaderd via HTTPS",
"resourcePortDescription": "De externe poort op de Pangolin-instantie of -node waar de bron toegankelijk zal zijn.",
@@ -425,6 +427,31 @@
"accessRolesAdd": "Rol toevoegen",
"accessRoleDelete": "Verwijder rol",
"accessApprovalsManage": "Goedkeuringen beheren",
"resourceSessionsManage": "Bronsessies beheren",
"resourceSessionsDescription": "Bekijk wie momenteel een actieve sessie heeft op uw openbare bronnen en trek de toegang in",
"resourceSessionsSearch": "Zoek op gebruiker, e-mail, link of bron...",
"resourceSessionsMethod": "Methode",
"resourceSessionsIssued": "Uitgegeven",
"resourceSessionsAnonymous": "Anoniem",
"resourceSessionsFilterAnyUser": "Alle gebruikers",
"resourceSessionsFilteredToSession": "Eén sessie weergeven",
"resourceSessionsMethodSso": "SSO",
"resourceSessionsMethodPassword": "Wachtwoord",
"resourceSessionsMethodPincode": "Pincode",
"resourceSessionsMethodWhitelist": "E-mail",
"resourceSessionsMethodAccessToken": "Deelbare Link",
"resourceSessionsRevoke": "Intrekken",
"resourceSessionsRevokeTitle": "Sessie intrekken",
"resourceSessionsRevokeQuestion": "De sessie intrekken voor {identity} op {resource}? Ze moeten opnieuw verifiëren om toegang te verkrijgen tot de bron.",
"resourceSessionsRevokeSsoNote": "Deze gebruiker is aangemeld via SSO. Al hun sessies voor deze bron vanuit dezelfde inlog moeten opnieuw worden aangemeld, zelfs als ze nog steeds zijn ingelogd bij Pangolin.",
"resourceSessionsRevoked": "Sessie ingetrokken",
"resourceSessionsRevokedDescription": "De sessie is ingetrokken. Het kan enkele seconden duren voordat dit effect heeft.",
"resourceSessionsErrorRevoke": "Intrekken van sessie mislukt",
"resourceSessionsErrorRevokeDescription": "Er is een fout opgetreden bij het intrekken van de sessie",
"resourceSessionReauthRequired": "Uw sessie voor deze bron is door een beheerder beëindigd. Gelieve opnieuw in te loggen om verder te gaan.",
"logLinks": "Links",
"logLinkUser": "Gebruiker",
"logLinkResourceSession": "Sessiedetails van de bron",
"accessApprovalsDescription": "Bekijk en beheer openstaande goedkeuringen voor toegang tot deze organisatie",
"description": "Beschrijving",
"inviteTitle": "Open uitnodigingen",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "Er is geen limiet op het aantal gebruikers voor deze host.",
"licenseUsersUsedMax": "{usedUsers} van {maxUsers} gebruikers gebruikt",
"licenseUsersUsed": "{count, plural, =0 {# gebruikers} one {# gebruiker} other {# gebruikers}}",
"licenseUsageMachines": "Machineclients",
"licenseNoMachineLimit": "Er is geen limiet op het aantal machineclients voor deze host.",
"licenseMachinesUsedMax": "{usedMachines} van {maxMachines} machineclients gebruikt",
"licenseMachinesUsed": "{count, plural, =0 {# machineclients} one {# machineclient} other {# machineclients}}",
"licenseUnlimited": "Onbeperkt",
"licenseTierLabel": "Niveau",
"licensePurchaseDescription": "Kies hoeveel sites je wilt {selectedMode, select, license {Koop een licentie. Je kunt later altijd meer sites toevoegen.} other {Voeg je bestaande licentie toe}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "De gebruiker is bijgewerkt.",
"autoProvisioned": "Automatisch bevestigen",
"autoProvisionSettings": "Auto Provisie Instellingen",
"autoProvisionedDescription": "Toestaan dat deze gebruiker automatisch wordt beheerd door een identiteitsprovider",
"autoProvisionedDescription": "Als de identiteitsprovider automatische toewijzing heeft ingeschakeld, staat u toe dat deze gebruiker automatisch beheerd wordt.",
"accessControlsDescription": "Beheer wat deze gebruiker toegang heeft tot en doet in de organisatie",
"accessControlsSubmit": "Bewaar Toegangsbesturing",
"singleRolePerUserPlanNotice": "Uw plan ondersteunt slechts één rol per gebruiker.",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "Verdeel aanvragen gelijkmatig over doelen in rotatie.",
"loadBalancingStrategy_p2c": "Kracht van Twee Kiezen",
"loadBalancingStrategy_p2c_description": "Kies twee willekeurige doelen en stuur het verzoek naar degene met de minste actieve verbindingen.",
"loadBalancingStrategy_hrw": "Client IP-hash",
"loadBalancingStrategy_hrw": "Hoogste Willekeurige Gewicht",
"loadBalancingStrategy_hrw_description": "Hash het client-IP, zodat dezelfde client consequent naar hetzelfde doel wordt gerouteerd.",
"loadBalancingStrategy_leasttime": "Minimum reactietijd",
"loadBalancingStrategy_leasttime_description": "Verstuur aanvragen naar het doel met de snelste reactietijd en de minste actieve verbindingen.",
@@ -829,6 +860,8 @@
"save": "Opslaan",
"proxyAdditional": "Extra Proxy-instellingen",
"proxyAdditionalDescription": "Configureer hoe de bron omgaat met proxy-instellingen",
"proxyForwardIdentityHeaders": "Identiteitsheaders doorsturen",
"proxyForwardIdentityHeadersDescription": "Stuur de identiteit van de geauthenticeerde gebruiker naar de backend in de headers Remote-User, Remote-Email, Remote-Name, Remote-Role en Remote-User-Id. Uitschakelen voor backends die verzoeken met onbekende headers afwijzen.",
"proxyCustomHeader": "Aangepaste Host-header",
"proxyCustomHeaderDescription": "De hostkop om in te stellen bij proxying verzoeken. Laat leeg om de standaard te gebruiken.",
"proxyAdditionalSubmit": "Proxyinstellingen opslaan",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "Basic Header Authenticatie",
"policyAuthHeaderAuthDescription": "Valideer een aangepaste HTTP-headernaam en waarde bij elk verzoek",
"policyAuthHeaderAuthSummary": "Header geconfigureerd",
"policyAuthMtlsTitle": "Clientcertificaat (mTLS)",
"policyAuthMtlsDescription": "Vereis een clientcertificaat ondertekend door een vertrouwde CA tijdens de TLS-handshake",
"policyAuthMtlsSummary": "{count} vertrouwde CA-certificaten",
"policyAuthHeaderName": "Gebruikersnaam",
"policyAuthHeaderValue": "Wachtwoord",
"policyAuthSetPasscode": "Stel toegangscode in",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "Auto Provisie Gebruikers",
"idpAutoProvisionUsersDescription": "Wanneer ingeschakeld, worden gebruikers automatisch in het systeem aangemaakt wanneer ze de eerste keer inloggen met de mogelijkheid om gebruikers toe te wijzen aan rollen en organisaties.",
"idpAutoProvisionConfigureAfterCreate": "U kunt automatische voorzieningsinstellingen configureren zodra de identiteitsprovider is aangemaakt.",
"idpSessionRevalidation": "Continue sessieherziening",
"idpSessionRevalidationDescription": "Wanneer ingeschakeld, periodiek sessies herzien met deze identiteitsprovider. Als de provider de toegang intrekt, worden gekoppelde sessies beëindigd.",
"idpSessionRevalidationConfigureAfterCreate": "U kunt continue sessieherziening configureren zodra de identiteitsprovider is aangemaakt.",
"licenseBadge": "EE",
"idpType": "Type provider",
"idpTypeDescription": "Selecteer het type identiteitsprovider dat u wilt configureren",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "Domein verwijderen",
"actionGetDNSRecords": "Krijg DNS Records",
"actionRestartOrgDomain": "Domein opnieuw starten",
"actionListCustomDnsRecords": "Lijst aangepaste DNS-records",
"actionCreateCustomDnsRecord": "Aangepast DNS-record maken",
"actionUpdateCustomDnsRecord": "Aangepast DNS-record bijwerken",
"actionDeleteCustomDnsRecord": "Aangepast DNS-record verwijderen",
"actionCreateSite": "Site aanmaken",
"actionDeleteSite": "Site verwijderen",
"actionGetSite": "Site ophalen",
"actionListSites": "Sites weergeven",
"actionReGenerateSecret": "Referenties opnieuw genereren",
"actionCreateAiProvider": "AI-provider aanmaken",
"actionDeleteAiProvider": "Verwijder AI-provider",
"actionGetAiProvider": "AI-provider ophalen",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "Genereer Toegangstoken",
"actionDeleteAccessToken": "Verwijder toegangstoken",
"actionListAccessTokens": "Lijst toegangstokens",
"actionListResourceSessions": "Lijst met resourcesessies",
"actionDeleteResourceSession": "Bronsessie intrekken",
"actionCreateResourceSessionToken": "Maak Resource Sessie Token",
"actionCreateResourceRule": "Bronregel aanmaken",
"actionDeleteResourceRule": "Verwijder Resource Regel",
"actionListResourceRules": "Bron regels weergeven",
"actionUpdateResourceRule": "Bronregel bewerken",
"actionCreateResourceMtlsCertificate": "Bron mTLS-certificaat aanmaken",
"actionDeleteResourceMtlsCertificate": "Bron mTLS-certificaat verwijderen",
"actionListResourceMtlsCertificates": "Lijst met mTLS-certificaten van de bron",
"actionListOrgLabels": "Labels weergeven",
"actionCreateOrgLabel": "Label aanmaken",
"actionUpdateOrgLabel": "Label bijwerken",
"actionDeleteOrgLabel": "Label verwijderen",
"actionAttachLabelToItem": "Label bijvoegen",
"actionDetachLabelFromItem": "Label loskoppelen",
"actionCreateEventStreamingDestination": "Bestemming voor gebeurtenisstreaming aanmaken",
"actionUpdateEventStreamingDestination": "Bestemming voor gebeurtenisstreaming bijwerken",
"actionDeleteEventStreamingDestination": "Bestemming voor gebeurtenisstreaming verwijderen",
"actionListEventStreamingDestinations": "Lijst met bestemmingen voor gebeurtenisstreaming",
"actionCreateAlertRule": "Waarschuwingsregel aanmaken",
"actionUpdateAlertRule": "Waarschuwingsregel bijwerken",
"actionDeleteAlertRule": "Waarschuwingsregel verwijderen",
"actionGetAlertRule": "Waarschuwingsregel ophalen",
"actionListAlertRules": "Lijst met waarschuwingsregels",
"actionCreateHealthCheck": "Gezondheidscontrole aanmaken",
"actionUpdateHealthCheck": "Gezondheidscontrole bijwerken",
"actionDeleteHealthCheck": "Gezondheidscontrole verwijderen",
"actionListHealthChecks": "Gezondheidscontroles weergeven",
"actionListOrgs": "Organisaties weergeven",
"actionCheckOrgId": "ID controleren",
"actionCreateOrg": "Nieuwe organisatie aanmaken",
@@ -1726,6 +1794,7 @@
"sidebarHome": "Startpagina",
"sidebarSites": "Sites",
"sidebarApprovals": "Goedkeuringsverzoeken",
"sidebarResourceSessions": "Resourcesessies",
"sidebarResources": "Bronnen",
"sidebarProxyResources": "Openbaar",
"sidebarClientResources": "Privé",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "Openbare Bronnenbeleid",
"commandIdentityProviders": "Identiteitsproviders",
"commandApprovals": "Goedkeuring Aanvragen",
"commandResourceSessions": "Resourcesessies",
"commandShareableLinks": "Deelbare Links",
"commandOrganization": "Organisatie",
"commandLogsAndAnalytics": "Logboeken & Analytics",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "Toegang tot logboek",
"commandLogsAction": "Actie Logboeken",
"commandLogsConnection": "Connectie Logs",
"commandLogsSsh": "SSH-sessielogboeken",
"commandLogsStreaming": "Streamen",
"commandManagement": "Beheren",
"commandAlerting": "Waarschuwingen",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "Hoe lang de verbindingslogs onderhouden",
"logRetentionAISessionsLabel": "AI Gateway Sessie Log Retentie",
"logRetentionAISessionsDescription": "Hoe lang AI-gateway prompt/response sessielogs te behouden",
"logRetentionSshLabel": "SSH-sessielogboekomvang",
"logRetentionSshDescription": "Hoe lang de SSH- en commando-logboeken behouden",
"logRetentionDisabled": "Uitgeschakeld",
"logRetention3Days": "3 dagen",
"logRetention7Days": "7 dagen",
@@ -3707,6 +3780,27 @@
"connectionLogs": "Connectie Logs",
"connectionLogsDescription": "Toon verbindingslogs voor tunnels in deze organisatie",
"sidebarLogsConnection": "Connectie Logs",
"sidebarLogsSsh": "SSH-sessielogboeken",
"sshSessionLogs": "SSH-sessielogboeken",
"sshSessionLogsDescription": "Bekijk SSH-sessies en de opdrachten die erin zijn uitgevoerd op sites binnen deze organisatie",
"sshSystemUser": "Systeemgebruiker",
"sshCommands": "Opdrachten",
"sshSessionId": "Sessie-ID",
"sshAuthMethod": "Authenticatiemethode",
"sshTransport": "Verbinding",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "Browser",
"sshKeyFingerprint": "Sleutelfingerprint",
"sshCertKeyId": "Certificaatsleutel-id",
"sshStartedAt": "Gestart op",
"sshEndedAt": "Beëindigd op",
"sshSessionLoadingCommands": "Opdrachten laden...",
"sshSessionCommandsError": "Opdrachten voor deze sessie niet geladen",
"sshSessionNoCommands": "Geen opdrachten geregistreerd voor deze sessie",
"sshSessionRecalledCommand": "(teruggeroepen uit shell-geschiedenis)",
"sshSessionInexactNotice": "Sommige opdrachten zijn bewerkt met geschiedenis-oproep of tab-aanvulling en kunnen niet exact overeenkomen met wat de shell heeft uitgevoerd.",
"sshSessionInexactTooltip": "Bewerkt met geschiedenis-oproep of tab-aanvulling; kan niet exact overeenkomen met wat de shell heeft uitgevoerd",
"sshSessionCommandsTruncated": "Toont de eerste {shown} van {total} opdrachten. Exporteer de logs om ze allemaal te zien.",
"sidebarLogsStreaming": "Streamen",
"sourceAddress": "Bron adres",
"destinationAddress": "Adres bestemming",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "Hoe voeg ik Records toe",
"dnsRecord": "DNS-records",
"domainTabSetup": "Instellen",
"domainTabRecords": "Aantekeningen",
"domainSetupRecordsDescription": "Maak deze records aan bij uw domeinregistrar of huidige DNS-provider. Pangolin gebruikt ze om het domein te verifiëren en het verkeer te routeren.",
"domainRecordsInfoTitle": "Records bediend door Pangolin",
"domainRecordsInfoDescription": "Pangolin's nameservers beantwoorden DNS-queries voor {domain}. Records die u hier toevoegt, worden rechtstreeks van Pangolin geserveerd, zodat u ze niet bij uw registrar hoeft aan te maken.",
"customDnsRecords": "Aangepaste DNS-records",
"customDnsRecordsAdd": "Record toevoegen",
"customDnsRecordsSearch": "Zoek records...",
"customDnsRecordsUnverifiedTitle": "Records worden nog niet geserveerd",
"customDnsRecordsUnverifiedDescription": "Dit domein is niet geverifieerd. U kunt nu records toevoegen, maar Pangolin's nameservers zullen ze pas serveren wanneer de nameservers van het domein naar Pangolin wijzen en de verificatie voltooid is.",
"customDnsRecordCreate": "DNS-record toevoegen",
"customDnsRecordEdit": "DNS-record bewerken",
"customDnsRecordFormDescription": "Dit record wordt bediend voor {domain}. Wijzigingen kunnen enkele minuten duren om zich te propageren.",
"customDnsRecordNameDescription": "Gebruik @ voor de root van het domein of * als eerste label voor een wildcard.",
"customDnsRecordValueDescription": "Voer één waarde per regel in, bijvoorbeeld beide nameservers van een NS-record.",
"customDnsRecordPriority": "Prioriteit",
"customDnsRecordWeight": "Gewicht",
"customDnsRecordPort": "Poort",
"customDnsRecordCreated": "DNS-record aangemaakt",
"customDnsRecordUpdated": "DNS-record bijgewerkt",
"customDnsRecordSaveError": "Kon DNS-record niet opslaan",
"customDnsRecordDeleted": "DNS-record verwijderd",
"customDnsRecordDeleteError": "Kon DNS-record niet verwijderen",
"customDnsRecordDeleteTitle": "DNS-record verwijderen",
"customDnsRecordDeleteQuestion": "Weet u zeker dat u het {type} record voor {name} wilt verwijderen? Het zal ophouden met oplossen zodra de gecachte antwoorden verlopen.",
"customDnsRecordDeleteConfirm": "Record verwijderen",
"required": "vereist",
"domainSettingsUpdated": "Domeininstellingen succesvol bijgewerkt",
"orgOrDomainIdMissing": "Organisatie of domein ID ontbreekt",
@@ -3938,7 +4058,7 @@
"backToHome": "Ga terug naar de startpagina",
"needToSignInToOrg": "Moet u de identiteit provider van uw organisatie gebruiken?",
"mtls": "mTLS",
"mtlsCaCertificates": "Vertrouwde CA-certificaten",
"mtlsCaCertificates": "Clientcertificaten instellen",
"mtlsCaCertificatesDescription": "Clients moeten een certificaat presenteren dat is ondertekend door een van deze certificeringsinstanties om verbinding te maken. Upload alleen openbare CA-certificaten, nooit een privésleutel.",
"mtlsCertificateName": "Naam (optioneel)",
"mtlsCertificateNamePlaceholder": "CA-certificaat apparaat",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "Het certificaat kon niet worden verwijderd.",
"mtlsLastCertificateRemoved": "mTLS is uitgeschakeld omdat het laatste CA-certificaat is verwijderd.",
"mtlsNoCertificates": "Geen CA-certificaten toegevoegd",
"mtlsPendingCertificate": "CA-certificaat {number}",
"mtlsCertificateInvalidPem": "Geen PEM-gecodeerd certificaat gevonden. Verwacht een blok beginnend met -----BEGIN CERTIFICATE-----.",
"mtlsCertificatePrivateKey": "Dit lijkt op een privésleutel, niet op een certificaat. Upload alleen het publieke certificaat van de CA.",
"mtlsCertificateDuplicate": "Dit CA-certificaat is al toegevoegd.",
"mtlsRemoveCertificate": "CA-certificaat verwijderen",
"mtlsExpired": "Verlopen",
"mtlsExpiresSoon": "Verloopt binnenkort",
+127 -3
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "Przekieruj cały ruch internetowy przez sieć witryny.",
"resourceTypeGatewayDescription": "Przekieruj cały ruch internetowy przez sieć witryny.",
"resourceDomainDescription": "Zasób będzie udostępniany pod tym w pełni kwalifikowanym adresem domenowym.",
"resourceMatchBaseDomain": "Również dopasuj {host}",
"resourceMatchBaseDomainDescription": "Symbol wieloznaczny pasuje tylko do nazw hostów pod {host}. Włącz to, aby również kierować {host} przez to zasoby.",
"resourceHTTPSSettings": "Ustawienia HTTPS",
"resourceHTTPSSettingsDescription": "Skonfiguruj jak zasób będzie dostępny przez HTTPS",
"resourcePortDescription": "Zewnętrzny port na instancji lub węźle Pangolina, gdzie zasób będzie dostępny.",
@@ -425,6 +427,31 @@
"accessRolesAdd": "Dodaj rolę",
"accessRoleDelete": "Usuń rolę",
"accessApprovalsManage": "Zarządzaj zatwierdzaniem",
"resourceSessionsManage": "Zarządzaj sesjami zasobów",
"resourceSessionsDescription": "Zobacz, kto obecnie ma aktywną sesję na publicznych zasobach i cofnij dostęp",
"resourceSessionsSearch": "Szukaj po użytkowniku, e-mailu, linku lub zasobie...",
"resourceSessionsMethod": "Metoda",
"resourceSessionsIssued": "Wydane",
"resourceSessionsAnonymous": "Anonimowy",
"resourceSessionsFilterAnyUser": "Wszyscy użytkownicy",
"resourceSessionsFilteredToSession": "Pokazano jedną sesję",
"resourceSessionsMethodSso": "Jednolity System Logowania",
"resourceSessionsMethodPassword": "Hasło",
"resourceSessionsMethodPincode": "Kod PIN",
"resourceSessionsMethodWhitelist": "E-mail",
"resourceSessionsMethodAccessToken": "Link do udostępnienia",
"resourceSessionsRevoke": "Cofnij",
"resourceSessionsRevokeTitle": "Cofnij sesję",
"resourceSessionsRevokeQuestion": "Cofnąć sesję dla {identity} na {resource}? Będą musieli się ponownie uwierzytelnić, aby uzyskać dostęp do zasobu.",
"resourceSessionsRevokeSsoNote": "Ten użytkownik jest zalogowany za pomocą SSO. Wszystkie ich sesje dla tego zasobu z tego samego logowania zostaną cofnięte, a oni będą musieli się ponownie zalogować, aby uzyskać do niego dostęp, nawet jeśli wciąż są zalogowani do Pangolina.",
"resourceSessionsRevoked": "Sesja cofnięta",
"resourceSessionsRevokedDescription": "Sesja została cofnięta. Może to potrwać kilka sekund, aby wprowadzić w życie.",
"resourceSessionsErrorRevoke": "Nie udało się cofnąć sesji",
"resourceSessionsErrorRevokeDescription": "Wystąpił błąd podczas cofania sesji",
"resourceSessionReauthRequired": "Administrator zakończył twoją sesję dla tego zasobu. Proszę zalogować się ponownie, aby kontynuować.",
"logLinks": "Linki",
"logLinkUser": "Użytkownik",
"logLinkResourceSession": "Sesja zasobu",
"accessApprovalsDescription": "Przeglądaj i zarządzaj oczekującymi zatwierdzeniami dostępu do tej organizacji",
"description": "Opis",
"inviteTitle": "Otwórz zaproszenia",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "Nie ma limitu liczby użytkowników dla tego hosta.",
"licenseUsersUsedMax": "{usedUsers} z {maxUsers} użytkowników wykorzystano",
"licenseUsersUsed": "{count, plural, =0 {# użytkowników} one {# użytkownik} few {# użytkownicy} many {# użytkowników} other {# użytkowników}}",
"licenseUsageMachines": "Klienci maszyn",
"licenseNoMachineLimit": "Nie ma limitu liczby klientów maszyn dla tego hosta.",
"licenseMachinesUsedMax": "{usedMachines} z {maxMachines} klientów maszyn używanych",
"licenseMachinesUsed": "{count, plural, =0 {# klientów maszyn} one {# klient maszyny} few {# klienci maszyn} many {# klientów maszyn} other {# klientów maszyn}}",
"licenseUnlimited": "Nieograniczona",
"licenseTierLabel": "Poziom",
"licensePurchaseDescription": "Wybierz ile witryn chcesz {selectedMode, select, license {kupić licencję. Zawsze możesz dodać więcej witryn później.} other {dodaj do swojej istniejącej licencji.}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "Użytkownik został zaktualizowany.",
"autoProvisioned": "Przesłane automatycznie",
"autoProvisionSettings": "Ustawienia automatycznego dostarczania",
"autoProvisionedDescription": "Pozwól temu użytkownikowi na automatyczne zarządzanie przez dostawcę tożsamości",
"autoProvisionedDescription": "Jeśli dostawca tożsamości ma włączoną automatyczną prowizję, pozwól zarządzać tym użytkownikiem automatycznie.",
"accessControlsDescription": "Zarządzaj tym, do czego użytkownik ma dostęp i co może robić w organizacji",
"accessControlsSubmit": "Zapisz kontrole dostępu",
"singleRolePerUserPlanNotice": "Twój plan obsługuje tylko jedną rolę na użytkownika.",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "Równomiernie rozdzielaj żądania między cele w rotacji.",
"loadBalancingStrategy_p2c": "Moc dwóch wyborów",
"loadBalancingStrategy_p2c_description": "Wybierz dwa losowe cele i wyślij żądanie do tego z mniejszą liczbą aktywnych połączeń.",
"loadBalancingStrategy_hrw": "Hash IP klienta",
"loadBalancingStrategy_hrw": "Najwyższa losowa waga",
"loadBalancingStrategy_hrw_description": "Zahasuj IP klienta, tak aby ten sam klient był konsekwentnie kierowany do tego samego celu.",
"loadBalancingStrategy_leasttime": "Najmniejszy czas odpowiedzi",
"loadBalancingStrategy_leasttime_description": "Wyślij żądania do celu z najszybszym czasem odpowiedzi i najmniejszą liczbą aktywnych połączeń.",
@@ -829,6 +860,8 @@
"save": "Zapisz",
"proxyAdditional": "Dodatkowe ustawienia proxy",
"proxyAdditionalDescription": "Skonfiguruj sposób obsługi ustawień proxy",
"proxyForwardIdentityHeaders": "Przekaż nagłówki tożsamości",
"proxyForwardIdentityHeadersDescription": "Przekaż tożsamość uwierzytelnionego użytkownika do zaplecza w nagłówkach Remote-User, Remote-Email, Remote-Name, Remote-Role i Remote-User-Id. Wyłącz dla zapleczy, które odrzucają żądania z niezrozumiałymi nagłówkami.",
"proxyCustomHeader": "Niestandardowy nagłówek hosta",
"proxyCustomHeaderDescription": "Nagłówek hosta do ustawienia podczas proxy żądań. Pozostaw puste, aby użyć domyślnego.",
"proxyAdditionalSubmit": "Zapisz ustawienia proxy",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "Podstawowe Uwierzytelnianie Nagłówka",
"policyAuthHeaderAuthDescription": "Walidacja niestandardowej nazwy i wartości nagłówka HTTP przy każdym żądaniu",
"policyAuthHeaderAuthSummary": "Skonfigurowany nagłówek",
"policyAuthMtlsTitle": "Certyfikat klienta (mTLS)",
"policyAuthMtlsDescription": "Wymagaj certyfikatu klienta podpisanego przez zaufany urząd certyfikacji podczas uścisku TLS",
"policyAuthMtlsSummary": "{count} zaufanych certyfikatów CA",
"policyAuthHeaderName": "Nazwa użytkownika",
"policyAuthHeaderValue": "Hasło",
"policyAuthSetPasscode": "Ustaw hasło dostępu",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "Automatyczne tworzenie użytkowników",
"idpAutoProvisionUsersDescription": "Gdy włączone, użytkownicy będą automatycznie tworzeni w systemie przy pierwszym logowaniu z możliwością mapowania użytkowników do ról i organizacji.",
"idpAutoProvisionConfigureAfterCreate": "Możesz skonfigurować automatyczne ustawienia provision, gdy dostawca tożsamości zostanie utworzony.",
"idpSessionRevalidation": "Ciągła ponowna walidacja sesji",
"idpSessionRevalidationDescription": "Gdy włączona, okresowo ponownie waliduje sesje z tym dostawcą tożsamości. Jeśli dostawca anuluje dostęp, powiązane sesje są zakończone.",
"idpSessionRevalidationConfigureAfterCreate": "Możesz skonfigurować ciągłą ponowną walidację sesji, gdy dostawca tożsamości zostanie utworzony.",
"licenseBadge": "EE",
"idpType": "Typ dostawcy",
"idpTypeDescription": "Wybierz typ dostawcy tożsamości, który chcesz skonfigurować",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "Usuń domenę",
"actionGetDNSRecords": "Pobierz rekordy DNS",
"actionRestartOrgDomain": "Zrestartuj domenę",
"actionListCustomDnsRecords": "Lista niestandardowych rekordów DNS",
"actionCreateCustomDnsRecord": "Utwórz niestandardowy rekord DNS",
"actionUpdateCustomDnsRecord": "Aktualizuj niestandardowy rekord DNS",
"actionDeleteCustomDnsRecord": "Usuń niestandardowy rekord DNS",
"actionCreateSite": "Utwórz witrynę",
"actionDeleteSite": "Usuń witrynę",
"actionGetSite": "Pobierz witrynę",
"actionListSites": "Lista witryn",
"actionReGenerateSecret": "Wygeneruj dane logowania",
"actionCreateAiProvider": "Utwórz Dostawcę AI",
"actionDeleteAiProvider": "Usuń Dostawcę AI",
"actionGetAiProvider": "Pobierz Dostawcę AI",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "Wygeneruj token dostępu",
"actionDeleteAccessToken": "Usuń token dostępu",
"actionListAccessTokens": "Lista tokenów dostępu",
"actionListResourceSessions": "Lista sesji zasobów",
"actionDeleteResourceSession": "Cofnij sesję zasobu",
"actionCreateResourceSessionToken": "Utwórz token sesji zasobu",
"actionCreateResourceRule": "Utwórz regułę zasobu",
"actionDeleteResourceRule": "Usuń regułę zasobu",
"actionListResourceRules": "Lista reguł zasobu",
"actionUpdateResourceRule": "Aktualizuj regułę zasobu",
"actionCreateResourceMtlsCertificate": "Utwórz certyfikat mTLS zasobu",
"actionDeleteResourceMtlsCertificate": "Usuń certyfikat mTLS zasobu",
"actionListResourceMtlsCertificates": "Lista certyfikatów mTLS zasobu",
"actionListOrgLabels": "Lista etykiet",
"actionCreateOrgLabel": "Utwórz etykietę",
"actionUpdateOrgLabel": "Aktualizuj etykietę",
"actionDeleteOrgLabel": "Usuń etykietę",
"actionAttachLabelToItem": "Dodaj etykietę",
"actionDetachLabelFromItem": "Odłącz etykietę",
"actionCreateEventStreamingDestination": "Utwórz miejsce docelowe strumieniowania wydarzeń",
"actionUpdateEventStreamingDestination": "Aktualizuj miejsce docelowe strumieniowania wydarzeń",
"actionDeleteEventStreamingDestination": "Usuń miejsce docelowe strumieniowania wydarzeń",
"actionListEventStreamingDestinations": "Lista miejsc docelowych strumieniowania wydarzeń",
"actionCreateAlertRule": "Utwórz regułę alarmową",
"actionUpdateAlertRule": "Aktualizuj regułę alarmową",
"actionDeleteAlertRule": "Usuń regułę alarmową",
"actionGetAlertRule": "Pobierz regułę alarmową",
"actionListAlertRules": "Lista reguł alarmowych",
"actionCreateHealthCheck": "Utwórz kontrolę zdrowia",
"actionUpdateHealthCheck": "Aktualizuj kontrolę zdrowia",
"actionDeleteHealthCheck": "Usuń kontrolę zdrowia",
"actionListHealthChecks": "Lista kontroli zdrowia",
"actionListOrgs": "Lista organizacji",
"actionCheckOrgId": "Sprawdź ID",
"actionCreateOrg": "Utwórz organizację",
@@ -1726,6 +1794,7 @@
"sidebarHome": "Strona główna",
"sidebarSites": "Witryny",
"sidebarApprovals": "Wnioski o zatwierdzenie",
"sidebarResourceSessions": "Sesje zasobów",
"sidebarResources": "Zasoby",
"sidebarProxyResources": "Publiczne",
"sidebarClientResources": "Prywatny",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "Polityki zasobów publicznych",
"commandIdentityProviders": "Dostawcy tożsamości",
"commandApprovals": "Żądania zatwierdzeń",
"commandResourceSessions": "Sesje zasobów",
"commandShareableLinks": "Linki do udostępnienia",
"commandOrganization": "Organizacja",
"commandLogsAndAnalytics": "Dzienniki i analizy",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "Dzienniki uwierzytelniania",
"commandLogsAction": "Dzienniki działań administratora",
"commandLogsConnection": "Dzienniki połączeń",
"commandLogsSsh": "Dzienniki sesji SSH",
"commandLogsStreaming": "Strumieniowanie zdarzeń",
"commandManagement": "Zarządzanie",
"commandAlerting": "Alarmy",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "Jak długo zachować dzienniki połączeń",
"logRetentionAISessionsLabel": "Retencja Dzienników Sesji Bramy AI",
"logRetentionAISessionsDescription": "Jak długo przechowywać dzienniki sesji podpowiedzi/odpowiedzi bramy AI",
"logRetentionSshLabel": "Retencja dzienników sesji SSH",
"logRetentionSshDescription": "Jak długo przechowywać dzienniki sesji i poleceń SSH",
"logRetentionDisabled": "Wyłączone",
"logRetention3Days": "3 dni",
"logRetention7Days": "7 dni",
@@ -3707,6 +3780,27 @@
"connectionLogs": "Dzienniki połączeń",
"connectionLogsDescription": "Wyświetl dzienniki połączeń dla tuneli w tej organizacji",
"sidebarLogsConnection": "Dzienniki połączeń",
"sidebarLogsSsh": "Dzienniki sesji SSH",
"sshSessionLogs": "Dzienniki sesji SSH",
"sshSessionLogsDescription": "Zobacz sesje SSH i polecenia wykonywane na nich w witrynach tej organizacji",
"sshSystemUser": "Użytkownik systemowy",
"sshCommands": "Polecenia",
"sshSessionId": "Identyfikator sesji",
"sshAuthMethod": "Metoda uwierzytelniania",
"sshTransport": "Połączenie",
"sshTransportSsh": "Szyfrowany Powłokowy Protokół",
"sshTransportBrowser": "Przeglądarka",
"sshKeyFingerprint": "Odpis kluczowy",
"sshCertKeyId": "Identyfikator klucza certyfikatu",
"sshStartedAt": "Rozpoczęto o",
"sshEndedAt": "Zakończono o",
"sshSessionLoadingCommands": "Ładowanie poleceń...",
"sshSessionCommandsError": "Nie udało się załadować poleceń dla tej sesji",
"sshSessionNoCommands": "Brak zarejestrowanych poleceń dla tej sesji",
"sshSessionRecalledCommand": "(przypomniano z historii powłoki)",
"sshSessionInexactNotice": "Niektóre polecenia zostały edytowane z przypomnieniem historii lub autouzupełnieniem kartą i mogą nie dokładnie odpowiadać temu, co uruchomiono w powłoce.",
"sshSessionInexactTooltip": "Edytowano z przypomnieniem historii lub autouzupełnieniem kartą; mogą nie dokładnie odpowiadać temu, co uruchomiono w powłoce",
"sshSessionCommandsTruncated": "Pokazuję pierwsze {shown} z {total} poleceń. Wyeksportuj dzienniki, aby zobaczyć wszystkie.",
"sidebarLogsStreaming": "Strumieniowanie",
"sourceAddress": "Adres źródłowy",
"destinationAddress": "Adres docelowy",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "Jak dodać rekordy",
"dnsRecord": "Wpisy DNS",
"domainTabSetup": "Ustawienie",
"domainTabRecords": "Rekordy",
"domainSetupRecordsDescription": "Utwórz te rekordy u swojego rejestratora domen lub obecnego dostawcy DNS. Pangolin używa ich do weryfikacji domeny i kierowania jej ruchem.",
"domainRecordsInfoTitle": "Rekordy obsługiwane przez Pangolina",
"domainRecordsInfoDescription": "Serwery nazw Pangolina odpowiadają na zapytania DNS dla {domain}. Rekordy, które dodasz tutaj, są obsługiwane bezpośrednio przez Pangolina, więc nie musisz ich tworzyć u rejestratora.",
"customDnsRecords": "Niestandardowe rekordy DNS",
"customDnsRecordsAdd": "Dodaj rekord",
"customDnsRecordsSearch": "Szukaj rekordów...",
"customDnsRecordsUnverifiedTitle": "Rekordy nie są jeszcze obsługiwane",
"customDnsRecordsUnverifiedDescription": "Ta domena nie została zweryfikowana. Możesz teraz dodać rekordy, ale serwery nazw Pangolin nie będą ich obsługiwać, dopóki serwery nazw domeny nie będą wskazywać na Pangolin i weryfikacja nie zostanie zakończona.",
"customDnsRecordCreate": "Dodaj rekord DNS",
"customDnsRecordEdit": "Edytuj rekord DNS",
"customDnsRecordFormDescription": "Ten rekord będzie obsługiwany dla {domain}. Zmiany mogą zająć kilka minut, aby się rozpropagować.",
"customDnsRecordNameDescription": "Użyj @ dla korzenia domeny lub * jako pierwszej etykiety dla symbolu wieloznacznego.",
"customDnsRecordValueDescription": "Wprowadź jedną wartość na linię, np. oba serwery nazw rekordu NS.",
"customDnsRecordPriority": "Priorytet",
"customDnsRecordWeight": "Waga",
"customDnsRecordPort": "Port",
"customDnsRecordCreated": "Rekord DNS utworzony",
"customDnsRecordUpdated": "Rekord DNS zaktualizowany",
"customDnsRecordSaveError": "Nie udało się zapisać rekordu DNS",
"customDnsRecordDeleted": "Rekord DNS usunięty",
"customDnsRecordDeleteError": "Nie udało się usunąć rekordu DNS",
"customDnsRecordDeleteTitle": "Usuń rekord DNS",
"customDnsRecordDeleteQuestion": "Czy na pewno chcesz usunąć rekord typu {type} dla {name}? Przestanie być rozwiązywany, gdy tylko wygaśnie pamięć podręczna odpowiedzi.",
"customDnsRecordDeleteConfirm": "Usuń rekord",
"required": "Wymagane",
"domainSettingsUpdated": "Ustawienia domeny zaktualizowane pomyślnie",
"orgOrDomainIdMissing": "Brakuje identyfikatora organizacji lub domeny",
@@ -3938,7 +4058,7 @@
"backToHome": "Wróć do strony głównej",
"needToSignInToOrg": "Czy potrzebujesz użyć dostawcy tożsamości organizacji?",
"mtls": "mTLS",
"mtlsCaCertificates": "Zaufane certyfikaty CA",
"mtlsCaCertificates": "Ustaw certyfikaty klienta",
"mtlsCaCertificatesDescription": "Klienci muszą przedstawić certyfikat podpisany przez jedną z tych władz certyfikacyjnych, aby nawiązać połączenie. Przesyłaj tylko publiczne certyfikaty CA, nigdy klucz prywatny.",
"mtlsCertificateName": "Nazwa (opcjonalnie)",
"mtlsCertificateNamePlaceholder": "Firmowy certyfikat urządzenia CA",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "Nie można było usunąć certyfikatu.",
"mtlsLastCertificateRemoved": "mTLS został wyłączony, ponieważ usunięto ostatni certyfikat CA.",
"mtlsNoCertificates": "Nie dodano jeszcze żadnych certyfikatów CA",
"mtlsPendingCertificate": "Certyfikat CA {number}",
"mtlsCertificateInvalidPem": "Nie znaleziono certyfikatu zakodowanego za pomocą PEM. Oczekiwano bloku zaczynającego się od -----BEGIN CERTIFICATE-----.",
"mtlsCertificatePrivateKey": "Wygląda na to, że to klucz prywatny, a nie certyfikat. Prześlij tylko publiczny certyfikat urzędu CA.",
"mtlsCertificateDuplicate": "Ten certyfikat CA został już dodany.",
"mtlsRemoveCertificate": "Usuń certyfikat CA",
"mtlsExpired": "Wygasło",
"mtlsExpiresSoon": "Wygasa wkrótce",
+130 -6
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "Envie todo o tráfego da internet para sair pela rede do site.",
"resourceTypeGatewayDescription": "Envie todo o tráfego da internet para sair pela rede do site.",
"resourceDomainDescription": "O recurso será servido neste nome de domínio totalmente qualificado.",
"resourceMatchBaseDomain": "Também combinar com {host}",
"resourceMatchBaseDomainDescription": "Um caractere curinga só corresponde a nomes de host sob {host}. Ative isto para rotear {host} através deste recurso também.",
"resourceHTTPSSettings": "Configurações de HTTPS",
"resourceHTTPSSettingsDescription": "Configure como o recurso será acessado por HTTPS",
"resourcePortDescription": "A porta externa na instância ou nó Pangolin onde o recurso estará acessível.",
@@ -425,6 +427,31 @@
"accessRolesAdd": "Adicionar função",
"accessRoleDelete": "Excluir Papel",
"accessApprovalsManage": "Gerenciar aprovações",
"resourceSessionsManage": "Gerir Sessões de Recurso",
"resourceSessionsDescription": "Visualizar quem atualmente possui uma sessão ativa em seus recursos públicos e revogar o acesso",
"resourceSessionsSearch": "Pesquisar por usuário, email, link ou recurso...",
"resourceSessionsMethod": "Método",
"resourceSessionsIssued": "Emitido",
"resourceSessionsAnonymous": "Anônimo",
"resourceSessionsFilterAnyUser": "Todos os Usuários",
"resourceSessionsFilteredToSession": "Mostrando uma sessão",
"resourceSessionsMethodSso": "SSO",
"resourceSessionsMethodPassword": "Palavra-passe",
"resourceSessionsMethodPincode": "Código PIN",
"resourceSessionsMethodWhitelist": "E-mail",
"resourceSessionsMethodAccessToken": "Link Compartilhável",
"resourceSessionsRevoke": "Revogar",
"resourceSessionsRevokeTitle": "Revogar Sessão",
"resourceSessionsRevokeQuestion": "Revogar a sessão de {identity} em {resource}? Eles precisarão autenticar novamente para acessar o recurso.",
"resourceSessionsRevokeSsoNote": "Este usuário está conectado com SSO. Todas as suas sessões para este recurso do mesmo login serão revogadas, e eles terão que fazer login novamente para acessá-lo, mesmo que ainda estejam logados no Pangolin.",
"resourceSessionsRevoked": "Sessão revogada",
"resourceSessionsRevokedDescription": "A sessão foi revogada. Pode levar alguns segundos para surtir efeito.",
"resourceSessionsErrorRevoke": "Falha ao revogar sessão",
"resourceSessionsErrorRevokeDescription": "Ocorreu um erro ao revogar a sessão",
"resourceSessionReauthRequired": "Sua sessão para este recurso foi encerrada por um administrador. Por favor, faça login novamente para continuar.",
"logLinks": "Links",
"logLinkUser": "Usuário",
"logLinkResourceSession": "Sessão de Recurso",
"accessApprovalsDescription": "Visualizar e gerenciar aprovações pendentes para acesso a esta organização",
"description": "Descrição:",
"inviteTitle": "Convites Abertos",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "Não há limite para o número de usuários para este host.",
"licenseUsersUsedMax": "{usedUsers} de {maxUsers} utilizadores utilizados",
"licenseUsersUsed": "{count, plural, =0 {# utilizadores} one {# utilizador} other {# utilizadores}}",
"licenseUsageMachines": "Clientes de máquina",
"licenseNoMachineLimit": "Não há limite para o número de clientes de máquina para este host.",
"licenseMachinesUsedMax": "{usedMachines} de {maxMachines} clientes de máquina utilizados",
"licenseMachinesUsed": "{count, plural, =0 {# clientes de máquina} one {# cliente de máquina} other {# clientes de máquina}}",
"licenseUnlimited": "Ilimitado",
"licenseTierLabel": "Nível",
"licensePurchaseDescription": "Escolha quantos sites você quer {selectedMode, select, license {Compre uma licença. Você sempre pode adicionar mais sites depois.} other {adicione à sua licença existente.}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "O utilizador foi atualizado.",
"autoProvisioned": "Auto provisionado",
"autoProvisionSettings": "Configurações de provisão automática",
"autoProvisionedDescription": "Permitir que este utilizador seja gerido automaticamente pelo provedor de identidade",
"autoProvisionedDescription": "Se o provedor de identidade tiver o provisionamento automático habilitado, permita que este usuário seja gerenciado automaticamente.",
"accessControlsDescription": "Gerir o que este utilizador pode aceder e fazer na organização",
"accessControlsSubmit": "Guardar Controlos de Acesso",
"singleRolePerUserPlanNotice": "Seu plano suporta apenas uma função por usuário.",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "Distribua as solicitações uniformemente entre os alvos em rotação.",
"loadBalancingStrategy_p2c": "Poder de Duas Escolhas",
"loadBalancingStrategy_p2c_description": "Escolha dois alvos aleatórios e envie a solicitação para o que tiver menos conexões ativas.",
"loadBalancingStrategy_hrw": "Hash de IP do Cliente",
"loadBalancingStrategy_hrw": "Maior Peso Aleatório",
"loadBalancingStrategy_hrw_description": "Hashee o IP do cliente para que o mesmo cliente seja consistentemente roteado para o mesmo alvo.",
"loadBalancingStrategy_leasttime": "Menor Tempo de Resposta",
"loadBalancingStrategy_leasttime_description": "Envie solicitações ao destino com o tempo de resposta mais rápido e menos conexões ativas.",
@@ -829,6 +860,8 @@
"save": "Guardar",
"proxyAdditional": "Configurações Adicionais de Proxy",
"proxyAdditionalDescription": "Configurar como o recurso lida com as configurações de proxy",
"proxyForwardIdentityHeaders": "Encaminhar Cabeçalhos de Identidade",
"proxyForwardIdentityHeadersDescription": "Enviar a identidade do usuário autenticado para o backend nos cabeçalhos Remote-User, Remote-Email, Remote-Name, Remote-Role e Remote-User-Id. Desative para backends que rejeitam pedidos com cabeçalhos não reconhecidos.",
"proxyCustomHeader": "Cabeçalho Host Personalizado",
"proxyCustomHeaderDescription": "O cabeçalho host para definir ao fazer proxy de requisições. Deixe vazio para usar o padrão.",
"proxyAdditionalSubmit": "Guardar Configurações de Proxy",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "Autenticação de Cabeçalho Básico",
"policyAuthHeaderAuthDescription": "Valide um nome e valor de cabeçalho HTTP personalizado em cada solicitação",
"policyAuthHeaderAuthSummary": "Cabeçalho configurado",
"policyAuthMtlsTitle": "Certificado de Cliente (mTLS)",
"policyAuthMtlsDescription": "Exigir um certificado de cliente assinado por uma CA confiável durante o handshake TLS",
"policyAuthMtlsSummary": "{count, plural, one {<b>#</b> certificado de CA confiável} other {<b>#</b> certificados de CA confiáveis}}",
"policyAuthHeaderName": "Nome de usuário",
"policyAuthHeaderValue": "Palavra-passe",
"policyAuthSetPasscode": "Definir Código de Acesso",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "Provisionamento Automático de Utilizadores",
"idpAutoProvisionUsersDescription": "Quando ativado, os utilizadores serão criados automaticamente no sistema no primeiro login com a capacidade de mapear utilizadores para funções e organizações.",
"idpAutoProvisionConfigureAfterCreate": "Você pode configurar as definições de auto provisão assim que o provedor de identidade for criado.",
"idpSessionRevalidation": "Revalidação Contínua de Sessão",
"idpSessionRevalidationDescription": "Quando ativado, periodicamente revalida sessões com este provedor de identidade. Se o provedor revogar o acesso, as sessões vinculadas são encerradas.",
"idpSessionRevalidationConfigureAfterCreate": "Você pode configurar a revalidação contínua de sessão assim que o provedor de identidade for criado.",
"licenseBadge": "EE",
"idpType": "Tipo de Provedor",
"idpTypeDescription": "Selecione o tipo de provedor de identidade que deseja configurar",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "Excluir domínio",
"actionGetDNSRecords": "Obter registros de DNS",
"actionRestartOrgDomain": "Reiniciar domínio",
"actionListCustomDnsRecords": "Listar Registros DNS Personalizados",
"actionCreateCustomDnsRecord": "Criar Registro DNS Personalizado",
"actionUpdateCustomDnsRecord": "Atualizar Registro DNS Personalizado",
"actionDeleteCustomDnsRecord": "Excluir Registro DNS Personalizado",
"actionCreateSite": "Criar Site",
"actionDeleteSite": "Eliminar Site",
"actionGetSite": "Obter Site",
"actionListSites": "Listar Sites",
"actionReGenerateSecret": "Regerar Credenciais",
"actionCreateAiProvider": "Criar Provedor de IA",
"actionDeleteAiProvider": "Excluir Provedor de IA",
"actionGetAiProvider": "Obter Provedor de IA",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "Gerar Token de Acesso",
"actionDeleteAccessToken": "Eliminar Token de Acesso",
"actionListAccessTokens": "Listar Tokens de Acesso",
"actionListResourceSessions": "Listar Sessões de Recurso",
"actionDeleteResourceSession": "Revogar Sessão de Recurso",
"actionCreateResourceSessionToken": "Criar Token de Sessão de Recurso",
"actionCreateResourceRule": "Criar Regra de Recurso",
"actionDeleteResourceRule": "Eliminar Regra de Recurso",
"actionListResourceRules": "Listar Regras de Recurso",
"actionUpdateResourceRule": "Atualizar Regra de Recurso",
"actionCreateResourceMtlsCertificate": "Criar Certificado mTLS de Recurso",
"actionDeleteResourceMtlsCertificate": "Excluir Certificado mTLS de Recurso",
"actionListResourceMtlsCertificates": "Listar Certificados mTLS de Recurso",
"actionListOrgLabels": "Listar Rótulos",
"actionCreateOrgLabel": "Criar Rótulo",
"actionUpdateOrgLabel": "Atualizar Rótulo",
"actionDeleteOrgLabel": "Excluir Rótulo",
"actionAttachLabelToItem": "Anexar Rótulo",
"actionDetachLabelFromItem": "Desanexar Rótulo",
"actionCreateEventStreamingDestination": "Criar Destino de Streaming de Eventos",
"actionUpdateEventStreamingDestination": "Atualizar Destino de Streaming de Eventos",
"actionDeleteEventStreamingDestination": "Excluir Destino de Streaming de Eventos",
"actionListEventStreamingDestinations": "Listar Destinos de Streaming de Eventos",
"actionCreateAlertRule": "Criar Regra de Alerta",
"actionUpdateAlertRule": "Atualizar Regra de Alerta",
"actionDeleteAlertRule": "Excluir Regra de Alerta",
"actionGetAlertRule": "Obter Regra de Alerta",
"actionListAlertRules": "Listar Regras de Alerta",
"actionCreateHealthCheck": "Criar Verificação de Saúde",
"actionUpdateHealthCheck": "Atualizar Verificação de Saúde",
"actionDeleteHealthCheck": "Excluir Verificação de Saúde",
"actionListHealthChecks": "Listar Verificações de Saúde",
"actionListOrgs": "Listar Organizações",
"actionCheckOrgId": "Verificar ID",
"actionCreateOrg": "Criar Organização",
@@ -1726,6 +1794,7 @@
"sidebarHome": "Residencial",
"sidebarSites": "sites",
"sidebarApprovals": "Solicitações de aprovação",
"sidebarResourceSessions": "Sessões de Recurso",
"sidebarResources": "Recursos",
"sidebarProxyResources": "Público",
"sidebarClientResources": "Privado",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "Políticas de Recursos Públicos",
"commandIdentityProviders": "Provedores de Identidade",
"commandApprovals": "Pedidos de Aprovação",
"commandResourceSessions": "Sessões de Recurso",
"commandShareableLinks": "Links Compartilháveis",
"commandOrganization": "Organização",
"commandLogsAndAnalytics": "Logs e Análises",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "Logs de Autenticação",
"commandLogsAction": "Logs de Ações do Administrador",
"commandLogsConnection": "Logs da Conexão",
"commandLogsSsh": "Logs de Sessão SSH",
"commandLogsStreaming": "Transmissão de Eventos",
"commandManagement": "Gestão",
"commandAlerting": "Alertas",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "Por quanto tempo manter os registros de conexão",
"logRetentionAISessionsLabel": "Retenção de Registros de Sessão do Gateway de IA",
"logRetentionAISessionsDescription": "Quanto tempo reter logs de sessão de prompt/resposta do gateway de IA",
"logRetentionSshLabel": "Retenção de Logs de Sessão SSH",
"logRetentionSshDescription": "Por quanto tempo reter os logs de sessão e comando SSH",
"logRetentionDisabled": "Desabilitado",
"logRetention3Days": "3 dias",
"logRetention7Days": "7 dias",
@@ -3707,6 +3780,27 @@
"connectionLogs": "Logs da conexão",
"connectionLogsDescription": "Ver logs de conexão para túneis nesta organização",
"sidebarLogsConnection": "Logs da conexão",
"sidebarLogsSsh": "Logs de Sessão SSH",
"sshSessionLogs": "Logs de Sessão SSH",
"sshSessionLogsDescription": "Visualizar sessões SSH e os comandos executados nelas nos sites desta organização",
"sshSystemUser": "Usuário do Sistema",
"sshCommands": "Comandos",
"sshSessionId": "ID de Sessão",
"sshAuthMethod": "Método de Autenticação",
"sshTransport": "Conexão",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "Navegador",
"sshKeyFingerprint": "Impressão Digital da Chave",
"sshCertKeyId": "ID da Chave do Certificado",
"sshStartedAt": "Iniciado Em",
"sshEndedAt": "Encerrado Em",
"sshSessionLoadingCommands": "Carregando comandos...",
"sshSessionCommandsError": "Falha ao carregar comandos para esta sessão",
"sshSessionNoCommands": "Nenhum comando registrado para esta sessão",
"sshSessionRecalledCommand": "(recuperado do histórico do shell)",
"sshSessionInexactNotice": "Alguns comandos foram editados com recuperação de histórico ou conclusão com tabulação e podem não corresponder exatamente ao que o shell executou.",
"sshSessionInexactTooltip": "Editado com recuperação de histórico ou conclusão com tabulação; pode não corresponder exatamente ao que o shell executou",
"sshSessionCommandsTruncated": "Mostrando os primeiros {shown} de {total} comandos. Exporte os logs para ver todos eles.",
"sidebarLogsStreaming": "Transmitindo",
"sourceAddress": "Endereço de origem",
"destinationAddress": "Endereço de destino",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "Como adicionar registros",
"dnsRecord": "Registros DNS",
"domainTabSetup": "Configuração",
"domainTabRecords": "Registros",
"domainSetupRecordsDescription": "Crie esses registros no seu registrador de domínio ou provedor de DNS atual. Pangolin os usa para verificar o domínio e rotear seu tráfego.",
"domainRecordsInfoTitle": "Registros servidos pelo Pangolin",
"domainRecordsInfoDescription": "Os servidores de nomes do Pangolin respondem às consultas DNS para {domain}. Os registros que você adicionar aqui serão servidos diretamente pelo Pangolin, então você não precisará criá-los no seu registrador.",
"customDnsRecords": "Registros DNS Personalizados",
"customDnsRecordsAdd": "Adicionar Registro",
"customDnsRecordsSearch": "Pesquisar registros...",
"customDnsRecordsUnverifiedTitle": "Os registros ainda não estão sendo servidos",
"customDnsRecordsUnverifiedDescription": "Este domínio ainda não foi verificado. Você pode adicionar registros agora, mas os nameservers do Pangolin não os servirão até que os nameservers do domínio apontem para o Pangolin e a verificação seja concluída.",
"customDnsRecordCreate": "Adicionar Registro DNS",
"customDnsRecordEdit": "Editar Registro DNS",
"customDnsRecordFormDescription": "Este registro será servido para {domain}. As alterações podem demorar alguns minutos para se propagarem.",
"customDnsRecordNameDescription": "Use @ para a raiz do domínio ou * como o primeiro rótulo para um curinga.",
"customDnsRecordValueDescription": "Insira um valor por linha, por exemplo, ambos os nameservers de um registro NS.",
"customDnsRecordPriority": "Prioridade",
"customDnsRecordWeight": "Peso",
"customDnsRecordPort": "Porta",
"customDnsRecordCreated": "Registro DNS criado",
"customDnsRecordUpdated": "Registro DNS atualizado",
"customDnsRecordSaveError": "Falha ao salvar o registro DNS",
"customDnsRecordDeleted": "Registro DNS excluído",
"customDnsRecordDeleteError": "Falha ao excluir o registro DNS",
"customDnsRecordDeleteTitle": "Excluir Registro DNS",
"customDnsRecordDeleteQuestion": "Tem certeza de que deseja excluir o registro {type} para {name}? Ele deixará de resolver quando as respostas em cache expirarem.",
"customDnsRecordDeleteConfirm": "Excluir Registro",
"required": "Obrigatório",
"domainSettingsUpdated": "Configurações de domínio atualizadas com sucesso",
"orgOrDomainIdMissing": "ID da organização ou domínio está faltando",
@@ -3938,14 +4058,14 @@
"backToHome": "Voltar para a página inicial",
"needToSignInToOrg": "Precisa usar o provedor de identidade da sua organização?",
"mtls": "mTLS",
"mtlsCaCertificates": "Certificados CA confiáveis",
"mtlsCaCertificates": "Definir Certificados de Cliente",
"mtlsCaCertificatesDescription": "Os clientes devem apresentar um certificado assinado por uma destas autoridades certificadoras para se conectar. Carregue apenas certificados CA públicos, nunca uma chave privada.",
"mtlsCertificateName": "Nome (Opcional)",
"mtlsCertificateNamePlaceholder": "Dispositivo Corporativo CA",
"mtlsCertificatePem": "Certificado CA (PEM)",
"mtlsCertificatePemDescription": "Cole um certificado codificado em PEM ou um conjunto de certificados, ou carregue um arquivo .pem, .crt ou .cer.",
"mtlsUploadFile": "Carregar arquivo",
"mtlsAddCertificate": "Adicionar certificado CA",
"mtlsUploadFile": "Carregar Arquivo",
"mtlsAddCertificate": "Adicionar Certificado CA",
"mtlsCertificateAdded": "Certificado CA adicionado",
"mtlsCertificateAddedDescription": "Clientes com certificados assinados por este CA agora podem ser verificados.",
"mtlsCertificateErrorAdd": "Falha ao adicionar certificado CA",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "O certificado não pôde ser removido.",
"mtlsLastCertificateRemoved": "mTLS foi desativado porque o último certificado CA foi removido.",
"mtlsNoCertificates": "Ainda não foram adicionados certificados CA",
"mtlsPendingCertificate": "Certificado CA {number}",
"mtlsCertificateInvalidPem": "Certificado em formato PEM não encontrado. Esperado um bloco começando com -----BEGIN CERTIFICATE-----.",
"mtlsCertificatePrivateKey": "Parece ser uma chave privada, e não um certificado. Carregue apenas o certificado público da CA.",
"mtlsCertificateDuplicate": "Este certificado CA já foi adicionado.",
"mtlsRemoveCertificate": "Remover certificado CA",
"mtlsExpired": "Expirado",
"mtlsExpiresSoon": "Expira em breve",
@@ -4488,7 +4612,7 @@
"rdpFull": "Completo",
"rdpReal": "Real",
"rdpMeta": "Meta",
"rdpUploadFiles": "Upload de arquivos",
"rdpUploadFiles": "Carregar Arquivos",
"rdpFilesReadyToPaste": "Arquivos prontos para colar",
"rdpFilesReadyToPasteDescription": "{count} arquivo(s) copiado(s) para a área de transferência remota — pressione Ctrl+V na área de trabalho remota para colar.",
"rdpUploadFailed": "Falha no upload",
+128 -4
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "Отправлять весь интернет-трафик через сетевую структуру сайтов.",
"resourceTypeGatewayDescription": "Отправлять весь интернет-трафик через сетевую структуру сайтов.",
"resourceDomainDescription": "Ресурс будет предоставлен по этому полностью определенному доменному имени.",
"resourceMatchBaseDomain": "Также сопоставить {host}",
"resourceMatchBaseDomainDescription": "Подстановочный знак соответствует только именам хостов под {host}. Включите это, чтобы направлять {host} через этот ресурс также.",
"resourceHTTPSSettings": "Настройки HTTPS",
"resourceHTTPSSettingsDescription": "Настройка доступа к ресурсу по HTTPS",
"resourcePortDescription": "Внешний порт на экземпляре или узле Pangolin, где ресурс будет доступен.",
@@ -425,6 +427,31 @@
"accessRolesAdd": "Добавить роль",
"accessRoleDelete": "Удалить роль",
"accessApprovalsManage": "Управление утверждениями",
"resourceSessionsManage": "Управление сессиями ресурсов",
"resourceSessionsDescription": "Просмотр тех, у кого в настоящее время активная сессия на ваших общедоступных ресурсах и отзыва доступа",
"resourceSessionsSearch": "Поиск по пользователю, электронной почте, ссылке или ресурсу...",
"resourceSessionsMethod": "Метод",
"resourceSessionsIssued": "Выдано",
"resourceSessionsAnonymous": "Анонимный",
"resourceSessionsFilterAnyUser": "Все пользователи",
"resourceSessionsFilteredToSession": "Показана одна сессия",
"resourceSessionsMethodSso": "SSO",
"resourceSessionsMethodPassword": "Пароль",
"resourceSessionsMethodPincode": "ПИН-код",
"resourceSessionsMethodWhitelist": "Электронная почта",
"resourceSessionsMethodAccessToken": "Общедоступная ссылка",
"resourceSessionsRevoke": "Отозвать",
"resourceSessionsRevokeTitle": "Отозвать сессию",
"resourceSessionsRevokeQuestion": "Отозвать сеанс для {identity} на {resource}? Им будет необходимо повторно выполнить аутентификацию для доступа к ресурсу.",
"resourceSessionsRevokeSsoNote": "Этот пользователь вошел с помощью SSO. Все их сессии для этого ресурса с одного входа будут отозваны, и им придется снова войти, чтобы получить доступ, даже если они все еще авторизованы в Pangolin.",
"resourceSessionsRevoked": "Сессия отозвана",
"resourceSessionsRevokedDescription": "Сессия была отозвана. Это может занять несколько секунд, чтобы вступить в силу.",
"resourceSessionsErrorRevoke": "Не удалось отозвать сессию",
"resourceSessionsErrorRevokeDescription": "Произошла ошибка при отзыве сессии",
"resourceSessionReauthRequired": "Ваш сеанс для этого ресурса был завершен администратором. Пожалуйста, войдите снова, чтобы продолжить.",
"logLinks": "Ссылки",
"logLinkUser": "Пользователь",
"logLinkResourceSession": "Сессия ресурса",
"accessApprovalsDescription": "Просмотр и управление утверждениями в ожидании доступа к этой организации",
"description": "Описание",
"inviteTitle": "Открытые приглашения",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "Нет ограничения на количество пользователей для этого хоста.",
"licenseUsersUsedMax": "{usedUsers} из {maxUsers} пользователей использовано",
"licenseUsersUsed": "{count, plural, =0 {# пользователей} one {# пользователь} few {# пользователя} many {# пользователей} other {# пользователей}}",
"licenseUsageMachines": "Машинные клиенты",
"licenseNoMachineLimit": "Нет ограничения на количество машинных клиентов для этого хоста.",
"licenseMachinesUsedMax": "Использовано {usedMachines} из {maxMachines} машинных клиентов",
"licenseMachinesUsed": "{count, plural, =0 {# машинных клиентов} one {# машинный клиент} few {# машинных клиента} many {# машинных клиентов} other {# машинных клиентов}}",
"licenseUnlimited": "Неограниченный",
"licenseTierLabel": "Уровень",
"licensePurchaseDescription": "Выберите, для скольких сайтов вы хотите {selectedMode, select, license {приобрести лицензию. Вы всегда можете добавить больше сайтов позже.} other {добавить к существующей лицензии.}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "Пользователь был обновлён.",
"autoProvisioned": "Автоподбор",
"autoProvisionSettings": "Настройки автоматического обеспечения",
"autoProvisionedDescription": "Разрешить автоматическое управление этим пользователем",
"autoProvisionedDescription": "Если у поставщика идентификации включено автоприсвоение, разрешите автоматическое управление этим пользователем.",
"accessControlsDescription": "Управляйте тем, к чему этот пользователь может получить доступ и что делать в организации",
"accessControlsSubmit": "Сохранить контроль доступа",
"singleRolePerUserPlanNotice": "Ваш план поддерживает только одну роль каждого пользователя.",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "Распределяйте запросы равномерно между целями по очереди.",
"loadBalancingStrategy_p2c": "Сила двух выборов",
"loadBalancingStrategy_p2c_description": "Выберите две случайные цели и отправьте запрос той, у которой меньше активных соединений.",
"loadBalancingStrategy_hrw": "Хэш клиентского IP",
"loadBalancingStrategy_hrw": "Наивысший случайный вес",
"loadBalancingStrategy_hrw_description": "Хэшируйте IP клиента, чтобы один и тот же клиент постоянно маршрутизировался на одну и ту же цель.",
"loadBalancingStrategy_leasttime": "Наименьшее время ответа",
"loadBalancingStrategy_leasttime_description": "Отправляйте запросы к цели с самым быстрым временем отклика и наименьшим количеством активных соединений.",
@@ -829,6 +860,8 @@
"save": "Сохранить",
"proxyAdditional": "Дополнительные настройки прокси",
"proxyAdditionalDescription": "Настроить обработку параметров прокси ресурса",
"proxyForwardIdentityHeaders": "Переслать заголовки удостоверения",
"proxyForwardIdentityHeadersDescription": "Отправьте удостоверение аутентифицированного пользователя в бекенд в заголовках Remote-User, Remote-Email, Remote-Name, Remote-Role и Remote-User-Id. Отключите для бекендов, которые отклоняют запросы с неизвестными заголовками.",
"proxyCustomHeader": "Пользовательский заголовок Host",
"proxyCustomHeaderDescription": "Заголовок host для установки при проксировании запросов. Оставьте пустым для использования по умолчанию.",
"proxyAdditionalSubmit": "Сохранить настройки прокси",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "Базовая аутентификация заголовка",
"policyAuthHeaderAuthDescription": "Проверка пользовательского имени и значения HTTP-заголовка для каждого запроса",
"policyAuthHeaderAuthSummary": "Заголовок настроен",
"policyAuthMtlsTitle": "Клиентский сертификат (mTLS)",
"policyAuthMtlsDescription": "Требуется клиентский сертификат, подписанный доверенным УЦ, во время рукопожатия TLS",
"policyAuthMtlsSummary": "{count} доверенных сертификатов УЦ",
"policyAuthHeaderName": "Имя пользователя",
"policyAuthHeaderValue": "Пароль",
"policyAuthSetPasscode": "Установить пароль",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "Автоматическое создание пользователей",
"idpAutoProvisionUsersDescription": "При включении пользователи будут автоматически создаваться в системе при первом входе с возможностью сопоставления пользователей с ролями и организациями.",
"idpAutoProvisionConfigureAfterCreate": "Вы можете настроить параметры автоматического обеспечения после создания поставщика удостоверений.",
"idpSessionRevalidation": "Непрерывная повторная валидация сессии",
"idpSessionRevalidationDescription": "Когда включено, периодически выполняется повторная валидация сессий с этим поставщиком удостоверений. Если поставщик отзывает доступ, связанные сессии завершаются.",
"idpSessionRevalidationConfigureAfterCreate": "Вы можете настроить непрерывную повторную валидацию сессии после создания поставщика удостоверений.",
"licenseBadge": "EE",
"idpType": "Тип поставщика",
"idpTypeDescription": "Выберите тип поставщика удостоверений, который вы хотите настроить",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "Удалить домен",
"actionGetDNSRecords": "Получить записи DNS",
"actionRestartOrgDomain": "Перезапустить домен",
"actionListCustomDnsRecords": "Список пользовательских записей DNS",
"actionCreateCustomDnsRecord": "Создать пользовательскую запись DNS",
"actionUpdateCustomDnsRecord": "Обновить пользовательскую запись DNS",
"actionDeleteCustomDnsRecord": "Удалить пользовательскую запись DNS",
"actionCreateSite": "Создать сайт",
"actionDeleteSite": "Удалить сайт",
"actionGetSite": "Получить сайт",
"actionListSites": "Список сайтов",
"actionReGenerateSecret": "Пересоздать учетные данные",
"actionCreateAiProvider": "Создать поставщика ИИ",
"actionDeleteAiProvider": "Удалить поставщика ИИ",
"actionGetAiProvider": "Получить поставщика ИИ",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "Сгенерировать токен доступа",
"actionDeleteAccessToken": "Удалить токен доступа",
"actionListAccessTokens": "Список токенов доступа",
"actionListResourceSessions": "Список сессий ресурсов",
"actionDeleteResourceSession": "Отзыв сессии ресурса",
"actionCreateResourceSessionToken": "Создать токен сеанса ресурса",
"actionCreateResourceRule": "Создать правило ресурса",
"actionDeleteResourceRule": "Удалить правило ресурса",
"actionListResourceRules": "Список правил ресурса",
"actionUpdateResourceRule": "Обновить правило ресурса",
"actionCreateResourceMtlsCertificate": "Создать mTLS-сертификат ресурса",
"actionDeleteResourceMtlsCertificate": "Удалить mTLS-сертификат ресурса",
"actionListResourceMtlsCertificates": "Список mTLS-сертификатов ресурсов",
"actionListOrgLabels": "Список меток",
"actionCreateOrgLabel": "Создать метку",
"actionUpdateOrgLabel": "Обновить метку",
"actionDeleteOrgLabel": "Удалить метку",
"actionAttachLabelToItem": "Присоединить метку",
"actionDetachLabelFromItem": "Отсоединить метку",
"actionCreateEventStreamingDestination": "Создать направление потоковой передачи событий",
"actionUpdateEventStreamingDestination": "Обновить направление потоковой передачи событий",
"actionDeleteEventStreamingDestination": "Удалить направление потоковой передачи событий",
"actionListEventStreamingDestinations": "Список направлений потоковой передачи событий",
"actionCreateAlertRule": "Создать правило оповещений",
"actionUpdateAlertRule": "Обновить правило оповещений",
"actionDeleteAlertRule": "Удалить правило оповещений",
"actionGetAlertRule": "Получить правило оповещений",
"actionListAlertRules": "Список правил оповещений",
"actionCreateHealthCheck": "Создать проверку состояния",
"actionUpdateHealthCheck": "Обновить проверку состояния",
"actionDeleteHealthCheck": "Удалить проверку состояния",
"actionListHealthChecks": "Список проверок состояния",
"actionListOrgs": "Список организаций",
"actionCheckOrgId": "Проверить ID",
"actionCreateOrg": "Создать организацию",
@@ -1726,6 +1794,7 @@
"sidebarHome": "Главная",
"sidebarSites": "Сайты",
"sidebarApprovals": "Запросы на утверждение",
"sidebarResourceSessions": "Сессии ресурсов",
"sidebarResources": "Ресурсы",
"sidebarProxyResources": "Публичный",
"sidebarClientResources": "Приватный",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "Политики публичных ресурсов",
"commandIdentityProviders": "Поставщики удостоверений",
"commandApprovals": "Запросы на одобрение",
"commandResourceSessions": "Сессии ресурсов",
"commandShareableLinks": "Общие ссылки",
"commandOrganization": "Организация",
"commandLogsAndAnalytics": "Логи и аналитика",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "Журналы аутентификации",
"commandLogsAction": "Журналы административных действий",
"commandLogsConnection": "Журнал сетевых подключений",
"commandLogsSsh": "Журналы сессий SSH",
"commandLogsStreaming": "Трансляция события",
"commandManagement": "Управление",
"commandAlerting": "Оповещения",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "Как долго хранить журналы подключений",
"logRetentionAISessionsLabel": "Сохранение логов сессий AI шлюза",
"logRetentionAISessionsDescription": "Как долго сохранять логи сессий запросов/ответов шлюза AI",
"logRetentionSshLabel": "Сохранение журнала сессий SSH",
"logRetentionSshDescription": "Как долго сохранять журналы сессий SSH и команд",
"logRetentionDisabled": "Отключено",
"logRetention3Days": "3 дня",
"logRetention7Days": "7 дней",
@@ -3707,6 +3780,27 @@
"connectionLogs": "Журнал подключений",
"connectionLogsDescription": "Просмотр журналов подключения туннелей в этой организации",
"sidebarLogsConnection": "Журнал подключений",
"sidebarLogsSsh": "Журналы сессий SSH",
"sshSessionLogs": "Журналы сессий SSH",
"sshSessionLogsDescription": "Просмотр сессий SSH и запущенных в них команд на сайтах этой организации",
"sshSystemUser": "Системный пользователь",
"sshCommands": "Команды",
"sshSessionId": "ID сессии",
"sshAuthMethod": "Метод аутентификации",
"sshTransport": "Подключение",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "Браузер",
"sshKeyFingerprint": "Отпечаток ключа",
"sshCertKeyId": "ID сертифицированного ключа",
"sshStartedAt": "Начато в",
"sshEndedAt": "Завершено в",
"sshSessionLoadingCommands": "Загрузка команд...",
"sshSessionCommandsError": "Не удалось загрузить команды для этой сессии",
"sshSessionNoCommands": "Для этой сессии команды не записаны",
"sshSessionRecalledCommand": "(восстановлено из истории команд)",
"sshSessionInexactNotice": "Некоторые команды были отредактированы с помощью восстановления истории или автозаполнения и могут не полностью соответствовать тому, что было выполнено в shell.",
"sshSessionInexactTooltip": "Отредактировано с помощью восстановления истории или автозаполнения; может не полностью соответствовать выполненной shell команде",
"sshSessionCommandsTruncated": "Показаны первые {shown} из {total} команд. Экспортируйте логи, чтобы увидеть все команды.",
"sidebarLogsStreaming": "Вещание",
"sourceAddress": "Адрес источника",
"destinationAddress": "Адрес назначения",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "Как добавить записи",
"dnsRecord": "DNS записи",
"domainTabSetup": "Настройка",
"domainTabRecords": "Записи",
"domainSetupRecordsDescription": "Создайте эти записи у вашего регистратора домена или текущего DNS-провайдера. Pangolin использует их для проверки домена и маршрутизации его трафика.",
"domainRecordsInfoTitle": "Записи, обслуживаемые Pangolin",
"domainRecordsInfoDescription": "Серверы имен Pangolin отвечают на DNS-запросы для {domain}. Записи, которые вы добавите здесь, обслуживаются напрямую через Pangolin, поэтому вам не нужно создавать их у вашего регистратора.",
"customDnsRecords": "Пользовательские записи DNS",
"customDnsRecordsAdd": "Добавить запись",
"customDnsRecordsSearch": "Поиск записей...",
"customDnsRecordsUnverifiedTitle": "Записи еще не обслуживаются",
"customDnsRecordsUnverifiedDescription": "Этот домен не подтвержден. Вы можете добавить записи сейчас, но серверы имен Pangolin не будут их обслуживать, пока серверы имен домена не укажут на Pangolin и подтверждение не завершится.",
"customDnsRecordCreate": "Добавить запись DNS",
"customDnsRecordEdit": "Редактировать запись DNS",
"customDnsRecordFormDescription": "Эта запись будет обслуживаться для {domain}. Изменения могут занять несколько минут для распространения.",
"customDnsRecordNameDescription": "Используйте @ для корня домена или * в качестве первой метки для универсального символа.",
"customDnsRecordValueDescription": "Введите одно значение на строку, например, оба сервера имен для записи NS.",
"customDnsRecordPriority": "Приоритет",
"customDnsRecordWeight": "Вес",
"customDnsRecordPort": "Порт",
"customDnsRecordCreated": "Запись DNS создана",
"customDnsRecordUpdated": "Запись DNS обновлена",
"customDnsRecordSaveError": "Не удалось сохранить запись DNS",
"customDnsRecordDeleted": "Запись DNS удалена",
"customDnsRecordDeleteError": "Не удалось удалить запись DNS",
"customDnsRecordDeleteTitle": "Удалить запись DNS",
"customDnsRecordDeleteQuestion": "Вы уверены, что хотите удалить запись {type} для {name}? Она прекратит разрешение после истечения срока кэша ответов.",
"customDnsRecordDeleteConfirm": "Удалить запись",
"required": "Требуется",
"domainSettingsUpdated": "Настройки домена успешно обновлены",
"orgOrDomainIdMissing": "Отсутствует организация или ID домена",
@@ -3938,14 +4058,14 @@
"backToHome": "Вернуться домой",
"needToSignInToOrg": "Нужно использовать провайдера идентификаций вашей организации?",
"mtls": "mTLS",
"mtlsCaCertificates": "Доверенные сертификаты CA",
"mtlsCaCertificates": "Установить клиентские сертификаты",
"mtlsCaCertificatesDescription": "Клиенты должны представить сертификат, подписанный одним из этих удостоверяющих центров, чтобы подключиться. Загружайте только открытые сертификаты CA, никогда не загружайте закрытый ключ.",
"mtlsCertificateName": "Имя (необязательно)",
"mtlsCertificateNamePlaceholder": "Корпоративное устройство CA",
"mtlsCertificatePem": "Сертификат CA (PEM)",
"mtlsCertificatePemDescription": "Вставьте сертификат в кодировке PEM или комплект сертификатов, либо загрузите файл .pem, .crt или .cer.",
"mtlsUploadFile": "Загрузить файл",
"mtlsAddCertificate": "Добавить сертификат CA",
"mtlsAddCertificate": "Добавить сертификат УЦ",
"mtlsCertificateAdded": "Сертификат CA добавлен",
"mtlsCertificateAddedDescription": "Те, у кого сертификаты, подписанные этим CA, теперь могут быть проверены.",
"mtlsCertificateErrorAdd": "Не удалось добавить сертификат CA",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "Сертификат не может быть удалён.",
"mtlsLastCertificateRemoved": "mTLS был отключён, потому что последний сертификат CA был удалён.",
"mtlsNoCertificates": "Сертификаты CA ещё не добавлены",
"mtlsPendingCertificate": "Сертификат УЦ {number}",
"mtlsCertificateInvalidPem": "PEM-кодированный сертификат не найден. Ожидался блок, начинающийся с -----BEGIN CERTIFICATE-----.",
"mtlsCertificatePrivateKey": "Похоже на закрытый ключ, а не сертификат. Загрузите только публичный сертификат УЦ.",
"mtlsCertificateDuplicate": "Этот сертификат УЦ уже был добавлен.",
"mtlsRemoveCertificate": "Удалить сертификат CA",
"mtlsExpired": "Истёк",
"mtlsExpiresSoon": "Скоро истечёт",
+130 -6
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "Tüm internet trafiğini site ağı üzerinden çıkış yapacak şekilde gönderin.",
"resourceTypeGatewayDescription": "Tüm internet trafiğini site ağı üzerinden çıkış yapacak şekilde gönderin.",
"resourceDomainDescription": "Kaynak bu tam etki alanı adıyla sunulacak.",
"resourceMatchBaseDomain": "{host} için de eşleme yap",
"resourceMatchBaseDomainDescription": "Bir joker karakter yalnızca {host} altındaki ana bilgisayar adlarını eşleştirir. {host}'u bu kaynak üzerinden yönlendirmek için bunu etkinleştirin.",
"resourceHTTPSSettings": "HTTPS Ayarları",
"resourceHTTPSSettingsDescription": "Kaynağınıza HTTPS üzerinden erişimin nasıl sağlanacağını yapılandırın",
"resourcePortDescription": "Kaynağa erişilebilecek Pangolin örneği veya düğüm üzerindeki harici bağlantı noktası.",
@@ -425,6 +427,31 @@
"accessRolesAdd": "Rol Ekle",
"accessRoleDelete": "Rolü Sil",
"accessApprovalsManage": "Onayları Yönet",
"resourceSessionsManage": "Kaynak Oturumlarını Yönet",
"resourceSessionsDescription": "Genel kaynaklarınızda şu anda hangi kullanıcıların aktif oturumda olduğunu görüntüleyin ve erişimi iptal edin",
"resourceSessionsSearch": "Kullanıcı, e-posta, bağlantı veya kaynak ile arayın...",
"resourceSessionsMethod": "Yöntem",
"resourceSessionsIssued": "Verildi",
"resourceSessionsAnonymous": "Anonim",
"resourceSessionsFilterAnyUser": "Tüm Kullanıcılar",
"resourceSessionsFilteredToSession": "Bir oturum gösteriliyor",
"resourceSessionsMethodSso": "SSO",
"resourceSessionsMethodPassword": "Şifre",
"resourceSessionsMethodPincode": "PIN Kodu",
"resourceSessionsMethodWhitelist": "E-posta",
"resourceSessionsMethodAccessToken": "Paylaşılabilir Bağlantı",
"resourceSessionsRevoke": "İptal Et",
"resourceSessionsRevokeTitle": "Oturumu İptal Et",
"resourceSessionsRevokeQuestion": "{identity} için {resource} üzerindeki oturumu iptal et? Kaynağa erişmek için tekrar kimlik doğrulaması yapmaları gerekecek.",
"resourceSessionsRevokeSsoNote": "Bu kullanıcı SSO ile oturum açmış. Bu kaynaktaki aynı oturum açma ile ilgili tüm oturumları iptal edilecek ve erişim sağlamak için tekrar oturum açmaları gerekecek, Pangolin'e hala bağlı olsalar bile.",
"resourceSessionsRevoked": "Oturum iptal edildi",
"resourceSessionsRevokedDescription": "Oturum iptal edildi. İşlemin etkili olması birkaç saniye sürebilir.",
"resourceSessionsErrorRevoke": "Oturum iptal edilemedi",
"resourceSessionsErrorRevokeDescription": "Oturum iptal edilirken bir hata oluştu",
"resourceSessionReauthRequired": "Bu kaynağa ilişkin oturumunuz bir yönetici tarafından sona erdirildi. Devam etmek için lütfen tekrar oturum açın.",
"logLinks": "Bağlantılar",
"logLinkUser": "Kullanıcı",
"logLinkResourceSession": "Kaynak Oturumu",
"accessApprovalsDescription": "Bu kuruluşa erişim için bekleyen onayları görüntüleyin ve yönetin",
"description": "Açıklama",
"inviteTitle": "Açık Davetiyeler",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "Bu ana bilgisayar için kullanıcı sayısında bir sınır yoktur.",
"licenseUsersUsedMax": "{usedUsers} / {maxUsers} kullanıcılar kullanıldı",
"licenseUsersUsed": "{count, plural, =0 {# kullanıcı} one {# kullanıcı} other {# kullanıcılar}}",
"licenseUsageMachines": "Makine İstemcileri",
"licenseNoMachineLimit": "Bu ana bilgisayar için makine istemcisi sayısında bir sınır yoktur.",
"licenseMachinesUsedMax": "{usedMachines} / {maxMachines} makine istemcisi kullanıldı",
"licenseMachinesUsed": "{count, plural, =0 {# makine istemcisi} one {# makine istemcisi} other {# makine istemcisi}}",
"licenseUnlimited": "Sınırsız",
"licenseTierLabel": "Seviye",
"licensePurchaseDescription": "{selectedMode, select, license {Lisans satın almak için kaç site istediğinizi seçin. Daha sonra daha fazla site ekleyebilirsiniz.} other {mevcut lisansınıza kaç site ekleneceğini seçin.}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "Kullanıcı güncellenmiştir.",
"autoProvisioned": "Otomatik Sağlandı",
"autoProvisionSettings": "Otomatik Tedarik Ayarları",
"autoProvisionedDescription": "Bu kullanıcının kimlik sağlayıcısı tarafından otomatik olarak yönetilmesine izin ver",
"autoProvisionedDescription": "Kimlik sağlayıcısında otomatik sağlama etkinse, bu kullanıcının otomatik olarak yönetilmesine izin verin.",
"accessControlsDescription": "Bu kullanıcının organizasyonda neleri erişebileceğini ve yapabileceğini yönetin",
"accessControlsSubmit": "Erişim Kontrollerini Kaydet",
"singleRolePerUserPlanNotice": "Planınız yalnızca kullanıcı başına bir rol desteler.",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "İstekleri hedefler arasında sırayla eşit olarak dağıtın.",
"loadBalancingStrategy_p2c": "İki Seçeneğin Gücü",
"loadBalancingStrategy_p2c_description": "İki rasgele hedef seçin ve istekleri daha az aktif bağlantıya sahip olana gönderin.",
"loadBalancingStrategy_hrw": "İstemci IP Hash",
"loadBalancingStrategy_hrw": "En Yüksek Rastgele Ağırlık",
"loadBalancingStrategy_hrw_description": "Aynı istemcinin aynı hedefe sürekli olarak yönlendirilmesi için istemci IP'sine hash uygulayın.",
"loadBalancingStrategy_leasttime": "En Az Yanıt Süresi",
"loadBalancingStrategy_leasttime_description": "Hedefe en hızlı yanıt süresine ve en az aktif bağlantıya sahip istekleri gönderin.",
@@ -829,6 +860,8 @@
"save": "Kaydet",
"proxyAdditional": "Ek Proxy Ayarları",
"proxyAdditionalDescription": "Kaynağın proxy ayarlarını nasıl yöneteceği yapılandırın",
"proxyForwardIdentityHeaders": "Kimlik Başlıklarını İlet",
"proxyForwardIdentityHeadersDescription": "Kimliği doğrulanmış kullanıcının kimliğini Remote-User, Remote-Email, Remote-Name, Remote-Role ve Remote-User-Id başlıklarında arka uca gönderin. Tanınmayan başlıklarla istekleri reddeden arka uçlar için devre dışı bırakın.",
"proxyCustomHeader": "Özel Ana Bilgisayar Başlığı",
"proxyCustomHeaderDescription": "İstekleri proxy'lerken ayarlanacak ana bilgisayar başlığı. Varsayılanı kullanmak için boş bırakılır.",
"proxyAdditionalSubmit": "Proxy Ayarlarını Kaydet",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "Temel Başlık Kimlik Doğrulama",
"policyAuthHeaderAuthDescription": "Her istekte özel bir HTTP başlık adını ve değerini doğrulayın",
"policyAuthHeaderAuthSummary": "Başlık yapılandırıldı",
"policyAuthMtlsTitle": "İstemci Sertifikası (mTLS)",
"policyAuthMtlsDescription": "TLS el sıkışması sırasında güvenilir bir CA tarafından imzalanmış istemci sertifikası gereklidir",
"policyAuthMtlsSummary": "{count} güvenilir CA sertifikası",
"policyAuthHeaderName": "Kullanıcı Adı",
"policyAuthHeaderValue": "Şifre",
"policyAuthSetPasscode": "Şifreyi Ayarla",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "Kullanıcıları Otomatik Sağla",
"idpAutoProvisionUsersDescription": "Etkinleştirildiğinde, kullanıcılar rol ve organizasyonlara eşleme yeteneğiyle birlikte sistemde otomatik olarak oluşturulacak.",
"idpAutoProvisionConfigureAfterCreate": "Kimlik sağlayıcı oluşturulduktan sonra otomatik sağlama ayarlarını yapılandırabilirsiniz.",
"idpSessionRevalidation": "Sürekli Oturum Yeniden Doğrulama",
"idpSessionRevalidationDescription": "Etkinleştirildiğinde, bu kimlik sağlayıcı ile oturumları periyodik olarak yeniden doğrulayın. Sağlayıcı erişimi iptal ederse, bağlı oturumlar sona erer.",
"idpSessionRevalidationConfigureAfterCreate": "Kimlik sağlayıcı oluşturulduktan sonra sürekli oturum yeniden doğrulamayı yapılandırabilirsiniz.",
"licenseBadge": " ",
"idpType": "Sağlayıcı Türü",
"idpTypeDescription": "Yapılandırmak istediğiniz kimlik sağlayıcısı türünü seçin",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "Alan Adını Sil",
"actionGetDNSRecords": "DNS Kayıtlarını Al",
"actionRestartOrgDomain": "Alanı Yeniden Başlat",
"actionListCustomDnsRecords": "Özel DNS Kayıtlarını Listele",
"actionCreateCustomDnsRecord": "Özel DNS Kaydı Oluştur",
"actionUpdateCustomDnsRecord": "Özel DNS Kaydını Güncelle",
"actionDeleteCustomDnsRecord": "Özel DNS Kaydını Sil",
"actionCreateSite": "Site Oluştur",
"actionDeleteSite": "Siteyi Sil",
"actionGetSite": "Siteyi Al",
"actionListSites": "Siteleri Listele",
"actionReGenerateSecret": "Kimlik Bilgilerini Yeniden Oluştur",
"actionCreateAiProvider": "AI Sağlayıcı Oluştur",
"actionDeleteAiProvider": "AI Sağlayıcı Sil",
"actionGetAiProvider": "AI Sağlayıcı Al",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "Erişim Jetonu Oluştur",
"actionDeleteAccessToken": "Erişim Jetonunu Sil",
"actionListAccessTokens": "Erişim Jetonlarını Listele",
"actionListResourceSessions": "Kaynak Oturumlarını Listele",
"actionDeleteResourceSession": "Kaynak Oturumu İptal Et",
"actionCreateResourceSessionToken": "Kaynak Oturum Otomasyonu Oluştur",
"actionCreateResourceRule": "Kaynak Kuralı Oluştur",
"actionDeleteResourceRule": "Kaynak Kuralını Sil",
"actionListResourceRules": "Kaynak Kurallarını Listele",
"actionUpdateResourceRule": "Kaynak Kuralını Güncelle",
"actionCreateResourceMtlsCertificate": "Kaynak mTLS Sertifikası Oluştur",
"actionDeleteResourceMtlsCertificate": "Kaynak mTLS Sertifikasını Sil",
"actionListResourceMtlsCertificates": "Kaynak mTLS Sertifikalarını Listele",
"actionListOrgLabels": "Etiketleri Listele",
"actionCreateOrgLabel": "Etiket Oluştur",
"actionUpdateOrgLabel": "Etiketi Güncelle",
"actionDeleteOrgLabel": "Etiketi Sil",
"actionAttachLabelToItem": "Etiketi Bağla",
"actionDetachLabelFromItem": "Etiketi Ayır",
"actionCreateEventStreamingDestination": "Etkinlik Yayınlama Hedefi Oluştur",
"actionUpdateEventStreamingDestination": "Etkinlik Yayınlama Hedefini Güncelle",
"actionDeleteEventStreamingDestination": "Etkinlik Yayınlama Hedefini Sil",
"actionListEventStreamingDestinations": "Etkinlik Yayınlama Hedeflerini Listele",
"actionCreateAlertRule": "Uyarı Kuralı Oluştur",
"actionUpdateAlertRule": "Uyarı Kuralını Güncelle",
"actionDeleteAlertRule": "Uyarı Kuralını Sil",
"actionGetAlertRule": "Uyarı Kuralı Al",
"actionListAlertRules": "Uyarı Kurallarını Listele",
"actionCreateHealthCheck": "Sağlık Kontrolü Oluştur",
"actionUpdateHealthCheck": "Sağlık Kontrolünü Güncelle",
"actionDeleteHealthCheck": "Sağlık Kontrolünü Sil",
"actionListHealthChecks": "Sağlık Kontrollerini Listele",
"actionListOrgs": "Organizasyonları Listele",
"actionCheckOrgId": "Kimliği Kontrol Et",
"actionCreateOrg": "Organizasyon Oluştur",
@@ -1726,6 +1794,7 @@
"sidebarHome": "Ana Sayfa",
"sidebarSites": "Siteler",
"sidebarApprovals": "Onay Talepleri",
"sidebarResourceSessions": "Kaynak Oturumları",
"sidebarResources": "Kaynaklar",
"sidebarProxyResources": "Herkese Açık",
"sidebarClientResources": "Özel",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "Genel Kaynaklar Politikaları",
"commandIdentityProviders": "Kimlik Sağlayıcılar",
"commandApprovals": "Onay İstekleri",
"commandResourceSessions": "Kaynak Oturumları",
"commandShareableLinks": "Paylaşılabilir Bağlantılar",
"commandOrganization": "Organizasyon",
"commandLogsAndAnalytics": "Günlükler ve Analitik",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "Kimlik Doğrulama Günlükleri",
"commandLogsAction": "Yönetim Eylemi Günlükleri",
"commandLogsConnection": "Ağ Günlükleri",
"commandLogsSsh": "SSH Oturum Günlükleri",
"commandLogsStreaming": "Olay Akışı",
"commandManagement": "Yönetim",
"commandAlerting": "Uyarılar",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "Bağlantı kayıtlarını ne kadar süre saklayacağınız",
"logRetentionAISessionsLabel": "AI Ağ Geçidi Oturum Günlüğü Saklama Süresi",
"logRetentionAISessionsDescription": "AI ağ geçidi istem/yanıt oturum günlüklerinin ne kadar süreyle saklanacağını belirleyin",
"logRetentionSshLabel": "SSH Oturum Günlüğü Saklama Süresi",
"logRetentionSshDescription": "SSH oturum ve komut günlüklerini ne kadar süre saklamalı",
"logRetentionDisabled": "Devre Dışı",
"logRetention3Days": "3 gün",
"logRetention7Days": "7 gün",
@@ -3707,6 +3780,27 @@
"connectionLogs": "Bağlantı Kayıtları",
"connectionLogsDescription": "Bu organizasyondaki tüneller için bağlantı geçmişine bakın",
"sidebarLogsConnection": "Bağlantı Kayıtları",
"sidebarLogsSsh": "SSH Oturum Günlükleri",
"sshSessionLogs": "SSH Oturum Günlükleri",
"sshSessionLogsDescription": "Bu organizasyondaki sitelerde SSH oturumlarını ve çalıştırılan komutları görüntüleyin",
"sshSystemUser": "Sistem Kullanıcısı",
"sshCommands": "Komutlar",
"sshSessionId": "Oturum ID",
"sshAuthMethod": "Kimlik Doğrulama Yöntemi",
"sshTransport": "Bağlantı",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "Tarayıcı",
"sshKeyFingerprint": "Anahtar İmzası",
"sshCertKeyId": "Sertifika Anahtar ID",
"sshStartedAt": "Başlangıç Tarihi",
"sshEndedAt": "Bitiş Tarihi",
"sshSessionLoadingCommands": "Komutlar yükleniyor...",
"sshSessionCommandsError": "Bu oturum için komutlar yüklenemedi",
"sshSessionNoCommands": "Bu oturum için kayıtlı hiçbir komut yok",
"sshSessionRecalledCommand": "(kabuk geçmişinden hatırlandı)",
"sshSessionInexactNotice": "Bazı komutlar geçmiş hatırlama veya sekme tamamlama ile düzenlendi ve kabuğun çalıştırdığıyla tam eşleşmeye bilir.",
"sshSessionInexactTooltip": "Geçmiş hatırlama veya sekme tamamlama ile düzenlendi; kabuğun çalıştırdığıyla tam olarak eşleşmeyebilir",
"sshSessionCommandsTruncated": "{total} komutun ilk {shown} tanesi gösteriliyor. Tümünü görmek için günlükleri dışa aktarın.",
"sidebarLogsStreaming": "Akış",
"sourceAddress": "Kaynak Adresi",
"destinationAddress": "Hedef Adresi",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "Kayıtları Nasıl Ekleyebilirsiniz",
"dnsRecord": "DNS Kayıtları",
"domainTabSetup": "Kurulum",
"domainTabRecords": "Kayıtlar",
"domainSetupRecordsDescription": "Bu kayıtları, etki alanı kayıt sağlayıcınızda veya mevcut DNS sağlayıcınızda oluşturun. Pangolin, etki alanını doğrulamak ve trafiği yönlendirmek için bunları kullanır.",
"domainRecordsInfoTitle": "Pangolin Tarafından Sunulan Kayıtlar",
"domainRecordsInfoDescription": "Pangolin'in ad sunucuları, {domain} için DNS sorgularına yanıt verir. Buraya eklediğiniz kayıtlar doğrudan Pangolin'den sunulur, bu yüzden kayıt sağlayıcınızda onları oluşturmanıza gerek yoktur.",
"customDnsRecords": "Özel DNS Kayıtları",
"customDnsRecordsAdd": "Kayıt Ekle",
"customDnsRecordsSearch": "Kayıtları ara...",
"customDnsRecordsUnverifiedTitle": "Kayıtlar henüz sunulmuyor",
"customDnsRecordsUnverifiedDescription": "Bu alan adı doğrulanmadı. Şimdi kayıt ekleyebilirsiniz, ancak Pangolin isim sunucuları bunları alan adının isim sunucuları Pangolin'e yönlendirilene ve doğrulama tamamlanıncaya kadar sunmayacaktır.",
"customDnsRecordCreate": "DNS Kaydı Ekle",
"customDnsRecordEdit": "DNS Kaydını Düzenle",
"customDnsRecordFormDescription": "Bu kayıt {domain} için sunulacaktır. Değişikliklerin yayılması birkaç dakika sürebilir.",
"customDnsRecordNameDescription": "Alan adının kökü için @ veya genel bir ad için ilk etiket olarak * kullanın.",
"customDnsRecordValueDescription": "Satır başına bir değer girin, örneğin NS kaydının her iki isim sunucusu.",
"customDnsRecordPriority": "Öncelik",
"customDnsRecordWeight": "Ağırlık",
"customDnsRecordPort": "Bağlantı Noktası",
"customDnsRecordCreated": "DNS kaydı oluşturuldu",
"customDnsRecordUpdated": "DNS kaydı güncellendi",
"customDnsRecordSaveError": "DNS kaydı kaydedilemedi",
"customDnsRecordDeleted": "DNS kaydı silindi",
"customDnsRecordDeleteError": "DNS kaydı silinemedi",
"customDnsRecordDeleteTitle": "DNS Kaydını Sil",
"customDnsRecordDeleteQuestion": "{name} için {type} kaydını silmek istediğinizden emin misiniz? Önbellekteki yanıtlar süresi dolduktan sonra çözülmeyecektir.",
"customDnsRecordDeleteConfirm": "Kaydı Sil",
"required": "Gerekli",
"domainSettingsUpdated": "Alan adına yönelik ayarlar başarıyla güncellendi",
"orgOrDomainIdMissing": "Organizasyon veya Alan Adı Kimliği eksik",
@@ -3938,14 +4058,14 @@
"backToHome": "Ana sayfaya geri dön",
"needToSignInToOrg": "Kuruluşunuzun kimlik sağlayıcısını kullanmanız mı gerekiyor?",
"mtls": "mTLS",
"mtlsCaCertificates": "Güvenilir CA Sertifikaları",
"mtlsCaCertificates": "İstemci Sertifikalarını Ayarla",
"mtlsCaCertificatesDescription": "Bağlanmak için bu sertifika otoritelerinden biri tarafından imzalanmış bir sertifika sunmalıdır. Sadece genel CA sertifikaları yükleyin, asla özel anahtar yüklemeyin.",
"mtlsCertificateName": "İsim (İsteğe Bağlı)",
"mtlsCertificateNamePlaceholder": "Kurumsal cihaz CA",
"mtlsCertificatePem": "CA sertifikası (PEM)",
"mtlsCertificatePemDescription": "Bir PEM kodlamalı sertifika veya bir sertifika yığını yapıştırın ya da bir .pem, .crt veya .cer dosyası yükleyin.",
"mtlsUploadFile": "Dosya yükle",
"mtlsAddCertificate": "CA sertifikası ekle",
"mtlsUploadFile": "Dosya Yükle",
"mtlsAddCertificate": "CA Sertifikası Ekle",
"mtlsCertificateAdded": "CA sertifikası eklendi",
"mtlsCertificateAddedDescription": "Bu CA tarafından imzalanan sertifikalara sahip istemciler artık doğrulanabilir.",
"mtlsCertificateErrorAdd": "CA sertifikası eklenemedi",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "Sertifika kaldırılamadı.",
"mtlsLastCertificateRemoved": "mTLS son CA sertifikası kaldırıldığı için kapatıldı.",
"mtlsNoCertificates": "Henüz CA sertifikası eklenmedi",
"mtlsPendingCertificate": "CA sertifikası {number}",
"mtlsCertificateInvalidPem": "PEM kodlu sertifika bulunamadı. -----BEGIN CERTIFICATE----- ile başlayan bir blok bekleniyor.",
"mtlsCertificatePrivateKey": "Bu, bir sertifikadan çok özel anahtar benziyor. Yalnızca CA'nın genel sertifikasını yükleyin.",
"mtlsCertificateDuplicate": "Bu CA sertifikası zaten eklenmiş.",
"mtlsRemoveCertificate": "CA sertifikasını kaldır",
"mtlsExpired": "Süresi doldu",
"mtlsExpiresSoon": "Yakında süresi dolacak",
@@ -4488,7 +4612,7 @@
"rdpFull": "Tam",
"rdpReal": "Gerçek",
"rdpMeta": "Meta",
"rdpUploadFiles": "Dosya yükle",
"rdpUploadFiles": "Dosyaları Yükle",
"rdpFilesReadyToPaste": "Yapıştırmak üzere dosyalar hazır",
"rdpFilesReadyToPasteDescription": "{count} dosya uzak panoya kopyalandı — yapıştırmak için uzak masaüstünde Ctrl+V tuşlarına basın.",
"rdpUploadFailed": "Yükleme başarısız",
+128 -4
View File
@@ -311,6 +311,8 @@
"privateResourceTypeGatewayDescription": "发送所有互联网流量通过站点网络出口。",
"resourceTypeGatewayDescription": "发送所有互联网流量通过站点网络出口。",
"resourceDomainDescription": "资源将在此完全限定的域名上提供。",
"resourceMatchBaseDomain": "还匹配{host}",
"resourceMatchBaseDomainDescription": "通配符只匹配{host}下的主机名。启用此选项以将{host}也通过此资源路由。",
"resourceHTTPSSettings": "HTTPS 设置",
"resourceHTTPSSettingsDescription": "配置如何通过 HTTPS 访问资源",
"resourcePortDescription": "在 Pangolin 实例或节点上资源可访问的外部端口。",
@@ -425,6 +427,31 @@
"accessRolesAdd": "添加角色",
"accessRoleDelete": "删除角色",
"accessApprovalsManage": "管理批准",
"resourceSessionsManage": "管理资源会话",
"resourceSessionsDescription": "查看当前在您的公共资源上有活动会话的人员,并撤销访问权限",
"resourceSessionsSearch": "按用户、电子邮件、链接或资源搜索……",
"resourceSessionsMethod": "方法",
"resourceSessionsIssued": "已发出",
"resourceSessionsAnonymous": "匿名",
"resourceSessionsFilterAnyUser": "所有用户",
"resourceSessionsFilteredToSession": "显示一个会话",
"resourceSessionsMethodSso": "单点登录",
"resourceSessionsMethodPassword": "密码",
"resourceSessionsMethodPincode": "PIN码",
"resourceSessionsMethodWhitelist": "电子邮件",
"resourceSessionsMethodAccessToken": "分享链接",
"resourceSessionsRevoke": "撤销",
"resourceSessionsRevokeTitle": "撤销会话",
"resourceSessionsRevokeQuestion": "撤销{identity}在{resource}上的会话?他们将需要再次验证以访问该资源。",
"resourceSessionsRevokeSsoNote": "此用户通过SSO登录。他们此资源的所有会话将被撤销,即使他们仍然登录到Pangolin,他们也需要再次登录以访问该资源。",
"resourceSessionsRevoked": "会话已撤销",
"resourceSessionsRevokedDescription": "会话已被撤销。可能需要几秒钟才能生效。",
"resourceSessionsErrorRevoke": "撤销会话失败",
"resourceSessionsErrorRevokeDescription": "撤销会话时发生错误",
"resourceSessionReauthRequired": "管理员已结束您对此资源的会话。请重新登录以继续。",
"logLinks": "链接",
"logLinkUser": "用户",
"logLinkResourceSession": "资源会话",
"accessApprovalsDescription": "查看和管理待审批的组织访问权限",
"description": "描述",
"inviteTitle": "打开邀请",
@@ -604,6 +631,10 @@
"licenseNoUserLimit": "此主机的用户数量没有限制。",
"licenseUsersUsedMax": "使用了 {usedUsers}/{maxUsers} 个用户",
"licenseUsersUsed": "{count, plural, =0 {# 用户} other {# 用户}}",
"licenseUsageMachines": "机器客户端",
"licenseNoMachineLimit": "对于此主机,机器客户端的数量没有限制。",
"licenseMachinesUsedMax": "已使用 {usedMachines} 个机器客户端,共有 {maxMachines} 个机器客户端",
"licenseMachinesUsed": "{count, plural, =0 {# 个机器客户端} one {# 个机器客户端} other {# 个机器客户端}}",
"licenseUnlimited": "无限制",
"licenseTierLabel": "级别",
"licensePurchaseDescription": "请选择您希望 {selectedMode, select, license {直接购买许可证,您可以随时增加更多站点。} other {为现有许可证购买更多站点}}",
@@ -745,7 +776,7 @@
"userSavedDescription": "用户已更新。",
"autoProvisioned": "自动设置",
"autoProvisionSettings": "自动提供设置",
"autoProvisionedDescription": "允许此用户由身份提供商自动管理",
"autoProvisionedDescription": "如果身份提供者启用了自动预置,则允许自动管理此用户。",
"accessControlsDescription": "管理此用户在组织中可以访问和做什么",
"accessControlsSubmit": "保存访问控制",
"singleRolePerUserPlanNotice": "您的计划仅支持每个用户一个角色。",
@@ -801,7 +832,7 @@
"loadBalancingStrategy_wrr_description": "按顺序均匀分配请求到目标。",
"loadBalancingStrategy_p2c": "二选一",
"loadBalancingStrategy_p2c_description": "随机选择两个目标,并将请求发送到连接较少的那个。",
"loadBalancingStrategy_hrw": "客户端 IP 哈希",
"loadBalancingStrategy_hrw": "最高随机权重",
"loadBalancingStrategy_hrw_description": "散列客户端 IP,因此同一客户端始终路由到相同的目标。",
"loadBalancingStrategy_leasttime": "最短响应时间",
"loadBalancingStrategy_leasttime_description": "将请求发送到响应时间最快且活动连接最少的目标。",
@@ -829,6 +860,8 @@
"save": "保存",
"proxyAdditional": "附加代理设置",
"proxyAdditionalDescription": "配置资源如何处理代理设置",
"proxyForwardIdentityHeaders": "转发身份头信息",
"proxyForwardIdentityHeadersDescription": "在Remote-User、Remote-Email、Remote-Name、Remote-Role和Remote-User-Id头中将经过身份验证的用户身份发送到后端。对于拒绝带有未识别头部请求的后端,请禁用。",
"proxyCustomHeader": "自定义主机标题",
"proxyCustomHeaderDescription": "代理请求时设置的主机头。留空则使用默认值。",
"proxyAdditionalSubmit": "保存代理设置",
@@ -933,6 +966,9 @@
"policyAuthHeaderAuthTitle": "基础标题认证",
"policyAuthHeaderAuthDescription": "在每次请求上验证自定义 HTTP 标头名称和值",
"policyAuthHeaderAuthSummary": "标头已配置",
"policyAuthMtlsTitle": "客户端证书(mTLS)",
"policyAuthMtlsDescription": "在TLS握手过程中需要一个受信任CA签署的客户端证书",
"policyAuthMtlsSummary": "{count} 个受信任的CA证书",
"policyAuthHeaderName": "用户名",
"policyAuthHeaderValue": "密码",
"policyAuthSetPasscode": "设定密码",
@@ -1190,6 +1226,9 @@
"idpAutoProvisionUsers": "自动提供用户",
"idpAutoProvisionUsersDescription": "如果启用,用户将在首次登录时自动在系统中创建,并且能够映射用户到角色和组织。",
"idpAutoProvisionConfigureAfterCreate": "您可以在创建身份提供者后配置自动配置设置。",
"idpSessionRevalidation": "持续会话重新验证",
"idpSessionRevalidationDescription": "启用后,定期重新验证与此身份提供者的会话。如果提供者撤销访问权限,关联的会话将被终止。",
"idpSessionRevalidationConfigureAfterCreate": "创建身份提供者后,您可以配置持续会话重新验证。",
"licenseBadge": "企业版",
"idpType": "提供者类型",
"idpTypeDescription": "选择您想要配置的身份提供者类型",
@@ -1500,10 +1539,15 @@
"actionDeleteOrgDomain": "删除域",
"actionGetDNSRecords": "获取 DNS 记录",
"actionRestartOrgDomain": "重新启动域",
"actionListCustomDnsRecords": "列出自定义DNS记录",
"actionCreateCustomDnsRecord": "创建自定义DNS记录",
"actionUpdateCustomDnsRecord": "更新自定义DNS记录",
"actionDeleteCustomDnsRecord": "删除自定义DNS记录",
"actionCreateSite": "创建站点",
"actionDeleteSite": "删除站点",
"actionGetSite": "获取站点",
"actionListSites": "站点列表",
"actionReGenerateSecret": "重新生成证书",
"actionCreateAiProvider": "创建AI提供者",
"actionDeleteAiProvider": "删除AI提供者",
"actionGetAiProvider": "获取AI提供者",
@@ -1590,11 +1634,35 @@
"actionGenerateAccessToken": "生成访问令牌",
"actionDeleteAccessToken": "删除访问令牌",
"actionListAccessTokens": "访问令牌",
"actionListResourceSessions": "列出资源会话",
"actionDeleteResourceSession": "撤销资源会话",
"actionCreateResourceSessionToken": "创建资源会话令牌",
"actionCreateResourceRule": "创建资源规则",
"actionDeleteResourceRule": "删除资源规则",
"actionListResourceRules": "列出资源规则",
"actionUpdateResourceRule": "更新资源规则",
"actionCreateResourceMtlsCertificate": "创建资源mTLS证书",
"actionDeleteResourceMtlsCertificate": "删除资源mTLS证书",
"actionListResourceMtlsCertificates": "列出资源mTLS证书",
"actionListOrgLabels": "列出标签",
"actionCreateOrgLabel": "创建标签",
"actionUpdateOrgLabel": "更新标签",
"actionDeleteOrgLabel": "删除标签",
"actionAttachLabelToItem": "附加标签",
"actionDetachLabelFromItem": "分离标签",
"actionCreateEventStreamingDestination": "创建事件流目标",
"actionUpdateEventStreamingDestination": "更新事件流目标",
"actionDeleteEventStreamingDestination": "删除事件流目标",
"actionListEventStreamingDestinations": "列出事件流目标",
"actionCreateAlertRule": "创建告警规则",
"actionUpdateAlertRule": "更新告警规则",
"actionDeleteAlertRule": "删除告警规则",
"actionGetAlertRule": "获取警报规则",
"actionListAlertRules": "列出告警规则",
"actionCreateHealthCheck": "创建健康检查",
"actionUpdateHealthCheck": "更新健康检查",
"actionDeleteHealthCheck": "删除健康检查",
"actionListHealthChecks": "列出健康检查",
"actionListOrgs": "列出组织",
"actionCheckOrgId": "检查组织ID",
"actionCreateOrg": "创建组织",
@@ -1726,6 +1794,7 @@
"sidebarHome": "首页",
"sidebarSites": "站点",
"sidebarApprovals": "审批请求",
"sidebarResourceSessions": "资源会话",
"sidebarResources": "资源",
"sidebarProxyResources": "公开资源",
"sidebarClientResources": "私有资源",
@@ -2195,6 +2264,7 @@
"commandResourcePolicies": "公共资源政策",
"commandIdentityProviders": "身份提供商",
"commandApprovals": "审批请求",
"commandResourceSessions": "资源会话",
"commandShareableLinks": "可共享链接",
"commandOrganization": "组织",
"commandLogsAndAnalytics": "日志与分析",
@@ -2203,6 +2273,7 @@
"commandLogsAccess": "身份验证日志",
"commandLogsAction": "管理员操作日志",
"commandLogsConnection": "网络日志",
"commandLogsSsh": "SSH会话日志",
"commandLogsStreaming": "事件流",
"commandManagement": "管理",
"commandAlerting": "警报",
@@ -3694,6 +3765,8 @@
"logRetentionConnectionDescription": "保留连接日志的时间",
"logRetentionAISessionsLabel": "AI 网关会话日志保留",
"logRetentionAISessionsDescription": "保留AI网关提示/响应会话日志的时间",
"logRetentionSshLabel": "SSH会话日志保留",
"logRetentionSshDescription": "保留SSH会话和命令日志的时间",
"logRetentionDisabled": "已禁用",
"logRetention3Days": "3 天",
"logRetention7Days": "7 天",
@@ -3707,6 +3780,27 @@
"connectionLogs": "连接日志",
"connectionLogsDescription": "查看此机构隧道的连接日志",
"sidebarLogsConnection": "连接日志",
"sidebarLogsSsh": "SSH会话日志",
"sshSessionLogs": "SSH会话日志",
"sshSessionLogsDescription": "查看SSH会话及其中运行的命令,在本组织内的网站上",
"sshSystemUser": "系统用户",
"sshCommands": "命令",
"sshSessionId": "会话ID",
"sshAuthMethod": "认证方法",
"sshTransport": "连接",
"sshTransportSsh": "SSH",
"sshTransportBrowser": "浏览器",
"sshKeyFingerprint": "密钥指纹",
"sshCertKeyId": "证书密钥ID",
"sshStartedAt": "开始于",
"sshEndedAt": "结束于",
"sshSessionLoadingCommands": "加载命令中……",
"sshSessionCommandsError": "加载此会话命令失败",
"sshSessionNoCommands": "此会话没有记录命令",
"sshSessionRecalledCommand": "(从shell历史中召回)",
"sshSessionInexactNotice": "某些命令经过历史召回或Tab完成编辑,可能与shell运行的内容不完全匹配。",
"sshSessionInexactTooltip": "编辑了历史召回或Tab完成;可能与shell运行的内容不完全匹配",
"sshSessionCommandsTruncated": "显示{total}个命令中的前{shown}个。导出日志以查看所有命令。",
"sidebarLogsStreaming": "事件流",
"sourceAddress": "源地址",
"destinationAddress": "目的地址",
@@ -3727,6 +3821,32 @@
"TTL": "TTL",
"howToAddRecords": "如何添加记录",
"dnsRecord": "DNS记录",
"domainTabSetup": "设置",
"domainTabRecords": "记录",
"domainSetupRecordsDescription": "在您的域注册商或当前的DNS提供商处创建这些记录。Pangolin使用它们来验证域和路由其流量。",
"domainRecordsInfoTitle": "由Pangolin提供的记录",
"domainRecordsInfoDescription": "Pangolin的名称服务器为{domain}回答DNS查询。您在此添加的记录由Pangolin直接提供,因此您无需在注册商处创建它们。",
"customDnsRecords": "自定义DNS记录",
"customDnsRecordsAdd": "添加记录",
"customDnsRecordsSearch": "搜索记录……",
"customDnsRecordsUnverifiedTitle": "记录尚未被服务",
"customDnsRecordsUnverifiedDescription": "该域名尚未验证。您现在可以添加记录,但Pangolin的名称服务器不会提供这些记录,直到该域名的名称服务器指向Pangolin并完成验证。",
"customDnsRecordCreate": "添加DNS记录",
"customDnsRecordEdit": "编辑DNS记录",
"customDnsRecordFormDescription": "此记录将为{domain}提供服务。更改可能需要几分钟才能传播。",
"customDnsRecordNameDescription": "使用@表示域名根,或使用*作为第一个标签来表示通配符。",
"customDnsRecordValueDescription": "每行输入一个值,例如NS记录的两个名称服务器。",
"customDnsRecordPriority": "优先级",
"customDnsRecordWeight": "权重",
"customDnsRecordPort": "端口",
"customDnsRecordCreated": "DNS记录已创建",
"customDnsRecordUpdated": "DNS记录已更新",
"customDnsRecordSaveError": "保存DNS记录失败",
"customDnsRecordDeleted": "DNS记录已删除",
"customDnsRecordDeleteError": "删除DNS记录失败",
"customDnsRecordDeleteTitle": "删除DNS记录",
"customDnsRecordDeleteQuestion": "您确定要删除类型为{type}的<b>{name}</b>记录吗?缓存答案到期后,它将停止解析。",
"customDnsRecordDeleteConfirm": "删除记录",
"required": "必填",
"domainSettingsUpdated": "域设置更新成功",
"orgOrDomainIdMissing": "缺少机构或域 ID",
@@ -3938,14 +4058,14 @@
"backToHome": "返回首页",
"needToSignInToOrg": "需要使用您组织的身份提供商吗?",
"mtls": "mTLS",
"mtlsCaCertificates": "可信 CA 证书",
"mtlsCaCertificates": "设置客户端证书",
"mtlsCaCertificatesDescription": "客户端必须出示由这些证书颁发机构签名的证书才能连接。仅上传公共 CA 证书,切勿上传私钥。",
"mtlsCertificateName": "名称(可选)",
"mtlsCertificateNamePlaceholder": "企业设备 CA",
"mtlsCertificatePem": "CA 证书(PEM)",
"mtlsCertificatePemDescription": "粘贴 PEM 编码的证书或证书束,或上传 .pem、 .crt 或 .cer 文件。",
"mtlsUploadFile": "上传文件",
"mtlsAddCertificate": "添加 CA 证书",
"mtlsAddCertificate": "添加CA证书",
"mtlsCertificateAdded": "CA 证书已添加",
"mtlsCertificateAddedDescription": "现在可以验证由此 CA 签名证书的客户端。",
"mtlsCertificateErrorAdd": "添加 CA 证书失败",
@@ -3955,6 +4075,10 @@
"mtlsCertificateErrorRemoveDescription": "无法移除证书。",
"mtlsLastCertificateRemoved": "因为最后一个 CA 证书被移除,mTLS 已关闭。",
"mtlsNoCertificates": "尚未添加 CA 证书",
"mtlsPendingCertificate": "CA证书 {number}",
"mtlsCertificateInvalidPem": "找不到PEM编码的证书。预期的块以-----BEGIN CERTIFICATE-----开始。",
"mtlsCertificatePrivateKey": "这看起来像是私钥,而不是证书。仅上传CA的公钥证书。",
"mtlsCertificateDuplicate": "该CA证书已被添加。",
"mtlsRemoveCertificate": "移除 CA 证书",
"mtlsExpired": "已过期",
"mtlsExpiresSoon": "即将到期",
+614 -623
View File
File diff suppressed because it is too large. Load diff
+29 -28
View File
@@ -29,15 +29,16 @@
"email": "email dev --dir server/emails/templates --port 3005",
"build:cli": "node esbuild.mjs -e cli/index.ts -o dist/cli.mjs",
"format:check": "prettier --check .",
"format": "prettier --write ."
"format": "prettier --write .",
"test": "node scripts/run-tests.mjs"
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "9.1.0",
"@aws-sdk/client-s3": "3.1152.0",
"@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.0.tgz",
"@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.1.tgz",
"@aws-sdk/client-s3": "3.1160.0",
"@devolutions/iron-remote-desktop": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-0.0.1.tgz",
"@devolutions/iron-remote-desktop-rdp": "https://static.pangolin.net/packages/devolutions-iron-remote-desktop-rdp-0.0.2.tgz",
"@headlessui/react": "2.2.10",
"@hookform/resolvers": "5.9.1",
"@hookform/resolvers": "6.0.2",
"@monaco-editor/react": "4.7.0",
"@node-rs/argon2": "2.2.2",
"@novnc/novnc": "^1.7.0",
@@ -63,19 +64,19 @@
"@radix-ui/react-tooltip": "1.4.1",
"@react-email/body": "0.3.0",
"@react-email/components": "1.0.12",
"@react-email/render": "2.1.0",
"@react-email/render": "2.2.0",
"@react-email/tailwind": "2.0.7",
"@simplewebauthn/browser": "14.0.0",
"@simplewebauthn/server": "14.0.3",
"@tailwindcss/forms": "0.5.11",
"@tanstack/react-query": "5.104.3",
"@tanstack/react-query": "5.105.0",
"@tanstack/react-table": "8.21.3",
"@xterm/addon-fit": "^0.11.0",
"@xterm/addon-web-links": "^0.12.0",
"@xterm/xterm": "^6.0.0",
"acme-client": "^5.4.0",
"arctic": "3.7.0",
"axios": "1.20.0",
"axios": "1.21.0",
"better-sqlite3": "11.9.1",
"canvas-confetti": "1.9.4",
"class-variance-authority": "0.7.1",
@@ -88,9 +89,9 @@
"dns-packet": "^5.6.1",
"drizzle-orm": "0.45.4",
"express": "5.3.0",
"express-rate-limit": "8.7.1",
"glob": "13.0.6",
"gpt-tokenizer": "^4.0.3",
"express-rate-limit": "8.7.2",
"glob": "14.0.3",
"gpt-tokenizer": "^4.1.0",
"helmet": "8.3.0",
"http-errors": "2.0.1",
"input-otp": "1.5.0",
@@ -99,29 +100,29 @@
"js-yaml": "5.5.0",
"jsonwebtoken": "9.0.3",
"lru-cache": "11.5.3",
"lucide-react": "1.57.0",
"lucide-react": "1.64.0",
"maxmind": "5.0.7",
"moment": "2.31.0",
"next": "16.4.2",
"next": "16.4.3",
"next-intl": "4.14.9",
"next-themes": "0.4.6",
"nextjs-toploader": "3.9.17",
"nodemailer": "10.1.1",
"nodemailer": "10.3.1",
"oslo": "1.2.1",
"pg": "8.23.1",
"posthog-node": "5.56.1",
"posthog-node": "5.60.1",
"qrcode.react": "4.2.0",
"react": "19.3.1",
"react-day-picker": "10.0.2",
"react-dom": "19.3.1",
"react-easy-sort": "1.8.0",
"react-hook-form": "7.90.0",
"react-hook-form": "7.91.0",
"react-icons": "5.7.0",
"recharts": "3.10.1",
"reodotdev": "1.1.0",
"semver": "7.8.5",
"sshpk": "1.18.0",
"stripe": "23.0.0",
"stripe": "23.1.0",
"swagger-ui-express": "5.0.1",
"tailwind-merge": "3.7.0",
"topojson-client": "3.1.0",
@@ -139,11 +140,11 @@
"zod-validation-error": "5.0.0"
},
"devDependencies": {
"@dotenvx/dotenvx": "2.37.0",
"@dotenvx/dotenvx": "2.38.2",
"@esbuild-plugins/tsconfig-paths": "0.1.2",
"@react-email/ui": "^6.11.3",
"@react-email/ui": "^6.11.6",
"@tailwindcss/postcss": "4.3.3",
"@tanstack/react-query-devtools": "5.104.3",
"@tanstack/react-query-devtools": "5.105.0",
"@types/better-sqlite3": "7.6.13",
"@types/cookie-parser": "1.4.10",
"@types/cors": "2.8.19",
@@ -155,7 +156,7 @@
"@types/jmespath": "0.15.2",
"@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "9.0.10",
"@types/node": "26.6.5",
"@types/node": "26.6.6",
"@types/nodemailer": "8.0.2",
"@types/nprogress": "0.2.3",
"@types/pg": "8.23.1",
@@ -171,20 +172,20 @@
"drizzle-kit": "0.31.11",
"esbuild": "0.28.2",
"esbuild-node-externals": "2.0.0",
"eslint": "10.12.0",
"eslint-config-next": "16.4.2",
"postcss": "8.5.30",
"eslint": "10.13.0",
"eslint-config-next": "16.4.3",
"postcss": "8.5.32",
"prettier": "3.9.10",
"react-email": "6.11.2",
"react-email": "6.11.6",
"tailwindcss": "4.3.3",
"tsc-alias": "1.9.7",
"tsc-alias": "1.9.9",
"tsx": "4.23.15",
"typescript": "7.0.2",
"typescript-eslint": "8.71.2"
"typescript-eslint": "8.72.1"
},
"overrides": {
"esbuild": "0.28.2",
"dompurify": "3.4.0",
"postcss": "8.5.30"
"postcss": "8.5.32"
}
}
+72
View File
@@ -0,0 +1,72 @@
// Writes a package.json that depends only on the npm packages an esbuild
// bundle imports at runtime, so a standalone image (e.g. Dockerfile.dns) can
// `npm install` just those instead of the full dependency tree. Versions and
// overrides come from the root package.json, and running `npm install` next
// to the root package-lock.json keeps transitive versions pinned to the lock.
//
// Usage: node scripts/bundle-package-json.mjs <metafile.json> <out/package.json>
import { isBuiltin } from "node:module";
import fs from "node:fs";
import path from "node:path";
const [metafilePath, outPath] = process.argv.slice(2);
if (!metafilePath || !outPath) {
console.error(
"Usage: node scripts/bundle-package-json.mjs <metafile.json> <out/package.json>"
);
process.exit(1);
}
const metafile = JSON.parse(fs.readFileSync(metafilePath, "utf8"));
const rootPkg = JSON.parse(fs.readFileSync("package.json", "utf8"));
// "@scope/name/sub/path" -> "@scope/name", "name/sub/path" -> "name"
function packageName(specifier) {
const parts = specifier.split("/");
return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : parts[0];
}
const used = new Set();
for (const output of Object.values(metafile.outputs)) {
for (const imp of output.imports) {
if (!imp.external || isBuiltin(imp.path)) continue;
used.add(packageName(imp.path));
}
}
const dependencies = {};
const missing = [];
for (const name of [...used].sort()) {
const version = rootPkg.dependencies?.[name];
if (version) {
dependencies[name] = version;
} else {
missing.push(name);
}
}
// A runtime import of something that is only a devDependency (or not
// declared at all) would be pruned from the image and fail at startup
if (missing.length > 0) {
console.error(
`Bundle imports packages not in package.json dependencies: ${missing.join(", ")}`
);
process.exit(1);
}
const pkg = {
name: rootPkg.name,
version: rootPkg.version,
private: true,
type: rootPkg.type,
dependencies,
...(rootPkg.overrides && { overrides: rootPkg.overrides })
};
fs.mkdirSync(path.dirname(outPath), { recursive: true });
fs.writeFileSync(outPath, JSON.stringify(pkg, null, 4) + "\n");
console.log(
`Wrote ${outPath} with ${Object.keys(dependencies).length} dependencies: ${Object.keys(dependencies).join(", ")}`
);
+43
View File
@@ -0,0 +1,43 @@
import { spawnSync } from "node:child_process";
import { readdirSync, statSync } from "node:fs";
import { join } from "node:path";
// Test files that fail today. Give each a reason, fix it, then remove it.
const knownFailing = new Map([
[
"server/setup/migrationsSqlite.backup.test.ts",
"migration 1.24.0 fails: missing resources table in seeded db"
],
[
"server/lib/traefik/traefikConfig.test.ts",
"config undefined on import (logger circular dependency)"
]
]);
const skipDirs = new Set(["node_modules", ".next", "dist", ".git"]);
function find(dir) {
return readdirSync(dir).flatMap((name) => {
if (skipDirs.has(name) || name.startsWith("private")) return [];
const p = join(dir, name);
if (statSync(p).isDirectory()) return find(p);
return p.endsWith(".test.ts") ? [p] : [];
});
}
const files = find(".");
const failed = [];
for (const f of files) {
if (knownFailing.has(f)) {
console.log(`\n== SKIPPED ${f}: ${knownFailing.get(f)}`);
continue;
}
console.log(`\n== ${f}`);
const r = spawnSync("npx", ["tsx", f], { stdio: "inherit", shell: process.platform === "win32" });
if (r.status !== 0) failed.push(f);
}
const ran = files.length - knownFailing.size;
console.log(`\n${ran - failed.length}/${ran} test files passed`);
if (failed.length) {
console.log("FAILED:\n" + failed.join("\n"));
process.exit(1);
}
+6
View File
@@ -134,6 +134,10 @@ export enum ActionsEnum {
createOrgDomain = "createOrgDomain",
deleteOrgDomain = "deleteOrgDomain",
restartOrgDomain = "restartOrgDomain",
listCustomDnsRecords = "listCustomDnsRecords",
createCustomDnsRecord = "createCustomDnsRecord",
updateCustomDnsRecord = "updateCustomDnsRecord",
deleteCustomDnsRecord = "deleteCustomDnsRecord",
createRemoteExitNode = "createRemoteExitNode",
updateRemoteExitNode = "updateRemoteExitNode",
getRemoteExitNode = "getRemoteExitNode",
@@ -151,6 +155,8 @@ export enum ActionsEnum {
exportLogs = "exportLogs",
listApprovals = "listApprovals",
updateApprovals = "updateApprovals",
listResourceSessions = "listResourceSessions",
deleteResourceSession = "deleteResourceSession",
signSshKey = "signSshKey",
createEventStreamingDestination = "createEventStreamingDestination",
updateEventStreamingDestination = "updateEventStreamingDestination",
+13 -4
View File
@@ -17,6 +17,11 @@ import config from "@server/lib/config";
import type { RandomReader } from "@oslojs/crypto/random";
import { generateRandomString } from "@oslojs/crypto/random";
import logger from "@server/logger";
import { revalidateIdpSession } from "@server/auth/sessions/idpRevalidation";
import {
resolveClientInfo,
type SessionClientInfo
} from "@server/auth/sessions/clientInfo";
export const SESSION_COOKIE_NAME =
config.getRawConfig().server.session_cookie_name;
@@ -38,7 +43,8 @@ export function generateSessionToken(): string {
export async function createSession(
token: string,
userId: string
userId: string,
clientInfo?: SessionClientInfo
): Promise<Session> {
const sessionId = encodeHexLowerCase(
sha256(new TextEncoder().encode(token))
@@ -49,7 +55,8 @@ export async function createSession(
sessionId: sessionId,
userId,
expiresAt: new Date(Date.now() + SESSION_COOKIE_EXPIRES).getTime(),
issuedAt: new Date().getTime()
issuedAt: new Date().getTime(),
...(await resolveClientInfo(clientInfo))
})
.returning();
return session;
@@ -74,6 +81,9 @@ export async function validateSessionToken(
return { session: null, user: null };
}
const { user, session } = result[0];
if (!(await revalidateIdpSession(session.sessionId))) {
return { session: null, user: null };
}
if (Date.now() >= session.expiresAt) {
await db
.delete(sessions)
@@ -212,5 +222,4 @@ export function generateIdFromEntropySize(size: number): string {
}
export type SessionValidationResult =
| { session: Session; user: User }
| { session: null; user: null };
{ session: Session; user: User } | { session: null; user: null };
+44
View File
@@ -0,0 +1,44 @@
import type { Request } from "express";
import { getCountryCodeForIp } from "@server/lib/geoip";
import { getAsnForIp } from "@server/lib/asn";
// Details about the client that created a session, shown to admins when
// reviewing active sessions. Country and ASN are looked up from the IP when
// not provided.
export type SessionClientInfo = {
ip?: string | null;
userAgent?: string | null;
countryCode?: string | null;
asn?: number | null;
};
const MAX_USER_AGENT_LENGTH = 1024;
export function getRequestClientInfo(req: Request): SessionClientInfo {
return {
ip: req.ip,
userAgent: req.headers["user-agent"]
};
}
export async function resolveClientInfo(
info: SessionClientInfo | undefined
): Promise<Required<SessionClientInfo>> {
const ip = info?.ip || null;
const userAgent = info?.userAgent
? info.userAgent.slice(0, MAX_USER_AGENT_LENGTH)
: null;
let countryCode = info?.countryCode ?? null;
let asn = info?.asn ?? null;
if (ip) {
if (info?.countryCode === undefined) {
countryCode = (await getCountryCodeForIp(ip)) ?? null;
}
if (info?.asn === undefined) {
asn = (await getAsnForIp(ip)) ?? null;
}
}
return { ip, userAgent, countryCode, asn };
}
+356
View File
@@ -0,0 +1,356 @@
import {
db,
idp,
idpGrants,
idpOidcConfig,
IdpOidcConfig,
primaryDb,
resourceSessions,
safeRead,
sessionIdpGrants,
sessions
} from "@server/db";
import config from "@server/lib/config";
import { decrypt, encrypt } from "@server/lib/crypto";
import logger from "@server/logger";
import { encodeBase32LowerCaseNoPadding, encodeBase64 } from "@oslojs/encoding";
import * as arctic from "arctic";
import { and, eq, inArray, isNull, lt, or } from "drizzle-orm";
const REVALIDATION_INTERVAL_MS =
1000 * 60 * config.getRawConfig().server.idp_session_revalidation_minutes;
const RETRY_INTERVAL_MS = Math.min(1000 * 60, REVALIDATION_INTERVAL_MS);
const TOKEN_REQUEST_TIMEOUT_MS = 1000 * 10;
const LEASE_MS = 1000 * 60;
const inFlight = new Map<string, Promise<boolean>>();
function randomId(): string {
return encodeBase32LowerCaseNoPadding(
crypto.getRandomValues(new Uint8Array(20))
);
}
export async function storeIdpRefreshToken(
sessionId: string,
idpId: number,
tokens: arctic.OAuth2Tokens
): Promise<void> {
const [existingIdp] = await db
.select({ sessionRevalidation: idp.sessionRevalidation })
.from(idp)
.where(eq(idp.idpId, idpId))
.limit(1);
if (!existingIdp?.sessionRevalidation) {
return;
}
if (!tokens.hasRefreshToken()) {
logger.debug("IdP returned no refresh token", { idpId });
return;
}
const grantId = randomId();
const key = config.getRawConfig().server.secret!;
await db.transaction(async (trx) => {
await trx.insert(idpGrants).values({
grantId,
idpId,
refreshToken: encrypt(tokens.refreshToken(), key),
validatedAt: Date.now()
});
await trx.insert(sessionIdpGrants).values({ sessionId, grantId });
});
}
export async function clearIdpSessionGrants(idpId: number): Promise<void> {
await db.delete(idpGrants).where(eq(idpGrants.idpId, idpId));
}
export async function revalidateIdpSession(
sessionId: string
): Promise<boolean> {
// select the session so safeRead only falls back if the replica fails
const [row] = await safeRead((d) =>
d
.select({ grant: idpGrants })
.from(sessions)
.leftJoin(
sessionIdpGrants,
eq(sessionIdpGrants.sessionId, sessions.sessionId)
)
.leftJoin(
idpGrants,
eq(idpGrants.grantId, sessionIdpGrants.grantId)
)
.where(eq(sessions.sessionId, sessionId))
);
if (!row?.grant) {
return true;
}
const { grant } = row;
if (grant.revokedAt) {
await endGrantSessions(grant.grantId);
return false;
}
if (Date.now() - grant.validatedAt < REVALIDATION_INTERVAL_MS) {
return true;
}
return revalidateGrant(grant.grantId);
}
export async function inheritIdpGrant(
approvingSessionId: string,
deviceSessionId: string
): Promise<boolean> {
// primary: a lagging replica could miss the grant link
const [approving] = await primaryDb
.select({ grantId: sessionIdpGrants.grantId })
.from(sessions)
.leftJoin(
sessionIdpGrants,
eq(sessionIdpGrants.sessionId, sessions.sessionId)
)
.where(eq(sessions.sessionId, approvingSessionId));
if (!approving) {
return false;
}
if (!approving.grantId) {
return true;
}
if (!(await revalidateIdpSession(approvingSessionId))) {
return false;
}
await primaryDb.insert(sessionIdpGrants).values({
sessionId: deviceSessionId,
grantId: approving.grantId
});
return true;
}
function revalidateGrant(grantId: string): Promise<boolean> {
const pending = inFlight.get(grantId);
if (pending) {
return pending;
}
const check = revalidate(grantId).finally(() => {
inFlight.delete(grantId);
});
inFlight.set(grantId, check);
return check;
}
async function revalidate(grantId: string): Promise<boolean> {
const now = Date.now();
const leaseId = randomId();
const [grant] = await primaryDb
.update(idpGrants)
.set({ leaseId, leaseExpiresAt: now + LEASE_MS })
.where(
and(
eq(idpGrants.grantId, grantId),
isNull(idpGrants.revokedAt),
lt(idpGrants.validatedAt, now - REVALIDATION_INTERVAL_MS),
or(
isNull(idpGrants.attemptedAt),
lt(idpGrants.attemptedAt, now - RETRY_INTERVAL_MS)
),
or(
isNull(idpGrants.leaseExpiresAt),
lt(idpGrants.leaseExpiresAt, now)
)
)
)
.returning();
if (!grant) {
const [current] = await primaryDb
.select({ revokedAt: idpGrants.revokedAt })
.from(idpGrants)
.where(eq(idpGrants.grantId, grantId));
if (current && !current.revokedAt) {
return true;
}
await endGrantSessions(grantId);
return false;
}
const leased = and(
eq(idpGrants.grantId, grantId),
eq(idpGrants.leaseId, leaseId)
);
const releaseLease = { leaseId: null, leaseExpiresAt: null };
const [oidc] = await primaryDb
.select()
.from(idpOidcConfig)
.where(eq(idpOidcConfig.idpId, grant.idpId));
let tokens: arctic.OAuth2Tokens;
try {
tokens = await redeemRefreshToken(oidc, grant.refreshToken);
} catch (err: unknown) {
if (
err instanceof arctic.OAuth2RequestError &&
err.code === "invalid_grant"
) {
const [revoked] = await primaryDb
.update(idpGrants)
.set({ revokedAt: Date.now(), ...releaseLease })
.where(leased)
.returning({ grantId: idpGrants.grantId });
if (!revoked) {
logger.warn("Lost IdP grant lease while revalidating", {
idpId: grant.idpId
});
return true;
}
logger.info("IdP rejected refresh token, ending sessions", {
idpId: grant.idpId,
error: err.code,
description: err.description
});
await endGrantSessions(grantId);
return false;
}
// config errors and outages must not log out every user
if (err instanceof arctic.OAuth2RequestError) {
logger.error("IdP refused to revalidate session", {
idpId: grant.idpId,
error: err.code,
description: err.description
});
} else {
logger.warn("Could not revalidate session with IdP", {
idpId: grant.idpId,
error: err instanceof Error ? err.message : String(err)
});
}
await primaryDb
.update(idpGrants)
.set({ attemptedAt: Date.now(), ...releaseLease })
.where(leased);
return true;
}
const [stored] = await primaryDb
.update(idpGrants)
.set({
refreshToken: rotatedRefreshToken(tokens, grant.refreshToken),
validatedAt: Date.now(),
attemptedAt: null,
...releaseLease
})
.where(leased)
.returning({ grantId: idpGrants.grantId });
if (!stored) {
logger.warn("Lost IdP grant lease while revalidating", {
idpId: grant.idpId
});
}
return true;
}
// the revoked grant stays so sessions linked to it later are refused too
async function endGrantSessions(grantId: string): Promise<void> {
const linked = primaryDb
.select({ sessionId: sessionIdpGrants.sessionId })
.from(sessionIdpGrants)
.where(eq(sessionIdpGrants.grantId, grantId));
await primaryDb.transaction(async (trx) => {
await trx
.delete(resourceSessions)
.where(inArray(resourceSessions.userSessionId, linked));
await trx.delete(sessions).where(inArray(sessions.sessionId, linked));
});
}
// arctic's refreshAccessToken cannot be aborted
async function redeemRefreshToken(
oidc: IdpOidcConfig,
encryptedRefreshToken: string
): Promise<arctic.OAuth2Tokens> {
const key = config.getRawConfig().server.secret!;
const credentials = encodeBase64(
new TextEncoder().encode(
`${decrypt(oidc.clientId, key)}:${decrypt(oidc.clientSecret, key)}`
)
);
const body = new URLSearchParams({
grant_type: "refresh_token",
refresh_token: decrypt(encryptedRefreshToken, key)
});
const scopes = oidc.scopes
.split(" ")
.map((scope) => scope.trim())
.filter((scope) => scope.length > 0);
if (scopes.length > 0) {
body.set("scope", scopes.join(" "));
}
const response = await fetch(oidc.tokenUrl, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
Accept: "application/json",
Authorization: `Basic ${credentials}`
},
body,
signal: AbortSignal.timeout(TOKEN_REQUEST_TIMEOUT_MS)
});
const data: unknown = await response.json().catch(() => null);
if (response.status === 200 && typeof data === "object" && data) {
return new arctic.OAuth2Tokens(data);
}
if (
(response.status === 400 || response.status === 401) &&
typeof data === "object" &&
data &&
"error" in data &&
typeof data.error === "string"
) {
const description =
"error_description" in data &&
typeof data.error_description === "string"
? data.error_description
: null;
throw new arctic.OAuth2RequestError(
data.error,
description,
null,
null
);
}
throw new Error(`Unexpected token endpoint response: ${response.status}`);
}
function rotatedRefreshToken(
tokens: arctic.OAuth2Tokens,
encryptedPrevious: string
): string {
if (!tokens.hasRefreshToken()) {
return encryptedPrevious;
}
return encrypt(tokens.refreshToken(), config.getRawConfig().server.secret!);
}
+11 -1
View File
@@ -4,6 +4,10 @@ import { resourceSessions, ResourceSession } from "@server/db";
import { db, safeRead } from "@server/db";
import { eq, and } from "drizzle-orm";
import config from "@server/lib/config";
import {
resolveClientInfo,
type SessionClientInfo
} from "@server/auth/sessions/clientInfo";
export const SESSION_COOKIE_NAME =
config.getRawConfig().server.session_cookie_name;
@@ -25,6 +29,8 @@ export async function createResourceSession(opts: {
doNotExtend?: boolean;
expiresAt?: number | null;
sessionLength?: number | null;
requestSessionId?: string | null;
clientInfo?: SessionClientInfo;
}): Promise<ResourceSession> {
if (
!opts.passwordId &&
@@ -43,6 +49,8 @@ export async function createResourceSession(opts: {
sha256(new TextEncoder().encode(opts.token))
);
const clientInfo = await resolveClientInfo(opts.clientInfo);
const session: ResourceSession = {
sessionId: sessionId,
expiresAt:
@@ -60,7 +68,9 @@ export async function createResourceSession(opts: {
policyWhitelistId: opts.policyWhitelistId || null,
isRequestToken: opts.isRequestToken || false,
userSessionId: opts.userSessionId || null,
issuedAt: new Date().getTime()
issuedAt: new Date().getTime(),
requestSessionId: opts.requestSessionId || null,
...clientInfo
};
await db.insert(resourceSessions).values(session);
+1
View File
@@ -168,6 +168,7 @@
"iPhone18,5": "iPhone 17e",
"iPhone19,2": "iPhone 18 Pro",
"iPhone19,3": "iPhone 18 Pro Max",
"iPhone19,4": "iPhone Duo",
"iPhone19,7": "iPhone 18 Pro Max",
"iPod1,1": "iPod touch Original",
"iPod2,1": "iPod touch 2nd",
+144
View File
@@ -41,6 +41,36 @@ export const dnsChallenge = pgTable("dnsChallenges", {
completed: boolean("completed").default(false)
});
// User-defined records served by the authoritative DNS server for a domain
// delegated to Pangolin (type "ns"). fullDomain is the lowercase FQDN without
// a trailing dot, precomputed so the DNS server can look records up directly.
export const customDnsRecords = pgTable(
"customDnsRecords",
{
customDnsRecordId: serial("customDnsRecordId").primaryKey(),
domainId: varchar("domainId")
.notNull()
.references(() => domains.domainId, { onDelete: "cascade" }),
name: varchar("name", { length: 255 }).notNull(), // relative to the domain, "@" for the apex
fullDomain: varchar("fullDomain", { length: 255 }).notNull(),
type: varchar("type", { length: 10 })
.$type<"A" | "AAAA" | "CNAME" | "MX" | "NS" | "SRV" | "TXT">()
.notNull(),
values: text("values").notNull(), // JSON array of strings, one resource record each
ttl: integer("ttl").notNull().default(300),
priority: integer("priority"), // MX and SRV
weight: integer("weight"), // SRV
port: integer("port"), // SRV
enabled: boolean("enabled").notNull().default(true),
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
},
(t) => [
index("idx_customDnsRecords_fullDomain").on(t.fullDomain),
index("idx_customDnsRecords_domainId").on(t.domainId)
]
);
export const account = pgTable("account", {
accountId: serial("accountId").primaryKey(),
userId: varchar("userId")
@@ -697,10 +727,119 @@ export const aiSessionLog = pgTable(
]
);
// One row per SSH connection to a site's native SSH server. Newt reports a
// long-running session once per flush, so rows are upserted on
// (siteId, sessionId) and commandCount accumulates across flushes.
export const sshSessionLog = pgTable(
"sshSessionLog",
{
id: serial("id").primaryKey(),
sessionId: varchar("sessionId").notNull(),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" }),
siteResourceId: integer("siteResourceId").references(
() => siteResources.siteResourceId,
{ onDelete: "set null" }
),
// public resource, for sessions opened through the browser gateway
resourceId: integer("resourceId").references(
() => resources.resourceId,
{ onDelete: "set null" }
),
clientId: integer("clientId").references(() => clients.clientId, {
onDelete: "set null"
}),
userId: varchar("userId").references(() => users.userId, {
onDelete: "set null"
}),
transport: varchar("transport").notNull().default("ssh"), // ssh or browser
sourceAddr: varchar("sourceAddr").notNull(),
destAddr: varchar("destAddr").notNull(),
username: varchar("username").notNull(), // system user on the host
authMethod: varchar("authMethod"), // publickey, certificate, password
keyFingerprint: varchar("keyFingerprint"),
certKeyId: varchar("certKeyId"),
commandCount: integer("commandCount").notNull().default(0),
startedAt: bigint("startedAt", { mode: "number" }).notNull(), // epoch seconds
endedAt: bigint("endedAt", { mode: "number" })
},
(t) => [
uniqueIndex("idx_ssh_session_log_site_session").on(
t.siteId,
t.sessionId
),
index("idx_ssh_session_log_org_started").on(t.orgId, t.startedAt),
index("idx_ssh_session_log_org_site_resource_started").on(
t.orgId,
t.siteResourceId,
t.startedAt
),
index("idx_ssh_session_log_org_resource_started").on(
t.orgId,
t.resourceId,
t.startedAt
),
index("idx_ssh_session_log_org_user_started").on(
t.orgId,
t.userId,
t.startedAt
)
]
);
export const sshCommandLog = pgTable(
"sshCommandLog",
{
id: serial("id").primaryKey(),
sshSessionLogId: integer("sshSessionLogId")
.notNull()
.references(() => sshSessionLog.id, { onDelete: "cascade" }),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
command: text("command").notNull(),
channel: varchar("channel").notNull(), // shell or exec
// Interactive line edited with history recall or tab completion, so
// the reconstructed text may differ from what the shell ran.
inexact: boolean("inexact").notNull().default(false),
timestamp: bigint("timestamp", { mode: "number" }).notNull() // epoch seconds
},
(t) => [
index("idx_ssh_command_log_session_timestamp").on(
t.sshSessionLogId,
t.timestamp
),
index("idx_ssh_command_log_org_timestamp").on(t.orgId, t.timestamp)
]
);
// Marks a dashboard session that must log in again before it can get a new
// SSO session for a resource, set when an admin revokes that resource session.
export const resourceSessionReauth = pgTable(
"resourceSessionReauth",
{
userSessionId: varchar("userSessionId")
.notNull()
.references(() => sessions.sessionId, { onDelete: "cascade" }),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
revokedAt: bigint("revokedAt", { mode: "number" }).notNull()
},
(table) => [
primaryKey({ columns: [table.userSessionId, table.resourceId] })
]
);
export type Approval = InferSelectModel<typeof approvals>;
export type Limit = InferSelectModel<typeof limits>;
export type Account = InferSelectModel<typeof account>;
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
export type CustomDnsRecord = InferSelectModel<typeof customDnsRecords>;
export type Customer = InferSelectModel<typeof customers>;
export type Subscription = InferSelectModel<typeof subscriptions>;
export type SubscriptionItem = InferSelectModel<typeof subscriptionItems>;
@@ -744,3 +883,8 @@ export type AlertEmailRecipients = InferSelectModel<
export type AlertWebhookActions = InferSelectModel<typeof alertWebhookActions>;
export type TrialNotification = InferSelectModel<typeof trialNotifications>;
export type AiSessionLog = InferSelectModel<typeof aiSessionLog>;
export type SshSessionLog = InferSelectModel<typeof sshSessionLog>;
export type SshCommandLog = InferSelectModel<typeof sshCommandLog>;
export type ResourceSessionReauth = InferSelectModel<
typeof resourceSessionReauth
>;
+50 -4
View File
@@ -69,6 +69,9 @@ export const orgs = pgTable("orgs", {
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull()
.default(0),
settingsLogRetentionDaysSsh: integer("settingsLogRetentionDaysSsh") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull()
.default(0),
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
isBillingOrg: boolean("isBillingOrg"),
@@ -210,6 +213,7 @@ export const resources = pgTable(
postAuthPath: text("postAuthPath"),
health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown"
wildcard: boolean("wildcard").notNull().default(false),
matchBaseDomain: boolean("matchBaseDomain").notNull().default(false), // wildcard resources only: also serve the host the wildcard sits under
mode: text("mode")
.default("http")
.$type<
@@ -753,11 +757,38 @@ export const sessions = pgTable(
.references(() => users.userId, { onDelete: "cascade" }),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
issuedAt: bigint("issuedAt", { mode: "number" }),
deviceAuthUsed: boolean("deviceAuthUsed").notNull().default(false)
deviceAuthUsed: boolean("deviceAuthUsed").notNull().default(false),
// Client details captured when the session was created
ip: varchar("ip"),
userAgent: text("userAgent"),
countryCode: varchar("countryCode"),
asn: integer("asn")
},
(t) => [index("idx_sessions_userid").on(t.userId)]
);
export const idpGrants = pgTable("idpGrants", {
grantId: varchar("grantId").primaryKey(),
idpId: integer("idpId")
.notNull()
.references(() => idp.idpId, { onDelete: "cascade" }),
refreshToken: text("refreshToken").notNull(),
validatedAt: bigint("validatedAt", { mode: "number" }).notNull(),
attemptedAt: bigint("attemptedAt", { mode: "number" }),
leaseId: varchar("leaseId"),
leaseExpiresAt: bigint("leaseExpiresAt", { mode: "number" }),
revokedAt: bigint("revokedAt", { mode: "number" })
});
export const sessionIdpGrants = pgTable("sessionIdpGrants", {
sessionId: varchar("sessionId")
.primaryKey()
.references(() => sessions.sessionId, { onDelete: "cascade" }),
grantId: varchar("grantId")
.notNull()
.references(() => idpGrants.grantId, { onDelete: "cascade" })
});
export const newtSessions = pgTable("newtSession", {
sessionId: varchar("id").primaryKey(),
newtId: varchar("newtId")
@@ -1160,7 +1191,15 @@ export const resourceSessions = pgTable("resourceSessions", {
onDelete: "cascade"
}
),
issuedAt: bigint("issuedAt", { mode: "number" })
issuedAt: bigint("issuedAt", { mode: "number" }),
// Id of the short-lived request token this session was exchanged from.
// Login audit logs reference that id, so this links them to the session.
requestSessionId: varchar("requestSessionId"),
// Client details captured when the session was created
ip: varchar("ip"),
userAgent: text("userAgent"),
countryCode: varchar("countryCode"),
asn: integer("asn")
});
export const resourceWhitelist = pgTable("resourceWhitelist", {
@@ -1278,6 +1317,9 @@ export const idp = pgTable("idp", {
defaultRoleMapping: varchar("defaultRoleMapping"),
defaultOrgMapping: varchar("defaultOrgMapping"),
autoProvision: boolean("autoProvision").notNull().default(false),
sessionRevalidation: boolean("sessionRevalidation")
.notNull()
.default(false),
tags: text("tags")
});
@@ -1391,7 +1433,8 @@ export const idpOrg = pgTable("idpOrg", {
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
roleMapping: varchar("roleMapping"),
orgMapping: varchar("orgMapping")
orgMapping: varchar("orgMapping"),
autoProvision: boolean("autoProvision").notNull().default(false)
});
export const clients = pgTable(
@@ -1488,6 +1531,7 @@ export const olms = pgTable(
dateCreated: varchar("dateCreated").notNull(),
version: text("version"),
agent: text("agent"),
agentVersion: text("agentVersion"),
name: varchar("name"),
clientId: integer("clientId").references(() => clients.clientId, {
// we will switch this depending on the current org it wants to connect to
@@ -1739,7 +1783,8 @@ export const deviceWebAuthCodes = pgTable("deviceWebAuthCodes", {
verified: boolean("verified").notNull().default(false),
userId: varchar("userId").references(() => users.userId, {
onDelete: "cascade"
})
}),
approvingSessionId: varchar("approvingSessionId")
});
export const roundTripMessageTracker = pgTable("roundTripMessageTracker", {
@@ -2157,6 +2202,7 @@ export type SetupToken = InferSelectModel<typeof setupTokens>;
export type HostMeta = InferSelectModel<typeof hostMeta>;
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
export type IdpGrant = InferSelectModel<typeof idpGrants>;
export type Blueprint = InferSelectModel<typeof blueprints>;
export type LicenseKey = InferSelectModel<typeof licenseKey>;
export type SecurityKey = InferSelectModel<typeof securityKeys>;
+48 -28
View File
@@ -42,6 +42,7 @@ import {
import { alias } from "@server/db";
import { and, desc, eq, inArray, isNull, not, or, sql } from "drizzle-orm";
import logger from "@server/logger";
import { pickResourceForHost } from "@server/lib/traefik/rule";
export type ResourceWithAuth = {
resource: Resource | null;
@@ -123,28 +124,59 @@ export async function getRedirectsByHost(
not(isNull(resources.domainId))
)
),
inArray(redirectHost, candidates)
or(
inArray(redirectHost, candidates),
// A redirect on a resource follows it onto its base host
and(
eq(resources.matchBaseDomain, true),
eq(redirectHost, `*.${host}`)
)
)
)
)
.orderBy(desc(redirects.priority));
}
/**
* Condition for the resources that may serve a host. Narrow the matches down
* with pickResourceForHost.
*/
export function resourceHostCondition(host: string) {
// Build wildcard domain variants to match against.
// For a domain like "me.example.test.com", we want to match:
// - "*.example.test.com" (subdomain wildcard)
// - "*.test.com" (parent wildcard, i.e. just "*" subdomain on parent)
const parts = host.split(".");
const wildcardCandidates: string[] = [];
for (let i = 1; i < parts.length; i++) {
wildcardCandidates.push(`*.${parts.slice(i).join(".")}`);
}
return or(
// Exact match
eq(resources.fullDomain, host),
// Wildcard match: resource fullDomain is one of the wildcard candidates
wildcardCandidates.length > 0
? and(
eq(resources.wildcard, true),
inArray(resources.fullDomain, wildcardCandidates)
)
: sql`false`,
// Base match: "*.me.example.test.com" also serving "me.example.test.com"
and(
eq(resources.wildcard, true),
eq(resources.matchBaseDomain, true),
eq(resources.fullDomain, `*.${host}`)
)
);
}
/**
* Get resource by domain with pincode and password information
*/
export async function getResourceByDomain(
domain: string
): Promise<ResourceWithAuth | null> {
// Build wildcard domain variants to match against.
// For a domain like "me.example.test.com", we want to match:
// - "*.example.test.com" (subdomain wildcard)
// - "*.test.com" (parent wildcard, i.e. just "*" subdomain on parent)
const parts = domain.split(".");
const wildcardCandidates: string[] = [];
for (let i = 1; i < parts.length; i++) {
wildcardCandidates.push(`*.${parts.slice(i).join(".")}`);
}
const sharedPolicy = alias(resourcePolicies, "sharedPolicy");
const defaultPolicy = alias(resourcePolicies, "defaultPolicy");
const sharedPolicyPincode = alias(
@@ -248,29 +280,17 @@ export async function getResourceByDomain(
)
)
.innerJoin(orgs, eq(orgs.orgId, resources.orgId))
.where(
or(
// Exact match
eq(resources.fullDomain, domain),
// Wildcard match: resource fullDomain is one of the wildcard candidates
wildcardCandidates.length > 0
? and(
eq(resources.wildcard, true),
inArray(resources.fullDomain, wildcardCandidates)
)
: sql`false`
)
);
.where(resourceHostCondition(domain));
if (!potentialResults.length) {
return null;
}
// Prefer exact match over wildcard match
const exactMatch = potentialResults.find(
(r) => r.resources?.fullDomain === domain
const result = pickResourceForHost(
potentialResults,
domain,
(r) => r.resources?.fullDomain
);
const result = exactMatch ?? potentialResults[0];
if (!result) {
return null;
+149
View File
@@ -37,6 +37,40 @@ export const dnsChallenge = sqliteTable("dnsChallenges", {
completed: integer("completed", { mode: "boolean" }).default(false)
});
// User-defined records served by the authoritative DNS server for a domain
// delegated to Pangolin (type "ns"). fullDomain is the lowercase FQDN without
// a trailing dot, precomputed so the DNS server can look records up directly.
export const customDnsRecords = sqliteTable(
"customDnsRecords",
{
customDnsRecordId: integer("customDnsRecordId").primaryKey({
autoIncrement: true
}),
domainId: text("domainId")
.notNull()
.references(() => domains.domainId, { onDelete: "cascade" }),
name: text("name").notNull(), // relative to the domain, "@" for the apex
fullDomain: text("fullDomain").notNull(),
type: text("type")
.$type<"A" | "AAAA" | "CNAME" | "MX" | "NS" | "SRV" | "TXT">()
.notNull(),
values: text("values").notNull(), // JSON array of strings, one resource record each
ttl: integer("ttl").notNull().default(300),
priority: integer("priority"), // MX and SRV
weight: integer("weight"), // SRV
port: integer("port"), // SRV
enabled: integer("enabled", { mode: "boolean" })
.notNull()
.default(true),
createdAt: integer("createdAt").notNull(),
updatedAt: integer("updatedAt").notNull()
},
(t) => [
index("idx_customDnsRecords_fullDomain").on(t.fullDomain),
index("idx_customDnsRecords_domainId").on(t.domainId)
]
);
export const account = sqliteTable("account", {
accountId: integer("accountId").primaryKey({ autoIncrement: true }),
userId: text("userId")
@@ -696,10 +730,120 @@ export const aiSessionLog = sqliteTable(
]
);
// One row per SSH connection to a site's native SSH server. Newt reports a
// long-running session once per flush, so rows are upserted on
// (siteId, sessionId) and commandCount accumulates across flushes.
export const sshSessionLog = sqliteTable(
"sshSessionLog",
{
id: integer("id").primaryKey({ autoIncrement: true }),
sessionId: text("sessionId").notNull(),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
siteId: integer("siteId")
.notNull()
.references(() => sites.siteId, { onDelete: "cascade" }),
siteResourceId: integer("siteResourceId").references(
() => siteResources.siteResourceId,
{ onDelete: "set null" }
),
// public resource, for sessions opened through the browser gateway
resourceId: integer("resourceId").references(
() => resources.resourceId,
{ onDelete: "set null" }
),
clientId: integer("clientId").references(() => clients.clientId, {
onDelete: "set null"
}),
userId: text("userId").references(() => users.userId, {
onDelete: "set null"
}),
transport: text("transport").notNull().default("ssh"), // ssh or browser
sourceAddr: text("sourceAddr").notNull(),
destAddr: text("destAddr").notNull(),
username: text("username").notNull(), // system user on the host
authMethod: text("authMethod"), // publickey, certificate, password
keyFingerprint: text("keyFingerprint"),
certKeyId: text("certKeyId"),
commandCount: integer("commandCount").notNull().default(0),
startedAt: integer("startedAt").notNull(), // epoch seconds
endedAt: integer("endedAt")
},
(t) => [
uniqueIndex("idx_ssh_session_log_site_session").on(
t.siteId,
t.sessionId
),
index("idx_ssh_session_log_org_started").on(t.orgId, t.startedAt),
index("idx_ssh_session_log_org_site_resource_started").on(
t.orgId,
t.siteResourceId,
t.startedAt
),
index("idx_ssh_session_log_org_resource_started").on(
t.orgId,
t.resourceId,
t.startedAt
),
index("idx_ssh_session_log_org_user_started").on(
t.orgId,
t.userId,
t.startedAt
)
]
);
export const sshCommandLog = sqliteTable(
"sshCommandLog",
{
id: integer("id").primaryKey({ autoIncrement: true }),
sshSessionLogId: integer("sshSessionLogId")
.notNull()
.references(() => sshSessionLog.id, { onDelete: "cascade" }),
orgId: text("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
command: text("command").notNull(),
channel: text("channel").notNull(), // shell or exec
// Interactive line edited with history recall or tab completion, so
// the reconstructed text may differ from what the shell ran.
inexact: integer("inexact", { mode: "boolean" })
.notNull()
.default(false),
timestamp: integer("timestamp").notNull() // epoch seconds
},
(t) => [
index("idx_ssh_command_log_session_timestamp").on(
t.sshSessionLogId,
t.timestamp
),
index("idx_ssh_command_log_org_timestamp").on(t.orgId, t.timestamp)
]
);
// Marks a dashboard session that must log in again before it can get a new
// SSO session for a resource, set when an admin revokes that resource session.
export const resourceSessionReauth = sqliteTable(
"resourceSessionReauth",
{
userSessionId: text("userSessionId")
.notNull()
.references(() => sessions.sessionId, { onDelete: "cascade" }),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
revokedAt: integer("revokedAt").notNull()
},
(table) => [
primaryKey({ columns: [table.userSessionId, table.resourceId] })
]
);
export type Approval = InferSelectModel<typeof approvals>;
export type Limit = InferSelectModel<typeof limits>;
export type Account = InferSelectModel<typeof account>;
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
export type CustomDnsRecord = InferSelectModel<typeof customDnsRecords>;
export type Customer = InferSelectModel<typeof customers>;
export type Subscription = InferSelectModel<typeof subscriptions>;
export type SubscriptionItem = InferSelectModel<typeof subscriptionItems>;
@@ -735,3 +879,8 @@ export type AlertEmailRecipient = InferSelectModel<typeof alertEmailRecipients>;
export type AlertWebhookAction = InferSelectModel<typeof alertWebhookActions>;
export type TrialNotification = InferSelectModel<typeof trialNotifications>;
export type AiSessionLog = InferSelectModel<typeof aiSessionLog>;
export type SshSessionLog = InferSelectModel<typeof sshSessionLog>;
export type SshCommandLog = InferSelectModel<typeof sshCommandLog>;
export type ResourceSessionReauth = InferSelectModel<
typeof resourceSessionReauth
>;
+58 -4
View File
@@ -68,6 +68,9 @@ export const orgs = sqliteTable("orgs", {
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull()
.default(7),
settingsLogRetentionDaysSsh: integer("settingsLogRetentionDaysSsh") // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull()
.default(0),
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
isBillingOrg: integer("isBillingOrg", { mode: "boolean" }),
@@ -236,6 +239,9 @@ export const resources = sqliteTable(
wildcard: integer("wildcard", { mode: "boolean" })
.notNull()
.default(false),
matchBaseDomain: integer("matchBaseDomain", { mode: "boolean" }) // wildcard resources only: also serve the host the wildcard sits under
.notNull()
.default(false),
mode: text("mode")
.default("http")
.$type<
@@ -843,6 +849,7 @@ export const olms = sqliteTable(
dateCreated: text("dateCreated").notNull(),
version: text("version"),
agent: text("agent"),
agentVersion: text("agentVersion"),
name: text("name"),
clientId: integer("clientId").references(() => clients.clientId, {
// we will switch this depending on the current org it wants to connect to
@@ -1030,11 +1037,38 @@ export const sessions = sqliteTable(
issuedAt: integer("issuedAt"),
deviceAuthUsed: integer("deviceAuthUsed", { mode: "boolean" })
.notNull()
.default(false)
.default(false),
// Client details captured when the session was created
ip: text("ip"),
userAgent: text("userAgent"),
countryCode: text("countryCode"),
asn: integer("asn")
},
(table) => [index("idx_sessions_userId").on(table.userId)]
);
export const idpGrants = sqliteTable("idpGrants", {
grantId: text("grantId").primaryKey(),
idpId: integer("idpId")
.notNull()
.references(() => idp.idpId, { onDelete: "cascade" }),
refreshToken: text("refreshToken").notNull(),
validatedAt: integer("validatedAt").notNull(),
attemptedAt: integer("attemptedAt"),
leaseId: text("leaseId"),
leaseExpiresAt: integer("leaseExpiresAt"),
revokedAt: integer("revokedAt")
});
export const sessionIdpGrants = sqliteTable("sessionIdpGrants", {
sessionId: text("sessionId")
.primaryKey()
.references(() => sessions.sessionId, { onDelete: "cascade" }),
grantId: text("grantId")
.notNull()
.references(() => idpGrants.grantId, { onDelete: "cascade" })
});
export const newtSessions = sqliteTable("newtSession", {
sessionId: text("id").primaryKey(),
newtId: text("newtId")
@@ -1436,7 +1470,15 @@ export const resourceSessions = sqliteTable("resourceSessions", {
onDelete: "cascade"
}
),
issuedAt: integer("issuedAt")
issuedAt: integer("issuedAt"),
// Id of the short-lived request token this session was exchanged from.
// Login audit logs reference that id, so this links them to the session.
requestSessionId: text("requestSessionId"),
// Client details captured when the session was created
ip: text("ip"),
userAgent: text("userAgent"),
countryCode: text("countryCode"),
asn: integer("asn")
});
export const resourceWhitelist = sqliteTable("resourceWhitelist", {
@@ -1593,6 +1635,11 @@ export const idp = sqliteTable("idp", {
})
.notNull()
.default(false),
sessionRevalidation: integer("sessionRevalidation", {
mode: "boolean"
})
.notNull()
.default(false),
tags: text("tags")
});
@@ -1711,7 +1758,12 @@ export const idpOrg = sqliteTable("idpOrg", {
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
roleMapping: text("roleMapping"),
orgMapping: text("orgMapping")
orgMapping: text("orgMapping"),
autoProvision: integer("autoProvision", {
mode: "boolean"
})
.notNull()
.default(false)
});
// Blueprint runs
@@ -1780,7 +1832,8 @@ export const deviceWebAuthCodes = sqliteTable("deviceWebAuthCodes", {
verified: integer("verified", { mode: "boolean" }).notNull().default(false),
userId: text("userId").references(() => users.userId, {
onDelete: "cascade"
})
}),
approvingSessionId: text("approvingSessionId")
});
export const roundTripMessageTracker = sqliteTable("roundTripMessageTracker", {
@@ -2204,6 +2257,7 @@ export type SetupToken = InferSelectModel<typeof setupTokens>;
export type HostMeta = InferSelectModel<typeof hostMeta>;
export type TargetHealthCheck = InferSelectModel<typeof targetHealthCheck>;
export type IdpOidcConfig = InferSelectModel<typeof idpOidcConfig>;
export type IdpGrant = InferSelectModel<typeof idpGrants>;
export type Blueprint = InferSelectModel<typeof blueprints>;
export type LicenseKey = InferSelectModel<typeof licenseKey>;
export type SecurityKey = InferSelectModel<typeof securityKeys>;
+97 -9
View File
@@ -18,6 +18,14 @@ export type CatalogProvider = (typeof CATALOG_PROVIDERS)[number];
const CATALOG_PROVIDER_SET = new Set<string>(CATALOG_PROVIDERS);
/**
* Operator-defined entries for models served by `custom` providers (e.g. a
* local Ollama instance). These live in their own index so they never leak
* into the typed catalogs or the cross-provider pricing fallback used by
* aggregators.
*/
export const CUSTOM_CATALOG_PROVIDER = "custom";
// Each of our provider types maps to at most one catalog provider. Provider
// types that proxy arbitrary underlying models (openRouter, vercelAiGateway,
// custom) have no mapping.
@@ -59,7 +67,12 @@ export type AiModelCapabilityFlags = {
};
export type AiModelCatalogEntry = {
provider: CatalogProvider;
provider: CatalogProvider | typeof CUSTOM_CATALOG_PROVIDER;
/**
* Only meaningful on `custom` entries: scopes the entry to the custom
* provider with this niceId. Null applies to every custom provider.
*/
providerNiceId: string | null;
model: string;
pricing: {
in: number | null;
@@ -84,6 +97,7 @@ const flag = z.boolean().nullable().optional();
const catalogEntrySchema = z.object({
model: z.string(),
provider: z.string(),
providerNiceId: z.string().nullable().optional(),
pricing: z
.object({
in: z.number().nullable().optional(),
@@ -116,7 +130,12 @@ const catalogFileSchema = z.object({
type RawCatalogEntry = z.infer<typeof catalogEntrySchema>;
function normalizeCatalogProvider(raw: string): CatalogProvider | null {
function normalizeCatalogProvider(
raw: string
): CatalogProvider | typeof CUSTOM_CATALOG_PROVIDER | null {
if (raw === CUSTOM_CATALOG_PROVIDER) {
return CUSTOM_CATALOG_PROVIDER;
}
if (CATALOG_PROVIDER_SET.has(raw)) {
return raw as CatalogProvider;
}
@@ -144,6 +163,10 @@ function normalizeEntry(raw: RawCatalogEntry): AiModelCatalogEntry | null {
return {
provider,
providerNiceId:
provider === CUSTOM_CATALOG_PROVIDER
? raw.providerNiceId || null
: null,
model: raw.model,
pricing: {
in: raw.pricing?.in ?? null,
@@ -170,11 +193,18 @@ function providerKey(provider: CatalogProvider, key: string): string {
return `${provider}\0${key}`;
}
// niceIds are never empty, so "" safely stands for "every custom provider".
function customKey(providerNiceId: string | null, key: string): string {
return `${providerNiceId ?? ""}\0${key}`;
}
export class AiModelCatalog {
private entries: AiModelCatalogEntry[] = [];
private byProvider = new Map<CatalogProvider, AiModelCatalogEntry[]>();
private byProviderAndKey = new Map<string, AiModelCatalogEntry>();
private byKey = new Map<string, AiModelCatalogEntry[]>();
private customByKey = new Map<string, AiModelCatalogEntry>();
private customEntries: AiModelCatalogEntry[] = [];
private refreshTimer: NodeJS.Timeout | null = null;
/**
@@ -204,6 +234,41 @@ export class AiModelCatalog {
return this.byKey.get(key) ?? [];
}
/**
* Custom-provider entry for a model key: one scoped to `providerNiceId`
* wins over an unscoped one.
*/
getCustom(
providerNiceId: string | null | undefined,
key: string
): AiModelCatalogEntry | undefined {
if (providerNiceId) {
const scoped = this.customByKey.get(customKey(providerNiceId, key));
if (scoped) {
return scoped;
}
}
return this.customByKey.get(customKey(null, key));
}
/**
* Custom-provider entries visible to `providerNiceId`: those scoped to it
* plus unscoped ones, scoped first.
*/
listCustom(
providerNiceId: string | null | undefined
): AiModelCatalogEntry[] {
const scoped = providerNiceId
? this.customEntries.filter(
(e) => e.providerNiceId === providerNiceId
)
: [];
const unscoped = this.customEntries.filter(
(e) => e.providerNiceId == null
);
return [...scoped, ...unscoped];
}
/** Full in-memory catalog. */
getAll(): AiModelCatalogEntry[] {
return this.entries;
@@ -213,8 +278,19 @@ export class AiModelCatalog {
const byProvider = new Map<CatalogProvider, AiModelCatalogEntry[]>();
const byProviderAndKey = new Map<string, AiModelCatalogEntry>();
const byKey = new Map<string, AiModelCatalogEntry[]>();
const customByKey = new Map<string, AiModelCatalogEntry>();
const customEntries: AiModelCatalogEntry[] = [];
for (const entry of entries) {
if (entry.provider === CUSTOM_CATALOG_PROVIDER) {
customEntries.push(entry);
const mapKey = customKey(entry.providerNiceId, entry.model);
if (!customByKey.has(mapKey)) {
customByKey.set(mapKey, entry);
}
continue;
}
const list = byProvider.get(entry.provider) ?? [];
list.push(entry);
byProvider.set(entry.provider, list);
@@ -233,6 +309,8 @@ export class AiModelCatalog {
this.byProvider = byProvider;
this.byProviderAndKey = byProviderAndKey;
this.byKey = byKey;
this.customByKey = customByKey;
this.customEntries = customEntries;
}
private async fetchFromFile(
@@ -342,15 +420,22 @@ export const aiModelCatalog = new AiModelCatalog();
* Full catalog entries for a provider type, deduplicated by model id and
* sorted by id. Model discovery uses these to report real token limits and
* capability flags; `listCatalogModelsForType` is the id-only view of the
* same list.
* same list. Custom providers get the operator's `custom` entries, narrowed
* to `providerNiceId` when given.
*/
export function listCatalogEntriesForType(
type: AiProviderType,
query?: string
query?: string,
providerNiceId?: string | null
): AiModelCatalogEntry[] {
const catalogProvider = getCatalogProviderForType(type);
let entries = catalogProvider ? aiModelCatalog.list(catalogProvider) : [];
let entries =
type === "custom"
? aiModelCatalog.listCustom(providerNiceId)
: catalogProvider
? aiModelCatalog.list(catalogProvider)
: [];
if (query) {
const q = query.toLowerCase();
@@ -371,11 +456,14 @@ export function listCatalogEntriesForType(
export function listCatalogModelsForType(
type: AiProviderType,
query?: string
query?: string,
providerNiceId?: string | null
): { model: string }[] {
return listCatalogEntriesForType(type, query).map((entry) => ({
model: entry.model
}));
return listCatalogEntriesForType(type, query, providerNiceId).map(
(entry) => ({
model: entry.model
})
);
}
/**
+29 -4
View File
@@ -66,20 +66,45 @@ function findEntry(
return null;
}
function findCustomEntry(
modelId: string,
providerNiceId: string | null | undefined
): AiModelCatalogEntry | null {
for (const key of [modelId, stripVendorPrefix(modelId)]) {
if (key == null) {
continue;
}
const match = aiModelCatalog.getCustom(providerNiceId, key);
if (match) {
return match;
}
}
return null;
}
/**
* Looks up per-token pricing for a model, scoped first to the catalog
* provider that corresponds to our provider type, then falling back to a
* global search across every provider (marked `approximate`) for provider
* types that proxy arbitrary underlying models.
* provider that corresponds to our provider type (or, for custom providers,
* the operator's `custom` entries for this provider's niceId), then falling
* back to a global search across every provider (marked `approximate`) for
* provider types that proxy arbitrary underlying models.
*/
export function getModelPricing(
providerType: AiProviderType,
modelId: string | undefined
modelId: string | undefined,
providerNiceId?: string | null
): AiModelPricing | null {
if (!modelId) {
return null;
}
if (providerType === "custom") {
const custom = findCustomEntry(modelId, providerNiceId);
if (custom) {
return toPricing(custom, false);
}
}
const catalogProvider = getCatalogProviderForType(providerType);
if (catalogProvider) {
+83
View File
@@ -0,0 +1,83 @@
import { extractUsage } from "./aiUsageExtraction";
import { assertEqualsObj } from "@test/assert";
// Bedrock reports `inputTokens` as the NON-cached input tokens when prompt
// caching is on; cache reads/writes are separate counters. So `promptTokens`
// (input billed at the normal rate) must equal `inputTokens` as-is.
const headers = new Headers();
function testConverseJsonWithCache() {
console.log("Running Bedrock Converse JSON cache usage test...");
const body = JSON.stringify({
usage: {
inputTokens: 100,
outputTokens: 20,
totalTokens: 1120,
cacheReadInputTokens: 800,
cacheWriteInputTokens: 200
}
});
assertEqualsObj(
extractUsage("bedrock_converse", body, false, headers),
{
promptTokens: 100,
cacheReadTokens: 800,
cacheWriteTokens: 200,
completionTokens: 20,
reasoningTokens: 0,
estimated: false
},
"cache reads must not be subtracted from inputTokens"
);
}
function testConverseStreamWithCache() {
console.log("Running Bedrock Converse stream cache usage test...");
// converse-stream frames the metadata event in binary envelope; the JSON
// text survives inside it, so it is not valid JSON as a whole.
const body =
'\u0000\u0001{"metrics":{"latencyMs":5},"usage":{"cacheReadInputTokens":800,' +
'"cacheWriteInputTokens":0,"inputTokens":100,"outputTokens":20,"totalTokens":920}}\u0002';
assertEqualsObj(
extractUsage("bedrock_converse", body, true, headers),
{
promptTokens: 100,
cacheReadTokens: 800,
cacheWriteTokens: 0,
completionTokens: 20,
reasoningTokens: 0,
estimated: false
},
"streamed cache reads must not be subtracted from inputTokens"
);
}
function testConverseWithoutCache() {
console.log("Running Bedrock Converse no-cache usage test...");
const body = JSON.stringify({
usage: { inputTokens: 50, outputTokens: 7, totalTokens: 57 }
});
assertEqualsObj(
extractUsage("bedrock_converse", body, false, headers),
{
promptTokens: 50,
cacheReadTokens: 0,
cacheWriteTokens: 0,
completionTokens: 7,
reasoningTokens: 0,
estimated: false
},
"uncached usage is unchanged"
);
}
try {
testConverseJsonWithCache();
testConverseStreamWithCache();
testConverseWithoutCache();
console.log("All aiUsageExtraction tests passed!");
} catch (error) {
console.error("Test failed:", error);
process.exit(1);
}
+5 -7
View File
@@ -248,12 +248,11 @@ function extractBedrockConverse(
const parsed = tryParseJson(text);
const usage = parsed?.usage;
if (usage) {
// With prompt caching on, Bedrock's `inputTokens` already excludes
// the cache read/write counters, so it is used as-is.
const cacheReadTokens = usage.cacheReadInputTokens ?? 0;
return {
promptTokens: Math.max(
0,
(usage.inputTokens ?? 0) - cacheReadTokens
),
promptTokens: usage.inputTokens ?? 0,
cacheReadTokens,
cacheWriteTokens: usage.cacheWriteInputTokens ?? 0,
completionTokens: usage.outputTokens ?? 0,
@@ -271,10 +270,9 @@ function extractBedrockConverse(
if (fields.inputTokens === undefined) {
return null;
}
const cacheReadTokens = fields.cacheReadInputTokens ?? 0;
return {
promptTokens: Math.max(0, fields.inputTokens - cacheReadTokens),
cacheReadTokens,
promptTokens: fields.inputTokens,
cacheReadTokens: fields.cacheReadInputTokens ?? 0,
cacheWriteTokens: fields.cacheWriteInputTokens ?? 0,
completionTokens: fields.outputTokens ?? 0,
reasoningTokens: 0,
+8 -2
View File
@@ -10,6 +10,7 @@ export enum TierFeature {
ActionLogs = "actionLogs", // set the retention period to none on downgrade
ConnectionLogs = "connectionLogs",
AISessionLogs = "aiSessionLogs",
SSHSessionLogs = "sshSessionLogs",
MaintenancePage = "maintenancePage", // handle downgrade
DevicePosture = "devicePosture",
TwoFactorEnforcement = "twoFactorEnforcement", // handle downgrade by setting to optional
@@ -25,7 +26,9 @@ export enum TierFeature {
WildcardSubdomain = "wildcardSubdomain",
NewtAutoUpdate = "newtAutoUpdate",
ResourcePolicies = "resourcePolicies",
RoleBasedSSHControls = "roleBasedSSHControls"
RoleBasedSSHControls = "roleBasedSSHControls",
CustomDnsRecords = "customDnsRecords", // handle downgrade by disabling custom DNS records
ResourceSessions = "resourceSessions"
}
export const tierMatrix: Record<TierFeature, Tier[]> = {
@@ -38,6 +41,7 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
[TierFeature.ActionLogs]: ["tier2", "tier3", "enterprise"],
[TierFeature.ConnectionLogs]: ["tier2", "tier3", "enterprise"],
[TierFeature.AISessionLogs]: ["tier2", "tier3", "enterprise"],
[TierFeature.SSHSessionLogs]: ["tier2", "tier3", "enterprise"],
[TierFeature.MaintenancePage]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.DevicePosture]: ["tier2", "tier3", "enterprise"],
[TierFeature.TwoFactorEnforcement]: [
@@ -68,5 +72,7 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.NewtAutoUpdate]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.ResourcePolicies]: ["tier3", "enterprise"],
[TierFeature.RoleBasedSSHControls]: ["tier3", "enterprise"]
[TierFeature.RoleBasedSSHControls]: ["tier3", "enterprise"],
[TierFeature.CustomDnsRecords]: ["tier1", "tier2", "tier3", "enterprise"],
[TierFeature.ResourceSessions]: ["tier2", "tier3", "enterprise"]
};
@@ -417,6 +417,9 @@ export async function buildPublicResourceBlueprintConfig(
if (resource.fullDomain) {
resourceConfig["full-domain"] = resource.fullDomain;
}
if (resource.matchBaseDomain) {
resourceConfig["match-base-domain"] = true;
}
resourceConfig.ssl = resource.ssl;
} else {
if (resource.proxyPort) {
+47 -7
View File
@@ -47,6 +47,7 @@ import logger from "@server/logger";
import { defaultRoleAllowedActions } from "@server/routers/role/createRole";
import { pickPort } from "@server/routers/target/helpers";
import { and, asc, eq, isNotNull, ne } from "drizzle-orm";
import { checkWildcardDomainConflict } from "@server/lib/domainUtils";
import { tierMatrix } from "../billing/tierMatrix";
import {
isValidCIDR,
@@ -109,7 +110,8 @@ export async function updatePublicResources(
async function createTarget( // reusable function to create a target
resourceId: number,
targetData: TargetData
targetData: TargetData,
resourceName: string
) {
const targetSiteId = targetData.site;
let site: Partial<Site> | undefined;
@@ -197,10 +199,14 @@ export async function updatePublicResources(
? JSON.stringify(healthcheckData.headers)
: null;
const healthCheckName =
healthcheckData?.name ||
`Resource ${resourceName} - ${targetData.hostname}:${targetData.port}`;
const [newHealthcheck] = await trx
.insert(targetHealthCheck)
.values({
name: `${targetData.hostname}:${targetData.port}`,
name: healthCheckName,
siteId: site.siteId!,
targetId: newTarget.targetId,
orgId: orgId,
@@ -323,7 +329,8 @@ export async function updatePublicResources(
resourceData["full-domain"]!,
orgId,
resourceData.mode === "inference",
trx
trx,
resourceData["match-base-domain"]
);
await enforceDomainNamespacePaywall(
@@ -410,6 +417,8 @@ export async function updatePublicResources(
subdomain: domain ? domain.subdomain : null,
domainId: domain ? domain.domainId : null,
wildcard: domain ? domain.wildcard : false,
matchBaseDomain:
resourceData["match-base-domain"] ?? false,
enabled: resourceEnabled,
sso: resourceData.auth?.["sso-enabled"] || false,
skipToIdpId:
@@ -639,6 +648,8 @@ export async function updatePublicResources(
subdomain: domain ? domain.subdomain : null,
domainId: domain ? domain.domainId : null,
wildcard: domain ? domain.wildcard : false,
matchBaseDomain:
resourceData["match-base-domain"] ?? false,
enabled: resourceEnabled,
ssl: resourceSsl,
setHostHeader: resourceData["host-header"] || null,
@@ -898,9 +909,14 @@ export async function updatePublicResources(
? JSON.stringify(healthcheckData.headers)
: null;
const healthCheckName =
healthcheckData?.name ||
`Resource ${existingResource.name} - ${targetData.hostname}:${targetData.port}`;
const [newHealthcheck] = await trx
.update(targetHealthCheck)
.set({
name: healthCheckName,
hcEnabled: healthcheckData?.enabled || false,
hcPath: healthcheckData?.path,
hcScheme: healthcheckData?.scheme,
@@ -963,7 +979,11 @@ export async function updatePublicResources(
);
}
} else {
await createTarget(existingResource.resourceId, targetData);
await createTarget(
existingResource.resourceId,
targetData,
existingResource.name
);
}
}
@@ -1177,7 +1197,8 @@ export async function updatePublicResources(
resourceData["full-domain"]!,
orgId,
resourceData.mode === "inference",
trx
trx,
resourceData["match-base-domain"]
);
await enforceDomainNamespacePaywall(
@@ -1277,6 +1298,7 @@ export async function updatePublicResources(
subdomain: domain ? domain.subdomain : null,
domainId: domain ? domain.domainId : null,
wildcard: domain ? domain.wildcard : false,
matchBaseDomain: resourceData["match-base-domain"] ?? false,
enabled: resourceEnabled,
setHostHeader: resourceData["host-header"] || null,
forwardIdentityHeaders:
@@ -1495,7 +1517,11 @@ export async function updatePublicResources(
// If targetData is null or an empty object, we can skip it
continue;
}
await createTarget(newResource.resourceId, targetData);
await createTarget(
newResource.resourceId,
targetData,
newResource.name
);
}
await usageService.add(orgId, LimitId.PUBLIC_RESOURCES, 1, trx);
@@ -2156,6 +2182,7 @@ function checkIfHealthcheckChanged(
if (existing && !incoming) return true;
if (!existing || !incoming) return false;
if (existing.name !== incoming.name) return true;
if (existing.hcEnabled !== incoming.hcEnabled) return true;
if (existing.hcPath !== incoming.hcPath) return true;
if (existing.hcScheme !== incoming.hcScheme) return true;
@@ -2233,7 +2260,8 @@ export async function getDomain(
fullDomain: string,
orgId: string,
isInference: boolean,
trx: Transaction
trx: Transaction,
matchBaseDomain: boolean = false
) {
const [fullDomainExists] = await trx
.select({ resourceId: resources.resourceId })
@@ -2271,6 +2299,18 @@ export async function getDomain(
);
}
if (matchBaseDomain) {
const wildcardConflict = await checkWildcardDomainConflict(
fullDomain,
resourceId,
true,
trx
);
if (wildcardConflict.conflict) {
throw new Error(wildcardConflict.message);
}
}
await createCertificate(domain.domainId, fullDomain, trx);
return domain;
+18 -9
View File
@@ -182,15 +182,7 @@ export async function updateResourcePolicies(
});
}
// Sync header auth
await trx
.delete(resourcePolicyHeaderAuth)
.where(
eq(
resourcePolicyHeaderAuth.resourcePolicyId,
resourcePolicyId
)
);
// Sync header auth (hash before delete so delete+insert stay adjacent)
if (policyData["basic-auth"]) {
const basicAuth = policyData["basic-auth"];
const headerAuthHash = await hashPassword(
@@ -198,12 +190,29 @@ export async function updateResourcePolicies(
`${basicAuth.user}:${basicAuth.password}`
).toString("base64")
);
await trx
.delete(resourcePolicyHeaderAuth)
.where(
eq(
resourcePolicyHeaderAuth.resourcePolicyId,
resourcePolicyId
)
);
await trx.insert(resourcePolicyHeaderAuth).values({
resourcePolicyId,
headerAuthHash,
extendedCompatibility:
basicAuth["extended-compatibility"] ?? true
});
} else {
await trx
.delete(resourcePolicyHeaderAuth)
.where(
eq(
resourcePolicyHeaderAuth.resourcePolicyId,
resourcePolicyId
)
);
}
// Sync SSO roles
+39 -4
View File
@@ -6,7 +6,7 @@ import { MtlsSchema } from "#dynamic/lib/blueprints/MtlsSchema";
import { resourceModeSupportsMtls } from "@server/lib/mtlsModes";
import { isValidRegionId } from "@server/db/regions";
import { isValidHttpMethodList } from "@server/lib/validators";
import { wildcardSubdomainSchema } from "@server/lib/schemas";
import { routePathSchema, wildcardSubdomainSchema } from "@server/lib/schemas";
import config from "@server/lib/config";
import {
aiBudgetEnforcementSchema,
@@ -59,6 +59,7 @@ const healthCheckHostnameSchema = z
);
export const TargetHealthCheckSchema = z.object({
name: z.string().optional(),
hostname: healthCheckHostnameSchema,
port: z.int().min(1).max(65535),
enabled: z.boolean().optional().default(true),
@@ -90,7 +91,7 @@ export const TargetSchema = z.object({
port: z.int().min(1).max(65535),
enabled: z.boolean().optional().default(true),
"internal-port": z.int().min(1).max(65535).optional(),
path: z.string().optional(),
path: routePathSchema.optional(),
"path-match": z.enum(["exact", "prefix", "regex"]).optional().nullable(),
healthcheck: TargetHealthCheckSchema.optional(),
rewritePath: z.string().optional(), // deprecated alias
@@ -327,6 +328,24 @@ export const AuthDaemonSchema = z
}
);
// A full-domain must be made of hostname labels ("*" is allowed as a label;
// where it may appear is checked by the resource schemas). Anything else
// would end up in a Traefik Host() rule and certificate requests verbatim.
const fullDomainSchema = z
.string()
.refine(
(val) =>
val === "" ||
val
.split(".")
.every(
(label) =>
label === "*" ||
/^[a-zA-Z0-9]([a-zA-Z0-9-]*[a-zA-Z0-9])?$/.test(label)
),
{ message: "full-domain must be a valid hostname" }
);
// Schema for individual resource
export const PublicResourceSchema = z
.object({
@@ -340,7 +359,8 @@ export const PublicResourceSchema = z
policy: z.string().optional(),
ssl: z.boolean().optional(),
scheme: z.enum(["http", "https"]).optional(),
"full-domain": z.string().optional(),
"full-domain": fullDomainSchema.optional(),
"match-base-domain": z.boolean().optional(),
"proxy-port": z.int().min(1).max(65535).optional(),
enabled: z.boolean().optional(),
targets: z.array(TargetSchema.nullable()).optional().default([]),
@@ -658,6 +678,21 @@ export const PublicResourceSchema = z
'Wildcard full-domain must have "*" as the leftmost label only, followed by at least two valid hostname labels (e.g. "*.example.com" or "*.level1.example.com"). Patterns like "*example.com" or "level2.*.example.com" are not supported.'
}
)
.refine(
(resource) => {
if (!resource["match-base-domain"]) return true;
const effectiveMode = resource.mode ?? resource.protocol;
return (
effectiveMode === "http" &&
!!resource["full-domain"]?.startsWith("*.")
);
},
{
path: ["match-base-domain"],
message:
"'match-base-domain' can only be set on 'http' resources with a wildcard 'full-domain'"
}
)
.refine(
(resource) => {
const effectiveMode = resource.mode ?? resource.protocol;
@@ -712,7 +747,7 @@ export const PrivateResourceSchema = z
"tcp-ports": portRangeStringSchema.optional().default("*"),
"udp-ports": portRangeStringSchema.optional().default("*"),
"disable-icmp": z.boolean().optional().default(false),
"full-domain": z.string().optional(),
"full-domain": fullDomainSchema.optional(),
ssl: z.boolean().optional(),
scheme: z.enum(["http", "https"]).optional().nullable(),
alias: z
+18 -4
View File
@@ -2,7 +2,10 @@ import { db, orgs } from "@server/db";
import { cleanUpOldLogs as cleanUpOldAccessLogs } from "#dynamic/lib/logAccessAudit";
import { cleanUpOldLogs as cleanUpOldActionLogs } from "#dynamic/middlewares/logActionAudit";
import { cleanUpOldLogs as cleanUpOldRequestLogs } from "@server/routers/badger/logRequestAudit";
import { cleanUpOldLogs as cleanUpOldConnectionLogs } from "#dynamic/routers/newt";
import {
cleanUpOldLogs as cleanUpOldConnectionLogs,
cleanUpOldSshSessionLogs
} from "#dynamic/routers/newt";
import { cleanUpOldLogs as cleanUpOldAiSessionLogs } from "@server/routers/aiGateway/logAiSession";
import { gt, or } from "drizzle-orm";
import { cleanUpOldFingerprintSnapshots } from "@server/routers/olm/fingerprintingUtils";
@@ -27,7 +30,9 @@ export function initLogCleanupInterval() {
settingsLogRetentionDaysConnection:
orgs.settingsLogRetentionDaysConnection,
settingsLogRetentionDaysAISessions:
orgs.settingsLogRetentionDaysAISessions
orgs.settingsLogRetentionDaysAISessions,
settingsLogRetentionDaysSsh:
orgs.settingsLogRetentionDaysSsh
})
.from(orgs)
.where(
@@ -36,7 +41,8 @@ export function initLogCleanupInterval() {
gt(orgs.settingsLogRetentionDaysAccess, 0),
gt(orgs.settingsLogRetentionDaysRequest, 0),
gt(orgs.settingsLogRetentionDaysConnection, 0),
gt(orgs.settingsLogRetentionDaysAISessions, 0)
gt(orgs.settingsLogRetentionDaysAISessions, 0),
gt(orgs.settingsLogRetentionDaysSsh, 0)
)
);
@@ -48,7 +54,8 @@ export function initLogCleanupInterval() {
settingsLogRetentionDaysAccess,
settingsLogRetentionDaysRequest,
settingsLogRetentionDaysConnection,
settingsLogRetentionDaysAISessions
settingsLogRetentionDaysAISessions,
settingsLogRetentionDaysSsh
} = org;
if (settingsLogRetentionDaysAction > 0) {
@@ -85,6 +92,13 @@ export function initLogCleanupInterval() {
settingsLogRetentionDaysAISessions
);
}
if (settingsLogRetentionDaysSsh > 0) {
await cleanUpOldSshSessionLogs(
orgId,
settingsLogRetentionDaysSsh
);
}
}
await cleanUpOldFingerprintSnapshots(365);
+39 -3
View File
@@ -1,16 +1,20 @@
import semver from "semver";
// NEWT AND OLM
// Pass the newt/olm library version (newts.version / olms.version), not
// agentVersion: the agent may be an app embedding the library (the CLI,
// Pangolin Windows, ...) whose version is unrelated to the protocol support.
export function canCompress(
clientVersion: string | null | undefined,
type: "newt" | "olm"
agent: "newt" | "olm"
): boolean {
try {
if (!clientVersion) return false;
// check if it is a valid semver
if (!semver.valid(clientVersion)) return false;
if (type === "newt") {
if (agent === "newt") {
return semver.gte(clientVersion, "1.10.3");
} else if (type === "olm") {
} else if (agent === "olm") {
return semver.gte(clientVersion, "1.4.3");
}
return false;
@@ -23,6 +27,7 @@ export function canCompress(
// 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.
// NEWT
export function supportsCertReferences(
clientVersion: string | null | undefined
): boolean {
@@ -38,6 +43,7 @@ export function supportsCertReferences(
// Whether this newt client understands newt/wg/client-identities/add, used to
// set Remote-* headers on requests through its HTTP proxy. Bump the version
// floor here to match whatever release first ships the newt-side support.
// NEWT
export function supportsClientIdentities(
clientVersion: string | null | undefined
): boolean {
@@ -49,3 +55,33 @@ export function supportsClientIdentities(
return false;
}
}
// Whether this newt client understands the v2 subnet proxy target format.
// Older clients need targets converted back to v1 before they are sent.
// NEWT
export function supportsTargetsV2(
clientVersion: string | null | undefined
): boolean {
try {
if (!clientVersion) return false;
if (!semver.valid(clientVersion)) return false;
return semver.gte(clientVersion, "1.10.3");
} catch {
return false;
}
}
// Whether this newt client understands the sync message sent when its config
// version falls behind the server's.
// NEWT
export function supportsSync(
clientVersion: string | null | undefined
): boolean {
try {
if (!clientVersion) return false;
if (!semver.valid(clientVersion)) return false;
return semver.gte(clientVersion, "1.14.0");
} catch {
return false;
}
}
+1 -1
View File
@@ -2,7 +2,7 @@ import path from "path";
import { fileURLToPath } from "url";
// This is a placeholder value replaced by the build process
export const APP_VERSION = "1.25.0";
export const APP_VERSION = "1.26.0";
export const __FILENAME = fileURLToPath(import.meta.url);
export const __DIRNAME = path.dirname(__FILENAME);
+138 -14
View File
@@ -1,8 +1,17 @@
import { db } from "@server/db";
import { domains, orgDomains, domainNamespaces, resources } from "@server/db";
import { eq, and, like, not } from "drizzle-orm";
import {
domains,
orgDomains,
domainNamespaces,
customDnsRecords,
loginPage,
resources,
Transaction
} from "@server/db";
import { eq, and, inArray, like, not, or } from "drizzle-orm";
import { subdomainSchema, wildcardSubdomainSchema } from "@server/lib/schemas";
import { fromError } from "zod-validation-error";
import { build } from "@server/build";
import config from "./config";
export type DomainValidationResult =
@@ -124,6 +133,16 @@ export async function validateAndConstructDomain(
if (domainRes.domains.type === "ns") {
if (subdomain) {
if (!isWildcard) {
const parsedSubdomain =
subdomainSchema.safeParse(subdomain);
if (!parsedSubdomain.success) {
return {
success: false,
error: fromError(parsedSubdomain.error).toString()
};
}
}
fullDomain = `${subdomain}.${domainRes.domains.baseDomain}`;
} else {
fullDomain = domainRes.domains.baseDomain;
@@ -177,13 +196,22 @@ export async function validateAndConstructDomain(
* or (if the fullDomain is itself a wildcard) whether any existing resources would
* be matched by it.
*
* A wildcard resource with matchBaseDomain also serves the host it sits under,
* so that host has to be free too. Traefik and badger resolve hosts
* independently, so two resources claiming one host could authenticate against
* one and route to the other.
*
* @param fullDomain - The fully-constructed domain to check (may contain a leading `*`)
* @param excludeResourceId - Optional resource ID to exclude from the check (for updates)
* @param matchBaseDomain - Whether the wildcard `fullDomain` also serves its base host
* @param trx - Transaction to read through, when called from inside one
* @returns An object with `conflict: true` and a human-readable `message`, or `conflict: false`
*/
export async function checkWildcardDomainConflict(
fullDomain: string,
excludeResourceId?: number
excludeResourceId?: number,
matchBaseDomain: boolean = false,
trx: Transaction | typeof db = db
): Promise<{ conflict: false } | { conflict: true; message: string }> {
const isWildcard = fullDomain.startsWith("*.");
@@ -193,21 +221,36 @@ export async function checkWildcardDomainConflict(
// Find any existing non-wildcard resource whose fullDomain ends with this suffix
// e.g. "test.example.com" or "foo.example.com"
const conflicting = await db
const conflicting = await trx
.select({
resourceId: resources.resourceId,
fullDomain: resources.fullDomain
fullDomain: resources.fullDomain,
matchBaseDomain: resources.matchBaseDomain
})
.from(resources)
.where(like(resources.fullDomain, `%${suffix}`));
const matches = conflicting.filter(
(r) =>
!r.fullDomain!.startsWith("*.") &&
r.fullDomain!.endsWith(suffix) &&
(excludeResourceId === undefined ||
r.resourceId !== excludeResourceId)
);
const matches = conflicting.filter((r) => {
if (
excludeResourceId !== undefined &&
r.resourceId === excludeResourceId
) {
return false;
}
if (!r.fullDomain!.startsWith("*.")) {
return r.fullDomain!.endsWith(suffix);
}
// "*.foo.example.com" serving "foo.example.com" collides with
// this wildcard, which matches any single label under suffix
const baseHost = r.fullDomain!.slice(2);
return (
r.matchBaseDomain &&
baseHost.endsWith(suffix) &&
!baseHost.slice(0, -suffix.length).includes(".")
);
});
if (matches.length > 0) {
return {
@@ -215,6 +258,14 @@ export async function checkWildcardDomainConflict(
message: `Wildcard domain ${fullDomain} conflicts with existing resource(s): ${matches.map((r) => r.fullDomain).join(", ")}`
};
}
if (matchBaseDomain) {
return checkBaseHostConflict(
fullDomain.slice(2),
excludeResourceId,
trx
);
}
} else {
// Specific domain — check if any existing wildcard would match it.
// e.g. fullDomain = "test.example.com"
@@ -224,13 +275,22 @@ export async function checkWildcardDomainConflict(
const suffix = fullDomain.slice(dotIndex); // ".example.com"
const wildcardPattern = `*.${fullDomain.slice(dotIndex + 1)}`; // "*.example.com"
const conflicting = await db
const conflicting = await trx
.select({
resourceId: resources.resourceId,
fullDomain: resources.fullDomain
})
.from(resources)
.where(eq(resources.fullDomain, wildcardPattern));
.where(
or(
eq(resources.fullDomain, wildcardPattern),
// "*.test.example.com" serving this host as its base
and(
eq(resources.fullDomain, `*.${fullDomain}`),
eq(resources.matchBaseDomain, true)
)
)
);
const matches = conflicting.filter(
(r) =>
@@ -249,3 +309,67 @@ export async function checkWildcardDomainConflict(
return { conflict: false };
}
// Whether a wildcard resource can take on baseHost in addition to its own
// fullDomain: the same rules as a resource being created at that host.
async function checkBaseHostConflict(
baseHost: string,
excludeResourceId: number | undefined,
trx: Transaction | typeof db
): Promise<{ conflict: false } | { conflict: true; message: string }> {
const dashboardUrl = config.getRawConfig().app.dashboard_url;
if (dashboardUrl && new URL(dashboardUrl).hostname === baseHost) {
return {
conflict: true,
message: `Base domain ${baseHost} cannot be the same as the dashboard domain`
};
}
if (build != "oss") {
const existingLoginPages = await trx
.select()
.from(loginPage)
.where(eq(loginPage.fullDomain, baseHost));
if (existingLoginPages.length > 0) {
return {
conflict: true,
message: `Login page with base domain ${baseHost} already exists`
};
}
// Pangolin DNS answers address queries for the resource ahead of
// custom records, which would stop serving these, and a delegated
// name is not ours to answer at all
const existingRecords = await trx
.select({ type: customDnsRecords.type })
.from(customDnsRecords)
.where(
and(
eq(customDnsRecords.fullDomain, baseHost),
inArray(customDnsRecords.type, ["A", "AAAA", "CNAME", "NS"])
)
);
if (existingRecords.length > 0) {
return {
conflict: true,
message: `Custom DNS records (${[...new Set(existingRecords.map((r) => r.type))].join(", ")}) already exist for base domain ${baseHost}`
};
}
}
const existingResources = await trx
.select({ resourceId: resources.resourceId })
.from(resources)
.where(eq(resources.fullDomain, baseHost));
if (existingResources.some((r) => r.resourceId !== excludeResourceId)) {
return {
conflict: true,
message: `Resource with base domain ${baseHost} already exists`
};
}
return checkWildcardDomainConflict(baseHost, excludeResourceId, false, trx);
}
@@ -0,0 +1,74 @@
import { createServer } from "node:http";
import type { AddressInfo } from "node:net";
import { assertEquals } from "@test/assert";
import { sendToExitNode } from "./exitNodeComms";
// A Gerbil that accepts the connection but never answers. Without a request
// timeout the call below never settles, so each attempt is raced against a
// watchdog to make the failure observable instead of hanging the test run.
const WATCHDOG_MS = 3000;
async function settle(
port: number,
method: "POST" | "DELETE" | "GET" | "PUT"
): Promise<string> {
const exitNode = {
exitNodeId: 1,
reachableAt: `http://127.0.0.1:${port}`
} as any;
let watchdog: NodeJS.Timeout | undefined;
const hung = new Promise<string>((resolve) => {
watchdog = setTimeout(() => resolve("HUNG"), WATCHDOG_MS);
});
const attempt = sendToExitNode(exitNode, {
localPath: "/peer",
method,
data: method === "POST" || method === "PUT" ? { a: 1 } : undefined,
timeoutMs: 200
}).then(
() => "RESOLVED",
(error) => (error as { code?: string }).code ?? "REJECTED"
);
const result = await Promise.race([attempt, hung]);
clearTimeout(watchdog);
return result;
}
async function testUnresponsiveExitNodeTimesOut() {
console.log("Running exit node timeout tests...");
const sockets = new Set<import("node:net").Socket>();
const server = createServer(() => {
// never respond
});
server.on("connection", (s) => sockets.add(s));
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const port = (server.address() as AddressInfo).port;
try {
for (const method of ["POST", "DELETE", "GET", "PUT"] as const) {
assertEquals(
await settle(port, method),
"ECONNABORTED",
`${method} to an unresponsive exit node must time out`
);
}
} finally {
sockets.forEach((s) => s.destroy());
await new Promise<void>((resolve) => server.close(() => resolve()));
}
}
async function runTests() {
try {
await testUnresponsiveExitNodeTimesOut();
console.log("All exit node comms tests passed!");
process.exit(0);
} catch (error) {
console.error("Test failed:", error);
process.exit(1);
}
}
runTests();
+13 -4
View File
@@ -3,12 +3,17 @@ import logger from "@server/logger";
import { ExitNode } from "@server/db";
import { hasExitNodeCheckedIn } from "./exitNodeCheckIn";
// axios has no request timeout by default, so a Gerbil that accepts the
// connection but never answers would leave the caller waiting forever.
export const EXIT_NODE_REQUEST_TIMEOUT_MS = 15_000;
interface ExitNodeRequest {
remoteType?: string;
localPath: string;
method?: "POST" | "DELETE" | "GET" | "PUT";
data?: any;
queryParams?: Record<string, string>;
timeoutMs?: number;
}
/**
@@ -37,6 +42,8 @@ export async function sendToExitNode(
url += `?${params.toString()}`;
}
const timeout = request.timeoutMs ?? EXIT_NODE_REQUEST_TIMEOUT_MS;
try {
let response;
@@ -45,20 +52,22 @@ export async function sendToExitNode(
response = await axios.post(url, request.data, {
headers: {
"Content-Type": "application/json"
}
},
timeout
});
break;
case "DELETE":
response = await axios.delete(url);
response = await axios.delete(url, { timeout });
break;
case "GET":
response = await axios.get(url);
response = await axios.get(url, { timeout });
break;
case "PUT":
response = await axios.put(url, request.data, {
headers: {
"Content-Type": "application/json"
}
},
timeout
});
break;
default:
@@ -0,0 +1,29 @@
export function stripOfflineAccessScope(scopes: string): string {
return scopes
.split(/\s+/)
.map((scope) => scope.trim())
.filter((scope) => scope.length > 0 && scope !== "offline_access")
.join(" ");
}
export function ensureOfflineAccessScope(scopes: string): string {
const parts = scopes
.split(/\s+/)
.map((scope) => scope.trim())
.filter((scope) => scope.length > 0);
if (!parts.includes("offline_access")) {
parts.push("offline_access");
}
return parts.join(" ");
}
/** Google uses access_type=offline (+ prompt=consent) instead of the offline_access scope. */
export function scopesForSessionRevalidation(
scopes: string,
variant?: string | null
): string {
if (variant === "google") {
return stripOfflineAccessScope(scopes);
}
return ensureOfflineAccessScope(scopes);
}
+1
View File
@@ -1,3 +1,4 @@
import "../extendZod";
import { cidrToRange, findNextAvailableCidr } from "./ip";
import { assertEquals } from "@test/assert";
+3 -5
View File
@@ -492,6 +492,9 @@ export async function getNextAvailableOrgSubnet(): Promise<{
}
}
const cidrSchema = z.union([z.cidrv4(), z.cidrv6()]);
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
export function generateRemoteSubnets(
allSiteResources: SiteResource[]
): string[] {
@@ -503,13 +506,11 @@ export function generateRemoteSubnets(
// we purposely filter out the gateway resource here because we add it manually on the client
if (sr.mode === "cidr") {
// check if its a valid CIDR using zod
const cidrSchema = z.union([z.cidrv4(), z.cidrv6()]);
const parseResult = cidrSchema.safeParse(sr.destination);
return parseResult.success;
}
if (sr.mode === "host" || sr.mode === "ssh") {
// check if its a valid IP using zod
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
const parseResult = ipSchema.safeParse(sr.destination);
return parseResult.success;
}
@@ -598,7 +599,6 @@ export function generateSubnetProxyTargets(
if (siteResource.mode == "host" || siteResource.mode == "ssh") {
let destination = siteResource.destination;
// check if this is a valid ip
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
if (ipSchema.safeParse(destination).success) {
destination = `${destination}/32`;
@@ -785,7 +785,6 @@ export async function generateSubnetProxyTargetV2(
if (siteResource.mode == "host" || siteResource.mode == "ssh") {
let destination = siteResource.destination;
// check if this is a valid ip
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
if (ipSchema.safeParse(destination).success) {
destination = `${destination}/32`;
@@ -820,7 +819,6 @@ export async function generateSubnetProxyTargetV2(
} else if (siteResource.mode == "http") {
let destination = siteResource.destination;
// check if this is a valid ip
const ipSchema = z.union([z.ipv4(), z.ipv6()]);
if (ipSchema.safeParse(destination).success) {
destination = `${destination}/32`;
}
+15
View File
@@ -110,6 +110,21 @@ export class LockManager {
};
}
/**
* Check whether any of the given locks currently exist
* @param lockKeys - Unique identifiers for the locks
* @returns Promise<boolean> - true if at least one lock is held
*/
async anyLockExists(lockKeys: string[]): Promise<boolean> {
for (const lockKey of lockKeys) {
this.clearExpiredLocalLock(lockKey);
if (localLocks.has(lockKey)) {
return true;
}
}
return false;
}
/**
* Extend the TTL of an existing lock, provided the token matches the
* acquisition currently holding it.
+15
View File
@@ -4,6 +4,7 @@ import { configFilePath1, configFilePath2 } from "./consts";
import { z } from "zod";
import stoi from "./stoi";
import { getEnvOrYaml, readEnvOrFile } from "./getEnvOrYaml";
import { getRunMode } from "./runMode";
// type-only: offlineThresholds imports config, which imports this file
import type { OfflineThresholds } from "@server/lib/offlineThresholds";
@@ -150,6 +151,12 @@ export const configSchema = z
.gt(0)
.optional()
.default(720),
idp_session_revalidation_minutes: z
.number()
.positive()
.gt(0)
.optional()
.default(5),
cors: z
.object({
origins: z.array(z.string()).optional(),
@@ -325,6 +332,10 @@ export const configSchema = z
base_endpoint: z
.string()
.optional()
// The standalone DNS server never creates exit nodes
.transform((url) =>
url === undefined && getRunMode() === "dns" ? "" : url
)
.pipe(z.string())
.transform((url) => url.toLowerCase()),
subnet_group: z.string().optional().default("100.89.137.0/18"),
@@ -516,6 +527,10 @@ export const configSchema = z
)
.refine(
(data) => {
// The standalone DNS server never uses the secret
if (getRunMode() === "dns") {
return true;
}
// If hybrid is not defined, server secret must be defined
return (
data.server?.secret !== undefined &&
+26 -16
View File
@@ -92,6 +92,24 @@ export async function hasActiveSiteResourceRebuild(
return rebuildQueue.isQueued({ type: "site-resource", id: siteResourceId });
}
/**
* Returns true if a rebuild for any of the given site resources is currently
* active or pending in the rebuild queue. Checks all of them in a couple of
* round trips instead of one per resource.
*/
export async function hasAnyActiveSiteResourceRebuild(
siteResourceIds: number[]
): Promise<boolean> {
if (siteResourceIds.length === 0) return false;
const lockKeys = siteResourceIds.map(
(id) => `rebuild-client-associations:site-resource:${id}`
);
if (await lockManager.anyLockExists(lockKeys)) return true;
return rebuildQueue.anyQueued(
siteResourceIds.map((id) => ({ type: "site-resource", id }))
);
}
/**
* Resolves once there is no active or queued rebuild for the given site resource.
* Logs a warning and resolves early if the timeout is reached.
@@ -130,14 +148,10 @@ export async function waitForSiteRebuildIdle(
eq(siteNetworks.networkId, siteResources.networkId)
)
.where(eq(siteNetworks.siteId, siteId));
let allIdle = true;
for (const { siteResourceId } of resourceRows) {
if (await hasActiveSiteResourceRebuild(siteResourceId)) {
allIdle = false;
break;
}
}
if (allIdle) return;
const busy = await hasAnyActiveSiteResourceRebuild(
resourceRows.map((row) => row.siteResourceId)
);
if (!busy) return;
await new Promise<void>((r) =>
setTimeout(r, REBUILD_IDLE_POLL_INTERVAL_MS)
);
@@ -164,14 +178,10 @@ export async function waitForClientRebuildIdle(
})
.from(clientSiteResourcesAssociationsCache)
.where(eq(clientSiteResourcesAssociationsCache.clientId, clientId));
let allIdle = true;
for (const { siteResourceId } of resourceRows) {
if (await hasActiveSiteResourceRebuild(siteResourceId)) {
allIdle = false;
break;
}
}
if (allIdle) return;
const busy = await hasAnyActiveSiteResourceRebuild(
resourceRows.map((row) => row.siteResourceId)
);
if (!busy) return;
await new Promise<void>((r) =>
setTimeout(r, REBUILD_IDLE_POLL_INTERVAL_MS)
);
+5
View File
@@ -20,6 +20,7 @@ export interface RebuildQueueManager {
enqueue(job: RebuildJob): Promise<void>;
startProcessing(handlers: RebuildJobHandlers): void;
isQueued(job: RebuildJob): Promise<boolean>;
anyQueued(jobs: RebuildJob[]): Promise<boolean>;
}
// In-process FIFO used when there is no Redis to back a distributed queue
@@ -48,6 +49,10 @@ class InMemoryRebuildQueue implements RebuildQueueManager {
return this.queuedSet.has(dedupeKey(job));
}
async anyQueued(jobs: RebuildJob[]): Promise<boolean> {
return jobs.some((job) => this.queuedSet.has(dedupeKey(job)));
}
async enqueue(job: RebuildJob): Promise<void> {
const key = dedupeKey(job);
if (this.queuedSet.has(key)) {
+8
View File
@@ -0,0 +1,8 @@
// Revoking resource sessions is an enterprise feature, so nothing can ever
// require a reauth in the open source build.
export async function isResourceReauthRequired(
userSessionId: string,
resourceId: number
): Promise<boolean> {
return false;
}
+14
View File
@@ -0,0 +1,14 @@
// Which entry point is running. Standalone entry points set this before
// anything loads the config, so the config schema can skip requirements
// that only apply to the full Pangolin server.
export type RunMode = "server" | "dns";
let runMode: RunMode = "server";
export function setRunMode(mode: RunMode) {
runMode = mode;
}
export function getRunMode(): RunMode {
return runMode;
}
+11
View File
@@ -62,3 +62,14 @@ export const privateNamespaceSubdomainSchema = z
.min(1, "Namespace subdomain must be at least 1 character long")
.max(32, "Namespace subdomain must be at most 32 characters long")
.transform((val) => val.toLowerCase());
/**
* A path matched by a Traefik router (target path, redirect match path).
* Backticks and control characters are never valid in a URL path and are
* rejected so the value can't break out of its literal in the router rule.
*/
export const routePathSchema = z
.string()
.refine((val) => !/[`\u0000-\u001f\u007f]/.test(val), {
message: "Path cannot contain backticks or control characters"
});
+58
View File
@@ -0,0 +1,58 @@
import { computeBuckets } from "./statusHistory";
import { assertEquals } from "@test/assert";
function event(id: number, timestamp: number, status: string) {
return {
entityType: "resource",
entityId: 1,
orgId: "org",
status,
timestamp,
id
};
}
// Regression test for https://github.com/fosrl/pangolin/issues/3994
function testSameSecondEventsUseInsertionOrder() {
console.log("Running computeBuckets same-second ordering tests...");
// Two days ago the resource went unhealthy and recovered within the same
// second. The database may return the two rows in either order.
const nowSec = Math.floor(Date.now() / 1000);
const flap = nowSec - 2 * 86400;
const unhealthy = event(1, flap, "unhealthy");
const healthy = event(2, flap, "healthy");
for (const events of [
[unhealthy, healthy],
[healthy, unhealthy]
]) {
const { buckets, totalDowntime } = computeBuckets(
events,
5,
"healthy",
0
);
const order = events.map((e) => e.id).join(",");
assertEquals(
totalDowntime,
0,
`No downtime when the later event is healthy (rows ${order})`
);
assertEquals(
buckets.at(-1)!.status,
"good",
`Today should be good (rows ${order})`
);
}
console.log("All computeBuckets same-second ordering tests passed!");
}
try {
testSameSecondEventsUseInsertionOrder();
console.log("All tests passed successfully!");
} catch (error) {
console.error("Test failed:", error);
process.exit(1);
}
+12 -4
View File
@@ -56,7 +56,7 @@ export async function getCachedStatusHistory(
gte(statusHistory.timestamp, startSec)
)
)
.orderBy(asc(statusHistory.timestamp));
.orderBy(asc(statusHistory.timestamp), asc(statusHistory.id));
// Fetch the last known state before the window so that entities that
// haven't changed status recently still show the correct status rather
@@ -71,7 +71,7 @@ export async function getCachedStatusHistory(
lt(statusHistory.timestamp, startSec)
)
)
.orderBy(desc(statusHistory.timestamp))
.orderBy(desc(statusHistory.timestamp), desc(statusHistory.id))
.limit(1);
const priorStatus = lastKnownEvent?.status ?? null;
@@ -168,6 +168,14 @@ export function computeBuckets(
// don't drift as the cache expires and is recomputed
const todayMidnightSec = localMidnightSec(tzOffsetMinutes);
// Several status changes can land in the same second (e.g. a resource
// going degraded -> healthy as each target reports in), and the database
// does not keep a stable order for rows with equal timestamps. Order by id
// as well so the last event of the second is the one that was written last.
events = [...events].sort(
(a, b) => a.timestamp - b.timestamp || a.id - b.id
);
const buckets: StatusHistoryDayBucket[] = [];
let totalDowntime = 0;
@@ -328,7 +336,7 @@ export async function getBatchedStatusHistory(
gte(statusHistory.timestamp, startSec)
)
)
.orderBy(asc(statusHistory.timestamp));
.orderBy(asc(statusHistory.timestamp), asc(statusHistory.id));
// Fetch the last known state before the window so that entities that
// haven't changed status recently still show the correct status rather
@@ -349,7 +357,7 @@ export async function getBatchedStatusHistory(
status: statusHistory.status,
timestamp: statusHistory.timestamp,
row_number:
sql<number>`ROW_NUMBER() OVER (PARTITION BY ${statusHistory.entityId} ORDER BY ${statusHistory.timestamp} DESC)`.as(
sql<number>`ROW_NUMBER() OVER (PARTITION BY ${statusHistory.entityId} ORDER BY ${statusHistory.timestamp} DESC, ${statusHistory.id} DESC)`.as(
"row_number"
)
})
+5 -22
View File
@@ -16,6 +16,7 @@ import { getValidCertificatesForDomains } from "@server/lib/certificates";
import { sendToExitNode } from "#dynamic/lib/exitNodes";
import { build } from "@server/build";
import license from "#dynamic/license/license";
import { domainsFromHostRule } from "./rule";
// Flat files in the certificates dir; the per-domain directory scan and
// cleanup only look at directories, so these don't collide with them.
@@ -582,28 +583,10 @@ export class TraefikConfigManager {
traefikConfig.http.routers
)) {
if (router.rule && typeof router.rule === "string") {
// Match Host(`domain`)
const match = router.rule.match(/Host\(`([^`]+)`\)/);
if (match && match[1]) {
domains.add(match[1]);
}
// Match HostRegexp(`^[^.]+\.parent.domain$`) generated for wildcard resources
const hostRegexpMatch = router.rule.match(
/HostRegexp\(`([^`]+)`\)/
);
if (hostRegexpMatch && hostRegexpMatch[1]) {
const innerRegex = hostRegexpMatch[1];
// Pattern is always ^[^.]+\.PARENT_DOMAIN$ where dots are escaped as \.
const domainMatch = innerRegex.match(
/^\^\[\^\.\]\+\\\.(.+)\$$/
);
if (domainMatch && domainMatch[1]) {
const parentDomain = domainMatch[1].replace(
/\\\./g,
"."
);
domains.add(`*.${parentDomain}`);
}
for (const domain of domainsFromHostRule(
router.rule
)) {
domains.add(domain);
}
}
}
+2 -1
View File
@@ -1,5 +1,6 @@
import config from "@server/lib/config";
import { applyMtlsConfig } from "./mtls";
import { buildHostRule } from "./rule";
import { sanitize } from "./utils";
export type BrowserGatewayResourceRow = {
@@ -174,7 +175,7 @@ export function buildBrowserGatewayConfig(params: {
...additionalMiddlewares
];
const hostRule = `Host(\`${fullDomain}\`)`;
const hostRule = buildHostRule(fullDomain);
// Build TLS config
let tls = resolveTls({
+21 -4
View File
@@ -11,9 +11,18 @@ export function buildWildcardTls(params: {
hasSubdomain: boolean;
domainCertResolver?: string | null;
preferWildcardCert?: boolean | null;
}): { certResolver: string | undefined; domains?: { main: string }[] } {
const { fullDomain, hasSubdomain, domainCertResolver, preferWildcardCert } =
params;
matchBaseDomain?: boolean | null;
}): {
certResolver: string | undefined;
domains?: { main: string; sans?: string[] }[];
} {
const {
fullDomain,
hasSubdomain,
domainCertResolver,
preferWildcardCert,
matchBaseDomain
} = params;
const domainParts = fullDomain.split(".");
let wildCard =
@@ -37,8 +46,16 @@ export function buildWildcardTls(params: {
? preferWildcardCert
: globalDefaultPreferWildcard;
// A wildcard cert does not cover the name it sits under
const baseDomainSans =
matchBaseDomain && fullDomain.startsWith("*.")
? { sans: [fullDomain.slice(2)] }
: {};
return {
certResolver: resolverName,
...(preferWildcard ? { domains: [{ main: wildCard }] } : {})
...(preferWildcard
? { domains: [{ main: wildCard, ...baseDomainSans }] }
: {})
};
}
+7 -2
View File
@@ -50,7 +50,12 @@ import {
import { applyPathRewriteMiddleware } from "./middleware";
import { applyMtlsConfig, getMtlsCaCertificatesByResource } from "./mtls";
import { buildRedirectConfig, RedirectRouteRow } from "./redirect";
import { appendPathMatch, buildHostRule, computeRoutePriority } from "./rule";
import {
appendPathMatch,
buildHostRule,
computeRoutePriority,
ruleValue
} from "./rule";
import { buildSiteResourceAliasCertPlaceholders } from "./siteResourceAlias";
import { TargetWithSite } from "./types";
import { encodePath, sanitize, validatePathRewriteConfig } from "./utils";
@@ -857,7 +862,7 @@ export async function getTraefikConfig(
const srKey = `inference-sr${sr.siteResourceId}`;
const routerName = `${srKey}-router`;
const serviceName = `${srKey}-service`;
const rule = `Host(\`${fullDomain}\`) && ClientIP(\`${exitNode.address}\`)`; // restrict to coming from the exit node ip range that the client is connected to
const rule = `${buildHostRule(fullDomain)} && ClientIP(${ruleValue(exitNode.address)})`; // restrict to coming from the exit node ip range that the client is connected to
// siteResource aliases don't have a per-domain cert resolver
// stored, so always fall back to the global defaults.
+6 -1
View File
@@ -18,6 +18,7 @@ export type RedirectRouteRow = {
enabled: boolean;
name: string;
wildcard: boolean | null;
matchBaseDomain?: boolean | null;
ssl: boolean;
matchPath: string | null;
pathMatchType: string;
@@ -115,7 +116,11 @@ export function buildRedirectConfig(params: {
}
const rule = appendPathMatch(
buildHostRule(redirect.fullDomain, redirect.wildcard),
buildHostRule(
redirect.fullDomain,
redirect.wildcard,
redirect.matchBaseDomain
),
redirect.matchPath,
redirect.pathMatchType
);
+148 -1
View File
@@ -1,8 +1,13 @@
import { assertEquals } from "../../../test/assert";
import {
appendPathMatch,
buildHostRule,
computeRedirectPriority,
computeRoutePriority,
matchesPath
domainsFromHostRule,
matchesPath,
pickResourceForHost,
ruleValue
} from "./rule";
function runTests() {
@@ -128,6 +133,148 @@ function runTests() {
);
console.log("All priority tests passed!");
console.log("Running rule quoting tests...");
// Ordinary values keep the backtick form
assertEquals(ruleValue("/api"), "`/api`", "plain value");
assertEquals(
buildHostRule("app.example.com"),
"Host(`app.example.com`)",
"plain host"
);
assertEquals(
buildHostRule("*.example.com", true),
"HostRegexp(`^[^.]+\\.example\\.com$`)",
"wildcard host"
);
assertEquals(
buildHostRule("*.example.com", true, false),
"HostRegexp(`^[^.]+\\.example\\.com$`)",
"wildcard host without its base"
);
assertEquals(
buildHostRule("*.apps.example.com", true, true),
"HostRegexp(`^([^.]+\\.)?apps\\.example\\.com$`)",
"wildcard host with its base"
);
assertEquals(
buildHostRule("app.example.com", false, true),
"Host(`app.example.com`)",
"matchBaseDomain is ignored without a wildcard"
);
const withBase = new RegExp("^([^.]+\\.)?apps\\.example\\.com$");
assertEquals(withBase.test("apps.example.com"), true, "matches the base");
assertEquals(withBase.test("a.apps.example.com"), true, "matches a label");
assertEquals(
withBase.test("a.b.apps.example.com"),
false,
"one label only"
);
assertEquals(withBase.test("xapps.example.com"), false, "no suffix match");
// The path clause has to bind to both hosts, which an || would not
assertEquals(
appendPathMatch(
buildHostRule("*.example.com", true, true),
"/x",
"exact"
),
"HostRegexp(`^([^.]+\\.)?example\\.com$`) && Path(`/x`)",
"path applies to the wildcard and its base"
);
// The cert domain is recovered from every rule buildHostRule emits
const certDomains = (
fullDomain: string,
wildcard?: boolean,
matchBaseDomain?: boolean
) =>
domainsFromHostRule(
appendPathMatch(
buildHostRule(fullDomain, wildcard, matchBaseDomain),
"/x",
"prefix"
)
).join(",");
assertEquals(
certDomains("app.example.com"),
"app.example.com",
"cert domain for a plain host"
);
assertEquals(
certDomains("*.example.com", true),
"*.example.com",
"cert domain for a wildcard"
);
assertEquals(
certDomains("*.test.example.com", true, true),
"*.test.example.com",
"cert domain for a wildcard with its base"
);
assertEquals(
domainsFromHostRule("HostRegexp(`^api-.+\\.example\\.com$`)").join(","),
"",
"foreign HostRegexp yields nothing"
);
const pick = (host: string, rows: string[]) =>
pickResourceForHost(rows, host, (r) => r);
assertEquals(
pick("a.example.com", ["*.example.com", "a.example.com"]),
"a.example.com",
"exact beats wildcard"
);
assertEquals(
pick("apps.example.com", ["*.example.com", "*.apps.example.com"]),
"*.apps.example.com",
"base of the narrower wildcard beats the broader wildcard"
);
assertEquals(
pick("a.b.example.com", ["*.example.com", "*.b.example.com"]),
"*.b.example.com",
"most specific wildcard wins"
);
assertEquals(pick("a.example.com", []), undefined, "no match");
assertEquals(
appendPathMatch("Host(`a.com`)", "/x", "exact"),
"Host(`a.com`) && Path(`/x`)",
"plain exact path"
);
assertEquals(
appendPathMatch("Host(`a.com`)", "^/v[0-9]+/", "regex"),
"Host(`a.com`) && PathRegexp(`^/v[0-9]+/`)",
"plain regex path"
);
// A backtick must not be able to close the literal and inject matchers
const injectedPath = "/a`) || Host(`victim.example.com`) && PathPrefix(`/";
assertEquals(
appendPathMatch("Host(`attacker.example.com`)", injectedPath, "exact"),
'Host(`attacker.example.com`) && Path("/a`) || Host(`victim.example.com`) && PathPrefix(`/")',
"injected exact path stays one literal"
);
assertEquals(
appendPathMatch("Host(`a.com`)", "`) || HostRegexp(`.+", "regex"),
'Host(`a.com`) && PathRegexp("`) || HostRegexp(`.+")',
"injected regex path stays one literal"
);
assertEquals(
buildHostRule("x`) || Host(`victim.example.com"),
'Host("x`) || Host(`victim.example.com")',
"injected host stays one literal"
);
// Double-quoted form escapes quotes, backslashes and control characters
assertEquals(
ruleValue('`"\\\n'),
'"`\\"\\\\\\n"',
"escapes in quoted form"
);
console.log("All rule quoting tests passed!");
}
try {
+79 -7
View File
@@ -1,15 +1,87 @@
// Characters that cannot appear inside a Go raw (backtick) string literal:
// the backtick itself would terminate it, and control characters are
// rejected by the scanner Traefik uses to parse rules.
const UNSAFE_RAW_RULE_VALUE = /[`\u0000-\u001f\u007f]/;
/**
* Quote a value for use as a matcher argument in a Traefik rule.
*
* Every value interpolated into a rule must go through this. Rules are
* parsed as Go expressions, so a value containing a backtick could otherwise
* close its literal and inject arbitrary matchers (e.g. a Host() clause for
* another org's domain). Ordinary values keep the conventional backtick form;
* anything else is emitted as a double-quoted Go string literal, which
* Traefik unquotes back to exactly the original value.
*/
export function ruleValue(value: string): string {
if (!UNSAFE_RAW_RULE_VALUE.test(value)) {
return `\`${value}\``;
}
// JSON string escapes are a subset of Go's interpreted string escapes.
// Lone surrogates are replaced since Go rejects them in \u escapes.
return JSON.stringify(value.toWellFormed());
}
// Build the Host()/HostRegexp() Traefik rule for a resource's domain.
// Wildcard resources match any single subdomain via HostRegexp.
// Wildcard resources match any single subdomain via HostRegexp, and the host
// the wildcard sits under too when matchBaseDomain is set. That stays a single
// matcher rather than an || so appended && clauses apply to both hosts.
export function buildHostRule(
fullDomain: string,
wildcard?: boolean | null
wildcard?: boolean | null,
matchBaseDomain?: boolean | null
): string {
if (wildcard && fullDomain.startsWith("*.")) {
// Convert *.foo.bar.com -> HostRegexp(`^[^.]+\.foo\.bar\.com$`)
const escaped = fullDomain.slice(2).replace(/\./g, "\\.");
return `HostRegexp(\`^[^.]+\\.${escaped}$\`)`;
const label = matchBaseDomain ? "([^.]+\\.)?" : "[^.]+\\.";
return `HostRegexp(${ruleValue(`^${label}${escaped}$`)})`;
}
return `Host(\`${fullDomain}\`)`;
return `Host(${ruleValue(fullDomain)})`;
}
// The two HostRegexp patterns buildHostRule emits, capturing the escaped
// domain the wildcard sits under.
const WILDCARD_HOST_PATTERN = /^\^(?:\[\^\.\]\+\\\.|\(\[\^\.\]\+\\\.\)\?)(.+)\$$/;
/**
* Inverse of buildHostRule for TLS purposes: the domains a router rule needs
* a certificate for. A wildcard rule maps back to `*.parent`; when it also
* matches the base host, no separate entry is needed because the wildcard
* certificate carries the base name as a SAN.
*/
export function domainsFromHostRule(rule: string): string[] {
const domains: string[] = [];
const host = rule.match(/Host\(`([^`]+)`\)/);
if (host?.[1]) {
domains.push(host[1]);
}
const hostRegexp = rule.match(/HostRegexp\(`([^`]+)`\)/);
const parent = hostRegexp?.[1]?.match(WILDCARD_HOST_PATTERN)?.[1];
if (parent) {
domains.push(`*.${parent.replace(/\\\./g, ".")}`);
}
return domains;
}
/**
* Pick the resource serving `host` from the rows that matched it. An exact
* fullDomain wins, then the most specific wildcard, which is what Traefik
* routes to when a broader wildcard also matches.
*/
export function pickResourceForHost<T>(
rows: T[],
host: string,
getFullDomain: (row: T) => string | null | undefined
): T | undefined {
const length = (row: T) => getFullDomain(row)?.length ?? 0;
return (
rows.find((row) => getFullDomain(row) === host) ??
[...rows].sort((a, b) => length(b) - length(a))[0]
);
}
// Append a path-matching clause to a Traefik rule based on the resource's
@@ -27,11 +99,11 @@ export function appendPathMatch(
}
if (pathMatchType === "exact") {
return `${rule} && Path(\`${p}\`)`;
return `${rule} && Path(${ruleValue(p)})`;
} else if (pathMatchType === "prefix") {
return `${rule} && PathPrefix(\`${p}\`)`;
return `${rule} && PathPrefix(${ruleValue(p)})`;
} else if (pathMatchType === "regex") {
return `${rule} && PathRegexp(\`${path}\`)`; // this is the raw path because it's a regex
return `${rule} && PathRegexp(${ruleValue(path)})`; // this is the raw path because it's a regex
}
return rule;
}
+4 -3
View File
@@ -1,4 +1,5 @@
import config from "@server/lib/config";
import { buildHostRule } from "./rule";
export type SiteResourceAliasRow = {
siteResourceId: number;
@@ -82,7 +83,7 @@ export function buildSiteResourceAliasCertPlaceholders(params: {
entryPoints: [config.getRawConfig().traefik.http_entrypoint],
middlewares: [redirectHttpsMiddlewareName],
service: siteResourceServiceName,
rule: `Host(\`${fullDomain}\`)`,
rule: buildHostRule(fullDomain),
priority: 100
};
@@ -97,7 +98,7 @@ export function buildSiteResourceAliasCertPlaceholders(params: {
entryPoints: [config.getRawConfig().traefik.https_entrypoint],
service: siteResourceServiceName,
middlewares: [siteResourceRewriteMiddlewareName],
rule: `Host(\`${fullDomain}\`)`,
rule: buildHostRule(fullDomain),
priority: 100,
tls
};
@@ -106,7 +107,7 @@ export function buildSiteResourceAliasCertPlaceholders(params: {
config_output.http.routers[`${siteResourceRouterName}-assets`] = {
entryPoints: [config.getRawConfig().traefik.https_entrypoint],
service: siteResourceServiceName,
rule: `Host(\`${fullDomain}\`) && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
rule: `${buildHostRule(fullDomain)} && (PathPrefix(\`/_next\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
priority: 101,
tls
};
+1
View File
@@ -41,6 +41,7 @@ export type ResourceWithTargets = Pick<
| "responseHeaders"
| "proxyProtocol"
| "wildcard"
| "matchBaseDomain"
| "mode"
| "maintenanceModeEnabled"
| "maintenanceModeType"
+1 -3
View File
@@ -2,7 +2,6 @@ import next from "next";
import express from "express";
import fs from "fs";
import path from "path";
import { parse } from "url";
import logger from "@server/logger";
import config from "@server/lib/config";
import { stripDuplicateSesions } from "./middlewares/stripDuplicateSessions";
@@ -43,8 +42,7 @@ export async function createNextServer() {
nextServer.use(stripDuplicateSesions);
nextServer.all("/{*splat}", (req, res) => {
const parsedUrl = parse(req.url!, true);
return handle(req, res, parsedUrl);
return handle(req, res);
});
nextServer.listen(nextPort, (err?: any) => {
+1
View File
@@ -16,6 +16,7 @@ export enum OpenAPITags {
Rule = "Rule",
Invitation = "User Invitation",
AccessToken = "Access Token",
ResourceSession = "Resource Session",
GlobalIdp = "Identity Provider (Global)",
OrgIdp = "Identity Provider (Organization Only)",
ApiKey = "API Key",
+4 -2
View File
@@ -16,13 +16,15 @@ import { privateConfig } from "#private/lib/config";
let dnsServer: AuthoritativeDNSServer | undefined;
export async function startDnsServer() {
export async function startDnsServer(
options: { requireLicense?: boolean } = {}
) {
const dnsConfig = privateConfig.getRawPrivateConfig().dns;
if (!dnsConfig || !dnsConfig.enabled) {
return;
}
dnsServer = new AuthoritativeDNSServer(dnsConfig.listen_port);
dnsServer = new AuthoritativeDNSServer(dnsConfig.listen_port, options);
await dnsServer.start();
}
+18
View File
@@ -0,0 +1,18 @@
/*
* 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.
*/
// Imported first by dnsServer.ts: modules evaluate in import order, so this
// runs before any later import loads (and validates) the config.
import { setRunMode } from "@server/lib/runMode";
setRunMode("dns");
+73
View File
@@ -0,0 +1,73 @@
/*
* 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.
*/
// Standalone entry point that runs only the authoritative DNS server against
// an existing Pangolin database, without the API, dashboard or schedulers.
// Migrations are left to the main Pangolin server that owns the database.
// Must stay the first import - see dnsRunMode.ts
import "./dnsRunMode";
import "@server/extendZod";
import http from "http";
import logger from "@server/logger";
import { privateConfig } from "#private/lib/config";
import { rateLimitService } from "#private/lib/rateLimit";
import { startDnsServer, stopDnsServer } from "./dns";
// The DNS server only speaks UDP, which orchestrators can't probe directly
const HEALTHCHECK_PORT = 8053;
async function start() {
if (!privateConfig.getRawPrivateConfig().dns?.enabled) {
throw new Error(
"DNS server is not enabled - set dns.enabled: true in the private config"
);
}
await startDnsServer({ requireLicense: false });
const healthcheckServer = http.createServer((req, res) => {
if (req.url === "/healthz") {
res.writeHead(200, { "Content-Type": "text/plain" });
res.end("OK");
} else {
res.writeHead(404);
res.end();
}
});
healthcheckServer.listen(HEALTHCHECK_PORT, () => {
logger.info(`Healthcheck server listening on port ${HEALTHCHECK_PORT}`);
});
let stopping = false;
const shutdown = async () => {
if (stopping) return;
stopping = true;
try {
healthcheckServer.close();
await stopDnsServer();
await rateLimitService.cleanup();
} catch (error) {
logger.error("Error during DNS server shutdown:", error);
}
process.exit(0);
};
process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
}
start().catch((error) => {
logger.error("Failed to start DNS server:", error);
process.exit(1);
});
@@ -0,0 +1,185 @@
/*
* 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 { matchCustomRecords } from "./customRecordMatch";
import { assertEquals } from "@test/assert";
const zone = "example.com";
function record(fullDomain: string, type = "A") {
return {
fullDomain,
type,
values: ["192.0.2.1"],
ttl: 300,
priority: null,
weight: null,
port: null
};
}
const zoneRecords = [
record("example.com"),
record("example.com", "MX"),
record("coder.example.com"),
record("*.coder.example.com"),
record("fleet.example.com", "NS"),
record("_imaps._tcp.example.com", "SRV"),
record("_submission._tcp.example.com", "SRV"),
record("app.nested.coder.example.com"),
record("*.svc.example.com"),
record("*.x.svc.example.com", "TXT")
];
function names(name: string) {
return matchCustomRecords(zoneRecords, name, zone)
.records.map((r) => `${r.fullDomain}/${r.type}`)
.sort()
.join(",");
}
function exists(name: string) {
return matchCustomRecords(zoneRecords, name, zone).nameExists;
}
function testExactMatches() {
console.log("Running exact match tests...");
assertEquals(
names("example.com"),
"example.com/A,example.com/MX",
"apex returns all its records"
);
assertEquals(
names("coder.example.com"),
"coder.example.com/A",
"an exact name is never answered by its child wildcard"
);
assertEquals(
names("Coder.Example.Com."),
"coder.example.com/A",
"names are matched case-insensitively and without a trailing dot"
);
assertEquals(
names("*.coder.example.com"),
"*.coder.example.com/A",
"the wildcard owner name itself matches literally"
);
}
function testWildcards() {
console.log("Running wildcard tests...");
assertEquals(
names("foo.coder.example.com"),
"*.coder.example.com/A",
"single-label name below a wildcard"
);
assertEquals(
names("bar.baz.coder.example.com"),
"*.coder.example.com/A",
"multi-label name below a wildcard (RFC 4592)"
);
assertEquals(
names("a.b.c.coder.example.com"),
"*.coder.example.com/A",
"deeply nested name below a wildcard"
);
assertEquals(
names("other.nested.coder.example.com"),
"",
"an existing intermediate name blocks the wildcard above it"
);
assertEquals(
exists("other.nested.coder.example.com"),
false,
"a blocked name is NXDOMAIN"
);
assertEquals(
names("a.b.x.svc.example.com"),
"*.x.svc.example.com/TXT",
"the closest wildcard wins over one further up"
);
assertEquals(
names("q.svc.example.com"),
"*.svc.example.com/A",
"a sibling of a nested wildcard uses the outer one"
);
assertEquals(
names("unknown.example.com"),
"",
"no wildcard at the apex means nothing matches"
);
assertEquals(
exists("unknown.example.com"),
false,
"an unknown name is NXDOMAIN"
);
}
function testEmptyNonTerminals() {
console.log("Running empty non-terminal tests...");
assertEquals(
names("_tcp.example.com"),
"",
"an empty non-terminal has no records"
);
assertEquals(
exists("_tcp.example.com"),
true,
"an empty non-terminal exists (NODATA, not NXDOMAIN)"
);
assertEquals(
names("nested.coder.example.com"),
"",
"an empty non-terminal is not answered by the wildcard above it"
);
assertEquals(
exists("nested.coder.example.com"),
true,
"an empty non-terminal under a wildcard still exists"
);
assertEquals(
exists("svc.example.com"),
true,
"a wildcard's parent exists"
);
assertEquals(
exists("x.svc.example.com"),
true,
"a nested wildcard's parent exists"
);
}
function testOutsideZone() {
console.log("Running out-of-zone tests...");
assertEquals(
names("example.org"),
"",
"names outside the zone never match"
);
assertEquals(
names("notexample.com"),
"",
"a name that merely ends with the zone string does not match"
);
assertEquals(exists("com"), false, "the zone's parent does not exist");
}
testExactMatches();
testWildcards();
testEmptyNonTerminals();
testOutsideZone();
console.log("All custom record match tests passed");
@@ -0,0 +1,76 @@
/*
* 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.
*/
export type CustomZoneRecord = {
fullDomain: string;
type: string;
values: string[];
ttl: number;
priority: number | null;
weight: number | null;
port: number | null;
};
export type CustomRecordMatch<T> = {
// The records that answer for the name: its own, or those of the
// wildcard that covers it
records: T[];
// Whether the name exists in the zone's custom records, including as an
// empty non-terminal (e.g. _tcp.example.com when only
// _imaps._tcp.example.com has records) - for NODATA vs NXDOMAIN
nameExists: boolean;
};
// Picks which of a zone's custom records answer for `name`, following
// RFC 4592: a name with records of its own (or with records beneath it)
// is never answered by a wildcard. Otherwise the closest existing ancestor
// (the "closest encloser") is found and its wildcard, if any, answers -
// so *.example.com covers a.b.example.com unless b.example.com exists.
// Only custom record names are considered when deciding what exists.
export function matchCustomRecords<T extends { fullDomain: string }>(
zoneRecords: T[],
name: string,
zone: string
): CustomRecordMatch<T> {
const normalized = name.replace(/\.$/, "").toLowerCase();
if (normalized !== zone && !normalized.endsWith(`.${zone}`)) {
return { records: [], nameExists: false };
}
const existsAtOrBelow = (candidate: string) =>
zoneRecords.some(
(r) =>
r.fullDomain === candidate ||
r.fullDomain.endsWith(`.${candidate}`)
);
const exact = zoneRecords.filter((r) => r.fullDomain === normalized);
if (exact.length > 0) {
return { records: exact, nameExists: true };
}
if (normalized === zone || existsAtOrBelow(normalized)) {
return { records: [], nameExists: true };
}
// Walk up to the closest encloser; the zone apex always exists, so the
// walk never leaves the zone
let encloser = normalized;
do {
encloser = encloser.slice(encloser.indexOf(".") + 1);
} while (encloser !== zone && !existsAtOrBelow(encloser));
const wildcard = zoneRecords.filter(
(r) => r.fullDomain === `*.${encloser}`
);
return { records: wildcard, nameExists: wildcard.length > 0 };
}
+384 -31
View File
@@ -30,14 +30,20 @@ import {
sites,
targets,
dnsChallenge,
domains
domains,
customDnsRecords
} from "@server/db";
import { privateConfig as config } from "#private/lib/config";
import * as dnsResolver from "dns";
import logger from "@server/logger";
import { listExitNodes } from "../exitNodes";
import { listExitNodesAcrossRegions } from "../exitNodes/exitNodes";
import { parseCustomDnsRecordValues } from "@server/routers/customDnsRecord/types";
import {
matchCustomRecords,
type CustomRecordMatch,
type CustomZoneRecord
} from "./customRecordMatch";
import { rateLimitService } from "../rateLimit";
import license from "#private/license/license";
type DNSRecord = {
id: number;
@@ -50,7 +56,18 @@ type DNSRecord = {
enabled: boolean;
};
type CustomRecord = {
type: string;
value: string;
ttl: number;
priority: number | null;
weight: number | null;
port: number | null;
};
const DNS_CACHE_TTL_SECONDS = 300;
// Kept short so edits to custom records propagate quickly
const CUSTOM_RECORD_CACHE_TTL_SECONDS = 60;
export class AuthoritativeDNSServer {
private cache: LocalCache = createLocalCache(10_000, DNS_CACHE_TTL_SECONDS);
@@ -71,6 +88,7 @@ export class AuthoritativeDNSServer {
// set refresh and read from memory here.
private isLicensed: boolean = false;
private licenseRefreshInterval: NodeJS.Timeout | null = null;
private requireLicense: boolean;
// Cache for per-queryName zone resolution and SOA records
private authoritativeDomainCache: LocalCache = createLocalCache(
@@ -82,8 +100,9 @@ export class AuthoritativeDNSServer {
DNS_CACHE_TTL_SECONDS
);
constructor(port: number) {
constructor(port: number, options: { requireLicense?: boolean } = {}) {
this.port = port;
this.requireLicense = options.requireLicense ?? true;
this.server = dgram.createSocket("udp4");
this.setupServer();
@@ -162,6 +181,10 @@ export class AuthoritativeDNSServer {
const originalQueryName = queryName;
const queryType = question.type;
let baseDomain: string | null;
// True when baseDomain is the customer zone the queried name itself
// lives in (not a synthetic cname-extension lookup), i.e. when
// user-defined custom records may apply.
let isCustomerZone = false;
logger.info(
`DNS Query: ${queryName} ${queryType} from ${rinfo.address}:${rinfo.port}`
@@ -284,6 +307,34 @@ export class AuthoritativeDNSServer {
this.sendResponse(packet, [], rinfo, false, 3, []); // NXDOMAIN rcode=3 - not authoritative
return;
}
isCustomerZone = true;
}
// A subdomain the user delegated elsewhere with NS records is no
// longer ours to answer for - refer the resolver to its nameservers
// for the cut and everything beneath it, ahead of any other handling.
if (isCustomerZone) {
const delegation = await this.findDelegation(queryName, baseDomain);
if (delegation) {
logger.info(
`Referring ${originalQueryName} (${queryType}) to delegated zone ${delegation.cut}`
);
const [authorities, additionals] = await this.buildReferral(
delegation,
baseDomain
);
// Referrals are non-authoritative (AA=0) with no answers
this.sendResponse(
packet,
[],
rinfo,
false,
0,
authorities,
additionals
);
return;
}
}
// Special handling for _acme-challenge CNAME requests
@@ -452,11 +503,43 @@ export class AuthoritativeDNSServer {
return;
}
// User-defined records come after everything Pangolin manages itself,
// so they can never shadow a resource, login page or redirect.
const customMatch: CustomRecordMatch<CustomRecord> = isCustomerZone
? await this.getCustomRecords(queryName, baseDomain!)
: { records: [], nameExists: false };
const customRecords = customMatch.records;
if (customRecords.length > 0) {
let matchingRecords = customRecords.filter(
(record) => record.type === queryType
);
// A CNAME answers queries for every other type at its name
if (matchingRecords.length === 0 && queryType !== "CNAME") {
matchingRecords = customRecords.filter(
(record) => record.type === "CNAME"
);
}
if (matchingRecords.length > 0) {
const answers = this.convertCustomRecordsToAnswers(
matchingRecords,
originalQueryName
);
logger.info(
`Returning ${answers.length} custom answers for ${originalQueryName} (${queryType})`
);
this.sendResponse(packet, answers, rinfo, true, 0, []);
return;
}
}
// Check if the domain exists (for NODATA vs NXDOMAIN)
const [domainExists, soaRecord] = await Promise.all([
const [resourceExists, soaRecord] = await Promise.all([
this.domainExists(queryName, baseDomain!),
this.getSOARecord(baseDomain!)
]);
const domainExists = resourceExists || customMatch.nameExists;
if (!domainExists) {
// NXDOMAIN: Name does not exist
@@ -581,17 +664,10 @@ export class AuthoritativeDNSServer {
}
// Check resources table (exact or wildcard match)
const wildcardPattern = this.getWildcardPattern(normalizedQuery);
const existsCondition = wildcardPattern
? or(
eq(resources.fullDomain, queryName),
eq(resources.fullDomain, wildcardPattern)
)!
: eq(resources.fullDomain, queryName);
const [row] = await db
.select({ fullDomain: resources.fullDomain })
.from(resources)
.where(existsCondition)
.where(this.getResourceDomainCondition(queryName))
.limit(1);
const exists = !!row;
this.cache.set(cacheKey, exists, 60);
@@ -645,14 +721,6 @@ export class AuthoritativeDNSServer {
logger.debug(`Cache miss for ${cacheKey}, querying database`);
try {
const wildcardPattern = this.getWildcardPattern(name);
const domainCondition = wildcardPattern
? or(
eq(resources.fullDomain, name),
eq(resources.fullDomain, wildcardPattern)
)!
: eq(resources.fullDomain, name);
// LEFT JOINs so we always get the resource row (and its orgId) even
// when the resource has no targets/sites/exitNodes assigned yet.
const resourceRows = await db
@@ -673,7 +741,7 @@ export class AuthoritativeDNSServer {
exitNodes,
eq(sites.exitNodeId, exitNodes.exitNodeId)
)
.where(domainCondition);
.where(this.getResourceDomainCondition(name));
if (resourceRows.length === 0) {
// Resource doesn't exist at all — also pre-populate the domainExists
@@ -883,7 +951,7 @@ export class AuthoritativeDNSServer {
queryType: dns.RecordType
): Promise<DNSRecord[] | null> {
// TODO: EVENTUALLY WE SHOULD PICK THE CLOSEST EXIT NODE TO THE USER BASED ON LATENCY
const exitNodesList = await listExitNodes(orgId, true);
const exitNodesList = await listExitNodesAcrossRegions(orgId, true);
if (!exitNodesList || exitNodesList.length === 0) {
logger.warn(`No exit nodes found for orgId: ${orgId}`);
@@ -1117,9 +1185,9 @@ export class AuthoritativeDNSServer {
}
case "MX":
// MX records need priority and exchange
// MX records need preference and exchange
return {
priority: record.priority,
preference: record.priority,
exchange: record.value
};
@@ -1146,13 +1214,271 @@ export class AuthoritativeDNSServer {
}
}
// User-defined records for `name`, plus whether the name exists among
// them (see matchCustomRecords for how wildcards apply).
private async getCustomRecords(
name: string,
zone: string
): Promise<CustomRecordMatch<CustomRecord>> {
const match = matchCustomRecords(
await this.getCustomZoneRecords(zone),
name,
zone
);
return {
// Each stored value is served as its own resource record
records: match.records.flatMap(
({ fullDomain, values, ...record }) =>
values.map((value) => ({ ...record, value }))
),
nameExists: match.nameExists
};
}
// All enabled custom records of `zone`. Only loaded when the zone is a
// verified NS delegation, so records can never surface under another
// org's zone or a domain whose ownership hasn't been proven. Loaded
// whole (a domain has at most 100) since wildcard matching needs to know
// every name that exists in the zone.
private async getCustomZoneRecords(
zone: string
): Promise<CustomZoneRecord[]> {
const cacheKey = `customZoneRecords:${zone}`;
const cached = this.cache.get<CustomZoneRecord[]>(cacheKey);
if (cached !== undefined) {
return cached;
}
return this.withInFlightDedup<CustomZoneRecord[]>(
cacheKey,
async () => {
try {
const rows = await db
.select({
fullDomain: customDnsRecords.fullDomain,
type: customDnsRecords.type,
values: customDnsRecords.values,
ttl: customDnsRecords.ttl,
priority: customDnsRecords.priority,
weight: customDnsRecords.weight,
port: customDnsRecords.port
})
.from(customDnsRecords)
.innerJoin(
domains,
eq(domains.domainId, customDnsRecords.domainId)
)
.where(
and(
eq(customDnsRecords.enabled, true),
eq(domains.baseDomain, zone),
eq(domains.type, "ns"),
eq(domains.verified, true)
)
);
const result: CustomZoneRecord[] = rows.map((row) => ({
...row,
values: parseCustomDnsRecordValues(row.values)
}));
this.cache.set(
cacheKey,
result,
CUSTOM_RECORD_CACHE_TTL_SECONDS
);
return result;
} catch (error) {
logger.error("Error querying custom DNS records:", error);
return [];
}
}
);
}
// Finds the user-defined delegation (NS records on a subdomain) that
// `name` falls at or beneath within `zone`. When cuts are nested the
// topmost one wins, since everything below it belongs to the child zone.
// Subject to the same verified-NS-zone scoping as getCustomRecords.
private async findDelegation(
name: string,
zone: string
): Promise<{ cut: string; nameservers: CustomRecord[] } | null> {
const normalized = name.replace(/\.$/, "").toLowerCase();
if (!normalized.endsWith(`.${zone}`)) {
return null; // the apex itself is never delegated
}
const cacheKey = `delegation:${zone}:${normalized}`;
const cached = this.cache.get<{
cut: string;
nameservers: CustomRecord[];
} | null>(cacheKey);
if (cached !== undefined) {
return cached;
}
return this.withInFlightDedup(cacheKey, async () => {
try {
// Every ancestor of the name below the zone apex, plus the
// name itself
const labels = normalized.split(".");
const zoneLabelCount = zone.split(".").length;
const candidates: string[] = [];
for (let i = 0; i < labels.length - zoneLabelCount; i++) {
candidates.push(labels.slice(i).join("."));
}
const rows = await db
.select({
fullDomain: customDnsRecords.fullDomain,
type: customDnsRecords.type,
values: customDnsRecords.values,
ttl: customDnsRecords.ttl,
priority: customDnsRecords.priority,
weight: customDnsRecords.weight,
port: customDnsRecords.port
})
.from(customDnsRecords)
.innerJoin(
domains,
eq(domains.domainId, customDnsRecords.domainId)
)
.where(
and(
inArray(customDnsRecords.fullDomain, candidates),
eq(customDnsRecords.type, "NS"),
eq(customDnsRecords.enabled, true),
eq(domains.baseDomain, zone),
eq(domains.type, "ns"),
eq(domains.verified, true)
)
);
let result: {
cut: string;
nameservers: CustomRecord[];
} | null = null;
if (rows.length > 0) {
const cut = rows.reduce((a, b) =>
a.fullDomain.length <= b.fullDomain.length ? a : b
).fullDomain;
const nameservers = rows
.filter((row) => row.fullDomain === cut)
.flatMap(({ fullDomain, values, ...record }) =>
parseCustomDnsRecordValues(values).map((value) => ({
...record,
value
}))
);
if (nameservers.length > 0) {
result = { cut, nameservers };
}
}
this.cache.set(
cacheKey,
result,
CUSTOM_RECORD_CACHE_TTL_SECONDS
);
return result;
} catch (error) {
logger.error("Error looking up DNS delegation:", error);
return null;
}
});
}
// Builds the authority (NS) and additional (glue) sections of a referral
// to a delegated subdomain. Glue is only needed - and only included -
// for nameservers named inside the delegated subdomain itself, which a
// resolver could not otherwise find the address of.
private async buildReferral(
delegation: { cut: string; nameservers: CustomRecord[] },
zone: string
): Promise<[dns.Answer[], dns.Answer[]]> {
const authorities = this.convertCustomRecordsToAnswers(
delegation.nameservers,
delegation.cut
);
const additionals: dns.Answer[] = [];
for (const nameserver of delegation.nameservers) {
const target = nameserver.value;
if (
target !== delegation.cut &&
!target.endsWith(`.${delegation.cut}`)
) {
continue;
}
const glue = (
await this.getCustomRecords(target, zone)
).records.filter(
(record) => record.type === "A" || record.type === "AAAA"
);
additionals.push(
...this.convertCustomRecordsToAnswers(glue, target)
);
}
return [authorities, additionals];
}
private convertCustomRecordsToAnswers(
records: CustomRecord[],
queryName: string
): dns.Answer[] {
return records.map((record) => {
let data: any;
switch (record.type) {
case "MX":
data = {
preference: record.priority ?? 0,
exchange: record.value
};
break;
case "SRV":
data = {
priority: record.priority ?? 0,
weight: record.weight ?? 0,
port: record.port ?? 0,
target: record.value
};
break;
case "TXT": {
// A single TXT character-string is at most 255 bytes;
// longer values are split across several strings, which
// clients concatenate. Values are printable ASCII only.
const chunks: string[] = [];
for (let i = 0; i < record.value.length; i += 255) {
chunks.push(record.value.slice(i, i + 255));
}
data = chunks;
break;
}
default:
data = record.value;
}
return {
name: queryName,
type: record.type,
ttl: record.ttl,
data
} as dns.Answer;
});
}
private sendResponse(
originalPacket: dns.Packet,
answers: dns.Answer[],
rinfo: dgram.RemoteInfo,
authoritative: boolean = true,
rcode: number = 0,
authorities: dns.Answer[] = []
authorities: dns.Answer[] = [],
additionals: dns.Answer[] = []
): void {
const response: dns.Packet = {
id: originalPacket.id,
@@ -1161,7 +1487,7 @@ export class AuthoritativeDNSServer {
questions: originalPacket.questions,
answers: answers,
authorities: authorities,
additionals: []
additionals: additionals
};
// Set authoritative flag if this is an authoritative response
@@ -1209,6 +1535,12 @@ export class AuthoritativeDNSServer {
private async refreshLicenseStatus(): Promise<void> {
try {
// Imported lazily so the standalone DNS server, which runs
// without a license, never loads the license module (it touches
// the database and starts phoning home as soon as it is imported)
const { default: license } = await import(
"#private/license/license"
);
this.isLicensed = await license.hasTier([
"personal",
"tier2",
@@ -1228,10 +1560,14 @@ export class AuthoritativeDNSServer {
);
}, 60_000);
await this.refreshLicenseStatus();
this.licenseRefreshInterval = setInterval(() => {
this.refreshLicenseStatus();
}, 60_000);
if (this.requireLicense) {
await this.refreshLicenseStatus();
this.licenseRefreshInterval = setInterval(() => {
this.refreshLicenseStatus();
}, 60_000);
} else {
this.isLicensed = true;
}
return new Promise((resolve, reject) => {
this.server.bind(this.port, (err?: Error) => {
@@ -1553,6 +1889,23 @@ export class AuthoritativeDNSServer {
return `*.${normalized.slice(dotIndex + 1)}`;
}
// A name resolves through its own resource, a wildcard resource one level
// up, or a wildcard resource directly under it that also serves its base
private getResourceDomainCondition(name: string) {
const normalized = name.replace(/\.$/, "");
const wildcardPattern = this.getWildcardPattern(normalized);
return or(
eq(resources.fullDomain, name),
wildcardPattern
? eq(resources.fullDomain, wildcardPattern)
: undefined,
and(
eq(resources.fullDomain, `*.${normalized}`),
eq(resources.matchBaseDomain, true)
)
)!;
}
private createRecordFromIp(
ip: string,
name: string,
+46
View File
@@ -333,6 +333,52 @@ export async function listExitNodes(
return exitNodesList;
}
// Like listExitNodes, but gerbil nodes are not restricted to this server's
// region, so a caller that prefers same-region nodes (e.g. the DNS server)
// can still fall back to another region when this one has none online.
export async function listExitNodesAcrossRegions(
orgId: string,
filterOnline = false
) {
const allExitNodes = await db
.select({
exitNodeId: exitNodes.exitNodeId,
endpoint: exitNodes.endpoint,
online: exitNodes.online,
type: exitNodes.type,
region: exitNodes.region
})
.from(exitNodes)
.leftJoin(
exitNodeOrgs,
eq(exitNodes.exitNodeId, exitNodeOrgs.exitNodeId)
)
.where(
or(
eq(exitNodes.type, "gerbil"),
and(
eq(exitNodes.type, "remoteExitNode"),
eq(exitNodeOrgs.orgId, orgId)
)
)
);
const onlineNodes = allExitNodes.filter(
(node) => !filterOnline || node.online
);
const remoteExitNodesList = onlineNodes.filter(
(node) => node.type === "remoteExitNode"
);
const gerbilExitNodes = onlineNodes.filter(
(node) => node.type === "gerbil"
);
// An org's own remote exit nodes take precedence over the cloud ones
return remoteExitNodesList.length > 0
? remoteExitNodesList
: gerbilExitNodes;
}
export type ExitNodePingResult = {
exitNodeId: number;
latencyMs: number;
+36
View File
@@ -17,6 +17,9 @@ import { v4 as uuidv4 } from "uuid";
const instanceId = uuidv4();
// Max keys passed to a single EXISTS call in anyLockExists
const LOCK_EXISTS_CHUNK_SIZE = 1000;
type LocalLockRecord = {
owner: string;
expiresAt: number;
@@ -254,6 +257,39 @@ export class LockManager {
}
}
/**
* Check whether any of the given locks currently exist, using one EXISTS
* call per chunk of keys instead of a round trip per key
* @param lockKeys - Unique identifiers for the locks
* @returns Promise<boolean> - true if at least one lock is held
*/
async anyLockExists(lockKeys: string[]): Promise<boolean> {
if (!redis || !redis.status || redis.status !== "ready") {
for (const lockKey of lockKeys) {
this.clearExpiredLocalLock(lockKey);
if (localLocks.has(lockKey)) {
return true;
}
}
return false;
}
try {
for (let i = 0; i < lockKeys.length; i += LOCK_EXISTS_CHUNK_SIZE) {
const redisKeys = lockKeys
.slice(i, i + LOCK_EXISTS_CHUNK_SIZE)
.map((lockKey) => `lock:${lockKey}`);
if ((await redis.exists(...redisKeys)) > 0) {
return true;
}
}
return false;
} catch (error) {
logger.error("Failed to check lock existence:", error);
return false;
}
}
/**
* Extend the TTL of an existing lock, provided the token matches the
* acquisition currently holding it.
+2 -2
View File
@@ -113,8 +113,8 @@ export async function logAccessAudit(data: {
const timestamp = Math.floor(Date.now() / 1000);
let metadata = null;
if (metadata) {
metadata = JSON.stringify(metadata);
if (data.metadata) {
metadata = JSON.stringify(data.metadata);
}
const clientIp = data.requestIp
+109
View File
@@ -0,0 +1,109 @@
/*
* 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 { logsDb, sshCommandLog, sshSessionLog } from "@server/db";
import logger from "@server/logger";
import { and, eq, lt, sql } from "drizzle-orm";
import { calculateCutoffTimestamp } from "@server/lib/cleanupLogs";
/** Maximum number of command rows to insert in a single statement. */
const INSERT_BATCH_SIZE = 100;
export interface SshSessionLogRecord {
sessionId: string;
orgId: string;
siteId: number;
siteResourceId: number | null;
resourceId: number | null;
clientId: number | null;
userId: string | null;
transport: "ssh" | "browser";
sourceAddr: string;
destAddr: string;
username: string;
authMethod: string | null;
keyFingerprint: string | null;
certKeyId: string | null;
startedAt: number; // epoch seconds
endedAt: number | null;
commands: {
command: string;
channel: string;
inexact: boolean;
timestamp: number; // epoch seconds
}[];
}
/**
* Persist one batch entry for an SSH session. Newt reports long-running
* sessions once per flush with only the commands recorded since the last
* flush, so the session row is upserted on (siteId, sessionId): the end time
* is filled in once known and the command count accumulates.
*/
export async function logSshSession(record: SshSessionLogRecord) {
const { commands, ...session } = record;
const [row] = await logsDb
.insert(sshSessionLog)
.values({ ...session, commandCount: commands.length })
.onConflictDoUpdate({
target: [sshSessionLog.siteId, sshSessionLog.sessionId],
set: {
endedAt: sql`coalesce(excluded."endedAt", ${sshSessionLog.endedAt})`,
commandCount: sql`${sshSessionLog.commandCount} + excluded."commandCount"`
}
})
.returning({ id: sshSessionLog.id });
if (!row || commands.length === 0) {
return;
}
for (let i = 0; i < commands.length; i += INSERT_BATCH_SIZE) {
await logsDb.insert(sshCommandLog).values(
commands.slice(i, i + INSERT_BATCH_SIZE).map((c) => ({
...c,
sshSessionLogId: row.id,
orgId: record.orgId
}))
);
}
}
export async function cleanUpOldSshSessionLogs(
orgId: string,
retentionDays: number
): Promise<void> {
const cutoffTimestamp = calculateCutoffTimestamp(retentionDays);
try {
await logsDb
.delete(sshCommandLog)
.where(
and(
lt(sshCommandLog.timestamp, cutoffTimestamp),
eq(sshCommandLog.orgId, orgId)
)
);
await logsDb
.delete(sshSessionLog)
.where(
and(
lt(sshSessionLog.startedAt, cutoffTimestamp),
eq(sshSessionLog.orgId, orgId)
)
);
} catch (error) {
logger.error("Error cleaning up old SSH session logs:", error);
}
}
+18
View File
@@ -66,6 +66,24 @@ class RedisRebuildQueue {
}
}
// Pipelines the membership checks so any number of jobs costs one round trip
async anyQueued(jobs: RebuildJob[]): Promise<boolean> {
if (jobs.length === 0) return false;
if (!redis || redis.status !== "ready") return false;
try {
const pipeline = redis.pipeline();
for (const job of jobs) {
pipeline.sismember(QUEUED_SET_KEY, `${job.type}:${job.id}`);
}
const results = await pipeline.exec();
return (results ?? []).some(
([err, member]) => !err && member === 1
);
} catch {
return false;
}
}
async enqueue(job: RebuildJob): Promise<void> {
if (!redis || redis.status !== "ready") {
logger.warn(
@@ -0,0 +1,109 @@
/*
* 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 { clientSitesAssociationsCache, clients, db } from "@server/db";
import logger from "@server/logger";
import { and, eq, inArray } from "drizzle-orm";
export type ResolvedClient = {
clientId: number;
userId: string | null;
clientEndpoint: string | null;
};
/**
* Strip the port from a source address (e.g. "100.90.128.1:38004" →
* "100.90.128.1").
*/
export function sourceAddrToIp(addr: string): string {
return addr.includes(":") ? addr.split(":")[0] : addr;
}
/**
* Map the source addresses newt saw on its tunnel to the clients that own
* them. A source address is the client's IP on the WireGuard network, which
* is the IP portion of the client's subnet (stored with the org's CIDR
* suffix, e.g. "100.90.128.5/16"), so we rebuild that exact string and do a
* targeted IN query. The returned map is keyed by IP without port.
*/
export async function resolveClientsBySourceAddr({
orgId,
orgSubnet,
siteId,
sourceAddrs
}: {
orgId: string;
orgSubnet: string | null;
siteId: number;
sourceAddrs: Iterable<string>;
}): Promise<Map<string, ResolvedClient>> {
const ipToClient = new Map<string, ResolvedClient>();
const cidrSuffix = orgSubnet?.includes("/")
? orgSubnet.substring(orgSubnet.indexOf("/"))
: null;
if (!cidrSuffix) {
return ipToClient;
}
const uniqueIps = new Set<string>();
for (const addr of sourceAddrs) {
if (addr) {
uniqueIps.add(sourceAddrToIp(addr));
}
}
if (uniqueIps.size === 0) {
return ipToClient;
}
const subnetQueries = Array.from(uniqueIps).map(
(ip) => `${ip}${cidrSuffix}`
);
logger.debug(`Subnet queries: ${JSON.stringify(subnetQueries)}`);
const matchedClients = await db
.select({
clientId: clients.clientId,
userId: clients.userId,
subnet: clients.subnet,
clientEndpoint: clientSitesAssociationsCache.endpoint
})
.from(clients)
.leftJoin(
// this should be one to one
clientSitesAssociationsCache,
and(
eq(clients.clientId, clientSitesAssociationsCache.clientId),
eq(clientSitesAssociationsCache.siteId, siteId)
)
)
.where(
and(
eq(clients.orgId, orgId),
inArray(clients.subnet, subnetQueries)
)
);
for (const c of matchedClients) {
const ip = c.subnet.split("/")[0];
logger.debug(`Client ${c.clientId} subnet ${c.subnet} matches ${ip}`);
ipToClient.set(ip, {
clientId: c.clientId,
userId: c.userId,
clientEndpoint: c.clientEndpoint
});
}
return ipToClient;
}
@@ -0,0 +1,36 @@
/*
* 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 { db, resourceSessionReauth } from "@server/db";
import { and, eq } from "drizzle-orm";
// True when an admin revoked this dashboard session's SSO access to the
// resource. The marker doesn't need clearing: logging in again creates a new
// dashboard session, and the row cascades away once the old one is deleted.
export async function isResourceReauthRequired(
userSessionId: string,
resourceId: number
): Promise<boolean> {
const [row] = await db
.select({ resourceId: resourceSessionReauth.resourceId })
.from(resourceSessionReauth)
.where(
and(
eq(resourceSessionReauth.userSessionId, userSessionId),
eq(resourceSessionReauth.resourceId, resourceId)
)
)
.limit(1);
return !!row;
}
+50 -14
View File
@@ -20,6 +20,7 @@ import {
db,
domainNamespaces,
domains,
exitNodeOrgs,
exitNodes,
loginPage,
redirects,
@@ -71,7 +72,8 @@ import {
import {
appendPathMatch,
buildHostRule,
computeRoutePriority
computeRoutePriority,
ruleValue
} from "@server/lib/traefik/rule";
import { buildSiteResourceAliasCertPlaceholders } from "@server/lib/traefik/siteResourceAlias";
import { ResourceWithTargets } from "@server/lib/traefik/types";
@@ -146,6 +148,7 @@ export async function getTraefikConfig(
proxyProtocol: resources.proxyProtocol,
proxyProtocolVersion: resources.proxyProtocolVersion,
wildcard: resources.wildcard,
matchBaseDomain: resources.matchBaseDomain,
mode: resources.mode,
maintenanceModeEnabled: resources.maintenanceModeEnabled,
@@ -297,6 +300,7 @@ export async function getTraefikConfig(
domainCertResolver: row.domainCertResolver,
preferWildcardCert: row.preferWildcardCert,
wildcard: row.wildcard,
matchBaseDomain: row.matchBaseDomain,
maintenanceModeEnabled: row.maintenanceModeEnabled,
maintenanceModeType: row.maintenanceModeType,
@@ -411,6 +415,9 @@ export async function getTraefikConfig(
// queried separately and emitted on every exit node. A redirect listens
// either on a resource's fullDomain or on subdomain.baseDomain of a
// domain; the domain join resolves to whichever one applies.
// Remote exit nodes belong to specific orgs, so only emit the redirects
// of those orgs - otherwise every other org's redirect hosts leak to them.
const isRemoteExitNode = exitNode?.type === "remoteExitNode";
const redirectRows = await db
.select({
name: redirects.name,
@@ -427,6 +434,7 @@ export async function getTraefikConfig(
resourceSubdomain: resources.subdomain,
resourceSsl: resources.ssl,
resourceWildcard: resources.wildcard,
resourceMatchBaseDomain: resources.matchBaseDomain,
// Domain (the redirect's own, or the resource's)
baseDomain: domains.baseDomain,
domainCertResolver: domains.certResolver,
@@ -455,7 +463,16 @@ export async function getTraefikConfig(
eq(resources.enabled, true),
not(isNull(resources.domainId))
)
)
),
isRemoteExitNode
? inArray(
redirects.orgId,
db
.select({ orgId: exitNodeOrgs.orgId })
.from(exitNodeOrgs)
.where(eq(exitNodeOrgs.exitNodeId, exitNodeId))
)
: undefined
)
)
.orderBy(desc(redirects.priority), redirects.redirectId); // stable ordering
@@ -486,6 +503,7 @@ export async function getTraefikConfig(
? !!row.resourceSubdomain
: !!row.subdomain,
wildcard: row.resourceWildcard,
matchBaseDomain: row.resourceMatchBaseDomain,
ssl: attachedToResource ? !!row.resourceSsl : row.ssl,
attachedTo: attachedToResource ? "resource" : "domain",
resourceId: row.resourceId,
@@ -646,7 +664,11 @@ export async function getTraefikConfig(
...additionalMiddlewares
];
let rule: string = buildHostRule(fullDomain, resource.wildcard);
let rule: string = buildHostRule(
fullDomain,
resource.wildcard,
resource.matchBaseDomain
);
const priority = computeRoutePriority(
resource.priority,
@@ -661,7 +683,8 @@ export async function getTraefikConfig(
hasSubdomain: !!resource.subdomain,
domainCertResolver: resource.domainCertResolver,
preferWildcardCert:
resource.preferWildcardCert || resource.wildcard
resource.preferWildcardCert || resource.wildcard,
matchBaseDomain: resource.matchBaseDomain
});
} else {
// find a cert that matches the full domain, if not continue
@@ -937,7 +960,8 @@ export async function getTraefikConfig(
hasSubdomain: redirect.hasSubdomain,
domainCertResolver: redirect.domainCertResolver,
preferWildcardCert:
redirect.preferWildcardCert || redirect.wildcard
redirect.preferWildcardCert || redirect.wildcard,
matchBaseDomain: redirect.matchBaseDomain
});
}
const matchingCert = validCerts.find(
@@ -1189,7 +1213,7 @@ export async function getTraefikConfig(
const srKey = `inference-sr${sr.siteResourceId}`;
const routerName = `${srKey}-router`;
const serviceName = `${srKey}-service`;
const rule = `Host(\`${fullDomain}\`) && ClientIP(\`${exitNode.address}\`)`; // restrict to coming from the exit node ip range that the client is connected to
const rule = `${buildHostRule(fullDomain)} && ClientIP(${ruleValue(exitNode.address)})`; // restrict to coming from the exit node ip range that the client is connected to
let tls: any = {};
if (!pangolinCertModeEnabled) {
@@ -1250,23 +1274,30 @@ export async function getTraefikConfig(
}
}
const usePangolinDns =
privateConfig.getRawPrivateConfig().flags.use_pangolin_dns;
if (generateLoginPageRouters) {
const exitNodeLoginPages = await db
.select({
loginPageId: loginPage.loginPageId,
fullDomain: loginPage.fullDomain,
subdomain: loginPage.subdomain,
exitNodeId: exitNodes.exitNodeId,
domainId: loginPage.domainId
domainId: loginPage.domainId,
domainCertResolver: domains.certResolver,
preferWildcardCert: domains.preferWildcardCert
})
.from(loginPage)
.innerJoin(
exitNodes,
eq(exitNodes.exitNodeId, loginPage.exitNodeId)
)
.leftJoin(domains, eq(domains.domainId, loginPage.domainId))
.where(eq(exitNodes.exitNodeId, exitNodeId));
let validCertsLoginPages: CertificateResult[] = [];
if (pangolinCertModeEnabled) {
if (usePangolinDns) {
// create a list of all domains to get certs for
const domains = new Set<string>();
for (const lp of exitNodeLoginPages) {
@@ -1310,9 +1341,14 @@ export async function getTraefikConfig(
continue;
}
const tls = {};
if (!pangolinCertModeEnabled) {
// TODO: we need to add the wildcard logic here too
let tls: Record<string, any> = {};
if (!usePangolinDns) {
tls = buildWildcardTls({
fullDomain: lp.fullDomain,
hasSubdomain: !!lp.subdomain,
domainCertResolver: lp.domainCertResolver,
preferWildcardCert: lp.preferWildcardCert
});
} else {
// find a cert that matches the full domain, if not continue
const matchingCert = validCertsLoginPages.find(
@@ -1345,7 +1381,7 @@ export async function getTraefikConfig(
],
middlewares: [landingRateLimitMiddlewareName],
service: "landing-service",
rule: `Host(\`${fullDomain}\`) && (PathRegexp(\`^/auth/resource/[^/]+$\`) || PathRegexp(\`^/auth/idp/[0-9]+/oidc/callback\`) || PathPrefix(\`/_next\`) || Path(\`/auth/org\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
rule: `${buildHostRule(fullDomain)} && (PathRegexp(\`^/auth/resource/[^/]+$\`) || PathRegexp(\`^/auth/idp/[0-9]+/oidc/callback\`) || PathPrefix(\`/_next\`) || Path(\`/auth/org\`) || PathRegexp(\`^/__nextjs*\`) || Path(\`/favicon.ico\`))`,
priority: 203,
tls: tls
};
@@ -1367,7 +1403,7 @@ export async function getTraefikConfig(
redirectToRootMiddlewareName
],
service: "landing-service",
rule: `Host(\`${fullDomain}\`)`,
rule: buildHostRule(fullDomain),
priority: 202,
tls: tls
};
@@ -1379,7 +1415,7 @@ export async function getTraefikConfig(
],
middlewares: [redirectHttpsMiddlewareName],
service: "landing-service",
rule: `Host(\`${fullDomain}\`)`,
rule: buildHostRule(fullDomain),
priority: 201
};
}
@@ -0,0 +1,149 @@
/*
* 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 { logsDb, sshCommandLog, sshSessionLog } from "@server/db";
import { registry } from "@server/openApi";
import { NextFunction } from "express";
import { Request, Response } from "express";
import { OpenAPITags } from "@server/openApi";
import createHttpError from "http-errors";
import HttpCode from "@server/types/HttpCode";
import { fromError } from "zod-validation-error";
import { z } from "zod";
import { asc, count, eq } from "drizzle-orm";
import logger from "@server/logger";
import {
querySshSessionLogsParams,
querySshSessionLogsQuery,
getSshSessionWhere
} from "./querySshSessionLog";
import { generateCSV } from "@server/routers/auditLogs/generateCSV";
import { MAX_EXPORT_LIMIT } from "@server/routers/auditLogs";
registry.registerPath({
method: "get",
path: "/org/{orgId}/logs/ssh/export",
description:
"Export the SSH session log for an organization as CSV, one row per command",
tags: [OpenAPITags.Logs],
request: {
query: querySshSessionLogsQuery.omit({ limit: true, offset: true }),
params: querySshSessionLogsParams
},
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()
})
}
}
}
}
});
export async function exportSshSessionLogs(
req: Request,
res: Response,
next: NextFunction
): Promise<any> {
try {
const parsedQuery = querySshSessionLogsQuery.safeParse(req.query);
if (!parsedQuery.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedQuery.error)
)
);
}
const parsedParams = querySshSessionLogsParams.safeParse(req.params);
if (!parsedParams.success) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
fromError(parsedParams.error)
)
);
}
const data = { ...parsedQuery.data, ...parsedParams.data };
const where = getSshSessionWhere(data);
const [{ count: rowCount }] = await logsDb
.select({ count: count() })
.from(sshCommandLog)
.innerJoin(
sshSessionLog,
eq(sshCommandLog.sshSessionLogId, sshSessionLog.id)
)
.where(where);
if (rowCount > MAX_EXPORT_LIMIT) {
return next(
createHttpError(
HttpCode.BAD_REQUEST,
`Export limit exceeded. Your selection contains ${rowCount} rows, but the maximum is ${MAX_EXPORT_LIMIT} rows. Please select a shorter time range to reduce the data.`
)
);
}
const log = await logsDb
.select({
timestamp: sshCommandLog.timestamp,
sessionId: sshSessionLog.sessionId,
siteId: sshSessionLog.siteId,
siteResourceId: sshSessionLog.siteResourceId,
resourceId: sshSessionLog.resourceId,
clientId: sshSessionLog.clientId,
userId: sshSessionLog.userId,
transport: sshSessionLog.transport,
username: sshSessionLog.username,
authMethod: sshSessionLog.authMethod,
sourceAddr: sshSessionLog.sourceAddr,
destAddr: sshSessionLog.destAddr,
channel: sshCommandLog.channel,
command: sshCommandLog.command,
inexact: sshCommandLog.inexact
})
.from(sshCommandLog)
.innerJoin(
sshSessionLog,
eq(sshCommandLog.sshSessionLogId, sshSessionLog.id)
)
.where(where)
.orderBy(asc(sshCommandLog.timestamp), asc(sshCommandLog.id))
.limit(MAX_EXPORT_LIMIT);
const csvData = generateCSV(log);
res.setHeader("Content-Type", "text/csv; charset=utf-8");
res.setHeader(
"Content-Disposition",
`attachment; filename="ssh-session-logs-${data.orgId}-${Date.now()}.csv"`
);
return res.send(csvData);
} catch (error) {
logger.error(error);
return next(
createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred")
);
}
}
Loaded 100 of 304 files, more files were not shown because too many files have changed in this diff. Show more