Compare commits

...

727 Commits

Author SHA1 Message Date
Owen Schwartz b982639dc4 Merge pull request #3746 from fosrl/dev
Resolve oss build issue
2026-09-14 18:05:52 -04:00
Owen 56e758a656 Resolve oss build issue 2026-09-14 18:05:30 -04:00
Owen Schwartz a84db679b2 Merge pull request #3741 from fosrl/dev
1.23.0-rc.0
2026-09-14 17:53:01 -04:00
Owen Schwartz fb726cc6b2 Merge pull request #3743 from fosrl/l10n_dev
New Crowdin updates
2026-09-14 17:51:52 -04:00
Owen Schwartz 5d0ceebe0d New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-09-14 17:51:05 -04:00
Owen Schwartz c964821195 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-09-14 17:51:03 -04:00
Owen Schwartz 5c44c6da73 New translations en-us.json (Turkish)
[ci skip]
2026-09-14 17:51:01 -04:00
Owen Schwartz 6e52758d6e New translations en-us.json (Russian)
[ci skip]
2026-09-14 17:50:59 -04:00
Owen Schwartz 0af194550f New translations en-us.json (Portuguese)
[ci skip]
2026-09-14 17:50:57 -04:00
Owen Schwartz 0d12402662 New translations en-us.json (Polish)
[ci skip]
2026-09-14 17:50:54 -04:00
Owen Schwartz 3fb2e34256 New translations en-us.json (Dutch)
[ci skip]
2026-09-14 17:50:52 -04:00
Owen Schwartz ec2b1fbd37 New translations en-us.json (Korean)
[ci skip]
2026-09-14 17:50:50 -04:00
Owen Schwartz 806ae93a12 New translations en-us.json (Italian)
[ci skip]
2026-09-14 17:50:48 -04:00
Owen Schwartz 5d99e0a2f6 New translations en-us.json (German)
[ci skip]
2026-09-14 17:50:45 -04:00
Owen Schwartz a75f335791 New translations en-us.json (Danish)
[ci skip]
2026-09-14 17:50:43 -04:00
Owen Schwartz 153d29c47c New translations en-us.json (Czech)
[ci skip]
2026-09-14 17:50:41 -04:00
Owen Schwartz ee8749c292 New translations en-us.json (Bulgarian)
[ci skip]
2026-09-14 17:50:39 -04:00
Owen Schwartz 4f905ddae5 New translations en-us.json (Spanish)
[ci skip]
2026-09-14 17:50:37 -04:00
Owen Schwartz 74e686ff3f New translations en-us.json (French)
[ci skip]
2026-09-14 17:50:35 -04:00
Owen a4ade43380 Fix compilation issue with prop 2026-09-14 17:47:44 -04:00
miloschwartz 68821514a0 replace node cache in dns server 2026-09-14 17:41:41 -04:00
Owen Schwartz a2652cf692 New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-09-14 17:41:14 -04:00
Owen Schwartz 957144ac3a New translations en-us.json (Chinese Simplified)
[ci skip]
2026-09-14 17:41:11 -04:00
Owen Schwartz 20c23cdfc5 New translations en-us.json (Turkish)
[ci skip]
2026-09-14 17:41:09 -04:00
Owen Schwartz c03e1c5781 New translations en-us.json (Russian)
[ci skip]
2026-09-14 17:41:06 -04:00
Owen Schwartz e69d3eded5 New translations en-us.json (Portuguese)
[ci skip]
2026-09-14 17:41:04 -04:00
Owen Schwartz 34084cc3a6 New translations en-us.json (Polish)
[ci skip]
2026-09-14 17:41:02 -04:00
Owen Schwartz 91776ad2e4 New translations en-us.json (Dutch)
[ci skip]
2026-09-14 17:41:00 -04:00
Owen Schwartz 77eabf41e7 New translations en-us.json (Korean)
[ci skip]
2026-09-14 17:40:58 -04:00
Owen Schwartz d68895e64a New translations en-us.json (Italian)
[ci skip]
2026-09-14 17:40:56 -04:00
Owen Schwartz 1c2d81d77c New translations en-us.json (German)
[ci skip]
2026-09-14 17:40:53 -04:00
Owen Schwartz 998778eb72 New translations en-us.json (Danish)
[ci skip]
2026-09-14 17:40:51 -04:00
Owen Schwartz 7be206947a New translations en-us.json (Czech)
[ci skip]
2026-09-14 17:40:49 -04:00
Owen Schwartz 125d33c071 New translations en-us.json (Bulgarian)
[ci skip]
2026-09-14 17:40:47 -04:00
Owen Schwartz 410d26f2df New translations en-us.json (Spanish)
[ci skip]
2026-09-14 17:40:45 -04:00
Owen Schwartz 6aa1dafa10 New translations en-us.json (French)
[ci skip]
2026-09-14 17:40:43 -04:00
miloschwartz 8f1344a665 bump version and add 1.23 migrations 2026-09-14 17:18:31 -04:00
Owen d23028d38e Update lock 2026-09-14 17:06:14 -04:00
Owen Schwartz 7506a2614b Update Crowdin configuration file
[ci skip]
2026-09-14 17:00:52 -04:00
Owen b3e5de4a4d Fix cache separation for versions 2026-09-14 16:48:12 -04:00
Owen f9f38fb3f0 Handle the agent and agent version 2026-09-14 16:48:12 -04:00
Owen 888ccce397 Rename newt site to site 2026-09-14 16:48:12 -04:00
Owen 49e0a8bd47 Update the install and run commands to use the cli 2026-09-14 16:48:11 -04:00
Owen f59aed8482 Replace newt and olm with pangolin-cli 2026-09-14 16:48:11 -04:00
Owen b6c048c805 Store the cli version for display and handle auto update 2026-09-14 16:48:11 -04:00
Owen 018ee17304 Include erid new node 2026-09-14 16:48:11 -04:00
Owen 5cad796023 Dont restrict 2026-09-14 16:48:11 -04:00
Owen 5a445817e0 Show the remote nodes on enterprise pangolin dns 2026-09-14 16:48:11 -04:00
Owen fd2c397bfc Update comment to be more clear 2026-09-14 16:48:11 -04:00
Owen 6c794e0b0c Just point to the docs 2026-09-14 16:48:11 -04:00
Owen 7acb98e245 Working on ha config and docs 2026-09-14 16:48:11 -04:00
Owen d59ab30c22 Add reference documentation 2026-09-14 16:48:11 -04:00
Owen e4a8e9ac8c Add region from config file 2026-09-14 16:48:10 -04:00
Owen a874a0b745 Add explicit process exit to prevent hanging in migrations script 2026-09-14 16:48:10 -04:00
Owen 9ba2cfdef2 Make the email optional 2026-09-14 16:48:10 -04:00
Owen bba8d4e7a2 Add license checks to JobScheduler and AuthoritativeDNSServer 2026-09-14 16:48:10 -04:00
Owen 350f8c012a Pull in static domains to the traefik config again 2026-09-14 16:48:10 -04:00
Owen 8a97ed5200 Move domain information to all in private 2026-09-14 16:48:10 -04:00
Owen e66f7fe71b Implement exit node check-in tracking and adjust logging for connection errors 2026-09-14 16:48:10 -04:00
Owen a37a59f39a Add default dns port and allow sites to be empty 2026-09-14 16:48:10 -04:00
Owen d0bf553f6f Quiet up logs 2026-09-14 16:48:10 -04:00
Owen 87e005d32f Seperate dns from certificates 2026-09-14 16:48:10 -04:00
Owen ad343a7453 Wire up to start 2026-09-14 16:48:10 -04:00
Owen 7e4d38548f Add certificate generation 2026-09-14 16:48:09 -04:00
Owen 3421441635 Add cert_mode to know when to gen or pull certs 2026-09-14 16:48:09 -04:00
Owen 073bfb32e9 Adjust lic headers 2026-09-14 16:48:09 -04:00
Owen 2c5b1e5f0f Add dns server 2026-09-14 16:48:09 -04:00
miloschwartz 294830db08 show ssh commands 2026-09-14 16:48:09 -04:00
miloschwartz fb8d531435 add sites to resource launcher panel 2026-09-14 16:48:09 -04:00
miloschwartz 11d947f4ef add better page loading indicator 2026-09-14 16:48:09 -04:00
miloschwartz aad18e6501 more cosmetic improvememnts 2026-09-14 16:48:09 -04:00
miloschwartz dac8b5a132 move improvements to user management ui 2026-09-14 16:48:09 -04:00
miloschwartz 162dade852 improvements to create user 2026-09-14 16:48:09 -04:00
miloschwartz dfe7f60244 add server side filter for server admin 2026-09-14 16:48:09 -04:00
Fred KISSIE 07bea71cb9 ♻️ refactor 2026-09-14 16:48:08 -04:00
Fred KISSIE 557c398d0b 💬 update text 2026-09-14 16:48:08 -04:00
Fred KISSIE 0a385d1e44 🚧 toggle server admin 2026-09-14 16:48:08 -04:00
Fred KISSIE 521f78c2f3 🚧 server admin 2026-09-14 16:48:08 -04:00
miloschwartz 0ea4a5fb3d visual adjustments to logo empty state and profile dropdown 2026-09-14 16:48:08 -04:00
miloschwartz e2609f1a0d cosmetic adjustments 2026-09-14 16:48:08 -04:00
Fred KISSIE 8b20a88838 delete org 2026-09-14 16:48:08 -04:00
Fred KISSIE 7aae51ca9d 🚧 wip 2026-09-14 16:48:08 -04:00
Fred KISSIE 1d6d885f30 ♻️ Only show the username instead of the name+username 2026-09-14 16:48:08 -04:00
Fred KISSIE fef1476f67 List of orgs, with all columns 2026-09-14 16:48:08 -04:00
Fred KISSIE e371f26d73 List orgs in server 2026-09-14 16:48:08 -04:00
Fred KISSIE 9c8b93d6cc 🚧 WIP 2026-09-14 16:48:08 -04:00
miloschwartz aed325f273 switch to lru in memory cache and dont cache failed sessions 2026-09-14 16:48:07 -04:00
miloschwartz c7645e5c5b update screenshots 2026-09-14 16:48:07 -04:00
Owen c0eed078c8 Fix tsconfig to use react-jsx 2026-09-14 16:48:07 -04:00
Owen 0f69012c4d Fix circular import 2026-09-14 16:48:07 -04:00
Owen ba2eb87f20 Widen subnet 2026-09-14 16:48:07 -04:00
Owen c0ea32863c Quiet up error logs 2026-09-14 16:48:07 -04:00
Owen 4d71fcbac8 Quiet log message 2026-09-14 16:48:07 -04:00
Owen 1d3dcec4c8 Use endpoint instead of reachableAt for remote nodes 2026-09-14 16:48:07 -04:00
Owen Schwartz 17375348b0 Merge pull request #3702 from fosrl/dev
1.22.2
2026-09-04 17:18:42 -04:00
Owen ea9017ac06 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2026-09-04 17:15:33 -04:00
Owen 88770ff97b Refactor form submissions to use startTransition for improved performance 2026-09-04 17:15:20 -04:00
miloschwartz 8e75425887 update screenshots 2026-09-04 15:46:56 -04:00
Owen 44b0186044 Fix yaml import 2026-09-04 15:32:59 -04:00
Owen 063f6b5ca9 Basic DNS config send empty for olm 2026-09-04 12:31:04 -04:00
Owen Schwartz e7fdbf9e85 Merge pull request #3699 from fosrl/dev
1.22.1-s.1
2026-09-04 10:38:39 -04:00
Owen 778a840ed7 Fix react bug not making it possible to complete security form 2026-09-04 10:08:57 -04:00
Owen 9d19195089 Configurable tab title and disable flag for keys 2026-09-04 09:20:20 -04:00
Owen 54bbe82504 Dont log invalid message type
Fixes #3695
2026-09-04 09:06:58 -04:00
Milo Schwartz cddb5ecc3d Merge pull request #3691 from fosrl/dev
update readme
2026-09-03 16:19:47 -04:00
miloschwartz de57df2520 update readme and screenshots 2026-09-03 16:17:07 -04:00
Owen 9e392a967d Add AI disclosure 2026-09-03 15:41:12 -04:00
Owen Schwartz 52f7364c35 Merge pull request #3690 from fosrl/dev
1.22.1
2026-09-03 12:05:18 -04:00
Owen 1bc5fbbf0f Always pull all of the users for the blueprints 2026-09-03 12:02:41 -04:00
Owen be0dd65557 Update imports 2026-09-03 11:35:28 -04:00
Owen 11be7b8ee4 Fix export 2026-09-03 11:24:36 -04:00
Owen 9373cc0408 Revert tanstak components 2026-09-03 11:20:13 -04:00
Owen e50763c340 Pin ts 2026-09-03 11:20:13 -04:00
dependabot[bot] 8826240548 Bump the npm-dependencies group across 1 directory with 74 updates
Bumps the npm-dependencies group with 74 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@asteasolutions/zod-to-openapi](https://github.com/asteasolutions/zod-to-openapi) | `8.5.0` | `9.1.0` |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1056.0` | `3.1121.0` |
| [@hookform/resolvers](https://github.com/react-hook-form/resolvers) | `5.4.0` | `5.9.1` |
| [@node-rs/argon2](https://github.com/napi-rs/node-rs) | `2.0.2` | `2.2.0` |
| [@radix-ui/react-avatar](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/avatar) | `1.1.11` | `1.2.6` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.3` | `1.3.11` |
| [@radix-ui/react-collapsible](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/collapsible) | `1.1.12` | `1.1.20` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.15` | `1.1.23` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.16` | `2.1.24` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.8` | `2.1.15` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.15` | `1.1.23` |
| [@radix-ui/react-progress](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/progress) | `1.1.8` | `1.1.16` |
| [@radix-ui/react-radio-group](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radio-group) | `1.3.8` | `1.4.7` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.10` | `1.2.18` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.2.6` | `2.3.7` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.8` | `1.1.15` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.2.4` | `1.3.3` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.2.6` | `1.3.7` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.13` | `1.1.21` |
| [@radix-ui/react-toast](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toast) | `1.2.15` | `1.2.23` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.8` | `1.2.16` |
| [@react-email/render](https://github.com/resend/react-email/tree/HEAD/packages/render) | `2.0.8` | `2.1.0` |
| [@simplewebauthn/server](https://github.com/MasterKale/SimpleWebAuthn/tree/HEAD/packages/server) | `13.3.1` | `13.3.3` |
| [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.100.14` | `5.102.8` |
| [@tanstack/react-table](https://github.com/TanStack/table/tree/HEAD/packages/react-table) | `8.21.3` | `9.2.4` |
| [axios](https://github.com/axios/axios) | `1.18.0` | `1.20.0` |
| [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) | `8.5.2` | `8.7.0` |
| [gpt-tokenizer](https://github.com/niieani/gpt-tokenizer) | `3.4.0` | `4.0.0` |
| [helmet](https://github.com/helmetjs/helmet) | `8.2.0` | `8.3.0` |
| [input-otp](https://github.com/guilhermerodz/input-otp/tree/HEAD/packages/input-otp) | `1.4.2` | `1.5.0` |
| [ioredis](https://github.com/redis/ioredis) | `5.11.0` | `6.0.0` |
| [js-yaml](https://github.com/nodeca/js-yaml) | `4.3.1` | `5.4.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.17.0` | `1.38.0` |
| [maxmind](https://github.com/runk/node-maxmind) | `5.0.6` | `5.0.7` |
| [next](https://github.com/vercel/next.js) | `16.3.1` | `16.3.3` |
| [next-intl](https://github.com/amannn/next-intl) | `4.13.0` | `4.14.1` |
| [nodemailer](https://github.com/nodemailer/nodemailer) | `9.0.1` | `9.1.0` |
| [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) | `8.21.0` | `8.23.0` |
| [@types/pg](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pg) | `8.20.0` | `8.23.1` |
| [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.35.6` | `5.51.4` |
| [react](https://github.com/react/react/tree/HEAD/packages/react) | `19.2.6` | `19.2.8` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.15` | `19.2.18` |
| [react-day-picker](https://github.com/gpbl/react-day-picker/tree/HEAD/packages/react-day-picker) | `9.14.0` | `10.0.1` |
| [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom) | `19.2.6` | `19.2.8` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.3` | `19.2.5` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.76.1` | `7.87.0` |
| [react-icons](https://github.com/react-icons/react-icons) | `5.6.0` | `5.7.0` |
| [recharts](https://github.com/recharts/recharts) | `3.8.1` | `3.10.1` |
| [semver](https://github.com/npm/node-semver) | `7.8.1` | `7.8.5` |
| [@types/semver](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/semver) | `7.7.1` | `7.8.0` |
| [stripe](https://github.com/stripe/stripe-node) | `22.2.0` | `22.6.0` |
| [uuid](https://github.com/uuidjs/uuid) | `14.0.0` | `14.0.2` |
| [ws](https://github.com/websockets/ws) | `8.21.0` | `8.21.3` |
| [yargs](https://github.com/yargs/yargs) | `18.0.0` | `18.1.0` |
| [zod](https://github.com/colinhacks/zod) | `4.4.3` | `4.5.4` |
| [@dotenvx/dotenvx](https://github.com/dotenvx/dotenvx) | `1.69.1` | `2.23.0` |
| [@react-email/ui](https://github.com/resend/react-email/tree/HEAD/packages/ui) | `6.9.2` | `6.9.3` |
| [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) | `4.3.0` | `4.3.3` |
| [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) | `5.100.14` | `5.102.8` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.1` | `26.4.0` |
| [@types/nodemailer](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/nodemailer) | `8.0.0` | `8.0.1` |
| [@types/sshpk](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sshpk) | `1.17.4` | `1.17.5` |
| [esbuild](https://github.com/evanw/esbuild) | `0.28.0` | `0.28.2` |
| [esbuild-node-externals](https://github.com/pradel/esbuild-node-externals) | `1.22.0` | `2.0.0` |
| [eslint](https://github.com/eslint/eslint) | `10.4.0` | `10.9.1` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `16.2.6` | `16.3.3` |
| [postcss](https://github.com/postcss/postcss) | `8.5.23` | `8.5.26` |
| [prettier](https://github.com/prettier/prettier) | `3.8.3` | `3.9.6` |
| [react-email](https://github.com/resend/react-email/tree/HEAD/packages/react-email) | `6.5.0` | `6.9.3` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.0` | `4.3.3` |
| [tsc-alias](https://github.com/justkey007/tsc-alias) | `1.8.17` | `1.9.2` |
| [tsx](https://github.com/privatenumber/tsx) | `4.22.3` | `4.23.13` |
| [typescript](https://github.com/microsoft/TypeScript) | `6.0.3` | `7.0.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.60.0` | `8.68.0` |



Updates `@asteasolutions/zod-to-openapi` from 8.5.0 to 9.1.0
- [Release notes](https://github.com/asteasolutions/zod-to-openapi/releases)
- [Commits](https://github.com/asteasolutions/zod-to-openapi/compare/v8.5.0...v9.1.0)

Updates `@aws-sdk/client-s3` from 3.1056.0 to 3.1121.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1121.0/clients/client-s3)

Updates `@hookform/resolvers` from 5.4.0 to 5.9.1
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v5.4.0...v5.9.1)

Updates `@node-rs/argon2` from 2.0.2 to 2.2.0
- [Release notes](https://github.com/napi-rs/node-rs/releases)
- [Commits](https://github.com/napi-rs/node-rs/compare/@node-rs/argon2@2.0.2...@node-rs/argon2@2.2.0)

Updates `@radix-ui/react-avatar` from 1.1.11 to 1.2.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/avatar/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/avatar)

Updates `@radix-ui/react-checkbox` from 1.3.3 to 1.3.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

Updates `@radix-ui/react-collapsible` from 1.1.12 to 1.1.20
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/collapsible/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/collapsible)

Updates `@radix-ui/react-dialog` from 1.1.15 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.16 to 2.1.24
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-label` from 2.1.8 to 2.1.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-popover` from 1.1.15 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

Updates `@radix-ui/react-progress` from 1.1.8 to 1.1.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/progress/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/progress)

Updates `@radix-ui/react-radio-group` from 1.3.8 to 1.4.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radio-group/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/radio-group)

Updates `@radix-ui/react-scroll-area` from 1.2.10 to 1.2.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

Updates `@radix-ui/react-select` from 2.2.6 to 2.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-separator` from 1.1.8 to 1.1.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slot` from 1.2.4 to 1.3.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-switch` from 1.2.6 to 1.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-tabs` from 1.1.13 to 1.1.21
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `@radix-ui/react-toast` from 1.2.15 to 1.2.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toast/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toast)

Updates `@radix-ui/react-tooltip` from 1.2.8 to 1.2.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

Updates `@react-email/render` from 2.0.8 to 2.1.0
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/render/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/render@2.1.0/packages/render)

Updates `@simplewebauthn/server` from 13.3.1 to 13.3.3
- [Release notes](https://github.com/MasterKale/SimpleWebAuthn/releases)
- [Changelog](https://github.com/MasterKale/SimpleWebAuthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/MasterKale/SimpleWebAuthn/commits/v13.3.3/packages/server)

Updates `@tanstack/react-query` from 5.100.14 to 5.102.8
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.102.8/packages/react-query)

Updates `@tanstack/react-table` from 8.21.3 to 9.2.4
- [Release notes](https://github.com/TanStack/table/releases)
- [Changelog](https://github.com/TanStack/table/blob/main/packages/react-table/CHANGELOG.md)
- [Commits](https://github.com/TanStack/table/commits/@tanstack/react-table@9.2.4/packages/react-table)

Updates `axios` from 1.18.0 to 1.20.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.18.0...v1.20.0)

Updates `express-rate-limit` from 8.5.2 to 8.7.0
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.5.2...v8.7.0)

Updates `gpt-tokenizer` from 3.4.0 to 4.0.0
- [Release notes](https://github.com/niieani/gpt-tokenizer/releases)
- [Commits](https://github.com/niieani/gpt-tokenizer/compare/3.4.0...4.0.0)

Updates `helmet` from 8.2.0 to 8.3.0
- [Changelog](https://github.com/helmetjs/helmet/blob/main/CHANGELOG.md)
- [Commits](https://github.com/helmetjs/helmet/compare/v8.2.0...v8.3.0)

Updates `input-otp` from 1.4.2 to 1.5.0
- [Release notes](https://github.com/guilhermerodz/input-otp/releases)
- [Changelog](https://github.com/guilhermerodz/input-otp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/guilhermerodz/input-otp/commits/v1.5.0/packages/input-otp)

Updates `ioredis` from 5.11.0 to 6.0.0
- [Release notes](https://github.com/redis/ioredis/releases)
- [Changelog](https://github.com/redis/ioredis/blob/main/CHANGELOG.md)
- [Commits](https://github.com/redis/ioredis/compare/v5.11.0...v6.0.0)

Updates `js-yaml` from 4.3.1 to 5.4.1
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.3.1...5.4.1)

Updates `lucide-react` from 1.17.0 to 1.38.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.38.0/packages/lucide-react)

Updates `maxmind` from 5.0.6 to 5.0.7
- [Release notes](https://github.com/runk/node-maxmind/releases)
- [Commits](https://github.com/runk/node-maxmind/compare/v5.0.6...v5.0.7)

Updates `next` from 16.3.1 to 16.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.1...v16.3.3)

Updates `next-intl` from 4.13.0 to 4.14.1
- [Release notes](https://github.com/amannn/next-intl/releases)
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v4.13.0...v4.14.1)

Updates `nodemailer` from 9.0.1 to 9.1.0
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v9.0.1...v9.1.0)

Updates `pg` from 8.21.0 to 8.23.0
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.23.0/packages/pg)

Updates `@types/pg` from 8.20.0 to 8.23.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pg)

Updates `posthog-node` from 5.35.6 to 5.51.4
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/commits/posthog-node@5.51.4/packages/node)

Updates `react` from 19.2.6 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react)

Updates `@types/react` from 19.2.15 to 19.2.18
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `react-day-picker` from 9.14.0 to 10.0.1
- [Release notes](https://github.com/gpbl/react-day-picker/releases)
- [Changelog](https://github.com/gpbl/react-day-picker/blob/main/packages/react-day-picker/CHANGELOG.md)
- [Commits](https://github.com/gpbl/react-day-picker/commits/v10.0.1/packages/react-day-picker)

Updates `react-dom` from 19.2.6 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react-dom)

Updates `@types/react-dom` from 19.2.3 to 19.2.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `react-hook-form` from 7.76.1 to 7.87.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.76.1...v7.87.0)

Updates `react-icons` from 5.6.0 to 5.7.0
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v5.6.0...v5.7.0)

Updates `recharts` from 3.8.1 to 3.10.1
- [Release notes](https://github.com/recharts/recharts/releases)
- [Changelog](https://github.com/recharts/recharts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/recharts/recharts/compare/v3.8.1...v3.10.1)

Updates `semver` from 7.8.1 to 7.8.5
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.8.1...v7.8.5)

Updates `@types/semver` from 7.7.1 to 7.8.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/semver)

Updates `stripe` from 22.2.0 to 22.6.0
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v22.2.0...v22.6.0)

Updates `uuid` from 14.0.0 to 14.0.2
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v14.0.0...v14.0.2)

Updates `ws` from 8.21.0 to 8.21.3
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.21.0...8.21.3)

Updates `yargs` from 18.0.0 to 18.1.0
- [Release notes](https://github.com/yargs/yargs/releases)
- [Changelog](https://github.com/yargs/yargs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/yargs/yargs/compare/v18.0.0...v18.1.0)

Updates `zod` from 4.4.3 to 4.5.4
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.4.3...v4.5.4)

Updates `@dotenvx/dotenvx` from 1.69.1 to 2.23.0
- [Release notes](https://github.com/dotenvx/dotenvx/releases)
- [Changelog](https://github.com/dotenvx/dotenvx/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dotenvx/dotenvx/compare/v1.69.1...v2.23.0)

Updates `@react-email/ui` from 6.9.2 to 6.9.3
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/ui/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/ui@6.9.3/packages/ui)

Updates `@tailwindcss/postcss` from 4.3.0 to 4.3.3
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/@tailwindcss-postcss)

Updates `@tanstack/react-query-devtools` from 5.100.14 to 5.102.8
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query-devtools/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query-devtools@5.102.8/packages/react-query-devtools)

Updates `@types/node` from 25.9.1 to 26.4.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/nodemailer` from 8.0.0 to 8.0.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/nodemailer)

Updates `@types/pg` from 8.20.0 to 8.23.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pg)

Updates `@types/react` from 19.2.15 to 19.2.18
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/react-dom` from 19.2.3 to 19.2.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `@types/semver` from 7.7.1 to 7.8.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/semver)

Updates `@types/sshpk` from 1.17.4 to 1.17.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/sshpk)

Updates `esbuild` from 0.28.0 to 0.28.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.28.0...v0.28.2)

Updates `esbuild-node-externals` from 1.22.0 to 2.0.0
- [Release notes](https://github.com/pradel/esbuild-node-externals/releases)
- [Commits](https://github.com/pradel/esbuild-node-externals/compare/esbuild-node-externals-v1.22.0...esbuild-node-externals-v2.0.0)

Updates `eslint` from 10.4.0 to 10.9.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.4.0...v10.9.1)

Updates `eslint-config-next` from 16.2.6 to 16.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.3/packages/eslint-config-next)

Updates `postcss` from 8.5.23 to 8.5.26
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.23...8.5.26)

Updates `prettier` from 3.8.3 to 3.9.6
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.3...3.9.6)

Updates `react-email` from 6.5.0 to 6.9.3
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/react-email/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/react-email@6.9.3/packages/react-email)

Updates `tailwindcss` from 4.3.0 to 4.3.3
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/tailwindcss)

Updates `tsc-alias` from 1.8.17 to 1.9.2
- [Release notes](https://github.com/justkey007/tsc-alias/releases)
- [Commits](https://github.com/justkey007/tsc-alias/compare/v1.8.17...v1.9.2)

Updates `tsx` from 4.22.3 to 4.23.13
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.22.3...v4.23.13)

Updates `typescript` from 6.0.3 to 7.0.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v6.0.3...v7.0.2)

Updates `typescript-eslint` from 8.60.0 to 8.68.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.68.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@asteasolutions/zod-to-openapi"
  dependency-version: 9.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1121.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@dotenvx/dotenvx"
  dependency-version: 2.22.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@node-rs/argon2"
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-avatar"
  dependency-version: 1.2.6
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-collapsible"
  dependency-version: 1.1.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-progress"
  dependency-version: 1.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-radio-group"
  dependency-version: 1.4.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.3.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@react-email/render"
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@react-email/ui"
  dependency-version: 6.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@simplewebauthn/server"
  dependency-version: 13.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.102.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@tanstack/react-query-devtools"
  dependency-version: 5.102.8
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@tanstack/react-table"
  dependency-version: 9.2.4
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.4.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@types/nodemailer"
  dependency-version: 8.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/pg"
  dependency-version: 8.23.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@types/pg"
  dependency-version: 8.23.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/semver"
  dependency-version: 7.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@types/semver"
  dependency-version: 7.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@types/sshpk"
  dependency-version: 1.17.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: axios
  dependency-version: 1.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: esbuild
  dependency-version: 0.28.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: esbuild-node-externals
  dependency-version: 2.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: eslint
  dependency-version: 10.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: eslint-config-next
  dependency-version: 16.3.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: express-rate-limit
  dependency-version: 8.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: gpt-tokenizer
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: helmet
  dependency-version: 8.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: input-otp
  dependency-version: 1.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: ioredis
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: js-yaml
  dependency-version: 5.4.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: lucide-react
  dependency-version: 1.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: maxmind
  dependency-version: 5.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: next
  dependency-version: 16.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: next-intl
  dependency-version: 4.14.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: nodemailer
  dependency-version: 9.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: pg
  dependency-version: 8.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: postcss
  dependency-version: 8.5.26
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: posthog-node
  dependency-version: 5.51.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: prettier
  dependency-version: 3.9.6
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: react-day-picker
  dependency-version: 10.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: react-dom
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: react-email
  dependency-version: 6.9.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.86.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-icons
  dependency-version: 5.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: recharts
  dependency-version: 3.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: semver
  dependency-version: 7.8.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: stripe
  dependency-version: 22.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: tsc-alias
  dependency-version: 1.9.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: tsx
  dependency-version: 4.23.12
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.68.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: uuid
  dependency-version: 14.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: ws
  dependency-version: 8.21.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: yargs
  dependency-version: 18.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: zod
  dependency-version: 4.5.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 11:20:13 -04:00
dependabot[bot] 6d83614482 Bump browserslist from 4.28.2 to 4.28.8
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.28.2 to 4.28.8.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.28.2...4.28.8)

---
updated-dependencies:
- dependency-name: browserslist
  dependency-version: 4.28.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 11:20:13 -04:00
dependabot[bot] c319b7a65f Bump qs from 6.15.2 to 6.16.0
Bumps [qs](https://github.com/ljharb/qs) from 6.15.2 to 6.16.0.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.15.2...v6.16.0)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 11:20:12 -04:00
dependabot[bot] aa7fe7ee0e Bump fast-uri from 3.1.5 to 3.1.7
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.5 to 3.1.7.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.5...v3.1.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 11:20:12 -04:00
Owen Schwartz 5fd71df2ae Merge pull request #3672 from fosrl/dependabot/npm_and_yarn/npm-dependencies-6e680b3dcf
Bump the npm-dependencies group across 1 directory with 74 updates
2026-09-03 11:19:47 -04:00
Owen c7e1462e46 Revert tanstak components 2026-09-03 11:16:12 -04:00
Owen cfb73c9e21 Pin ts 2026-09-03 11:11:05 -04:00
dependabot[bot] 72a9040c2e Bump the npm-dependencies group across 1 directory with 74 updates
Bumps the npm-dependencies group with 74 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@asteasolutions/zod-to-openapi](https://github.com/asteasolutions/zod-to-openapi) | `8.5.0` | `9.1.0` |
| [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3) | `3.1056.0` | `3.1121.0` |
| [@hookform/resolvers](https://github.com/react-hook-form/resolvers) | `5.4.0` | `5.9.1` |
| [@node-rs/argon2](https://github.com/napi-rs/node-rs) | `2.0.2` | `2.2.0` |
| [@radix-ui/react-avatar](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/avatar) | `1.1.11` | `1.2.6` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.3` | `1.3.11` |
| [@radix-ui/react-collapsible](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/collapsible) | `1.1.12` | `1.1.20` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.15` | `1.1.23` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.16` | `2.1.24` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.8` | `2.1.15` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.15` | `1.1.23` |
| [@radix-ui/react-progress](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/progress) | `1.1.8` | `1.1.16` |
| [@radix-ui/react-radio-group](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/radio-group) | `1.3.8` | `1.4.7` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.10` | `1.2.18` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.2.6` | `2.3.7` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.8` | `1.1.15` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.2.4` | `1.3.3` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.2.6` | `1.3.7` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.13` | `1.1.21` |
| [@radix-ui/react-toast](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toast) | `1.2.15` | `1.2.23` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.8` | `1.2.16` |
| [@react-email/render](https://github.com/resend/react-email/tree/HEAD/packages/render) | `2.0.8` | `2.1.0` |
| [@simplewebauthn/server](https://github.com/MasterKale/SimpleWebAuthn/tree/HEAD/packages/server) | `13.3.1` | `13.3.3` |
| [@tanstack/react-query](https://github.com/TanStack/query/tree/HEAD/packages/react-query) | `5.100.14` | `5.102.8` |
| [@tanstack/react-table](https://github.com/TanStack/table/tree/HEAD/packages/react-table) | `8.21.3` | `9.2.4` |
| [axios](https://github.com/axios/axios) | `1.18.0` | `1.20.0` |
| [express-rate-limit](https://github.com/express-rate-limit/express-rate-limit) | `8.5.2` | `8.7.0` |
| [gpt-tokenizer](https://github.com/niieani/gpt-tokenizer) | `3.4.0` | `4.0.0` |
| [helmet](https://github.com/helmetjs/helmet) | `8.2.0` | `8.3.0` |
| [input-otp](https://github.com/guilhermerodz/input-otp/tree/HEAD/packages/input-otp) | `1.4.2` | `1.5.0` |
| [ioredis](https://github.com/redis/ioredis) | `5.11.0` | `6.0.0` |
| [js-yaml](https://github.com/nodeca/js-yaml) | `4.3.1` | `5.4.1` |
| [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) | `1.17.0` | `1.38.0` |
| [maxmind](https://github.com/runk/node-maxmind) | `5.0.6` | `5.0.7` |
| [next](https://github.com/vercel/next.js) | `16.3.1` | `16.3.3` |
| [next-intl](https://github.com/amannn/next-intl) | `4.13.0` | `4.14.1` |
| [nodemailer](https://github.com/nodemailer/nodemailer) | `9.0.1` | `9.1.0` |
| [pg](https://github.com/brianc/node-postgres/tree/HEAD/packages/pg) | `8.21.0` | `8.23.0` |
| [@types/pg](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/pg) | `8.20.0` | `8.23.1` |
| [posthog-node](https://github.com/PostHog/posthog-js/tree/HEAD/packages/node) | `5.35.6` | `5.51.4` |
| [react](https://github.com/react/react/tree/HEAD/packages/react) | `19.2.6` | `19.2.8` |
| [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react) | `19.2.15` | `19.2.18` |
| [react-day-picker](https://github.com/gpbl/react-day-picker/tree/HEAD/packages/react-day-picker) | `9.14.0` | `10.0.1` |
| [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom) | `19.2.6` | `19.2.8` |
| [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom) | `19.2.3` | `19.2.5` |
| [react-hook-form](https://github.com/react-hook-form/react-hook-form) | `7.76.1` | `7.87.0` |
| [react-icons](https://github.com/react-icons/react-icons) | `5.6.0` | `5.7.0` |
| [recharts](https://github.com/recharts/recharts) | `3.8.1` | `3.10.1` |
| [semver](https://github.com/npm/node-semver) | `7.8.1` | `7.8.5` |
| [@types/semver](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/semver) | `7.7.1` | `7.8.0` |
| [stripe](https://github.com/stripe/stripe-node) | `22.2.0` | `22.6.0` |
| [uuid](https://github.com/uuidjs/uuid) | `14.0.0` | `14.0.2` |
| [ws](https://github.com/websockets/ws) | `8.21.0` | `8.21.3` |
| [yargs](https://github.com/yargs/yargs) | `18.0.0` | `18.1.0` |
| [zod](https://github.com/colinhacks/zod) | `4.4.3` | `4.5.4` |
| [@dotenvx/dotenvx](https://github.com/dotenvx/dotenvx) | `1.69.1` | `2.23.0` |
| [@react-email/ui](https://github.com/resend/react-email/tree/HEAD/packages/ui) | `6.9.2` | `6.9.3` |
| [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) | `4.3.0` | `4.3.3` |
| [@tanstack/react-query-devtools](https://github.com/TanStack/query/tree/HEAD/packages/react-query-devtools) | `5.100.14` | `5.102.8` |
| [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) | `25.9.1` | `26.4.0` |
| [@types/nodemailer](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/nodemailer) | `8.0.0` | `8.0.1` |
| [@types/sshpk](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/sshpk) | `1.17.4` | `1.17.5` |
| [esbuild](https://github.com/evanw/esbuild) | `0.28.0` | `0.28.2` |
| [esbuild-node-externals](https://github.com/pradel/esbuild-node-externals) | `1.22.0` | `2.0.0` |
| [eslint](https://github.com/eslint/eslint) | `10.4.0` | `10.9.1` |
| [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) | `16.2.6` | `16.3.3` |
| [postcss](https://github.com/postcss/postcss) | `8.5.23` | `8.5.26` |
| [prettier](https://github.com/prettier/prettier) | `3.8.3` | `3.9.6` |
| [react-email](https://github.com/resend/react-email/tree/HEAD/packages/react-email) | `6.5.0` | `6.9.3` |
| [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) | `4.3.0` | `4.3.3` |
| [tsc-alias](https://github.com/justkey007/tsc-alias) | `1.8.17` | `1.9.2` |
| [tsx](https://github.com/privatenumber/tsx) | `4.22.3` | `4.23.13` |
| [typescript](https://github.com/microsoft/TypeScript) | `6.0.3` | `7.0.2` |
| [typescript-eslint](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/typescript-eslint) | `8.60.0` | `8.68.0` |



Updates `@asteasolutions/zod-to-openapi` from 8.5.0 to 9.1.0
- [Release notes](https://github.com/asteasolutions/zod-to-openapi/releases)
- [Commits](https://github.com/asteasolutions/zod-to-openapi/compare/v8.5.0...v9.1.0)

Updates `@aws-sdk/client-s3` from 3.1056.0 to 3.1121.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1121.0/clients/client-s3)

Updates `@hookform/resolvers` from 5.4.0 to 5.9.1
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v5.4.0...v5.9.1)

Updates `@node-rs/argon2` from 2.0.2 to 2.2.0
- [Release notes](https://github.com/napi-rs/node-rs/releases)
- [Commits](https://github.com/napi-rs/node-rs/compare/@node-rs/argon2@2.0.2...@node-rs/argon2@2.2.0)

Updates `@radix-ui/react-avatar` from 1.1.11 to 1.2.6
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/avatar/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/avatar)

Updates `@radix-ui/react-checkbox` from 1.3.3 to 1.3.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

Updates `@radix-ui/react-collapsible` from 1.1.12 to 1.1.20
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/collapsible/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/collapsible)

Updates `@radix-ui/react-dialog` from 1.1.15 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.16 to 2.1.24
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-label` from 2.1.8 to 2.1.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-popover` from 1.1.15 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

Updates `@radix-ui/react-progress` from 1.1.8 to 1.1.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/progress/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/progress)

Updates `@radix-ui/react-radio-group` from 1.3.8 to 1.4.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/radio-group/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/radio-group)

Updates `@radix-ui/react-scroll-area` from 1.2.10 to 1.2.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

Updates `@radix-ui/react-select` from 2.2.6 to 2.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-separator` from 1.1.8 to 1.1.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slot` from 1.2.4 to 1.3.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-switch` from 1.2.6 to 1.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-tabs` from 1.1.13 to 1.1.21
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `@radix-ui/react-toast` from 1.2.15 to 1.2.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toast/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toast)

Updates `@radix-ui/react-tooltip` from 1.2.8 to 1.2.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

Updates `@react-email/render` from 2.0.8 to 2.1.0
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/render/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/render@2.1.0/packages/render)

Updates `@simplewebauthn/server` from 13.3.1 to 13.3.3
- [Release notes](https://github.com/MasterKale/SimpleWebAuthn/releases)
- [Changelog](https://github.com/MasterKale/SimpleWebAuthn/blob/master/CHANGELOG.md)
- [Commits](https://github.com/MasterKale/SimpleWebAuthn/commits/v13.3.3/packages/server)

Updates `@tanstack/react-query` from 5.100.14 to 5.102.8
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query@5.102.8/packages/react-query)

Updates `@tanstack/react-table` from 8.21.3 to 9.2.4
- [Release notes](https://github.com/TanStack/table/releases)
- [Changelog](https://github.com/TanStack/table/blob/main/packages/react-table/CHANGELOG.md)
- [Commits](https://github.com/TanStack/table/commits/@tanstack/react-table@9.2.4/packages/react-table)

Updates `axios` from 1.18.0 to 1.20.0
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.18.0...v1.20.0)

Updates `express-rate-limit` from 8.5.2 to 8.7.0
- [Release notes](https://github.com/express-rate-limit/express-rate-limit/releases)
- [Commits](https://github.com/express-rate-limit/express-rate-limit/compare/v8.5.2...v8.7.0)

Updates `gpt-tokenizer` from 3.4.0 to 4.0.0
- [Release notes](https://github.com/niieani/gpt-tokenizer/releases)
- [Commits](https://github.com/niieani/gpt-tokenizer/compare/3.4.0...4.0.0)

Updates `helmet` from 8.2.0 to 8.3.0
- [Changelog](https://github.com/helmetjs/helmet/blob/main/CHANGELOG.md)
- [Commits](https://github.com/helmetjs/helmet/compare/v8.2.0...v8.3.0)

Updates `input-otp` from 1.4.2 to 1.5.0
- [Release notes](https://github.com/guilhermerodz/input-otp/releases)
- [Changelog](https://github.com/guilhermerodz/input-otp/blob/master/CHANGELOG.md)
- [Commits](https://github.com/guilhermerodz/input-otp/commits/v1.5.0/packages/input-otp)

Updates `ioredis` from 5.11.0 to 6.0.0
- [Release notes](https://github.com/redis/ioredis/releases)
- [Changelog](https://github.com/redis/ioredis/blob/main/CHANGELOG.md)
- [Commits](https://github.com/redis/ioredis/compare/v5.11.0...v6.0.0)

Updates `js-yaml` from 4.3.1 to 5.4.1
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.3.1...5.4.1)

Updates `lucide-react` from 1.17.0 to 1.38.0
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.38.0/packages/lucide-react)

Updates `maxmind` from 5.0.6 to 5.0.7
- [Release notes](https://github.com/runk/node-maxmind/releases)
- [Commits](https://github.com/runk/node-maxmind/compare/v5.0.6...v5.0.7)

Updates `next` from 16.3.1 to 16.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.1...v16.3.3)

Updates `next-intl` from 4.13.0 to 4.14.1
- [Release notes](https://github.com/amannn/next-intl/releases)
- [Changelog](https://github.com/amannn/next-intl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/amannn/next-intl/compare/v4.13.0...v4.14.1)

Updates `nodemailer` from 9.0.1 to 9.1.0
- [Release notes](https://github.com/nodemailer/nodemailer/releases)
- [Changelog](https://github.com/nodemailer/nodemailer/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodemailer/nodemailer/compare/v9.0.1...v9.1.0)

Updates `pg` from 8.21.0 to 8.23.0
- [Changelog](https://github.com/brianc/node-postgres/blob/master/CHANGELOG.md)
- [Commits](https://github.com/brianc/node-postgres/commits/pg@8.23.0/packages/pg)

Updates `@types/pg` from 8.20.0 to 8.23.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pg)

Updates `posthog-node` from 5.35.6 to 5.51.4
- [Release notes](https://github.com/PostHog/posthog-js/releases)
- [Changelog](https://github.com/PostHog/posthog-js/blob/main/packages/node/CHANGELOG.md)
- [Commits](https://github.com/PostHog/posthog-js/commits/posthog-node@5.51.4/packages/node)

Updates `react` from 19.2.6 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react)

Updates `@types/react` from 19.2.15 to 19.2.18
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `react-day-picker` from 9.14.0 to 10.0.1
- [Release notes](https://github.com/gpbl/react-day-picker/releases)
- [Changelog](https://github.com/gpbl/react-day-picker/blob/main/packages/react-day-picker/CHANGELOG.md)
- [Commits](https://github.com/gpbl/react-day-picker/commits/v10.0.1/packages/react-day-picker)

Updates `react-dom` from 19.2.6 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react-dom)

Updates `@types/react-dom` from 19.2.3 to 19.2.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `react-hook-form` from 7.76.1 to 7.87.0
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.76.1...v7.87.0)

Updates `react-icons` from 5.6.0 to 5.7.0
- [Release notes](https://github.com/react-icons/react-icons/releases)
- [Commits](https://github.com/react-icons/react-icons/compare/v5.6.0...v5.7.0)

Updates `recharts` from 3.8.1 to 3.10.1
- [Release notes](https://github.com/recharts/recharts/releases)
- [Changelog](https://github.com/recharts/recharts/blob/main/CHANGELOG.md)
- [Commits](https://github.com/recharts/recharts/compare/v3.8.1...v3.10.1)

Updates `semver` from 7.8.1 to 7.8.5
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/main/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v7.8.1...v7.8.5)

Updates `@types/semver` from 7.7.1 to 7.8.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/semver)

Updates `stripe` from 22.2.0 to 22.6.0
- [Release notes](https://github.com/stripe/stripe-node/releases)
- [Changelog](https://github.com/stripe/stripe-node/blob/master/CHANGELOG.md)
- [Commits](https://github.com/stripe/stripe-node/compare/v22.2.0...v22.6.0)

Updates `uuid` from 14.0.0 to 14.0.2
- [Release notes](https://github.com/uuidjs/uuid/releases)
- [Changelog](https://github.com/uuidjs/uuid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/uuidjs/uuid/compare/v14.0.0...v14.0.2)

Updates `ws` from 8.21.0 to 8.21.3
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.21.0...8.21.3)

Updates `yargs` from 18.0.0 to 18.1.0
- [Release notes](https://github.com/yargs/yargs/releases)
- [Changelog](https://github.com/yargs/yargs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/yargs/yargs/compare/v18.0.0...v18.1.0)

Updates `zod` from 4.4.3 to 4.5.4
- [Release notes](https://github.com/colinhacks/zod/releases)
- [Commits](https://github.com/colinhacks/zod/compare/v4.4.3...v4.5.4)

Updates `@dotenvx/dotenvx` from 1.69.1 to 2.23.0
- [Release notes](https://github.com/dotenvx/dotenvx/releases)
- [Changelog](https://github.com/dotenvx/dotenvx/blob/main/CHANGELOG.md)
- [Commits](https://github.com/dotenvx/dotenvx/compare/v1.69.1...v2.23.0)

Updates `@react-email/ui` from 6.9.2 to 6.9.3
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/ui/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/ui@6.9.3/packages/ui)

Updates `@tailwindcss/postcss` from 4.3.0 to 4.3.3
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/@tailwindcss-postcss)

Updates `@tanstack/react-query-devtools` from 5.100.14 to 5.102.8
- [Release notes](https://github.com/TanStack/query/releases)
- [Changelog](https://github.com/TanStack/query/blob/main/packages/react-query-devtools/CHANGELOG.md)
- [Commits](https://github.com/TanStack/query/commits/@tanstack/react-query-devtools@5.102.8/packages/react-query-devtools)

Updates `@types/node` from 25.9.1 to 26.4.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

Updates `@types/nodemailer` from 8.0.0 to 8.0.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/nodemailer)

Updates `@types/pg` from 8.20.0 to 8.23.1
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/pg)

Updates `@types/react` from 19.2.15 to 19.2.18
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `@types/react-dom` from 19.2.3 to 19.2.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `@types/semver` from 7.7.1 to 7.8.0
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/semver)

Updates `@types/sshpk` from 1.17.4 to 1.17.5
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/sshpk)

Updates `esbuild` from 0.28.0 to 0.28.2
- [Release notes](https://github.com/evanw/esbuild/releases)
- [Changelog](https://github.com/evanw/esbuild/blob/main/CHANGELOG.md)
- [Commits](https://github.com/evanw/esbuild/compare/v0.28.0...v0.28.2)

Updates `esbuild-node-externals` from 1.22.0 to 2.0.0
- [Release notes](https://github.com/pradel/esbuild-node-externals/releases)
- [Commits](https://github.com/pradel/esbuild-node-externals/compare/esbuild-node-externals-v1.22.0...esbuild-node-externals-v2.0.0)

Updates `eslint` from 10.4.0 to 10.9.1
- [Release notes](https://github.com/eslint/eslint/releases)
- [Commits](https://github.com/eslint/eslint/compare/v10.4.0...v10.9.1)

Updates `eslint-config-next` from 16.2.6 to 16.3.3
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.3/packages/eslint-config-next)

Updates `postcss` from 8.5.23 to 8.5.26
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.23...8.5.26)

Updates `prettier` from 3.8.3 to 3.9.6
- [Release notes](https://github.com/prettier/prettier/releases)
- [Changelog](https://github.com/prettier/prettier/blob/main/CHANGELOG.md)
- [Commits](https://github.com/prettier/prettier/compare/3.8.3...3.9.6)

Updates `react-email` from 6.5.0 to 6.9.3
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/react-email/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/react-email@6.9.3/packages/react-email)

Updates `tailwindcss` from 4.3.0 to 4.3.3
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/tailwindcss)

Updates `tsc-alias` from 1.8.17 to 1.9.2
- [Release notes](https://github.com/justkey007/tsc-alias/releases)
- [Commits](https://github.com/justkey007/tsc-alias/compare/v1.8.17...v1.9.2)

Updates `tsx` from 4.22.3 to 4.23.13
- [Release notes](https://github.com/privatenumber/tsx/releases)
- [Changelog](https://github.com/privatenumber/tsx/blob/master/release.config.cjs)
- [Commits](https://github.com/privatenumber/tsx/compare/v4.22.3...v4.23.13)

Updates `typescript` from 6.0.3 to 7.0.2
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v6.0.3...v7.0.2)

Updates `typescript-eslint` from 8.60.0 to 8.68.0
- [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases)
- [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/main/packages/typescript-eslint/CHANGELOG.md)
- [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v8.68.0/packages/typescript-eslint)

---
updated-dependencies:
- dependency-name: "@asteasolutions/zod-to-openapi"
  dependency-version: 9.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1121.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@dotenvx/dotenvx"
  dependency-version: 2.22.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.9.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@node-rs/argon2"
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-avatar"
  dependency-version: 1.2.6
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-collapsible"
  dependency-version: 1.1.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-progress"
  dependency-version: 1.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-radio-group"
  dependency-version: 1.4.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.3.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@react-email/render"
  dependency-version: 2.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@react-email/ui"
  dependency-version: 6.9.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@simplewebauthn/server"
  dependency-version: 13.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@tanstack/react-query"
  dependency-version: 5.102.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@tanstack/react-query-devtools"
  dependency-version: 5.102.8
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@tanstack/react-table"
  dependency-version: 9.2.4
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@types/node"
  dependency-version: 26.4.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: "@types/nodemailer"
  dependency-version: 8.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/pg"
  dependency-version: 8.23.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@types/pg"
  dependency-version: 8.23.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/react"
  dependency-version: 19.2.18
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: "@types/semver"
  dependency-version: 7.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@types/semver"
  dependency-version: 7.8.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: "@types/sshpk"
  dependency-version: 1.17.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: axios
  dependency-version: 1.20.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: esbuild
  dependency-version: 0.28.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: esbuild-node-externals
  dependency-version: 2.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: eslint
  dependency-version: 10.9.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: eslint-config-next
  dependency-version: 16.3.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: express-rate-limit
  dependency-version: 8.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: gpt-tokenizer
  dependency-version: 4.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: helmet
  dependency-version: 8.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: input-otp
  dependency-version: 1.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: ioredis
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: js-yaml
  dependency-version: 5.4.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: lucide-react
  dependency-version: 1.35.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: maxmind
  dependency-version: 5.0.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: next
  dependency-version: 16.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: next-intl
  dependency-version: 4.14.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: nodemailer
  dependency-version: 9.0.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: pg
  dependency-version: 8.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: postcss
  dependency-version: 8.5.26
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: posthog-node
  dependency-version: 5.51.4
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: prettier
  dependency-version: 3.9.6
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: react-day-picker
  dependency-version: 10.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: react-dom
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: react-email
  dependency-version: 6.9.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-hook-form
  dependency-version: 7.86.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: react-icons
  dependency-version: 5.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: recharts
  dependency-version: 3.10.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: semver
  dependency-version: 7.8.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: stripe
  dependency-version: 22.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: tailwindcss
  dependency-version: 4.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: tsc-alias
  dependency-version: 1.9.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: tsx
  dependency-version: 4.23.12
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: npm-dependencies
- dependency-name: typescript-eslint
  dependency-version: 8.68.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: uuid
  dependency-version: 14.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: ws
  dependency-version: 8.21.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: npm-dependencies
- dependency-name: yargs
  dependency-version: 18.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
- dependency-name: zod
  dependency-version: 4.5.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: npm-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 14:58:30 +00:00
Owen Schwartz ce53b8c73d Merge pull request #3687 from fosrl/dependabot/npm_and_yarn/browserslist-4.28.8
Bump browserslist from 4.28.2 to 4.28.8
2026-09-03 10:52:28 -04:00
Owen Schwartz 6ff2b3e2fe Merge pull request #3688 from fosrl/crowdin_dev
New Crowdin updates
2026-09-03 10:52:08 -04:00
Owen 4dada38cb0 Please eslint 2026-09-03 10:51:46 -04:00
Owen Schwartz 732ea034f8 New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-09-03 10:48:02 -04:00
Owen Schwartz 581137c486 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-09-03 10:48:00 -04:00
Owen Schwartz d75c6da3be New translations en-us.json (Turkish)
[ci skip]
2026-09-03 10:47:57 -04:00
Owen Schwartz 11b51202b8 New translations en-us.json (Russian)
[ci skip]
2026-09-03 10:47:55 -04:00
Owen Schwartz c35de18b9b New translations en-us.json (Portuguese)
[ci skip]
2026-09-03 10:47:53 -04:00
Owen Schwartz 353273a9f2 New translations en-us.json (Polish)
[ci skip]
2026-09-03 10:47:50 -04:00
Owen Schwartz 7c20a292e1 New translations en-us.json (Dutch)
[ci skip]
2026-09-03 10:47:48 -04:00
Owen Schwartz 6c13d6e343 New translations en-us.json (Korean)
[ci skip]
2026-09-03 10:47:46 -04:00
Owen Schwartz e641a8ce1b New translations en-us.json (Italian)
[ci skip]
2026-09-03 10:47:44 -04:00
Owen Schwartz a3419f60d6 New translations en-us.json (German)
[ci skip]
2026-09-03 10:47:41 -04:00
Owen Schwartz 2db3051a1a New translations en-us.json (Danish)
[ci skip]
2026-09-03 10:47:39 -04:00
Owen Schwartz 3c49b6f3d6 New translations en-us.json (Czech)
[ci skip]
2026-09-03 10:47:37 -04:00
Owen Schwartz 9ad4e36fa9 New translations en-us.json (Bulgarian)
[ci skip]
2026-09-03 10:47:34 -04:00
Owen Schwartz c489ed5724 New translations en-us.json (Spanish)
[ci skip]
2026-09-03 10:47:32 -04:00
Owen Schwartz fca3a1e5fa New translations en-us.json (French)
[ci skip]
2026-09-03 10:47:30 -04:00
dependabot[bot] b29348d004 Bump browserslist from 4.28.2 to 4.28.8
Bumps [browserslist](https://github.com/browserslist/browserslist) from 4.28.2 to 4.28.8.
- [Release notes](https://github.com/browserslist/browserslist/releases)
- [Changelog](https://github.com/browserslist/browserslist/blob/main/CHANGELOG.md)
- [Commits](https://github.com/browserslist/browserslist/compare/4.28.2...4.28.8)

---
updated-dependencies:
- dependency-name: browserslist
  dependency-version: 4.28.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 14:47:29 +00:00
Owen Schwartz 337d9a33a3 Merge pull request #3684 from fosrl/dependabot/npm_and_yarn/qs-6.16.0
Bump qs from 6.15.2 to 6.16.0
2026-09-03 10:46:26 -04:00
Owen Schwartz c61e96b1f0 Merge pull request #3682 from fosrl/dependabot/npm_and_yarn/fast-uri-3.1.7
Bump fast-uri from 3.1.5 to 3.1.7
2026-09-03 10:46:16 -04:00
dependabot[bot] 35bc692892 Bump qs from 6.15.2 to 6.16.0
Bumps [qs](https://github.com/ljharb/qs) from 6.15.2 to 6.16.0.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.15.2...v6.16.0)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.16.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 06:50:13 +00:00
dependabot[bot] 714e7e0ba4 Bump fast-uri from 3.1.5 to 3.1.7
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.5 to 3.1.7.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.5...v3.1.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-09-03 01:36:44 +00:00
Owen 49d5b0ec34 Increase maxRetriesPerRequest for Redis connections to improve resilience 2026-09-02 11:57:50 -04:00
Owen e0937a3afa Add validation for health check hostname
Fixes #3677
2026-09-02 10:42:59 -04:00
Owen 8d7e73afa8 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2026-09-02 10:32:24 -04:00
Owen a5ce56ea89 Move the session logs to private where it should be 2026-09-01 17:56:36 -04:00
miloschwartz 2f013335f9 make user lookup deterministic in blueprints by sorting 2026-09-01 15:36:39 -04:00
miloschwartz b4f6ae74d7 show warning when changing idp identifier mapping 2026-09-01 11:38:53 -04:00
Owen f1711ee0b0 Make it more clear that its a prefer 2026-09-01 10:09:16 -04:00
Owen 780d767a65 Merge branch 'main' into dev 2026-08-31 17:07:56 -04:00
Owen Schwartz d6d923e972 Merge pull request #3628 from aithal007/perf/add-fk-indexes
perf: add secondary FK indexes to core OSS schemas
2026-08-31 17:05:04 -04:00
Parikshith 41139f2fd0 perf: add secondary FK indexes to core OSS schemas
Add 13 secondary indexes across 10 tables in the SQLite schema and
5 missing indexes to the PostgreSQL schema.

All list endpoints (listSites, listResources, listClients, listRoles,
listTargets, listUserOrgs) filter and join on these FK columns. In the
SQLite schema, no secondary indexes existed at all on these foreign keys,
forcing full sequential scans on every paginated request and session lookup.

SQLite changes (13 new indexes):
  - sites.orgId
  - resources.orgId
  - targets.resourceId, targets.siteId
  - newt.siteId
  - clients.orgId, clients.userId
  - labels.orgId
  - olms.userId
  - session.userId
  - userOrgs.userId, userOrgs.orgId
  - roles.orgId

PG changes (5 new indexes, rest already present):
  - labels.orgId
  - session.userId
  - userOrgs.userId, userOrgs.orgId
  - roles.orgId
  - olms.userId
2026-08-31 17:04:01 -04:00
Owen Schwartz ebcdeab414 Merge pull request #3597 from shleeable/patch-11
Fix: License.check can fail silently
2026-08-31 17:02:53 -04:00
Owen 2cc7d03ace Update to fall back but still use cache 2026-08-31 17:02:03 -04:00
Owen Schwartz c36cf698c1 Merge pull request #3627 from fosrl/feat/ip-filtering
feat: Add IP column filtering to tables
2026-08-31 16:59:34 -04:00
Shlee 34671c6b13 Update license.ts 2026-08-31 16:57:36 -04:00
Owen Schwartz 8dfc95347f Merge pull request #3655 from moverq1337/fix/access-token-delete-root-key
Fix access token deletion for root API keys
2026-08-31 16:54:18 -04:00
Owen Schwartz 22db0319c2 Merge pull request #3654 from iMord0/crowdsec-v1.7.1
Crowdsec v1.7.1
2026-08-31 16:50:59 -04:00
miloschwartz 39722d30af resolve security-key login only for a unique internal user 2026-08-31 14:39:36 -04:00
miloschwartz 8aef14cf9f harden initial server-admin setup against duplicate users and races 2026-08-31 12:36:42 -04:00
Owen Schwartz 0dece5fef1 Merge pull request #3665 from fosrl/dev
Fix domain namespaces and country is not in blueprints
2026-08-31 11:03:06 -04:00
Owen e7f38c089f Merge branch 'main' into dev 2026-08-31 11:02:01 -04:00
miloschwartz dd0a5a359a check for namespace domain before blocking org check 2026-08-31 10:59:50 -04:00
moverq1337 1650ece0c3 Fix root API key access in verifyApiKeyAccessTokenAccess 2026-08-28 22:45:31 +03:00
iMord0 7f94d99455 Update CrowdSec plugin version
Updated CrowdSec plugin version from v1.4.4 to v1.7.1
2026-08-28 20:51:51 +02:00
iMord0 1f9e99219d Updated CrowdSec plugin version
Updated CrowdSec plugin version from v1.4.4 to v1.7.8
2026-08-28 20:42:26 +02:00
Owen Schwartz bc56a2bed0 Merge pull request #3647 from argueta-xyz/country-is-not-rule-via-blueprint
Allow COUNTRY_IS_NOT rules to be created via Blueprints
2026-08-28 10:52:24 -04:00
Owen Schwartz 49dcc590ce Merge pull request #3651 from fosrl/dev
Update readme
2026-08-28 10:19:58 -04:00
Owen da3e3ff33f Fix typo 2026-08-28 10:19:35 -04:00
Owen Schwartz 69d539f107 Merge pull request #3650 from fosrl/dev
Update readme, tel, and fix EE feature flag
2026-08-28 09:55:40 -04:00
Owen 872e0f9ae1 Merge branch 'main' into dev 2026-08-28 09:54:46 -04:00
Owen 5b3713a72f Update readme 2026-08-28 09:54:07 -04:00
Owen Schwartz f02be1fdbf Merge pull request #3649 from fosrl/dependabot/npm_and_yarn/multi-2e40a8c091
Bump ws and socket.io-adapter
2026-08-28 09:21:25 -04:00
dependabot[bot] 0bf04cf0cd Bump ws and socket.io-adapter
Bumps [ws](https://github.com/websockets/ws) and [socket.io-adapter](https://github.com/socketio/socket.io). These dependencies needed to be updated together.

Updates `ws` from 8.18.3 to 8.21.0
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.18.3...8.21.0)

Updates `socket.io-adapter` from 2.5.6 to 2.5.8
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-adapter@2.5.6...socket.io-adapter@2.5.8)

---
updated-dependencies:
- dependency-name: ws
  dependency-version: 8.21.0
  dependency-type: indirect
- dependency-name: socket.io-adapter
  dependency-version: 2.5.8
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-28 13:12:35 +00:00
Alejandro Argueta 7cda28d685 Update RuleSchema to allow COUNTRY_IS_NOT rules to be created via blueprints as well. 2026-08-27 14:18:48 -07:00
Owen 60bc74c4df Add AI provider and usage metrics to telemetry data collection 2026-08-27 16:54:04 -04:00
Owen 48ab6c501f Fix #3646 2026-08-27 16:37:53 -04:00
Milo Schwartz 7a95e543d8 Merge pull request #3645 from fosrl/dev
Dev
2026-08-27 14:53:42 -04:00
Owen b87b7c7e80 Fix timestamp handling in AI session logs to use seconds instead of milliseconds 2026-08-27 14:26:47 -04:00
miloschwartz a47a68d8e1 update badger version 2026-08-27 11:40:46 -04:00
Owen Schwartz ed0d6fb6b9 Merge pull request #3644 from fosrl/dev
1.22.0-s.2
2026-08-26 11:30:38 -04:00
Owen a02d16fd58 Handle compression of ai session logs 2026-08-26 11:28:14 -04:00
Owen 331fee24d4 Make the default session storage 0 2026-08-26 11:02:39 -04:00
Owen Schwartz 5bdb12dafe Merge pull request #3643 from fosrl/dev
1.22.0-s.1
2026-08-26 10:53:37 -04:00
Owen e57826d6e0 Session logs and usage logs should use seconds not ms 2026-08-26 10:50:16 -04:00
miloschwartz 3d4e143c1f normalize key in rate limiters 2026-08-26 10:25:29 -04:00
miloschwartz 10a25c184d fix get /site-resource/:siteResourceId always returning 400 2026-08-26 10:03:39 -04:00
miloschwartz 906099d1e1 fix org domain access check so unmapped domains are rejected 2026-08-26 09:55:40 -04:00
miloschwartz 9a5824900d strip encrypted ssh key from org response 2026-08-26 09:39:05 -04:00
Owen Schwartz f3474dac98 Merge pull request #3641 from fosrl/dev
1.22.0
2026-08-25 17:19:46 -04:00
Owen 72d2c79793 Add migration for streaming table 2026-08-25 10:42:56 -04:00
Owen 23764feb4f Move the messaging out of the transaction 2026-08-25 09:27:02 -04:00
Owen Schwartz d2809fbfd1 Merge pull request #3637 from fosrl/fix/generic-oidc-icons
Fix: Use variant instead of type only for the Org login IDPs
2026-08-24 16:46:38 -04:00
Fred KISSIE 2d18db3597 🐛 fix loginIdps using variant instead of type only 2026-08-24 22:32:40 +02:00
Fred KISSIE d00b9478a2 Merge branch 'dev' into feat/ip-filtering 2026-08-24 22:06:55 +02:00
Fred KISSIE 4ddf36ebcc 💄 some last UI fixes 2026-08-24 20:39:30 +02:00
Owen 77cab56fa9 Add valueFormatter prop to ToggleableTrendChart and ChartTooltipContent so we see more decimals 2026-08-24 14:39:29 -04:00
miloschwartz 2051e5df37 fix syntax 2026-08-24 14:32:52 -04:00
Owen 26f9026621 Fix width too big by adding min-width constraints 2026-08-24 14:31:17 -04:00
Owen 85b40b7164 Fix expanded row display issue on page change in LogDataTable 2026-08-24 14:05:31 -04:00
Owen 753cbd45d0 Add AI disclosure request 2026-08-24 11:55:39 -04:00
Owen 1c2fe44c54 Fix #2648 2026-08-24 11:43:28 -04:00
Owen 5b782a842c Fix #2937 2026-08-24 11:42:35 -04:00
Owen 935410b15e Fixes #2612 2026-08-24 11:34:47 -04:00
Owen Schwartz 89066aa5b1 Merge pull request #3636 from fosrl/crowdin_dev
New Crowdin updates
2026-08-24 11:30:01 -04:00
Owen Schwartz ddf89d0afa New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-08-24 11:23:51 -04:00
Owen Schwartz e4ec6f7cbe New translations en-us.json (Chinese Simplified)
[ci skip]
2026-08-24 11:23:49 -04:00
Owen Schwartz c1e5769003 New translations en-us.json (Turkish)
[ci skip]
2026-08-24 11:23:46 -04:00
Owen Schwartz 4b31326b34 New translations en-us.json (Russian)
[ci skip]
2026-08-24 11:23:44 -04:00
Owen Schwartz 10d2c6438b New translations en-us.json (Portuguese)
[ci skip]
2026-08-24 11:23:42 -04:00
Owen Schwartz c3140c5da3 New translations en-us.json (Polish)
[ci skip]
2026-08-24 11:23:40 -04:00
Owen Schwartz 0f00a2337d New translations en-us.json (Dutch)
[ci skip]
2026-08-24 11:23:38 -04:00
Owen Schwartz 1831b1af58 New translations en-us.json (Korean)
[ci skip]
2026-08-24 11:23:36 -04:00
Owen Schwartz 85257b941b New translations en-us.json (Italian)
[ci skip]
2026-08-24 11:23:34 -04:00
Owen Schwartz 4aa43fd14d New translations en-us.json (German)
[ci skip]
2026-08-24 11:23:32 -04:00
Owen Schwartz 90a77ee450 New translations en-us.json (Danish)
[ci skip]
2026-08-24 11:23:30 -04:00
Owen Schwartz d237d6545e New translations en-us.json (Czech)
[ci skip]
2026-08-24 11:23:28 -04:00
Owen Schwartz 262aaa2756 New translations en-us.json (Bulgarian)
[ci skip]
2026-08-24 11:23:26 -04:00
Owen Schwartz 547ac2284a New translations en-us.json (Spanish)
[ci skip]
2026-08-24 11:23:24 -04:00
Owen Schwartz 7d2af1837e New translations en-us.json (French)
[ci skip]
2026-08-24 11:23:21 -04:00
Owen 8ae42e1852 Fix list users not respecting policy
Fixes #3632
2026-08-24 11:18:09 -04:00
Owen Schwartz 7319bf84f7 Merge pull request #3481 from ThanatosDi/feat/zh-tw-for-1.21.0
feat: translate zh-tw for 1.21.0
2026-08-24 11:04:08 -04:00
Owen Schwartz 9ec9908ed7 Merge pull request #3509 from shubhamsinnh/codex/fix-zh-tw-language-detection
Fix regional locale detection
2026-08-24 11:03:09 -04:00
Owen 9b0e049a21 Scope exit node creation to orgs 2026-08-24 10:44:54 -04:00
Fred KISSIE 28b32fe6f7 🏷️ fix types 2026-08-21 23:26:18 +02:00
Fred KISSIE adfb6003d9 Merge branch 'dev' into feat/ip-filtering 2026-08-21 23:24:09 +02:00
Owen 19ce236262 Support AI session log streaming 2026-08-21 17:22:00 -04:00
Fred KISSIE 6a5ecab013 Implement IP filtering for admin access logs 2026-08-21 22:49:45 +02:00
Owen eca1c9044c Rename siteResourceId to resourceId 2026-08-21 15:24:20 -04:00
Fred KISSIE 2c197fab9f IP filtering on request log table finished 2026-08-21 21:04:40 +02:00
Owen 4229ef9173 Change log to warn 2026-08-21 14:03:36 -04:00
Owen d5ea0ecbc1 Include user in the request logs 2026-08-21 13:50:56 -04:00
Owen 59f286299e Move the request log to be public 2026-08-21 13:41:45 -04:00
Owen 442cefda84 Dont allow clients to connect to remote nodes quite yet 2026-08-21 12:25:14 -04:00
Owen e3e1508e8a Handle warning and no routing to remote exit nodes for ai providers 2026-08-21 12:09:22 -04:00
Owen 2e87927b83 Remove unused use_subdomain 2026-08-21 10:21:21 -04:00
Fred KISSIE 65e4fe91b9 🚧 wip: add ip is column filter 2026-08-20 23:59:31 +02:00
Owen e65a79cc48 Rename to v1_models and use with openai as well 2026-08-20 16:01:07 -04:00
Owen 365a905e69 Add more data to the models catalog list 2026-08-20 15:17:48 -04:00
Owen bafbf6e096 Add anthropic_models capability 2026-08-20 14:28:19 -04:00
Owen df7e26a444 Show required key when nessicary for private resources 2026-08-20 11:38:47 -04:00
Owen c1051db4a5 Add gemini as a client option 2026-08-20 10:28:25 -04:00
Owen c1caa30cb9 Move session logs to private 2026-08-19 17:25:32 -04:00
Milo Schwartz fd0a0818c1 Merge pull request #3617 from fosrl/dev
Dev
2026-08-19 16:41:48 -04:00
miloschwartz 887e13888d redirect differently for org auth domain 2026-08-19 16:26:55 -04:00
Owen 114592add8 Show a warning if the logs are disabled to reduce confusion 2026-08-19 16:16:05 -04:00
Owen ef051c2f8c Dont count usage when the response was not successful 2026-08-19 15:59:01 -04:00
miloschwartz 5c6da72ec1 remove cursor 2026-08-19 15:46:56 -04:00
Owen acd64a049f Show that opencode has providers warning 2026-08-19 15:45:40 -04:00
Owen 967deaff88 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2026-08-19 15:07:37 -04:00
Owen ef8985b0af Set the parser limit for the gateway because AI messages can be pretty big 2026-08-19 15:07:29 -04:00
miloschwartz 1f80bdd361 change default model for cursor config 2026-08-19 14:25:59 -04:00
Milo Schwartz 929acc5b1c Merge pull request #3614 from fosrl/dev
Dev
2026-08-19 12:05:12 -04:00
Owen 437ec50019 Update the usage records to use logs db 2026-08-19 12:03:30 -04:00
miloschwartz a7bbafd2f0 bump badger version 2026-08-19 12:01:01 -04:00
Owen 6824ec6e3e Clean up empty execute in migration 2026-08-19 11:51:12 -04:00
Milo Schwartz 71348f45b2 Merge pull request #3611 from fosrl/dev
1.22.0
2026-08-19 11:26:08 -04:00
Owen Schwartz 664b7d52af Merge pull request #3610 from fosrl/crowdin_dev
New Crowdin updates
2026-08-19 11:14:26 -04:00
Owen Schwartz fb6f9c91c6 New translations en-us.json (French)
[ci skip]
2026-08-19 11:12:39 -04:00
Owen Schwartz 051c873fdf New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-08-19 11:12:37 -04:00
Owen Schwartz 12224458e7 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-08-19 11:12:36 -04:00
Owen Schwartz b6719d2a3e New translations en-us.json (Turkish)
[ci skip]
2026-08-19 11:12:34 -04:00
Owen Schwartz 7843b56f39 New translations en-us.json (Russian)
[ci skip]
2026-08-19 11:12:32 -04:00
Owen Schwartz 19be6c4b3b New translations en-us.json (Portuguese)
[ci skip]
2026-08-19 11:12:30 -04:00
Owen Schwartz 4a283710ad New translations en-us.json (Polish)
[ci skip]
2026-08-19 11:12:28 -04:00
Owen Schwartz 1af7c5ee73 New translations en-us.json (Dutch)
[ci skip]
2026-08-19 11:12:27 -04:00
Owen Schwartz 77dd47fa8a New translations en-us.json (Korean)
[ci skip]
2026-08-19 11:12:25 -04:00
Owen Schwartz ccfe91f08d New translations en-us.json (Italian)
[ci skip]
2026-08-19 11:12:23 -04:00
Owen Schwartz 624fb8322f New translations en-us.json (German)
[ci skip]
2026-08-19 11:12:21 -04:00
Owen Schwartz 6bc3242a29 New translations en-us.json (Danish)
[ci skip]
2026-08-19 11:12:20 -04:00
Owen Schwartz 33f528ef79 New translations en-us.json (Czech)
[ci skip]
2026-08-19 11:12:18 -04:00
Owen Schwartz 6c1afc4453 New translations en-us.json (Bulgarian)
[ci skip]
2026-08-19 11:12:16 -04:00
Owen Schwartz 2a14447f8c New translations en-us.json (Spanish)
[ci skip]
2026-08-19 11:12:14 -04:00
miloschwartz ad50f7473a adjust translation 2026-08-19 10:53:30 -04:00
miloschwartz 5f1218f7b1 show placeholder in logs for nullable fields 2026-08-19 10:51:21 -04:00
miloschwartz e377afe9f6 add id sorting to http request logs 2026-08-19 10:42:23 -04:00
Milo Schwartz 21eb4d2876 Merge pull request #3598 from shleeable/patch-12
Fix: ensuring only internal users can request password resets.
2026-08-19 10:35:52 -04:00
Milo Schwartz bb824a7070 Merge pull request #3541 from fosrl/feat/test-alert-button
Feat: test alert rules
2026-08-19 10:33:16 -04:00
Milo Schwartz f98de98288 Merge pull request #3527 from fosrl/feat/log-audit-fails
Fix: log access depends on the wrong org log retention settings field
2026-08-19 10:29:04 -04:00
miloschwartz 350de6ad2e add alert warning for configure coding agents 2026-08-19 10:25:06 -04:00
Owen dc8e724482 Fix issue with subnets in migrations 2026-08-19 09:30:02 -04:00
miloschwartz fb5b564c00 improve expanded client config sections 2026-08-18 17:43:11 -04:00
Fred KISSIE 52c078a489 💄 ui 2026-08-18 23:28:31 +02:00
Owen Schwartz 18270381c1 Merge pull request #3607 from fosrl/dependabot/npm_and_yarn/multi-ff8097116b
Bump sharp, next and @react-email/ui
2026-08-18 17:21:25 -04:00
Owen b32dc30050 Also look for niceId 2026-08-18 17:18:25 -04:00
Owen 667804043c Implement cli commands to configure ai clients 2026-08-18 17:12:01 -04:00
Owen 3fd3d90c10 add the logos 2026-08-18 17:12:01 -04:00
miloschwartz 6e3619fb1a fix mobile responsiveness issues for charts 2026-08-18 16:27:33 -04:00
miloschwartz 987c0e992b make budgets input mobile responsive 2026-08-18 16:11:49 -04:00
miloschwartz bdd7f40688 batch identity key emails 2026-08-18 16:00:07 -04:00
Owen 809bc662e0 Update aiClientConfig 2026-08-18 15:38:23 -04:00
Fred KISSIE 195f67c6eb 💄 QoL for location column 2026-08-18 21:08:16 +02:00
Owen a74e1e765a Pass 3 - add commands, remove run, formatting, mobile view 2026-08-18 14:59:29 -04:00
Owen c356af3eda Fix spacing issue 2026-08-18 14:59:29 -04:00
Owen a4c7121b93 Pass 2 showing the usage commands 2026-08-18 14:59:29 -04:00
Owen 2a3c00045f Pass 1 of the config instructions 2026-08-18 14:59:28 -04:00
dependabot[bot] 8e938a2723 Bump sharp, next and @react-email/ui
Bumps [sharp](https://github.com/lovell/sharp) to 0.35.3 and updates ancestor dependencies [sharp](https://github.com/lovell/sharp), [next](https://github.com/vercel/next.js) and [@react-email/ui](https://github.com/resend/react-email/tree/HEAD/packages/ui). These dependencies need to be updated together.


Updates `sharp` from 0.34.5 to 0.35.3
- [Release notes](https://github.com/lovell/sharp/releases)
- [Commits](https://github.com/lovell/sharp/compare/v0.34.5...v0.35.3)

Updates `next` from 16.2.11 to 16.3.1
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.2.11...v16.3.1)

Updates `@react-email/ui` from 6.5.0 to 6.9.2
- [Release notes](https://github.com/resend/react-email/releases)
- [Changelog](https://github.com/resend/react-email/blob/canary/packages/ui/CHANGELOG.md)
- [Commits](https://github.com/resend/react-email/commits/@react-email/ui@6.9.2/packages/ui)

---
updated-dependencies:
- dependency-name: sharp
  dependency-version: 0.35.3
  dependency-type: indirect
- dependency-name: next
  dependency-version: 16.3.1
  dependency-type: direct:production
- dependency-name: "@react-email/ui"
  dependency-version: 6.9.2
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-18 16:26:28 +00:00
Owen Schwartz 197f8f7ba5 Merge pull request #3503 from fosrl/dependabot/npm_and_yarn/engine.io-6.6.9
Bump engine.io from 6.6.7 to 6.6.9
2026-08-18 12:25:27 -04:00
Owen Schwartz 10b528642d Merge pull request #3517 from fosrl/dependabot/github_actions/github-actions-dependencies-7eb9e48ea9
Bump the github-actions-dependencies group across 1 directory with 2 updates
2026-08-18 12:24:54 -04:00
Owen Schwartz 60d6fff085 Merge pull request #3525 from fosrl/dependabot/npm_and_yarn/ip-address-10.4.0
Bump ip-address from 10.2.0 to 10.4.0
2026-08-18 12:24:30 -04:00
Owen Schwartz c664b3da91 Merge pull request #3526 from fosrl/dependabot/npm_and_yarn/multi-5e81c1b34f
Bump brace-expansion
2026-08-18 12:24:17 -04:00
Owen Schwartz 492282e758 Merge pull request #3530 from fosrl/dependabot/npm_and_yarn/socket.io-parser-4.2.7
Bump socket.io-parser from 4.2.6 to 4.2.7
2026-08-18 12:24:03 -04:00
Owen Schwartz 47f4aefc25 Merge pull request #3531 from fosrl/dependabot/npm_and_yarn/fast-uri-3.1.5
Bump fast-uri from 3.1.4 to 3.1.5
2026-08-18 12:23:43 -04:00
Owen Schwartz 7c0ff9ede7 Merge pull request #3569 from fosrl/dependabot/npm_and_yarn/js-yaml-4.3.1
Bump js-yaml from 4.3.0 to 4.3.1
2026-08-18 12:23:32 -04:00
Owen Schwartz 44e81ea979 Merge pull request #3570 from fosrl/dependabot/npm_and_yarn/nanoid-3.3.18
Bump nanoid from 3.3.12 to 3.3.18
2026-08-18 12:23:17 -04:00
Owen Schwartz 56dc10330a Merge pull request #3572 from fosrl/dependabot/npm_and_yarn/postcss-8.5.23
Bump postcss from 8.5.15 to 8.5.23
2026-08-18 12:23:03 -04:00
Owen Schwartz 6c6e5c0fdf Merge pull request #3605 from fosrl/crowdin_dev
New Crowdin updates
2026-08-18 12:21:47 -04:00
Owen 4be73558d5 Restrict wildcard domains for inference resources 2026-08-18 11:30:44 -04:00
Owen 87ff7650a1 Show a - on the resource column when no resource is found 2026-08-18 11:29:37 -04:00
Owen Schwartz ae5af013b4 New translations en-us.json (French)
[ci skip]
2026-08-18 11:13:09 -04:00
Owen Schwartz 5adfe65aba New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-08-18 11:13:07 -04:00
Owen Schwartz d3ae06deeb New translations en-us.json (Chinese Simplified)
[ci skip]
2026-08-18 11:13:05 -04:00
Owen Schwartz ee4a72bcb7 New translations en-us.json (Turkish)
[ci skip]
2026-08-18 11:13:03 -04:00
Owen Schwartz f941c23927 New translations en-us.json (Russian)
[ci skip]
2026-08-18 11:13:00 -04:00
Owen Schwartz 3ae6a08b71 New translations en-us.json (Portuguese)
[ci skip]
2026-08-18 11:12:58 -04:00
Owen Schwartz 620eeaed4c New translations en-us.json (Polish)
[ci skip]
2026-08-18 11:12:56 -04:00
Owen Schwartz 9b08228398 New translations en-us.json (Dutch)
[ci skip]
2026-08-18 11:12:53 -04:00
Owen Schwartz eb102a0d23 New translations en-us.json (Korean)
[ci skip]
2026-08-18 11:12:50 -04:00
Owen Schwartz 18317e0028 New translations en-us.json (Italian)
[ci skip]
2026-08-18 11:12:48 -04:00
Owen Schwartz 2ec93c6710 New translations en-us.json (German)
[ci skip]
2026-08-18 11:12:46 -04:00
Owen Schwartz 5933eeb05f New translations en-us.json (Danish)
[ci skip]
2026-08-18 11:12:43 -04:00
Owen Schwartz 3fae2d687f New translations en-us.json (Czech)
[ci skip]
2026-08-18 11:12:40 -04:00
Owen Schwartz 285d7c6030 New translations en-us.json (Bulgarian)
[ci skip]
2026-08-18 11:12:38 -04:00
Owen Schwartz 0fd57f5843 New translations en-us.json (Spanish)
[ci skip]
2026-08-18 11:12:36 -04:00
Owen a296460124 Move out of local directory; fixes turbopack 2026-08-18 10:39:13 -04:00
Owen 35104f7b29 Add AI Provider and Virtual API Key actions to command palette 2026-08-18 10:19:56 -04:00
Owen 51375ed8b7 Fix key issue 2026-08-18 09:59:42 -04:00
Owen 1bb4f604e9 Fix select from missing new columns 2026-08-18 09:52:24 -04:00
Owen Schwartz 8fd990df93 New translations en-us.json (French)
[ci skip]
2026-08-17 18:29:37 -04:00
Owen Schwartz 1f390cabc6 New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-08-17 18:29:35 -04:00
Owen Schwartz 7f1f8c54c1 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-08-17 18:29:33 -04:00
Owen Schwartz b1515db229 New translations en-us.json (Turkish)
[ci skip]
2026-08-17 18:29:31 -04:00
Owen Schwartz d78c226734 New translations en-us.json (Russian)
[ci skip]
2026-08-17 18:29:30 -04:00
Owen Schwartz 52865bc1f5 New translations en-us.json (Portuguese)
[ci skip]
2026-08-17 18:29:28 -04:00
Owen Schwartz efbb6c9814 New translations en-us.json (Polish)
[ci skip]
2026-08-17 18:29:26 -04:00
Owen Schwartz 1e3dea3dad New translations en-us.json (Dutch)
[ci skip]
2026-08-17 18:29:24 -04:00
Owen Schwartz 25f84482ba New translations en-us.json (Korean)
[ci skip]
2026-08-17 18:29:22 -04:00
Owen Schwartz 3f2a116fdb New translations en-us.json (Italian)
[ci skip]
2026-08-17 18:29:20 -04:00
Owen Schwartz dce002c6cc New translations en-us.json (German)
[ci skip]
2026-08-17 18:29:18 -04:00
Owen Schwartz dcfbbdc79d New translations en-us.json (Danish)
[ci skip]
2026-08-17 18:29:16 -04:00
Owen Schwartz f8cac37eef New translations en-us.json (Czech)
[ci skip]
2026-08-17 18:29:14 -04:00
Owen Schwartz cb37713c4c New translations en-us.json (Bulgarian)
[ci skip]
2026-08-17 18:29:12 -04:00
Owen Schwartz 7ae4082d7e New translations en-us.json (Spanish)
[ci skip]
2026-08-17 18:29:11 -04:00
Owen aa8d339d35 Adjust sudo mode on the roles to full for use of ssh 2026-08-17 17:24:16 -04:00
Owen 7a61b182e0 Deduplicate inference resources by fullDomain to prevent duplicate routers 2026-08-17 17:13:22 -04:00
Owen 91b697b80b Track public inference domains to avoid duplicate routers for siteResource aliases 2026-08-17 17:06:37 -04:00
Owen 7d10b63dc5 Add v1 migrations 2026-08-17 16:50:51 -04:00
Owen 92916cd9db Remove wrong named 1.21.1 migration for 1.22 2026-08-17 16:01:30 -04:00
Owen 075d112861 Add base migrations 2026-08-17 15:54:42 -04:00
Owen 7e9407fbc7 Don't generate ai resource for remote nodes 2026-08-17 15:48:32 -04:00
miloschwartz 313f777e44 rename routes from inference to ai-gateway 2026-08-17 15:38:43 -04:00
Owen 3ba9eb7ad7 Default form to full sudo mode 2026-08-17 15:11:03 -04:00
Owen 328a2d52da Remove defaults from private config for legacy acme 2026-08-17 15:01:26 -04:00
miloschwartz 697bea814e bump version 2026-08-17 15:00:18 -04:00
Owen 83cda218b7 Merge branch 'free-me-up' into aig 2026-08-17 14:54:05 -04:00
Owen 76435440af Fix disabled sections in the ui 2026-08-17 14:53:20 -04:00
miloschwartz 0a8b5e46ca Bind share link sessions to the token's own resource 2026-08-17 14:24:21 -04:00
Owen a74b0934fc Allow changing the allow ssh and update shcmea 2026-08-17 14:12:39 -04:00
Owen c05d2aeafb Update lock 2026-08-17 14:06:40 -04:00
Owen 87a3e5ceea Generate the cert properly 2026-08-17 13:45:51 -04:00
Owen 555b36e13e Fix target pulling still being private 2026-08-17 13:40:09 -04:00
Owen 021866011f Update lock 2026-08-17 12:03:54 -04:00
Owen 749abdcb56 Fix circular import issue 2026-08-17 12:00:13 -04:00
Owen 7d6f3a2925 Refactor license key validation and recheck logic to improve error handling and maintain cache integrity 2026-08-17 12:00:13 -04:00
Owen af515f1072 Pass 2 bring over browser resources and http resource cert gen 2026-08-17 12:00:12 -04:00
Owen c90c67eab3 Pass 1 of pulling traefik config into functions 2026-08-17 12:00:12 -04:00
Owen 51c9507d08 Show the cert status on the frontend 2026-08-17 12:00:12 -04:00
Owen 11daf4f927 Move certificates 2026-08-17 12:00:12 -04:00
Owen 58195b5959 Move the acme cert sync 2026-08-17 12:00:12 -04:00
Owen b51aecf45a Remove advanced resources paywall 2026-08-17 12:00:12 -04:00
dependabot[bot] eb8ad6a181 Bump the github-actions-dependencies group across 1 directory with 2 updates
Bumps the github-actions-dependencies group with 2 updates in the / directory: [docker/login-action](https://github.com/docker/login-action) and [actions/stale](https://github.com/actions/stale).


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

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

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: 11.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
  dependency-group: github-actions-dependencies
- dependency-name: docker/login-action
  dependency-version: 4.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: github-actions-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-17 01:34:14 +00:00
Shlee 4edd2e4d32 Update requestPasswordReset.ts 2026-08-16 22:21:37 +09:30
Shlee 813c3abe54 Update requestPasswordReset.ts 2026-08-16 22:03:00 +09:30
miloschwartz ca7ea72ff1 restyle domain picker 2026-08-14 17:53:53 -04:00
miloschwartz 53b1d8a9f3 option to send identity keys in email 2026-08-14 17:37:17 -04:00
miloschwartz a3dfb30a42 send email upon generate virtual api key 2026-08-14 16:51:27 -04:00
Fred KISSIE 668a04bcd2 🚧 wip: IP column filtering 2026-08-14 21:52:16 +02:00
Owen 111d4b1b8c Make sure certs are created when creating and updating private resources 2026-08-14 14:33:59 -04:00
Owen 32d76ee2ac Resolve cert not generated without a provider 2026-08-14 14:27:14 -04:00
Owen 98ad6fb31b Merge branch 'aig' of github.com:fosrl/pangolin into aig 2026-08-14 14:21:04 -04:00
Owen 3a18a3994f Fix private resource ssl when applying through blueprints 2026-08-14 14:21:00 -04:00
Owen 6d35b20880 Fix overlap check 2026-08-14 14:14:56 -04:00
miloschwartz 173d4a536f visual tweaks 2026-08-14 13:45:52 -04:00
Fred KISSIE ea740e12d1 💄 nice little animation on alert rule field 2026-08-14 19:07:13 +02:00
miloschwartz d7bff686c0 add help banner to providers and virtual api keys 2026-08-14 12:24:00 -04:00
Owen 193f6da8d1 Handle the list of models without allow/block 2026-08-14 12:23:20 -04:00
Owen 954dc8d1d9 Handle overlapping fullDomain properly 2026-08-14 12:23:20 -04:00
Owen 234819f8f2 Dont load private resource inference without a exit node loaded 2026-08-14 12:23:20 -04:00
miloschwartz b34a8d116b improve org selector 2026-08-14 12:18:55 -04:00
miloschwartz 7375b9efae improve model picker 2026-08-14 11:56:02 -04:00
Owen c8f170d197 Allow budgets to be set on the resources 2026-08-14 11:43:01 -04:00
Owen 4989d1e31a Allow overlapping domains on public inference resources 2026-08-14 11:43:01 -04:00
miloschwartz c568251d8d improve model picker 2026-08-14 11:38:11 -04:00
miloschwartz db2de4af89 improve usage analytics filters 2026-08-14 11:11:52 -04:00
Owen 574ae8f5f9 Include inference support in blueprints 2026-08-14 10:51:59 -04:00
Owen 125091d719 Send email whitelist user downstream headers
Fix #1226
2026-08-14 10:51:59 -04:00
Owen 5d37f44241 Add niceid to ui and api for providers 2026-08-14 10:51:58 -04:00
Owen f0f5e9219b add niceId to provider 2026-08-14 10:51:58 -04:00
miloschwartz ee75a09f8c improve sign in url style 2026-08-14 10:33:44 -04:00
miloschwartz c49c6f5837 improve budget editor component 2026-08-14 10:27:17 -04:00
Owen 03118f1ede Support labels on blueprints
Ref https://github.com/orgs/fosrl/discussions/2849
2026-08-14 09:16:50 -04:00
miloschwartz 0ff5ea4f6a strip settings from tab name 2026-08-13 17:08:37 -04:00
Owen 978fa8bfc6 Fix refresh buttons and dont auto refresh
Fixes #3560
2026-08-13 16:57:43 -04:00
Owen 36c7c5dc34 Rename AI tables to not have ai in the name 2026-08-13 16:57:43 -04:00
miloschwartz 8f3628d4ec rename inference to ai gateway 2026-08-13 16:57:29 -04:00
miloschwartz fcd3c72cac add missing routes to integration api 2026-08-13 16:35:47 -04:00
Owen 3f0be4a39d Filter out unhealthy targets 2026-08-13 16:32:04 -04:00
miloschwartz f9ff3a5715 improve resource type picker 2026-08-13 15:52:33 -04:00
miloschwartz ed71c90d73 move ai logs under gateway section 2026-08-13 15:28:22 -04:00
miloschwartz e580c7db7c use pangolin-key prefix for virtual api keys 2026-08-13 15:22:18 -04:00
miloschwartz 4a9b0117a2 copy key without clicking reveal first 2026-08-13 15:16:39 -04:00
miloschwartz 9b10292e02 add model picker to create provider wizard 2026-08-13 15:08:10 -04:00
miloschwartz d9a9ae14fd various ui improvements 2026-08-13 14:54:09 -04:00
Owen db716f781e Revert "fix logo warning in logs"
This reverts commit 355294a2d4.
2026-08-13 13:51:11 -04:00
Fred KISSIE 46f341f7fd 🏷️ fix types 2026-08-13 19:26:15 +02:00
Fred KISSIE 9bb413bb1e ♻️ refactor 2026-08-13 19:23:39 +02:00
Fred KISSIE 2878d5690c 💬 update texts for heading & trigger 2026-08-13 19:19:55 +02:00
Fred KISSIE 1143404a65 💄alert rule popover 2026-08-13 19:15:23 +02:00
Owen e92bb9043b Fix issues with possible null providerId 2026-08-13 11:04:11 -04:00
Owen a2f5d830d6 Remove not null constraint where setting null 2026-08-13 10:20:35 -04:00
miloschwartz 77e3422e88 change provider descriptions and dont set default url for bedrock 2026-08-13 10:13:32 -04:00
miloschwartz 9a551c91e5 always require upstream url 2026-08-13 10:07:11 -04:00
Owen c35ac493ed Rebalance the data in the expand column 2026-08-13 09:38:42 -04:00
Owen e100645a00 Use the right subnet 2026-08-13 09:38:42 -04:00
miloschwartz 96bfc66a94 set default provider name 2026-08-13 09:37:53 -04:00
miloschwartz e21c9eec31 Merge branch 'aig' of https://github.com/fosrl/pangolin into aig 2026-08-13 09:29:02 -04:00
miloschwartz a9f32102b5 format custom error codes according to capability 2026-08-13 09:26:58 -04:00
Owen 7291628f86 optionally stamp ip header for downstream use 2026-08-13 09:14:37 -04:00
Owen 5fbb205044 search the right resource 2026-08-12 17:52:39 -04:00
Owen 02ab24d01e Fix clients not connecting on first connect 2026-08-12 17:16:59 -04:00
Owen 3cfbd66a80 Merge branch 'dev' into aig 2026-08-12 16:34:52 -04:00
Owen Schwartz 048e4fc73c Merge pull request #3574 from fosrl/dev
1.21.1-s.5
2026-08-12 16:34:31 -04:00
Owen 860fa47b7c Split out the token header into a common middleware for efficiency 2026-08-12 16:30:42 -04:00
miloschwartz 71d9d8f010 fix accept invite as idp user 2026-08-12 16:25:20 -04:00
Owen cc58e28e54 Allow merging a catalog file with the api response 2026-08-12 16:06:12 -04:00
Owen 653dd920ad Allow remote header overrides and fix display issues with log 2026-08-12 15:54:22 -04:00
Fred KISSIE 49b4fcf063 ♻️ refactor 2026-08-12 21:36:40 +02:00
miloschwartz dd78c2cc08 dont show servers idps in create user when idp mode is org on enterprise 2026-08-12 15:20:41 -04:00
Owen 47ae017f94 add virtual api keys to budgets 2026-08-12 13:21:56 -04:00
Fred KISSIE 4b61e12ca6 ♻️ trigger alert correctly 2026-08-12 18:56:56 +02:00
Fred KISSIE c6bd657ee6 send webhook action 2026-08-12 18:48:14 +02:00
miloschwartz c33fa8782b Merge branch 'aig' of https://github.com/fosrl/pangolin into aig 2026-08-12 12:35:30 -04:00
miloschwartz 93cba1d098 support sending capability specific error codes 2026-08-12 12:34:25 -04:00
Owen d3a1f9798d Merge branch 'aig' of github.com:fosrl/pangolin into aig 2026-08-12 11:45:28 -04:00
miloschwartz 115c3cbf07 show sso page in browser for gateway resource 2026-08-12 11:32:22 -04:00
Owen bed5817da3 send, process, store, display virtual api key ai information in usage and sessions 2026-08-12 11:20:25 -04:00
miloschwartz 83035753af support bypass auth rules on gateway 2026-08-12 11:18:16 -04:00
miloschwartz 379b53bcca redirect to api key page when access gateway in browser 2026-08-12 11:15:50 -04:00
Owen 355294a2d4 fix logo warning in logs 2026-08-12 10:48:06 -04:00
Owen 833b27ab4a separate the users and roles tabs 2026-08-12 10:40:38 -04:00
Owen 8a04f13dd4 translate keys and restructure roles and users 2026-08-12 10:40:38 -04:00
miloschwartz 49020fa6ea add virtual api key validation in verifySession 2026-08-12 10:38:26 -04:00
Owen ac3402a8b3 Adjust structure delete models 2026-08-12 09:41:42 -04:00
Owen 9369e60695 Merge branch 'dev' into aig 2026-08-12 09:19:12 -04:00
Owen 295e38d2af Remove arbitrary max 50 limit from email whitelist
Fix #3568
2026-08-12 09:11:10 -04:00
dependabot[bot] 923371e5b4 Bump postcss from 8.5.15 to 8.5.23
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.15 to 8.5.23.
- [Release notes](https://github.com/postcss/postcss/releases)
- [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/postcss/compare/8.5.15...8.5.23)

---
updated-dependencies:
- dependency-name: postcss
  dependency-version: 8.5.23
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-12 02:57:16 +00:00
dependabot[bot] 81430ba3d3 Bump nanoid from 3.3.12 to 3.3.18
Bumps [nanoid](https://github.com/ai/nanoid) from 3.3.12 to 3.3.18.
- [Release notes](https://github.com/ai/nanoid/releases)
- [Changelog](https://github.com/ai/nanoid/blob/3.3.18/CHANGELOG.md)
- [Commits](https://github.com/ai/nanoid/compare/3.3.12...3.3.18)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-version: 3.3.18
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-11 23:54:36 +00:00
Owen 11595f5f96 use title case and fix colors 2026-08-11 18:02:17 -04:00
Owen b08e875b37 basic ai analytics created 2026-08-11 17:52:27 -04:00
Owen 0016b8fce7 show other tokens on the expanded row 2026-08-11 16:40:24 -04:00
Owen f5b10df7cf fix non-https crypto issue 2026-08-11 16:40:24 -04:00
miloschwartz 1782f31075 show link to provider settings 2026-08-11 16:38:22 -04:00
Fred KISSIE 21032bc22b test alert email works 2026-08-11 22:25:26 +02:00
dependabot[bot] e4aaadc9f9 Bump js-yaml from 4.3.0 to 4.3.1
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.3.0 to 4.3.1.
- [Changelog](https://github.com/nodeca/js-yaml/blob/4.3.1/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.3.0...4.3.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-11 20:19:18 +00:00
Owen f7aca85417 link the usage with the session to display together 2026-08-11 16:15:01 -04:00
miloschwartz c42df737b0 improve model provider selection algorithm 2026-08-11 16:10:46 -04:00
Owen 75c6af3b11 Merge branch 'aig' of github.com:fosrl/pangolin into aig 2026-08-11 15:58:12 -04:00
Owen e734cc93a1 small visual adjustments and chat button 2026-08-11 15:57:57 -04:00
Owen e3ccc4f8d4 show the sessions in the ui 2026-08-11 15:50:51 -04:00
miloschwartz 896c362dce only support sso on inference resource auth policy 2026-08-11 15:35:28 -04:00
miloschwartz 864ab97293 hide expire column 2026-08-11 14:59:03 -04:00
Fred KISSIE 899c47e9a3 🚧 write process test alert function 2026-08-11 20:52:13 +02:00
Owen bc2f291352 normalize the requests to also store in the log for viewing later 2026-08-11 14:45:25 -04:00
miloschwartz 98f5e39a7f show ai gateway resource details in launcher 2026-08-11 14:42:07 -04:00
Owen e0a66e79bb Continue to quiet log messages 2026-08-11 14:07:33 -04:00
Owen c781be4e70 quiet up some more logs 2026-08-11 14:05:58 -04:00
Owen 48c4b44f72 log chat sessions to database 2026-08-11 12:19:28 -04:00
Owen 68a84181d9 remove excess logging 2026-08-11 12:19:27 -04:00
Owen b60390d805 batch up usage records for insert 2026-08-11 12:19:27 -04:00
miloschwartz b33e776072 add virtual keys link in header on launcher 2026-08-11 12:09:11 -04:00
miloschwartz 2187f23588 add page to retrieve user virtual api keys 2026-08-11 11:53:39 -04:00
miloschwartz 732fd4eba1 allow retrieving api key secret 2026-08-11 10:47:04 -04:00
Owen 495c772d6b Merge branch 'dev' into aig 2026-08-11 09:42:10 -04:00
Owen 02e4fe8b48 Show the provided domains when changing properly 2026-08-11 09:40:55 -04:00
miloschwartz 6d45486bb5 add manual virtual api key create ui 2026-08-10 21:44:38 -04:00
Owen 4c1f7f6243 add caching to budget check 2026-08-10 17:39:00 -04:00
miloschwartz 1e3a9fb921 add virtual api key schema and crud endpoints 2026-08-10 17:20:06 -04:00
Owen 8b8e7913dc budget enforcement logic 2026-08-10 17:17:24 -04:00
Owen 88515a7dad remove duplicate save buttons 2026-08-10 15:31:45 -04:00
Owen e012063f5b add the budget to the models on the provider 2026-08-10 15:09:39 -04:00
Owen e76377d3c7 Match the pg query logging 2026-08-10 14:55:44 -04:00
Owen a357f42c48 allow creating role with budgets 2026-08-10 14:55:44 -04:00
Owen 2becb15916 put the budget on the role form 2026-08-10 14:55:44 -04:00
miloschwartz 186eeed784 add richer model editor on provider 2026-08-10 14:54:58 -04:00
Owen 216c932cb9 order by budget id 2026-08-10 14:12:34 -04:00
Owen 52de5eb539 add the budget config to the resources 2026-08-10 14:08:17 -04:00
Owen eec3119297 Merge branch 'aig' of github.com:fosrl/pangolin into aig 2026-08-10 13:55:10 -04:00
Owen f2e7f83b3d match the budget picker to the rules picker 2026-08-10 13:55:05 -04:00
miloschwartz 93dea0525e basic provider model selector 2026-08-10 12:32:17 -04:00
Owen 7e6e0a8a9c make the budget match the rules 2026-08-10 12:20:47 -04:00
Owen 211d3a53f5 initial budget ui on the provider 2026-08-10 12:03:51 -04:00
miloschwartz 187936e5dd use class with helper methods for model catelog 2026-08-10 11:54:57 -04:00
Owen b180d064d1 adjust conflict to be on unit<>period and add list endpoints for providers 2026-08-10 11:39:34 -04:00
Owen ed46afd81a add subscription override column 2026-08-10 11:15:48 -04:00
Owen 0050fad70d add basic crud for ai budgets 2026-08-10 11:05:10 -04:00
miloschwartz 3dc9c100e9 better form feedback when saving roles 2026-08-10 11:02:44 -04:00
Owen 969e7c9296 add crud endpoint skill 2026-08-10 10:58:11 -04:00
Owen 591caab45a Add documentation for creating new crud endpoints 2026-08-10 10:47:18 -04:00
miloschwartz 02e97d6ae4 add copy button to org id 2026-08-10 10:44:25 -04:00
miloschwartz 996160fadc add missing resource policy actions to api key closes #3542 2026-08-10 10:38:40 -04:00
Owen dc1923ab1f Remove budget periods 2026-08-10 09:57:01 -04:00
Owen fe5831eb48 pull the models from the upstream server 2026-08-10 09:57:00 -04:00
Owen ca79abc9d4 Update structure 2026-08-10 09:57:00 -04:00
miloschwartz 9eafa067b9 improved provider picker ui on resource 2026-08-07 16:13:51 -04:00
Fred KISSIE 403b8a12e4 🚧 wip 2026-08-07 20:57:54 +02:00
Fred KISSIE 3f305e4d5c 🚧 process test alert 2026-08-07 20:52:25 +02:00
miloschwartz 5e5e172d39 add streaming function to capability 2026-08-07 14:36:15 -04:00
miloschwartz bc7a883f6c add some parallelization to ai gateway pipeline 2026-08-07 14:36:11 -04:00
Owen 12056aebc6 Add basic cost calculations for testing 2026-08-07 13:54:32 -04:00
Fred KISSIE 6689a8d93e 🚧 wip: test alert rule 2026-08-07 19:05:28 +02:00
miloschwartz 297cb9c8f2 set provider default capabilities 2026-08-07 12:25:29 -04:00
miloschwartz d699455b38 support changing capabilities on provider integrations 2026-08-07 11:23:07 -04:00
miloschwartz 9e7b4afaec support allow list and blocklist 2026-08-07 10:59:50 -04:00
Owen e91c344e64 Update link to be correct 2026-08-07 10:21:15 -04:00
Owen 07f628b928 add the pangolin header information 2026-08-06 17:53:51 -04:00
Owen 184e1425a4 Private connection working with traefik 2026-08-06 17:22:01 -04:00
Owen 22f2990f56 support streaming and closes properly on site targets 2026-08-06 17:15:11 -04:00
Owen 6c28c5f383 Basic target routing 2026-08-06 15:48:13 -04:00
Owen e9f9cf54f4 Include the sheme in the routing header 2026-08-06 15:18:11 -04:00
Owen 751d1b083d Add back port restrictions 2026-08-06 14:55:07 -04:00
Owen a4d77a4fd3 Fix info box ui 2026-08-06 14:45:25 -04:00
Owen f72252552f Fix saving 2026-08-06 14:28:14 -04:00
miloschwartz c5d68675c9 support patterns in model key 2026-08-06 14:27:08 -04:00
Owen Schwartz d04740fede Merge pull request #3537 from fosrl/dev
1.21.1-s.4
2026-08-06 14:07:42 -04:00
miloschwartz 36b8ef5fba Merge branch 'dev' into aig 2026-08-06 13:59:07 -04:00
Owen 6cca5e0472 Use fulldoman instead of the alias for the site resources 2026-08-06 13:45:51 -04:00
miloschwartz 4048fa274a fix non admins cant see private resources details in launcher 2026-08-06 12:32:19 -04:00
Owen b4d2974e19 Properly configure ssl and domain when creating and editing 2026-08-06 12:20:56 -04:00
Owen fb896d6c0f Add domain to the resource selection 2026-08-06 12:03:40 -04:00
miloschwartz 82b86263dc allow chars in 2fa input form closes #3532 2026-08-06 11:39:06 -04:00
Owen 6564bfe8ae disabled advanced mode on the providers 2026-08-06 11:00:34 -04:00
Owen 75ce7e91d7 Restrict the ai site resource router to exit node subnet 2026-08-06 11:00:34 -04:00
Owen 656eea5bb1 add a target routing provider for custom target resources 2026-08-06 11:00:34 -04:00
miloschwartz aba27a7bbf support skip tls per request 2026-08-05 17:53:28 -04:00
miloschwartz 3d7e322bf9 add headers to provider 2026-08-05 17:42:58 -04:00
miloschwartz 1f3fff4a9d Merge branch 'aig' of https://github.com/fosrl/pangolin into aig 2026-08-05 16:55:58 -04:00
miloschwartz 39e06f2b6d add api capabilities 2026-08-05 16:55:48 -04:00
Owen b775c5b674 include clients in the peer config 2026-08-05 16:24:09 -04:00
Owen 796bf37141 ai gateway override only required sometimes 2026-08-05 16:24:01 -04:00
Owen c5b0e1e876 add QUERY_LOGGING env to sqlite driver for verbose logging 2026-08-05 16:17:28 -04:00
miloschwartz 2e8bd7a8c7 improve form error codes 2026-08-05 15:56:12 -04:00
miloschwartz 790daba796 allow no provider on a resource 2026-08-05 15:43:42 -04:00
miloschwartz bcf6b86b84 add no auth and passthrough auth 2026-08-05 15:39:08 -04:00
miloschwartz 2e9bd50172 add provider specific auth modes 2026-08-05 15:17:50 -04:00
dependabot[bot] 4677a0d501 Bump fast-uri from 3.1.4 to 3.1.5
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.4...v3.1.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-05 18:18:41 +00:00
miloschwartz c673dce484 pin docker base image 2026-08-05 13:34:45 -04:00
Owen e1dd8965dc Remove extra checks 2026-08-05 11:31:12 -04:00
Owen 346a0bdc98 Resolve syntax issue 2026-08-05 10:31:14 -04:00
Owen 8c1169738e Merge branch 'aig' of github.com:fosrl/pangolin into aig 2026-08-05 10:24:42 -04:00
miloschwartz bc80f91a45 gateway endpoint mvp 2026-08-05 10:24:37 -04:00
Owen 3d062389e9 update oss with gateway traefik routers 2026-08-05 10:14:40 -04:00
Owen 7afddb5eb5 Clean logging 2026-08-05 10:00:36 -04:00
Owen 425a99e5ee Show the auth settings for testing 2026-08-05 09:59:41 -04:00
Owen a5e9339af9 Merge branch 'dev' into aig 2026-08-05 09:32:03 -04:00
dependabot[bot] b4463f0e1a Bump socket.io-parser from 4.2.6 to 4.2.7
Bumps [socket.io-parser](https://github.com/socketio/socket.io) from 4.2.6 to 4.2.7.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/socket.io-parser@4.2.6...socket.io-parser@4.2.7)

---
updated-dependencies:
- dependency-name: socket.io-parser
  dependency-version: 4.2.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-05 06:42:16 +00:00
Owen Schwartz b7c0669c38 Merge pull request #3528 from fosrl/dev
1.21.1-s.3
2026-08-04 17:46:49 -04:00
Owen 835a30cffe Show the cert status of the namespace domains properly 2026-08-04 17:44:34 -04:00
miloschwartz 83e20c2dfa adjust placeholder ui for rules and targets 2026-08-04 17:36:00 -04:00
miloschwartz f85d41945a Merge branch 'aig' of https://github.com/fosrl/pangolin into aig 2026-08-04 17:27:10 -04:00
miloschwartz 149eb17b27 create basic public inference resource 2026-08-04 17:26:57 -04:00
Owen 18b90da6ab Merge branch 'main' into dev 2026-08-04 17:22:32 -04:00
Owen 72d469b19c Revert changes to traefik config to pull site resources 2026-08-04 17:17:51 -04:00
Owen 6526d7f225 Remove completions endpoints 2026-08-04 17:09:44 -04:00
Owen 36c0edc62e Use p host to pass the host header 2026-08-04 17:08:59 -04:00
Owen aad26b9ae4 move the ai gateway to its own server 2026-08-04 17:08:59 -04:00
Owen 80dcdfe251 change override to work for bother badger and ai gateway 2026-08-04 17:08:59 -04:00
miloschwartz 1696fc37a8 dont set port restrictions for inference 2026-08-04 17:04:29 -04:00
miloschwartz 7759d87835 add basic ui for private inference resource 2026-08-04 16:54:23 -04:00
miloschwartz c085de1e9e Merge branch 'aig' of https://github.com/fosrl/pangolin into aig
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-04 15:56:27 -04:00
Fred KISSIE e99cd52e99 🐛 Fix log retention access pulling from the wrong org settings field 2026-08-04 21:55:34 +02:00
miloschwartz e38359c74f add crud for adding providers and models to resources 2026-08-04 15:54:24 -04:00
Owen 2cfd7e867b delete the client peers from the exit nodes properly 2026-08-04 15:46:22 -04:00
Owen ec5a2b0cbe add peers to exit node 2026-08-04 15:46:22 -04:00
Owen 2bf426bc22 dont restrict to enterprise and dont link with providers yet 2026-08-04 15:46:21 -04:00
miloschwartz ed8545f8a2 update schema to support models and provider access on inferences resources and add budgets 2026-08-04 14:50:07 -04:00
Owen 9811492a0b send the aliases when connecting for the first time 2026-08-04 14:23:25 -04:00
Owen 973925b35d allow creating basic inference resource 2026-08-04 14:22:36 -04:00
Owen 0b30cfc341 Working on inference resource management 2026-08-04 11:48:36 -04:00
Owen a7e44944fb Use ip to find user 2026-08-04 10:43:49 -04:00
Owen 9b25ebd6cd remove token fallback 2026-08-04 10:19:22 -04:00
miloschwartz 33b775e263 clean up providers ui 2026-08-04 10:15:49 -04:00
miloschwartz b0edc6302e providers table, create, and edit first pass 2026-08-04 10:15:49 -04:00
Owen 1a644b131d Pull the session cookie properly 2026-08-04 10:15:49 -04:00
Owen f7689b7a5a Endpoints to update models on the resource 2026-08-04 10:15:49 -04:00
Owen 1073011a2a first pass of traefik -> basic gateway 2026-08-04 10:15:49 -04:00
miloschwartz 6fa0009ebf add targets and refactor endpoints 2026-08-04 10:15:49 -04:00
Owen 42c0abedb7 Add basic page to configure the alias 2026-08-04 10:15:48 -04:00
Owen 56e59a93f3 Handle the alias create and update special case exit nodes 2026-08-04 10:15:48 -04:00
Owen 9d581f3897 Fix types 2026-08-04 10:15:48 -04:00
miloschwartz e5ac6ec7cd add default providers and support overrides 2026-08-04 10:15:48 -04:00
miloschwartz 694fe98131 add crud for providers and models 2026-08-04 10:15:48 -04:00
miloschwartz 730078597e add basic schema 2026-08-04 10:15:48 -04:00
Owen 32ac8db803 Handle aliases when registering 2026-08-04 10:15:48 -04:00
Owen aad2ed2719 Get exit node every time 2026-08-04 10:15:48 -04:00
Owen a790fed297 sync the exit node connection 2026-08-04 10:15:48 -04:00
Owen deb2d5ce2a Update site resources to handle new inference 2026-08-04 10:15:48 -04:00
Owen 093097c619 send if we need to connect to the gerbil or not 2026-08-04 10:15:48 -04:00
Owen 2cdeb7c104 Add inference resource type 2026-08-04 10:15:47 -04:00
Owen 4dbb04bfb8 Handle the ping exit node request backward mode for olm and install handler 2026-08-04 10:15:47 -04:00
Owen fa8b921635 Fix postgres schema exit node subnet 2026-08-04 10:15:47 -04:00
Owen 33dd10c670 Rename subnet for clarity, pick subnet on client 2026-08-04 10:15:47 -04:00
Owen ba24e1c4f5 Add exit node selection to the clients 2026-08-04 10:15:47 -04:00
Owen f079714caf Dont redirect when the browser agent is not real 2026-08-04 10:07:52 -04:00
dependabot[bot] 152d2fb1d6 Bump brace-expansion
Bumps  and [brace-expansion](https://github.com/juliangruber/brace-expansion). These dependencies needed to be updated together.

Updates `brace-expansion` from 5.0.6 to 5.0.9
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.6...v5.0.9)

Updates `brace-expansion` from 1.1.14 to 1.1.18
- [Release notes](https://github.com/juliangruber/brace-expansion/releases)
- [Commits](https://github.com/juliangruber/brace-expansion/compare/v5.0.6...v5.0.9)

---
updated-dependencies:
- dependency-name: brace-expansion
  dependency-version: 5.0.9
  dependency-type: indirect
- dependency-name: brace-expansion
  dependency-version: 1.1.18
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-04 05:33:39 +00:00
dependabot[bot] d374b4f66e Bump ip-address from 10.2.0 to 10.4.0
Bumps [ip-address](https://github.com/beaugunderson/ip-address) from 10.2.0 to 10.4.0.
- [Release notes](https://github.com/beaugunderson/ip-address/releases)
- [Commits](https://github.com/beaugunderson/ip-address/compare/v10.2.0...v10.4.0)

---
updated-dependencies:
- dependency-name: ip-address
  dependency-version: 10.4.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-04 01:45:48 +00:00
Owen efd2792197 bump default rate limit 2026-08-03 17:57:36 -04:00
Owen Schwartz efe22c889c Merge pull request #3522 from fosrl/dev
Move rate linmit to file
2026-08-03 14:35:43 -04:00
Owen 7f2b3eb481 Move rate linmit to file 2026-08-03 14:34:11 -04:00
Owen Schwartz 81be4a35d9 Merge pull request #3521 from fosrl/dev
1.21.1-s.1
2026-08-03 14:11:05 -04:00
Owen e84da6a8df Add rate limits to sensitve pages 2026-08-03 14:08:09 -04:00
miloschwartz 3ef3ede7df dont pass orgId in generate oidc url 2026-08-03 11:41:22 -04:00
Owen 1e521b0b54 Dont double log error 2026-08-03 10:35:26 -04:00
Owen 59ea701304 Merge branch 'dev' of github.com:fosrl/pangolin into dev 2026-07-31 14:23:43 -04:00
miloschwartz 7d7c54107d add first seen and last seen columns to user devices table 2026-07-31 14:23:06 -04:00
Owen Schwartz f0f6673d69 Merge pull request #3511 from fosrl/feat/show-idp-icon-in-smart-login-form
feat: show the correct idp type icon in the last used idp
2026-07-31 09:32:01 -04:00
Owen Schwartz 71561d0e65 Merge pull request #3512 from fosrl/fix/batch-status-hover-data
Fix: resolve batch certificates for NS & Wildcard domains
2026-07-31 09:31:21 -04:00
Fred KISSIE af87edf3a6 ♻️ use const 2026-07-30 22:06:38 +01:00
Fred KISSIE 13caad18c7 🎨 format file 2026-07-30 21:53:46 +01:00
Fred KISSIE 47522b7e3a ♻️ set cert error if initial value is null 2026-07-30 21:53:31 +01:00
Fred KISSIE c8c8d74452 🐛 resolve certificates for non exact domains too 2026-07-30 21:52:51 +01:00
Fred KISSIE e7098963d6 ♻️ Code Refactoring 2026-07-30 17:56:15 +01:00
Fred KISSIE f015fb592b ♻️ Prioritize variant in place of idp.type if available 2026-07-30 17:55:58 +01:00
Fred KISSIE c099167905 Merge branch 'dev' into feat/show-idp-icon-in-smart-login-form 2026-07-30 17:37:47 +01:00
Owen b0e274f5a9 Fix 100% showing on resources with no hc anymore 2026-07-30 11:14:34 -04:00
Shubham Singh cb3f0b49a8 Fix regional locale detection 2026-07-30 14:29:25 +05:30
Owen Schwartz 146c287aba Merge pull request #3505 from fosrl/dev
1.21.1
2026-07-29 21:27:33 -04:00
Owen Schwartz 95ee56217c Merge pull request #3504 from fosrl/crowdin_dev
New Crowdin updates
2026-07-29 21:25:59 -04:00
Owen Schwartz 7625cc208d New translations en-us.json (French)
[ci skip]
2026-07-29 18:14:07 -04:00
Owen Schwartz f2dfd939bc New translations en-us.json (Norwegian Bokmal)
[ci skip]
2026-07-29 18:14:05 -04:00
Owen Schwartz 699bc8ddb4 New translations en-us.json (Chinese Simplified)
[ci skip]
2026-07-29 18:14:03 -04:00
Owen Schwartz e658e007e1 New translations en-us.json (Turkish)
[ci skip]
2026-07-29 18:14:01 -04:00
Owen Schwartz 3103265450 New translations en-us.json (Russian)
[ci skip]
2026-07-29 18:13:59 -04:00
Owen Schwartz 97789d9e2e New translations en-us.json (Portuguese)
[ci skip]
2026-07-29 18:13:57 -04:00
Owen Schwartz a1aa3d96d4 New translations en-us.json (Polish)
[ci skip]
2026-07-29 18:13:56 -04:00
Owen Schwartz ae39bc0ecd New translations en-us.json (Dutch)
[ci skip]
2026-07-29 18:13:54 -04:00
Owen Schwartz e4d81aa610 New translations en-us.json (Korean)
[ci skip]
2026-07-29 18:13:52 -04:00
Owen Schwartz f59b524266 New translations en-us.json (Italian)
[ci skip]
2026-07-29 18:13:50 -04:00
Owen Schwartz 58c499acae New translations en-us.json (German)
[ci skip]
2026-07-29 18:13:48 -04:00
Owen Schwartz f22301a1eb New translations en-us.json (Danish)
[ci skip]
2026-07-29 18:13:46 -04:00
Owen Schwartz d6264fb39a New translations en-us.json (Czech)
[ci skip]
2026-07-29 18:13:44 -04:00
Owen Schwartz 08b5a9b34b New translations en-us.json (Bulgarian)
[ci skip]
2026-07-29 18:13:42 -04:00
Owen Schwartz 263976bf41 New translations en-us.json (Spanish)
[ci skip]
2026-07-29 18:13:40 -04:00
Owen Schwartz 0e4cd3a5ba Merge pull request #3414 from fosrl/dependabot/go_modules/install/go-install-dependencies-3804ca7238
Bump golang.org/x/term from 0.44.0 to 0.45.0 in /install in the go-install-dependencies group across 1 directory
2026-07-29 18:00:54 -04:00
dependabot[bot] 192542629f Bump engine.io from 6.6.7 to 6.6.9
Bumps [engine.io](https://github.com/socketio/socket.io) from 6.6.7 to 6.6.9.
- [Release notes](https://github.com/socketio/socket.io/releases)
- [Changelog](https://github.com/socketio/socket.io/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/socket.io/compare/engine.io@6.6.7...engine.io@6.6.9)

---
updated-dependencies:
- dependency-name: engine.io
  dependency-version: 6.6.9
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-29 21:59:23 +00:00
Owen Schwartz aeed271f3e Merge pull request #3494 from fosrl/dependabot/npm_and_yarn/next-16.2.11
Bump next from 16.2.6 to 16.2.11
2026-07-29 17:58:09 -04:00
Owen Schwartz d41e9bbe68 Merge pull request #3472 from fosrl/dependabot/npm_and_yarn/axios-1.18.0
Bump axios from 1.16.1 to 1.18.0
2026-07-29 17:57:46 -04:00
Owen Schwartz 904a5520b8 Merge pull request #3476 from fosrl/dependabot/npm_and_yarn/body-parser-2.3.0
Bump body-parser from 2.2.2 to 2.3.0
2026-07-29 17:57:34 -04:00
Owen Schwartz 0f43ae4871 Merge pull request #3486 from fosrl/dependabot/npm_and_yarn/fast-uri-3.1.4
Bump fast-uri from 3.1.2 to 3.1.4
2026-07-29 17:57:23 -04:00
Owen Schwartz bd0cf98319 Merge pull request #3489 from fosrl/dependabot/github_actions/github-actions-dependencies-3a33fbbccd
Bump the github-actions-dependencies group across 1 directory with 5 updates
2026-07-29 17:57:07 -04:00
Owen Schwartz fc7d6bbaf9 Merge pull request #3496 from fosrl/dependabot/npm_and_yarn/js-yaml-4.3.0
Bump js-yaml from 4.2.0 to 4.3.0
2026-07-29 17:56:37 -04:00
Owen 1b89c328de Merge branch 'main' into dev 2026-07-29 17:54:59 -04:00
miloschwartz e7f147d4ca fix site status stuck loading sometimes 2026-07-29 17:52:55 -04:00
miloschwartz 41dbc7d5ed remove time logs 2026-07-29 17:45:19 -04:00
miloschwartz 9f297317f2 bump version 2026-07-29 17:33:49 -04:00
Fred KISSIE e0a8721207 🚧 wip: idp type icon in last used idp 2026-07-29 22:29:56 +01:00
Owen Schwartz a69b310109 Merge pull request #3460 from shubhamsinnh/fix/idn-punycode-domain-validation
Fix Punycode domain validation
2026-07-29 17:29:25 -04:00
miloschwartz 864a6a39cd Merge branch 'main' into dev 2026-07-29 17:27:22 -04:00
Milo Schwartz 1882d4df88 Merge pull request #3469 from fosrl/refactor/batch-status-requests
refactor: batch status histories
2026-07-29 17:27:11 -04:00
miloschwartz ef3f71e9b5 Merge branch 'main' into dev 2026-07-29 17:09:54 -04:00
Milo Schwartz 9981c4c035 Merge pull request #3410 from Adityakk9031/#3408
fix: redirect to /auth/initial-setup after hitting auth rate limit (#…
2026-07-29 17:09:40 -04:00
Fred KISSIE bc267b7107 ♻️ refactor 2026-07-29 19:11:16 +01:00
Fred KISSIE 5cbb767e5f Set default certificates for private resources 2026-07-29 19:11:03 +01:00
Fred KISSIE ac79621cae ♻️ Adjust the polling interval to 30s for valid certs 2026-07-29 19:05:33 +01:00
miloschwartz f47c94d05b check idp org ownership on save policy closes #3290 2026-07-29 09:38:15 -04:00
Fred KISSIE 10773432bb ♻️ Batch certificate queries 2026-07-28 20:59:42 +01:00
dependabot[bot] d5a56fb71d Bump js-yaml from 4.2.0 to 4.3.0
Bumps [js-yaml](https://github.com/nodeca/js-yaml) from 4.2.0 to 4.3.0.
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.2.0...4.3.0)

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 07:10:39 +00:00
Owen 9a9ae649ef Add default to path
Fix #3484
2026-07-27 10:02:32 -04:00
Owen aa6dc67015 Pull the version on the info page and use api 2026-07-27 10:01:12 -04:00
dependabot[bot] 0da96b06ea Bump the github-actions-dependencies group across 1 directory with 5 updates
Bumps the github-actions-dependencies group with 5 updates in the / directory:

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



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

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

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

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

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

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

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

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

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


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

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

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

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

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

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

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-20 19:06:46 +00:00
Fred KISSIE 4c873e7c48 Merge branch 'dev' into refactor/batch-status-requests 2026-07-20 18:38:00 +01:00
Fred KISSIE 56fcb80b23 💄 fix labels dropdown flicker if no changes applied 2026-07-20 17:36:45 +01:00
Owen Schwartz 41c68148a9 Merge pull request #3467 from fosrl/dev
1.21.0
2026-07-20 11:46:48 -04:00
Owen Schwartz 5d38059b95 Merge pull request #3466 from fosrl/api-improvements
api improvements
2026-07-20 11:44:25 -04:00
Owen df0198df9c Update internal -> private 2026-07-20 11:44:07 -04:00
Owen 26713b53f6 Fix #3462 2026-07-20 11:44:07 -04:00
Owen 2bc6b28978 Unify put vs post 2026-07-20 11:44:07 -04:00
Owen f1ed4da8a4 Move to legacy 2026-07-20 11:44:07 -04:00
Owen adeefb9dbd Add missing endpoints to api 2026-07-20 11:44:05 -04:00
Owen bf1cc705a5 Update tags 2026-07-20 11:43:10 -04:00
Owen 0b82dae01e More route cleanup 2026-07-20 11:43:09 -04:00
Owen 75afe6ece2 Order more api routes 2026-07-20 11:43:09 -04:00
Owen 70e2fe1e4e Just add ordering 2026-07-20 11:43:06 -04:00
Owen 17e03457e1 Recategorize 2026-07-20 11:42:37 -04:00
Owen 4d8cb7e231 Add aliaes for public/private resources 2026-07-20 11:42:35 -04:00
Owen 9561d23f1e Resovle endcoding issue 2026-07-19 14:41:07 -04:00
Shubham Singh 0275f44056 Fix Punycode domain validation 2026-07-19 13:45:44 +05:30
Owen a2e1c7b751 Go to domains if no domains 2026-07-18 16:30:29 -04:00
Owen 9e2ec72ced Fix missing resource id in cache 2026-07-18 16:15:30 -04:00
Owen 02fe1f3abd Claify that this is only for the cloud 2026-07-18 11:53:56 -04:00
Fred KISSIE 1580b7abff 🚧 wip: batch status histories 2026-07-10 07:02:22 +02:00
Aditya kumar singh bb5547a157 fix: redirect to /auth/initial-setup after hitting auth rate limit (#3408) 2026-07-08 23:33:57 +05:30
727 changed files with 75171 additions and 14329 deletions
+31
View File
@@ -0,0 +1,31 @@
---
name: crud-endpoints
description: Use whenever asked to add, create, or scaffold a CRUD endpoint, router, or entity in this repo's server (create/list/get/update/delete handlers, new `server/routers/<entity>/` or `server/private/routers/<entity>/` folder). Points to the established file layout, middleware, ActionsEnum, and route-registration conventions before writing any code.
---
Before writing any router/handler/middleware code for a new entity, read
`docs/crud-endpoints.md` in full. It documents, with real examples from
`server/routers/aiProvider/` (public) and `server/private/routers/alertRule/`
(enterprise-only), how this repo structures CRUD endpoints:
- Directory/file layout per entity (`index.ts`, `types.ts`, `validation.ts`,
one file per operation).
- The standard handler anatomy (zod parsing, OpenAPI registry, response
envelope, error handling).
- Where access-control middleware (`verify<Entity>Access`) lives and when
it's needed vs. plain `verifyOrgAccess`.
- How to wire up `ActionsEnum` entries, `verifyUserHasAction`, and
`logActionAudit`.
- Which of the four router files (`server/routers/external.ts`,
`server/routers/internal.ts`, `server/private/routers/external.ts`,
`server/private/routers/internal.ts`) to register routes in, and the
middleware chain template per HTTP verb.
- The repo's non-standard verb convention: **`PUT` = create, `POST` =
update** (backwards from typical REST) — don't "fix" this to standard
REST verbs, match the existing convention.
- The `#dynamic` import alias, for the rare case of a hook needing different
implementations in OSS vs. enterprise builds.
Follow that doc's checklist (§8) step by step rather than improvising a
structure. If the doc and the actual code in `aiProvider`/`alertRule` ever
disagree, trust the code and flag the doc as stale.
+8
View File
@@ -34,6 +34,14 @@ body:
validations:
required: true
- type: textarea
attributes:
label: AI Disclosure
description: |
If you used AI to help write this issue, please disclose it here. This is important for transparency and helps maintain the integrity of the issue tracking process.
validations:
required: true
- type: textarea
attributes:
label: Expected Behavior
+4
View File
@@ -4,6 +4,10 @@ perpetual license to use, modify, and redistribute these contributions under any
choose, including both the AGPLv3 and the Fossorial Commercial license terms. I
represent that I have the right to grant this license for all contributed content.
## AI Disclosure
> Please disclose how AI was used in this pull request. The use of AI does not preclude this from being merged but is an important factor in how we review your request.
## Description
+9 -9
View File
@@ -62,7 +62,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Monitor storage space
run: |
@@ -77,7 +77,7 @@ jobs:
fi
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -134,7 +134,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Monitor storage space
run: |
@@ -149,7 +149,7 @@ jobs:
fi
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -201,10 +201,10 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Log in to Docker Hub
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: docker.io
username: ${{ secrets.DOCKER_HUB_USERNAME }}
@@ -256,7 +256,7 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Extract tag name
id: get-tag
@@ -264,7 +264,7 @@ jobs:
shell: bash
- name: Install Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version: 1.25
@@ -407,7 +407,7 @@ jobs:
shell: bash
- name: Login to GitHub Container Registry (for cosign)
uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0
uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4.6.0
with:
registry: ghcr.io
username: ${{ github.actor }}
+2 -2
View File
@@ -21,10 +21,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
stale:
runs-on: ubuntu-latest
steps:
- uses: actions/stale@eb5cf3af3ac0a1aa4c9c45633dd1ae542a27a899 # v10.3.0
- uses: actions/stale@4391f3da665fdf50b6810c1a66712fb9ba21aa93 # v11.0.0
with:
days-before-stale: 14
days-before-close: 14
+4 -4
View File
@@ -14,10 +14,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24'
@@ -62,7 +62,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build Docker image sqlite
run: make dev-build-sqlite
@@ -71,7 +71,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Build Docker image pg
run: make dev-build-pg
-1
View File
@@ -46,7 +46,6 @@ public/branding
server/db/index.ts
server/build.ts
postgres/
dynamic/
*.mmdb
scratch/
tsconfig.json
+4 -4
View File
@@ -1,5 +1,5 @@
# FROM node:24-slim AS base
FROM public.ecr.aws/docker/library/node:24-slim AS base
# FROM node:24.18.1-slim AS base
FROM public.ecr.aws/docker/library/node:24.18.1-slim AS base
WORKDIR /app
@@ -32,8 +32,8 @@ FROM base AS builder
RUN npm ci --omit=dev
# FROM node:24-slim AS runner
FROM public.ecr.aws/docker/library/node:24-slim AS runner
# FROM node:24.18.1-slim AS runner
FROM public.ecr.aws/docker/library/node:24.18.1-slim AS runner
WORKDIR /app
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:24-alpine
FROM node:24.18.1-alpine
WORKDIR /app
+29 -5
View File
@@ -37,11 +37,22 @@
<p align="center">
<strong>
Get started with Pangolin at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
Get started with Pangolin Cloud at <a href="https://app.pangolin.net/auth/signup">app.pangolin.net</a>
</strong>
</p>
Pangolin is an open-source, identity-based remote access platform built on WireGuard® that enables secure connectivity to infrastructure anywhere. It combines reverse-proxy and VPN capabilities into one platform, providing browser-based access to web applications and client-based access to private resources with NAT traversal, all with granular access control.
Pangolin is an open-source SASE platform, built on WireGuard®, with a simple mission: connect and protect your users, wherever they are. It brings networking and security together as one system including a zero-trust VPN, zero-trust reverse proxy, privileged access management, and an identity-aware AI gateway, all sharing one identity and policy model. It's the same idea behind platforms like Cloudflare One, Zscaler, and Prisma but open, self-hostable, and built to stay light and easy to deploy.
### Networking and security that's unified, open, and simple
Legacy SASE platforms got the idea right: connectivity and security belong together. But they delivered it as a heavyweight, closed, cloud-locked stack assembled from years of patchwork. Pangolin exists to do that unification differently, in the open, self-hostable, and simple enough that administrators actually enjoy running it.
* **Open source, not a black box**: the code is open and auditable, so you can see exactly how your traffic is handled and how access decisions get made, instead of trusting a closed cloud control plane.
* **Networking and security as one platform**: sites, reverse proxy, client access, RBAC, and the AI gateway share one identity and policy model, so protecting users and connecting them are executed together.
* **Lightweight by design**: the whole platform is built to stay small and fast: easy to self-host on a small server, with a lightweight, user-space connector that goes in your private networks.
* **Enjoyable to use**: a clean, modern interface and a setup flow that gets out of your way, so managing access feels simple instead of like fighting a legacy admin console.
* **Zero trust from day one**: access is granted per resource, not per network, with identity provider integration, role-based access control, and full audit logging.
* **Run it your way**: self-host the Community Edition for free, step up to the Enterprise Edition for advanced features, or use Pangolin Cloud if you'd rather not manage infrastructure at all.
## Installation
@@ -53,9 +64,9 @@ Pangolin is an open-source, identity-based remote access platform built on WireG
## Deployment Options
- **Pangolin Cloud** - Fully managed service - no infrastructure required.
- **Self-Host: Community Edition** - Free, open source, and licensed under AGPL-3.
- **Self-Host: Enterprise Edition** - Licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses making less than \$100K USD gross annual revenue.
- **Pangolin Cloud** - Fully managed service with no infrastructure required.
- **Self-Host: Community Edition** - Free, open-source, and licensed under AGPL-3.
- **Self-Host: Enterprise Edition** - Open-core, and licensed under Fossorial Commercial License. Free for personal and hobbyist use, and for businesses making less than \$100K USD gross annual revenue.
## Key Features
@@ -99,6 +110,19 @@ Access private resources like SSH servers, databases, RDP, and entire network ra
<img src="public/screenshots/private-resources.png" alt="Private resources" width="100%" />
### Identity-aware AI gateway
Put an identity-aware proxy in front of public cloud (OpenAI, Anthropic, Gemini, etc.) and self-hosted model servers (Ollama, vLLM, Mistral, etc.) so coding agents and AI clients call a single Pangolin URL. Publish it as a public resource with personal API keys, or keep it private on a client tunnel where the connected client is the credential for keyless access. Budgets, session history, and usage analytics sit in front of every call.
* Access self-hosted models (vLLM, Ollama, etc) alongside cloud models (OpenAI, Anthropic, etc) in one place
* Keyless access by authenticating users with the Pangolin desktop client
* Or, provide users with personal API keys
* Control costs and token usage by setting budgets
* Audit with detailed session history and analytics
* Integrate AI clients and coding agents (Claude Code, Codex, OpenCode, etc)
<img src="public/screenshots/expanded-session-logs.png" alt="AI Session Logs" width="100%" />
### Give users and roles access to resources
Use Pangolin's built-in users or bring your own identity provider and set up role-based access control (RBAC). Grant users access to specific resources, not entire networks. Unlike traditional VPNs that expose full network access, Pangolin's zero-trust model ensures users can only reach the applications, services, and routes you explicitly define.
+1 -1
View File
@@ -23,7 +23,7 @@ export const clearExitNodes: CommandModule<
// Delete all exit nodes
const deletedCount = await db
.delete(exitNodes)
.where(eq(exitNodes.exitNodeId, exitNodes.exitNodeId)) .returning();; // delete all
.where(eq(exitNodes.exitNodeId, exitNodes.exitNodeId)).returning();; // delete all
console.log(`Deleted ${deletedCount.length} exit node(s) from the database`);
+1 -1
View File
@@ -5,7 +5,7 @@ import { encrypt } from "@server/lib/crypto";
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
import { generateCA } from "@server/lib/sshCA";
import fs from "fs";
import yaml from "js-yaml";
import * as yaml from "js-yaml";
type GenerateOrgCaKeysArgs = {
orgId: string;
+102 -2
View File
@@ -1,10 +1,10 @@
import { CommandModule } from "yargs";
import { db, idpOidcConfig, licenseKey, certificates, eventStreamingDestinations, alertWebhookActions } from "@server/db";
import { db, idpOidcConfig, licenseKey, certificates, eventStreamingDestinations, alertWebhookActions, aiProviders, virtualApiKeys } from "@server/db";
import { encrypt, decrypt } from "@server/lib/crypto";
import { configFilePath1, configFilePath2 } from "@server/lib/consts";
import { eq } from "drizzle-orm";
import fs from "fs";
import yaml from "js-yaml";
import * as yaml from "js-yaml";
type RotateServerSecretArgs = {
"old-secret": string;
@@ -132,12 +132,16 @@ export const rotateServerSecret: CommandModule<
const certs = await db.select().from(certificates);
const streamingDestinations = await db.select().from(eventStreamingDestinations);
const webhookActions = await db.select().from(alertWebhookActions);
const providers = await db.select().from(aiProviders);
const virtualKeys = await db.select().from(virtualApiKeys);
console.log(`Found ${idpConfigs.length} OIDC IdP configuration(s)`);
console.log(`Found ${licenseKeys.length} license key(s)`);
console.log(`Found ${certs.length} certificate(s)`);
console.log(`Found ${streamingDestinations.length} event streaming destination(s)`);
console.log(`Found ${webhookActions.length} alert webhook action(s)`);
console.log(`Found ${providers.length} AI provider(s)`);
console.log(`Found ${virtualKeys.length} virtual API key(s)`);
// Prepare all decrypted and re-encrypted values
console.log("\nDecrypting and re-encrypting values...");
@@ -171,11 +175,24 @@ export const rotateServerSecret: CommandModule<
encryptedConfig: string;
};
type AiProviderUpdate = {
providerId: number;
encryptedApiKey: string | null;
encryptedHeaders: string | null;
};
type VirtualApiKeyUpdate = {
virtualApiKeyId: string;
encryptedToken: string;
};
const idpUpdates: IdpUpdate[] = [];
const licenseKeyUpdates: LicenseKeyUpdate[] = [];
const certUpdates: CertUpdate[] = [];
const streamingDestinationUpdates: StreamingDestinationUpdate[] = [];
const webhookActionUpdates: WebhookActionUpdate[] = [];
const aiProviderUpdates: AiProviderUpdate[] = [];
const virtualApiKeyUpdates: VirtualApiKeyUpdate[] = [];
// Process idpOidcConfig entries
for (const idpConfig of idpConfigs) {
@@ -306,6 +323,60 @@ export const rotateServerSecret: CommandModule<
}
}
// Process aiProviders entries (apiKey + headers)
for (const provider of providers) {
try {
if (!provider.apiKey && !provider.headers) {
continue;
}
const encryptedApiKey = provider.apiKey
? encrypt(decrypt(provider.apiKey, oldSecret), newSecret)
: null;
const encryptedHeaders = provider.headers
? encrypt(
decrypt(provider.headers, oldSecret),
newSecret
)
: null;
aiProviderUpdates.push({
providerId: provider.providerId,
encryptedApiKey,
encryptedHeaders
});
} catch (error) {
console.error(
`Error processing AI provider ${provider.providerId}:`,
error
);
throw error;
}
}
// Process virtualApiKeys entries (token)
for (const key of virtualKeys) {
try {
if (!key.token) {
continue;
}
virtualApiKeyUpdates.push({
virtualApiKeyId: key.virtualApiKeyId,
encryptedToken: encrypt(
decrypt(key.token, oldSecret),
newSecret
)
});
} catch (error) {
console.error(
`Error processing virtual API key ${key.virtualApiKeyId}:`,
error
);
throw error;
}
}
// Perform all database updates in a single transaction
console.log("\nUpdating database in transaction...");
await db.transaction(async (trx) => {
@@ -376,6 +447,32 @@ export const rotateServerSecret: CommandModule<
)
);
}
// Update AI provider entries
for (const update of aiProviderUpdates) {
await trx
.update(aiProviders)
.set({
apiKey: update.encryptedApiKey,
headers: update.encryptedHeaders
})
.where(eq(aiProviders.providerId, update.providerId));
}
// Update virtual API key entries
for (const update of virtualApiKeyUpdates) {
await trx
.update(virtualApiKeys)
.set({
token: update.encryptedToken
})
.where(
eq(
virtualApiKeys.virtualApiKeyId,
update.virtualApiKeyId
)
);
}
});
console.log(`Rotated ${idpUpdates.length} OIDC IdP configuration(s)`);
@@ -383,6 +480,8 @@ export const rotateServerSecret: CommandModule<
console.log(`Rotated ${certUpdates.length} certificate(s)`);
console.log(`Rotated ${streamingDestinationUpdates.length} event streaming destination(s)`);
console.log(`Rotated ${webhookActionUpdates.length} alert webhook action(s)`);
console.log(`Rotated ${aiProviderUpdates.length} AI provider(s)`);
console.log(`Rotated ${virtualApiKeyUpdates.length} virtual API key(s)`);
// Update config file with new secret
console.log("\nUpdating config file...");
@@ -402,6 +501,7 @@ export const rotateServerSecret: CommandModule<
console.log(` - Certificates: ${certUpdates.length}`);
console.log(` - Event streaming destinations: ${streamingDestinationUpdates.length}`);
console.log(` - Alert webhook actions: ${webhookActionUpdates.length}`);
console.log(` - AI providers: ${aiProviderUpdates.length}`);
console.log(
`\n IMPORTANT: Restart the server for the new secret to take effect.`
);
+1 -1
View File
@@ -8,7 +8,7 @@ services:
POSTGRES_USER: postgres # Default user
POSTGRES_PASSWORD: password # Default password (change for production!)
volumes:
- ./config/postgres:/var/lib/postgresql/data
- ${HOME}/.local/share/pangolin-dev/postgres:/var/lib/postgresql/data
ports:
- "5432:5432" # Map host port 5432 to container port 5432
restart: no
+3
View File
@@ -0,0 +1,3 @@
## Example Docker Reference HA Deployment
This directory contains basic config for a highly available deployment of Pangolin with two nodes. For more information [refer to the docs](/self-host/clustering/understanding-clustering).
@@ -0,0 +1,23 @@
services:
postgres:
image: postgres:17
container_name: postgres
environment:
POSTGRES_DB: postgres # Default database name
POSTGRES_USER: postgres # Default user
POSTGRES_PASSWORD: password # Default password (change for production!)
volumes:
- postgres_data:/var/lib/postgresql/data
ports:
- "5432:5432"
restart: always
redis:
image: redis:latest
container_name: redis
ports:
- "6379:6379"
restart: always
volumes:
postgres_data:
@@ -0,0 +1,38 @@
# To see all available options, please visit the docs:
# https://docs.pangolin.net/
gerbil:
start_port: 51820
base_endpoint: "<THIS_NODE_EXTERNAL_IP>"
exit_node_name: "node1"
app:
dashboard_url: "https://pangolin.example.com"
log_level: "info"
postgres:
connection_string: postgresql://<POSTGRES_USERNAME>:<POSTGRES_PASSWORD>@<POSTGRES_INTERNAL_HOST>:5432/postgres
traefik:
site_types: ["newt"] # Wireguard and local sites are not support in clustering
file_mode: true # Pangolin will generate and save yaml files in a shared volume
server:
secret: "<SECRET>"
cors:
origins: ["https://pangolin.example.com"]
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: false
maxmind_db_path: "./config/GeoLite2-Country.mmdb" # Make sure to download and place into the config dir
maxmind_asn_path: "./config/GeoLite2-ASN.mmdb"
flags:
require_email_verification: false
disable_signup_without_invite: true
disable_user_create_org: false
allow_raw_resources: false
enable_acme_cert_sync: false
disable_local_sites: true
disable_basic_wireguard_sites: true
disable_config_managed_domains: true
@@ -0,0 +1,67 @@
http:
middlewares:
badger:
plugin:
badger:
disableForwardAuth: true
routers:
# Next.js router (handles everything except API and WebSocket paths)
next-router:
rule: "!PathPrefix(`/api/v1`)"
service: next-service
entryPoints:
- dashboard
middlewares:
- badger
# API router (handles /api/v1 paths)
api-router:
rule: "PathPrefix(`/api/v1`)"
service: api-service
entryPoints:
- dashboard
middlewares:
- badger
# WebSocket router
ws-router:
rule: "PathPrefix(`/`)"
service: api-service
entryPoints:
- dashboard
middlewares:
- badger
services:
next-service:
loadBalancer:
servers:
- url: "http://pangolin:3002" # Next.js server
api-service:
loadBalancer:
servers:
- url: "http://pangolin:3000" # API/WebSocket server
tcp:
serversTransports:
pp-transport-v1:
proxyProtocol:
version: 1
pp-transport-v2:
proxyProtocol:
version: 2
udp:
routers:
dns-router:
entryPoints:
- dns
service: dns-service
services:
dns-service:
loadBalancer:
servers:
- address: "pangolin:53"
@@ -0,0 +1,18 @@
app:
region: "region1"
identity_provider_mode: "org"
redis:
host: "<REDIS_INTERNAL_HOST>"
port: 6379
flags:
enable_redis: true
use_pangolin_dns: true
acme:
cert_mode: "pangolin"
contact_email: "<CONTACT_EMAIL>"
enable_acme_client: true
dns:
enabled: true
nameserver_name: "ns.example.com"
cname_extension: "cname.example.com"
site_extension: "site.example.com" # Optional
@@ -0,0 +1,45 @@
providers:
file:
directory: "/var/dynamic"
watch: true
experimental:
plugins:
badger:
moduleName: "github.com/fosrl/badger"
version: "v1.7.0"
log:
level: "INFO"
format: "common"
maxSize: 100
maxBackups: 3
maxAge: 3
compress: true
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
proxyProtocol: # We trust gerbil upstream
trustedIPs:
- 0.0.0.0/0
- ::1/128
transport:
respondingTimeouts:
readTimeout: "30m"
http:
encodedCharacters:
allowEncodedSlash: true
allowEncodedQuestionMark: true
dashboard:
address: ":3000"
dns:
address: ":53/udp"
serversTransport:
insecureSkipVerify: true
ping:
entryPoint: "web"
@@ -0,0 +1,62 @@
name: pangolin
services:
pangolin:
image: docker.io/fosrl/pangolin:ee-latest
container_name: pangolin
restart: unless-stopped
volumes:
- ./config:/app/config
- ./config/certificates:/var/certificates
- ./config/dynamic:/var/dynamic
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
interval: "10s"
timeout: "10s"
retries: 15
gerbil:
image: docker.io/fosrl/gerbil:latest
container_name: gerbil
restart: unless-stopped
depends_on:
pangolin:
condition: service_healthy
command:
- --reachableAt=http://<NODE1_INTERNAL_IP>:3004
- --generateAndSaveKeyTo=/var/config/key
- --remoteConfig=http://pangolin:3001/api/v1/
- --trusted-upstreams=<NODE1_EXTERNAL_IP>,<NODE2_EXTERNAL_IP>
volumes:
- ./config/:/var/config
cap_add:
- NET_ADMIN
- SYS_MODULE
ports:
- 51820:51820/udp # wireguard
- 21820:21820/udp # relay
- 53:53/udp # DNS
- 443:8443 # resources
- 80:80 # web
- 3004:3004 # gerbil api
- 3000:3000 # Pangolin UI
traefik:
image: docker.io/traefik:v3.7.11
container_name: traefik
restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service
depends_on:
pangolin:
condition: service_healthy
command:
- --configFile=/etc/traefik/traefik_config.yml
volumes:
- ./config/traefik:/etc/traefik:ro
- ./config/traefik/logs:/var/log/traefik
- ./config/certificates:/var/certificates:ro
- ./config/dynamic:/var/dynamic:ro
networks:
default:
driver: bridge
name: pangolin
@@ -0,0 +1,38 @@
# To see all available options, please visit the docs:
# https://docs.pangolin.net/
gerbil:
start_port: 51820
base_endpoint: "<THIS_NODE_EXTERNAL_IP>"
exit_node_name: "node2"
app:
dashboard_url: "https://pangolin.example.com"
log_level: "info"
postgres:
connection_string: postgresql://<POSTGRES_USERNAME>:<POSTGRES_PASSWORD>@<POSTGRES_INTERNAL_HOST>:5432/postgres
traefik:
site_types: ["newt"] # Wireguard and local sites are not support in clustering
file_mode: true # Pangolin will generate and save yaml files in a shared volume
server:
secret: "<SECRET>"
cors:
origins: ["https://pangolin.example.com"]
methods: ["GET", "POST", "PUT", "DELETE", "PATCH"]
allowed_headers: ["X-CSRF-Token", "Content-Type"]
credentials: false
maxmind_db_path: "./config/GeoLite2-Country.mmdb" # Make sure to download and place into the config dir
maxmind_asn_path: "./config/GeoLite2-ASN.mmdb"
flags:
require_email_verification: false
disable_signup_without_invite: true
disable_user_create_org: false
allow_raw_resources: false
enable_acme_cert_sync: false
disable_local_sites: true
disable_basic_wireguard_sites: true
disable_config_managed_domains: true
@@ -0,0 +1,67 @@
http:
middlewares:
badger:
plugin:
badger:
disableForwardAuth: true
routers:
# Next.js router (handles everything except API and WebSocket paths)
next-router:
rule: "!PathPrefix(`/api/v1`)"
service: next-service
entryPoints:
- dashboard
middlewares:
- badger
# API router (handles /api/v1 paths)
api-router:
rule: "PathPrefix(`/api/v1`)"
service: api-service
entryPoints:
- dashboard
middlewares:
- badger
# WebSocket router
ws-router:
rule: "PathPrefix(`/`)"
service: api-service
entryPoints:
- dashboard
middlewares:
- badger
services:
next-service:
loadBalancer:
servers:
- url: "http://pangolin:3002" # Next.js server
api-service:
loadBalancer:
servers:
- url: "http://pangolin:3000" # API/WebSocket server
tcp:
serversTransports:
pp-transport-v1:
proxyProtocol:
version: 1
pp-transport-v2:
proxyProtocol:
version: 2
udp:
routers:
dns-router:
entryPoints:
- dns
service: dns-service
services:
dns-service:
loadBalancer:
servers:
- address: "pangolin:53"
@@ -0,0 +1,16 @@
app:
region: "region1"
identity_provider_mode: "org"
redis:
host: "<REDIS_INTERNAL_HOST>"
port: 6379
flags:
enable_redis: true
use_pangolin_dns: true
acme:
cert_mode: "pangolin"
dns:
enabled: true
nameserver_name: "ns.example.com"
cname_extension: "cname.example.com"
site_extension: "site.example.com" # Optional
@@ -0,0 +1,45 @@
providers:
file:
directory: "/var/dynamic"
watch: true
experimental:
plugins:
badger:
moduleName: "github.com/fosrl/badger"
version: "v1.7.0"
log:
level: "INFO"
format: "common"
maxSize: 100
maxBackups: 3
maxAge: 3
compress: true
entryPoints:
web:
address: ":80"
websecure:
address: ":443"
proxyProtocol: # We trust gerbil upstream
trustedIPs:
- 0.0.0.0/0
- ::1/128
transport:
respondingTimeouts:
readTimeout: "30m"
http:
encodedCharacters:
allowEncodedSlash: true
allowEncodedQuestionMark: true
dashboard:
address: ":3000"
dns:
address: ":53/udp"
serversTransport:
insecureSkipVerify: true
ping:
entryPoint: "web"
@@ -0,0 +1,62 @@
name: pangolin
services:
pangolin:
image: docker.io/fosrl/pangolin:ee-latest
container_name: pangolin
restart: unless-stopped
volumes:
- ./config:/app/config
- ./config/certificates:/var/certificates
- ./config/dynamic:/var/dynamic
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3001/api/v1/"]
interval: "10s"
timeout: "10s"
retries: 15
gerbil:
image: docker.io/fosrl/gerbil:latest
container_name: gerbil
restart: unless-stopped
depends_on:
pangolin:
condition: service_healthy
command:
- --reachableAt=http://<NODE1_INTERNAL_IP>:3004
- --generateAndSaveKeyTo=/var/config/key
- --remoteConfig=http://pangolin:3001/api/v1/
- --trusted-upstreams=<NODE1_EXTERNAL_IP>,<NODE2_EXTERNAL_IP>
volumes:
- ./config/:/var/config
cap_add:
- NET_ADMIN
- SYS_MODULE
ports:
- 51820:51820/udp # wireguard
- 21820:21820/udp # relay
- 53:53/udp # DNS
- 443:8443 # resources
- 80:80 # web
- 3004:3004 # gerbil api
- 3000:3000 # Pangolin UI
traefik:
image: docker.io/traefik:v3.7.11
container_name: traefik
restart: unless-stopped
network_mode: service:gerbil # Ports appear on the gerbil service
depends_on:
pangolin:
condition: service_healthy
command:
- --configFile=/etc/traefik/traefik_config.yml
volumes:
- ./config/traefik:/etc/traefik:ro
- ./config/traefik/logs:/var/log/traefik
- ./config/certificates:/var/certificates:ro
- ./config/dynamic:/var/dynamic:ro
networks:
default:
driver: bridge
name: pangolin
+1 -1
View File
@@ -1,3 +1,3 @@
files:
- source: /messages/en-US.json
translation: /messages/%locale%.json
translation: /messages/%locale%.json
+350
View File
@@ -0,0 +1,350 @@
# AI Gateway Provider Selection
How the AI gateway picks which attached provider handles a request when an
inference resource has more than one AI provider.
**Code:**
- Route → capability binding: `server/routers/aiGateway/createAiGatewayRouter.ts`
- Request pipeline: `server/routers/aiGateway/pipeline.ts` (`selectProvider`)
- Model discovery: `server/routers/aiGateway/v1Models.ts` and
`server/lib/aiModelDiscovery.ts`
- Tie-break scoring: `server/lib/aiProviderSelection.ts`
- Allow/block matching: `server/lib/aiModelKeyMatch.ts`
- Model catalog: `server/lib/aiModelCatalog.ts`
- Default capabilities per provider type: `server/lib/aiProviderDefaults.ts`
Overlapping model allows are permitted at save time. Selection happens at
request time. If the algorithm cannot confidently pick one provider, the
gateway returns `403` with an ambiguous-provider error.
## Selection Pipeline
Every gateway request runs through these steps in order. Each step narrows
the candidate set. Later steps only run when more than one provider remains.
```
1. Capability filter
2. Allow / block lists
3. Most specific allow pattern
4. Catalog ownership
5. Provider class preference
6. Ambiguous → error
```
### 1. Capability Filter
The incoming path selects a capability before any provider logic runs.
| Path | Capability |
|------|------------|
| `POST /v1/chat/completions` | `openai_chat` |
| `POST /v1/responses` | `openai_responses` |
| `POST /v1/messages` | `anthropic_messages` |
| `GET /v1/models`, `GET /v1/models/{id}` | `v1_models` |
| Gemini / Vertex / Bedrock routes | their respective capability ids |
Only attached providers that advertise that capability stay in the candidate
set. Default capabilities do not overlap for native OpenAI vs Anthropic:
| Provider type | Default capabilities |
|---------------|----------------------|
| `openai` | `openai_chat`, `openai_responses` |
| `anthropic` | `anthropic_messages`, `v1_models` |
| `openRouter` | `openai_chat` |
| `vercelAiGateway` | `openai_chat`, `openai_responses` |
| `microsoftFoundry` | `openai_chat`, `openai_responses`, `anthropic_messages`, `v1_models` |
| `custom` | whatever was configured |
### 2. Allow / Block Lists
For each remaining provider, the gateway resolves the effective allow and
block patterns:
- **`inherit`**: use the provider's own model lists
- **`select`**: use the resource-selected subset of those lists
A candidate is kept only if `isAllowedByLists(requestedModel, allows, blocks)`
passes:
1. At least one allow pattern must match
2. No block pattern may match
Patterns support `*` and `?` globs (`gpt-*`, `claude-3-5-sonnet-?`).
### 3. Most Specific Allow Pattern
Among providers that allow the model, keep those whose matching allow
pattern is most specific:
1. Exact keys beat patterns
2. Fewer wildcard characters win
3. Longer literal length wins
Example: `gpt-4o` beats `gpt-*` beats `*`.
### 4. Catalog Ownership
When specificity is tied (common with multiple `*` allows), score each
provider against the known model catalog:
| Score | Meaning |
|------:|---------|
| 2 | Typed provider whose catalog contains the model (`openai` → openai catalog, `anthropic` → anthropic, etc.) |
| 1 | Aggregator or custom (`openRouter`, `vercelAiGateway`, `custom`) and the model exists somewhere in the catalog |
| 0 | No ownership signal (typed catalog miss, or unknown model on aggregator/custom) |
Model id lookup tries the raw id, then a stripped `vendor/model` form
(e.g. `openai/gpt-4o` → also try `gpt-4o`).
Typed providers map to catalog providers as:
| Provider type | Catalog |
|---------------|---------|
| `openai` | `openai` |
| `anthropic` | `anthropic` |
| `googleGemini` | `gemini` |
| `vertexAi` | `vertex` |
| `bedrock` | `bedrock` |
| `microsoftFoundry` | `azure` |
| `openRouter` / `vercelAiGateway` / `custom` | none (aggregator/custom path) |
### 5. Provider Class Preference
If catalog ownership is still tied, prefer:
| Rank | Class |
|-----:|-------|
| 2 | Native typed provider (`openai`, `anthropic`, `googleGemini`, …) |
| 1 | Aggregator (`openRouter`, `vercelAiGateway`) |
| 0 | `custom` |
### 6. Ambiguous Error
If more than one distinct provider remains after all steps, the gateway
rejects the request:
```
Model "<id>" is ambiguous across multiple AI providers on this resource
```
Typical remaining ties: two OpenAI-type providers both with `*`, or two
customs advertising the same capability for an unknown model.
## Model Discovery Is Not Selection
`GET /v1/models` and `GET /v1/models/{id}` (`v1_models`) skip steps 3-6
entirely. There is no requested model to disambiguate on, so the gateway does
not pick one provider - it returns the **union** of what every attached
provider advertising `v1_models` would accept, deduplicated by model id
(lowest `providerId` wins a collision).
Discovery is answered from the gateway's own view of the allow/block lists,
never proxied upstream. Providers that expose no `/v1/models` endpoint of their
own still get a working listing, and a model an allow/block list forbids is
never advertised.
Each provider's candidate ids come from two places:
| Source | Contributes |
|--------|-------------|
| Exact (non-wildcard) allow entries | the model key itself |
| The model catalog for the provider's type | every catalog id matching an allow pattern |
Both sources are then filtered through the same
`isAllowedByLists(id, allows, blocks)` check step 2 applies, so a block pattern
hides a model from discovery exactly as it would reject it at request time.
The catalog source is what makes a wildcard allow such as `claude-*`
enumerable. Provider types with no catalog mapping (`openRouter`,
`vercelAiGateway`, `custom`) have nothing to expand against, so a wildcard
allow on those types lists nothing - **add exact allow entries to make their
models discoverable.**
### Where each field comes from
Token limits and capability flags can't be derived from an allow/block list.
They come from the model catalog (`server/lib/aiModelCatalog.ts`), which the
Fossorial API builds from LiteLLM:
| Field | Source |
|-------|--------|
| `max_input_tokens` | catalog `limits.input` |
| `max_tokens` | catalog `limits.output` |
| `capabilities` | catalog flags, mapped to the Models API shape by `capabilitiesFromCatalog` |
| `display_name` | the configured model row's name, else the model id |
| `created_at` | the configured model row's timestamp, else the epoch |
A model the catalog doesn't know (an exact allow entry for a fine-tune, say)
reports `null` for all three metadata fields. The Models API declares them
nullable, so that is a valid answer rather than a broken one.
The catalog's flags are coarser than the Models API describes: it carries a
single `reasoning` flag with no way to distinguish adaptive from
`budget_tokens`-style thinking, and nothing at all for batch, citations, code
execution, PDF input, or context management. Anything it reports as unknown
(`null`) is surfaced as unsupported rather than invented, so `capabilities`
understates rather than overstates what a model can do.
The gateway does **not** query the provider's own `/v1/models`. Discovery is
answered entirely from local state.
Results are ordered newest-first with the id as tie-break, and paginated with
Anthropic's `limit` / `after_id` / `before_id` semantics (default 20, max
1000).
## Examples
Assume each provider below is attached and enabled on the same inference
resource.
### Example A: OpenAI + Anthropic, Both `*`
| Provider | Allow | Capabilities |
|----------|-------|--------------|
| OpenAI | `*` | `openai_chat`, `openai_responses` |
| Anthropic | `*` | `anthropic_messages` |
**Request:** `POST /v1/chat/completions` with `model: "gpt-4o"`
1. Capability → only OpenAI remains
2. Allow → OpenAI matches `*`
3. Result → **OpenAI**
Anthropic never reaches pattern or catalog scoring. Capability alone decides.
**Request:** `POST /v1/messages` with `model: "claude-3-5-sonnet-latest"`
1. Capability → only Anthropic remains
2. Result → **Anthropic**
### Example B: OpenAI + OpenRouter, Both `*`
| Provider | Allow | Capabilities |
|----------|-------|--------------|
| OpenAI | `*` | `openai_chat`, … |
| OpenRouter | `*` | `openai_chat` |
**Request:** `POST /v1/chat/completions` with `model: "gpt-4o"`
1. Capability → both remain (`openai_chat`)
2. Allow → both match `*`
3. Specificity → tie (`*` vs `*`)
4. Catalog → OpenAI scores `2` (owns `gpt-4o`); OpenRouter scores `1`
5. Result → **OpenAI**
### Example C: OpenRouter Only Serving a Claude Model Over OpenAI Chat
| Provider | Allow | Capabilities |
|----------|-------|--------------|
| OpenRouter | `*` | `openai_chat` |
**Request:** `POST /v1/chat/completions` with `model: "anthropic/claude-3.5-sonnet"`
1. Capability → OpenRouter remains
2. Only one candidate → **OpenRouter**
No tie-breaking needed.
### Example D: OpenAI (`gpt-*`) + OpenRouter (`*`)
| Provider | Allow |
|----------|-------|
| OpenAI | `gpt-*` |
| OpenRouter | `*` |
**Request:** `model: "gpt-4o"` on `openai_chat`
1. Capability → both
2. Allow → both match
3. Specificity → OpenAI's `gpt-*` beats OpenRouter's `*`
4. Result → **OpenAI**
Catalog scoring is not needed because specificity already unique'd the set.
### Example E: OpenAI + Anthropic With Overlapping Custom Capabilities
Someone grants Anthropic `openai_chat` as well (non-default).
| Provider | Allow | Capabilities |
|----------|-------|--------------|
| OpenAI | `*` | `openai_chat`, … |
| Anthropic | `*` | `anthropic_messages`, `openai_chat` |
**Request:** `POST /v1/chat/completions` with `model: "gpt-4o"`
1. Capability → both remain
2. Allow → both match `*`
3. Specificity → tie
4. Catalog → OpenAI `2`, Anthropic `0` (`gpt-4o` is not in the anthropic catalog)
5. Result → **OpenAI**
### Example F: Two Aggregators, Known Model
| Provider | Allow |
|----------|-------|
| OpenRouter | `*` |
| Vercel AI Gateway | `*` |
**Request:** `model: "gpt-4o"` on `openai_chat`
1. Capability → both
2. Allow / specificity → tie
3. Catalog → both score `1` (known model, no typed owner in the set)
4. Class → both aggregators (rank `1`) → still tied
5. Result → **ambiguous error**
Attach a native OpenAI provider (or narrow one aggregator's allow list) to
make this determinable.
### Example G: Two OpenAI Providers, Both `*`
| Provider | Type | Allow |
|----------|------|-------|
| OpenAI Prod | `openai` | `*` |
| OpenAI Staging | `openai` | `*` |
**Request:** `model: "gpt-4o"`
15 all leave both candidates (same capability, same specificity, same
catalog ownership, same class).
Result → **ambiguous error**
Disambiguate with different allow patterns, disable one attachment, or
split across resources.
### Example H: Unknown Model Across Native + Aggregator
| Provider | Allow |
|----------|-------|
| OpenAI | `*` |
| OpenRouter | `*` |
**Request:** `model: "my-fine-tune-v3"` (not in catalog)
1. Capability → both
2. Allow / specificity → tie
3. Catalog → both score `0` (typed miss + unknown aggregator model)
4. Class → OpenAI (`2`) beats OpenRouter (`1`)
5. Result → **OpenAI**
## Practical Guidance
- Native OpenAI + Anthropic with `*` is safe. Different default APIs never
collide.
- OpenAI + OpenRouter with `*` is usually fine for catalog-known OpenAI
models. Native wins.
- Prefer specific allow patterns (`gpt-4o`, `gpt-*`) when two providers share
a capability.
- Two providers of the same type both using `*` will stay ambiguous. Narrow
at least one allow list.
- Custom providers only win ties when no stronger native/aggregator signal
remains.
## Related Behavior
- **Saving providers on a resource does not reject overlapping allows.**
Collisions are resolved (or rejected) per request.
- Budgets, auth, and upstream URL / target routing run after a single
provider has been selected.
+347
View File
@@ -0,0 +1,347 @@
# How to build a CRUD endpoint in this repo
Reference for adding a new CRUD entity to the server. Based on two real
examples already in the codebase — read them side by side with this doc:
- **Public / open-source (Community Edition) pattern**: `server/routers/aiProvider/`
- **Enterprise-only pattern**: `server/private/routers/alertRule/`
The two are structurally identical. The only difference is *where the files
live* and *which router they get wired into*.
## 1. Decide: public or private?
- `server/routers/<entity>/` — ships in the open-source Community Edition.
Anyone running Pangolin gets this.
- `server/private/routers/<entity>/` — Enterprise/SaaS only. Gated behind
`verifyValidLicense` (and often `verifyValidSubscription(tierMatrix.x)`).
Every file here starts with the Fossorial Commercial License header block
(copy it verbatim from an existing private file).
Everything below applies to both — swap `@server/...` for `#private/...`
import paths and add license headers when building the private version.
## 2. Directory layout
One folder per entity, one file per operation, a barrel `index.ts`:
```
server/routers/<entity>/
index.ts # export * from each operation file + ./types
types.ts # response payload types + row->public mapper
validation.ts # zod schemas/refinements shared by create + update (optional)
create<Entity>.ts
list<Entities>.ts
get<Entity>.ts
update<Entity>.ts
delete<Entity>.ts
```
`index.ts` is a flat barrel:
```ts
export * from "./createAiProvider";
export * from "./listAiProviders";
export * from "./getAiProvider";
export * from "./updateAiProvider";
export * from "./deleteAiProvider";
export * from "./types";
```
## 3. Anatomy of a single handler
Every handler file (`create<Entity>.ts`, etc.) follows the same shape:
```ts
import { Request, Response, NextFunction } from "express";
import { z } from "zod";
import { <table>, db } from "@server/db";
import response from "@server/lib/response";
import HttpCode from "@server/types/HttpCode";
import createHttpError from "http-errors";
import logger from "@server/logger";
import { fromError } from "zod-validation-error";
import { OpenAPITags, registry } from "@server/openApi";
import { eq } from "drizzle-orm";
import type { GetXResponse } from "@server/routers/<entity>/types";
const paramsSchema = z.strictObject({
orgId: z.string().nonempty() // or entityId: z.coerce.number().int().positive()
});
const bodySchema = z.strictObject({ /* ... */ }); // create/update only
registry.registerPath({
method: "get", // put | post | delete
path: "/org/{orgId}/x",
description: "...",
tags: [OpenAPITags.<Entity>],
request: { params: paramsSchema, /* body: {...} for write ops, query: for list */ },
responses: { 200: { description: "Successful response" } }
});
export async function getX(req: Request, res: Response, next: NextFunction): Promise<any> {
try {
const parsedParams = paramsSchema.safeParse(req.params);
if (!parsedParams.success) {
return next(createHttpError(HttpCode.BAD_REQUEST, fromError(parsedParams.error).toString()));
}
// parse body too, if present, same pattern
// ...business logic against db...
if (!row) {
return next(createHttpError(HttpCode.NOT_FOUND, `X with ID ${id} not found`));
}
return response<GetXResponse>(res, {
data: { /* ... */ },
success: true,
error: false,
message: "X retrieved successfully",
status: HttpCode.OK
});
} catch (error) {
logger.error(error);
return next(createHttpError(HttpCode.INTERNAL_SERVER_ERROR, "An error occurred"));
}
}
```
Rules to keep consistent with the rest of the codebase:
- `z.strictObject` for params/body — rejects unknown keys.
- Params parsed first, then body; each on its own `safeParse` + early
`next(createHttpError(...))` — never throw raw errors.
- Every handler registers itself with the OpenAPI `registry` even if nobody
reads the spec directly — it's how `/api/v1/docs` stays accurate.
- Catch-all `try/catch` at the bottom: `logger.error(error)` +
generic `500` message. Never leak internal error details to the client.
- Use `response<T>(res, { data, success, error, message, status })` from
`@server/lib/response` for every response, success or otherwise (errors go
through `next(createHttpError(...))` instead, not through `response`).
- If the route already ran an access-control middleware that fetched the row
(see §5), reuse it instead of re-querying:
`req.aiProvider && req.aiProvider.providerId === providerId ? [req.aiProvider] : await db.select()...`
### List handler specifics
Pagination is a fixed shape (`page`, `pageSize`, optional `query` for
search). See `listAiProviders.ts`:
```ts
const listSchema = z.object({
pageSize: z.coerce.number<string>().int().positive().optional().catch(20).default(20),
page: z.coerce.number<string>().int().min(0).optional().catch(1).default(1),
query: z.string().optional()
});
```
Run the count query and the page query in `Promise.all`, and return
`PaginatedResponse<{ items: T[] }>` (`@server/types/Pagination`) with
`{ total, pageSize, page }`.
### types.ts specifics
- Define one response type per operation: `List<Entities>Response`,
`Get<Entity>Response`, `CreateOrEdit<Entity>Response` (create and update
commonly share a response shape).
- If the raw DB row needs to be shaped for clients (decrypting secrets,
parsing a serialized column, hiding a column), put a `toPublic<Entity>()`
mapper here — see `toPublicAiProvider` for the pattern of stripping
`apiKey`/serialized columns and re-adding decrypted/parsed versions.
### validation.ts specifics
Only needed when create and update share non-trivial zod pieces (enums,
`superRefine` cross-field rules). Export the raw schemas (`z.enum([...])`)
and refinement functions, and import them into both `createX.ts` and
`updateX.ts` — see `aiProvider/validation.ts`'s
`refineProviderUpstreamFields`.
## 4. Wire up an access-control middleware (for id-scoped routes)
For routes scoped to a single row (`/x/:xId`, as opposed to
`/org/:orgId/x` create/list), add a `verify<Entity>Access` middleware in
`server/middlewares/` (or `server/private/middlewares/` for enterprise-only
entities) and export it from that directory's `index.ts`.
Pattern (`verifyAiProviderAccess.ts`):
1. Read the id param, `Number.parseInt`/validate it.
2. Load the row by id.
3. `404` if it doesn't exist.
4. Resolve the row's `orgId`, then check/attach `req.userOrg` (query
`userOrgs` if not already on the request), `403` if the user isn't in
that org.
5. Run `checkOrgAccessPolicy` if `req.orgPolicyAllowed` hasn't been resolved
yet.
6. Set `req.userOrgId`, `req.userOrgRoleIds`, and stash the row on the
request (e.g. `req.aiProvider = provider`) so downstream handlers and
`verifyUserHasAction` don't have to refetch it.
Org-scoped create/list routes (`/org/:orgId/x`) don't need a bespoke
middleware — they use the existing generic `verifyOrgAccess` from
`@server/middlewares`.
## 5. Register an action + permission check
Add one `ActionsEnum` entry per operation in `server/auth/actions.ts`,
grouped near the entity's other actions, named `create<Entity>`,
`get<Entity>`, `update<Entity>`, `delete<Entity>`, `list<Entities>`:
```ts
createAiProvider = "createAiProvider",
deleteAiProvider = "deleteAiProvider",
getAiProvider = "getAiProvider",
listAiProviders = "listAiProviders",
updateAiProvider = "updateAiProvider",
```
Every route uses `verifyUserHasAction(ActionsEnum.x)` to check the caller's
role/permissions for that action, and mutating routes (create/update/delete)
follow it with `logActionAudit(ActionsEnum.x)` to record the action in the
audit log.
## 6. Register the routes
There are four router files; which one(s) you touch depends on public vs.
private and user-facing vs. service-to-service:
| File | Purpose |
|---|---|
| `server/routers/external.ts` | Public, user-facing API. Exports `authenticated`, `unauthenticated`, `authRouter` Express routers. |
| `server/routers/internal.ts` | Public, internal service-to-service API (gerbil, badger, traefik-config) — no user auth, exports `internalRouter`. |
| `server/private/routers/external.ts` | Enterprise-only, user-facing. Imports `authenticated`/`unauthenticated`/`authRouter` **from the public `external.ts`** and re-exports them, then adds more routes on top. |
| `server/private/routers/internal.ts` | Enterprise-only, service-to-service. Same re-export trick with `internalRouter`. |
Private router files always start:
```ts
import {
unauthenticated as ua,
authenticated as a,
authRouter as aa
} from "@server/routers/external";
export const authenticated = a;
export const unauthenticated = ua;
export const authRouter = aa;
```
...and then call `authenticated.get/put/post/delete(...)` to bolt on
additional, enterprise-only routes on the *same* router instances the public
build uses. This is why the private build has strictly more routes than the
public build, not a divergent copy.
### Route registration order (mutating vs read)
Standard middleware chain per verb, using `alertRule`'s registrations as the
template:
```ts
// Create — org-scoped, no row exists yet
authenticated.put(
"/org/:orgId/x",
verifyValidLicense, // private/enterprise routes only
verifyOrgAccess,
verifyLimits, // if the entity counts against a plan limit
verifyUserHasAction(ActionsEnum.createX),
logActionAudit(ActionsEnum.createX),
x.createX
);
// Update — row-scoped
authenticated.post(
"/org/:orgId/x/:xId", // or "/x/:xId" if id is globally unique
verifyValidLicense,
verifyOrgAccess, // or verifyXAccess if globally-keyed
verifyUserHasAction(ActionsEnum.updateX),
logActionAudit(ActionsEnum.updateX),
x.updateX
);
// Delete — row-scoped
authenticated.delete(
"/org/:orgId/x/:xId",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.deleteX),
logActionAudit(ActionsEnum.deleteX),
x.deleteX
);
// List — org-scoped, read-only, no audit log
authenticated.get(
"/org/:orgId/xs",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.listXs),
x.listXs
);
// Get one — row-scoped, read-only, no audit log
authenticated.get(
"/org/:orgId/x/:xId",
verifyValidLicense,
verifyOrgAccess,
verifyUserHasAction(ActionsEnum.getX),
x.getX
);
```
Notes:
- HTTP verbs: `PUT` = create, `POST` = update, `GET` = read, `DELETE` =
delete. This repo does not use `PATCH` for entity updates (site
provisioning keys are the one exception, using `PATCH`).
- `verifyValidLicense` is only needed on private/enterprise routes; public
OSS routes skip it.
- Use `verifyValidSubscription(tierMatrix.someFeature)` right after
`verifyValidLicense` when a feature is gated to specific SaaS tiers (see
`tierMatrix` usages in `server/private/routers/external.ts`).
- `verifyLimits` goes on create routes for entities that count against a
plan/seat limit.
- For entities keyed by a globally-unique id (not nested under `/org/:orgId`),
use the dedicated `verify<Entity>Access` middleware from §4 instead of
`verifyOrgAccess` on the row-scoped routes (see how `/ai-provider/:providerId`
uses `verifyAiProviderAccess`, while `/org/:orgId/ai-provider` create/list
use plain `verifyOrgAccess`).
- Read-only routes (`get`, `list`) skip `logActionAudit` — only mutations are
audited.
- `internal*.ts` routes are for trusted internal callers (gerbil/badger
sidecars) and generally skip user-facing auth entirely, using
`verifySessionUserMiddleware` / `verifyUserFromResourceSessionMiddleware`
instead of `verifyOrgAccess`/`verifyUserHasAction`. CRUD entities almost
never need internal router entries — only add one if a sidecar process
needs direct access to the resource.
## 7. The `#dynamic` alias (advanced — most CRUD work can ignore this)
Some middleware (e.g. `logActionAudit`) needs a real implementation in the
enterprise/SaaS build but a no-op stub in the open-source build, while
being imported by identical code in `server/routers/external.ts` in both
builds. That's done via the `#dynamic/*` import alias, which
`tsconfig.oss.json` points at `./server/*` and `tsconfig.enterprise.json` /
`tsconfig.saas.json` point at `./server/private/*`. You only need this
pattern if you're adding a genuinely dual-implementation hook; a normal
private-only CRUD entity (like `alertRule`) never touches `#dynamic` — it
just lives entirely under `server/private/` and is imported with `#private/*`
directly from `server/private/routers/external.ts`.
## 8. Checklist for a new entity
1. Add the DB table to `server/db/pg/schema/schema.ts` (and sqlite schema if
applicable).
2. Add `ActionsEnum` entries in `server/auth/actions.ts`.
3. Create `server/routers/<entity>/` (or `server/private/routers/<entity>/`):
`types.ts`, optional `validation.ts`, one file per operation, `index.ts`
barrel.
4. If routes are row-scoped by a global id, add
`verify<Entity>Access.ts` to `server/middlewares/` or
`server/private/middlewares/`, and export it from that directory's
`index.ts`.
5. Wire routes into `external.ts` (public or private) following the verb/
middleware table in §6. Add to `internal.ts` only if a sidecar needs
direct access.
6. Add license header block to every new file if it's under `server/private/`.
+2
View File
@@ -0,0 +1,2 @@
tls:
certificates: []
+1
View File
@@ -0,0 +1 @@
{}
+1 -1
View File
@@ -16,7 +16,7 @@ experimental:
version: "{{.BadgerVersion}}"
crowdsec: # CrowdSec plugin configuration added
moduleName: "github.com/maxlerebourg/crowdsec-bouncer-traefik-plugin"
version: "v1.4.4"
version: "v1.7.1"
log:
level: "INFO"
+2 -2
View File
@@ -5,7 +5,7 @@ go 1.25.0
require (
github.com/charmbracelet/huh v1.0.0
github.com/charmbracelet/lipgloss v1.1.0
golang.org/x/term v0.44.0
golang.org/x/term v0.45.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -33,6 +33,6 @@ require (
github.com/rivo/uniseg v0.4.7 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
golang.org/x/sync v0.15.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/sys v0.47.0 // indirect
golang.org/x/text v0.23.0 // indirect
)
+4 -4
View File
@@ -69,10 +69,10 @@ golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc=
golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+604 -25
View File
@@ -43,6 +43,8 @@
"inviteLoginUser": "Моля, уверете се, че сте влезли като правилния потребител.",
"inviteErrorNoUser": "Съжаляваме, но изглежда, че поканата, до която се опитвате да получите достъп, не е за съществуващ потребител.",
"inviteCreateUser": "Моля, първо създайте акаунт.",
"inviteErrorOidcNotAllowed": "Поканите могат да се приемат само от вътрешни акаунти. Отпишете се и влезте с вашата парола за този имейл.",
"inviteLoginInternalOnly": "Поканите изискват вътрешен акаунт с парола. Създайте акаунт или влезте с вашата парола.",
"goHome": "Отиди вкъщи",
"inviteLogInOtherUser": "Влезте като друг потребител",
"createAnAccount": "Създайте профил",
@@ -151,6 +153,7 @@
"siteResourcesTargetsOnSite": "Цели на този сайт",
"siteSetting": "Настройки на {siteName}",
"siteNewtTunnel": "Нов Сайт (Препоръчително)",
"pangolinSite": "Сайт Панголиин",
"siteNewtTunnelDescription": "Най-лесният начин да създадете точка за достъп до всяка мрежа. Няма нужда от допълнителни настройки.",
"siteWg": "Основен WireGuard",
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required. ONLY WORKS ON SELF HOSTED NODES",
@@ -178,7 +181,7 @@
"shareDeleteConfirm": "Потвърдете изтриването на споделима връзка",
"shareQuestionRemove": "Сигурни ли сте, че искате да изтриете тази споделена връзка?",
"shareMessageRemove": "След изтриване връзката вече няма да работи и всеки, който я използва, ще загуби достъп до ресурса.",
"shareTokenDescription": "Достъпният токен може да бъде предаван по два начина: като параметър или в хедърите на заявките. Те трябва да бъдат предавани от клиента при всяка заявка за удостоверен достъп.",
"shareTokenDescription": "Токен за достъп може да бъде предаден като параметър на заявка или в хедърите на заявката. По подразбиране трябва да се изпраща при всяка заявка. Ако е активирано задържане на сесията, първата заявка го заменя за сесийна бисквитка.",
"accessToken": "Достъп Токен",
"usageExamples": "Примери за използване",
"tokenId": "Токен ID",
@@ -196,8 +199,14 @@
"shareTitleOptional": "Заглавие (по избор)",
"sharePathOptional": "Път (по избор)",
"sharePathDescription": "След удостоверяване, линкът ще препрати потребителите на този път.",
"shareAssociateUserOptional": "Асоцииране на потребител (по избор)",
"shareAssociateUserDescription": "Когато е настроен, заявките използващи този линк се свързват с потребителя в достъпните логове и заглавия за идентичност. Линкът се премахва, ако потребителят напусне организацията.",
"userSelect": "Изберете потребител",
"usersNotFound": "Няма намерени потребители",
"expireIn": "Изтече",
"neverExpire": "Никога не изтича",
"sharePersistSession": "Задръж сесия след първо използване",
"sharePersistSessionDescription": "Когато е активирано, първата заявка с този токен чрез параметър на заявка или хедър създава сесийна бисквитка, така че следващите заявки не се нуждаят от токена. Оставете го изключено за клиенти на API, които трябва да изпращат токена при всяка заявка.",
"shareExpireDescription": "Времето на изтичане е колко дълго връзката ще бъде използваема и ще предоставя достъп до ресурса. След това време, връзката няма да работи и потребителите, които са я използвали, ще загубят достъп до ресурса.",
"shareSeeOnce": "Ще можете да видите този линк само веднъж. Уверете се, че го копирате.",
"shareAccessHint": "Всеки с тази връзка може да има достъп до ресурса. Споделяйте я с внимание.",
@@ -280,7 +289,21 @@
"noCountryFound": "Не е намерена държава.",
"siteSelectionDescription": "Този сайт ще осигури свързаност до целта.",
"resourceType": "Тип ресурс",
"resourceTypeDescription": "Това контролира протокола на ресурса и как той ще се визуализира в браузъра. Това не може да бъде променено по-късно.",
"resourceTypeDescription": "Това контролира протокола на ресурса и не може да се промени по-късно.",
"resourceTypeSearch": "Търсене на типове ресурси...",
"resourceTypeNotFound": "Не е намерен тип ресурс",
"resourceTypeHttpDescription": "Прокси на уеб трафик през HTTPS, използвайки име на домейн",
"resourceTypeInferenceDescription": "Направа на AI заявки чрез прикрепени доставчици",
"resourceTypeSshDescription": "Достъп до SSH сървър от уеб браузъра",
"resourceTypeRdpDescription": "Достъп до отдалечен работен плот от уеб браузъра",
"resourceTypeVncDescription": "Достъп до VNC десктоп от уеб браузъра",
"resourceTypeTcpDescription": "Прокси на суров TCP трафик, използвайки номер на порт",
"resourceTypeUdpDescription": "Прокси на суров UDP трафик, използвайки номер на порт",
"privateResourceTypeDescription": "Това контролира как клиентите достигат до ресурса. Това не може да бъде променено по-късно.",
"privateResourceTypeHostDescription": "Излагане на единичен хост в мрежата на сайта към свързани клиенти",
"privateResourceTypeCidrDescription": "Излагане на CIDR обхват в мрежата на сайта към свързани клиенти",
"privateResourceTypeHttpDescription": "Достъп до HTTP или HTTPS услуга чрез домейн",
"privateResourceTypeSshDescription": "Достъп до SSH сървър от свързани клиенти",
"resourceDomainDescription": "Ресурсът ще се обслужва на това напълно квалифицирано име на домейн.",
"resourceHTTPSSettings": "HTTPS настройки",
"resourceHTTPSSettingsDescription": "Конфигурирайте как ресурсът ще бъде достъпен по HTTPS",
@@ -443,6 +466,8 @@
"apiKeysDelete": "Изтрийте API ключа",
"apiKeysManage": "Управление на API ключове",
"apiKeysDescription": "API ключове се използват за удостоверяване с интеграционния API",
"orgsManage": "Управление на организации",
"orgsDescription": "Преглед и управление на всички организации в тази инстанция",
"provisioningKeysTitle": "Ключ за осигуряване",
"provisioningKeysManage": "Управление на ключове за осигуряване",
"provisioningKeysDescription": "Ключовете за осигуряване се използват за удостоверяване на автоматичното осигуряване на сайта за вашата организация.",
@@ -694,6 +719,7 @@
"nameOptional": "Име (по избор)",
"accessControls": "Контрол на достъпа",
"userDescription2": "Управление на настройките на този потребител",
"userGeneralSettingsDescription": "Управление на ролите и настройките на този потребител в организацията",
"accessRoleErrorAdd": "Неуспешно добавяне на потребител към роля",
"accessRoleErrorAddDescription": "Възникна грешка при добавяне на потребителя към ролята.",
"userSaved": "Потребителят е запазен",
@@ -849,12 +875,16 @@
"authMethodsSave": "Запази Настройки",
"policyAuthStackTitle": "Удостоверяване",
"policyAuthStackDescription": "Контролирайте кои методи за удостоверяване са необходими за достъп до този ресурс",
"policyAuthInferenceStackDescription": "Изберете кои потребители и роли могат да се удостоверят с този AI шлюз",
"policyAuthOrLogicTitle": "Множество активни методи за удостоверяване",
"policyAuthOrLogicBanner": "Посетителите могат да се удостоверяват чрез който и да е от активните методи по-долу. Не е необходимо да преминават през всичките.",
"policyAuthMethodActive": "Активен",
"policyAuthMethodOff": "Изключено",
"policyAuthSsoTitle": "Платформено SSO",
"policyAuthSsoDescription": "Изисква вход чрез удостоверител на вашата организация",
"policyAuthInferenceIdentityKeySignInUrl": "URL за вход",
"policyAuthInferenceIdentityKeyHelpNoUrl": "Всеки потребител вече има клавиш за API идентичност, така че трябва да създадете виртуални API ключове само за клиенти, които не са потребители или за споделен достъп. Потребителите могат да получат своя ключ, като влязат с доставчик на идентичност на URL адреса на този ресурс, където ще бъде показан след вход.",
"policyAuthInferenceIdentityKeyHelp": "Всеки потребител вече има клавиш за API идентичност, така че трябва да създадете виртуални API ключове само за клиенти, които не са потребители или за споделен достъп. Потребителите получават своя ключ тук след влизане с доставчик на идентичност.",
"policyAuthSsoSummary": "{idp} · {users} потребители, {roles} роли",
"policyAuthSsoDefaultIdp": "По подразбиране удостоверител",
"policyAuthAddDefaultIdentityProvider": "Добавете удостоверител по подразбиране",
@@ -886,7 +916,7 @@
"policyAccessRulesFallthroughOff": "Когато правилата са изключени, целият трафик преминава към удостоверяване.",
"policyAccessRulesFallthroughOn": "Когато няма съвпадение, трафикът преминава към удостоверяване.",
"rulesPlaceholderCidr": "10.0.0.0/8",
"rulesPlaceholderPath": "/admin/*",
"rulesPlaceholderPath": "/администратор/*",
"rulesPlaceholderGeo": "RU, KP",
"rulesSave": "Запазете правилата",
"resourceErrorCreate": "Грешка при създаване на ресурс",
@@ -928,7 +958,7 @@
"newtVersion": "Версия",
"architecture": "Архитектура",
"sites": "Сайтове",
"siteWgAnyClients": "Използвайте клиент на WireGuard, за да се свържете. Ще трябва да използвате вътрешните ресурси чрез IP адреса на връстника.",
"siteWgAnyClients": "Използвайте всеки WireGuard клиент, за да се свържете. Ще трябва да адресирате частни ресурси, използвайки IP на връстника.",
"siteWgCompatibleAllClients": "Съвместим с всички WireGuard клиенти",
"siteWgManualConfigurationRequired": "Необходима е ръчна конфигурация",
"userErrorNotAdminOrOwner": "Потребителят не е администратор или собственик",
@@ -1073,6 +1103,8 @@
"accessRoleErrorNewRequired": "Нова роля е необходима",
"accessRoleErrorRemove": "Неуспешно премахване на роля",
"accessRoleErrorRemoveDescription": "Възникна грешка при премахването на роля.",
"accessRoleInferenceBudget": "AI Бюджет",
"accessRoleInferenceBudgetDescription": "Конфигурирайте как членовете на тази роля ограничават използването на AI въз основа на лимити на разходи или жетони",
"accessRoleName": "Име на роля",
"accessRoleQuestionRemove": "Ще изтриете ролята `{name}`. Не можете да отмените това действие.",
"accessRoleRemove": "Премахни роля",
@@ -1148,6 +1180,10 @@
"idpJmespathAboutDescriptionLink": "Научете повече за JMESPath",
"idpJmespathLabel": "Идентификатор на пътя",
"idpJmespathLabelDescription": "Пътят към идентификатора на потребителя в ID токена",
"idpIdentifierChangeTitle": "Предупреждение за промяна на пътя на идентификатора",
"idpIdentifierChangeDescription": "Ще промените пътя на идентификатора. Това ще повлияе на начина, по който съществуващите потребители са разпределени. Потребители, които преди са влизали чрез този доставчик на идентичности, може вече да не бъдат разпознавани като същите потребители.",
"idpIdentifierChangeConfirmMessage": "Потвърждавам",
"idpIdentifierChangeWarningText": "Това ще повлияе на начина, по който съществуващите потребители са разпределени",
"idpJmespathEmailPathOptional": "Път за имейл (по избор)",
"idpJmespathEmailPathOptionalDescription": "Пътят до имейла на потребителя в ID токена",
"idpJmespathNamePathOptional": "Път (по избор) на име",
@@ -1392,6 +1428,24 @@
"logoutError": "Грешка при излизане",
"signingAs": "Влезли сте като",
"serverAdmin": "Администратор на сървъра",
"promoteServerAdmin": "Повишаване до администратор на сървъра",
"promoteServerAdminTitle": "Повишаване до Admin на сървъра",
"promoteServerAdminQuestion": "Сигурни ли сте, че искате да повишите {selectedUser} до администратор на сървъра?",
"promoteServerAdminMessage": "Администраторите на сървъра имат най-високите привилегии и могат да управляват сървъра.",
"promoteServerAdminWarning": "Това може да бъде отменено по всяко време посредством понижаване на потребителя.",
"promoteServerAdminConfirm": "Повишаване до Admin на сървъра",
"promoteServerAdminSuccess": "Потребителят е повишен",
"promoteServerAdminSuccessDescription": "{selectedUser} вече е администратор на сървъра.",
"promoteServerAdminError": "Неуспешно повишаване на потребител",
"demoteServerAdmin": "Понижаване от администратор на сървъра",
"demoteServerAdminTitle": "Понижаване от Admin на сървъра",
"demoteServerAdminQuestion": "Сигурни ли сте, че искате да понижите {selectedUser} от администратор на сървъра?",
"demoteServerAdminMessage": "{selectedUser} ще загуби всички привилегии на администратор на сървъра.",
"demoteServerAdminWarning": "Това може да бъде отменено по всяко време посредством повишаване на потребителя.",
"demoteServerAdminConfirm": "Понижаване от администратор на сървъра",
"demoteServerAdminSuccess": "Потребителят е понижен",
"demoteServerAdminSuccessDescription": "{selectedUser} вече не е администратор на сървъра.",
"demoteServerAdminError": "Неуспешно понижаване на потребител",
"managedSelfhosted": "Управлявано Самостоятелно-хоствано",
"otpEnable": "Включване на двуфакторен",
"otpDisable": "Изключване на двуфакторен",
@@ -1418,6 +1472,28 @@
"actionDeleteSite": "Изтриване на сайта",
"actionGetSite": "Вземете сайт",
"actionListSites": "Изброяване на сайтове",
"actionCreateAiProvider": "Създаване на AI Доставчик",
"actionDeleteAiProvider": "Изтриване на AI Доставчик",
"actionGetAiProvider": "Получаване на AI Доставчик",
"actionListAiProviders": "Списък на AI Доставчици",
"actionUpdateAiProvider": "Актуализиране на AI Доставчик",
"actionCreateAiModel": "Създаване на AI Модел",
"actionDeleteAiModel": "Изтриване на AI Модел",
"actionGetAiModel": "Получаване на AI Модел",
"actionListAiModels": "Списък на AI Модели",
"actionUpdateAiModel": "Актуализиране на AI Модел",
"actionCreateAiBudget": "Създаване на AI Бюджет",
"actionDeleteAiBudget": "Изтриване на AI Бюджет",
"actionGetAiBudget": "Получаване на AI Бюджет",
"actionListAiBudgets": "Списък на AI Бюджети",
"actionUpdateAiBudget": "Актуализиране на AI Бюджет",
"actionListResourceAiModels": "Списък на модели на AI ресурс",
"actionSetResourceAiModels": "Задаване на модели на AI ресурс",
"actionCreateVirtualApiKey": "Създаване на Виртуален API Ключ",
"actionDeleteVirtualApiKey": "Изтриване на Виртуален API Ключ",
"actionGetVirtualApiKey": "Получаване на Виртуален API Ключ",
"actionListVirtualApiKeys": "Списък на Виртуални API Ключове",
"actionUpdateVirtualApiKey": "Актуализиране на Виртуален API Ключ",
"actionApplyBlueprint": "Приложи Чернова",
"actionListBlueprints": "Списък с планове.",
"actionGetBlueprint": "Вземи план.",
@@ -1443,8 +1519,11 @@
"actionSetResourcePincode": "Задайте ПИН код на ресурса",
"actionSetResourceEmailWhitelist": "Задайте списък на одобрените имейл адреси за ресурса",
"actionGetResourceEmailWhitelist": "Вземете списък на одобрените имейл адреси за ресурса",
"actionListResourcePolicies": "Списък на ресурсни политики",
"actionCreateResourcePolicy": "Създаване на ресурсна политика",
"actionGetResourcePolicy": "Получете политика за ресурси",
"actionUpdateResourcePolicy": "Актуализирайте политика за ресурси",
"actionDeleteResourcePolicy": "Изтриване на ресурсна политика",
"actionSetResourcePolicyUsers": "Задайте потребители на ресурсна политика",
"actionSetResourcePolicyRoles": "Задайте роли на ресурсна политика",
"actionSetResourcePolicyPassword": "Задайте парола на ресурсна политика",
@@ -1520,6 +1599,8 @@
"search": "Търси…",
"searchPlaceholder": "Търсене...",
"emptySearchOptions": "Няма намерени опции",
"ipFilterSearchPlaceholder": "Въведете IP адрес…",
"ipFilterEmptyMessage": "Въведете IP адрес, за да филтрирате по него",
"create": "Създаване",
"orgs": "Организации",
"loginError": "Възникна неочаквана грешка. Моля, опитайте отново.",
@@ -1554,6 +1635,8 @@
"commandPaletteCreateMachineClient": "Създаване на машинен клиент",
"commandPaletteCreateAlertRule": "Създаване на правила за известия",
"commandPaletteCreateIdentityProvider": "Създаване на доставчик на идентичност",
"commandPaletteCreateAiProvider": "Създайте AI доставчик",
"commandPaletteCreateVirtualApiKey": "Създайте виртуален API ключ",
"commandPaletteToggleTheme": "Смяна на темата",
"commandPaletteChooseOrganization": "Изберете организация",
"commandPaletteShortcutMac": "⌘K",
@@ -1616,7 +1699,425 @@
"sidebarInvitations": "Покани",
"sidebarRoles": "Роли",
"sidebarShareableLinks": "Споделими връзки",
"sidebarAiGateway": "AI Шлюз",
"sidebarAiProviders": "Доставчици",
"commandAiProviders": "AI Доставчици",
"sidebarVirtualApiKeys": "Виртуални API Ключове",
"sidebarMyApiKeys": "Вашите API Ключове",
"sidebarAccount": "Стартер",
"commandVirtualApiKeys": "Виртуални API Ключове",
"virtualApiKeysTitle": "Управление на Виртуални API Ключове",
"virtualApiKeysDescription": "Създаване и управление на ръчни API ключове за достъп до AI шлюза до публични AI шлюзове",
"virtualApiKeysTabIdentity": "Идентификационни ключове",
"virtualApiKeysTabVirtual": "Виртуални ключове",
"virtualApiKeysIdentitySplashTitle": "Идентификационни ключове за всеки потребител",
"virtualApiKeysIdentitySplashDescription": "Всеки потребител вече има клавиш за идентичност Pangolin за тази организация. Той е уникален за неговия профил и го удостоверява при достъп до AI шлюзове.",
"virtualApiKeysIdentitySplashExample": "Пример",
"virtualApiKeysIdentitySplashRetrieveTitle": "Как получават своя ключ потребителите",
"virtualApiKeysIdentitySplashRetrieveResource": "Посетете публичен AI шлюз URL адрес в браузъра и влезте с профила си.",
"virtualApiKeysIdentitySplashRetrievePage": "Или отидете на <url></url> докато сте влезли.",
"virtualApiKeysIdentitySplashManual": "Можете да създадете допълнителни виртуални API ключове в таба Виртуални ключове. Тези ключове могат да бъдат обхванати до конкретни публични AI шлюзове и при необходимост свързани с потребител. Създаването на ключ веднага предоставя достъп до избраните публични AI шлюзове.",
"virtualApiKeysIdentitySplashGoToVirtual": "Ръчно създаване на ключ",
"virtualApiKeysEmailIdentity": "Имейл идентификационни ключове",
"virtualApiKeysEmailIdentityDescription": "Изпратете на всеки избран потребител или роля неговия ключ за идентичност Pangolin.",
"virtualApiKeysEmailIdentitySendAll": "Изпратете на всички потребители",
"virtualApiKeysEmailIdentitySendAllDescription": "Изпратете имейл на всеки член на организацията, който има акаунт имейл.",
"virtualApiKeysEmailIdentitySelectUsers": "Потребители",
"virtualApiKeysEmailIdentitySelectRoles": "Роли",
"virtualApiKeysEmailIdentitySubmit": "Изпращане на имейли",
"virtualApiKeysEmailIdentitySuccess": "Идентификационни ключове изпратени",
"virtualApiKeysEmailIdentitySuccessDescription": "Изпратени {sent} имейли.",
"virtualApiKeysEmailIdentitySkipped": "{skipped} потребители бяха прескочени, защото нямат имейл адрес.",
"virtualApiKeysEmailIdentityRecipientsRequired": "Изберете поне един потребител или роля, или изпратете на всички потребители.",
"virtualApiKeysEmailIdentityError": "Грешка при изпращането на идентификационни ключове",
"virtualApiKeysEmailIdentityErrorDescription": "Неуспех при изпращането на идентификационни ключове",
"virtualApiKeysBannerTitle": "Идентификационни ключове за всеки потребител",
"virtualApiKeysBannerDescription": "Всеки потребител вече има наличен идентификационен ключ на {keysUrl}. Можете също така да генерирате ръчно ключове тук, които предоставят директен достъп до публични AI шлюзове.",
"virtualApiKeysBannerButtonText": "Преглеждане на идентификационни ключове",
"virtualApiKeys": "Виртуални API Ключове",
"virtualApiKeysSearch": "Търсене на ключове...",
"virtualApiKeysCreate": "Създаване на Виртуален API Ключ",
"virtualApiKeysCreateDescription": "Секретен ключ за всички публични AI шлюзове в тази организация",
"virtualApiKeysCreateButton": "Създаване на ключ",
"virtualApiKeysEmpty": "Все още няма Виртуални API Ключове",
"virtualApiKeysName": "Име",
"virtualApiKeysDescriptionOptional": "Описание (по избор)",
"virtualApiKeysAssociateUserOptional": "Свързване на потребител (по избор)",
"virtualApiKeysAssociateUserDescription": "Свържете този ключ с потребител, за да проследя появата. След създаването този ключ веднага предоставя достъп до избраните публични AI шлюзове. Не е необходимо да свързвате потребител ръчно, за да бъде видим ресурсът. Ключът ще се появи и в профила на потребителя.",
"virtualApiKeysAllResources": "Всички публични AI шлюзове",
"virtualApiKeysAllResourcesDescription": "Позволете на този ключ да има достъп до всеки публичен AI шлюз в организацията",
"virtualApiKeysSelectResources": "Публични AI шлюзове",
"virtualApiKeysSelectResourcesPlaceholder": "Избор на ресурси",
"virtualApiKeysSelectResourcesDescription": "Изберете кои публични AI шлюзове този ключ може да има достъп",
"virtualApiKeysNoResources": "Няма ресурси",
"virtualApiKeysSecret": "Ключ",
"virtualApiKeysCopyKey": "Копирайте този ключ. Можете да го видите отново по-късно от таблицата или при редактиране.",
"virtualApiKeysViewSecret": "Преглед на секрет",
"virtualApiKeysViewSecretTitle": "Секрет за Виртуален API Ключ",
"virtualApiKeysViewSecretDescription": "Този секрет предоставя достъп до публичните AI шлюзове, присвоени на този ключ",
"virtualApiKeysEdit": "Редактиране на Виртуален API Ключ",
"virtualApiKeysEditDescription": "Актуализирайте свързвания потребител и достъпа до публичния AI шлюз за този ключ",
"virtualApiKeysSaveButton": "Запазване на промените",
"virtualApiKeysSelectResourcesRequired": "Изберете поне един публичен AI шлюз, или активирайте всички публични AI шлюзове",
"virtualApiKeysUpdated": "Виртуален API ключ е актуализиран",
"virtualApiKeysUpdatedDescription": "Виртуалният API ключ е актуализиран",
"virtualApiKeysErrorUpdate": "Грешка при актуализация на виртуален API ключ",
"virtualApiKeysErrorUpdateDescription": "Неуспех при актуализация на виртуален API ключ",
"virtualApiKeysErrorCreate": "Грешка при създаване на виртуален API ключ",
"virtualApiKeysErrorCreateDescription": "Неуспех при създаване на виртуален API ключ",
"virtualApiKeysErrorDelete": "Грешка при изтриване на виртуален API ключ",
"virtualApiKeysErrorDeleteMessage": "Неуспех при изтриването на виртуален API ключ",
"virtualApiKeysDeleted": "Виртуален API ключ е изтрит",
"virtualApiKeysDeletedDescription": "Виртуалният API ключ е изтрит",
"virtualApiKeysDelete": "Изтриване на Виртуален API Ключ",
"virtualApiKeysDeleteConfirm": "Изтриване на ключ",
"virtualApiKeysQuestionRemove": "Сигурни ли сте, че искате да изтриете този виртуален API ключ?",
"virtualApiKeysMessageRemove": "Клиентите, които използват този ключ, ще загубят достъп веднага.",
"virtualApiKeysErrorFetchSecret": "Грешка при зареждане на секрет",
"virtualApiKeysErrorFetchSecretDescription": "Неуспех при зареждането на секрета на виртуалния API ключ",
"virtualApiKeysFilterUnassigned": "Неопределен",
"virtualApiKeysInferenceBudget": "Бюджет",
"virtualApiKeysInferenceBudgetDescription": "Конфигуриране как този ключ ограничава използването на AI въз основа на лимити на разходи или жетони",
"virtualApiKeysEmailOnGenerate": "Имейл ключ при генериране",
"virtualApiKeysEmailThisKey": "Имейл този ключ",
"virtualApiKeysEmailOnGenerateDescription": "Изпратете ключа на свързания потребител и допълнителни адреси след създаването му",
"virtualApiKeysEmailThisKeyDescription": "Изпратете текущия ключ на свързания потребител и допълнителни адреси",
"virtualApiKeysEmailSmtpRequired": "Имейлът не е конфигуриран на този сървър",
"virtualApiKeysEmailSmtpRequiredDescription": "Конфигурирайте SMTP за изпращане на виртуални API ключове.",
"virtualApiKeysEmailSendToUser": "Изпратете на свързан потребител",
"virtualApiKeysEmailSendToUserDescription": "Изпратете ключът на акаунтния имейл на свързания потребител",
"virtualApiKeysEmailSendToUserDisabled": "Свържете потребител за изпращане на ключа на този потребител",
"virtualApiKeysEmailAdditional": "Допълнителни имейли",
"virtualApiKeysEmailAdditionalPlaceholder": "Добавете имейл и натиснете Enter",
"virtualApiKeysEmailRecipientsRequired": "Изберете свързания потребител или добавете поне един имейл адрес",
"myVirtualApiKeysTitle": "Вашите API Ключове",
"myVirtualApiKeysDescription": "Прегледайте вашия идентификационен ключ и всички виртуални API ключове, които ви се отнасят в тази организация",
"myVirtualApiKeysResourceTitle": "Вашите API Ключове за {resourceName}",
"myVirtualApiKeysResourceDescription": "Прегледайте вашия идентификационен ключ и виртуални API ключове, които ви се отнасят и могат да достъпят {resourceName}",
"myVirtualApiKeysIdentityTitle": "Идентификационен ключ",
"myVirtualApiKeysIdentityHeadline": "Вашият Личен API Ключ",
"myVirtualApiKeysIdentityDescription": "Вашият личен ключ за тази организация. Той е уникален за вашия акаунт и се използва за идентификация при обаждания на AI Gateway ресурси.",
"myVirtualApiKeysIdentityResourceHeadline": "Вашият Личен API Ключ за {resourceName}",
"myVirtualApiKeysIdentityResourceDescription": "Вашият личен ключ за тази организация. Използвайте го, за да извикате {resourceName}.",
"myVirtualApiKeysManualTitle": "Свързани ключове",
"myVirtualApiKeysManualDescription": "Ръчни виртуални API ключове, свързани от администратор към вашия акаунт",
"myVirtualApiKeysManualResourceDescription": "Ръчни виртуални API ключове, свързани с вашия акаунт, които могат да достъпят {resourceName}",
"myVirtualApiKeysManualEmpty": "Все още няма приписани ключове",
"myVirtualApiKeysKindUser": "Идентичност",
"myVirtualApiKeysKindManual": "Ръчен",
"myVirtualApiKeysUnnamed": "Неназован ключ",
"myVirtualApiKeysRevealSecret": "Разкриване на секрет",
"myVirtualApiKeysViewSecretDescription": "Този секрет ви идентифицира за AI Gateway ресурси",
"aiClientConfigTitle": "Конфигуриране на кодиращи агенти",
"aiClientConfigDescription": "Копирайте конфигурацията за популярни кодиращи агенти или оставете Pangolin CLI да я настрои автоматично за вас.",
"aiClientConfigDescriptionClaude": "Инструментът за кодиране на Anthropic за терминала.",
"aiClientConfigDescriptionCodex": "Инструментът за кодиране на OpenAI за терминала.",
"aiClientConfigDescriptionOpencode": "Отворен кодиращ агент за терминал.",
"aiClientConfigDescriptionGemini": "Агентски инструмент на Google за кодиране на терминал.",
"aiClientConfigSetup": "Настройка",
"aiClientConfigTabCli": "Автоматичен (CLI)",
"aiClientConfigTabManual": "Ръчна конфигурация",
"aiClientConfigPreset": "Конфигурация",
"aiClientConfigStep": "Стъпка {number}",
"aiClientConfigEndpointPlaceholder": "https://example.resource.url.com",
"aiClientConfigPlaceholderWarning": "Този фрагмент включва примерни стойности, като име на модел или URL адрес на ресурс. Заменете ги със собствени, преди да го използвате.",
"aiClientConfigRevealError": "Не можа да зареди вашия API ключ.",
"aiClientConfigRevealRetry": "Опитайте отново",
"resourceGeneralAiClientConfigDescription": "Този ресурс може да се използва от общи клиенти като Claude Code и OpenCode. <configLink>Научете повече</configLink>",
"aiProvidersTitle": "AI Доставчици",
"aiProvidersDescription": "Свързване на модели доставчици за AI задачи в тази организация",
"aiProvidersBannerTitle": "Свързване на модели доставчици",
"aiProvidersBannerDescription": "Доставчиците са бекендите на модела, които Pangolin използва за AI задачи. Свържете OpenAI, Anthropic и други доставчици тук, след това ги прикачете към публични AI шлюзове, така че потребителите да могат да извикват модели с идентичност-назнаван достъп, бюджети и логиране.",
"aiProvidersAdd": "Добавяне на Доставчик",
"aiProvidersSearch": "Търсене на доставчици...",
"aiProvidersEmpty": "Все още няма AI доставчици",
"aiProviderCreate": "Създаване на AI Доставчик",
"aiProviderCreateDescription": "Добавяне на модел доставчик за тази организация",
"aiProviderSeeAll": "Преглед на всички доставчици",
"aiProviderSetting": "Настройки на доставчика за {providerName}",
"aiProviderSettingDescription": "Конфигурирайте този AI доставчик",
"aiProviderGeneral": "Общи",
"aiProviderGeneralDescription": "Основни настройки за този доставчик",
"aiProviderConfiguration": "Конфигурация",
"aiProviderConfigurationDescription": "Мрежов маршрутизация и удостоверяване за този доставчик",
"aiProviderNetworkSettings": "Мрежови настройки",
"aiProviderNetworkSettingsDescription": "Избор как трафикът достига до този доставчик",
"aiProviderAuthSettings": "Удостоверяване",
"aiProviderAuthSettingsDescription": "Конфигурирайте как този доставчик удостоверява заявки до своя URL",
"aiProviderBudgetSettings": "Бюджет",
"aiProviderBudgetSettingsDescription": "Конфигурирайте как този доставчик ограничава използването въз основа на разходи или жетонни лимити",
"aiBudgetAdd": "Добавяне на Бюджет",
"aiBudgetEmpty": "Все още няма конфигурирани бюджети. Щракнете върху Добавяне на Бюджет, за да настроите лимит на разходи или жетони.",
"aiBudgetUnit": "Вид на разхода",
"aiBudgetPeriod": "Период на нулиране",
"aiBudgetAmount": "Максимален разход",
"aiBudgetAmountPlaceholder": "Максимален разход",
"aiBudgetPeriodHourly": "Часов",
"aiBudgetPeriodDaily": "Дневен",
"aiBudgetPeriodWeekly": "Седмичен",
"aiBudgetPeriodMonthly": "Месечен",
"aiBudgetPeriodYearly": "Годишен",
"aiBudgetPeriodLifetime": "Пожизнен",
"aiBudgetUnitUsd": "USD",
"aiBudgetUnitTokens": "Жетони",
"aiBudgetConflictError": "Бюджет за този период на нулиране и вид на разхода вече съществува",
"aiBudgetInvalidAmountError": "Въведете максимален разход, по-голям от 0",
"aiBudgetUpdated": "Актуализирани бюджети",
"aiBudgetErrorSave": "Неуспех при актуализация на бюджети",
"aiProviderType": "Вид на доставчика",
"aiProviderTypeSearch": "Търсене на доставчици...",
"aiProviderTypeNotFound": "Не е намерен вид доставчик",
"aiProviderTypeOpenai": "OpenAI",
"aiProviderTypeAnthropic": "Anthropic",
"aiProviderTypeGoogleGemini": "Google Gemini",
"aiProviderTypeVertexAi": "Vertex AI",
"aiProviderTypeBedrock": "Amazon Bedrock",
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
"aiProviderTypeOpenRouter": "OpenRouter",
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
"aiProviderTypeCustom": "Персонализиран",
"aiProviderTypeOpenaiDescription": "OpenAI API",
"aiProviderTypeAnthropicDescription": "Anthropic API",
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
"aiProviderTypeBedrockDescription": "Amazon Bedrock Рунтайм API",
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
"aiProviderTypeCustomDescription": "Използвайте собствена крайна точка или маршрутирайте чрез цели на сайта",
"aiProviderUpstreamUrl": "Възходящ URL адрес",
"aiProviderUpstreamUrlDescription": "Основен URL за API на доставчика",
"aiProviderUpstreamUrlOptionalDescription": "Оставете празно, за да използвате стандартния възходящ URL за този доставчик",
"aiProviderEffectiveUpstreamUrl": "Ефективен възходящ URL адрес",
"aiProviderApiKey": "API ключ",
"aiProviderApiKeyDescription": "API ключ, използван за удостоверяване на заявки към този доставчик",
"aiProviderCustomHeadersDescription": "Заглавки, изпратени при всяка заявка към този доставчик. Отделени със нов ред: Име-на-заглавие: стойност",
"aiProviderApiKeyLastChars": "API ключ",
"aiProviderAuthType": "Тип удостоверяване",
"aiProviderAuthTypeSearch": "Търсене на типове удостоверяване...",
"aiProviderAuthTypeNotFound": "Не е намерен тип удостоверяване",
"aiProviderAuthTypeBearer": "Bearer",
"aiProviderAuthTypeBearerDescription": "Удостоверяване: Bearer ключ. Използва се от OpenAI и повечето доставчици",
"aiProviderAuthTypeXApiKey": "x-api-key",
"aiProviderAuthTypeXApiKeyDescription": "x-api-key заглавие. Използва се от Anthropic",
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key заглавие. Използва се от Google Gemini",
"aiProviderAuthTypeHec": "Splunk HEC",
"aiProviderAuthTypeHecDescription": "Удостоверяване: Splunk ключ. Използва се от Splunk HTTP Event Collector",
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Bearer ключ. Използва се от Cloudflare AI Gateway",
"aiProviderAuthTypeNone": "Без удостоверяване",
"aiProviderAuthTypePassthrough": "Премини",
"aiProviderAuthTypeDescription": "Как възходящото API удостоверява заявките",
"aiProviderAuthTypePassthroughDescription": "Препрати заглавките на API ключа на повикващия към възходящото",
"aiProviderAuthTypeNoneDescription": "Не изпращайте заглавки за удостоверяване към възходящото",
"aiProviderRoutingMode": "Режим на маршрутиране",
"aiProviderRoutingModeDescription": "Изпрати трафик към възходящ URL или HTTP цели на вашите сайтове",
"aiProviderRoutingModeUrl": "Възходящ URL адрес",
"aiProviderRoutingModeUrlDescription": "Обадете се на публичен или частен API основен URL",
"aiProviderRoutingModeTarget": "Цели на сайта",
"aiProviderRoutingModeTargetDescription": "Маршрутиране чрез цели на вашите сайтове",
"aiProviderRoutingModeTargetNote": "След създаването на този доставчик, конфигурирайте целите на сайта в раздела Настройки на мрежата.",
"aiProviderTargetNoOne": "Този доставчик няма цели. Добавете цел за маршрутиране на заявки чрез вашите сайтове.",
"aiProviderRemoteNodeTargetsWarning": "Уебсайтовете, свързани с отдалечени възли, са недостъпни за пренасочване към AI Gateway доставчици.",
"aiProviderSkipTlsVerification": "Пропуснете проверката на TLS",
"aiProviderSkipTlsVerificationDescription": "Деактивирайте проверката на TLS сертификат за възходящото свързване",
"aiProviderBudget": "Бюджет",
"aiProviderBudgetDescription": "Факултативен лимит за разходи или маркер за този доставчик",
"aiProviderBudgetAmount": "Сума на бюджета",
"aiProviderBudgetUnit": "Единица на бюджета",
"aiProviderBudgetUnitUsd": "USD",
"aiProviderBudgetUnitTokens": "Токени",
"aiProviderEnabled": "Активиран",
"aiProviderEnabledDescription": "Напълно деактивирайте този доставчик за всички ресурси",
"aiProviderErrorCreate": "Неуспешно създаване на AI доставчик",
"aiProviderErrorUpdate": "Неуспешно актуализиране на AI доставчик",
"aiProviderErrorDelete": "Неуспешно изтриване на AI доставчик",
"aiProviderErrorLoad": "Неуспешно зареждане на AI доставчик",
"aiProviderErrorUpstreamUrlInvalid": "Въведете валиден възходящ URL адрес",
"aiProviderErrorUpstreamUrlRequired": "Възходящ URL адрес е задължителен за този доставчик",
"aiProviderErrorAuthTypeRequired": "Необходим е тип за удостоверяване",
"aiProviderErrorApiKeyRequired": "API ключът е задължителен",
"aiProviderErrorRoutingModeTarget": "Маршрутизацията на целите на сайта е налична само за персонализирани доставчици",
"aiProviderErrorCapabilitiesRequired": "Изберете поне една възможност на API",
"aiProviderCapabilities": "Възможности на API",
"aiProviderCapabilitiesDescription": "Изберете, които API формати може да управлява този доставчик. Известни доставчици започват с препоръчани настройки по подразбиране.",
"aiProviderCapabilitiesCustomDescription": "Изберете кои формати на API този персонализиран доставчик може да обработва",
"aiProviderCapabilitiesSelect": "Изберете възможности",
"aiProviderCapabilitiesEmpty": "Няма намерени възможности",
"aiProviderCapabilitiesSearch": "Търсене на възможности...",
"aiCapabilityOpenaiChat": "OpenAI Чат Завършвания",
"aiCapabilityOpenaiChatDescription": "Поддържа /v1/chat/completions",
"aiCapabilityOpenaiResponses": "OpenAI Отговори",
"aiCapabilityOpenaiResponsesDescription": "Поддържа /v1/responses",
"aiCapabilityAnthropicMessages": "Anthropic Съобщения",
"aiCapabilityAnthropicMessagesDescription": "Поддържа /v1/messages",
"aiCapabilityV1Models": "Списък на модели",
"aiCapabilityV1ModelsDescription": "Поддържа /v1/models откриване на модели",
"aiCapabilityGeminiGenerateContent": "Gemini Генериране на Съдържание",
"aiCapabilityGeminiGenerateContentDescription": "Поддържа директния Gemini API",
"aiCapabilityBedrockModelInvoke": "Бедрок Модел Активирай",
"aiCapabilityBedrockModelInvokeDescription": "Поддържа Amazon Bedrock InvokeModel",
"aiCapabilityGoogleGenerateContent": "Vertex Генериране на Съдържание",
"aiCapabilityGoogleGenerateContentDescription": "Поддържа Vertex AI Gemini формат",
"aiCapabilityGoogleRawPredict": "Vertex Сурово Предсказване",
"aiCapabilityGoogleRawPredictDescription": "Поддържа Vertex AI rawPredict за Anthropic модели",
"aiCapabilityBedrockConverse": "Бедрок Разговор",
"aiCapabilityBedrockConverseDescription": "Поддържа Amazon Bedrock Converse API",
"aiProviderCreated": "AI доставчикът е създаден",
"aiProviderUpdated": "AI доставчикът е обновен",
"aiProviderDeleted": "AI доставчикът е изтрит",
"aiProviderDelete": "Изтриване на Доставчик",
"aiProviderDeleteConfirm": "Изтриване на Доставчик",
"aiProviderQuestionRemove": "Сигурни ли сте, че искате да изтриете този AI доставчик?",
"aiProviderMessageRemove": "Това ще изтрие постоянно доставчика и неговите модели и цели. Това не може да бъде отменено.",
"aiProviderErrorNoUpdate": "AI доставчикът не е наличен за обновяване",
"aiProviderModels": "Модели",
"aiProviderModelsDescription": "Определете позволителните и блокиращите списъци за този доставчик. Заявките трябва да съвпадат с позволителен запис и да не съвпадат с блокиращ запис.",
"aiProviderCreateModelsDescription": "Изберете кои модели този доставчик може да обслужва. Празен позволителен списък осъжда целия трафик. Можете да добавите или промените модели по-късно.",
"aiProviderModelsPlaceholder": "Търсене на модели или въведете персонален ключ",
"aiProviderModelsAllow": "Позволителен Списък",
"aiProviderModelsAllowDescription": "Модели, които могат да бъдат използвани чрез този доставчик. Празно означава отричане на всички.",
"aiProviderModelsAllowPlaceholder": "Въведете ключ на модела",
"aiProviderModelsAllowEmpty": "Все още няма добавени модели.",
"aiProviderModelsBlock": "Блокиращ Списък",
"aiProviderModelsBlockDescription": "Модели за отричане, дори ако съвпадат с позволителен запис.",
"aiProviderModelsBlockPlaceholder": "Въведете ключ на модела",
"aiProviderModelsBlockEmpty": "Все още няма добавени модели.",
"aiProviderModelsAdd": "Добавяне на Модели",
"aiProviderModelsClearAll": "Изчистване на Всички",
"aiProviderModelsAddCustom": "Добавяне \"{key}\"",
"aiProviderModelsAddCustomHint": "Натиснете Enter, за да добавите този персонален ключ на модел.",
"aiProviderModelsAddBulk": "Добавяне на {count} персонални ключове",
"aiProviderModelsAddBulkHint": "Натиснете Enter, за да добавите {count} персонални ключове.",
"aiProviderModelsAddOne": "Добавяне {key} сега",
"aiProviderModelsAddSelected": "Добавяне на Избраните",
"aiProviderModelsSelectedCount": "{count} избрани",
"aiProviderModelsSelectAll": "Избери всички",
"aiProviderModelsClearSelected": "Изчистване",
"aiProviderModelsBulkHint": "Изберете известни модели или въведете персонален ключ.",
"aiProviderModelsCatalogEmpty": "Няма съвпадение с каталога на модели.",
"aiProviderModelsCatalogHeading": "Известни Модели",
"aiProviderModelsAllLabel": "Всички модели",
"aiProviderModelsAllPatternHint": "Joker: *",
"aiProviderModelsAddAllAllow": "Позволете всички модели",
"aiProviderModelsAddAllBlock": "Блокирайте всички модели",
"aiProviderModelsAddAllDescription": "Използва * joker, така че всеки ключ на модел съвпада.",
"aiProviderModelsViewMore": "Вижте още ({count})",
"aiProviderModelsViewLess": "Вижте по-малко",
"aiProviderModelsRemove": "Премахване на модел",
"aiProviderModelsEditHint": "Натиснете, за да редактирате настройките на модела",
"aiProviderModelsSourceCatalog": "Известен каталогов модел",
"aiProviderModelsSourceCustom": "Персонализиран ключ на модел",
"aiProviderModelsSourcePattern": "Модел за съвпадение",
"aiProviderModelsSourceAll": "Съвпада с всеки ключ на модел",
"aiProviderModelsBudgetConfigured": "Конфигуриран бюджет",
"aiProviderModelsEditTitle": "Редактиране на Модел",
"aiProviderModelsEditDescription": "Обновете ключа на модела или конфигурирайте бюджета му.",
"aiProviderModelsBudgetTab": "Бюджет",
"aiProviderModelsKeyLabel": "Ключ на модел",
"aiProviderModelsKeyRequired": "Въведете ключ на модел",
"aiProviderModelsKeyDuplicate": "Този ключ на модел вече е в списък",
"aiProviderModelsOverlapError": "Тези образци не могат да бъдат в двата списъка: {keys}",
"aiProviderModelsUpdated": "Моделите са обновени",
"aiProviderModelsErrorUpdate": "Неуспешно обновяване на модели",
"aiResourceProviders": "Доставчици",
"aiResourceProvidersDescription": "Изберете кои AI доставчици този AI портал може да използва",
"aiResourceProvidersHelp": "Добавете доставчици и изберете наследяване (използване на списъците на всеки доставчик) или избор (изберете разрешителен списък за този ресурс). Разрешителните образци, които се конфликтират между прикрепени доставчици, не са разрешени.",
"aiResourceProvidersSelect": "Изберете доставчици",
"aiResourceProvidersEmpty": "Не са намерени AI доставчици",
"aiResourceProvidersNoneAttached": "Все още няма прикрепени доставчици.",
"aiResourceProvidersAdd": "Добавяне на доставчик",
"aiResourceProvidersRemove": "Премахване на доставчик",
"aiResourceProviderToggleEnabled": "Разрешете или забранете този доставчик на ресурса",
"aiResourceProviderDisabled": "Деактивиран",
"aiResourceProvidersUpdated": "Доставчиците са обновени",
"aiResourceProvidersErrorUpdate": "Неуспешно обновяване на доставчици",
"aiResourceProviderEditDescription": "Изберете как да се изложат моделите на този доставчик върху този ресурс.",
"viewProviderSettings": "Преглед на настройки на доставчик",
"aiResourceProviderMode": "Режим на достъп",
"aiResourceProviderModeInherit": "Наследяване",
"aiResourceProviderModeSelect": "Изберете",
"aiResourceProviderModeSelectSummary": "Изберете · {count} модели",
"aiResourceProviderModeInheritHelp": "Използвайте разрешителните и блокиращите списъци на този доставчик според конфигурацията.",
"aiResourceProviderModeSelectHelp": "Изберете подмножество на моделите с разрешителния списък на този доставчик за този ресурс.",
"aiResourceProviderAllowModels": "Разрешителен списък",
"aiResourceProviderAllowModelsSelect": "Изберете модели",
"aiResourceProviderAllowModelsSearch": "Търсене на модели...",
"aiResourceProviderAllowModelsEmpty": "Няма намерени модели",
"aiResourceProviderAllowModelsHelp": "Само модели от разрешителния списък на този доставчик могат да бъдат избрани.",
"aiResourceAliasRequired": "Псевдоним е необходим за AI портали",
"aiResourceDomainConfiguration": "Конфигурация на Домейн",
"aiResourceDomainConfigurationDescription": "Изберете домейна, който клиентите ще използват, за да достигнат до този AI портал.",
"aiUsageAnalyticsTitle": "Аналитика на AI Използването",
"aiUsageAnalyticsDescription": "Анализирайте разходите, употребата на токени и активността на AI портал през доставчици, ресурси, роли и потребители",
"aiUsageTabOverview": "Общ поглед",
"aiUsageTabProviders": "Доставчици",
"aiUsageTabResources": "Ресурси",
"aiUsageFilterProvider": "Доставчик",
"aiUsageFilterModel": "Модел",
"aiUsageFilterResource": "Ресурс",
"aiUsageFilterRole": "Роля",
"aiUsageFilterUser": "Потребител",
"aiUsageFilterAllProviders": "Всички Доставчици",
"aiUsageFilterAllModels": "Всички Модели",
"aiUsageFilterAllResources": "Всички Ресурси",
"aiUsageFilterAllRoles": "Всички Роли",
"aiUsageFilterAllUsers": "Всички Потребители",
"aiUsageFilterSearch": "Търсене...",
"aiUsageFilterNotFound": "Не са намерени опции",
"aiUsageResetFilters": "Възстановяване на Филтрите",
"aiUsageRefresh": "Обновяване",
"aiUsageTokenTypePrompt": "Подканя",
"aiUsageTokenTypeCacheRead": "Четене от кеш",
"aiUsageTokenTypeCacheWrite": "Писане в кеш",
"aiUsageTokenTypeCompletion": "Завършване",
"aiUsageTokenTypeReasoning": "Разсъждение",
"aiUsageRequests": "Запитвания",
"aiUsageCost": "Разход",
"aiUsageTokens": "Токени",
"aiUsageOther": "Друго",
"aiUsageTotalRequests": "Общо Запитвания",
"aiUsageTotalTokens": "Общо Токени",
"aiUsageTotalCost": "Общ Разход",
"aiUsageEstimated": "Оценени",
"aiUsageRequestVolume": "Обем на Запитване",
"aiUsageTokenUsage": "Употреба на Токени",
"aiUsageModelCost": "Модел Разход",
"aiUsageModelTokens": "Модел Токени",
"aiUsageTopModels": "Топ Модели",
"aiUsageTopProviders": "Топ Доставчици",
"aiUsageProviderCost": "Разход на Доставчик",
"aiUsageProviderTokenUsage": "Употреба на Доставчик на Токени",
"aiUsageTopResources": "Топ Ресурси",
"aiUsageResourceCost": "Разход на Ресурс",
"aiUsageResourceTokenUsage": "Употреба на Токени на Ресурс",
"aiUsageResourceTypePublic": "Публичен Ресурс",
"aiUsageResourceTypeSite": "Частен Ресурс",
"aiUsageNoResource": "Няма ресурс",
"aiUsageRolesTab": "Роли",
"aiUsageUsersTab": "Потребители",
"aiUsageTopRoles": "Топ Роли",
"aiUsageRoleCost": "Разход на Роля",
"aiUsageRoleTokenUsage": "Употреба на Токени на Роля",
"aiUsageTopUsers": "Топ Потребители",
"aiUsageUserCost": "Разход на Потребител",
"aiUsageUserTokenUsage": "Употреба на Токени на Потребител",
"aiUsageUnknownUser": "Непознат потребител",
"aiUsageVirtualApiKeysTab": "Виртуални API Ключове",
"aiUsageFilterVirtualApiKey": "Виртуален API Ключ",
"aiUsageFilterAllVirtualApiKeys": "Всички Виртуални API Ключове",
"aiUsageTopVirtualApiKeys": "Топ Виртуални API Ключове",
"aiUsageVirtualApiKeyCost": "Разход на Виртуален API Ключ",
"aiUsageVirtualApiKeyTokenUsage": "Употреба на Токени на Виртуален API Ключ",
"aiUsageUnknownVirtualApiKey": "Няма виртуален API ключ",
"aiUsageUnnamedVirtualApiKey": "Неназован ключ",
"aiUsageLoading": "Зареждане...",
"aiUsageNoData": "Няма данни",
"resourceBudgetSettings": "Бюджет",
"resourceBudgetSettingsDescription": "Конфигурирайте как AI порталът ограничава употребата, основавайки се на разходи или лимити на токени",
"sidebarApiKeys": "API ключове",
"sidebarOrgs": "Организации",
"sidebarProvisioning": "Осигуряване",
"sidebarSettings": "Настройки",
"sidebarAllUsers": "Всички потребители",
@@ -1689,6 +2190,8 @@
"alertingRuleSaved": "Правилото за предупреждение е запазено",
"alertingRuleSavedCreatedDescription": "Вашето ново правило за предупреждение беше създадено. Все още можете да го редактирате на тази страница.",
"alertingRuleSavedUpdatedDescription": "Промените, направени по това правило за предупреждение, бяха запазени.",
"alertingTestAlertSent": "Изпратено е тестово предупреждение",
"alertingTestAlertSentDescription": "Тестово предупреждение беше изпратено до действията, конфигурирани по това правило.",
"alertingEditRule": "Редактиране на правило за предупреждение",
"alertingCreateRule": "Създаване на правило за предупреждение",
"alertingRuleCredenzaDescription": "Изберете какво да наблюдавате, кога да се активира и как да уведомите",
@@ -1798,6 +2301,12 @@
"alertingRulesBannerDescription": "Всяко правило свързва това, което да се наблюдава (сайт, проверка на състоянието или ресурс), кога да се активира (например офлайн или нездраве) и как да уведомите екипа чрез имейл, уеб куки или интеграции. Използвайте този списък, за да създавате, активирате и управлявате тези правила.",
"alertingHealthChecksBannerTitle": "Наблюдавайте здравето и ресурсите",
"alertingHealthChecksBannerDescription": "Проверките на състоянието са HTTP или TCP монитори, които определяте веднъж. След това можете да ги използвате като източници в правила за предупреждения, така че да бъдете уведомени, когато целта стане здраве или нездраве. Проверките на състоянието на ресурсите също се появяват тук.",
"alertingTestRule": "Правило за тестово предупреждение",
"alertingAddActionHeading": "Добавете ново действие",
"alertingSelectActionType": "Изберете действие",
"alertingNoActionsTitle": "Не са конфигурирани действия",
"alertingNoActionsSaveDescription": "Добавете поне едно действие, за да може това правило да уведоми някого, когато се изпълни.",
"alertingNoActionsTestDescription": "Добавете поне едно действие, преди да можете да тествате това правило.",
"standaloneHcTableTitle": "Проверки на състоянието",
"standaloneHcSearchPlaceholder": "Търсене на проверки на състоянието…",
"standaloneHcAddButton": "Създаване на проверка на състоянието",
@@ -1989,6 +2498,9 @@
"domainPickerSubdomain": "Поддомейн: {subdomain}",
"domainPickerNamespace": "Име на пространство: {namespace}",
"domainPickerShowMore": "Покажи повече",
"domainPickerNoDomainsAvailableTitle": "Няма налични домейни",
"domainPickerNoDomainsAvailableDescription": "Все още нямате конфигурирани домейни. Създайте домейн, за да продължите.",
"domainPickerNoDomainsAvailableAction": "Отидете на Домейни",
"regionSelectorTitle": "Избор на регион",
"domainPickerRemoteExitNodeWarning": "Предоставените домейни не се поддържат, когато сайтовете се свързват към отдалечени крайни възли. За да бъдат ресурсите налични на отдалечени възли, използвайте персонализиран домейн вместо това.",
"regionSelectorInfo": "Изборът на регион ни помага да предоставим по-добра производителност за вашето местоположение. Не е необходимо да сте в същия регион като сървъра.",
@@ -2113,6 +2625,7 @@
"createDomainType": "Тип:",
"createDomainName": "Име:",
"createDomainValue": "Стойност:",
"multiSelectFilterCount": "{count} избрани",
"createDomainCnameRecords": "CNAME записи",
"createDomainARecords": "A записи",
"createDomainRecordNumber": "Запис {number}",
@@ -2184,6 +2697,8 @@
"subnetPlaceholder": "Мрежа",
"addressDescription": "Вътрешният адрес на клиента. Трябва да пада в подмрежата на организацията.",
"selectSites": "Избор на сайтове",
"selectResources": "Изберете ресурси",
"multiResourcesSelectorResourcesCount": "{count, plural, one {# ресурс} other {# ресурси}}",
"selectLabels": "Изберете етикети",
"sitesDescription": "Клиентът ще има връзка с избраните сайтове",
"clientInstallOlm": "Инсталиране на Olm",
@@ -2225,6 +2740,7 @@
"healthScheme": "Метод",
"healthSelectScheme": "Избор на метод",
"healthCheckPortInvalid": "Портът трябва да бъде между 1 и 65535",
"healthCheckHostnameInvalid": "Името на хоста не трябва да съдържа празни символи",
"healthCheckPath": "Път",
"healthHostname": "IP / Хост",
"healthPort": "Порт",
@@ -2236,10 +2752,11 @@
"timeIsInSeconds": "Времето е в секунди",
"requireDeviceApproval": "Изискват одобрение на устройства",
"requireDeviceApprovalDescription": "Потребители с тази роля трябва да имат нови устройства одобрени от администратор преди да могат да се свържат и да имат достъп до ресурси.",
"sshSettings": "Настройки за SSH",
"sshSettings": "SSH",
"inferenceSettings": "AI Портал",
"sshAccess": "SSH Достъп",
"rdpSettings": "Настройки за RDP",
"vncSettings": "Настройки за VNC",
"rdpSettings": "RDP",
"vncSettings": "VNC",
"sshServer": "SSH сървър",
"rdpServer": "RDP сървър",
"vncServer": "VNC сървър",
@@ -2352,7 +2869,7 @@
"resourcesTableProxyResources": "Публичен",
"resourcesTableClientResources": "Частен",
"resourcesTableNoProxyResourcesFound": "Не са намерени ресурсни проксита.",
"resourcesTableNoInternalResourcesFound": "Не са намерени вътрешни ресурси.",
"resourcesTableNoInternalResourcesFound": "Не са намерени частни ресурси.",
"resourcesTableDestination": "Дестинация",
"resourcesTableAlias": "Псевдоним",
"resourcesTableAliasAddress": "Адрес на псевдоним.",
@@ -2375,9 +2892,9 @@
"editInternalResourceDialogCancel": "Отмяна",
"editInternalResourceDialogSaveResource": "Запазване на ресурс",
"editInternalResourceDialogSuccess": "Успех",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Вътрешният ресурс успешно актуализиран",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Частният ресурс е успешно обновен",
"editInternalResourceDialogError": "Грешка",
"editInternalResourceDialogFailedToUpdateInternalResource": "Неуспешно актуализиране на вътрешен ресурс",
"editInternalResourceDialogFailedToUpdateInternalResource": "Неуспешен опит за обновяване на частен ресурс",
"editInternalResourceDialogNameRequired": "Името е задължително",
"editInternalResourceDialogNameMaxLength": "Името трябва да е по-малко от 255 символа",
"editInternalResourceDialogProxyPortMin": "Прокси портът трябва да бъде поне 1",
@@ -2392,6 +2909,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeInference": "AI Портал",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Метод",
"editInternalResourceDialogEnableSsl": "Активиране на TLS",
@@ -2403,7 +2921,7 @@
"editInternalResourceDialogAlias": "Псевдоним",
"editInternalResourceDialogAliasDescription": "По избор вътрешен DNS псевдоним за този ресурс.",
"createInternalResourceDialogNoSitesAvailable": "Няма достъпни сайтове",
"createInternalResourceDialogNoSitesAvailableDescription": "Трябва да имате поне един сайт на Newt с конфигурирана мрежа, за да създадете вътрешни ресурси.",
"createInternalResourceDialogNoSitesAvailableDescription": "Трябва да имате поне един Newt сайт с конфигуриран сабнет, за да създадете частни ресурси.",
"createInternalResourceDialogClose": "Затвори",
"createInternalResourceDialogCreateClientResource": "Създаване на частен ресурс",
"createInternalResourceDialogCreateClientResourceDescription": "Създайте нов ресурс, който ще бъде достъпен само за клиенти, свързани към организацията",
@@ -2412,8 +2930,8 @@
"privateResourceAllowIcmpPing": "Разрешете ICMP Ping",
"privateResourceNetworkAccess": "Достъп до мрежата",
"privateResourceNetworkAccessDescription": "Контролирайте достъпа до TCP/UDP портовете и дали ICMP пинг е разрешен за този ресурс.",
"hostSettings": "Настройки на хоста",
"cidrSettings": "Настройки на CIDR",
"hostSettings": "Хост",
"cidrSettings": "CIDR",
"createInternalResourceDialogResourceProperties": "Свойства на ресурса",
"createInternalResourceDialogName": "Име",
"createInternalResourceDialogSite": "Сайт",
@@ -2432,9 +2950,9 @@
"createInternalResourceDialogCancel": "Отмяна",
"createInternalResourceDialogCreateResource": "Създаване на ресурс",
"createInternalResourceDialogSuccess": "Успех",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Вътрешният ресурс създаден успешно",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Частният ресурс е създаден успешно",
"createInternalResourceDialogError": "Грешка",
"createInternalResourceDialogFailedToCreateInternalResource": "Неуспешно създаване на вътрешен ресурс",
"createInternalResourceDialogFailedToCreateInternalResource": "Неуспешен опит за създаване на частен ресурс",
"createInternalResourceDialogNameRequired": "Името е задължително",
"createInternalResourceDialogNameMaxLength": "Името трябва да е по-малко от 255 символа",
"createInternalResourceDialogPleaseSelectSite": "Моля, изберете сайт",
@@ -2451,6 +2969,7 @@
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"createInternalResourceDialogModeInference": "AI Портал",
"scheme": "Метод",
"createInternalResourceDialogScheme": "Метод",
"createInternalResourceDialogEnableSsl": "Активиране на TLS",
@@ -2505,7 +3024,7 @@
"remoteExitNodeNetworkingSubnetsPlaceholder": "Добавете CIDR диапазон (напр. 10.0.0.0/8)",
"remoteExitNodeNetworkingSubnetsLoadError": "Неуспешно зареждане на подмрежи",
"remoteExitNodeNetworkingLabelsTitle": "Етикети за Предпочитания",
"remoteExitNodeNetworkingLabelsDescription": "Сайтове с тези етикети ще бъдат принудени да се свържат чрез този отдалечен край.",
"remoteExitNodeNetworkingLabelsDescription": "Сайтовете с тези етикети ще предпочетат да се свържат чрез този отдалечен изходен възел.",
"remoteExitNodeNetworkingLabelsButtonText": "Изберете етикети...",
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "Търсене на етикети...",
"remoteExitNodeNetworkingLabelsLoadError": "Неуспешно зареждане на етикети",
@@ -2672,7 +3191,7 @@
"idpAzureConfiguration": "Конфигурация на Azure Entra ID",
"idpAzureConfigurationDescription": "Конфигурирайте OAuth2 идентификационни данни на Azure Entra ID",
"idpTenantId": "Идентификационен номер на наемателя",
"idpTenantIdPlaceholder": "tenant-id",
"idpTenantIdPlaceholder": "идентификационен номер на наемателя",
"idpAzureTenantIdDescription": "Идентификационен номер на наемателя на Azure (намира се в прегледа на Azure Active Directory)",
"idpAzureClientIdDescription": "Идентификационен код на клиента за регистриране на приложение в Azure",
"idpAzureClientSecretDescription": "Секретен код на клиента за регистриране на приложение в Azure",
@@ -2982,6 +3501,7 @@
"priority": "Приоритет",
"priorityDescription": "По-високите приоритетни маршрути се оценяват първи. Приоритет = 100 означава автоматично подреждане (системата решава). Използвайте друго число, за да наложите ръчен приоритет.",
"instanceName": "Име на инстанция",
"clearInstanceName": "Reset Server Association",
"pathMatchModalTitle": "Конфигурация на съвпадение по пътека",
"pathMatchModalDescription": "Настройте как трябва да бъдат съвпадани входящите заявки въз основа на техния път.",
"pathMatchType": "Вид на съвпадението",
@@ -3042,6 +3562,7 @@
"validPassword": "Валидна парола",
"validEmail": "Валиден имейл",
"validSSO": "Валидно SSO",
"validVirtualAPIKey": "Валиден Виртуален API Ключ",
"view": "Преглед",
"configManaged": "Управлявана конфигурация",
"connectedClient": "Свързан клиент",
@@ -3062,7 +3583,42 @@
"sidebarLogsAction": "Дневници на действията",
"logRetention": "Задържане на логове",
"logRetentionDescription": "Управлявайте времето за задържане на различни видове логове за тази организация или ги деактивирайте",
"logRetentionDisabledWarningTitle": "Деактивирано съхранение на дневници",
"logRetentionDisabledWarningDescription": "{logType} не се съхраняват за тази организация, така че новите дейности няма да се показват тук. Активирайте съхранението в настройките за сигурност, за да започнете събирането на тези дневници.",
"logRetentionDisabledWarningButton": "Отидете на настройки за сигурност",
"requestLogsDescription": "Прегледайте подробни логове на заявки за ресурси в тази организация",
"aiSessionLogs": "Журнали на AI Портал Сесиите",
"aiSessionLogsDescription": "Прегледайте подканянета и транскрипции на отговори за запитванията към AI портал в тази организация",
"sidebarLogsAi": "Журнали на Сесиите",
"commandLogsAi": "Журнали на Сесиите",
"sidebarLogsAiUsage": "Анализ на Използване",
"commandLogsAiUsage": "Анализ на Използване",
"provider": "Доставчик",
"capability": "Способност",
"model": "Модел",
"virtualApiKey": "Виртуален API Ключ",
"noVirtualApiKey": "Няма виртуален API ключ",
"stream": "Поток",
"streaming": "Стрийминг",
"nonStreaming": "Нестрийминг",
"statusCode": "Код на Статуса",
"aiSessionId": "ID на Сесията",
"aiSessionRequest": "Запитване",
"aiSessionResponse": "Отговор",
"aiSessionNoData": "Няма събрани данни",
"aiSessionCouldNotParse": "(суров, не може да се парсира транскрипт)",
"aiSessionLogTruncated": "Тази сесия беше прекъсната преди съхранение и може да е непълна.",
"aiSessionViewRaw": "Преглед на Суров JSON",
"aiSessionViewChat": "Преглед на Чат",
"cost": "Разход",
"estimated": "Оценени",
"tokenUsage": "Употреба на Токени",
"promptTokens": "Подканящи Токени",
"cacheReadTokens": "Кеш Прочитане на Токени",
"cacheWriteTokens": "Кеш Писане на Токени",
"completionTokens": "Токени за Завършване",
"reasoningTokens": "Токени за Разсъждение",
"totalTokens": "Общо Токени",
"requestAnalyticsDescription": "Вижте подробни анализи на заявки за ресурсите в тази организация",
"logRetentionRequestLabel": "Задържане на логове за HTTP заявки",
"logRetentionRequestDescription": "Колко дълго да се задържат логовете на заявките",
@@ -3072,6 +3628,8 @@
"logRetentionActionDescription": "Колко дълго да се задържат логовете за действия",
"logRetentionConnectionLabel": "Запазване на дневниците на връзките",
"logRetentionConnectionDescription": "Колко дълго да се съхраняват дневниците на връзките",
"logRetentionAISessionsLabel": "Съхранение на Журнали на AI Портал Сесиите",
"logRetentionAISessionsDescription": "Колко време да съхраняваме подканящите/отговоряващите журнали на AI портал сесии",
"logRetentionDisabled": "Деактивирано",
"logRetention3Days": "3 дни",
"logRetention7Days": "7 дни",
@@ -3089,8 +3647,8 @@
"sourceAddress": "Източен адрес",
"destinationAddress": "Адрес на дестинация",
"duration": "Продължителност",
"licenseRequiredToUse": "Изисква се лиценз за <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> или <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> за използване на тази функция. <bookADemoLink>Резервирайте демонстрация или пробен POC</bookADemoLink>.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> е необходим за използване на тази функция. Тази функция също е налична в <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Резервирайте демонстрация или пробен POC</bookADemoLink>.",
"licenseRequiredToUse": "<enterpriseEditionLink>Ентерпрайз Едишън лиценз</enterpriseEditionLink> е необходим. <bookADemoLink>Резервирайте демо или проба</bookADemoLink>",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Ентерпрайз Едишън лиценз</enterpriseEditionLink> е необходим. <bookADemoLink>Резервирайте демо или проба</bookADemoLink>",
"certResolver": "Решавач на сертификати",
"certResolverDescription": "Изберете решавач на сертификати за използване за този ресурс.",
"selectCertResolver": "Изберете решавач на сертификати",
@@ -3233,6 +3791,7 @@
"noData": "Няма Данни",
"machineClients": "Машинни клиенти",
"install": "Инсталирай",
"downloadInstaller": "Изтегляне на инсталатора",
"run": "Изпълни",
"envFile": "Файл за среда",
"serviceFile": "Файл за услуга",
@@ -3288,9 +3847,9 @@
"internalResourceFormMultiSiteRoutingHelp": "Избирайки няколко сайта, се осигурява сигурен път и пренасочване при висока достъпност.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Научете повече",
"editInternalResourceDialogPortRestrictionsDescription": "Ограничете достъпа до конкретни TCP/UDP портове или позволете/блокирайте всички портове.",
"createInternalResourceDialogHttpConfiguration": "Конфигурация HTTP",
"createInternalResourceDialogHttpConfiguration": "Конфигурация на Домейн",
"createInternalResourceDialogHttpConfigurationDescription": "Изберете домейна, който клиентите ще използват, за да достигнат този ресурс чрез HTTP или HTTPS.",
"editInternalResourceDialogHttpConfiguration": "Конфигурация HTTP",
"editInternalResourceDialogHttpConfiguration": "Конфигурация на Домейн",
"editInternalResourceDialogHttpConfigurationDescription": "Изберете домейна, който клиентите ще използват, за да достигнат този ресурс чрез HTTP или HTTPS.",
"editInternalResourceDialogTcp": "TCP.",
"editInternalResourceDialogUdp": "UDP.",
@@ -3440,6 +3999,7 @@
"disconnected": "Прекъснат",
"approvalsEmptyStateTitle": "Одобрения на устройство не са активирани",
"approvalsEmptyStateDescription": "Активирайте одобрения на устройства за роли, така че да изискват администраторско одобрение, преди потребителите да могат да свързват нови устройства.",
"approvalsEmptyStateHowToTitle": "Как да го активирате",
"approvalsEmptyStateStep1Title": "Отидете на роли",
"approvalsEmptyStateStep1Description": "Навигирайте до настройките на ролите на вашата организация, за да конфигурирате одобренията на устройства.",
"approvalsEmptyStateStep2Title": "Активирайте одобрения на устройства",
@@ -3561,6 +4121,8 @@
"httpDestConnectionLogsDescription": "Събития на свързване и прекъсване на сайта и тунела, включително свръзки и прекъсвания.",
"httpDestRequestLogsTitle": "Логове за HTTP заявки",
"httpDestRequestLogsDescription": "Регистри за HTTP заявките към проксирани ресурси, включително метод, път и код на отговор.",
"httpDestAISessionLogsTitle": "Дневници за AI сесии",
"httpDestAISessionLogsDescription": "AI заявки до шлюза и отговори на сесии, включително подканвания, отговори на модели и използване на жетони.",
"httpDestSaveChanges": "Запази промените",
"httpDestCreateDestination": "Създаване на дестинация",
"httpDestUpdatedSuccess": "Дестинацията беше актуализирана успешно",
@@ -3717,6 +4279,11 @@
"resourceLauncherViewAsAdmin": "Вижте като Админ",
"resourceLauncherResourceDetailsDescription": "Информация и статус на връзката за този ресурс.",
"resourceLauncherResourceDetails": "Детайли за ресурса",
"resourceLauncherSitesDescription": "Ресурсът е достъпен чрез следните сайтове.",
"resourceLauncherViewSiteAsAdmin": "Вижте сайта като Админ",
"resourceLauncherFilterBySite": "Филтриране по сайт",
"resourceLauncherSshCommand": "SSH Команда",
"resourceLauncherSshCommandDescription": "Използвайте Pangolin CLI, за да отворите SSH сесия до този ресурс.",
"resourceLauncherAuthMethodsDescription": "Методи на автентикация, активирани за този ресурс.",
"resourceLauncherPrivateClientRequired": "Свържете се с клиент на вашето устройство, за да получите частен достъп до този ресурс.",
"resourceLauncherPrivateClientRequiredTitle": "Изисква се връзка с клиента",
@@ -3726,7 +4293,18 @@
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherUnlabeled": "Без Етикет",
"resourceLauncherAiGateway": "AI Портал",
"resourceLauncherNoSite": "Няма Сайт",
"resourceLauncherAvailableModels": "Налични Модели",
"resourceLauncherAvailableModelsDescription": "Модели, които можете да използвате с този AI портал.",
"resourceLauncherAvailableModelsEmpty": "Няма налични модели за този ресурс.",
"resourceLauncherAvailableModelsError": "Не можа да се заредят наличните модели.",
"resourceLauncherApiKeys": "API Ключове",
"resourceLauncherApiKeysDescription": "Използвайте вашия ключ за идентичност или атрибутиран ключ за удостоверяване с този ресурс.",
"resourceLauncherApiKeysIdentity": "Ключ за Идентичност",
"resourceLauncherApiKeysManual": "Атрибуте Ключове",
"resourceLauncherApiKeysEmpty": "Няма налични API ключове за този ресурс.",
"resourceLauncherApiKeysError": "Не можа да се заредят API ключовете.",
"resourceLauncherNoResourcesInGroup": "Няма ресурси в тази група",
"resourceLauncherEmptyStateTitle": "Няма Налични Ресурси",
"resourceLauncherEmptyStateDescription": "Все още нямате достъп до никакви ресурси. Свържете се с вашия администратор, за да поискате достъп.",
@@ -3753,9 +4331,9 @@
"resourceLauncherViewDeleteFailedDescription": "Не можахте да изтриете изгледа на стартер. Моля, опитайте отново.",
"memberPortalPrevious": "Предишен",
"memberPortalNext": "Следващ",
"httpSettings": "HTTP настройки",
"tcpSettings": "TCP настройки",
"udpSettings": "UDP настройки",
"httpSettings": "HTTP",
"tcpSettings": "TCP",
"udpSettings": "UDP",
"sshTitle": "SSH",
"sshConnectingDescription": "Установяване на защитена връзка…",
"sshConnecting": "Свързване…",
@@ -3816,5 +4394,6 @@
"rdpUnicodeKeyboardMode": "Режим на unicode клавиатура",
"sessionToolbarShow": "Показване на лентата с инструменти",
"sessionToolbarHide": "Скриване на лентата с инструменти",
"actionUpdateSiteApprovals": "Обновяване на одобренията на сайта"
"actionUpdateSiteApprovals": "Обновяване на одобренията на сайта",
"check": "Проверка"
}
+646 -67
View File
File diff suppressed because it is too large Load Diff
+615 -36
View File
File diff suppressed because it is too large Load Diff
+614 -35
View File
File diff suppressed because it is too large Load Diff
+611 -38
View File
@@ -43,6 +43,8 @@
"inviteLoginUser": "Please make sure you're logged in as the correct user.",
"inviteErrorNoUser": "We're sorry, but it looks like the invite you're trying to access is not for a user that exists.",
"inviteCreateUser": "Please create an account first.",
"inviteErrorOidcNotAllowed": "Invites can only be accepted by internal accounts. Sign out and log in with your password for this email.",
"inviteLoginInternalOnly": "Invites require an internal account with a password. Create an account or sign in with your password.",
"goHome": "Go Home",
"inviteLogInOtherUser": "Log In as a Different User",
"createAnAccount": "Create an Account",
@@ -78,7 +80,7 @@
"siteManageSites": "Manage Sites",
"siteDescription": "Create and manage sites to enable connectivity to private networks",
"sitesBannerTitle": "Connect Any Network",
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector (Newt) anywhere you can run a binary or container to establish the connection.",
"sitesBannerDescription": "A site is a connection to a remote network that allows Pangolin to provide access to resources, whether public or private, to users anywhere. Install the site network connector anywhere you can run a binary or container to establish the connection.",
"sitesBannerButtonText": "Install Site Connector",
"approvalsBannerTitle": "Approve or Deny Device Access",
"approvalsBannerDescription": "Review and approve or deny device access requests from users. When device approvals are required, users must get admin approval before their devices can connect to your organization's resources.",
@@ -150,7 +152,8 @@
"siteResourcesHowToAccess": "How to access",
"siteResourcesTargetsOnSite": "Targets on this site",
"siteSetting": "{siteName} Settings",
"siteNewtTunnel": "Newt Site (Recommended)",
"siteNewtTunnel": "Pangolin Site (Recommended)",
"pangolinSite": "Pangolin Site",
"siteNewtTunnelDescription": "Easiest way to create an entrypoint into any network. No extra setup.",
"siteWg": "Basic WireGuard",
"siteWgDescription": "Use any WireGuard client to establish a tunnel. Manual NAT setup required.",
@@ -224,9 +227,9 @@
"never": "Never",
"shareErrorSelectResource": "Please select a resource",
"proxyResourceTitle": "Manage Public Resources",
"proxyResourceDescription": "Create and manage resources that are publicly accessible through a web browser",
"proxyResourceDescription": "Create and manage resources that are publicly accessible via a proxy",
"publicResourcesBannerTitle": "Web-based Public Access",
"publicResourcesBannerDescription": "Public resources are proxies accessible to anyone on the internet through a web browser and include identity and context-aware access policies. Unlike private resources, they do not require client-side software.",
"publicResourcesBannerDescription": "Public resources are proxies accessible to anyone on the internet, like a website or API, and include identity and context-aware access policies. Unlike private resources, they do not require any client-side software to access.",
"clientResourceTitle": "Manage Private Resources",
"clientResourceDescription": "Create and manage resources that are only accessible through a connected client",
"privateResourcesBannerTitle": "Zero-Trust Private Access",
@@ -286,7 +289,21 @@
"noCountryFound": "No country found.",
"siteSelectionDescription": "This site will provide connectivity to the target.",
"resourceType": "Resource Type",
"resourceTypeDescription": "This controls the resource protocol and how it will be rendered in the browser. This cant be changed later.",
"resourceTypeDescription": "This controls the resource protocol and cant be changed later.",
"resourceTypeSearch": "Search resource types...",
"resourceTypeNotFound": "No resource type found",
"resourceTypeHttpDescription": "Proxy web traffic over HTTPS using a domain name",
"resourceTypeInferenceDescription": "Route AI requests through attached providers",
"resourceTypeSshDescription": "Access an SSH server from the browser",
"resourceTypeRdpDescription": "Access a remote desktop from the browser",
"resourceTypeVncDescription": "Access a VNC desktop from the browser",
"resourceTypeTcpDescription": "Proxy raw TCP traffic using a port number",
"resourceTypeUdpDescription": "Proxy raw UDP traffic using a port number",
"privateResourceTypeDescription": "This controls how clients reach the resource. This cant be changed later.",
"privateResourceTypeHostDescription": "Expose a single host on the site network to connected clients",
"privateResourceTypeCidrDescription": "Expose a CIDR range on the site network to connected clients",
"privateResourceTypeHttpDescription": "Access an HTTP or HTTPS service through a domain",
"privateResourceTypeSshDescription": "Access an SSH server from connected clients",
"resourceDomainDescription": "The resource will be served at this fully qualified domain name.",
"resourceHTTPSSettings": "HTTPS Settings",
"resourceHTTPSSettingsDescription": "Configure how the resource will be accessed over HTTPS",
@@ -449,6 +466,8 @@
"apiKeysDelete": "Delete API Key",
"apiKeysManage": "Manage API Keys",
"apiKeysDescription": "API keys are used to authenticate with the integration API",
"orgsManage": "Manage Organizations",
"orgsDescription": "View and manage all organizations on this instance",
"provisioningKeysTitle": "Provisioning Key",
"provisioningKeysManage": "Manage Provisioning Keys",
"provisioningKeysDescription": "Provisioning keys are used to authenticate automated site provisioning for your organization.",
@@ -504,12 +523,12 @@
"pendingSitesBannerDescription": "Sites that connect using a provisioning key appear here for review.",
"pendingSitesBannerButtonText": "Learn More",
"apiKeysSettings": "{apiKeyName} Settings",
"userTitle": "Manage All Users",
"userDescription": "View and manage all users in the system",
"userTitle": "Manage Users",
"userDescription": "View and manage all users in this instance",
"userAbount": "About User Management",
"userAbountDescription": "This table displays all base user objects in the system. Each user may belong to multiple organizations. Removing a user from an organization does not delete their base user object. They will remain in the system. To completely remove a user from the system, you must delete their base user object using the delete action in this table.",
"userServer": "Server Users",
"userSearch": "Search server users...",
"userSearch": "Search users...",
"userErrorDelete": "Error deleting user",
"userDeleteConfirm": "Confirm Delete User",
"userDeleteServer": "Delete User from Server",
@@ -578,7 +597,7 @@
"licensePricingPage": "For the most up-to-date pricing and discounts, please visit the ",
"invite": "Invitations",
"inviteRegenerate": "Regenerate Invitation",
"inviteRegenerateDescription": "Revoke previous invitation and create a new one",
"inviteRegenerateDescription": "Create a new invite link for this user. The previous invitation will be revoked.",
"inviteRemove": "Remove Invitation",
"inviteRemoveError": "Failed to remove invitation",
"inviteRemoveErrorDescription": "An error occurred while removing the invitation.",
@@ -658,11 +677,11 @@
"accessUserCreateDescription": "Follow the steps below to create a new user",
"userSeeAll": "See All Users",
"userTypeTitle": "User Type",
"userTypeDescription": "Determine how you want to create the user",
"userTypeDescription": "Select the identity provider to use for this user",
"userSettings": "User Information",
"userSettingsDescription": "Enter the details for the new user",
"userSettingsDescription": "Enter the general details for the new user",
"inviteEmailSent": "Send invite email to user",
"inviteValid": "Invite Valid For (days)",
"inviteValid": "Invite Valid For",
"selectDuration": "Select duration",
"selectResource": "Select Resource",
"filterByResource": "Filter By Resource",
@@ -700,6 +719,7 @@
"nameOptional": "Name (Optional)",
"accessControls": "Access Controls",
"userDescription2": "Manage the settings on this user",
"userGeneralSettingsDescription": "Manage this user's roles and settings in the organization",
"accessRoleErrorAdd": "Failed to add user to role",
"accessRoleErrorAddDescription": "An error occurred while adding user to the role.",
"userSaved": "User saved",
@@ -855,12 +875,16 @@
"authMethodsSave": "Save Settings",
"policyAuthStackTitle": "Authentication",
"policyAuthStackDescription": "Control which authentication methods are required to access this resource",
"policyAuthInferenceStackDescription": "Choose which users and roles can authenticate to this AI gateway",
"policyAuthOrLogicTitle": "Multiple authentication methods active",
"policyAuthOrLogicBanner": "Visitors may authenticate using any one of the active methods below. They do not need to complete all of them.",
"policyAuthMethodActive": "Active",
"policyAuthMethodOff": "Off",
"policyAuthSsoTitle": "Platform SSO",
"policyAuthSsoDescription": "Require sign-in through your organization's identity provider",
"policyAuthInferenceIdentityKeySignInUrl": "Sign-in URL",
"policyAuthInferenceIdentityKeyHelpNoUrl": "Every user already has an identity API key, so you only need to create virtual API keys for non-user clients or shared access. Users can retrieve their key by signing in with their identity provider at this resource's URL, where it will be shown after login.",
"policyAuthInferenceIdentityKeyHelp": "Every user already has an identity API key, so you only need to create virtual API keys for non-user clients or shared access. Users retrieve their key here after signing in with their identity provider.",
"policyAuthSsoSummary": "{idp} · {users} users, {roles} roles",
"policyAuthSsoDefaultIdp": "Default provider",
"policyAuthAddDefaultIdentityProvider": "Add Default Identity Provider",
@@ -934,7 +958,7 @@
"newtVersion": "Version",
"architecture": "Architecture",
"sites": "Sites",
"siteWgAnyClients": "Use any WireGuard client to connect. You will have to address internal resources using the peer IP.",
"siteWgAnyClients": "Use any WireGuard client to connect. You will have to address private resources using the peer IP.",
"siteWgCompatibleAllClients": "Compatible with all WireGuard clients",
"siteWgManualConfigurationRequired": "Manual configuration required",
"userErrorNotAdminOrOwner": "User is not an admin or owner",
@@ -1079,6 +1103,8 @@
"accessRoleErrorNewRequired": "New role is required",
"accessRoleErrorRemove": "Failed to remove role",
"accessRoleErrorRemoveDescription": "An error occurred while removing the role.",
"accessRoleInferenceBudget": "AI Budget",
"accessRoleInferenceBudgetDescription": "Configure how members of this role restrict AI usage based on spending or token limits",
"accessRoleName": "Role Name",
"accessRoleQuestionRemove": "You're about to delete the `{name}` role. You cannot undo this action.",
"accessRoleRemove": "Remove Role",
@@ -1154,6 +1180,10 @@
"idpJmespathAboutDescriptionLink": "Learn more about JMESPath",
"idpJmespathLabel": "Identifier Path",
"idpJmespathLabelDescription": "The path to the user identifier in the ID token",
"idpIdentifierChangeTitle": "Identifier Path Change Warning",
"idpIdentifierChangeDescription": "You are about to change the identifier path. This will affect how existing users are mapped. Users who previously signed in through this identity provider may no longer be recognized as the same users.",
"idpIdentifierChangeConfirmMessage": "I confirm",
"idpIdentifierChangeWarningText": "This will affect how existing users are mapped",
"idpJmespathEmailPathOptional": "Email Path (Optional)",
"idpJmespathEmailPathOptionalDescription": "The path to the user's email in the ID token",
"idpJmespathNamePathOptional": "Name Path (Optional)",
@@ -1188,7 +1218,7 @@
"orgPoliciesEdit": "Edit Organization Policy",
"org": "Organization",
"orgSelect": "Select organization",
"orgSearch": "Search org",
"orgSearch": "Search organizations...",
"orgNotFound": "No org found.",
"roleMappingPathOptional": "Role Mapping Path (Optional)",
"orgMappingPathOptional": "Organization Mapping Path (Optional)",
@@ -1327,7 +1357,7 @@
"siteLabelsDescription": "Manage labels associated with this site.",
"labelsNotFound": "No labels found.",
"labelsEmptyCreateHint": "Start typing above to create a label.",
"labelSearch": "Search labels",
"labelSearch": "Search labels...",
"labelSearchOrCreate": "Search or create a label",
"accessLabelFilterCount": "{count, plural, one {# label} other {# labels}}",
"labelOverflowCount": "+{count, plural, one {# label} other {# labels}}",
@@ -1398,6 +1428,24 @@
"logoutError": "Error logging out",
"signingAs": "Signed in as",
"serverAdmin": "Server Admin",
"promoteServerAdmin": "Promote to Server admin",
"promoteServerAdminTitle": "Promote to Server Admin",
"promoteServerAdminQuestion": "Are you sure you want to promote {selectedUser} to server admin?",
"promoteServerAdminMessage": "Server admins have the highest privileges and can manage the server.",
"promoteServerAdminWarning": "This can be undone at any time by demoting the user.",
"promoteServerAdminConfirm": "Promote to Server Admin",
"promoteServerAdminSuccess": "User Promoted",
"promoteServerAdminSuccessDescription": "{selectedUser} is now a server admin.",
"promoteServerAdminError": "Failed to promote user",
"demoteServerAdmin": "Demote from Server admin",
"demoteServerAdminTitle": "Demote from Server Admin",
"demoteServerAdminQuestion": "Are you sure you want to demote {selectedUser} from server admin?",
"demoteServerAdminMessage": "{selectedUser} will lose all server admin privileges.",
"demoteServerAdminWarning": "This can be undone at any time by promoting the user.",
"demoteServerAdminConfirm": "Demote from server admin",
"demoteServerAdminSuccess": "User demoted",
"demoteServerAdminSuccessDescription": "{selectedUser} is no longer a server admin.",
"demoteServerAdminError": "Failed to demote user",
"managedSelfhosted": "Managed Self-Hosted",
"otpEnable": "Enable Two-factor",
"otpDisable": "Disable Two-factor",
@@ -1424,6 +1472,28 @@
"actionDeleteSite": "Delete Site",
"actionGetSite": "Get Site",
"actionListSites": "List Sites",
"actionCreateAiProvider": "Create AI Provider",
"actionDeleteAiProvider": "Delete AI Provider",
"actionGetAiProvider": "Get AI Provider",
"actionListAiProviders": "List AI Providers",
"actionUpdateAiProvider": "Update AI Provider",
"actionCreateAiModel": "Create AI Model",
"actionDeleteAiModel": "Delete AI Model",
"actionGetAiModel": "Get AI Model",
"actionListAiModels": "List AI Models",
"actionUpdateAiModel": "Update AI Model",
"actionCreateAiBudget": "Create AI Budget",
"actionDeleteAiBudget": "Delete AI Budget",
"actionGetAiBudget": "Get AI Budget",
"actionListAiBudgets": "List AI Budgets",
"actionUpdateAiBudget": "Update AI Budget",
"actionListResourceAiModels": "List Resource AI Models",
"actionSetResourceAiModels": "Set Resource AI Models",
"actionCreateVirtualApiKey": "Create Virtual API Key",
"actionDeleteVirtualApiKey": "Delete Virtual API Key",
"actionGetVirtualApiKey": "Get Virtual API Key",
"actionListVirtualApiKeys": "List Virtual API Keys",
"actionUpdateVirtualApiKey": "Update Virtual API Key",
"actionApplyBlueprint": "Apply Blueprint",
"actionListBlueprints": "List Blueprints",
"actionGetBlueprint": "Get Blueprint",
@@ -1449,8 +1519,11 @@
"actionSetResourcePincode": "Set Resource Pincode",
"actionSetResourceEmailWhitelist": "Set Resource Email Whitelist",
"actionGetResourceEmailWhitelist": "Get Resource Email Whitelist",
"actionListResourcePolicies": "List Resource Policies",
"actionCreateResourcePolicy": "Create Resource Policy",
"actionGetResourcePolicy": "Get Resource Policy",
"actionUpdateResourcePolicy": "Update Resource Policy",
"actionDeleteResourcePolicy": "Delete Resource Policy",
"actionSetResourcePolicyUsers": "Set Resource Policy Users",
"actionSetResourcePolicyRoles": "Set Resource Policy Roles",
"actionSetResourcePolicyPassword": "Set Resource Policy Password",
@@ -1526,6 +1599,8 @@
"search": "Search…",
"searchPlaceholder": "Search...",
"emptySearchOptions": "No options found",
"ipFilterSearchPlaceholder": "Enter an IP address…",
"ipFilterEmptyMessage": "Enter an IP address to filter by",
"create": "Create",
"orgs": "Organizations",
"loginError": "An unexpected error occurred. Please try again.",
@@ -1560,6 +1635,8 @@
"commandPaletteCreateMachineClient": "Create Machine Client",
"commandPaletteCreateAlertRule": "Create Alert Rule",
"commandPaletteCreateIdentityProvider": "Create Identity Provider",
"commandPaletteCreateAiProvider": "Create AI Provider",
"commandPaletteCreateVirtualApiKey": "Create Virtual API Key",
"commandPaletteToggleTheme": "Toggle Theme",
"commandPaletteChooseOrganization": "Choose Organization",
"commandPaletteShortcutMac": "⌘K",
@@ -1622,10 +1699,428 @@
"sidebarInvitations": "Invitations",
"sidebarRoles": "Roles",
"sidebarShareableLinks": "Shareable Links",
"sidebarAiGateway": "AI Gateway",
"sidebarAiProviders": "Providers",
"commandAiProviders": "AI Providers",
"sidebarVirtualApiKeys": "Virtual API Keys",
"sidebarMyApiKeys": "Your API Keys",
"sidebarAccount": "Launcher",
"commandVirtualApiKeys": "Virtual API Keys",
"virtualApiKeysTitle": "Manage Virtual API Keys",
"virtualApiKeysDescription": "Create and manage manual API keys for AI Gateway access to public AI gateways",
"virtualApiKeysTabIdentity": "Identity Keys",
"virtualApiKeysTabVirtual": "Virtual Keys",
"virtualApiKeysIdentitySplashTitle": "Identity Keys for Every User",
"virtualApiKeysIdentitySplashDescription": "Every user already has a Pangolin identity key for this organization. It is unique to their account and authenticates them to AI gateways they can access.",
"virtualApiKeysIdentitySplashExample": "Example",
"virtualApiKeysIdentitySplashRetrieveTitle": "How Users Get Their Key",
"virtualApiKeysIdentitySplashRetrieveResource": "Visit a public AI gateway URL in the browser and log in with their account.",
"virtualApiKeysIdentitySplashRetrievePage": "Or go to <url></url> while signed in.",
"virtualApiKeysIdentitySplashManual": "You can create additional virtual API keys on the Virtual Keys tab. Those keys can be scoped to specific public AI gateways and optionally associated with a user. Creating a key immediately grants access to the selected public AI gateways.",
"virtualApiKeysIdentitySplashGoToVirtual": "Manually Create a Key",
"virtualApiKeysEmailIdentity": "Email Identity Keys",
"virtualApiKeysEmailIdentityDescription": "Send each selected user or role their Pangolin identity key.",
"virtualApiKeysEmailIdentitySendAll": "Send to all users",
"virtualApiKeysEmailIdentitySendAllDescription": "Email every organization member who has an account email.",
"virtualApiKeysEmailIdentitySelectUsers": "Users",
"virtualApiKeysEmailIdentitySelectRoles": "Roles",
"virtualApiKeysEmailIdentitySubmit": "Send Emails",
"virtualApiKeysEmailIdentitySuccess": "Identity keys emailed",
"virtualApiKeysEmailIdentitySuccessDescription": "Sent {sent} emails.",
"virtualApiKeysEmailIdentitySkipped": "{skipped} users were skipped because they do not have an email address.",
"virtualApiKeysEmailIdentityRecipientsRequired": "Select at least one user or role, or send to all users.",
"virtualApiKeysEmailIdentityError": "Error sending identity keys",
"virtualApiKeysEmailIdentityErrorDescription": "Failed to email identity keys",
"virtualApiKeysBannerTitle": "Identity Keys for Every User",
"virtualApiKeysBannerDescription": "Every user already has an identity key available at {keysUrl}. You can also manually generate keys here that grant direct access to public AI gateways.",
"virtualApiKeysBannerButtonText": "View Identity Keys",
"virtualApiKeys": "Virtual API Keys",
"virtualApiKeysSearch": "Search keys...",
"virtualApiKeysCreate": "Create Virtual API Key",
"virtualApiKeysCreateDescription": "Mint a manual key that can call public AI gateways in this organization",
"virtualApiKeysCreateButton": "Create Key",
"virtualApiKeysEmpty": "No virtual API keys yet",
"virtualApiKeysName": "Name",
"virtualApiKeysDescriptionOptional": "Description (optional)",
"virtualApiKeysAssociateUserOptional": "Associate User (optional)",
"virtualApiKeysAssociateUserDescription": "Associate this key with a user to track usage. Once created, this key immediately grants access to the selected public AI gateways. You do not need to associate a user to manually to the resource. The key will also show up in the user's profile.",
"virtualApiKeysAllResources": "All public AI gateways",
"virtualApiKeysAllResourcesDescription": "Allow this key to access every public AI gateway in the organization",
"virtualApiKeysSelectResources": "Public AI Gateways",
"virtualApiKeysSelectResourcesPlaceholder": "Select resources",
"virtualApiKeysSelectResourcesDescription": "Choose which public AI gateways this key can access",
"virtualApiKeysNoResources": "No resources",
"virtualApiKeysSecret": "Key",
"virtualApiKeysCopyKey": "Copy this key. You can view it again later from the table or when editing.",
"virtualApiKeysViewSecret": "View Secret",
"virtualApiKeysViewSecretTitle": "Virtual API Key Secret",
"virtualApiKeysViewSecretDescription": "This secret grants access to the public AI gateways assigned to this key",
"virtualApiKeysEdit": "Edit Virtual API Key",
"virtualApiKeysEditDescription": "Update the associated user and public AI gateway access for this key",
"virtualApiKeysSaveButton": "Save Changes",
"virtualApiKeysSelectResourcesRequired": "Select at least one public AI gateway, or enable all public AI gateways",
"virtualApiKeysUpdated": "Virtual API key updated",
"virtualApiKeysUpdatedDescription": "The virtual API key has been updated",
"virtualApiKeysErrorUpdate": "Error updating virtual API key",
"virtualApiKeysErrorUpdateDescription": "Failed to update virtual API key",
"virtualApiKeysErrorCreate": "Error creating virtual API key",
"virtualApiKeysErrorCreateDescription": "Failed to create virtual API key",
"virtualApiKeysErrorDelete": "Error deleting virtual API key",
"virtualApiKeysErrorDeleteMessage": "Failed to delete virtual API key",
"virtualApiKeysDeleted": "Virtual API key deleted",
"virtualApiKeysDeletedDescription": "The virtual API key has been deleted",
"virtualApiKeysDelete": "Delete Virtual API Key",
"virtualApiKeysDeleteConfirm": "Delete Key",
"virtualApiKeysQuestionRemove": "Are you sure you want to delete this virtual API key?",
"virtualApiKeysMessageRemove": "Clients using this key will lose access immediately.",
"virtualApiKeysErrorFetchSecret": "Error loading secret",
"virtualApiKeysErrorFetchSecretDescription": "Failed to load the virtual API key secret",
"virtualApiKeysFilterUnassigned": "Unassigned",
"virtualApiKeysInferenceBudget": "Budget",
"virtualApiKeysInferenceBudgetDescription": "Configure how this key restricts AI usage based on spending or token limits",
"virtualApiKeysEmailOnGenerate": "Email key upon generation",
"virtualApiKeysEmailThisKey": "Email this key",
"virtualApiKeysEmailOnGenerateDescription": "Send the key to the associated user and additional addresses after it is created",
"virtualApiKeysEmailThisKeyDescription": "Send the current key to the associated user and additional addresses",
"virtualApiKeysEmailSmtpRequired": "Email is not configured on this server",
"virtualApiKeysEmailSmtpRequiredDescription": "Configure SMTP to email virtual API keys.",
"virtualApiKeysEmailSendToUser": "Send to associated user",
"virtualApiKeysEmailSendToUserDescription": "Email the key to the associated user's account email",
"virtualApiKeysEmailSendToUserDisabled": "Associate a user to send the key to that user",
"virtualApiKeysEmailAdditional": "Additional emails",
"virtualApiKeysEmailAdditionalPlaceholder": "Add email and press Enter",
"virtualApiKeysEmailRecipientsRequired": "Select the associated user or add at least one email address",
"myVirtualApiKeysTitle": "Your API Keys",
"myVirtualApiKeysDescription": "View your identity key and any virtual API keys attributed to you in this organization",
"myVirtualApiKeysResourceTitle": "Your API Keys for {resourceName}",
"myVirtualApiKeysResourceDescription": "View your identity key and virtual API keys attributed to you that can access {resourceName}",
"myVirtualApiKeysIdentityTitle": "Identity Key",
"myVirtualApiKeysIdentityHeadline": "Your Personal API Key",
"myVirtualApiKeysIdentityDescription": "Your personal key for this organization. It is unique to your account and used to identify you when calling AI Gateway resources.",
"myVirtualApiKeysIdentityResourceHeadline": "Your Personal API Key for {resourceName}",
"myVirtualApiKeysIdentityResourceDescription": "Your personal key for this organization. Use it to call {resourceName}.",
"myVirtualApiKeysManualTitle": "Attributed Keys",
"myVirtualApiKeysManualDescription": "Manual virtual API keys an admin associated with your account",
"myVirtualApiKeysManualResourceDescription": "Manual virtual API keys associated with your account that can access {resourceName}",
"myVirtualApiKeysManualEmpty": "No attributed keys yet",
"myVirtualApiKeysKindUser": "Identity",
"myVirtualApiKeysKindManual": "Manual",
"myVirtualApiKeysUnnamed": "Unnamed key",
"myVirtualApiKeysRevealSecret": "Reveal Secret",
"myVirtualApiKeysViewSecretDescription": "This secret authenticates you to AI Gateway resources",
"aiClientConfigTitle": "Configure Coding Agents",
"aiClientConfigDescription": "Copy configuration for popular coding agents, or let the Pangolin CLI set it up for you automatically.",
"aiClientConfigDescriptionClaude": "Anthropic's agentic coding tool for the terminal.",
"aiClientConfigDescriptionCodex": "OpenAI's agentic coding tool for the terminal.",
"aiClientConfigDescriptionOpencode": "Open source terminal coding agent.",
"aiClientConfigDescriptionGemini": "Google's agentic coding tool for the terminal.",
"aiClientConfigSetup": "Setup",
"aiClientConfigTabCli": "Automatic (CLI)",
"aiClientConfigTabManual": "Manual Configuration",
"aiClientConfigPreset": "Configuration",
"aiClientConfigStep": "Step {number}",
"aiClientConfigEndpointPlaceholder": "https://example.resource.url.com",
"aiClientConfigPlaceholderWarning": "This snippet includes example values, such as a model name or resource URL. Replace them with your own before using it.",
"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",
"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.",
"aiProvidersAdd": "Add Provider",
"aiProvidersSearch": "Search providers...",
"aiProvidersEmpty": "No AI providers yet",
"aiProviderCreate": "Create AI Provider",
"aiProviderCreateDescription": "Add a model provider for this organization",
"aiProviderSeeAll": "See All Providers",
"aiProviderSetting": "Provider Settings for {providerName}",
"aiProviderSettingDescription": "Configure this AI provider",
"aiProviderGeneral": "General",
"aiProviderGeneralDescription": "Basic settings for this provider",
"aiProviderConfiguration": "Configuration",
"aiProviderConfigurationDescription": "Network routing and authentication for this provider",
"aiProviderNetworkSettings": "Network Settings",
"aiProviderNetworkSettingsDescription": "Choose how traffic reaches this provider",
"aiProviderAuthSettings": "Authentication",
"aiProviderAuthSettingsDescription": "Configure how this provider authenticates requests to its upstream URL",
"aiProviderBudgetSettings": "Budget",
"aiProviderBudgetSettingsDescription": "Configure how this provider restricts usage based on spending or token limits",
"aiBudgetAdd": "Add Budget",
"aiBudgetEmpty": "No budgets configured yet. Click Add Budget to set a spending or token limit.",
"aiBudgetUnit": "Spend Type",
"aiBudgetPeriod": "Reset Period",
"aiBudgetAmount": "Maximum Spend",
"aiBudgetAmountPlaceholder": "Maximum spend",
"aiBudgetPeriodHourly": "Hourly",
"aiBudgetPeriodDaily": "Daily",
"aiBudgetPeriodWeekly": "Weekly",
"aiBudgetPeriodMonthly": "Monthly",
"aiBudgetPeriodYearly": "Yearly",
"aiBudgetPeriodLifetime": "Lifetime",
"aiBudgetUnitUsd": "USD",
"aiBudgetUnitTokens": "Tokens",
"aiBudgetConflictError": "A budget for this reset period and spend type already exists",
"aiBudgetInvalidAmountError": "Enter a maximum spend greater than 0",
"aiBudgetUpdated": "Budgets updated",
"aiBudgetErrorSave": "Failed to update budgets",
"aiProviderType": "Provider Type",
"aiProviderTypeSearch": "Search providers...",
"aiProviderTypeNotFound": "No provider type found",
"aiProviderTypeOpenai": "OpenAI",
"aiProviderTypeAnthropic": "Anthropic",
"aiProviderTypeGoogleGemini": "Google Gemini",
"aiProviderTypeVertexAi": "Vertex AI",
"aiProviderTypeBedrock": "Amazon Bedrock",
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
"aiProviderTypeOpenRouter": "OpenRouter",
"aiProviderTypeVercelAiGateway": "Vercel AI Gateway",
"aiProviderTypeCustom": "Custom",
"aiProviderTypeOpenaiDescription": "OpenAI API",
"aiProviderTypeAnthropicDescription": "Anthropic API",
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
"aiProviderTypeCustomDescription": "Bring your own endpoint or route via site targets",
"aiProviderUpstreamUrl": "Upstream URL",
"aiProviderUpstreamUrlDescription": "Base URL for the provider API",
"aiProviderUpstreamUrlOptionalDescription": "Leave blank to use the default upstream URL for this provider",
"aiProviderEffectiveUpstreamUrl": "Effective Upstream URL",
"aiProviderApiKey": "API Key",
"aiProviderApiKeyDescription": "API key used to authenticate requests to this provider",
"aiProviderCustomHeadersDescription": "Headers sent on every request to this provider. Newline separated: Header-Name: value",
"aiProviderApiKeyLastChars": "API Key",
"aiProviderAuthType": "Auth Type",
"aiProviderAuthTypeSearch": "Search auth types...",
"aiProviderAuthTypeNotFound": "No auth type found",
"aiProviderAuthTypeBearer": "Bearer",
"aiProviderAuthTypeBearerDescription": "Authorization: Bearer key. Used by OpenAI and most providers",
"aiProviderAuthTypeXApiKey": "x-api-key",
"aiProviderAuthTypeXApiKeyDescription": "x-api-key header. Used by Anthropic",
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key header. Used by Google Gemini",
"aiProviderAuthTypeHec": "Splunk HEC",
"aiProviderAuthTypeHecDescription": "Authorization: Splunk key. Used by Splunk HTTP Event Collector",
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Bearer key. Used by Cloudflare AI Gateway",
"aiProviderAuthTypeNone": "No Auth",
"aiProviderAuthTypePassthrough": "Passthrough",
"aiProviderAuthTypeDescription": "How the upstream API authenticates requests",
"aiProviderAuthTypePassthroughDescription": "Forward the caller's API key headers to the upstream",
"aiProviderAuthTypeNoneDescription": "Do not send authentication headers to the upstream",
"aiProviderRoutingMode": "Routing Mode",
"aiProviderRoutingModeDescription": "Send traffic to an upstream URL or to HTTP targets on your sites",
"aiProviderRoutingModeUrl": "Upstream URL",
"aiProviderRoutingModeUrlDescription": "Call a public or private API base URL",
"aiProviderRoutingModeTarget": "Site Targets",
"aiProviderRoutingModeTargetDescription": "Route through targets on your sites",
"aiProviderRoutingModeTargetNote": "After creating this provider, configure site targets on the Network Settings tab.",
"aiProviderTargetNoOne": "This provider doesn't have any targets. Add a target to route requests through your sites.",
"aiProviderRemoteNodeTargetsWarning": "Sites connected to remote nodes are inaccessable to be routed to on AI Gateway providers.",
"aiProviderSkipTlsVerification": "Skip TLS Verification",
"aiProviderSkipTlsVerificationDescription": "Disable TLS certificate verification for the upstream connection",
"aiProviderBudget": "Budget",
"aiProviderBudgetDescription": "Optional spending or token budget for this provider",
"aiProviderBudgetAmount": "Budget Amount",
"aiProviderBudgetUnit": "Budget Unit",
"aiProviderBudgetUnitUsd": "USD",
"aiProviderBudgetUnitTokens": "Tokens",
"aiProviderEnabled": "Enabled",
"aiProviderEnabledDescription": "Fully disable this provider across all resources",
"aiProviderErrorCreate": "Failed to create AI provider",
"aiProviderErrorUpdate": "Failed to update AI provider",
"aiProviderErrorDelete": "Failed to delete AI provider",
"aiProviderErrorLoad": "Failed to load AI provider",
"aiProviderErrorUpstreamUrlInvalid": "Enter a valid upstream URL",
"aiProviderErrorUpstreamUrlRequired": "Upstream URL is required for this provider",
"aiProviderErrorAuthTypeRequired": "Auth type is required",
"aiProviderErrorApiKeyRequired": "API key is required",
"aiProviderErrorRoutingModeTarget": "Site targets routing is only available for custom providers",
"aiProviderErrorCapabilitiesRequired": "Select at least one API capability",
"aiProviderCapabilities": "API Capabilities",
"aiProviderCapabilitiesDescription": "Select which API formats this provider can handle. Known providers start with recommended defaults.",
"aiProviderCapabilitiesCustomDescription": "Select which API formats this custom provider can handle",
"aiProviderCapabilitiesSelect": "Select capabilities",
"aiProviderCapabilitiesEmpty": "No capabilities found",
"aiProviderCapabilitiesSearch": "Search capabilities...",
"aiCapabilityOpenaiChat": "OpenAI Chat Completions",
"aiCapabilityOpenaiChatDescription": "Supports /v1/chat/completions",
"aiCapabilityOpenaiResponses": "OpenAI Responses",
"aiCapabilityOpenaiResponsesDescription": "Supports /v1/responses",
"aiCapabilityAnthropicMessages": "Anthropic Messages",
"aiCapabilityAnthropicMessagesDescription": "Supports /v1/messages",
"aiCapabilityV1Models": "Models List",
"aiCapabilityV1ModelsDescription": "Supports /v1/models model discovery",
"aiCapabilityGeminiGenerateContent": "Gemini Generate Content",
"aiCapabilityGeminiGenerateContentDescription": "Supports the direct Gemini API",
"aiCapabilityBedrockModelInvoke": "Bedrock Model Invoke",
"aiCapabilityBedrockModelInvokeDescription": "Supports Amazon Bedrock InvokeModel",
"aiCapabilityGoogleGenerateContent": "Vertex Generate Content",
"aiCapabilityGoogleGenerateContentDescription": "Supports Vertex AI Gemini format",
"aiCapabilityGoogleRawPredict": "Vertex Raw Predict",
"aiCapabilityGoogleRawPredictDescription": "Supports Vertex AI rawPredict for Anthropic models",
"aiCapabilityBedrockConverse": "Bedrock Converse",
"aiCapabilityBedrockConverseDescription": "Supports Amazon Bedrock Converse API",
"aiProviderCreated": "AI provider created",
"aiProviderUpdated": "AI provider updated",
"aiProviderDeleted": "AI provider deleted",
"aiProviderDelete": "Delete Provider",
"aiProviderDeleteConfirm": "Delete Provider",
"aiProviderQuestionRemove": "Are you sure you want to delete this AI provider?",
"aiProviderMessageRemove": "This will permanently delete the provider and its models and targets. This cannot be undone.",
"aiProviderErrorNoUpdate": "AI provider is not available to update",
"aiProviderModels": "Models",
"aiProviderModelsDescription": "Define allow and block lists for this provider. Requests must match an allow entry and must not match a block entry.",
"aiProviderCreateModelsDescription": "Choose which models this provider can serve. An empty allow list denies all traffic. You can add or change models later.",
"aiProviderModelsPlaceholder": "Search models or type a custom key",
"aiProviderModelsAllow": "Allow List",
"aiProviderModelsAllowDescription": "Models that may be used through this provider. Empty means deny all.",
"aiProviderModelsAllowPlaceholder": "Enter model key",
"aiProviderModelsAllowEmpty": "No models added yet.",
"aiProviderModelsBlock": "Block List",
"aiProviderModelsBlockDescription": "Models to deny even if they match an allow entry.",
"aiProviderModelsBlockPlaceholder": "Enter model key",
"aiProviderModelsBlockEmpty": "No models added yet.",
"aiProviderModelsAdd": "Add Models",
"aiProviderModelsClearAll": "Clear All",
"aiProviderModelsAddCustom": "Add \"{key}\"",
"aiProviderModelsAddCustomHint": "Press Enter to add this custom model key.",
"aiProviderModelsAddBulk": "Add {count} custom keys",
"aiProviderModelsAddBulkHint": "Press Enter to add {count} custom keys.",
"aiProviderModelsAddOne": "Add {key} now",
"aiProviderModelsAddSelected": "Add Selected",
"aiProviderModelsSelectedCount": "{count} selected",
"aiProviderModelsSelectAll": "Select all",
"aiProviderModelsClearSelected": "Clear",
"aiProviderModelsBulkHint": "Select known models or type a custom key.",
"aiProviderModelsCatalogEmpty": "No matching catalog models.",
"aiProviderModelsCatalogHeading": "Known Models",
"aiProviderModelsAllLabel": "All models",
"aiProviderModelsAllPatternHint": "Wildcard: *",
"aiProviderModelsAddAllAllow": "Allow all models",
"aiProviderModelsAddAllBlock": "Block all models",
"aiProviderModelsAddAllDescription": "Uses the * wildcard so every model key matches.",
"aiProviderModelsViewMore": "View more ({count})",
"aiProviderModelsViewLess": "View less",
"aiProviderModelsRemove": "Remove model",
"aiProviderModelsEditHint": "Click to edit model settings",
"aiProviderModelsSourceCatalog": "Known catalog model",
"aiProviderModelsSourceCustom": "Custom model key",
"aiProviderModelsSourcePattern": "Wildcard pattern",
"aiProviderModelsSourceAll": "Matches every model key",
"aiProviderModelsBudgetConfigured": "Budget configured",
"aiProviderModelsEditTitle": "Edit Model",
"aiProviderModelsEditDescription": "Update the model key or configure its budget.",
"aiProviderModelsBudgetTab": "Budget",
"aiProviderModelsKeyLabel": "Model Key",
"aiProviderModelsKeyRequired": "Enter a model key",
"aiProviderModelsKeyDuplicate": "This model key is already on a list",
"aiProviderModelsOverlapError": "These patterns cannot be on both lists: {keys}",
"aiProviderModelsUpdated": "Models updated",
"aiProviderModelsErrorUpdate": "Failed to update models",
"aiResourceProviders": "Providers",
"aiResourceProvidersDescription": "Choose which AI providers this AI gateway can use",
"aiResourceProvidersHelp": "Attach providers and choose inherit (use each provider's lists) or select (pick an allow list for this resource). Allow patterns that conflict across attached providers are not allowed.",
"aiResourceProvidersSelect": "Select providers",
"aiResourceProvidersEmpty": "No AI providers found",
"aiResourceProvidersNoneAttached": "No providers attached yet.",
"aiResourceProvidersAdd": "Add provider",
"aiResourceProvidersRemove": "Remove provider",
"aiResourceProviderToggleEnabled": "Enable or disable this provider on the resource",
"aiResourceProviderDisabled": "Disabled",
"aiResourceProvidersUpdated": "Providers updated",
"aiResourceProvidersErrorUpdate": "Failed to update providers",
"aiResourceProviderEditDescription": "Choose how this provider's models are exposed on this resource.",
"viewProviderSettings": "View Provider Settings",
"aiResourceProviderMode": "Access mode",
"aiResourceProviderModeInherit": "Inherit",
"aiResourceProviderModeSelect": "Select",
"aiResourceProviderModeSelectSummary": "Select · {count} models",
"aiResourceProviderModeInheritHelp": "Use this provider's allow and block lists as configured on the provider.",
"aiResourceProviderModeSelectHelp": "Choose a subset of this provider's allow-list models for this resource.",
"aiResourceProviderAllowModels": "Allow list",
"aiResourceProviderAllowModelsSelect": "Select models",
"aiResourceProviderAllowModelsSearch": "Search models...",
"aiResourceProviderAllowModelsEmpty": "No models found",
"aiResourceProviderAllowModelsHelp": "Only models from this provider's allow list can be selected.",
"aiResourceAliasRequired": "Alias is required for AI gateways",
"aiResourceDomainConfiguration": "Domain Configuration",
"aiResourceDomainConfigurationDescription": "Choose the domain clients will use to reach this AI gateway.",
"aiUsageAnalyticsTitle": "AI Usage Analytics",
"aiUsageAnalyticsDescription": "Analyze AI gateway cost, token usage, and activity across providers, resources, roles, and users",
"aiUsageTabOverview": "Overview",
"aiUsageTabProviders": "Providers",
"aiUsageTabResources": "Resources",
"aiUsageFilterProvider": "Provider",
"aiUsageFilterModel": "Model",
"aiUsageFilterResource": "Resource",
"aiUsageFilterRole": "Role",
"aiUsageFilterUser": "User",
"aiUsageFilterAllProviders": "All Providers",
"aiUsageFilterAllModels": "All Models",
"aiUsageFilterAllResources": "All Resources",
"aiUsageFilterAllRoles": "All Roles",
"aiUsageFilterAllUsers": "All Users",
"aiUsageFilterSearch": "Search...",
"aiUsageFilterNotFound": "No options found",
"aiUsageResetFilters": "Reset Filters",
"aiUsageRefresh": "Refresh",
"aiUsageTokenTypePrompt": "Prompt",
"aiUsageTokenTypeCacheRead": "Cache read",
"aiUsageTokenTypeCacheWrite": "Cache write",
"aiUsageTokenTypeCompletion": "Completion",
"aiUsageTokenTypeReasoning": "Reasoning",
"aiUsageRequests": "Requests",
"aiUsageCost": "Cost",
"aiUsageTokens": "Tokens",
"aiUsageOther": "Other",
"aiUsageTotalRequests": "Total Requests",
"aiUsageTotalTokens": "Total Tokens",
"aiUsageTotalCost": "Total Cost",
"aiUsageEstimated": "Estimated",
"aiUsageRequestVolume": "Request Volume",
"aiUsageTokenUsage": "Token Usage",
"aiUsageModelCost": "Model Cost",
"aiUsageModelTokens": "Model Tokens",
"aiUsageTopModels": "Top Models",
"aiUsageTopProviders": "Top Providers",
"aiUsageProviderCost": "Provider Cost",
"aiUsageProviderTokenUsage": "Provider Token Usage",
"aiUsageTopResources": "Top Resources",
"aiUsageResourceCost": "Resource Cost",
"aiUsageResourceTokenUsage": "Resource Token Usage",
"aiUsageResourceTypePublic": "Public Resource",
"aiUsageResourceTypeSite": "Private Resource",
"aiUsageNoResource": "No resource",
"aiUsageRolesTab": "Roles",
"aiUsageUsersTab": "Users",
"aiUsageTopRoles": "Top Roles",
"aiUsageRoleCost": "Role Cost",
"aiUsageRoleTokenUsage": "Role Token Usage",
"aiUsageTopUsers": "Top Users",
"aiUsageUserCost": "User Cost",
"aiUsageUserTokenUsage": "User Token Usage",
"aiUsageUnknownUser": "Unknown user",
"aiUsageVirtualApiKeysTab": "Virtual API Keys",
"aiUsageFilterVirtualApiKey": "Virtual API Key",
"aiUsageFilterAllVirtualApiKeys": "All Virtual API Keys",
"aiUsageTopVirtualApiKeys": "Top Virtual API Keys",
"aiUsageVirtualApiKeyCost": "Virtual API Key Cost",
"aiUsageVirtualApiKeyTokenUsage": "Virtual API Key Token Usage",
"aiUsageUnknownVirtualApiKey": "No virtual API key",
"aiUsageUnnamedVirtualApiKey": "Unnamed key",
"aiUsageLoading": "Loading...",
"aiUsageNoData": "No data",
"resourceBudgetSettings": "Budget",
"resourceBudgetSettingsDescription": "Configure how this AI gateway restricts usage based on spending or token limits",
"sidebarApiKeys": "API Keys",
"sidebarOrgs": "Organizations",
"sidebarProvisioning": "Provisioning",
"sidebarSettings": "Settings",
"sidebarAllUsers": "All Users",
"sidebarAllUsers": "Users",
"sidebarIdentityProviders": "Identity Providers",
"sidebarLicense": "License",
"sidebarClients": "Clients",
@@ -1695,6 +2190,8 @@
"alertingRuleSaved": "Alert rule saved",
"alertingRuleSavedCreatedDescription": "Your new alert rule was created. You can keep editing it on this page.",
"alertingRuleSavedUpdatedDescription": "Your changes to this alert rule were saved.",
"alertingTestAlertSent": "Test alert sent",
"alertingTestAlertSentDescription": "A test alert was sent to the actions configured on this rule.",
"alertingEditRule": "Edit Alert Rule",
"alertingCreateRule": "Create Alert Rule",
"alertingRuleCredenzaDescription": "Choose what to watch, when to fire, and how to notify",
@@ -1804,6 +2301,12 @@
"alertingRulesBannerDescription": "Each rule ties together what to watch (a site, health check, or resource), when to fire (for example offline or unhealthy), and how to notify your team via email, webhooks, or integrations. Use this list to create, enable, and manage those rules.",
"alertingHealthChecksBannerTitle": "Monitor Health & Resources",
"alertingHealthChecksBannerDescription": "Health checks are HTTP or TCP monitors you define once. You can then use them as sources in alert rules so you get notified when a target becomes healthy or unhealthy. Health checks on resources also appear here.",
"alertingTestRule": "Test Alert Rule",
"alertingAddActionHeading": "Add New Action",
"alertingSelectActionType": "Select an Action",
"alertingNoActionsTitle": "No actions configured",
"alertingNoActionsSaveDescription": "Add at least one action so this rule can notify someone when it fires.",
"alertingNoActionsTestDescription": "Add at least one action before you can test this rule.",
"standaloneHcTableTitle": "Health Checks",
"standaloneHcSearchPlaceholder": "Search health checks…",
"standaloneHcAddButton": "Create Health Check",
@@ -1995,6 +2498,9 @@
"domainPickerSubdomain": "Subdomain: {subdomain}",
"domainPickerNamespace": "Namespace: {namespace}",
"domainPickerShowMore": "Show More",
"domainPickerNoDomainsAvailableTitle": "No domains available",
"domainPickerNoDomainsAvailableDescription": "You don't have any domains set up yet. Create a domain to continue.",
"domainPickerNoDomainsAvailableAction": "Go to Domains",
"regionSelectorTitle": "Select Region",
"domainPickerRemoteExitNodeWarning": "Provided domains are not supported when sites connect to remote exit nodes. For resources to be available on remote nodes, use a custom domain instead.",
"regionSelectorInfo": "Selecting a region helps us provide better performance for your location. You do not have to be in the same region as your server.",
@@ -2119,6 +2625,7 @@
"createDomainType": "Type:",
"createDomainName": "Name:",
"createDomainValue": "Value:",
"multiSelectFilterCount": "{count} selected",
"createDomainCnameRecords": "CNAME Records",
"createDomainARecords": "A Records",
"createDomainRecordNumber": "Record {number}",
@@ -2190,6 +2697,8 @@
"subnetPlaceholder": "Subnet",
"addressDescription": "The internal address of the client. Must fall within the organization's subnet.",
"selectSites": "Select sites",
"selectResources": "Select resources",
"multiResourcesSelectorResourcesCount": "{count, plural, one {# resource} other {# resources}}",
"selectLabels": "Select labels",
"sitesDescription": "The client will have connectivity to the selected sites",
"clientInstallOlm": "Install Machine Client",
@@ -2231,6 +2740,7 @@
"healthScheme": "Method",
"healthSelectScheme": "Select Method",
"healthCheckPortInvalid": "Port must be between 1 and 65535",
"healthCheckHostnameInvalid": "Hostname must not contain whitespace",
"healthCheckPath": "Path",
"healthHostname": "IP / Host",
"healthPort": "Port",
@@ -2242,10 +2752,11 @@
"timeIsInSeconds": "Time is in seconds",
"requireDeviceApproval": "Require Device Approvals",
"requireDeviceApprovalDescription": "Users with this role need new devices approved by an admin before they can connect and access resources.",
"sshSettings": "SSH Settings",
"sshSettings": "SSH",
"inferenceSettings": "AI Gateway",
"sshAccess": "SSH Access",
"rdpSettings": "RDP Settings",
"vncSettings": "VNC Settings",
"rdpSettings": "RDP",
"vncSettings": "VNC",
"sshServer": "SSH Server",
"rdpServer": "RDP Server",
"vncServer": "VNC Server",
@@ -2358,7 +2869,7 @@
"resourcesTableProxyResources": "Public",
"resourcesTableClientResources": "Private",
"resourcesTableNoProxyResourcesFound": "No proxy resources found.",
"resourcesTableNoInternalResourcesFound": "No internal resources found.",
"resourcesTableNoInternalResourcesFound": "No private resources found.",
"resourcesTableDestination": "Destination",
"resourcesTableAlias": "Alias",
"resourcesTableAliasAddress": "Alias Address",
@@ -2381,9 +2892,9 @@
"editInternalResourceDialogCancel": "Cancel",
"editInternalResourceDialogSaveResource": "Save Resource",
"editInternalResourceDialogSuccess": "Success",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Internal resource updated successfully",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Private resource updated successfully",
"editInternalResourceDialogError": "Error",
"editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update internal resource",
"editInternalResourceDialogFailedToUpdateInternalResource": "Failed to update private resource",
"editInternalResourceDialogNameRequired": "Name is required",
"editInternalResourceDialogNameMaxLength": "Name must be less than 255 characters",
"editInternalResourceDialogProxyPortMin": "Proxy port must be at least 1",
@@ -2398,6 +2909,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeInference": "AI Gateway",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Scheme",
"editInternalResourceDialogEnableSsl": "Enable TLS",
@@ -2409,7 +2921,7 @@
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAliasDescription": "An optional internal DNS alias for this resource.",
"createInternalResourceDialogNoSitesAvailable": "No Sites Available",
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one Newt site with a subnet configured to create internal resources.",
"createInternalResourceDialogNoSitesAvailableDescription": "You need to have at least one site with a subnet configured to create private resources.",
"createInternalResourceDialogClose": "Close",
"createInternalResourceDialogCreateClientResource": "Create Private Resource",
"createInternalResourceDialogCreateClientResourceDescription": "Create a new resource that will only be accessible to clients connected to the organization",
@@ -2418,8 +2930,8 @@
"privateResourceAllowIcmpPing": "Allow ICMP Ping",
"privateResourceNetworkAccess": "Network Access",
"privateResourceNetworkAccessDescription": "Control TCP/UDP port access and whether ICMP ping is allowed for this resource.",
"hostSettings": "Host Settings",
"cidrSettings": "CIDR Settings",
"hostSettings": "Host",
"cidrSettings": "CIDR",
"createInternalResourceDialogResourceProperties": "Resource Properties",
"createInternalResourceDialogName": "Name",
"createInternalResourceDialogSite": "Site",
@@ -2438,9 +2950,9 @@
"createInternalResourceDialogCancel": "Cancel",
"createInternalResourceDialogCreateResource": "Create Resource",
"createInternalResourceDialogSuccess": "Success",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Internal resource created successfully",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Private resource created successfully",
"createInternalResourceDialogError": "Error",
"createInternalResourceDialogFailedToCreateInternalResource": "Failed to create internal resource",
"createInternalResourceDialogFailedToCreateInternalResource": "Failed to create private resource",
"createInternalResourceDialogNameRequired": "Name is required",
"createInternalResourceDialogNameMaxLength": "Name must be less than 255 characters",
"createInternalResourceDialogPleaseSelectSite": "Please select a site",
@@ -2457,6 +2969,7 @@
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"createInternalResourceDialogModeInference": "AI Gateway",
"scheme": "Scheme",
"createInternalResourceDialogScheme": "Scheme",
"createInternalResourceDialogEnableSsl": "Enable TLS",
@@ -2511,7 +3024,7 @@
"remoteExitNodeNetworkingSubnetsPlaceholder": "Add a CIDR range (e.g. 10.0.0.0/8)",
"remoteExitNodeNetworkingSubnetsLoadError": "Failed to load subnets",
"remoteExitNodeNetworkingLabelsTitle": "Preference Labels",
"remoteExitNodeNetworkingLabelsDescription": "Sites with these labels will be enforced to connect through this remote exit node.",
"remoteExitNodeNetworkingLabelsDescription": "Sites with these labels will prefer to connect through this remote exit node.",
"remoteExitNodeNetworkingLabelsButtonText": "Select labels...",
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "Search labels...",
"remoteExitNodeNetworkingLabelsLoadError": "Failed to load labels",
@@ -2987,7 +3500,8 @@
},
"priority": "Priority",
"priorityDescription": "Higher priority routes are evaluated first. Priority = 100 means automatic ordering (system decides). Use another number to enforce manual priority.",
"instanceName": "Instance Name",
"instanceName": "Server ID",
"clearInstanceName": "Reset Server Association",
"pathMatchModalTitle": "Configure Path Matching",
"pathMatchModalDescription": "Set up how incoming requests should be matched based on their path.",
"pathMatchType": "Match Type",
@@ -3048,6 +3562,7 @@
"validPassword": "Valid Password",
"validEmail": "Valid email",
"validSSO": "Valid SSO",
"validVirtualAPIKey": "Valid Virtual API Key",
"view": "View",
"configManaged": "Config Managed",
"connectedClient": "Connected Client",
@@ -3068,7 +3583,42 @@
"sidebarLogsAction": "Admin Action Logs",
"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",
"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",
"provider": "Provider",
"capability": "Capability",
"model": "Model",
"virtualApiKey": "Virtual API Key",
"noVirtualApiKey": "No virtual API key",
"stream": "Stream",
"streaming": "Streaming",
"nonStreaming": "Non-streaming",
"statusCode": "Status Code",
"aiSessionId": "Session ID",
"aiSessionRequest": "Request",
"aiSessionResponse": "Response",
"aiSessionNoData": "No data captured",
"aiSessionCouldNotParse": "(raw, could not parse transcript)",
"aiSessionLogTruncated": "This session was truncated before storage and may be incomplete.",
"aiSessionViewRaw": "View Raw JSON",
"aiSessionViewChat": "View Chat",
"cost": "Cost",
"estimated": "Estimated",
"tokenUsage": "Token Usage",
"promptTokens": "Prompt Tokens",
"cacheReadTokens": "Cache Read Tokens",
"cacheWriteTokens": "Cache Write Tokens",
"completionTokens": "Completion Tokens",
"reasoningTokens": "Reasoning Tokens",
"totalTokens": "Total Tokens",
"requestAnalyticsDescription": "View detailed request analytics for resources in this organization",
"logRetentionRequestLabel": "HTTP Request Log Retention",
"logRetentionRequestDescription": "How long to retain request logs",
@@ -3078,6 +3628,8 @@
"logRetentionActionDescription": "How long to retain action logs",
"logRetentionConnectionLabel": "Network Log Retention",
"logRetentionConnectionDescription": "How long to retain connection logs",
"logRetentionAISessionsLabel": "AI Gateway Session Log Retention",
"logRetentionAISessionsDescription": "How long to retain AI gateway prompt/response session logs",
"logRetentionDisabled": "Disabled",
"logRetention3Days": "3 days",
"logRetention7Days": "7 days",
@@ -3095,8 +3647,8 @@
"sourceAddress": "Source Address",
"destinationAddress": "Destination Address",
"duration": "Duration",
"licenseRequiredToUse": "An <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> license or <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> is required to use this feature. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"ossEnterpriseEditionRequired": "The <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> is required to use this feature. This feature is also available in <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Book a free demo or POC trial to learn more.</bookADemoLink>",
"licenseRequiredToUse": "<enterpriseEditionLink>Enterprise Edition license</enterpriseEditionLink> is required. <bookADemoLink>Book a demo or trial</bookADemoLink>",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition license</enterpriseEditionLink> is required. <bookADemoLink>Book a demo or trial</bookADemoLink>",
"certResolver": "Certificate Resolver",
"certResolverDescription": "Select the certificate resolver to use for this resource.",
"selectCertResolver": "Select Certificate Resolver",
@@ -3239,6 +3791,7 @@
"noData": "No Data",
"machineClients": "Machine Clients",
"install": "Install",
"downloadInstaller": "Download Installer",
"run": "Run",
"envFile": "Environment File",
"serviceFile": "Service File",
@@ -3294,9 +3847,9 @@
"internalResourceFormMultiSiteRoutingHelp": "Selecting multiple sites enables resilient routing and failover for high availability.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Learn more",
"editInternalResourceDialogPortRestrictionsDescription": "Restrict access to specific TCP/UDP ports or allow/block all ports.",
"createInternalResourceDialogHttpConfiguration": "HTTP configuration",
"createInternalResourceDialogHttpConfiguration": "Domain Configuration",
"createInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.",
"editInternalResourceDialogHttpConfiguration": "HTTP configuration",
"editInternalResourceDialogHttpConfiguration": "Domain Configuration",
"editInternalResourceDialogHttpConfigurationDescription": "Choose the domain clients will use to reach this resource over HTTP or HTTPS.",
"editInternalResourceDialogTcp": "TCP",
"editInternalResourceDialogUdp": "UDP",
@@ -3446,11 +3999,12 @@
"disconnected": "Disconnected",
"approvalsEmptyStateTitle": "Device Approvals Not Enabled",
"approvalsEmptyStateDescription": "Enable device approvals for roles to require admin approval before users can connect new devices.",
"approvalsEmptyStateHowToTitle": "How to Enable",
"approvalsEmptyStateStep1Title": "Go to Roles",
"approvalsEmptyStateStep1Description": "Navigate to your organization's roles settings to configure device approvals.",
"approvalsEmptyStateStep2Title": "Enable Device Approvals",
"approvalsEmptyStateStep2Description": "Edit a role and enable the 'Require Device Approvals' option. Users with this role will need admin approval for new devices.",
"approvalsEmptyStatePreviewDescription": "Preview: When enabled, pending device requests will appear here for review",
"approvalsEmptyStatePreviewDescription": "When enabled, pending device requests will appear here for review.",
"approvalsEmptyStateButtonText": "Manage Roles",
"domainErrorTitle": "We are having trouble verifying your domain",
"idpAdminAutoProvisionPoliciesTabHint": "Configure role mapping and organization policies on the <policiesTabLink>Auto Provision Settings</policiesTabLink> tab.",
@@ -3567,6 +4121,8 @@
"httpDestConnectionLogsDescription": "Site and tunnel connection events, including connects and disconnects.",
"httpDestRequestLogsTitle": "HTTP Request Logs",
"httpDestRequestLogsDescription": "HTTP request logs for proxied resources, including method, path, and response code.",
"httpDestAISessionLogsTitle": "AI Session Logs",
"httpDestAISessionLogsDescription": "AI gateway request and response sessions, including prompts, model responses, and token usage.",
"httpDestSaveChanges": "Save Changes",
"httpDestCreateDestination": "Create Destination",
"httpDestUpdatedSuccess": "Destination updated successfully",
@@ -3723,6 +4279,11 @@
"resourceLauncherViewAsAdmin": "View as Admin",
"resourceLauncherResourceDetailsDescription": "Connection information and status for this resource.",
"resourceLauncherResourceDetails": "Resource Details",
"resourceLauncherSitesDescription": "The resource is accessible via the following sites.",
"resourceLauncherViewSiteAsAdmin": "View Site as Admin",
"resourceLauncherFilterBySite": "Filter by Site",
"resourceLauncherSshCommand": "SSH Command",
"resourceLauncherSshCommandDescription": "Use the Pangolin CLI to open an SSH session to this resource.",
"resourceLauncherAuthMethodsDescription": "Authentication methods enabled for this resource.",
"resourceLauncherPrivateClientRequired": "Connect with a client on your device to access this resource privately.",
"resourceLauncherPrivateClientRequiredTitle": "Client Connection Required",
@@ -3732,7 +4293,18 @@
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherUnlabeled": "Unlabeled",
"resourceLauncherAiGateway": "AI Gateway",
"resourceLauncherNoSite": "No Site",
"resourceLauncherAvailableModels": "Available Models",
"resourceLauncherAvailableModelsDescription": "Models you can use with this AI gateway.",
"resourceLauncherAvailableModelsEmpty": "No models are available for this resource.",
"resourceLauncherAvailableModelsError": "Could not load available models.",
"resourceLauncherApiKeys": "API Keys",
"resourceLauncherApiKeysDescription": "Use your identity key or an attributed key to authenticate with this resource.",
"resourceLauncherApiKeysIdentity": "Identity Key",
"resourceLauncherApiKeysManual": "Attributed Keys",
"resourceLauncherApiKeysEmpty": "No API keys are available for this resource.",
"resourceLauncherApiKeysError": "Could not load API keys.",
"resourceLauncherNoResourcesInGroup": "No resources in this group",
"resourceLauncherEmptyStateTitle": "No Resources Available",
"resourceLauncherEmptyStateDescription": "You don't have access to any resources yet. Contact your administrator to request access.",
@@ -3759,9 +4331,9 @@
"resourceLauncherViewDeleteFailedDescription": "Could not delete the launcher view. Please try again.",
"memberPortalPrevious": "Previous",
"memberPortalNext": "Next",
"httpSettings": "HTTP Settings",
"tcpSettings": "TCP Settings",
"udpSettings": "UDP Settings",
"httpSettings": "HTTP",
"tcpSettings": "TCP",
"udpSettings": "UDP",
"sshTitle": "SSH",
"sshConnectingDescription": "Establishing a secure connection…",
"sshConnecting": "Connecting…",
@@ -3822,5 +4394,6 @@
"rdpUnicodeKeyboardMode": "Unicode keyboard mode",
"sessionToolbarShow": "Show toolbar",
"sessionToolbarHide": "Hide toolbar",
"actionUpdateSiteApprovals": "Update Site Approvals"
"actionUpdateSiteApprovals": "Update Site Approvals",
"check": "Check"
}
+619 -40
View File
File diff suppressed because it is too large Load Diff
+618 -39
View File
File diff suppressed because it is too large Load Diff
+645 -66
View File
File diff suppressed because it is too large Load Diff
+605 -26
View File
@@ -43,6 +43,8 @@
"inviteLoginUser": "올바른 사용자로 로그인했는지 확인하십시오.",
"inviteErrorNoUser": "죄송하지만, 접근하려는 초대가 존재하지 않는 사용자에 대한 것인 것 같습니다.",
"inviteCreateUser": "먼저 계정을 생성해 주세요.",
"inviteErrorOidcNotAllowed": "초대는 내부 계정만 수락할 수 있습니다. 이 이메일의 비밀번호로 로그아웃하고 다시 로그인하십시오.",
"inviteLoginInternalOnly": "초대에는 비밀번호가 있는 내부 계정이 필요합니다. 계정을 생성하거나 비밀번호로 로그인하십시오.",
"goHome": "홈으로 가기",
"inviteLogInOtherUser": "다른 사용자로 로그인",
"createAnAccount": "계정 만들기",
@@ -151,6 +153,7 @@
"siteResourcesTargetsOnSite": "이 사이트의 대상",
"siteSetting": "{siteName} 설정",
"siteNewtTunnel": "뉴트 사이트 (추천)",
"pangolinSite": "판골린 사이트",
"siteNewtTunnelDescription": "네트워크의 진입점을 생성하는 가장 쉬운 방법입니다. 추가 설정이 필요 없습니다.",
"siteWg": "기본 WireGuard",
"siteWgDescription": "모든 WireGuard 클라이언트를 사용하여 터널을 설정하세요. 수동 NAT 설정이 필요합니다.",
@@ -178,7 +181,7 @@
"shareDeleteConfirm": "공유 가능한 링크 삭제 확인",
"shareQuestionRemove": "이 공유 링크를 삭제하시겠습니까?",
"shareMessageRemove": "삭제되면 링크가 더 이상 작동하지 않으며, 이를 사용하는 모든 사용자는 자원에 대한 접근을 잃게 됩니다.",
"shareTokenDescription": "액세스 토큰은 쿼리 매개변수 또는 요청 헤더의 두 가지 방법으로 전달될 수 있습니다. 이는 인증된 액세스를 위해 클라이언트에서 모든 요청마다 전달되어야 합니다.",
"shareTokenDescription": "액세스 토큰은 쿼리 매개변수 또는 요청 헤더 전달될 수 있습니다. 기본적으로 모든 요청에 포함되어야 합니다. 세션 지속이 활성화된 경우, 첫 번째 요청이 세션 쿠키로 교환됩니다.",
"accessToken": "액세스 토큰",
"usageExamples": "사용 예",
"tokenId": "토큰 ID",
@@ -196,8 +199,14 @@
"shareTitleOptional": "제목 (선택 사항)",
"sharePathOptional": "경로 (선택 사항)",
"sharePathDescription": "링크는 인증 후 이 경로로 사용자를 리디렉션합니다.",
"shareAssociateUserOptional": "사용자 연관 (선택 사항)",
"shareAssociateUserDescription": "설정 시, 이 링크를 사용하는 요청은 액세스 로그와 ID 헤더에서 사용자로 기록됩니다. 사용자가 조직을 떠나면 링크가 제거됩니다.",
"userSelect": "사용자 선택",
"usersNotFound": "사용자를 찾을 수 없습니다",
"expireIn": "만료됨",
"neverExpire": "만료되지 않음",
"sharePersistSession": "첫 사용 후 세션 지속",
"sharePersistSessionDescription": "활성화된 경우, 쿼리 매개변수나 헤더를 통해 이 토큰으로 첫 요청 시 세션 쿠키가 설정되어 이후 요청에는 토큰이 필요하지 않습니다. 모든 요청에 토큰을 포함해야 하는 API 클라이언트의 경우, 이 옵션을 해제하세요.",
"shareExpireDescription": "만료 시간은 링크가 사용 가능하고 리소스에 접근할 수 있는 기간입니다. 이 시간이 지나면 링크는 더 이상 작동하지 않으며, 이 링크를 사용한 사용자는 리소스에 대한 접근 권한을 잃게 됩니다.",
"shareSeeOnce": "이 링크는 한 번만 볼 수 있습니다. 반드시 복사해 두세요.",
"shareAccessHint": "이 링크가 있는 누구나 리소스에 접근할 수 있습니다. 주의해서 공유하세요.",
@@ -280,7 +289,21 @@
"noCountryFound": "국가를 찾을 수 없습니다.",
"siteSelectionDescription": "이 사이트는 대상에 대한 연결을 제공합니다.",
"resourceType": "리소스 유형",
"resourceTypeDescription": "이것은 리소스 프로토콜 및 브라우저에서 렌더링되는 방식에 영향을 줍니다. 나중에 변경할 수 없습니다.",
"resourceTypeDescription": "이 리소스 프로토콜을 제어하며 나중에 변경할 수 없습니다.",
"resourceTypeSearch": "리소스 유형 검색...",
"resourceTypeNotFound": "리소스 유형이 발견되지 않았습니다",
"resourceTypeHttpDescription": "도메인 이름을 사용하여 HTTPS를 통한 웹 트래픽 프록시",
"resourceTypeInferenceDescription": "연결된 공급자를 통해 AI 요청을 라우팅",
"resourceTypeSshDescription": "브라우저에서 SSH 서버에 액세스",
"resourceTypeRdpDescription": "브라우저에서 원격 데스크톱에 액세스",
"resourceTypeVncDescription": "브라우저에서 VNC 데스크톱에 액세스",
"resourceTypeTcpDescription": "포트 번호를 사용하여 원시 TCP 트래픽 프록시",
"resourceTypeUdpDescription": "포트 번호를 사용하여 원시 UDP 트래픽 프록시",
"privateResourceTypeDescription": "클라이언트가 리소스에 도달하는 방법을 제어합니다. 나중에 변경할 수 없습니다.",
"privateResourceTypeHostDescription": "연결된 클라이언트에게 사이트 네트워크에서 단일 호스트를 노출",
"privateResourceTypeCidrDescription": "연결된 클라이언트에게 사이트 네트워크에서 CIDR 범위를 노출",
"privateResourceTypeHttpDescription": "도메인을 통해 HTTP 또는 HTTPS 서비스에 액세스",
"privateResourceTypeSshDescription": "연결된 클라이언트에서 SSH 서버에 액세스",
"resourceDomainDescription": "리소스는 숙주 네임에서 제공됩니다.",
"resourceHTTPSSettings": "HTTPS 설정",
"resourceHTTPSSettingsDescription": "리소스가 HTTPS로 접근할 수 있는 방식을 구성합니다.",
@@ -443,6 +466,8 @@
"apiKeysDelete": "API 키 삭제",
"apiKeysManage": "API 키 관리",
"apiKeysDescription": "API 키는 통합 API와 인증하는 데 사용됩니다.",
"orgsManage": "조직 관리",
"orgsDescription": "이 인스턴스에서 모든 조직을 보고 관리합니다",
"provisioningKeysTitle": "프로비저닝 키",
"provisioningKeysManage": "프로비저닝 키 관리",
"provisioningKeysDescription": "프로비저닝 키는 조직의 자동 사이트 프로비저닝 인증에 사용됩니다.",
@@ -694,6 +719,7 @@
"nameOptional": "이름 (선택 사항)",
"accessControls": "접근 제어",
"userDescription2": "이 사용자의 설정 관리",
"userGeneralSettingsDescription": "조직 내에서 이 사용자의 역할 및 설정을 관리하세요",
"accessRoleErrorAdd": "사용자를 역할에 추가하는 데 실패했습니다.",
"accessRoleErrorAddDescription": "사용자를 역할에 추가하는 동안 오류가 발생했습니다.",
"userSaved": "사용자 저장됨",
@@ -849,12 +875,16 @@
"authMethodsSave": "설정 저장",
"policyAuthStackTitle": "인증",
"policyAuthStackDescription": "이 리소스에 접근하려면 어떤 인증 방법이 필요한지 제어합니다",
"policyAuthInferenceStackDescription": "이 AI 게이트웨이에 인증할 수 있는 사용자 및 역할을 선택하십시오",
"policyAuthOrLogicTitle": "다수의 인증 방법 활성화",
"policyAuthOrLogicBanner": "방문자는 아래 활성화된 방법 중 하나만을 선택하여 인증할 수 있습니다. 모든 방법을 완료할 필요는 없습니다.",
"policyAuthMethodActive": "활성화",
"policyAuthMethodOff": "비활성화",
"policyAuthSsoTitle": "플랫폼 SSO",
"policyAuthSsoDescription": "사용자의 아이덴티티 공급자를 통해 로그인 필요",
"policyAuthInferenceIdentityKeySignInUrl": "로그인 URL",
"policyAuthInferenceIdentityKeyHelpNoUrl": "모든 사용자는 이미 ID API 키를 보유하고 있으므로 비사용자 클라이언트 또는 공유 액세스를 위한 가상 API 키만 생성하면 됩니다. 사용자는 이 리소스의 URL에서 ID 공급자로 로그인하면 키를 검색할 수 있습니다.",
"policyAuthInferenceIdentityKeyHelp": "모든 사용자는 이미 ID API 키를 보유하고 있으므로 비사용자 클라이언트 또는 공유 액세스를 위한 가상 API 키만 생성하면 됩니다. 사용자는 여기서 ID 공급자로 로그인 후 키를 검색할 수 있습니다.",
"policyAuthSsoSummary": "{idp} · {users} 사용자, {roles} 역할",
"policyAuthSsoDefaultIdp": "기본 공급자",
"policyAuthAddDefaultIdentityProvider": "기본 아이덴티티 공급자 추가",
@@ -886,7 +916,7 @@
"policyAccessRulesFallthroughOff": "규칙이 비활성화되면, 모든 트래픽은 인증으로 넘어갑니다.",
"policyAccessRulesFallthroughOn": "매칭되는 규칙이 없으면, 트래픽은 인증으로 넘어갑니다.",
"rulesPlaceholderCidr": "10.0.0.0/8",
"rulesPlaceholderPath": "/admin/*",
"rulesPlaceholderPath": "/관리자/*",
"rulesPlaceholderGeo": "RU, KP",
"rulesSave": "규칙 저장",
"resourceErrorCreate": "리소스 생성 오류",
@@ -928,7 +958,7 @@
"newtVersion": "버전",
"architecture": "아키텍처",
"sites": "사이트",
"siteWgAnyClients": "WireGuard 클라이언트를 사용하여 연결하십시오. 피어 IP를 사용하여 내부 리소스에 접근해야 합니다.",
"siteWgAnyClients": "WireGuard 클라이언트를 사용하여 연결하십시오. 피어 IP를 사용하여 개인 리소스에 접근해야 합니다.",
"siteWgCompatibleAllClients": "모든 WireGuard 클라이언트와 호환",
"siteWgManualConfigurationRequired": "수동 구성이 필요합니다.",
"userErrorNotAdminOrOwner": "사용자는 관리자 또는 소유자가 아닙니다.",
@@ -1073,6 +1103,8 @@
"accessRoleErrorNewRequired": "새 역할이 필요합니다.",
"accessRoleErrorRemove": "역할 제거에 실패했습니다.",
"accessRoleErrorRemoveDescription": "역할을 제거하는 동안 오류가 발생했습니다.",
"accessRoleInferenceBudget": "AI 예산",
"accessRoleInferenceBudgetDescription": "이 역할의 구성원이 AI 사용을 지출 또는 토큰 제한에 따라 제한하는 방법을 구성하십시오",
"accessRoleName": "역할 이름",
"accessRoleQuestionRemove": "`{name}` 역할을 삭제하려고 합니다. 이 작업은 되돌릴 수 없습니다.",
"accessRoleRemove": "역할 제거",
@@ -1148,6 +1180,10 @@
"idpJmespathAboutDescriptionLink": "JMESPath에 대해 더 알아보기",
"idpJmespathLabel": "식별자 경로",
"idpJmespathLabelDescription": "ID 토큰에서 사용자 식별자에 대한 경로",
"idpIdentifierChangeTitle": "식별자 경로 변경 경고",
"idpIdentifierChangeDescription": "식별자 경로를 변경하려고 합니다. 이는 기존 사용자의 매핑 방법에 영향을 미칩니다. 이전에 이 ID 공급자를 통해 로그인한 사용자는 더 이상 동일한 사용자로 인식되지 않을 수 있습니다. ",
"idpIdentifierChangeConfirmMessage": "확인합니다",
"idpIdentifierChangeWarningText": "이는 기존 사용자의 매핑 방법에 영향을 미칩니다",
"idpJmespathEmailPathOptional": "이메일 경로 (선택 사항)",
"idpJmespathEmailPathOptionalDescription": "ID 토큰에서 사용자의 이메일 경로",
"idpJmespathNamePathOptional": "이름 경로 (선택 사항)",
@@ -1392,6 +1428,24 @@
"logoutError": "로그아웃 중 오류 발생",
"signingAs": "로그인한 사용자",
"serverAdmin": "서버 관리자",
"promoteServerAdmin": "서버 관리자 승격",
"promoteServerAdminTitle": "서버 관리자 승격",
"promoteServerAdminQuestion": "{selectedUser}을 서버 관리자로 승격하시겠습니까?",
"promoteServerAdminMessage": "서버 관리자는 최고 권한을 갖고 서버를 관리할 수 있습니다.",
"promoteServerAdminWarning": "언제든지 사용자를 강등하여 이 작업을 되돌릴 수 있습니다.",
"promoteServerAdminConfirm": "서버 관리자 승격",
"promoteServerAdminSuccess": "사용자가 승격되었습니다",
"promoteServerAdminSuccessDescription": "{selectedUser}님이 이제 서버 관리자가 되었습니다.",
"promoteServerAdminError": "사용자 승격에 실패했습니다",
"demoteServerAdmin": "서버 관리자에서 강등",
"demoteServerAdminTitle": "서버 관리자에서 강등",
"demoteServerAdminQuestion": "{selectedUser}을 서버 관리자에서 강등하시겠습니까?",
"demoteServerAdminMessage": "{selectedUser}은 모든 서버 관리자 권한을 잃게 됩니다",
"demoteServerAdminWarning": "언제든지 사용자를 승격하여 이 작업을 되돌릴 수 있습니다.",
"demoteServerAdminConfirm": "서버 관리자에서 강등",
"demoteServerAdminSuccess": "사용자가 강등되었습니다",
"demoteServerAdminSuccessDescription": "{selectedUser}님이 더 이상 서버 관리자가 아닙니다.",
"demoteServerAdminError": "사용자 강등에 실패했습니다",
"managedSelfhosted": "관리 자체 호스팅",
"otpEnable": "이중 인증 활성화",
"otpDisable": "이중 인증 비활성화",
@@ -1418,6 +1472,28 @@
"actionDeleteSite": "사이트 삭제",
"actionGetSite": "사이트 가져오기",
"actionListSites": "사이트 목록",
"actionCreateAiProvider": "AI 공급자 생성",
"actionDeleteAiProvider": "AI 공급자 삭제",
"actionGetAiProvider": "AI 공급자 가져오기",
"actionListAiProviders": "AI 공급자 목록",
"actionUpdateAiProvider": "AI 공급자 업데이트",
"actionCreateAiModel": "AI 모델 생성",
"actionDeleteAiModel": "AI 모델 삭제",
"actionGetAiModel": "AI 모델 가져오기",
"actionListAiModels": "AI 모델 목록",
"actionUpdateAiModel": "AI 모델 업데이트",
"actionCreateAiBudget": "AI 예산 생성",
"actionDeleteAiBudget": "AI 예산 삭제",
"actionGetAiBudget": "AI 예산 가져오기",
"actionListAiBudgets": "AI 예산 목록",
"actionUpdateAiBudget": "AI 예산 업데이트",
"actionListResourceAiModels": "리소스 AI 모델 목록",
"actionSetResourceAiModels": "리소스 AI 모델 설정",
"actionCreateVirtualApiKey": "가상 API 키 생성",
"actionDeleteVirtualApiKey": "가상 API 키 삭제",
"actionGetVirtualApiKey": "가상 API 키 가져오기",
"actionListVirtualApiKeys": "가상 API 키 목록",
"actionUpdateVirtualApiKey": "가상 API 키 업데이트",
"actionApplyBlueprint": "청사진 적용",
"actionListBlueprints": "청사진 목록",
"actionGetBlueprint": "청사진 가져오기",
@@ -1443,8 +1519,11 @@
"actionSetResourcePincode": "리소스 핀코드 설정",
"actionSetResourceEmailWhitelist": "리소스 이메일 화이트리스트 설정",
"actionGetResourceEmailWhitelist": "리소스 이메일 화이트리스트 가져오기",
"actionListResourcePolicies": "리소스 정책 목록",
"actionCreateResourcePolicy": "리소스 정책 생성",
"actionGetResourcePolicy": "리소스 정책 가져오기",
"actionUpdateResourcePolicy": "리소스 정책 업데이트",
"actionDeleteResourcePolicy": "리소스 정책 삭제",
"actionSetResourcePolicyUsers": "리소스 정책 사용자 설정",
"actionSetResourcePolicyRoles": "리소스 정책 역할 설정",
"actionSetResourcePolicyPassword": "리소스 정책 비밀번호 설정",
@@ -1520,6 +1599,8 @@
"search": "검색…",
"searchPlaceholder": "검색...",
"emptySearchOptions": "옵션이 없습니다",
"ipFilterSearchPlaceholder": "IP 주소를 입력하세요…",
"ipFilterEmptyMessage": "필터링할 IP 주소를 입력하세요",
"create": "생성",
"orgs": "조직",
"loginError": "예기치 않은 오류가 발생했습니다. 다시 시도해주세요.",
@@ -1554,6 +1635,8 @@
"commandPaletteCreateMachineClient": "머신 클라이언트 생성",
"commandPaletteCreateAlertRule": "경고 규칙 생성",
"commandPaletteCreateIdentityProvider": "신원 공급자 생성",
"commandPaletteCreateAiProvider": "AI 공급자 생성",
"commandPaletteCreateVirtualApiKey": "가상 API 키 생성",
"commandPaletteToggleTheme": "테마 전환",
"commandPaletteChooseOrganization": "조직 선택",
"commandPaletteShortcutMac": "⌘K",
@@ -1616,7 +1699,425 @@
"sidebarInvitations": "초대",
"sidebarRoles": "역할",
"sidebarShareableLinks": "공유 가능한 링크",
"sidebarAiGateway": "AI 게이트웨이",
"sidebarAiProviders": "공급자",
"commandAiProviders": "AI 공급자",
"sidebarVirtualApiKeys": "가상 API 키",
"sidebarMyApiKeys": "귀하의 API 키",
"sidebarAccount": "실행기",
"commandVirtualApiKeys": "가상 API 키",
"virtualApiKeysTitle": "가상 API 키 관리",
"virtualApiKeysDescription": "공용 AI 게이트웨이에 대한 AI 게이트웨이 액세스를 위한 수동 API 키 생성 및 관리",
"virtualApiKeysTabIdentity": "신원 키",
"virtualApiKeysTabVirtual": "가상 키",
"virtualApiKeysIdentitySplashTitle": "모든 사용자에 대한 신원 키",
"virtualApiKeysIdentitySplashDescription": "이 조직을 위한 Pangolin 신원 키가 모든 사용자에게 이미 있습니다. 계정에 고유하며 액세스할 수 있는 AI 게이트웨이에 인증합니다.",
"virtualApiKeysIdentitySplashExample": "예시",
"virtualApiKeysIdentitySplashRetrieveTitle": "사용자가 키 받는 방법",
"virtualApiKeysIdentitySplashRetrieveResource": "브라우저에서 공용 AI 게이트웨이 URL을 방문하고 계정으로 로그인합니다.",
"virtualApiKeysIdentitySplashRetrievePage": "<url></url>을 방문하여 로그인한 상태에서 이동하십시오.",
"virtualApiKeysIdentitySplashManual": "추가 가상 API 키를 가상 키 탭에서 생성할 수 있습니다. 이러한 키는 특정 공용 AI 게이트웨이에 범위를 지정할 수 있으며, 선택적으로 사용자와 연결할 수 있습니다. 키 생성 즉시 선택된 공용 AI 게이트웨이에 대한 액세스 권한이 부여됩니다.",
"virtualApiKeysIdentitySplashGoToVirtual": "키 수동 생성",
"virtualApiKeysEmailIdentity": "이메일 신원 키",
"virtualApiKeysEmailIdentityDescription": "선택한 각 사용자 또는 역할에게 Pangolin 신원 키 보내기.",
"virtualApiKeysEmailIdentitySendAll": "모든 사용자에게 보내기",
"virtualApiKeysEmailIdentitySendAllDescription": "계정 이메일이 있는 모든 조직 구성원에게 이메일 발송.",
"virtualApiKeysEmailIdentitySelectUsers": "사용자",
"virtualApiKeysEmailIdentitySelectRoles": "역할",
"virtualApiKeysEmailIdentitySubmit": "이메일 보내기",
"virtualApiKeysEmailIdentitySuccess": "신원 키 전송",
"virtualApiKeysEmailIdentitySuccessDescription": "{sent}개의 이메일이 전송되었습니다.",
"virtualApiKeysEmailIdentitySkipped": "{skipped}명의 사용자가 이메일 주소가 없어 건너뛰었습니다.",
"virtualApiKeysEmailIdentityRecipientsRequired": "최소한 하나의 사용자 또는 역할을 선택하거나 모든 사용자에게 보냅니다.",
"virtualApiKeysEmailIdentityError": "신원 키 전송 오류",
"virtualApiKeysEmailIdentityErrorDescription": "신원 키 이메일 전송 실패",
"virtualApiKeysBannerTitle": "모든 사용자에 대한 신원 키",
"virtualApiKeysBannerDescription": "모든 사용자는 {keysUrl}에서 신원 키를 이미 사용할 수 있습니다. 공용 AI 게이트웨이에 대한 직접 액세스를 부여하는 키를 수동으로 여기에서 생성할 수도 있습니다.",
"virtualApiKeysBannerButtonText": "신원 키 보기",
"virtualApiKeys": "가상 API 키",
"virtualApiKeysSearch": "키 검색...",
"virtualApiKeysCreate": "가상 API 키 생성",
"virtualApiKeysCreateDescription": "이 조직의 공용 AI 게이트웨이를 호출할 수 있는 수동 키 발급",
"virtualApiKeysCreateButton": "키 생성",
"virtualApiKeysEmpty": "가상 API 키가 아직 없습니다",
"virtualApiKeysName": "이름",
"virtualApiKeysDescriptionOptional": "설명 (선택사항)",
"virtualApiKeysAssociateUserOptional": "사용자 연결 (선택사항)",
"virtualApiKeysAssociateUserDescription": "이 키를 사용자와 연결하여 사용을 추적합니다. 생성되면 선택한 공용 AI 게이트웨이에 즉시 액세스 권한이 부여됩니다. 수동으로 리소스와 연결할 필요가 없습니다. 키는 사용자의 프로필에도 표시됩니다.",
"virtualApiKeysAllResources": "모든 공용 AI 게이트웨이",
"virtualApiKeysAllResourcesDescription": "이 키가 조직의 모든 공용 AI 게이트웨이에 액세스할 수 있도록 허용",
"virtualApiKeysSelectResources": "공용 AI 게이트웨이",
"virtualApiKeysSelectResourcesPlaceholder": "리소스 선택",
"virtualApiKeysSelectResourcesDescription": "이 키가 액세스할 수 있는 공용 AI 게이트웨이를 선택하십시오",
"virtualApiKeysNoResources": "리소스 없음",
"virtualApiKeysSecret": "키",
"virtualApiKeysCopyKey": "이 키를 복사하십시오. 나중에 표에서 또는 편집할 때 다시 볼 수 있습니다.",
"virtualApiKeysViewSecret": "비밀 보기",
"virtualApiKeysViewSecretTitle": "가상 API 키 비밀",
"virtualApiKeysViewSecretDescription": "이 비밀은 이 키에 할당된 공용 AI 게이트웨이에 대한 액세스를 부여합니다",
"virtualApiKeysEdit": "가상 API 키 편집",
"virtualApiKeysEditDescription": "이 키의 관련 사용자 및 공용 AI 게이트웨이 액세스를 업데이트하십시오",
"virtualApiKeysSaveButton": "변경 사항 저장",
"virtualApiKeysSelectResourcesRequired": "최소한 하나의 공용 AI 게이트웨이를 선택하거나 모든 공용 AI 게이트웨이를 활성화하십시오",
"virtualApiKeysUpdated": "가상 API 키가 업데이트되었습니다",
"virtualApiKeysUpdatedDescription": "가상 API 키가 업데이트되었습니다",
"virtualApiKeysErrorUpdate": "가상 API 키 업데이트 오류",
"virtualApiKeysErrorUpdateDescription": "가상 API 키 업데이트에 실패했습니다",
"virtualApiKeysErrorCreate": "가상 API 키 생성 오류",
"virtualApiKeysErrorCreateDescription": "가상 API 키 생성에 실패했습니다",
"virtualApiKeysErrorDelete": "가상 API 키 삭제 오류",
"virtualApiKeysErrorDeleteMessage": "가상 API 키 삭제에 실패했습니다",
"virtualApiKeysDeleted": "가상 API 키가 삭제되었습니다",
"virtualApiKeysDeletedDescription": "가상 API 키가 삭제되었습니다",
"virtualApiKeysDelete": "가상 API 키 삭제",
"virtualApiKeysDeleteConfirm": "키 삭제",
"virtualApiKeysQuestionRemove": "이 가상 API 키를 삭제하시겠습니까?",
"virtualApiKeysMessageRemove": "이 키를 사용하는 클라이언트는 즉시 액세스를 잃습니다.",
"virtualApiKeysErrorFetchSecret": "비밀 로딩 오류",
"virtualApiKeysErrorFetchSecretDescription": "가상 API 키 비밀 로딩에 실패했습니다",
"virtualApiKeysFilterUnassigned": "할당되지 않음",
"virtualApiKeysInferenceBudget": "예산",
"virtualApiKeysInferenceBudgetDescription": "이 키가 지출 또는 토큰 제한에 따라 AI 사용을 제한하는 방법을 구성하십시오",
"virtualApiKeysEmailOnGenerate": "생성 시 이메일 키",
"virtualApiKeysEmailThisKey": "이 키를 이메일로 발송",
"virtualApiKeysEmailOnGenerateDescription": "생성 후 이 키를 관련 사용자 및 추가 주소로 보냅니다",
"virtualApiKeysEmailThisKeyDescription": "현재 키를 관련 사용자 및 추가 주소로 보냅니다",
"virtualApiKeysEmailSmtpRequired": "이 서버에 이메일 구성이 되어 있지 않습니다",
"virtualApiKeysEmailSmtpRequiredDescription": "가상 API 키를 이메일로 보내려면 SMTP를 구성하십시오.",
"virtualApiKeysEmailSendToUser": "관련 사용자에게 전송",
"virtualApiKeysEmailSendToUserDescription": "관련 사용자의 계정 이메일로 키 전송",
"virtualApiKeysEmailSendToUserDisabled": "키를 해당 사용자에게 보내기 위해 사용자를 연결하십시오",
"virtualApiKeysEmailAdditional": "추가 이메일",
"virtualApiKeysEmailAdditionalPlaceholder": "이메일을 추가하고 Enter를 누르십시오",
"virtualApiKeysEmailRecipientsRequired": "관련 사용자를 선택하거나 최소한 하나의 이메일 주소를 추가하십시오",
"myVirtualApiKeysTitle": "귀하의 API 키",
"myVirtualApiKeysDescription": "이 조직에서 자신에게 할당된 신원 키 및 가상 API 키를 확인하십시오",
"myVirtualApiKeysResourceTitle": "{resourceName}에 대한 귀하의 API 키",
"myVirtualApiKeysResourceDescription": "{resourceName}에 액세스할 수 있도록 귀하에게 할당된 신원 키 및 가상 API 키를 확인하십시오",
"myVirtualApiKeysIdentityTitle": "신원 키",
"myVirtualApiKeysIdentityHeadline": "개인 API 키",
"myVirtualApiKeysIdentityDescription": "이 조직에 대한 귀하의 개인 키입니다. AI 게이트웨이 리소스를 호출할 때 귀하를 식별하는 데 사용됩니다.",
"myVirtualApiKeysIdentityResourceHeadline": "{resourceName}에 대한 귀하의 개인 API 키",
"myVirtualApiKeysIdentityResourceDescription": "이 조직에 대한 귀하의 개인 키입니다. {resourceName}을 호출하는 데 사용하십시오.",
"myVirtualApiKeysManualTitle": "귀속 키",
"myVirtualApiKeysManualDescription": "관리자가 귀하의 계정에 연결한 수동 가상 API 키",
"myVirtualApiKeysManualResourceDescription": "{resourceName}에 액세스할 수 있는 귀하의 계정에 연결된 수동 가상 API 키",
"myVirtualApiKeysManualEmpty": "아직 귀속된 키가 없습니다",
"myVirtualApiKeysKindUser": "신원",
"myVirtualApiKeysKindManual": "수동",
"myVirtualApiKeysUnnamed": "이름 없는 키",
"myVirtualApiKeysRevealSecret": "비밀 공개",
"myVirtualApiKeysViewSecretDescription": "이 비밀은 AI 게이트웨이 리소스에 대한 귀하의 인증을 수행합니다",
"aiClientConfigTitle": "코딩 에이전트 구성",
"aiClientConfigDescription": "인기 코딩 에이전트의 구성을 복사하거나 Pangolin CLI가 자동으로 설정하도록 하세요.",
"aiClientConfigDescriptionClaude": "Anthropic의 터미널 에이전트 코딩 도구입니다.",
"aiClientConfigDescriptionCodex": "OpenAI의 터미널 에이전트 코딩 도구입니다.",
"aiClientConfigDescriptionOpencode": "오픈 소스 터미널 코딩 에이전트.",
"aiClientConfigDescriptionGemini": "터미널용 구글의 에이전시 코딩 도구.",
"aiClientConfigSetup": "설정",
"aiClientConfigTabCli": "자동 (CLI)",
"aiClientConfigTabManual": "수동 구성",
"aiClientConfigPreset": "설정",
"aiClientConfigStep": "단계 {number}",
"aiClientConfigEndpointPlaceholder": "https://example.resource.url.com",
"aiClientConfigPlaceholderWarning": "이 스니펫에는 모델 이름이나 리소스 URL과 같은 예제 값이 포함되어 있습니다. 사용하기 전에 이를 고유 값으로 바꾸세요.",
"aiClientConfigRevealError": "API 키를 로드할 수 없습니다.",
"aiClientConfigRevealRetry": "다시 시도하십시오",
"resourceGeneralAiClientConfigDescription": "이 리소스는 Claude Code 및 OpenCode와 같은 일반적인 클라이언트에서 사용할 수 있습니다. <configLink>더 알아보기</configLink>",
"aiProvidersTitle": "AI 공급자",
"aiProvidersDescription": "이 조직의 AI 작업량에 대한 모델 공급자를 연결하십시오",
"aiProvidersBannerTitle": "모델 공급자 연결",
"aiProvidersBannerDescription": "공급자는 AI 작업량을 위한 백엔드 모델입니다. 여기에서 OpenAI, Anthropic 및 기타 공급자를 연결한 다음 공용 AI 게이트웨이에 연결하여 사용자가 정체성 인식 액세스, 예산 및 로깅으로 모델을 호출할 수 있도록 하십시오.",
"aiProvidersAdd": "공급자 추가",
"aiProvidersSearch": "공급자 검색...",
"aiProvidersEmpty": "아직 AI 공급자가 없습니다",
"aiProviderCreate": "AI 공급자 생성",
"aiProviderCreateDescription": "이 조직을 위한 모델 공급자 추가",
"aiProviderSeeAll": "모든 공급자 보기",
"aiProviderSetting": "{providerName}을 위한 공급자 설정",
"aiProviderSettingDescription": "이 AI 공급자 구성을 설정하십시오",
"aiProviderGeneral": "일반",
"aiProviderGeneralDescription": "이 공급자의 기본 설정",
"aiProviderConfiguration": "설정",
"aiProviderConfigurationDescription": "이 공급자의 네트워크 라우팅 및 인증 설정",
"aiProviderNetworkSettings": "네트워크 설정",
"aiProviderNetworkSettingsDescription": "이 공급자로 트래픽을 전달하는 방법을 선택하십시오",
"aiProviderAuthSettings": "인증",
"aiProviderAuthSettingsDescription": "이 공급자가 업스트림 URL에 대한 요청을 인증하는 방법을 구성하십시오",
"aiProviderBudgetSettings": "예산",
"aiProviderBudgetSettingsDescription": "이 공급자가 지출 또는 토큰 한도에 따라 사용을 제한하는 방법을 구성하십시오",
"aiBudgetAdd": "예산 추가",
"aiBudgetEmpty": "아직 예산 구성되지 않았습니다. 예산 추가를 클릭하여 지출 또는 토큰 한도를 설정하십시오.",
"aiBudgetUnit": "지출 유형",
"aiBudgetPeriod": "재설정 기간",
"aiBudgetAmount": "최대 지출",
"aiBudgetAmountPlaceholder": "최대 지출",
"aiBudgetPeriodHourly": "시간당",
"aiBudgetPeriodDaily": "일일",
"aiBudgetPeriodWeekly": "주간",
"aiBudgetPeriodMonthly": "월간",
"aiBudgetPeriodYearly": "연간",
"aiBudgetPeriodLifetime": "평생",
"aiBudgetUnitUsd": "USD",
"aiBudgetUnitTokens": "토큰",
"aiBudgetConflictError": "이미 이 재설정 기간과 지출 유형에 대한 예산이 존재합니다",
"aiBudgetInvalidAmountError": "0보다 큰 최대 지출을 입력하십시오",
"aiBudgetUpdated": "예산 업데이트됨",
"aiBudgetErrorSave": "예산 업데이트 실패",
"aiProviderType": "공급자 유형",
"aiProviderTypeSearch": "공급자 검색...",
"aiProviderTypeNotFound": "공급자 유형 없음",
"aiProviderTypeOpenai": "OpenAI",
"aiProviderTypeAnthropic": "Anthropic",
"aiProviderTypeGoogleGemini": "Google Gemini",
"aiProviderTypeVertexAi": "Vertex AI",
"aiProviderTypeBedrock": "Amazon Bedrock",
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
"aiProviderTypeOpenRouter": "OpenRouter",
"aiProviderTypeVercelAiGateway": "Vercel AI 게이트웨이",
"aiProviderTypeCustom": "사용자 정의",
"aiProviderTypeOpenaiDescription": "OpenAI API",
"aiProviderTypeAnthropicDescription": "Anthropic API",
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI 게이트웨이 API",
"aiProviderTypeCustomDescription": "자체 엔드포인트를 가져오거나 사이트 타겟을 통해 라우트하십시오",
"aiProviderUpstreamUrl": "상류 URL",
"aiProviderUpstreamUrlDescription": "공급자 API의 기본 URL",
"aiProviderUpstreamUrlOptionalDescription": "기본 상류 URL 사용을 위해 빈칸으로 두십시오",
"aiProviderEffectiveUpstreamUrl": "유효 상류 URL",
"aiProviderApiKey": "API 키",
"aiProviderApiKeyDescription": "이 공급자에 대한 요청을 인증하기 위해 사용하는 API 키",
"aiProviderCustomHeadersDescription": "이 공급자에게 보내는 모든 요청에 대해 전송되는 헤더. 새 줄로 구분된: 헤더-이름: 값",
"aiProviderApiKeyLastChars": "API 키",
"aiProviderAuthType": "인증 유형",
"aiProviderAuthTypeSearch": "인증 유형 검색...",
"aiProviderAuthTypeNotFound": "인증 유형을 찾을 수 없습니다",
"aiProviderAuthTypeBearer": "Bearer",
"aiProviderAuthTypeBearerDescription": "Authorization: Bearer 키. OpenAI 및 대부분의 공급자에서 사용",
"aiProviderAuthTypeXApiKey": "x-api-key",
"aiProviderAuthTypeXApiKeyDescription": "x-api-key 헤더. Anthropic에서 사용",
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key 헤더. Google Gemini에서 사용",
"aiProviderAuthTypeHec": "Splunk HEC",
"aiProviderAuthTypeHecDescription": "Authorization: Splunk 키. Splunk HTTP 이벤트 수집기에서 사용",
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI 게이트웨이",
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Bearer 키. Cloudflare AI 게이트웨이에서 사용",
"aiProviderAuthTypeNone": "인증 없음",
"aiProviderAuthTypePassthrough": "통과",
"aiProviderAuthTypeDescription": "상류 API에서 요청을 인증하는 방법",
"aiProviderAuthTypePassthroughDescription": "발신자의 API 키 헤더를 상류로 전달",
"aiProviderAuthTypeNoneDescription": "상류로 인증 헤더를 보내지 않음",
"aiProviderRoutingMode": "라우팅 모드",
"aiProviderRoutingModeDescription": "상류 URL로 트래픽을 보내거나 사이트의 HTTP 타겟으로 보냅니다",
"aiProviderRoutingModeUrl": "상류 URL",
"aiProviderRoutingModeUrlDescription": "공개 또는 개인 API의 기본 URL 호출",
"aiProviderRoutingModeTarget": "사이트 타겟",
"aiProviderRoutingModeTargetDescription": "사이트의 타겟을 통해 라우트",
"aiProviderRoutingModeTargetNote": "이 공급자를 생성한 후 네트워크 설정 탭에 사이트 타겟을 구성합니다.",
"aiProviderTargetNoOne": "이 공급자에게 타겟이 없습니다. 사이트를 통해 요청을 라우트하기 위한 타겟을 추가하십시오.",
"aiProviderRemoteNodeTargetsWarning": "원격 노드에 연결된 사이트는 AI 게이트웨이 공급자에게 라우팅되지 않습니다.",
"aiProviderSkipTlsVerification": "TLS 검증 건너뛰기",
"aiProviderSkipTlsVerificationDescription": "상류 연결에 대한 TLS 인증서 검증 비활성화",
"aiProviderBudget": "예산",
"aiProviderBudgetDescription": "이 공급자에 대한 선택적 지출 또는 토큰 예산",
"aiProviderBudgetAmount": "예산 금액",
"aiProviderBudgetUnit": "예산 단위",
"aiProviderBudgetUnitUsd": "USD",
"aiProviderBudgetUnitTokens": "토큰",
"aiProviderEnabled": "활성화됨",
"aiProviderEnabledDescription": "모든 리소스에 대해 이 공급자를 완전히 비활성화합니다",
"aiProviderErrorCreate": "AI 공급자 생성에 실패했습니다",
"aiProviderErrorUpdate": "AI 공급자 업데이트에 실패했습니다",
"aiProviderErrorDelete": "AI 공급자 삭제에 실패했습니다",
"aiProviderErrorLoad": "AI 공급자 로드에 실패했습니다",
"aiProviderErrorUpstreamUrlInvalid": "유효한 상류 URL을 입력하십시오",
"aiProviderErrorUpstreamUrlRequired": "이 공급자에는 상류 URL이 필요합니다",
"aiProviderErrorAuthTypeRequired": "인증 유형이 필요합니다",
"aiProviderErrorApiKeyRequired": "API 키가 필요합니다",
"aiProviderErrorRoutingModeTarget": "사이트 타겟 라우팅은 맞춤형 공급자에게만 가능합니다",
"aiProviderErrorCapabilitiesRequired": "최소 한 가지 API 기능을 선택하십시오",
"aiProviderCapabilities": "API 기능",
"aiProviderCapabilitiesDescription": "이 공급자가 처리할 수 있는 API 형식을 선택하십시오. 알려진 공급자는 권장 기본값으로 시작합니다.",
"aiProviderCapabilitiesCustomDescription": "사용자 지정 제공자가 처리할 수 있는 API 형식을 선택하십시오",
"aiProviderCapabilitiesSelect": "기능 선택",
"aiProviderCapabilitiesEmpty": "능력을 찾을 수 없습니다",
"aiProviderCapabilitiesSearch": "능력 검색...",
"aiCapabilityOpenaiChat": "OpenAI 채팅 완료",
"aiCapabilityOpenaiChatDescription": "/v1/chat/completions 지원",
"aiCapabilityOpenaiResponses": "OpenAI 응답",
"aiCapabilityOpenaiResponsesDescription": "/v1/responses 지원",
"aiCapabilityAnthropicMessages": "Anthropic 메시지",
"aiCapabilityAnthropicMessagesDescription": "/v1/messages 지원",
"aiCapabilityV1Models": "모델 목록",
"aiCapabilityV1ModelsDescription": "/v1/models 모델 검색 지원",
"aiCapabilityGeminiGenerateContent": "Gemini 콘텐츠 생성",
"aiCapabilityGeminiGenerateContentDescription": "직접 Gemini API 지원",
"aiCapabilityBedrockModelInvoke": "Bedrock 모델 실행",
"aiCapabilityBedrockModelInvokeDescription": "Amazon Bedrock InvokeModel 지원",
"aiCapabilityGoogleGenerateContent": "Vertex 콘텐츠 생성",
"aiCapabilityGoogleGenerateContentDescription": "Vertex AI Gemini 형식 지원",
"aiCapabilityGoogleRawPredict": "Vertex Raw 예측",
"aiCapabilityGoogleRawPredictDescription": "Anthropic 모델에 대한 Vertex AI rawPredict 지원",
"aiCapabilityBedrockConverse": "Bedrock 회화",
"aiCapabilityBedrockConverseDescription": "Amazon Bedrock Converse API 지원",
"aiProviderCreated": "AI 제공자가 생성되었습니다",
"aiProviderUpdated": "AI 제공자가 업데이트되었습니다",
"aiProviderDeleted": "AI 제공자가 삭제되었습니다",
"aiProviderDelete": "제공자 삭제",
"aiProviderDeleteConfirm": "제공자 삭제",
"aiProviderQuestionRemove": "이 AI 제공자를 삭제하시겠습니까?",
"aiProviderMessageRemove": "이 작업을 실행하면 제공자와 해당 모델 및 대상이 영구적으로 삭제됩니다. 이는 되돌릴 수 없습니다.",
"aiProviderErrorNoUpdate": "AI 제공자를 업데이트할 수 없습니다",
"aiProviderModels": "모델",
"aiProviderModelsDescription": "이 제공자에 대한 허용 및 차단 목록을 정의하십시오. 요청은 허용 항목과 일치해야 하며 차단 항목과 일치하지 않아야 합니다.",
"aiProviderCreateModelsDescription": "이 제공자가 제공할 수 있는 모델을 선택합니다. 허용 목록이 비어 있으면 모든 트래픽이 거부됩니다. 나중에 모델을 추가하거나 변경할 수 있습니다.",
"aiProviderModelsPlaceholder": "모델을 검색하거나 사용자 지정 키를 입력하세요",
"aiProviderModelsAllow": "허용 목록",
"aiProviderModelsAllowDescription": "이 제공자를 통해 사용할 수 있는 모델입니다. 비어 있으면 모두 거부를 의미합니다.",
"aiProviderModelsAllowPlaceholder": "모델 키 입력",
"aiProviderModelsAllowEmpty": "아직 추가된 모델이 없습니다.",
"aiProviderModelsBlock": "차단 목록",
"aiProviderModelsBlockDescription": "허용 항목과 일치하더라도 사용할 수 없는 모델입니다.",
"aiProviderModelsBlockPlaceholder": "모델 키 입력",
"aiProviderModelsBlockEmpty": "아직 추가된 모델이 없습니다.",
"aiProviderModelsAdd": "모델 추가",
"aiProviderModelsClearAll": "모두 지우기",
"aiProviderModelsAddCustom": "\"{key}\" 추가",
"aiProviderModelsAddCustomHint": "이 사용자 지정 모델 키를 추가하려면 Enter 키를 누르십시오.",
"aiProviderModelsAddBulk": "{count}개의 사용자 지정 키 추가",
"aiProviderModelsAddBulkHint": "{count}개의 사용자 지정 키를 추가하려면 Enter 키를 누르십시오.",
"aiProviderModelsAddOne": "지금 {key} 추가",
"aiProviderModelsAddSelected": "선택 항목 추가",
"aiProviderModelsSelectedCount": "{count}개 선택됨",
"aiProviderModelsSelectAll": "모두 선택",
"aiProviderModelsClearSelected": "지우기",
"aiProviderModelsBulkHint": "알려진 모델을 선택하거나 사용자 지정 키를 입력하십시오.",
"aiProviderModelsCatalogEmpty": "일치하는 카탈로그 모델이 없습니다.",
"aiProviderModelsCatalogHeading": "알려진 모델",
"aiProviderModelsAllLabel": "모든 모델",
"aiProviderModelsAllPatternHint": "와일드카드: *",
"aiProviderModelsAddAllAllow": "모든 모델 허용",
"aiProviderModelsAddAllBlock": "모델 모두 차단",
"aiProviderModelsAddAllDescription": "* 와일드카드를 사용하여 모든 모델 키를 일치시킵니다.",
"aiProviderModelsViewMore": "더 보기 ({count})",
"aiProviderModelsViewLess": "덜 보기",
"aiProviderModelsRemove": "모델 제거",
"aiProviderModelsEditHint": "모델 설정을 수정하려면 클릭하세요",
"aiProviderModelsSourceCatalog": "알려진 카탈로그 모델",
"aiProviderModelsSourceCustom": "사용자 지정 모델 키",
"aiProviderModelsSourcePattern": "와일드카드 패턴",
"aiProviderModelsSourceAll": "모든 모델 키와 일치",
"aiProviderModelsBudgetConfigured": "설정된 예산",
"aiProviderModelsEditTitle": "모델 편집",
"aiProviderModelsEditDescription": "모델 키를 업데이트하거나 해당 예산을 구성하십시오.",
"aiProviderModelsBudgetTab": "예산",
"aiProviderModelsKeyLabel": "모델 키",
"aiProviderModelsKeyRequired": "모델 키를 입력하십시오",
"aiProviderModelsKeyDuplicate": "이 모델 키는 이미 목록에 있습니다",
"aiProviderModelsOverlapError": "이 패턴은 두 목록에 모두 있을 수 없습니다: {keys}",
"aiProviderModelsUpdated": "모델이 업데이트되었습니다",
"aiProviderModelsErrorUpdate": "모델 업데이트에 실패했습니다",
"aiResourceProviders": "제공자",
"aiResourceProvidersDescription": "이 AI 게이트웨이가 사용할 수 있는 AI 제공자를 선택합니다",
"aiResourceProvidersHelp": "공급자를 연결하고 상속(각 공급자의 목록 사용) 또는 선택(이 리소스에 대한 허용 목록 선택)을 선택하십시오. 연결된 공급자 전반에 걸쳐 충돌하는 허용 패턴은 허용되지 않습니다.",
"aiResourceProvidersSelect": "공급자 선택",
"aiResourceProvidersEmpty": "AI 제공자를 찾을 수 없습니다",
"aiResourceProvidersNoneAttached": "아직 연결된 공급자가 없습니다.",
"aiResourceProvidersAdd": "제공자 추가",
"aiResourceProvidersRemove": "제공자 제거",
"aiResourceProviderToggleEnabled": "이 리소스에서 공급자를 활성화하거나 비활성화합니다",
"aiResourceProviderDisabled": "사용 중지됨",
"aiResourceProvidersUpdated": "공급자가 업데이트되었습니다",
"aiResourceProvidersErrorUpdate": "공급자 업데이트에 실패했습니다",
"aiResourceProviderEditDescription": "이 공급자의 모델이 리소스에서 어떻게 노출되는지 선택하십시오.",
"viewProviderSettings": "제공자 설정 보기",
"aiResourceProviderMode": "액세스 모드",
"aiResourceProviderModeInherit": "상속",
"aiResourceProviderModeSelect": "선택",
"aiResourceProviderModeSelectSummary": "선택 · {count} 모델",
"aiResourceProviderModeInheritHelp": "공급자에서 구성한 대로 이 공급자의 허용 및 차단 목록을 사용하십시오.",
"aiResourceProviderModeSelectHelp": "이 리소스를 위한 공급자의 허용 목록 모델의 하위 집합을 선택하십시오.",
"aiResourceProviderAllowModels": "허용 목록",
"aiResourceProviderAllowModelsSelect": "모델 선택",
"aiResourceProviderAllowModelsSearch": "모델 검색...",
"aiResourceProviderAllowModelsEmpty": "모델을 찾을 수 없습니다",
"aiResourceProviderAllowModelsHelp": "제공자의 허용 목록에 있는 모델만 선택할 수 있습니다.",
"aiResourceAliasRequired": "AI 게이트웨이에 별칭이 필요합니다",
"aiResourceDomainConfiguration": "도메인 구성",
"aiResourceDomainConfigurationDescription": "고객이 이 AI 게이트웨이를 사용하기 위해 사용할 도메인을 선택하십시오.",
"aiUsageAnalyticsTitle": "AI 사용 분석",
"aiUsageAnalyticsDescription": "제공자, 리소스, 역할 및 사용자 간의 AI 게이트웨이 비용, 토큰 사용량 및 활동을 분석합니다",
"aiUsageTabOverview": "개요",
"aiUsageTabProviders": "공급자",
"aiUsageTabResources": "리소스",
"aiUsageFilterProvider": "제공자",
"aiUsageFilterModel": "모델",
"aiUsageFilterResource": "리소스",
"aiUsageFilterRole": "역할",
"aiUsageFilterUser": "사용자",
"aiUsageFilterAllProviders": "모든 공급자",
"aiUsageFilterAllModels": "모든 모델",
"aiUsageFilterAllResources": "모든 리소스",
"aiUsageFilterAllRoles": "모든 역할",
"aiUsageFilterAllUsers": "모든 사용자",
"aiUsageFilterSearch": "검색...",
"aiUsageFilterNotFound": "옵션을 찾을 수 없습니다",
"aiUsageResetFilters": "필터 재설정",
"aiUsageRefresh": "새로 고침",
"aiUsageTokenTypePrompt": "프롬프트",
"aiUsageTokenTypeCacheRead": "캐시 읽기",
"aiUsageTokenTypeCacheWrite": "캐시 쓰기",
"aiUsageTokenTypeCompletion": "완료",
"aiUsageTokenTypeReasoning": "추론",
"aiUsageRequests": "요청",
"aiUsageCost": "비용",
"aiUsageTokens": "토큰",
"aiUsageOther": "기타",
"aiUsageTotalRequests": "총 요청 수",
"aiUsageTotalTokens": "총 토큰수",
"aiUsageTotalCost": "총 비용",
"aiUsageEstimated": "추정치",
"aiUsageRequestVolume": "요청량",
"aiUsageTokenUsage": "토큰 사용",
"aiUsageModelCost": "모델 비용",
"aiUsageModelTokens": "모델 토큰",
"aiUsageTopModels": "상위 모델",
"aiUsageTopProviders": "상위 제공자",
"aiUsageProviderCost": "제공자 비용",
"aiUsageProviderTokenUsage": "제공자 토큰 사용량",
"aiUsageTopResources": "최고의 리소스",
"aiUsageResourceCost": "리소스 비용",
"aiUsageResourceTokenUsage": "리소스 토큰 사용량",
"aiUsageResourceTypePublic": "공용 리소스",
"aiUsageResourceTypeSite": "개인 리소스",
"aiUsageNoResource": "리소스 없음",
"aiUsageRolesTab": "역할",
"aiUsageUsersTab": "사용자들",
"aiUsageTopRoles": "최고의 역할",
"aiUsageRoleCost": "역할 비용",
"aiUsageRoleTokenUsage": "역할 토큰 사용량",
"aiUsageTopUsers": "최고의 사용자",
"aiUsageUserCost": "사용자 비용",
"aiUsageUserTokenUsage": "사용자 토큰 사용량",
"aiUsageUnknownUser": "알 수 없는 사용자",
"aiUsageVirtualApiKeysTab": "가상 API 키",
"aiUsageFilterVirtualApiKey": "가상 API 키",
"aiUsageFilterAllVirtualApiKeys": "모든 가상 API 키",
"aiUsageTopVirtualApiKeys": "최고의 가상 API 키",
"aiUsageVirtualApiKeyCost": "가상 API 키 비용",
"aiUsageVirtualApiKeyTokenUsage": "가상 API 키 사용량",
"aiUsageUnknownVirtualApiKey": "가상 API 키 없음",
"aiUsageUnnamedVirtualApiKey": "이름 없는 키",
"aiUsageLoading": "로딩 중...",
"aiUsageNoData": "데이터 없음",
"resourceBudgetSettings": "예산",
"resourceBudgetSettingsDescription": "이 AI 게이트웨이가 지출 또는 토큰 제한에 따라 사용을 제한하는 방법을 구성하십시오",
"sidebarApiKeys": "API 키",
"sidebarOrgs": "조직",
"sidebarProvisioning": "프로비저닝",
"sidebarSettings": "설정",
"sidebarAllUsers": "모든 사용자",
@@ -1689,6 +2190,8 @@
"alertingRuleSaved": "알림 규칙 저장됨",
"alertingRuleSavedCreatedDescription": "새 알림 규칙이 생성되었습니다. 이 페이지에서 계속 편집할 수 있습니다.",
"alertingRuleSavedUpdatedDescription": "이 알림 규칙에 대한 변경 사항이 저장되었습니다.",
"alertingTestAlertSent": "테스트 경고 전송됨",
"alertingTestAlertSentDescription": "이 규칙에 구성된 조치에 테스트 경고가 전송되었습니다.",
"alertingEditRule": "알림 규칙 편집",
"alertingCreateRule": "알림 규칙 생성",
"alertingRuleCredenzaDescription": "무엇을 감시할지, 언제 알릴지, 어떻게 알릴지를 선택하세요.",
@@ -1798,6 +2301,12 @@
"alertingRulesBannerDescription": "각 규칙은 무엇을 감시할지(사이트, 상태 확인, 리소스), 언제 발동할지(예: 오프라인 또는 비정상), 이메일, 웹훅 또는 통합을 통해 팀에 어떻게 알릴지를 연결합니다. 이 목록을 사용하여 규칙을 생성, 활성화 및 관리하세요.",
"alertingHealthChecksBannerTitle": "건강 및 리소스 모니터링",
"alertingHealthChecksBannerDescription": "상태 확인은 한 번 정의한 HTTP 또는 TCP 모니터링입니다. 그런 다음 이를 알림 규칙의 소스로 사용하여 타겟이 정상 또는 비정상이 되었을 때 알림을 받을 수 있습니다. 리소스의 상태 확인도 여기에 나타납니다.",
"alertingTestRule": "테스트 경고 규칙",
"alertingAddActionHeading": "새 작업 추가",
"alertingSelectActionType": "작업 선택",
"alertingNoActionsTitle": "구성된 작업이 없습니다",
"alertingNoActionsSaveDescription": "이 규칙이 작동할 때 누군가에게 알리기 위해 적어도 하나의 작업을 추가하십시오.",
"alertingNoActionsTestDescription": "이 규칙을 테스트하기 전에 적어도 하나의 작업을 추가하십시오.",
"standaloneHcTableTitle": "상태 확인",
"standaloneHcSearchPlaceholder": "상태 확인 검색…",
"standaloneHcAddButton": "상태 확인 생성",
@@ -1989,6 +2498,9 @@
"domainPickerSubdomain": "서브도메인: {subdomain}",
"domainPickerNamespace": "이름 공간: {namespace}",
"domainPickerShowMore": "더보기",
"domainPickerNoDomainsAvailableTitle": "사용 가능한 도메인이 없습니다",
"domainPickerNoDomainsAvailableDescription": "설정된 도메인이 아직 없습니다. 계속하려면 도메인을 생성하세요.",
"domainPickerNoDomainsAvailableAction": "도메인으로 이동",
"regionSelectorTitle": "지역 선택",
"domainPickerRemoteExitNodeWarning": "제공된 도메인은 원격 종료 노드에 연결된 사이트에서 지원되지 않습니다. 원격 노드에서 리소스를 사용하려면 사용자 지정 도메인을 사용하십시오.",
"regionSelectorInfo": "지역을 선택하면 위치에 따라 더 나은 성능이 제공됩니다. 서버와 같은 지역에 있을 필요는 없습니다.",
@@ -2113,6 +2625,7 @@
"createDomainType": "유형:",
"createDomainName": "이름:",
"createDomainValue": "값:",
"multiSelectFilterCount": "{count} 선택됨",
"createDomainCnameRecords": "CNAME 레코드",
"createDomainARecords": "A 레코드",
"createDomainRecordNumber": "레코드 {number}",
@@ -2184,6 +2697,8 @@
"subnetPlaceholder": "서브넷",
"addressDescription": "클라이언트의 내부 주소. 조직의 서브넷 내에 있어야 합니다.",
"selectSites": "사이트 선택",
"selectResources": "리소스 선택",
"multiResourcesSelectorResourcesCount": "{count, plural, other {# 자원}}",
"selectLabels": "레이블 선택",
"sitesDescription": "클라이언트는 선택한 사이트에 연결됩니다.",
"clientInstallOlm": "Olm 설치",
@@ -2225,6 +2740,7 @@
"healthScheme": "방법",
"healthSelectScheme": "방법 선택",
"healthCheckPortInvalid": "포트는 1에서 65535 사이여야 합니다",
"healthCheckHostnameInvalid": "호스트 이름에는 공백이 포함될 수 없습니다",
"healthCheckPath": "경로",
"healthHostname": "IP / 호스트",
"healthPort": "포트",
@@ -2236,10 +2752,11 @@
"timeIsInSeconds": "시간은 초 단위입니다",
"requireDeviceApproval": "장치 승인 요구",
"requireDeviceApprovalDescription": "이 역할을 가진 사용자는 장치가 연결되기 전에 관리자의 승인이 필요합니다.",
"sshSettings": "SSH 설정",
"sshSettings": "SSH",
"inferenceSettings": "AI 게이트웨이",
"sshAccess": "SSH 접속",
"rdpSettings": "RDP 설정",
"vncSettings": "VNC 설정",
"rdpSettings": "RDP",
"vncSettings": "VNC",
"sshServer": "SSH 서버",
"rdpServer": "RDP 서버",
"vncServer": "VNC 서버",
@@ -2248,7 +2765,7 @@
"vncServerDescription": "VNC 서버의 목적지 및 포트를 구성합니다",
"sshServerMode": "모드",
"sshServerModeStandard": "표준 SSH 서버",
"sshServerModePangolin": "Pangolin SSH",
"sshServerModePangolin": "판골린 SSH",
"sshServerModeStandardDescription": "네트워크를 통해 OpenSSH와 같은 SSH 서버로 명령을 전달합니다.",
"sshServerModeNative": "네이티브 SSH 서버",
"sshServerModeNativeDescription": "사이트 커넥터를 통해 호스트에서 직접 명령을 실행합니다. 네트워크 구성이 필요 없습니다.",
@@ -2352,7 +2869,7 @@
"resourcesTableProxyResources": "공유",
"resourcesTableClientResources": "비공개",
"resourcesTableNoProxyResourcesFound": "프록시 리소스를 찾을 수 없습니다.",
"resourcesTableNoInternalResourcesFound": "내부 리소스를 찾을 수 없습니다.",
"resourcesTableNoInternalResourcesFound": "개인 리소스를 찾을 수 없습니다.",
"resourcesTableDestination": "대상지",
"resourcesTableAlias": "별칭",
"resourcesTableAliasAddress": "별칭 주소",
@@ -2375,9 +2892,9 @@
"editInternalResourceDialogCancel": "취소",
"editInternalResourceDialogSaveResource": "리소스 저장",
"editInternalResourceDialogSuccess": "성공",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "내부 리소스가 성공적으로 업데이트되었습니다",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "개인 리소스가 성공적으로 업데이트되었습니다",
"editInternalResourceDialogError": "오류",
"editInternalResourceDialogFailedToUpdateInternalResource": "내부 리소스 업데이트 실패",
"editInternalResourceDialogFailedToUpdateInternalResource": "개인 리소스 업데이트 실패",
"editInternalResourceDialogNameRequired": "이름은 필수입니다.",
"editInternalResourceDialogNameMaxLength": "이름은 255자 이하이어야 합니다.",
"editInternalResourceDialogProxyPortMin": "프록시 포트는 최소 1이어야 합니다.",
@@ -2392,6 +2909,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeInference": "AI 게이트웨이",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "스킴",
"editInternalResourceDialogEnableSsl": "TLS 활성화",
@@ -2403,7 +2921,7 @@
"editInternalResourceDialogAlias": "별칭",
"editInternalResourceDialogAliasDescription": "이 리소스에 대한 선택적 내부 DNS 별칭입니다.",
"createInternalResourceDialogNoSitesAvailable": "사용 가능한 사이트가 없습니다.",
"createInternalResourceDialogNoSitesAvailableDescription": "내부 리소스를 생성하려면 서브넷이 구성된 최소 하나의 Newt 사이트가 필요합니다.",
"createInternalResourceDialogNoSitesAvailableDescription": "개인 리소스를 생성하려면 서브넷이 구성된 최소 하나의 Newt 사이트가 필요합니다.",
"createInternalResourceDialogClose": "닫기",
"createInternalResourceDialogCreateClientResource": "사이트 리소스 생성",
"createInternalResourceDialogCreateClientResourceDescription": "선택한 사이트에 연결된 클라이언트에 접근할 새 리소스를 생성합니다",
@@ -2412,8 +2930,8 @@
"privateResourceAllowIcmpPing": "ICMP 핑 허용",
"privateResourceNetworkAccess": "네트워크 접근",
"privateResourceNetworkAccessDescription": "이 리소스에 대한 TCP/UDP 포트 접근과 ICMP 핑이 허용되는지 여부를 제어합니다.",
"hostSettings": "호스트 설정",
"cidrSettings": "CIDR 설정",
"hostSettings": "호스트",
"cidrSettings": "CIDR",
"createInternalResourceDialogResourceProperties": "리소스 속성",
"createInternalResourceDialogName": "이름",
"createInternalResourceDialogSite": "사이트",
@@ -2432,9 +2950,9 @@
"createInternalResourceDialogCancel": "취소",
"createInternalResourceDialogCreateResource": "리소스 생성",
"createInternalResourceDialogSuccess": "성공",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "내부 리소스가 성공적으로 생성되었습니다.",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "개인 리소스가 성공적으로 생성되었습니다",
"createInternalResourceDialogError": "오류",
"createInternalResourceDialogFailedToCreateInternalResource": "내부 리소스 생성 실패",
"createInternalResourceDialogFailedToCreateInternalResource": "개인 리소스 생성 실패",
"createInternalResourceDialogNameRequired": "이름은 필수입니다.",
"createInternalResourceDialogNameMaxLength": "이름은 255자 이하이어야 합니다.",
"createInternalResourceDialogPleaseSelectSite": "사이트를 선택하세요",
@@ -2451,6 +2969,7 @@
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"createInternalResourceDialogModeInference": "AI 게이트웨이",
"scheme": "스킴",
"createInternalResourceDialogScheme": "스킴",
"createInternalResourceDialogEnableSsl": "TLS 활성화",
@@ -2505,7 +3024,7 @@
"remoteExitNodeNetworkingSubnetsPlaceholder": "CIDR 범위 추가 (예: 10.0.0.0/8)",
"remoteExitNodeNetworkingSubnetsLoadError": "서브넷 로드 실패",
"remoteExitNodeNetworkingLabelsTitle": "우선순위 레이블",
"remoteExitNodeNetworkingLabelsDescription": "이 레이블이 있는 사이트는 이 원격 출구 노드를 통해 연결니다.",
"remoteExitNodeNetworkingLabelsDescription": "이 레이블이 있는 사이트는 이 원격 종료 노드를 통해 연결하는 것을 선호합니다.",
"remoteExitNodeNetworkingLabelsButtonText": "레이블 선택...",
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "레이블 검색...",
"remoteExitNodeNetworkingLabelsLoadError": "레이블 로드 실패",
@@ -2852,7 +3371,7 @@
"manageMachineClientsDescription": "서버와 시스템이 리소스에 개인적으로 연결하는 데 사용하는 클라이언트를 생성하고 관리하십시오",
"machineClientsBannerTitle": "서버 및 자동 시스템",
"machineClientsBannerDescription": "머신 클라이언트는 특정 사용자와 연결되지 않은 서버 및 자동화된 시스템을 위한 것입니다. 이들은 ID와 비밀을 통해 인증하며, Pangolin CLI, Olm CLI, 또는 Olm 컨테이너로 실행될 수 있습니다.",
"machineClientsBannerPangolinCLI": "Pangolin CLI",
"machineClientsBannerPangolinCLI": "판골린 CLI",
"machineClientsBannerOlmCLI": "Olm CLI",
"machineClientsBannerOlmContainer": "Olm 컨테이너",
"clientsTableUserClients": "사용자",
@@ -2982,6 +3501,7 @@
"priority": "우선순위",
"priorityDescription": "우선 순위가 높은 경로가 먼저 평가됩니다. 우선 순위 = 100은 자동 정렬(시스템 결정)이 의미합니다. 수동 우선 순위를 적용하려면 다른 숫자를 사용하세요.",
"instanceName": "인스턴스 이름",
"clearInstanceName": "서버 연결 해제",
"pathMatchModalTitle": "경로 매칭 설정",
"pathMatchModalDescription": "경로별로 들어오는 요청을 어떻게 매칭할지 설정합니다.",
"pathMatchType": "일치 유형",
@@ -3042,6 +3562,7 @@
"validPassword": "유효한 비밀번호",
"validEmail": "유효한 이메일",
"validSSO": "유효한 SSO",
"validVirtualAPIKey": "유효한 가상 API 키",
"view": "보기",
"configManaged": "구성 관리됨",
"connectedClient": "연결된 클라이언트",
@@ -3062,7 +3583,42 @@
"sidebarLogsAction": "작업 로그",
"logRetention": "로그 보관",
"logRetentionDescription": "다양한 유형의 로그를 이 조직에 대해 얼마나 오래 보관할지 관리하거나 비활성화합니다",
"logRetentionDisabledWarningTitle": "로그 보존 비활성화",
"logRetentionDisabledWarningDescription": "{logType}이/가 이 조직에 대해 보존되지 않으므로 새로운 활동이 여기에 나타나지 않습니다. 보안을 설정해서 보존을 활성화하여 이러한 로그를 수집하기 시작하세요.",
"logRetentionDisabledWarningButton": "보안 설정으로 이동",
"requestLogsDescription": "이 조직의 자원에 대한 상세한 요청 로그를 봅니다",
"aiSessionLogs": "AI 게이트웨이 세션 로그",
"aiSessionLogsDescription": "이 조직의 AI 게이트웨이 요청에 대한 프롬프트 및 응답 대본을 봅니다",
"sidebarLogsAi": "세션 로그",
"commandLogsAi": "세션 로그",
"sidebarLogsAiUsage": "사용 분석",
"commandLogsAiUsage": "사용 분석",
"provider": "제공자",
"capability": "능력",
"model": "모델",
"virtualApiKey": "가상 API 키",
"noVirtualApiKey": "가상 API 키가 없습니다",
"stream": "스트림",
"streaming": "스트리밍",
"nonStreaming": "비스트리밍",
"statusCode": "상태 코드",
"aiSessionId": "세션 ID",
"aiSessionRequest": "요청",
"aiSessionResponse": "응답",
"aiSessionNoData": "캡처된 데이터 없음",
"aiSessionCouldNotParse": "(원시, 대본을 구문 분석할 수 없음)",
"aiSessionLogTruncated": "이 세션은 저장소 전송되기 전에 잘린 상태일 수 있습니다.",
"aiSessionViewRaw": "원시 JSON 보기",
"aiSessionViewChat": "채팅 보기",
"cost": "비용",
"estimated": "추정된",
"tokenUsage": "토큰 사용량",
"promptTokens": "프롬프트 토큰",
"cacheReadTokens": "캐시 조회 토큰",
"cacheWriteTokens": "캐시 쓰기 토큰",
"completionTokens": "완료 토큰",
"reasoningTokens": "추론 토큰",
"totalTokens": "총 토큰수",
"requestAnalyticsDescription": "이 조직의 리소스에 대한 자세한 요청 분석 보기",
"logRetentionRequestLabel": "HTTP 요청 로그 보관",
"logRetentionRequestDescription": "요청 로그를 얼마나 오래 보관할지",
@@ -3072,6 +3628,8 @@
"logRetentionActionDescription": "작업 로그를 얼마나 오래 보관할지",
"logRetentionConnectionLabel": "연결 로그 보유 기간",
"logRetentionConnectionDescription": "연결 로그를 얼마나 오래 보유할지",
"logRetentionAISessionsLabel": "AI 게이트웨이 세션 로그 보관",
"logRetentionAISessionsDescription": "AI 게이트웨이 프롬프트/응답 세션 로그를 보관할 기간",
"logRetentionDisabled": "비활성화됨",
"logRetention3Days": "3 일",
"logRetention7Days": "7 일",
@@ -3089,8 +3647,8 @@
"sourceAddress": "소스 주소",
"destinationAddress": "대상 주소",
"duration": "지속 시간",
"licenseRequiredToUse": "이 기능을 사용하려면 <enterpriseLicenseLink>엔터프라이즈 에디션</enterpriseLicenseLink> 라이선스가 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다. <bookADemoLink>데모 또는 POC 체험을 예약하세요</bookADemoLink>.",
"ossEnterpriseEditionRequired": "이 기능을 사용하려면 <enterpriseEditionLink>엔터프라이즈 에디션</enterpriseEditionLink>이(가) 필요합니다. 이 기능은 <pangolinCloudLink>판골린 클라우드</pangolinCloudLink>에서도 사용할 수 있습니다. <bookADemoLink>데모 또는 POC 체험을 예약하세요</bookADemoLink>.",
"licenseRequiredToUse": "<enterpriseEditionLink>Enterprise Edition 라이센스</enterpriseEditionLink>가 필요합니다. <bookADemoLink>데모 또는 평가판 예약</bookADemoLink>",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition 라이센스</enterpriseEditionLink> 필요합니다. <bookADemoLink>데모 또는 평가판 예약</bookADemoLink>",
"certResolver": "인증서 해결사",
"certResolverDescription": "이 리소스에 사용할 인증서 해결사를 선택하세요.",
"selectCertResolver": "인증서 해결사 선택",
@@ -3233,6 +3791,7 @@
"noData": "데이터 없음",
"machineClients": "기계 클라이언트",
"install": "설치",
"downloadInstaller": "설치 프로그램 다운로드",
"run": "실행",
"envFile": "환경 파일",
"serviceFile": "서비스 파일",
@@ -3288,9 +3847,9 @@
"internalResourceFormMultiSiteRoutingHelp": "다중 사이트를 선택하면 높은 가용성을 위해 회복력 있는 라우팅 및 페일오버가 가능해집니다.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "자세히 알아보기",
"editInternalResourceDialogPortRestrictionsDescription": "특정 TCP/UDP 포트에 대한 접근을 제한하거나 모든 포트를 허용/차단하십시오.",
"createInternalResourceDialogHttpConfiguration": "HTTP 구성",
"createInternalResourceDialogHttpConfiguration": "도메인 구성",
"createInternalResourceDialogHttpConfigurationDescription": "이 리소스에 HTTP 또는 HTTPS로 도달하기 위한 도메인을 선택하세요.",
"editInternalResourceDialogHttpConfiguration": "HTTP 구성",
"editInternalResourceDialogHttpConfiguration": "도메인 구성",
"editInternalResourceDialogHttpConfigurationDescription": "이 리소스에 HTTP 또는 HTTPS로 도달하기 위한 도메인을 선택하세요.",
"editInternalResourceDialogTcp": "TCP",
"editInternalResourceDialogUdp": "UDP",
@@ -3440,6 +3999,7 @@
"disconnected": "연결 해제됨",
"approvalsEmptyStateTitle": "장치 승인 비활성화됨",
"approvalsEmptyStateDescription": "사용자가 새 장치를 연결하기 전에 관리자의 승인을 필요로 하도록 역할에 대해 장치 승인을 활성화하세요.",
"approvalsEmptyStateHowToTitle": "활성화 방법",
"approvalsEmptyStateStep1Title": "역할로 이동",
"approvalsEmptyStateStep1Description": "조직의 역할 설정으로 이동하여 장치 승인을 구성하십시오.",
"approvalsEmptyStateStep2Title": "장치 승인 활성화",
@@ -3561,6 +4121,8 @@
"httpDestConnectionLogsDescription": "사이트 및 터널 연결 이벤트, 연결 및 연결 끊기를 포함합니다.",
"httpDestRequestLogsTitle": "HTTP 요청 로그",
"httpDestRequestLogsDescription": "프록시된 리소스에 대한 HTTP 요청 로그, 메서드, 경로 및 응답 코드를 포함합니다.",
"httpDestAISessionLogsTitle": "AI 세션 로그",
"httpDestAISessionLogsDescription": "AI 게이트웨이 요청 및 응답 세션, 프롬프트, 모델 응답 및 토큰 사용을 포함합니다.",
"httpDestSaveChanges": "변경 사항 저장",
"httpDestCreateDestination": "대상지 생성",
"httpDestUpdatedSuccess": "대상지가 성공적으로 업데이트되었습니다",
@@ -3717,6 +4279,11 @@
"resourceLauncherViewAsAdmin": "관리자로 보기",
"resourceLauncherResourceDetailsDescription": "이 리소스에 대한 연결 정보 및 상태입니다.",
"resourceLauncherResourceDetails": "리소스 세부 정보",
"resourceLauncherSitesDescription": "리소스는 다음 사이트를 통해 액세스할 수 있습니다.",
"resourceLauncherViewSiteAsAdmin": "관리자로 사이트 보기",
"resourceLauncherFilterBySite": "사이트별 필터",
"resourceLauncherSshCommand": "SSH 명령",
"resourceLauncherSshCommandDescription": "Pangolin CLI를 사용하여 이 리소스에 대한 SSH 세션을 엽니다.",
"resourceLauncherAuthMethodsDescription": "이 리소스에 활성화된 인증 방법입니다.",
"resourceLauncherPrivateClientRequired": "이 리소스에 개인적으로 접근하려면 기기에서 클라이언트로 연결하십시오.",
"resourceLauncherPrivateClientRequiredTitle": "클라이언트 연결 필요",
@@ -3726,7 +4293,18 @@
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherUnlabeled": "레이블 없음",
"resourceLauncherAiGateway": "AI 게이트웨이",
"resourceLauncherNoSite": "사이트 없음",
"resourceLauncherAvailableModels": "사용 가능한 모델",
"resourceLauncherAvailableModelsDescription": "이 AI 게이트웨이에서 사용할 수 있는 모델입니다.",
"resourceLauncherAvailableModelsEmpty": "이 리소스에 사용할 수 있는 모델이 없습니다.",
"resourceLauncherAvailableModelsError": "사용 가능한 모델을 로드할 수 없습니다.",
"resourceLauncherApiKeys": "API 키",
"resourceLauncherApiKeysDescription": "신원 키나 속성 키를 사용하여 이 리소스에 인증하십시오.",
"resourceLauncherApiKeysIdentity": "신원 키",
"resourceLauncherApiKeysManual": "속성 키",
"resourceLauncherApiKeysEmpty": "이 리소스에 사용할 수 있는 API 키가 없습니다.",
"resourceLauncherApiKeysError": "API 키를 로드할 수 없습니다.",
"resourceLauncherNoResourcesInGroup": "이 그룹에는 리소스가 없습니다",
"resourceLauncherEmptyStateTitle": "사용 가능한 리소스 없음",
"resourceLauncherEmptyStateDescription": "아직 리소스에 대한 액세스 권한이 없습니다. 액세스를 요청하려면 관리자에게 문의하세요.",
@@ -3753,9 +4331,9 @@
"resourceLauncherViewDeleteFailedDescription": "런처 뷰를 삭제할 수 없습니다. 다시 시도하세요.",
"memberPortalPrevious": "이전",
"memberPortalNext": "다음",
"httpSettings": "HTTP 설정",
"tcpSettings": "TCP 설정",
"udpSettings": "UDP 설정",
"httpSettings": "HTTP",
"tcpSettings": "TCP",
"udpSettings": "UDP",
"sshTitle": "SSH",
"sshConnectingDescription": "보안 연결 설정 중…",
"sshConnecting": "연결 중…",
@@ -3816,5 +4394,6 @@
"rdpUnicodeKeyboardMode": "유니코드 키보드 모드",
"sessionToolbarShow": "툴바 보기",
"sessionToolbarHide": "툴바 숨기기",
"actionUpdateSiteApprovals": "사이트 승인 업데이트"
"actionUpdateSiteApprovals": "사이트 승인 업데이트",
"check": "확인"
}
+615 -36
View File
@@ -43,6 +43,8 @@
"inviteLoginUser": "Vennligst sjekk at du er logget inn som riktig bruker.",
"inviteErrorNoUser": "Vi beklager, men det ser ut som invitasjonen du prøver å få tilgang til ikke er for en bruker som eksisterer.",
"inviteCreateUser": "Vennligst opprett en konto først.",
"inviteErrorOidcNotAllowed": "Invitasjoner kan kun aksepteres av interne kontoer. Logg ut og logg inn med passordet ditt for denne e-posten.",
"inviteLoginInternalOnly": "Invitasjoner krever en intern konto med et passord. Opprett en konto eller logg inn med passordet ditt.",
"goHome": "Gå hjem",
"inviteLogInOtherUser": "Logg inn som en annen bruker",
"createAnAccount": "Lag konto",
@@ -151,6 +153,7 @@
"siteResourcesTargetsOnSite": "Mål på dette nettstedet",
"siteSetting": "{siteName} Innstillinger",
"siteNewtTunnel": "Nyhetsnettsted (anbefalt)",
"pangolinSite": "Pangolin Site",
"siteNewtTunnelDescription": "Lekkeste måte å lage et inngangspunkt til ethvert nettverk. Ingen ekstra oppsett på.",
"siteWg": "Grunnleggende WireGuard",
"siteWgDescription": "Bruk hvilken som helst WireGuard-klient for å etablere en tunnel. Manuell NAT-oppsett kreves.",
@@ -178,11 +181,11 @@
"shareDeleteConfirm": "Bekreft sletting av delbar lenke",
"shareQuestionRemove": "Er du sikker på at du vil slette denne delingslenken?",
"shareMessageRemove": "Når slettet, vil lenken ikke lenger fungere, og alle som bruker den vil miste tilgang til ressursen.",
"shareTokenDescription": "Adgangstoken kan sendes på to måter: som en spørringsparameter eller i forespørselsoverskriftene. Disse må sendes fra klienten på hver forespørsel om autentisert tilgang.",
"shareTokenDescription": "Tilgangstokenn kan sendes som en spørringsparameter eller i forespørselshoder. Som standard må det sendes med hver forespørsel. Hvis sesjonsvedholdenhet er aktivert, byttes den første forespørselen mot en sesjons-cookie.",
"accessToken": "Tilgangsnøkkel",
"usageExamples": "Brukseksempler",
"tokenId": "Token-ID",
"requestHeades": "Request Headers",
"requestHeades": "Forespørselshoder",
"queryParameter": "Forespørsel Params",
"importantNote": "Viktig merknad",
"shareImportantDescription": "Av sikkerhetsgrunner anbefales det å bruke headere fremfor query parametere der det er mulig, da query parametere kan logges i serverlogger eller nettleserhistorikk.",
@@ -196,8 +199,14 @@
"shareTitleOptional": "Tittel (valgfritt)",
"sharePathOptional": "Bane (valgfritt)",
"sharePathDescription": "Lenken vil videresende brukere til denne stien etter autentisering.",
"shareAssociateUserOptional": "Tilknytt bruker (valgfritt)",
"shareAssociateUserDescription": "Når den er satt, blir forespørsler som bruker denne koblingen tilskrevet brukeren i tilgangslogger og identitetshoder. Koblingen fjernes hvis brukeren forlater organisasjonen.",
"userSelect": "Velg bruker",
"usersNotFound": "Ingen brukere funnet",
"expireIn": "Utløper om",
"neverExpire": "Utløper aldri",
"sharePersistSession": "Behold sesjonen etter første bruk",
"sharePersistSessionDescription": "Når den er aktivert, setter den første forespørselen med dette tokenet via en spørringsparameter eller et hode en sesjons-cookie slik at senere forespørsler ikke trenger tokenet. La det være av for API-klienter som skal sende tokenet ved hver forespørsel.",
"shareExpireDescription": "Utløpstid er hvor lenge lenken vil være brukbar og gi tilgang til ressursen. Etter denne tiden vil lenken ikke lenger fungere, og brukere som brukte denne lenken vil miste tilgangen til ressursen.",
"shareSeeOnce": "Du vil bare kunne se denne linken én gang. Pass på å kopiere den.",
"shareAccessHint": "Alle med denne lenken kan få tilgang til ressursen. Del forsiktig.",
@@ -280,7 +289,21 @@
"noCountryFound": "Ingen land funnet.",
"siteSelectionDescription": "Dette området vil gi tilkobling til mål.",
"resourceType": "Ressurstype",
"resourceTypeDescription": "Dette kontrollerer ressursprotokollen og hvordan den vil vises i nettleseren. Dette kan ikke endres senere.",
"resourceTypeDescription": "Dette styrer ressursprotokollen og kan ikke endres senere.",
"resourceTypeSearch": "Søk i ressurstyper...",
"resourceTypeNotFound": "Ingen ressurstype funnet",
"resourceTypeHttpDescription": "Proxy nettverkstrafikk over HTTPS ved bruk av et domenenavn",
"resourceTypeInferenceDescription": "Ruter AI-forespørsler gjennom tilkoblede leverandører",
"resourceTypeSshDescription": "Tilgang til en SSH-server fra nettleseren",
"resourceTypeRdpDescription": "Tilgang til en fjernskrivebord fra nettleseren",
"resourceTypeVncDescription": "Tilgang til et VNC-skrivebord fra nettleseren",
"resourceTypeTcpDescription": "Proxy rå TCP-trafikk ved bruk av et portnummer",
"resourceTypeUdpDescription": "Proxy rå UDP-trafikk ved bruk av et portnummer",
"privateResourceTypeDescription": "Dette styrer hvordan klienter kommer til ressursen. Dette kan ikke endres senere.",
"privateResourceTypeHostDescription": "Eksponer en enkel vert på nettverksområdet for tilkoblede klienter",
"privateResourceTypeCidrDescription": "Eksponer et CIDR-område på nettverksområdet for tilkoblede klienter",
"privateResourceTypeHttpDescription": "Tilgang til en HTTP- eller HTTPS-tjeneste gjennom et domene",
"privateResourceTypeSshDescription": "Tilgang til en SSH-server fra tilkoblede klienter",
"resourceDomainDescription": "Ressursen vil bli servert på dette fullstendig kvalifiserte domenenavnet.",
"resourceHTTPSSettings": "HTTPS-innstillinger",
"resourceHTTPSSettingsDescription": "Konfigurer hvordan ressursen skal nås over HTTPS",
@@ -443,6 +466,8 @@
"apiKeysDelete": "Slett API-nøkkel",
"apiKeysManage": "Administrer API-nøkler",
"apiKeysDescription": "API-nøkler brukes for å autentisere med integrasjons-API",
"orgsManage": "Administrer organisasjoner",
"orgsDescription": "Vis og administrer alle organisasjoner på denne instansen",
"provisioningKeysTitle": "Foreløpig nøkkel",
"provisioningKeysManage": "Behandle bestemmende nøkler",
"provisioningKeysDescription": "Bestemmelsesnøkler brukes til å godkjenne automatisert nettstedsløsning for din organisasjon.",
@@ -694,6 +719,7 @@
"nameOptional": "Navn (valgfritt)",
"accessControls": "Tilgangskontroller",
"userDescription2": "Administrer innstillingene for denne brukeren",
"userGeneralSettingsDescription": "Administrer denne brukerens roller og innstillinger i organisasjonen",
"accessRoleErrorAdd": "Kunne ikke legge til bruker i rolle",
"accessRoleErrorAddDescription": "Det oppstod en feil under tilordning av brukeren til rollen.",
"userSaved": "Bruker lagret",
@@ -714,7 +740,7 @@
"proxyErrorTls": "Ugyldig TLS-servernavn. Bruk domenenavnformat, eller la stå tomt for å fjerne TLS-servernavnet.",
"proxyEnableSSL": "Aktiver TLS",
"proxyEnableSSLDescription": "Aktivere SSL/TLS-kryptering for sikker HTTPS tilkobling til målene.",
"target": "Target",
"target": "Mål",
"configureTarget": "Konfigurer mål",
"targetErrorFetch": "Kunne ikke hente mål",
"targetErrorFetchDescription": "Det oppsto en feil under henting av mål",
@@ -849,12 +875,16 @@
"authMethodsSave": "Lagre innstillinger",
"policyAuthStackTitle": "Autentisering",
"policyAuthStackDescription": "Kontroller hvilke autentiseringsmetoder som kreves for å få tilgang til denne ressursen",
"policyAuthInferenceStackDescription": "Velg hvilke brukere og roller som kan autentisere seg til denne AI-porten",
"policyAuthOrLogicTitle": "Flere autentiseringsmetoder aktive",
"policyAuthOrLogicBanner": "Besøkende kan autentisere ved bruk av en hvilken som helst av de aktive metodene nedenfor. De trenger ikke å fullføre alle.",
"policyAuthMethodActive": "Aktiv",
"policyAuthMethodOff": "Av",
"policyAuthSsoTitle": "Plattform SSO",
"policyAuthSsoDescription": "Krev pålogging gjennom din organisasjons identitetsleverandør",
"policyAuthInferenceIdentityKeySignInUrl": "Logg inn URL",
"policyAuthInferenceIdentityKeyHelpNoUrl": "Hver bruker har allerede en identitet API-nøkkel, så du trenger bare å opprette virtuelle API-nøkler for ikke-brukerklienter eller delt tilgang. Brukere kan hente sin nøkkel ved å logge inn med sin identitetsleverandør på denne ressursens URL, hvor den vil vises etter innlogging.",
"policyAuthInferenceIdentityKeyHelp": "Hver bruker har allerede en identitet API-nøkkel, så du trenger bare å opprette virtuelle API-nøkler for ikke-brukerklienter eller delt tilgang. Brukere henter sin nøkkel her etter innlogging med sin identitetsleverandør.",
"policyAuthSsoSummary": "{idp} · {users} brukere, {roles} roller",
"policyAuthSsoDefaultIdp": "Standardleverandør",
"policyAuthAddDefaultIdentityProvider": "Legg til standard identitetsleverandør",
@@ -886,7 +916,7 @@
"policyAccessRulesFallthroughOff": "Når regler er deaktivert, går all trafikk gjennom til autentisering.",
"policyAccessRulesFallthroughOn": "Når ingen regler samsvarer, fortsetter trafikken til autentisering.",
"rulesPlaceholderCidr": "10.0.0.0/8",
"rulesPlaceholderPath": "/admin/*",
"rulesPlaceholderPath": "/administrator/*",
"rulesPlaceholderGeo": "RU, KP",
"rulesSave": "Lagre Regler",
"resourceErrorCreate": "Feil under oppretting av ressurs",
@@ -922,13 +952,13 @@
"unknownCommand": "Ukjent kommando",
"newtErrorFetchReleases": "Feilet å hente utgivelsesinfo: {err}",
"newtErrorFetchLatest": "Feil ved henting av siste utgivelse: {err}",
"newtEndpoint": "Endpoint",
"newtEndpoint": "Endepunkt",
"newtId": "ID",
"newtSecretKey": "Sikkerhetsnøkkel",
"newtVersion": "Versjon",
"architecture": "Arkitektur",
"sites": "Områder",
"siteWgAnyClients": "Bruk hvilken som helst WireGuard klient til å koble til. Du må adressere interne ressurser ved hjelp av peer IP.",
"siteWgAnyClients": "Bruk hvilken som helst WireGuard-klient for å koble til. Du må adressere private ressurser ved å bruke peer-IP.",
"siteWgCompatibleAllClients": "Kompatibel med alle WireGuard-klienter",
"siteWgManualConfigurationRequired": "Manuell konfigurasjon påkrevd",
"userErrorNotAdminOrOwner": "Bruker er ikke administrator eller eier",
@@ -1073,6 +1103,8 @@
"accessRoleErrorNewRequired": "Ny rolle kreves",
"accessRoleErrorRemove": "Kunne ikke fjerne rolle",
"accessRoleErrorRemoveDescription": "Det oppstod en feil under fjerning av rollen.",
"accessRoleInferenceBudget": "AI Budsjett",
"accessRoleInferenceBudgetDescription": "Konfigurer hvordan medlemmene av denne rollen begrenser AI-bruk basert på forbruk eller tokenbegrensninger",
"accessRoleName": "Rollenavn",
"accessRoleQuestionRemove": "Du er ferd med å slette rollen `{name}. Du kan ikke angre denne handlingen.",
"accessRoleRemove": "Fjern Rolle",
@@ -1148,6 +1180,10 @@
"idpJmespathAboutDescriptionLink": "Lær mer om JMESPath",
"idpJmespathLabel": "Identifikatorsti",
"idpJmespathLabelDescription": "Stien til brukeridentifikatoren i ID-tokenet",
"idpIdentifierChangeTitle": "Advarsel om identifikatorbanendring",
"idpIdentifierChangeDescription": "Du er i ferd med å endre identifikatorbanen. Dette vil påvirke hvordan eksisterende brukere kartlegges. Brukere som tidligere logget inn gjennom denne identitetsleverandøren kan ikke lenger bli gjenkjent som de samme brukerne.",
"idpIdentifierChangeConfirmMessage": "Jeg bekrefter",
"idpIdentifierChangeWarningText": "Dette vil påvirke hvordan eksisterende brukere kartlegges",
"idpJmespathEmailPathOptional": "E-poststi (Valgfritt)",
"idpJmespathEmailPathOptionalDescription": "Stien til brukerens e-postadresse i ID-tokenet",
"idpJmespathNamePathOptional": "Navn Sti (Valgfritt)",
@@ -1279,7 +1315,7 @@
"generatePasswordResetCode": "Lag tilbakestillingskode for passord",
"passwordResetCodeGenerated": "Passord tilbakestillingskoden er generert",
"passwordResetCodeGeneratedDescription": "Del denne koden med brukeren. De kan bruke den til å tilbakestille passordet.",
"passwordResetUrl": "Reset URL",
"passwordResetUrl": "Tilbakestill URL",
"passwordNew": "Nytt passord",
"passwordNewConfirm": "Bekreft nytt passord",
"changePassword": "Endre passord",
@@ -1392,6 +1428,24 @@
"logoutError": "Feil ved utlogging",
"signingAs": "Logget inn som",
"serverAdmin": "Serveradministrator",
"promoteServerAdmin": "Forfrem til Server Admin",
"promoteServerAdminTitle": "Forfrem til Server Admin",
"promoteServerAdminQuestion": "Er du sikker på at du vil forfremme {selectedUser} til server admin?",
"promoteServerAdminMessage": "Server administratorer har de høyeste rettighetene og kan administrere serveren.",
"promoteServerAdminWarning": "Dette kan angres når som helst ved å degradere brukeren.",
"promoteServerAdminConfirm": "Forfrem til Server Admin",
"promoteServerAdminSuccess": "Bruker forfremmet",
"promoteServerAdminSuccessDescription": "{selectedUser} er nå server admin.",
"promoteServerAdminError": "Kunne ikke forfremme bruker",
"demoteServerAdmin": "Degradér fra server admin",
"demoteServerAdminTitle": "Degradér fra Server Admin",
"demoteServerAdminQuestion": "Er du sikker på at du vil degradere {selectedUser} fra server admin?",
"demoteServerAdminMessage": "{selectedUser} vil miste alle rettigheter som server admin.",
"demoteServerAdminWarning": "Dette kan angres når som helst ved å forfremme brukeren.",
"demoteServerAdminConfirm": "Degradér fra server admin",
"demoteServerAdminSuccess": "Bruker degradert",
"demoteServerAdminSuccessDescription": "{selectedUser} er ikke lenger server admin.",
"demoteServerAdminError": "Kunne ikke degradere bruker",
"managedSelfhosted": "Administrert selv-hostet",
"otpEnable": "Aktiver tofaktor",
"otpDisable": "Deaktiver tofaktor",
@@ -1418,6 +1472,28 @@
"actionDeleteSite": "Slett område",
"actionGetSite": "Hent område",
"actionListSites": "List opp områder",
"actionCreateAiProvider": "Opprett AI-leverandør",
"actionDeleteAiProvider": "Slett AI-leverandør",
"actionGetAiProvider": "Hent AI-leverandør",
"actionListAiProviders": "List opp AI-leverandører",
"actionUpdateAiProvider": "Oppdater AI-leverandør",
"actionCreateAiModel": "Opprett AI-modell",
"actionDeleteAiModel": "Slett AI-modell",
"actionGetAiModel": "Hent AI-modell",
"actionListAiModels": "List opp AI-modeller",
"actionUpdateAiModel": "Oppdater AI-modell",
"actionCreateAiBudget": "Opprett AI-budsjett",
"actionDeleteAiBudget": "Slett AI-budsjett",
"actionGetAiBudget": "Hent AI-budsjett",
"actionListAiBudgets": "List opp AI-budsjetter",
"actionUpdateAiBudget": "Oppdater AI-budsjett",
"actionListResourceAiModels": "List opp ressurs AI-modeller",
"actionSetResourceAiModels": "Angi ressurs AI-modeller",
"actionCreateVirtualApiKey": "Opprett virtuell API-nøkkel",
"actionDeleteVirtualApiKey": "Slett virtuell API-nøkkel",
"actionGetVirtualApiKey": "Hent virtuell API-nøkkel",
"actionListVirtualApiKeys": "List opp virtuelle API-nøkler",
"actionUpdateVirtualApiKey": "Oppdater virtuell API-nøkkel",
"actionApplyBlueprint": "Bruk blåkopi",
"actionListBlueprints": "List opp blåkopier",
"actionGetBlueprint": "Hent blåkopi",
@@ -1443,8 +1519,11 @@
"actionSetResourcePincode": "Angi ressurspinkode",
"actionSetResourceEmailWhitelist": "Angi e-post-hviteliste for ressurs",
"actionGetResourceEmailWhitelist": "Hent e-post-hviteliste for ressurs",
"actionListResourcePolicies": "List opp ressursregler",
"actionCreateResourcePolicy": "Opprett ressursregel",
"actionGetResourcePolicy": "Hent ressursregel",
"actionUpdateResourcePolicy": "Oppdater ressursregel",
"actionDeleteResourcePolicy": "Slett ressursregel",
"actionSetResourcePolicyUsers": "Sett ressursregel for brukere",
"actionSetResourcePolicyRoles": "Sett ressursregel for roller",
"actionSetResourcePolicyPassword": "Sett ressursregel for passord",
@@ -1520,6 +1599,8 @@
"search": "Søk…",
"searchPlaceholder": "Søk...",
"emptySearchOptions": "Ingen valg funnet",
"ipFilterSearchPlaceholder": "Angi en IP-adresse…",
"ipFilterEmptyMessage": "Angi en IP-adresse å filtrere etter",
"create": "Opprett",
"orgs": "Organisasjoner",
"loginError": "En uventet feil oppstod. Vennligst prøv igjen.",
@@ -1554,6 +1635,8 @@
"commandPaletteCreateMachineClient": "Opprett maskinklient",
"commandPaletteCreateAlertRule": "Opprett varslingsregel",
"commandPaletteCreateIdentityProvider": "Opprett identitetsleverandør",
"commandPaletteCreateAiProvider": "Opprett AI-leverandør",
"commandPaletteCreateVirtualApiKey": "Opprett virtuell API-nøkkel",
"commandPaletteToggleTheme": "Bytt tema",
"commandPaletteChooseOrganization": "Velg organisasjon",
"commandPaletteShortcutMac": "⌘K",
@@ -1616,7 +1699,425 @@
"sidebarInvitations": "Invitasjoner",
"sidebarRoles": "Roller",
"sidebarShareableLinks": "Delbare lenker",
"sidebarAiGateway": "AI Portal",
"sidebarAiProviders": "Leverandører",
"commandAiProviders": "AI-leverandører",
"sidebarVirtualApiKeys": "Virtuelle API-nøkler",
"sidebarMyApiKeys": "Dine API-nøkler",
"sidebarAccount": "Starter",
"commandVirtualApiKeys": "Virtuelle API-nøkler",
"virtualApiKeysTitle": "Administrer virtuelle API-nøkler",
"virtualApiKeysDescription": "Opprett og administrer manuelle API-nøkler for AI Gateway-tilgang til offentlige AI-portene",
"virtualApiKeysTabIdentity": "Identitetsnøkler",
"virtualApiKeysTabVirtual": "Virtuelle nøkler",
"virtualApiKeysIdentitySplashTitle": "Identitetsnøkler for hver bruker",
"virtualApiKeysIdentitySplashDescription": "Hver bruker har allerede en Pangolin identitetsnøkkel for denne organisasjonen. Den er unik for brukerkontoen deres og autentiserer dem til AI-portene de kan få tilgang til.",
"virtualApiKeysIdentitySplashExample": "Eksempel",
"virtualApiKeysIdentitySplashRetrieveTitle": "Hvordan brukere henter sin nøkkel",
"virtualApiKeysIdentitySplashRetrieveResource": "Besøk en offentlig AI Gateway-URL i nettleseren og logg inn med kontoen din.",
"virtualApiKeysIdentitySplashRetrievePage": "Eller gå til <url></url> mens du er logget inn.",
"virtualApiKeysIdentitySplashManual": "Du kan opprette flere virtuelle API-nøkler i kategorien Virtuelle nøkler. Disse nøklene kan avgrenses til spesifikke offentlige AI-portene og eventuelt assosieres med en bruker. Opprettelse av en nøkkel gir umiddelbar tilgang til de valgte offentlige AI-portene.",
"virtualApiKeysIdentitySplashGoToVirtual": "Manuelt opprett en nøkkel",
"virtualApiKeysEmailIdentity": "E-postidentitetsnøkler",
"virtualApiKeysEmailIdentityDescription": "Send hver valgt bruker eller rolle deres Pangolin identitetsnøkkel.",
"virtualApiKeysEmailIdentitySendAll": "Send til alle brukere",
"virtualApiKeysEmailIdentitySendAllDescription": "E-post til alle medlemmer av organisasjonen som har en kontoe-post.",
"virtualApiKeysEmailIdentitySelectUsers": "Brukere",
"virtualApiKeysEmailIdentitySelectRoles": "Roller",
"virtualApiKeysEmailIdentitySubmit": "Send e-poster",
"virtualApiKeysEmailIdentitySuccess": "Identitetsnøkler sendt på e-post",
"virtualApiKeysEmailIdentitySuccessDescription": "Sendt {sent} e-poster.",
"virtualApiKeysEmailIdentitySkipped": "{skipped} brukere ble hoppet over fordi de ikke har en e-postadresse.",
"virtualApiKeysEmailIdentityRecipientsRequired": "Velg minst én bruker eller rolle, eller send til alle brukere.",
"virtualApiKeysEmailIdentityError": "Feil ved sending av identitetsnøkler",
"virtualApiKeysEmailIdentityErrorDescription": "Kunne ikke sende identitetsnøkler",
"virtualApiKeysBannerTitle": "Identitetsnøkler for hver bruker",
"virtualApiKeysBannerDescription": "Hver bruker har allerede en identitetsnøkkel tilgjengelig på {keysUrl}. Du kan også manuelt opprette nøkler her som gir direkte tilgang til offentlige AI-portene.",
"virtualApiKeysBannerButtonText": "Vis identitetsnøkler",
"virtualApiKeys": "Virtuelle API-nøkler",
"virtualApiKeysSearch": "Søk nøklene...",
"virtualApiKeysCreate": "Opprett virtuell API-nøkkel",
"virtualApiKeysCreateDescription": "Lag en manuell nøkkel som kan kalle offentlige AI-gatewayer i denne organisasjonen",
"virtualApiKeysCreateButton": "Opprett nøkkel",
"virtualApiKeysEmpty": "Ingen virtuelle API-nøkler ennå",
"virtualApiKeysName": "Navn",
"virtualApiKeysDescriptionOptional": "Beskrivelse (valgfritt)",
"virtualApiKeysAssociateUserOptional": "Assosier bruker (valgfritt)",
"virtualApiKeysAssociateUserDescription": "Assosier denne nøkkelen med en bruker for å spore bruken. Når den er opprettet, gir denne nøkkelen umiddelbar tilgang til de valgte offentlige AI-portene. Du trenger ikke å manuelt assosiere en bruker til ressursen. Nøkkelen vil også vises i brukerens profil.",
"virtualApiKeysAllResources": "Alle offentlige AI-portene",
"virtualApiKeysAllResourcesDescription": "Tillat denne nøkkelen å få tilgang til hver offentlig AI-port i organisasjonen",
"virtualApiKeysSelectResources": "Offentlige AI-portene",
"virtualApiKeysSelectResourcesPlaceholder": "Velg ressurser",
"virtualApiKeysSelectResourcesDescription": "Velg hvilke offentlige AI-portene denne nøkkelen kan få tilgang til",
"virtualApiKeysNoResources": "Ingen ressurser",
"virtualApiKeysSecret": "Nøkkel",
"virtualApiKeysCopyKey": "Kopier denne nøkkelen. Du kan se den igjen senere fra tabellen eller når du redigerer.",
"virtualApiKeysViewSecret": "Vis hemmelighet",
"virtualApiKeysViewSecretTitle": "Virtuell API-nøkkel hemmelighet",
"virtualApiKeysViewSecretDescription": "Denne hemmeligheten gir tilgang til de offentlige AI-portene som er tildelt denne nøkkelen",
"virtualApiKeysEdit": "Rediger virtuell API-nøkkel",
"virtualApiKeysEditDescription": "Oppdater den tilknyttede brukeren og tilgang til offentlige AI-port for denne nøkkelen",
"virtualApiKeysSaveButton": "Lagre endringer",
"virtualApiKeysSelectResourcesRequired": "Velg minst én offentlig AI-port, eller aktiver alle offentlige AI-portene",
"virtualApiKeysUpdated": "Virtuell API-nøkkel oppdatert",
"virtualApiKeysUpdatedDescription": "Den virtuelle API-nøkkelen er oppdatert",
"virtualApiKeysErrorUpdate": "Feil ved oppdatering av virtuell API-nøkkel",
"virtualApiKeysErrorUpdateDescription": "Kunne ikke oppdatere virtuell API-nøkkel",
"virtualApiKeysErrorCreate": "Feil ved opprettelse av virtuell API-nøkkel",
"virtualApiKeysErrorCreateDescription": "Kunne ikke opprette virtuell API-nøkkel",
"virtualApiKeysErrorDelete": "Feil ved sletting av virtuell API-nøkkel",
"virtualApiKeysErrorDeleteMessage": "Kunne ikke slette virtuell API-nøkkel",
"virtualApiKeysDeleted": "Virtuell API-nøkkel slettet",
"virtualApiKeysDeletedDescription": "Den virtuelle API-nøkkelen er slettet",
"virtualApiKeysDelete": "Slett virtuell API-nøkkel",
"virtualApiKeysDeleteConfirm": "Slett nøkkel",
"virtualApiKeysQuestionRemove": "Er du sikker på at du vil slette denne virtuelle API-nøkkelen?",
"virtualApiKeysMessageRemove": "Klienter som bruker denne nøkkelen vil miste tilgang umiddelbart.",
"virtualApiKeysErrorFetchSecret": "Feil ved lasting av hemmelighet",
"virtualApiKeysErrorFetchSecretDescription": "Kunne ikke laste den virtuelle API-nøkkelen hemmelighet",
"virtualApiKeysFilterUnassigned": "Uten hentydning",
"virtualApiKeysInferenceBudget": "Budsjett",
"virtualApiKeysInferenceBudgetDescription": "Konfigurer hvordan denne nøkkelen begrenser AI-bruk basert på forbruk eller token begrensninger",
"virtualApiKeysEmailOnGenerate": "E-postnøkkel ved generering",
"virtualApiKeysEmailThisKey": "E-post denne nøkkelen",
"virtualApiKeysEmailOnGenerateDescription": "Send nøkkelen til den tilknyttede brukeren og ytterligere adresser etter at den er opprettet",
"virtualApiKeysEmailThisKeyDescription": "Send den nåværende nøkkelen til den tilknyttede brukeren og ytterligere adresser",
"virtualApiKeysEmailSmtpRequired": "E-post er ikke konfigurert på denne serveren",
"virtualApiKeysEmailSmtpRequiredDescription": "Konfigurér SMTP for å sende virtuelle API-nøkler.",
"virtualApiKeysEmailSendToUser": "Send til tilknyttet bruker",
"virtualApiKeysEmailSendToUserDescription": "Send nøkkelen til den tilknyttede brukerens kontoe-post",
"virtualApiKeysEmailSendToUserDisabled": "Assosier en bruker for å sende nøkkelen til den brukeren",
"virtualApiKeysEmailAdditional": "Ekstra e-poster",
"virtualApiKeysEmailAdditionalPlaceholder": "Legg til e-post og trykk Enter",
"virtualApiKeysEmailRecipientsRequired": "Velg den tilknyttede brukeren eller legg til minst én e-postadresse",
"myVirtualApiKeysTitle": "Dine API-nøkler",
"myVirtualApiKeysDescription": "Vis din identitetsnøkkel og eventuelle virtuelle API-nøkler tilskrives deg i denne organisasjonen",
"myVirtualApiKeysResourceTitle": "Dine API-nøkler for {resourceName}",
"myVirtualApiKeysResourceDescription": "Vis din identitetsnøkkel og virtuelle API-nøkler tilskrives deg som kan få tilgang til {resourceName}",
"myVirtualApiKeysIdentityTitle": "Identitetsnøkkel",
"myVirtualApiKeysIdentityHeadline": "Din personlige API-nøkkel",
"myVirtualApiKeysIdentityDescription": "Din personlige nøkkel for denne organisasjonen. Den er unik til kontoen din og brukes til å identifisere deg når du kaller AI Gateway-ressurser.",
"myVirtualApiKeysIdentityResourceHeadline": "Din personlige API-nøkkel for {resourceName}",
"myVirtualApiKeysIdentityResourceDescription": "Din personlige nøkkel for denne organisasjonen. Bruk den til å kalle {resourceName}.",
"myVirtualApiKeysManualTitle": "Tilskrevne nøkler",
"myVirtualApiKeysManualDescription": "Manuelle virtuelle API-nøkler en admin assosiert med kontoen din",
"myVirtualApiKeysManualResourceDescription": "Manuelle virtuelle API-nøkler assosiert med kontoen din som kan få tilgang til {resourceName}",
"myVirtualApiKeysManualEmpty": "Ingen tilskrevne nøkler ennå",
"myVirtualApiKeysKindUser": "Identitet",
"myVirtualApiKeysKindManual": "Manuell",
"myVirtualApiKeysUnnamed": "Uten navngitt nøkkel",
"myVirtualApiKeysRevealSecret": "Avslør hemmelighet",
"myVirtualApiKeysViewSecretDescription": "Denne hemmeligheten autentiserer deg til AI Gateway-ressurser",
"aiClientConfigTitle": "Konfigurer kodeagenter",
"aiClientConfigDescription": "Kopier konfigurasjon for populære kodeagenter, eller la Pangolin CLI sette det opp for deg automatisk.",
"aiClientConfigDescriptionClaude": "Anthropics agentiske kodingsverktøy for terminalen.",
"aiClientConfigDescriptionCodex": "OpenAIs agentiske kodingsverktøy for terminalen.",
"aiClientConfigDescriptionOpencode": "Åpen kildekode terminal kodeagent.",
"aiClientConfigDescriptionGemini": "Googles agentiske koding verktøy for terminalen.",
"aiClientConfigSetup": "Oppsett",
"aiClientConfigTabCli": "Automatisk (CLI)",
"aiClientConfigTabManual": "Manuell konfigurasjon",
"aiClientConfigPreset": "Konfigurasjon",
"aiClientConfigStep": "Steg {number}",
"aiClientConfigEndpointPlaceholder": "https:\\/\\/example.resource.url.com",
"aiClientConfigPlaceholderWarning": "Denne koden inkluderer eksempelverdier, som et modellnavn eller en ressurs-URL. Erstatt de med dine egne før du bruker det.",
"aiClientConfigRevealError": "Kunne ikke laste inn din API-nøkkel.",
"aiClientConfigRevealRetry": "Prøv igjen",
"resourceGeneralAiClientConfigDescription": "Denne ressursen kan brukes fra vanlige klienter som Claude Code og OpenCode. <configLink>Lær mer<\\/configLink>",
"aiProvidersTitle": "AI-leverandører",
"aiProvidersDescription": "Koble modellleverandører for AI-arbeidslaster i denne organisasjonen",
"aiProvidersBannerTitle": "Koble modellleverandører",
"aiProvidersBannerDescription": "Leverandører er modell-backendene Pangolin bruker for AI-arbeidslaster. Koble OpenAI, Anthropic, og andre leverandører her, og fest dem deretter til offentlige AI-portene slik at brukere kan kalle modeller med identitetsbevisst tilgang, budsjetter, og logging.",
"aiProvidersAdd": "Legg til leverandør",
"aiProvidersSearch": "Søk leverandører...",
"aiProvidersEmpty": "Ingen AI-leverandører ennå",
"aiProviderCreate": "Opprett AI-leverandør",
"aiProviderCreateDescription": "Legg til en modellleverandør for denne organisasjonen",
"aiProviderSeeAll": "Se alle leverandører",
"aiProviderSetting": "Leverandørinnstillinger for {providerName}",
"aiProviderSettingDescription": "Konfigurer denne AI-leverandøren",
"aiProviderGeneral": "Generelt",
"aiProviderGeneralDescription": "Grunnleggende innstillinger for denne leverandøren",
"aiProviderConfiguration": "Konfigurasjon",
"aiProviderConfigurationDescription": "Nettverksrutting og autentisering for denne leverandøren",
"aiProviderNetworkSettings": "Nettverksinnstillinger",
"aiProviderNetworkSettingsDescription": "Velg hvordan trafikken når denne leverandøren",
"aiProviderAuthSettings": "Autentisering",
"aiProviderAuthSettingsDescription": "Konfigurer hvordan denne leverandøren autentiserer forespørsler til sin oppstrøms-URL",
"aiProviderBudgetSettings": "Budsjett",
"aiProviderBudgetSettingsDescription": "Konfigurer hvordan denne leverandøren begrenser bruk basert på forbruk eller tokenbegrensninger",
"aiBudgetAdd": "Legg til budsjett",
"aiBudgetEmpty": "Ingen budsjetter konfigurert ennå. Klikk Legg til budsjett for å angi en forbruks- eller tokenbegrensning.",
"aiBudgetUnit": "Brukstype",
"aiBudgetPeriod": "Tilbakestill periode",
"aiBudgetAmount": "Maksimal forbruk",
"aiBudgetAmountPlaceholder": "Maksimal forbruk",
"aiBudgetPeriodHourly": "Timebasis",
"aiBudgetPeriodDaily": "Daglig",
"aiBudgetPeriodWeekly": "Ukentlig",
"aiBudgetPeriodMonthly": "Månedlig",
"aiBudgetPeriodYearly": "Årlig",
"aiBudgetPeriodLifetime": "Livstid",
"aiBudgetUnitUsd": "USD",
"aiBudgetUnitTokens": "Token",
"aiBudgetConflictError": "Et budsjett for denne tilbakestillingsperioden og forbrukstype eksisterer allerede",
"aiBudgetInvalidAmountError": "Angi et maksimalt forbruk større enn 0",
"aiBudgetUpdated": "Budsjetter oppdatert",
"aiBudgetErrorSave": "Kunne ikke oppdatere budsjettene",
"aiProviderType": "Leverandørtype",
"aiProviderTypeSearch": "Søk leverandører...",
"aiProviderTypeNotFound": "Ingen leverandør type funnet",
"aiProviderTypeOpenai": "OpenAI",
"aiProviderTypeAnthropic": "Anthropic",
"aiProviderTypeGoogleGemini": "Google Gemini",
"aiProviderTypeVertexAi": "Vertex AI",
"aiProviderTypeBedrock": "Amazon Bedrock",
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
"aiProviderTypeOpenRouter": "OpenRouter",
"aiProviderTypeVercelAiGateway": "Offentlige AI-portene",
"aiProviderTypeCustom": "Tilpasset",
"aiProviderTypeOpenaiDescription": "OpenAI API",
"aiProviderTypeAnthropicDescription": "Anthropic API",
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Gateway API",
"aiProviderTypeCustomDescription": "Bruk din egen endepunkt eller rute via områdetargets",
"aiProviderUpstreamUrl": "Oppstrøms URL",
"aiProviderUpstreamUrlDescription": "Basis URL for leverandørens API",
"aiProviderUpstreamUrlOptionalDescription": "La stå tom for å bruke standard oppstrøms URL for denne leverandøren",
"aiProviderEffectiveUpstreamUrl": "Effektiv oppstrøms URL",
"aiProviderApiKey": "API-nøkkel",
"aiProviderApiKeyDescription": "API-nøkkel brukt for å autentisere forespørsler til denne leverandøren",
"aiProviderCustomHeadersDescription": "Overskrifter sendt ved hver forespørsel til denne leverandøren. Ny linje separert: Overskriftsnavn: verdi",
"aiProviderApiKeyLastChars": "API-nøkkel",
"aiProviderAuthType": "Autentiseringstype",
"aiProviderAuthTypeSearch": "Søk autentiseringstyper...",
"aiProviderAuthTypeNotFound": "Ingen autentiseringstype funnet",
"aiProviderAuthTypeBearer": "Bærer",
"aiProviderAuthTypeBearerDescription": "Autorisasjon: Bærer nøkkel. Brukt av OpenAI og de fleste leverandører",
"aiProviderAuthTypeXApiKey": "x-api-key",
"aiProviderAuthTypeXApiKeyDescription": "x-api-key overskrift. Brukt av Anthropic",
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key overskrift. Brukt av Google Gemini",
"aiProviderAuthTypeHec": "Splunk HEC",
"aiProviderAuthTypeHecDescription": "Autorisasjon: Splunk nøkkel. Brukt av Splunk HTTP Event Collector",
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Gateway",
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-authorization: Bærer nøkkel. Brukt av Cloudflare AI Gateway",
"aiProviderAuthTypeNone": "Ingen autentisering",
"aiProviderAuthTypePassthrough": "Gjennomkjøring",
"aiProviderAuthTypeDescription": "Hvordan det oppstrøms API-et autentiserer forespørsler",
"aiProviderAuthTypePassthroughDescription": "Videreformidle anroperens API-nøkler til oppstrøms",
"aiProviderAuthTypeNoneDescription": "Ikke send autentiseringsoverskrifter til oppstrøms",
"aiProviderRoutingMode": "Ruteringsmodus",
"aiProviderRoutingModeDescription": "Send trafikk til en oppstrøms URL eller til HTTP-mål på dine nettsteder",
"aiProviderRoutingModeUrl": "Oppstrøms URL",
"aiProviderRoutingModeUrlDescription": "Ring en offentlig eller privat API-basis URL",
"aiProviderRoutingModeTarget": "Områdemål",
"aiProviderRoutingModeTargetDescription": "Rute gjennom mål på dine nettsteder",
"aiProviderRoutingModeTargetNote": "Etter å ha opprettet denne leverandøren, konfigurer områdemål på fanen Nettverksinnstillinger.",
"aiProviderTargetNoOne": "Denne leverandøren har ingen mål. Legg til et mål for å rute forespørsler gjennom dine nettsteder.",
"aiProviderRemoteNodeTargetsWarning": "Nettsteder tilkoblet eksterne noder er utilgjengelige for ruting til på AI Gateway leverandører.",
"aiProviderSkipTlsVerification": "Hopp over TLS-verifisering",
"aiProviderSkipTlsVerificationDescription": "Deaktiver TLS-sertifikatverifisering for oppstrøms tilkobling",
"aiProviderBudget": "Budsjett",
"aiProviderBudgetDescription": "Valgfritt utgifts- eller tokenbudsjett for denne leverandøren",
"aiProviderBudgetAmount": "Budsjettbeløp",
"aiProviderBudgetUnit": "Budsjettenhet",
"aiProviderBudgetUnitUsd": "USD",
"aiProviderBudgetUnitTokens": "Token",
"aiProviderEnabled": "Aktivert",
"aiProviderEnabledDescription": "Deaktiver denne leverandøren fullstendig på tvers av alle ressurser",
"aiProviderErrorCreate": "Kunne ikke opprette AI-leverandør",
"aiProviderErrorUpdate": "Mislyktes å oppdatere AI-leverandør",
"aiProviderErrorDelete": "Kunne ikke slette AI-leverandør",
"aiProviderErrorLoad": "Kunne ikke laste AI-leverandør",
"aiProviderErrorUpstreamUrlInvalid": "Skriv inn en gyldig oppstrøms URL",
"aiProviderErrorUpstreamUrlRequired": "Oppstrøms URL er nødvendig for denne leverandøren",
"aiProviderErrorAuthTypeRequired": "Autentiseringstype er påkrevd",
"aiProviderErrorApiKeyRequired": "API-nøkkel er påkrevd",
"aiProviderErrorRoutingModeTarget": "Områdetargets rute er kun tilgjengelig for kundespesifikke leverandører",
"aiProviderErrorCapabilitiesRequired": "Velg minst en API-kapasitet",
"aiProviderCapabilities": "API Kapasiteter",
"aiProviderCapabilitiesDescription": "Velg hvilke API-formater denne leverandøren kan håndtere. Kjente leverandører starter med anbefalte standarder.",
"aiProviderCapabilitiesCustomDescription": "Velg hvilke API-formater denne tilpassede leverandøren kan håndtere",
"aiProviderCapabilitiesSelect": "Velg kapasiteter",
"aiProviderCapabilitiesEmpty": "Ingen kapasiteter funnet",
"aiProviderCapabilitiesSearch": "Søk kapasiteter...",
"aiCapabilityOpenaiChat": "OpenAI Chat Fullføringer",
"aiCapabilityOpenaiChatDescription": "Støtter /v1/chat/fullføringer",
"aiCapabilityOpenaiResponses": "OpenAI Responser",
"aiCapabilityOpenaiResponsesDescription": "Støtter /v1/responser",
"aiCapabilityAnthropicMessages": "Anthropic Meldinger",
"aiCapabilityAnthropicMessagesDescription": "Støtter /v1/meldinger",
"aiCapabilityV1Models": "Modelliste",
"aiCapabilityV1ModelsDescription": "Støtter /v1/modeller modelloppdagelse",
"aiCapabilityGeminiGenerateContent": "Gemini Generer Innhold",
"aiCapabilityGeminiGenerateContentDescription": "Støtter direkte Gemini API",
"aiCapabilityBedrockModelInvoke": "Bedrock Modell Påkalling",
"aiCapabilityBedrockModelInvokeDescription": "Støtter Amazon Bedrock InvokeModel",
"aiCapabilityGoogleGenerateContent": "Vertex Generer Innhold",
"aiCapabilityGoogleGenerateContentDescription": "Støtter Vertex AI Gemini format",
"aiCapabilityGoogleRawPredict": "Vertex Rå Prediksjon",
"aiCapabilityGoogleRawPredictDescription": "Støtter Vertex AI råPredikt for Anthropic-modeller",
"aiCapabilityBedrockConverse": "Bedrock Samtale",
"aiCapabilityBedrockConverseDescription": "Støtter Amazon Bedrock Converse API",
"aiProviderCreated": "AI-leverandør opprettet",
"aiProviderUpdated": "AI-leverandør oppdatert",
"aiProviderDeleted": "AI-leverandør slettet",
"aiProviderDelete": "Slett Leverandør",
"aiProviderDeleteConfirm": "Slett Leverandør",
"aiProviderQuestionRemove": "Er du sikker på at du vil slette denne AI-leverandøren?",
"aiProviderMessageRemove": "Dette vil permanent slette leverandøren og dens modeller og mål. Dette kan ikke angres.",
"aiProviderErrorNoUpdate": "AI-leverandør er ikke tilgjengelig for oppdatering",
"aiProviderModels": "Modeller",
"aiProviderModelsDescription": "Definer tillat- og blokk-lister for denne leverandøren. Forespørslene må matche en tillat oppføring og må ikke samsvare med en blokk oppføring.",
"aiProviderCreateModelsDescription": "Velg hvilke modeller denne leverandøren kan betjene. En tom tillatsliste avviser alle trafikk. Du kan legge til eller endre modeller senere.",
"aiProviderModelsPlaceholder": "Søk modeller eller skriv inn en tilpasset nøkkel",
"aiProviderModelsAllow": "Tillat Liste",
"aiProviderModelsAllowDescription": "Modeller som kan brukes gjennom denne leverandøren. Tom betyr å nekte alt.",
"aiProviderModelsAllowPlaceholder": "Skriv inn modellenøkkel",
"aiProviderModelsAllowEmpty": "Ingen modeller lagt til enda.",
"aiProviderModelsBlock": "Blokker Liste",
"aiProviderModelsBlockDescription": "Modeller for å nekte, selv om de samsvarer med en tillat oppføring.",
"aiProviderModelsBlockPlaceholder": "Skriv inn modellenøkkel",
"aiProviderModelsBlockEmpty": "Ingen modeller lagt til enda.",
"aiProviderModelsAdd": "Legg til Modeller",
"aiProviderModelsClearAll": "Fjern alle",
"aiProviderModelsAddCustom": "Legg til \"{key}\"",
"aiProviderModelsAddCustomHint": "Trykk Enter for å legge til denne tilpassede modellenøkkelen.",
"aiProviderModelsAddBulk": "Legg til {count} tilpassede nøkler",
"aiProviderModelsAddBulkHint": "Trykk Enter for å legge til {count} tilpassede nøkler.",
"aiProviderModelsAddOne": "Legg til {key} nå",
"aiProviderModelsAddSelected": "Legg til Valgt",
"aiProviderModelsSelectedCount": "{count} valgt",
"aiProviderModelsSelectAll": "Velg alle",
"aiProviderModelsClearSelected": "Fjern",
"aiProviderModelsBulkHint": "Velg kjente modeller eller skriv inn en tilpasset nøkkel.",
"aiProviderModelsCatalogEmpty": "Ingen matchende katalogmodeller.",
"aiProviderModelsCatalogHeading": "Kjente Modeller",
"aiProviderModelsAllLabel": "Alle modeller",
"aiProviderModelsAllPatternHint": "Jokertegn: *",
"aiProviderModelsAddAllAllow": "Tillat alle modeller",
"aiProviderModelsAddAllBlock": "Blokker alle modeller",
"aiProviderModelsAddAllDescription": "Bruker jokertegnet * slik at hver modellenøkkel matcher.",
"aiProviderModelsViewMore": "Vis mer ({count})",
"aiProviderModelsViewLess": "Vis mindre",
"aiProviderModelsRemove": "Fjern modell",
"aiProviderModelsEditHint": "Klikk for å redigere modellinnstillinger",
"aiProviderModelsSourceCatalog": "Kjent katalogmodell",
"aiProviderModelsSourceCustom": "Tilpasset modellenøkkel",
"aiProviderModelsSourcePattern": "Jokertegnsmønster",
"aiProviderModelsSourceAll": "Matcher hver modellenøkkel",
"aiProviderModelsBudgetConfigured": "Budsjett konfigurert",
"aiProviderModelsEditTitle": "Rediger Modell",
"aiProviderModelsEditDescription": "Oppdater modellenøkkelen eller konfigurer budsjettet.",
"aiProviderModelsBudgetTab": "Budsjett",
"aiProviderModelsKeyLabel": "Modell Nøkkel",
"aiProviderModelsKeyRequired": "Skriv inn en modellenøkkel",
"aiProviderModelsKeyDuplicate": "Denne modellenøkkelen er allerede på en liste",
"aiProviderModelsOverlapError": "Disse mønstrene kan ikke være på begge listene: {keys}",
"aiProviderModelsUpdated": "Modeller oppdatert",
"aiProviderModelsErrorUpdate": "Kunne ikke oppdatere modeller",
"aiResourceProviders": "Leverandører",
"aiResourceProvidersDescription": "Velg hvilke AI-leverandører denne AI-portalen kan bruke",
"aiResourceProvidersHelp": "Fest leverandører og velg arv (bruk hver leverandørlister) eller velg (velg en tillatliste for denne ressursen). Tillatmønstre som er i konflikt på tvers av vedlagte leverandører er ikke tillatt.",
"aiResourceProvidersSelect": "Velg leverandører",
"aiResourceProvidersEmpty": "Ingen AI-leverandører funnet",
"aiResourceProvidersNoneAttached": "Ingen leverandører knyttet ennå.",
"aiResourceProvidersAdd": "Legg til leverandør",
"aiResourceProvidersRemove": "Fjern leverandør",
"aiResourceProviderToggleEnabled": "Aktiver eller deaktiver denne leverandøren på ressursen",
"aiResourceProviderDisabled": "Deaktivert",
"aiResourceProvidersUpdated": "Leverandører oppdatert",
"aiResourceProvidersErrorUpdate": "Kunne ikke oppdatere leverandører",
"aiResourceProviderEditDescription": "Velg hvordan denne leverandørens modeller er eksponert på denne ressursen.",
"viewProviderSettings": "Vis Leverandørinnstillinger",
"aiResourceProviderMode": "Tilgangsmodus",
"aiResourceProviderModeInherit": "Arve",
"aiResourceProviderModeSelect": "Velg",
"aiResourceProviderModeSelectSummary": "Velg · {count} modeller",
"aiResourceProviderModeInheritHelp": "Bruk denne leverandørens tillat- og blokk-lister som konfigurert på leverandøren.",
"aiResourceProviderModeSelectHelp": "Velg en delmengde av denne leverandørens tillat-listemodeller for denne ressursen.",
"aiResourceProviderAllowModels": "Tillat liste",
"aiResourceProviderAllowModelsSelect": "Velg modeller",
"aiResourceProviderAllowModelsSearch": "Søk modeller...",
"aiResourceProviderAllowModelsEmpty": "Ingen modeller funnet",
"aiResourceProviderAllowModelsHelp": "Bare modeller fra denne leverandørens tillat liste kan velges.",
"aiResourceAliasRequired": "Alias er påkrevd for AI-portaler",
"aiResourceDomainConfiguration": "Domene Konfigurasjon",
"aiResourceDomainConfigurationDescription": "Velg domenet kunder vil bruke for å nå denne AI-portalen.",
"aiUsageAnalyticsTitle": "AI Bruksanalyse",
"aiUsageAnalyticsDescription": "Analyser AI-gatewaykostnad, tokenbruk og aktivitet på tvers av leverandører, ressurser, roller og brukere",
"aiUsageTabOverview": "Oversikt",
"aiUsageTabProviders": "Leverandører",
"aiUsageTabResources": "Ressurser",
"aiUsageFilterProvider": "Leverandør",
"aiUsageFilterModel": "Modell",
"aiUsageFilterResource": "Ressurs",
"aiUsageFilterRole": "Rolle",
"aiUsageFilterUser": "Bruker",
"aiUsageFilterAllProviders": "Alle Leverandører",
"aiUsageFilterAllModels": "Alle Modeller",
"aiUsageFilterAllResources": "Alle Ressurser",
"aiUsageFilterAllRoles": "Alle Roller",
"aiUsageFilterAllUsers": "Alle Brukere",
"aiUsageFilterSearch": "Søk...",
"aiUsageFilterNotFound": "Ingen alternativer funnet",
"aiUsageResetFilters": "Tilbakestill Filtre",
"aiUsageRefresh": "Oppdater",
"aiUsageTokenTypePrompt": "Stikkord",
"aiUsageTokenTypeCacheRead": "Cache-lesing",
"aiUsageTokenTypeCacheWrite": "Cache-skriving",
"aiUsageTokenTypeCompletion": "Fullføring",
"aiUsageTokenTypeReasoning": "Resonnering",
"aiUsageRequests": "Forespørsler",
"aiUsageCost": "Kostnad",
"aiUsageTokens": "Token",
"aiUsageOther": "Annet",
"aiUsageTotalRequests": "Totale Forespørsler",
"aiUsageTotalTokens": "Totale Token",
"aiUsageTotalCost": "Totale Kostnad",
"aiUsageEstimated": "Anslått",
"aiUsageRequestVolume": "Forespørselsvolum",
"aiUsageTokenUsage": "Tokenforbruk",
"aiUsageModelCost": "Modellkostnad",
"aiUsageModelTokens": "Modelltoken",
"aiUsageTopModels": "Toppmodeller",
"aiUsageTopProviders": "Topp Leverandører",
"aiUsageProviderCost": "Leverandørkostnad",
"aiUsageProviderTokenUsage": "Leverandør Tokenforbruk",
"aiUsageTopResources": "Top Ressurser",
"aiUsageResourceCost": "Ressurskostnad",
"aiUsageResourceTokenUsage": "Ressurs Tokenforbruk",
"aiUsageResourceTypePublic": "Offentlig Ressurs",
"aiUsageResourceTypeSite": "Privat Ressurs",
"aiUsageNoResource": "Ingen ressurs",
"aiUsageRolesTab": "Roller",
"aiUsageUsersTab": "Brukere",
"aiUsageTopRoles": "Topproller",
"aiUsageRoleCost": "Rollekostnad",
"aiUsageRoleTokenUsage": "Rolle Tokenforbruk",
"aiUsageTopUsers": "Toppbrukere",
"aiUsageUserCost": "Brukerkostnad",
"aiUsageUserTokenUsage": "Bruker Tokenforbruk",
"aiUsageUnknownUser": "Ukjent bruker",
"aiUsageVirtualApiKeysTab": "Virtuelle API-nøkler",
"aiUsageFilterVirtualApiKey": "Virtuell API-nøkkel",
"aiUsageFilterAllVirtualApiKeys": "Alle Virtuelle API-nøkler",
"aiUsageTopVirtualApiKeys": "Topp Virtuelle API-nøkler",
"aiUsageVirtualApiKeyCost": "Virtuell API-nøkkelkostnad",
"aiUsageVirtualApiKeyTokenUsage": "Virtuell API-nøkkel Tokenforbruk",
"aiUsageUnknownVirtualApiKey": "Ingen virtuell API-nøkkel",
"aiUsageUnnamedVirtualApiKey": "Uten navn nøkkel",
"aiUsageLoading": "Laster inn...",
"aiUsageNoData": "Ingen data",
"resourceBudgetSettings": "Budsjett",
"resourceBudgetSettingsDescription": "Konfigurer hvordan denne AI-portalen begrenser bruk basert på utgifter eller token-grenser",
"sidebarApiKeys": "API-nøkler",
"sidebarOrgs": "Organisasjoner",
"sidebarProvisioning": "Levering",
"sidebarSettings": "Innstillinger",
"sidebarAllUsers": "Alle brukere",
@@ -1689,6 +2190,8 @@
"alertingRuleSaved": "Varslingsregel lagret",
"alertingRuleSavedCreatedDescription": "Din nye varslingsregel ble opprettet. Du kan fortsette å redigere den på denne siden.",
"alertingRuleSavedUpdatedDescription": "Endringene dine i denne varslingsregelen ble lagret.",
"alertingTestAlertSent": "Testvarsel sendt",
"alertingTestAlertSentDescription": "Et testvarsel ble sendt til handlingene konfigurert på denne regelen.",
"alertingEditRule": "Rediger varslingsregel",
"alertingCreateRule": "Opprett varslingsregel",
"alertingRuleCredenzaDescription": "Velg hva som skal overvåkes, når det skal varsles, og hvordan du vil bli informert",
@@ -1798,6 +2301,12 @@
"alertingRulesBannerDescription": "Hver regel binder sammen hva som skal overvåkes (et område, helsekontroll eller ressurs), når det skal varsles (for eksempel offline eller usunn), og hvordan varsle teamet ditt via e-post, webhooks eller integrasjoner. Bruk denne listen for å opprette, aktivere og administrere disse reglene.",
"alertingHealthChecksBannerTitle": "Overvåk helse & ressurser",
"alertingHealthChecksBannerDescription": "Helsekontroller er HTTP- eller TCP-monitorer du definerer én gang. Du kan deretter bruke dem som kilder i varslingsregler slik at du blir varslet når et mål blir sunt eller usunt. Helsekontroller på ressurser vises også her.",
"alertingTestRule": "Test varselsregel",
"alertingAddActionHeading": "Legg til ny handling",
"alertingSelectActionType": "Velg en handling",
"alertingNoActionsTitle": "Ingen handlinger konfigurert",
"alertingNoActionsSaveDescription": "Legg til minst én handling slik at denne regelen kan varsle noen når den utløses.",
"alertingNoActionsTestDescription": "Legg til minst én handling før du kan teste denne regelen.",
"standaloneHcTableTitle": "Helsekontroller",
"standaloneHcSearchPlaceholder": "Søk i helsekontroller…",
"standaloneHcAddButton": "Opprett helsekontroll",
@@ -1989,6 +2498,9 @@
"domainPickerSubdomain": "Underdomene: {subdomain}",
"domainPickerNamespace": "Navnerom: {namespace}",
"domainPickerShowMore": "Vis mer",
"domainPickerNoDomainsAvailableTitle": "Ingen domener tilgjengelig",
"domainPickerNoDomainsAvailableDescription": "Du har ikke satt opp noen domener ennå. Opprett et domene for å fortsette.",
"domainPickerNoDomainsAvailableAction": "Gå til domener",
"regionSelectorTitle": "Velg Region",
"domainPickerRemoteExitNodeWarning": "Tilbudte domener støttes ikke når sider kobles til eksterne avkjøringsnoder. For ressurser som skal være tilgjengelige på eksterne noder, brukes et egendefinert domene i stedet.",
"regionSelectorInfo": "Å velge en region hjelper oss med å gi bedre ytelse for din lokasjon. Du trenger ikke være i samme region som serveren.",
@@ -2113,6 +2625,7 @@
"createDomainType": "Type:",
"createDomainName": "Navn:",
"createDomainValue": "Verdi:",
"multiSelectFilterCount": "{count} valgt",
"createDomainCnameRecords": "CNAME-oppføringer",
"createDomainARecords": "A-oppføringer",
"createDomainRecordNumber": "Oppføring {number}",
@@ -2184,13 +2697,15 @@
"subnetPlaceholder": "Subnett",
"addressDescription": "Den interne adressen til klienten. Må falle innenfor organisasjonens undernett.",
"selectSites": "Velg områder",
"selectResources": "Velg ressurser",
"multiResourcesSelectorResourcesCount": "{count, plural, one {# ressurs} other {# ressurser}}",
"selectLabels": "Velg etiketter",
"sitesDescription": "Klienten vil ha tilkobling til de valgte områdene",
"clientInstallOlm": "Installer Olm",
"clientInstallOlmDescription": "Få Olm til å kjøre på systemet ditt",
"clientOlmCredentials": "Legitimasjon",
"clientOlmCredentialsDescription": "Dette er hvordan klienten vil godkjenne med serveren",
"olmEndpoint": "Endpoint",
"olmEndpoint": "Endepunkt",
"olmId": "ID",
"olmSecretKey": "Sikkerhetsnøkkel",
"clientCredentialsSave": "Lagre brukeropplysninger",
@@ -2225,6 +2740,7 @@
"healthScheme": "Metode",
"healthSelectScheme": "Velg metode",
"healthCheckPortInvalid": "Porten må være mellom 1 og 65535",
"healthCheckHostnameInvalid": "Vertsnavnet må ikke inneholde mellomrom",
"healthCheckPath": "Sti",
"healthHostname": "IP / Vert",
"healthPort": "Port",
@@ -2236,10 +2752,11 @@
"timeIsInSeconds": "Tid er i sekunder",
"requireDeviceApproval": "Krev enhetsgodkjenning",
"requireDeviceApprovalDescription": "Brukere med denne rollen trenger nye enheter godkjent av en admin før de kan koble seg og få tilgang til ressurser.",
"sshSettings": "SSH Innstillinger",
"sshSettings": "SSH",
"inferenceSettings": "AI Portal",
"sshAccess": "SSH-tilgang",
"rdpSettings": "RDP Innstillinger",
"vncSettings": "VNC Innstillinger",
"rdpSettings": "RDP",
"vncSettings": "VNC",
"sshServer": "SSH-server",
"rdpServer": "RDP-server",
"vncServer": "VNC-server",
@@ -2352,9 +2869,9 @@
"resourcesTableProxyResources": "Offentlig",
"resourcesTableClientResources": "Privat",
"resourcesTableNoProxyResourcesFound": "Ingen proxy-ressurser funnet.",
"resourcesTableNoInternalResourcesFound": "Ingen interne ressurser funnet.",
"resourcesTableNoInternalResourcesFound": "Ingen private ressurser funnet.",
"resourcesTableDestination": "Destinasjon",
"resourcesTableAlias": "Alias",
"resourcesTableAlias": "Navn",
"resourcesTableAliasAddress": "Alias adresse",
"resourcesTableAliasAddressInfo": "Denne adressen er en del av organisasjonens undernettverk. Den brukes til å løse aliasposter ved hjelp av intern DNS-oppløsning.",
"resourcesTableClients": "Klienter",
@@ -2375,9 +2892,9 @@
"editInternalResourceDialogCancel": "Avbryt",
"editInternalResourceDialogSaveResource": "Lagre ressurs",
"editInternalResourceDialogSuccess": "Suksess",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Intern ressurs oppdatert vellykket",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Privat ressurs oppdatert vellykket",
"editInternalResourceDialogError": "Feil",
"editInternalResourceDialogFailedToUpdateInternalResource": "Mislyktes å oppdatere intern ressurs",
"editInternalResourceDialogFailedToUpdateInternalResource": "Feil ved oppdatering av privat ressurs",
"editInternalResourceDialogNameRequired": "Navn er påkrevd",
"editInternalResourceDialogNameMaxLength": "Navn kan ikke være lengre enn 255 tegn",
"editInternalResourceDialogProxyPortMin": "Proxy-port må være minst 1",
@@ -2392,6 +2909,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeInference": "AI Portal",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Skjema",
"editInternalResourceDialogEnableSsl": "Aktiver TLS",
@@ -2400,10 +2918,10 @@
"editInternalResourceDialogDestinationHostDescription": "IP-adressen eller vertsnavnet til ressursen på nettstedets nettverk.",
"editInternalResourceDialogDestinationIPDescription": "IP eller vertsnavn til ressursen på nettstedets nettverk.",
"editInternalResourceDialogDestinationCidrDescription": "CIDR-rekkevidden til ressursen på nettstedets nettverk.",
"editInternalResourceDialogAlias": "Alias",
"editInternalResourceDialogAlias": "Navn",
"editInternalResourceDialogAliasDescription": "Et valgfritt internt DNS-alias for denne ressursen.",
"createInternalResourceDialogNoSitesAvailable": "Ingen tilgjengelige steder",
"createInternalResourceDialogNoSitesAvailableDescription": "Du må ha minst ett Newt-område med et konfigureret delnett for å lage interne ressurser.",
"createInternalResourceDialogNoSitesAvailableDescription": "Du må ha minst ett Newt-område med et subnett konfigurert for å opprette private ressurser.",
"createInternalResourceDialogClose": "Lukk",
"createInternalResourceDialogCreateClientResource": "Opprett privat ressurs",
"createInternalResourceDialogCreateClientResourceDescription": "Opprett en ny ressurs som bare vil være tilgjengelig for kunder som er koblet til organisasjonen",
@@ -2412,8 +2930,8 @@
"privateResourceAllowIcmpPing": "Tillat ICMP Ping",
"privateResourceNetworkAccess": "Nettverkstilgang",
"privateResourceNetworkAccessDescription": "Kontroller TCP/UDP porttilgang og om ICMP ping tillates for denne ressursen.",
"hostSettings": "Vertinnstillinger",
"cidrSettings": "CIDR-innstillinger",
"hostSettings": "Vert",
"cidrSettings": "CIDR",
"createInternalResourceDialogResourceProperties": "Ressursegenskaper",
"createInternalResourceDialogName": "Navn",
"createInternalResourceDialogSite": "Område",
@@ -2432,9 +2950,9 @@
"createInternalResourceDialogCancel": "Avbryt",
"createInternalResourceDialogCreateResource": "Opprett ressurs",
"createInternalResourceDialogSuccess": "Suksess",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Intern ressurs opprettet vellykket",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Privat ressurs opprettet vellykket",
"createInternalResourceDialogError": "Feil",
"createInternalResourceDialogFailedToCreateInternalResource": "Kunne ikke opprette intern ressurs",
"createInternalResourceDialogFailedToCreateInternalResource": "Kunne ikke opprette privat ressurs",
"createInternalResourceDialogNameRequired": "Navn er påkrevd",
"createInternalResourceDialogNameMaxLength": "Navn kan ikke være lengre enn 255 tegn",
"createInternalResourceDialogPleaseSelectSite": "Vennligst velg et område",
@@ -2451,6 +2969,7 @@
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"createInternalResourceDialogModeInference": "AI Portal",
"scheme": "Skjema",
"createInternalResourceDialogScheme": "Skjema",
"createInternalResourceDialogEnableSsl": "Aktiver TLS",
@@ -2458,7 +2977,7 @@
"createInternalResourceDialogDestination": "Destinasjon",
"createInternalResourceDialogDestinationHostDescription": "IP-adressen eller vertsnavnet til ressursen på nettstedets nettverk.",
"createInternalResourceDialogDestinationCidrDescription": "CIDR-rekkevidden til ressursen på nettstedets nettverk.",
"createInternalResourceDialogAlias": "Alias",
"createInternalResourceDialogAlias": "Navn",
"createInternalResourceDialogAliasDescription": "Et valgfritt internt DNS-alias for denne ressursen.",
"internalResourceAliasLocalWarning": "Alias som slutter på .local kan forårsake oppløsningsproblemer på grunn av mDNS på enkelte nettverk.",
"internalResourceDownstreamSchemeRequired": "Skjema er påkrevd for HTTP-ressurser",
@@ -2505,7 +3024,7 @@
"remoteExitNodeNetworkingSubnetsPlaceholder": "Legg til et CIDR-område (f.eks. 10.0.0.0/8)",
"remoteExitNodeNetworkingSubnetsLoadError": "Feil ved lasting av subnett",
"remoteExitNodeNetworkingLabelsTitle": "Preferanseetiketter",
"remoteExitNodeNetworkingLabelsDescription": "Områder med disse etikettene vil bli tvunget til å koble gjennom denne fjerne utgangsnoden.",
"remoteExitNodeNetworkingLabelsDescription": "Nettsteder med disse etikettene vil foretrekke å koble til gjennom denne eksterne utgangsnoden.",
"remoteExitNodeNetworkingLabelsButtonText": "Velg etiketter...",
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "Søk etiketter...",
"remoteExitNodeNetworkingLabelsLoadError": "Feil ved lasting av etiketter",
@@ -2667,12 +3186,12 @@
"roleMappingRemoveRule": "Fjern",
"idpGoogleConfiguration": "Google Konfigurasjon",
"idpGoogleConfigurationDescription": "Konfigurer Google OAuth2 legitimasjonen",
"idpGoogleClientIdDescription": "Google OAuth2 Client ID",
"idpGoogleClientIdDescription": "Din Google OAuth2-klient-ID",
"idpGoogleClientSecretDescription": "Google OAuth2-klienten hemmelighet",
"idpAzureConfiguration": "Azure Entra ID konfigurasjon",
"idpAzureConfigurationDescription": "Konfigurer Azure Entra ID OAuth2 legitimasjon",
"idpTenantId": "Leietaker-ID",
"idpTenantIdPlaceholder": "tenant-id",
"idpTenantIdPlaceholder": "Leietaker-ID",
"idpAzureTenantIdDescription": "Azure leant ID (funnet i Azure Active Directory-oversikten)",
"idpAzureClientIdDescription": "Azure App registrerings klient-ID",
"idpAzureClientSecretDescription": "Azure App Registrering Klient Hemmelig",
@@ -2982,6 +3501,7 @@
"priority": "Prioritet",
"priorityDescription": "Høyere prioriterte ruter evalueres først. Prioritet = 100 betyr automatisk bestilling (systembeslutninger). Bruk et annet nummer til å håndheve manuell prioritet.",
"instanceName": "Forekomst navn",
"clearInstanceName": "Reset Server Association",
"pathMatchModalTitle": "Konfigurere matching av sti",
"pathMatchModalDescription": "Sett opp hvordan innkommende forespørsler skal matches basert på deres bane.",
"pathMatchType": "Trefftype",
@@ -3038,10 +3558,11 @@
"allowedNoAuth": "Tillatt Ingen Auth",
"validAccessToken": "Gyldig tilgangsnøkkel",
"validHeaderAuth": "Valid header auth",
"validPincode": "Valid Pincode",
"validPincode": "Gyldig PIN-kode",
"validPassword": "Gyldig passord",
"validEmail": "Valid email",
"validSSO": "Valid SSO",
"validVirtualAPIKey": "Gyldig Virtuell API-nøkkel",
"view": "Vis",
"configManaged": "Konfigurasjon administrert",
"connectedClient": "Tilkoblet klient",
@@ -3062,7 +3583,42 @@
"sidebarLogsAction": "Handlingslogger",
"logRetention": "Logg tilbaketrekning",
"logRetentionDescription": "Håndter hvor lenge ulike typer logger beholdes for denne organisasjonen, eller deaktiver dem",
"logRetentionDisabledWarningTitle": "Loggbevaring deaktivert",
"logRetentionDisabledWarningDescription": "{logType} blir ikke lagret for denne organisasjonen, så ny aktivitet vises ikke her. Aktiver lagring i sikkerhetsinnstillingene for å begynne å samle inn disse loggene.",
"logRetentionDisabledWarningButton": "Gå til sikkerhetsinnstillinger",
"requestLogsDescription": "Se detaljerte forespørselslogger for ressurser i denne organisasjonen",
"aiSessionLogs": "AI Portal Sesjonslogger",
"aiSessionLogsDescription": "Vis stikkord- og responsutskrifter for AI-portal forespørsler i denne organisasjonen",
"sidebarLogsAi": "Sesjonslogger",
"commandLogsAi": "Sesjonslogger",
"sidebarLogsAiUsage": "Bruksanalyse",
"commandLogsAiUsage": "Bruksanalyse",
"provider": "Leverandør",
"capability": "Kapasitet",
"model": "Modell",
"virtualApiKey": "Virtuell API-nøkkel",
"noVirtualApiKey": "Ingen virtuell API-nøkkel",
"stream": "Strøm",
"streaming": "Strømming",
"nonStreaming": "Ikke-strømming",
"statusCode": "Statuskode",
"aiSessionId": "Sesjons ID",
"aiSessionRequest": "Forespørsel",
"aiSessionResponse": "Respons",
"aiSessionNoData": "Ingen data registrert",
"aiSessionCouldNotParse": "(rå, kunne ikke analysere utskrift)",
"aiSessionLogTruncated": "Denne sesjonen ble avkortet før lagring og kan være ufullstendig.",
"aiSessionViewRaw": "Vis Rå JSON",
"aiSessionViewChat": "Vis Chat",
"cost": "Kostnad",
"estimated": "Anslått",
"tokenUsage": "Tokenforbruk",
"promptTokens": "Stikkord Token",
"cacheReadTokens": "Cache-lesing Token",
"cacheWriteTokens": "Cache-skriving Token",
"completionTokens": "Fullføring Token",
"reasoningTokens": "Resonnering Token",
"totalTokens": "Totale Token",
"requestAnalyticsDescription": "Se detaljert rekvisisjonsanalyse for ressurser i denne organisasjonen",
"logRetentionRequestLabel": "Be om loggbevaring",
"logRetentionRequestDescription": "Hvor lenge du vil beholde forespørselslogger",
@@ -3072,6 +3628,8 @@
"logRetentionActionDescription": "Hvor lenge handlingen skal lagres",
"logRetentionConnectionLabel": "Logg nyhet",
"logRetentionConnectionDescription": "Hvor lenge du vil beholde tilkoblingslogger",
"logRetentionAISessionsLabel": "Bevaringsinnstillinger AI-portal sesjonslogger",
"logRetentionAISessionsDescription": "Hvor lenge man skal bevare stikkord/response sesjonslogger for AI-portaler",
"logRetentionDisabled": "Deaktivert",
"logRetention3Days": "3 dager",
"logRetention7Days": "7 dager",
@@ -3089,8 +3647,8 @@
"sourceAddress": "Kilde adresse",
"destinationAddress": "Måladresse (Automatic Translation)",
"duration": "Varighet",
"licenseRequiredToUse": "En <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisens eller <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> er påkrevd for å bruke denne funksjonen. <bookADemoLink>Bestill en demo eller POC prøveversjon</bookADemoLink>.",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> er nødvendig for å bruke denne funksjonen. Denne funksjonen er også tilgjengelig i <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>. <bookADemoLink>Bestill en demo eller POC studie</bookADemoLink>.",
"licenseRequiredToUse": "<enterpriseEditionLink>Enterprise Edition lisens</enterpriseEditionLink> er påkrevd. <bookADemoLink>Bestill en demo eller prøveversjon</bookADemoLink>",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Enterprise Edition lisens</enterpriseEditionLink> er påkrevd. <bookADemoLink>Bestill en demo eller prøveversjon</bookADemoLink>",
"certResolver": "Sertifikat løser",
"certResolverDescription": "Velg sertifikatløser som skal brukes for denne ressursen.",
"selectCertResolver": "Velg sertifikatløser",
@@ -3216,7 +3774,7 @@
"regenerateCredentialsWarning": "Regenerering av legitimasjon vil ugyldiggjøre de forrige og forårsake en frakobling. Sørg for å oppdatere alle konfigurasjoner som bruker disse legitimasjonene.",
"confirm": "Bekreft",
"regenerateCredentialsConfirmation": "Er du sikker på at du vil regenerere legetimasjonene?",
"endpoint": "Endpoint",
"endpoint": "Endepunkt",
"Id": "Id",
"SecretKey": "Hemmelig nøkkel",
"niceId": "God ID",
@@ -3233,6 +3791,7 @@
"noData": "Ingen data",
"machineClients": "Maskinklienter",
"install": "Installer",
"downloadInstaller": "Download Installer",
"run": "Kjør",
"envFile": "Miljøfil",
"serviceFile": "Tjenestefil",
@@ -3288,9 +3847,9 @@
"internalResourceFormMultiSiteRoutingHelp": "Valg av flere nettsteder muliggjør motstandskraftig ruting og failover for høy tilgjengelighet.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Lær mer",
"editInternalResourceDialogPortRestrictionsDescription": "Begrens tilgang til spesifikke TCP/UDP-porter eller tillate/blokkere alle porter.",
"createInternalResourceDialogHttpConfiguration": "HTTP-konfigurasjon",
"createInternalResourceDialogHttpConfiguration": "Domene Konfigurasjon",
"createInternalResourceDialogHttpConfigurationDescription": "Velg domenet klienter vil bruke for å nå denne ressursen via HTTP eller HTTPS.",
"editInternalResourceDialogHttpConfiguration": "HTTP-konfigurasjon",
"editInternalResourceDialogHttpConfiguration": "Domene Konfigurasjon",
"editInternalResourceDialogHttpConfigurationDescription": "Velg domenet klienter vil bruke for å nå denne ressursen via HTTP eller HTTPS.",
"editInternalResourceDialogTcp": "TCP",
"editInternalResourceDialogUdp": "UDP",
@@ -3440,6 +3999,7 @@
"disconnected": "Frakoblet",
"approvalsEmptyStateTitle": "Enhetsgodkjenninger er ikke aktivert",
"approvalsEmptyStateDescription": "Aktivere godkjenninger av enheter for at roller må godkjennes av admin før brukere kan koble til nye enheter.",
"approvalsEmptyStateHowToTitle": "How to Enable",
"approvalsEmptyStateStep1Title": "Gå til roller",
"approvalsEmptyStateStep1Description": "Naviger til organisasjonens roller innstillinger for å konfigurere enhetsgodkjenninger.",
"approvalsEmptyStateStep2Title": "Aktiver enhetsgodkjenninger",
@@ -3561,6 +4121,8 @@
"httpDestConnectionLogsDescription": "Utstyrs- og tunneltilkoblingshendelser, inkludert forbindelser og frakobling.",
"httpDestRequestLogsTitle": "HTTP-forespørselslogger",
"httpDestRequestLogsDescription": "HTTP-forespørsel logger for bekreftede ressurser, inkludert metode, bane og responskode.",
"httpDestAISessionLogsTitle": "AI øktlogger",
"httpDestAISessionLogsDescription": "Forespørsels- og svarøkter for AI gateway, inkludert forespørsler, modellresponser og tokenbruk.",
"httpDestSaveChanges": "Lagre endringer",
"httpDestCreateDestination": "Opprett mål",
"httpDestUpdatedSuccess": "Målet er oppdatert",
@@ -3717,6 +4279,11 @@
"resourceLauncherViewAsAdmin": "Vis som administrator",
"resourceLauncherResourceDetailsDescription": "Tilkoblingsinformasjon og status for denne ressursen.",
"resourceLauncherResourceDetails": "Ressursdetaljer",
"resourceLauncherSitesDescription": "Ressursen er tilgjengelig via de følgende nettstedene.",
"resourceLauncherViewSiteAsAdmin": "Vis nettsted som administrator",
"resourceLauncherFilterBySite": "Filtrer etter nettsted",
"resourceLauncherSshCommand": "SSH-kommando",
"resourceLauncherSshCommandDescription": "Bruk Pangolin CLI for å åpne en SSH-sesjon til denne ressursen.",
"resourceLauncherAuthMethodsDescription": "Godkjenningsmetoder aktivert for denne ressursen.",
"resourceLauncherPrivateClientRequired": "Koble til med en klient på enheten din for å få privat tilgang til denne ressursen.",
"resourceLauncherPrivateClientRequiredTitle": "Klienttilkobling kreves",
@@ -3726,7 +4293,18 @@
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherUnlabeled": "Umerket",
"resourceLauncherAiGateway": "AI Portal",
"resourceLauncherNoSite": "Ingen område",
"resourceLauncherAvailableModels": "Tilgjengelige Modeller",
"resourceLauncherAvailableModelsDescription": "Modeller du kan bruke med denne AI-portalen.",
"resourceLauncherAvailableModelsEmpty": "Ingen modeller er tilgjengelige for denne ressursen.",
"resourceLauncherAvailableModelsError": "Kunne ikke laste inn tilgjengelige modeller.",
"resourceLauncherApiKeys": "API-nøkler",
"resourceLauncherApiKeysDescription": "Bruk din identitetsnøkkel eller en tillagt nøkkel for å autentisere med denne ressursen.",
"resourceLauncherApiKeysIdentity": "Identitetsnøkkel",
"resourceLauncherApiKeysManual": "Tildelte nøkler",
"resourceLauncherApiKeysEmpty": "Ingen API-nøkler er tilgjengelige for denne ressursen.",
"resourceLauncherApiKeysError": "Kunne ikke laste inn API-nøkler.",
"resourceLauncherNoResourcesInGroup": "Ingen ressurser i denne gruppen",
"resourceLauncherEmptyStateTitle": "Ingen tilgjengelige ressurser",
"resourceLauncherEmptyStateDescription": "Du har ennå ikke tilgang til noen ressurser. Kontakt administratoren din for å be om tilgang.",
@@ -3753,9 +4331,9 @@
"resourceLauncherViewDeleteFailedDescription": "Kunne ikke slette lanseringsvisningen. Vennligst prøv igjen.",
"memberPortalPrevious": "Forrige",
"memberPortalNext": "Neste",
"httpSettings": "HTTP Innstillinger",
"tcpSettings": "TCP Innstillinger",
"udpSettings": "UDP Innstillinger",
"httpSettings": "HTTP",
"tcpSettings": "TCP",
"udpSettings": "UDP",
"sshTitle": "SSH",
"sshConnectingDescription": "Opprette en sikker tilkobling…",
"sshConnecting": "Kobler til…",
@@ -3816,5 +4394,6 @@
"rdpUnicodeKeyboardMode": "Unicode tastaturmodus",
"sessionToolbarShow": "Vis verktøylinje",
"sessionToolbarHide": "Skjul verktøylinje",
"actionUpdateSiteApprovals": "Oppdater Stedsgodkjenninger"
"actionUpdateSiteApprovals": "Oppdater Stedsgodkjenninger",
"check": "Sjekk"
}
+680 -101
View File
File diff suppressed because it is too large Load Diff
+627 -48
View File
File diff suppressed because it is too large Load Diff
+625 -46
View File
File diff suppressed because it is too large Load Diff
+628 -49
View File
File diff suppressed because it is too large Load Diff
+606 -27
View File
@@ -43,6 +43,8 @@
"inviteLoginUser": "Lütfen doğru kullanıcı olarak oturum açtığınızdan emin olun.",
"inviteErrorNoUser": "Üzgünüz, ancak erişmeye çalıştığınız davet, var olan bir kullanıcı için değil gibi görünüyor.",
"inviteCreateUser": "Öncelikle bir hesap oluşturun.",
"inviteErrorOidcNotAllowed": "Davetler yalnızca dahili hesaplar tarafından kabul edilebilir. Oturumunuzu kapatın ve bu e-postayla parolanızla giriş yapın.",
"inviteLoginInternalOnly": "Davet, parolası olan bir dahili hesap gerektirir. Hesap oluşturun veya parolanızla giriş yapın.",
"goHome": "Ana Sayfaya Dön",
"inviteLogInOtherUser": "Başka bir kullanıcı olarak giriş yapın",
"createAnAccount": "Bir Hesap Oluşturun",
@@ -151,6 +153,7 @@
"siteResourcesTargetsOnSite": "Bu sitedeki hedefler",
"siteSetting": "{siteName} Ayarları",
"siteNewtTunnel": "Newt Site (Önerilen)",
"pangolinSite": "Pangolin Sitesi",
"siteNewtTunnelDescription": "Ağınıza giriş noktası oluşturmanın en kolay yolu. Ekstra kurulum gerekmez.",
"siteWg": "Temel WireGuard",
"siteWgDescription": "Bir tünel oluşturmak için herhangi bir WireGuard istemcisi kullanın. Manuel NAT kurulumu gereklidir.",
@@ -178,7 +181,7 @@
"shareDeleteConfirm": "Paylaşılabilir Bağlantıyı Silmeyi Onayla",
"shareQuestionRemove": "Bu paylaşım bağlantısını silmek istediğinizden emin misiniz?",
"shareMessageRemove": "Silindikten sonra, bağlantı artık çalışmayacak ve kullanan herkes kaynağa erişimini kaybedecek.",
"shareTokenDescription": "Erişim jetonunuz iki şekilde iletilebilir: sorgu parametresi olarak veya istek başlıklarında. Kimlik doğrulanmış erişim için her istekten müşteri tarafından iletilmelidir.",
"shareTokenDescription": "Erişim belirteci bir sorgu parametresi olarak veya istek başlıkları içinden gönderilebilir. Varsayılan olarak her istekte gönderilmelidir. Oturum kalıcılığı etkinse, ilk istek oturum çereziyle değiştirilir.",
"accessToken": "Erişim Jetonu",
"usageExamples": "Kullanım Örnekleri",
"tokenId": "Jeton ID",
@@ -196,8 +199,14 @@
"shareTitleOptional": "Başlık (isteğe bağlı)",
"sharePathOptional": "Yol (isteğe bağlı)",
"sharePathDescription": "Bağlantıdan sonra kullanıcıları bu yola yönlendirecek bağlantıyı tanımlayın.",
"shareAssociateUserOptional": "Kullanıcıyla İlişkilendir (isteğe bağlı)",
"shareAssociateUserDescription": "Ayarladığında, bu bağlantıyı kullanan istekler erişim günlüklerinde ve kimlik başlıklarında kullanıcıya atanır. Kullanıcı kuruluşu terk ederse bağlantı kaldırılır.",
"userSelect": "Kullanıcı seçin",
"usersNotFound": "Kullanıcı bulunamadı",
"expireIn": "Süresi Dolacak",
"neverExpire": "Hiçbir Zaman Sona Ermez",
"sharePersistSession": "İlk kullanım sonrası oturumu kalıcı yap",
"sharePersistSessionDescription": "Etkinleştirildiğinde, bu belirteçle yapılan ilk istek, sorgu parametresi veya başlık üzerinden, bir oturum çerezi ayarlayarak sonraki isteklerde belirteç gerektirmeyecek. Her istek için belirteç göndermesi gereken API müşterileri için bırakın.",
"shareExpireDescription": "Son kullanma süresi, bağlantının kullanılabilir ve kaynağa erişim sağlayacak süresidir. Bu süreden sonra bağlantı çalışmayı durduracak ve bu bağlantıyı kullanan kullanıcılar kaynağa erişimini kaybedecektir.",
"shareSeeOnce": "Bu bağlantıyı yalnızca bir kez görebileceksiniz. Kopyaladığınızdan emin olun.",
"shareAccessHint": "Bu bağlantıya sahip olan herkes kaynağa erişebilir. Dikkatle paylaşın.",
@@ -280,7 +289,21 @@
"noCountryFound": "Ülke bulunamadı.",
"siteSelectionDescription": "Bu site hedefe bağlantı sağlayacaktır.",
"resourceType": "Kaynak Türü",
"resourceTypeDescription": "Bu, kaynak protokolünü ve tarayıcıda nasıl görüntüleneceğini kontrol eder. Bu daha sonra değiştirilemez.",
"resourceTypeDescription": "Bu, kaynak protokolünü kontrol eder ve daha sonra değiştirilemez.",
"resourceTypeSearch": "Kaynak türlerini ara...",
"resourceTypeNotFound": "Hiçbir kaynak türü bulunamadı",
"resourceTypeHttpDescription": "Bir alan adı kullanarak HTTPS üzerinden web trafiğini proxy yapın",
"resourceTypeInferenceDescription": "Bağlı sağlayıcılar üzerinden AI isteklerini yönlendirin",
"resourceTypeSshDescription": "Tarayıcıdan bir SSH sunucusuna erişim",
"resourceTypeRdpDescription": "Tarayıcıdan bir uzaktan masaüstüne erişim",
"resourceTypeVncDescription": "Tarayıcıdan bir VNC masaüstüne erişim",
"resourceTypeTcpDescription": "Bir bağlantı noktası numarası kullanarak ham TCP trafiğini proxy yapın",
"resourceTypeUdpDescription": "Bir bağlantı noktası numarası kullanarak ham UDP trafiğini proxy yapın",
"privateResourceTypeDescription": "Bu, müşterilerin kaynağa nasıl erişeceğini kontrol eder. Daha sonra değiştirilemez.",
"privateResourceTypeHostDescription": "Bağlanan müşterilere site ağında tek bir ana makineyi açığa çıkartın",
"privateResourceTypeCidrDescription": "Bağlanan müşterilere site ağında bir CIDR aralığını açığa çıkartın",
"privateResourceTypeHttpDescription": "Bir alan adı üzerinden HTTP veya HTTPS hizmetine eriş",
"privateResourceTypeSshDescription": "Bağlanan müşteriler için bir SSH sunucusuna eriş",
"resourceDomainDescription": "Kaynak bu tam etki alanı adıyla sunulacak.",
"resourceHTTPSSettings": "HTTPS Ayarları",
"resourceHTTPSSettingsDescription": "Kaynağınıza HTTPS üzerinden erişimin nasıl sağlanacağını yapılandırın",
@@ -443,6 +466,8 @@
"apiKeysDelete": "API Anahtarını Sil",
"apiKeysManage": "API Anahtarlarını Yönet",
"apiKeysDescription": "API anahtarları entegrasyon API'sini doğrulamak için kullanılır",
"orgsManage": "Örgütleri Yönet",
"orgsDescription": "Bu örnekteki tüm örgütleri görüntüle ve yönet",
"provisioningKeysTitle": "Tedarik Anahtarı",
"provisioningKeysManage": "Tedarik Anahtarlarını Yönet",
"provisioningKeysDescription": "Tedarik anahtarları, organizasyonunuz için otomatik site sağlama işlemini doğrulamak için kullanılır.",
@@ -694,6 +719,7 @@
"nameOptional": "İsim (İsteğe Bağlı)",
"accessControls": "Erişim Kontrolleri",
"userDescription2": "Bu kullanıcı üzerindeki ayarları yönetin",
"userGeneralSettingsDescription": "Bu kullanıcının örgütteki rollerini ve ayarlarını yönetin",
"accessRoleErrorAdd": "Kullanıcıyı role ekleme başarısız oldu",
"accessRoleErrorAddDescription": "Kullanıcı role eklenirken bir hata oluştu.",
"userSaved": "Kullanıcı kaydedildi",
@@ -736,7 +762,7 @@
"proxyUpdatedDescription": "Proxy ayarları başarıyla güncellendi",
"proxyErrorUpdate": "Proxy ayarları güncellenemedi",
"proxyErrorUpdateDescription": "Proxy ayarlarını güncellerken bir hata oluştu",
"targetAddr": "Host",
"targetAddr": "Sunucu",
"targetPort": "Bağlantı Noktası",
"targetProtocol": "Protokol",
"targetTlsSettings": "HTTPS & TLS Settings",
@@ -849,12 +875,16 @@
"authMethodsSave": "Ayarları Kaydet",
"policyAuthStackTitle": "Kimlik Doğrulama",
"policyAuthStackDescription": "Bu kaynağa erişim için hangi kimlik doğrulama yöntemlerinin gerekli olduğuna karar verin",
"policyAuthInferenceStackDescription": "Bu AI geçidine kimlerin ve hangi rollerin kimlik doğrulama yapabileceğini seçin",
"policyAuthOrLogicTitle": "Birden fazla kimlik doğrulama yöntemi etkin",
"policyAuthOrLogicBanner": "Ziyaretçiler aşağıdaki etkin yöntemlerden herhangi birini kullanarak kimlik doğrulaması yapabilirler. Hepsini tamamlamaları gerekmez.",
"policyAuthMethodActive": "Etkin",
"policyAuthMethodOff": "Kapalı",
"policyAuthSsoTitle": "Platform SSO",
"policyAuthSsoTitle": "Platform SSO'sunu Kullanın",
"policyAuthSsoDescription": "Organizasyonunuzun kimlik sağlayıcısı üzerinden oturum açmayı zorunlu kılın",
"policyAuthInferenceIdentityKeySignInUrl": "Giriş URL'si",
"policyAuthInferenceIdentityKeyHelpNoUrl": "Her kullanıcının zaten bir kimlik API anahtarı vardır, bu nedenle yalnızca kullanıcı olmayan istemciler veya ortak erişim için sanal API anahtarları oluşturmanız gerekir. Kullanıcılar bu kaynağın URL'si üzerinden kimlik sağlayıcılarıyla oturum açarak anahtarlarını alabilirler, bu girişten sonra gösterilecektir.",
"policyAuthInferenceIdentityKeyHelp": "Her kullanıcının zaten bir kimlik API anahtarı vardır, bu nedenle yalnızca kullanıcı olmayan istemciler veya ortak erişim için sanal API anahtarları oluşturmanız gerekir. Kullanıcılar burada kimlik sağlayıcılarıyla giriş yaptıktan sonra anahtarlarını alırlar.",
"policyAuthSsoSummary": "{idp} · {users} kullanıcısı, {roles} rolü",
"policyAuthSsoDefaultIdp": "Varsayılan sağlayıcı",
"policyAuthAddDefaultIdentityProvider": "Varsayılan Kimlik Sağlayıcı Ekle",
@@ -886,7 +916,7 @@
"policyAccessRulesFallthroughOff": "Kurallar devre dışı bırakıldığında, tüm trafik kimlik doğrulamasına geçer.",
"policyAccessRulesFallthroughOn": "Herhangi bir kural eşleşmediğinde trafik kimlik doğrulamasına geçer.",
"rulesPlaceholderCidr": "10.0.0.0/8",
"rulesPlaceholderPath": "/admin/*",
"rulesPlaceholderPath": "/yönetici/*",
"rulesPlaceholderGeo": "RU, KP",
"rulesSave": "Kuralları Kaydet",
"resourceErrorCreate": "Kaynak oluşturma hatası",
@@ -928,7 +958,7 @@
"newtVersion": "Sürüm",
"architecture": "Mimari",
"sites": "Siteler",
"siteWgAnyClients": "Herhangi bir WireGuard istemcisi kullanarak bağlanın. Dahili kaynaklara eş IP adresini kullanarak erişmeniz gerekecek.",
"siteWgAnyClients": "Bağlanmak için herhangi bir WireGuard istemcisi kullanın. Özel kaynaklara eş IP adresini kullanarak erişmeniz gerekecek.",
"siteWgCompatibleAllClients": "Tüm WireGuard istemcileriyle uyumlu",
"siteWgManualConfigurationRequired": "Manuel yapılandırma gerekli",
"userErrorNotAdminOrOwner": "Kullanıcı yönetici veya sahibi değil",
@@ -1073,6 +1103,8 @@
"accessRoleErrorNewRequired": "Yeni rol gerekli",
"accessRoleErrorRemove": "Rol kaldırılamadı",
"accessRoleErrorRemoveDescription": "Rol kaldırılırken bir hata oluştu.",
"accessRoleInferenceBudget": "AI Bütçesi",
"accessRoleInferenceBudgetDescription": "Bu role ait üyelerin harcama veya jeton sınırlarına dayalı AI kullanımını nasıl kısıtlayacaklarını yapılandırın",
"accessRoleName": "Rol Adı",
"accessRoleQuestionRemove": "{name} rolünü silmek üzeresiniz. Bu eylemi geri alamazsınız.",
"accessRoleRemove": "Rolü Kaldır",
@@ -1148,6 +1180,10 @@
"idpJmespathAboutDescriptionLink": "JMESPath hakkında daha fazla bilgi edinin",
"idpJmespathLabel": "Tanımlayıcı Yolu",
"idpJmespathLabelDescription": "The JMESPath to the user identifier in the ID token",
"idpIdentifierChangeTitle": "Tanımlayıcı Yol Değişikliği Uyarısı",
"idpIdentifierChangeDescription": "Tanımlayıcı yolu değiştirmek üzeresiniz. Bu, mevcut kullanıcıların nasıl eşleneceğini etkileyecektir. Bu kimlik sağlayıcı üzerinden daha önce oturum açmış olan kullanıcılar artık aynı kullanıcılar olarak tanınmayabilir.",
"idpIdentifierChangeConfirmMessage": "Onaylıyorum",
"idpIdentifierChangeWarningText": "Bu, mevcut kullanıcıların nasıl eşleneceğini etkileyecek",
"idpJmespathEmailPathOptional": "E-posta Yolu (İsteğe Bağlı)",
"idpJmespathEmailPathOptionalDescription": "The JMESPath to the user's email in the ID token",
"idpJmespathNamePathOptional": "Ad Yolu (İsteğe Bağlı)",
@@ -1392,6 +1428,24 @@
"logoutError": "Çıkış yaparken hata",
"signingAs": "Olarak giriş yapıldı",
"serverAdmin": "Sunucu Yöneticisi",
"promoteServerAdmin": "Sunucu yöneticisine terfi et",
"promoteServerAdminTitle": "Sunucu Yöneticisine Terfi",
"promoteServerAdminQuestion": "{selectedUser} adresini sunucu yöneticisi olarak terfi etmek istediğinizden emin misiniz?",
"promoteServerAdminMessage": "Sunucu yöneticileri en yüksek ayrıcalıklara sahiptir ve sunucuyu yönetebilir.",
"promoteServerAdminWarning": "Bu, kullanıcıyı indirerek herhangi bir zamanda geri alınabilir.",
"promoteServerAdminConfirm": "Sunucu Yöneticisine Terfi",
"promoteServerAdminSuccess": "Kullanıcı Terfi Edildi",
"promoteServerAdminSuccessDescription": "{selectedUser} artık bir sunucu yöneticisi.",
"promoteServerAdminError": "Kullanıcıyı terfi ettirme başarısız oldu",
"demoteServerAdmin": "Sunucu yöneticisinden negatif terfi et",
"demoteServerAdminTitle": "Sunucu Yöneticisinden Negatif Terfi",
"demoteServerAdminQuestion": "{selectedUser} adresini sunucu yöneticiliğinden indirmek istediğinizden emin misiniz?",
"demoteServerAdminMessage": "{selectedUser} tüm sunucu yönetici ayrıcalıklarını kaybedecek.",
"demoteServerAdminWarning": "Bu, kullanıcıyı terfi ettirerek herhangi bir zamanda geri alınabilir.",
"demoteServerAdminConfirm": "Sunucu yöneticisinden negatif terfi et",
"demoteServerAdminSuccess": "Kullanıcı indirildi",
"demoteServerAdminSuccessDescription": "{selectedUser} artık bir sunucu yöneticisi değil.",
"demoteServerAdminError": "Kullanıcıyı indirirken başarısız oldu",
"managedSelfhosted": "Yönetilen Self-Hosted",
"otpEnable": "İki faktörlü özelliğini etkinleştir",
"otpDisable": "İki faktörlü özelliğini devre dışı bırak",
@@ -1418,6 +1472,28 @@
"actionDeleteSite": "Siteyi Sil",
"actionGetSite": "Siteyi Al",
"actionListSites": "Siteleri Listele",
"actionCreateAiProvider": "AI Sağlayıcı Oluştur",
"actionDeleteAiProvider": "AI Sağlayıcı Sil",
"actionGetAiProvider": "AI Sağlayıcı Al",
"actionListAiProviders": "AI Sağlayıcıları Listele",
"actionUpdateAiProvider": "AI Sağlayıcı Güncelle",
"actionCreateAiModel": "AI Modeli Oluştur",
"actionDeleteAiModel": "AI Modeli Sil",
"actionGetAiModel": "AI Modeli Al",
"actionListAiModels": "AI Modellerini Listele",
"actionUpdateAiModel": "AI Modelini Güncelle",
"actionCreateAiBudget": "AI Bütçesi Oluştur",
"actionDeleteAiBudget": "AI Bütçesini Sil",
"actionGetAiBudget": "AI Bütçesi Al",
"actionListAiBudgets": "AI Bütçelerini Listele",
"actionUpdateAiBudget": "AI Bütçesi Güncelle",
"actionListResourceAiModels": "Kaynak AI Modellerini Listele",
"actionSetResourceAiModels": "Kaynak AI Modellerini Ayarla",
"actionCreateVirtualApiKey": "Sanal API Anahtarı Oluştur",
"actionDeleteVirtualApiKey": "Sanal API Anahtarını Sil",
"actionGetVirtualApiKey": "Sanal API Anahtarı Al",
"actionListVirtualApiKeys": "Sanal API Anahtarlarını Listele",
"actionUpdateVirtualApiKey": "Sanal API Anahtarı Güncelle",
"actionApplyBlueprint": "Planı Uygula",
"actionListBlueprints": "Plan Listesini Görüntüle",
"actionGetBlueprint": "Planı Elde Et",
@@ -1443,8 +1519,11 @@
"actionSetResourcePincode": "Kaynak PIN Kodunu Ayarla",
"actionSetResourceEmailWhitelist": "Kaynak E-posta Beyaz Listesi Ayarla",
"actionGetResourceEmailWhitelist": "Kaynak E-posta Beyaz Listesini Al",
"actionListResourcePolicies": "Kaynak Politikalarını Listele",
"actionCreateResourcePolicy": "Kaynak Politikası Oluştur",
"actionGetResourcePolicy": "Kaynak Politikasını Al",
"actionUpdateResourcePolicy": "Kaynak Politikasını Güncelle",
"actionDeleteResourcePolicy": "Kaynak Politikasını Sil",
"actionSetResourcePolicyUsers": "Kaynak Politika Kullanıcılarını Ayarla",
"actionSetResourcePolicyRoles": "Kaynak Politika Rolleri Ayarla",
"actionSetResourcePolicyPassword": "Kaynak Politika Şifresini Ayarla",
@@ -1520,6 +1599,8 @@
"search": "Ara…",
"searchPlaceholder": "Ara...",
"emptySearchOptions": "Seçenek bulunamadı",
"ipFilterSearchPlaceholder": "Bir IP adresi girin…",
"ipFilterEmptyMessage": "Filtrelemek için bir IP adresi girin",
"create": "Oluştur",
"orgs": "Organizasyonlar",
"loginError": "Beklenmeyen bir hata oluştu. Lütfen tekrar deneyin.",
@@ -1554,6 +1635,8 @@
"commandPaletteCreateMachineClient": "Makine İstemcisi Oluştur",
"commandPaletteCreateAlertRule": "Uyarı Kuralı Oluştur",
"commandPaletteCreateIdentityProvider": "Kimlik Sağlayıcı Oluştur",
"commandPaletteCreateAiProvider": "Yapay Zeka Sağlayıcısı Oluştur",
"commandPaletteCreateVirtualApiKey": "Sanal API Anahtarı Oluştur",
"commandPaletteToggleTheme": "Temayı Aç/Kapat",
"commandPaletteChooseOrganization": "Organizasyon Seç",
"commandPaletteShortcutMac": "⌘K",
@@ -1616,7 +1699,425 @@
"sidebarInvitations": "Davetiye",
"sidebarRoles": "Roller",
"sidebarShareableLinks": "Paylaşılabilir Bağlantılar",
"sidebarAiGateway": "AI Geçidi",
"sidebarAiProviders": "Sağlayıcılar",
"commandAiProviders": "AI Sağlayıcıları",
"sidebarVirtualApiKeys": "Sanal API Anahtarları",
"sidebarMyApiKeys": "API Anahtarlarınız",
"sidebarAccount": "Başlatıcı",
"commandVirtualApiKeys": "Sanal API Anahtarları",
"virtualApiKeysTitle": "Sanal API Anahtarlarını Yönet",
"virtualApiKeysDescription": "AI Geçitlerine erişim için manuel API anahtarları oluşturun ve yönetin",
"virtualApiKeysTabIdentity": "Kimlik Anahtarları",
"virtualApiKeysTabVirtual": "Sanal Anahtarlar",
"virtualApiKeysIdentitySplashTitle": "Her Kullanıcı İçin Kimlik Anahtarları",
"virtualApiKeysIdentitySplashDescription": "Her kullanıcının bu organizasyon için zaten bir Pangolin kimlik anahtarı var. Hesaplarına özgüdür ve erişebilecekleri AI geçitlerine kimlik doğrulama sağlar.",
"virtualApiKeysIdentitySplashExample": "Örnek",
"virtualApiKeysIdentitySplashRetrieveTitle": "Kullanıcıların Anahtarlarını Nasıl Aldığı",
"virtualApiKeysIdentitySplashRetrieveResource": "Tarayıcıda herkese açık bir AI geçit URL'sini ziyaret edin ve hesaplarıyla giriş yapın.",
"virtualApiKeysIdentitySplashRetrievePage": "Veya giriş yaparken <url></url> adresine gidin.",
"virtualApiKeysIdentitySplashManual": "Sanal Anahtarlar sekmesinde ek sanal API anahtarları oluşturabilirsiniz. Bu anahtarlar belirli herkese açık AI geçitlerine göre kapsamlandırılabilir ve isteğe bağlı olarak bir kullanıcıyla ilişkilendirilebilir. Bir anahtar oluşturmak seçilen kamu AI geçitlerine erişim sağlar.",
"virtualApiKeysIdentitySplashGoToVirtual": "Anahtarı Manuel Olarak Oluştur",
"virtualApiKeysEmailIdentity": "Kimlik Anahtarlarını E-posta Gönder",
"virtualApiKeysEmailIdentityDescription": "Seçilen her kullanıcıya veya role Pangolin kimlik anahtarlarını gönderin.",
"virtualApiKeysEmailIdentitySendAll": "Tüm kullanıcılara gönder",
"virtualApiKeysEmailIdentitySendAllDescription": "Bir hesap e-posta adresi olan her organizasyon üyesine e-posta gönderin.",
"virtualApiKeysEmailIdentitySelectUsers": "Kullanıcılar",
"virtualApiKeysEmailIdentitySelectRoles": "Roller",
"virtualApiKeysEmailIdentitySubmit": "E-postaları Gönder",
"virtualApiKeysEmailIdentitySuccess": "Kimlik anahtarları e-posta ile gönderildi",
"virtualApiKeysEmailIdentitySuccessDescription": "{sent} e-posta gönderildi.",
"virtualApiKeysEmailIdentitySkipped": "E-posta adresine sahip olmayan {skipped} kullanıcılar atlandı.",
"virtualApiKeysEmailIdentityRecipientsRequired": "En az bir kullanıcı veya rol seçin ya da tüm kullanıcılara gönderin.",
"virtualApiKeysEmailIdentityError": "Kimlik anahtarlarını gönderme hatası",
"virtualApiKeysEmailIdentityErrorDescription": "Kimlik anahtarları e-posta ile gönderilemiyor",
"virtualApiKeysBannerTitle": "Her Kullanıcı İçin Kimlik Anahtarları",
"virtualApiKeysBannerDescription": "Her kullanıcı {keysUrl}'de kullanılabilir bir kimlik anahtarına sahiptir. Burada genel AI geçitlerine doğrudan erişim sağlayan anahtarlar da oluşturabilirsiniz.",
"virtualApiKeysBannerButtonText": "Kimlik Anahtarlarını Görüntüle",
"virtualApiKeys": "Sanal API Anahtarları",
"virtualApiKeysSearch": "Anahtarları ara...",
"virtualApiKeysCreate": "Sanal API Anahtarı Oluştur",
"virtualApiKeysCreateDescription": "Bu organizasyonda genel AI geçitlerini arayabilecek manuel bir anahtar mint edin",
"virtualApiKeysCreateButton": "Anahtar Oluştur",
"virtualApiKeysEmpty": "Henüz sanal API anahtarı yok",
"virtualApiKeysName": "Ad",
"virtualApiKeysDescriptionOptional": "Açıklama (isteğe bağlı)",
"virtualApiKeysAssociateUserOptional": "Kullanıcıyla İlişkilendir (isteğe bağlı)",
"virtualApiKeysAssociateUserDescription": "Bu anahtarı bir kullanıcıyla ilişkilendirerek kullanım takibini yapın. Oluşturulduktan hemen sonra bu anahtar seçilen kamu AI geçitlerine erişim sağlar. Anahtar, kullanıcının profilinde de gösterilebilir.",
"virtualApiKeysAllResources": "Tüm kamu AI geçitleri",
"virtualApiKeysAllResourcesDescription": "Bu anahtara organizasyondaki tüm genel AI geçitlerine erişim izni ver",
"virtualApiKeysSelectResources": "Genel AI Geçitleri",
"virtualApiKeysSelectResourcesPlaceholder": "Kaynakları seçin",
"virtualApiKeysSelectResourcesDescription": "Bu anahtarın hangi kamu AI geçitlerine erişebileceğini seçin",
"virtualApiKeysNoResources": "Kaynak yok",
"virtualApiKeysSecret": "Anahtar",
"virtualApiKeysCopyKey": "Bu anahtarı kopyalayın. Tablo veya düzenlemede tekrar görebilirsiniz.",
"virtualApiKeysViewSecret": "Gizliyi Görüntüle",
"virtualApiKeysViewSecretTitle": "Sanal API Anahtarı Gizli",
"virtualApiKeysViewSecretDescription": "Bu gizli, bu anahtara atanmış kamu AI geçitlerine erişim sağlar",
"virtualApiKeysEdit": "Sanal API Anahtarı Düzenle",
"virtualApiKeysEditDescription": "Bu anahtar için ilişkili kullanıcı ve kamu AI geçidine erişimi güncelle",
"virtualApiKeysSaveButton": "Değişiklikleri Kaydet",
"virtualApiKeysSelectResourcesRequired": "En az bir kamu AI geçidi seçin veya tüm kamu AI geçitlerini etkinleştirin",
"virtualApiKeysUpdated": "Sanal API anahtarı güncellendi",
"virtualApiKeysUpdatedDescription": "Sanal API anahtarı güncellendi",
"virtualApiKeysErrorUpdate": "Sanal API anahtarı güncellenirken hata",
"virtualApiKeysErrorUpdateDescription": "Sanal API anahtarı güncellenemedi",
"virtualApiKeysErrorCreate": "Sanal API anahtarı oluşturulurken hata",
"virtualApiKeysErrorCreateDescription": "Sanal API anahtarı oluşturulamadı",
"virtualApiKeysErrorDelete": "Sanal API anahtarı silinirken hata",
"virtualApiKeysErrorDeleteMessage": "Sanal API anahtarı silinemedi",
"virtualApiKeysDeleted": "Sanal API anahtarı silindi",
"virtualApiKeysDeletedDescription": "Sanal API anahtarı silinmiştir",
"virtualApiKeysDelete": "Sanal API Anahtarını Sil",
"virtualApiKeysDeleteConfirm": "Anahtarı Sil",
"virtualApiKeysQuestionRemove": "Bu sanal API anahtarını silmek istediğinizden emin misiniz?",
"virtualApiKeysMessageRemove": "Bu anahtarı kullanan müşteriler hemen erişimlerini kaybedecektir.",
"virtualApiKeysErrorFetchSecret": "Gizli yüklenirken hata",
"virtualApiKeysErrorFetchSecretDescription": "Sanal API anahtarı gizli yüklenemedi",
"virtualApiKeysFilterUnassigned": "Atanmamış",
"virtualApiKeysInferenceBudget": "Bütçe",
"virtualApiKeysInferenceBudgetDescription": "Bu anahtarın AI kullanımını harcama veya jeton sınırlarına göre nasıl kısıtladığını yapılandırın",
"virtualApiKeysEmailOnGenerate": "Anahtar oluşturulduğunda e-posta gönder",
"virtualApiKeysEmailThisKey": "Bu anahtarı e-posta gönder",
"virtualApiKeysEmailOnGenerateDescription": "Anahtar oluşturulduktan sonra ilişkili kullanıcıya ve ek adreslere gönderin",
"virtualApiKeysEmailThisKeyDescription": "Geçerli anahtarı ilişkili kullanıcıya ve ek adreslere gönderin",
"virtualApiKeysEmailSmtpRequired": "Bu sunucuda e-posta yapılandırılmamış",
"virtualApiKeysEmailSmtpRequiredDescription": "SMTP yapılandırmasını yaparak sanal API anahtarlarını e-posta gönderin.",
"virtualApiKeysEmailSendToUser": "İlişkili kullanıcıya gönder",
"virtualApiKeysEmailSendToUserDescription": "Anahtarı ilişkili kullanıcının hesabını alın ve e-posta gönderin",
"virtualApiKeysEmailSendToUserDisabled": "İlişkili bir kullanıcı seçin ve o kullanıcıya anahtarı gönder",
"virtualApiKeysEmailAdditional": "Ek e-postalar",
"virtualApiKeysEmailAdditionalPlaceholder": "E-posta ekle ve Enter tuşuna basın",
"virtualApiKeysEmailRecipientsRequired": "İlişkili kullanıcının hesabını seçin veya en az bir e-posta adresi ekleyin",
"myVirtualApiKeysTitle": "API Anahtarlarınız",
"myVirtualApiKeysDescription": "Bu organizasyonda size atanmış kimlik anahtarınızı ve sanal API anahtarlarınızı görüntüleyin",
"myVirtualApiKeysResourceTitle": "{resourceName} için API Anahtarlarınız",
"myVirtualApiKeysResourceDescription": "{resourceName} erişebileceğiniz kimlik anahtarınızı ve size atanmış sanal API anahtarlarını görüntüleyin",
"myVirtualApiKeysIdentityTitle": "Kimlik Anahtarı",
"myVirtualApiKeysIdentityHeadline": "Kişisel API Anahtarınız",
"myVirtualApiKeysIdentityDescription": "Bu organizasyon için kişisel anahtarınız. Hesabınıza özeldir ve AI Gateway kaynaklarına çağrı yaptığınızda sizi tanımlar.",
"myVirtualApiKeysIdentityResourceHeadline": "{resourceName} için Kişisel API Anahtarınız",
"myVirtualApiKeysIdentityResourceDescription": "Bu organizasyon için kişisel anahtarınız. {resourceName} 'e çağrı yaparken kullanın.",
"myVirtualApiKeysManualTitle": "Atanmış Anahtarlar",
"myVirtualApiKeysManualDescription": "Hesabınıza yönetici tarafından atanmış manuel sanal API anahtarları",
"myVirtualApiKeysManualResourceDescription": "{resourceName} erişebilecek hesabınıza atanmış manuel sanal API anahtarları",
"myVirtualApiKeysManualEmpty": "Henüz atanmış anahtar bulunmamaktadır",
"myVirtualApiKeysKindUser": "Kimlik",
"myVirtualApiKeysKindManual": "Manuel",
"myVirtualApiKeysUnnamed": "İsimsiz anahtar",
"myVirtualApiKeysRevealSecret": "Gizliyi Açıklayın",
"myVirtualApiKeysViewSecretDescription": "Bu gizli, sizi AI Gateway kaynaklarına kimlik doğrular",
"aiClientConfigTitle": "Kodlama Aracılarını Yapılandır",
"aiClientConfigDescription": "Yaygın kodlama aracılarının yapılandırmalarını kopyalayın veya Pangolin CLI sizin için otomatik olarak kurmasını sağlayın.",
"aiClientConfigDescriptionClaude": "Anthropic'in terminal için aracılık kodlama aracı.",
"aiClientConfigDescriptionCodex": "OpenAI'nin terminal için aracılık kodlama aracı.",
"aiClientConfigDescriptionOpencode": "Açık kaynak terminal kodlama aracı.",
"aiClientConfigDescriptionGemini": "Google'un terminal için agentik kodlama aracı.",
"aiClientConfigSetup": "Kurulum",
"aiClientConfigTabCli": "Otomatik (CLI)",
"aiClientConfigTabManual": "Manuel Yapılandırma",
"aiClientConfigPreset": "Yapılandırma",
"aiClientConfigStep": "Adım {number}",
"aiClientConfigEndpointPlaceholder": "https://example.resource.url.com",
"aiClientConfigPlaceholderWarning": "Bu kod parçası, model adı veya kaynak URL'si gibi örnek değerler içerir. Kullanmadan önce bunları kendi değerlerinizle değiştirin.",
"aiClientConfigRevealError": "API anahtarınızı yükleyemedik.",
"aiClientConfigRevealRetry": "Tekrar deneyin",
"resourceGeneralAiClientConfigDescription": "Bu kaynak, Claude Code ve OpenCode gibi yaygın istemcilerden kullanılabilir. <configLink>Daha fazla bilgi edinin</configLink>",
"aiProvidersTitle": "AI Sağlayıcıları",
"aiProvidersDescription": "Bu organizasyonda AI iş yükleri için model sağlayıcılarla bağlantı kurun",
"aiProvidersBannerTitle": "Model Sağlayıcılarla Bağlan",
"aiProvidersBannerDescription": "Sağlayıcılar, Pangolin'in AI iş yükleri için kullandığı model arka uçlarıdır. Burada OpenAI, Anthropic ve diğer sağlayıcıları bağlayın, ardından bu sağlayıcıları genel AI geçitlerine ekleyin, böylece kullanıcılar modelleri kimlik farkındalığı ile erişebilir.",
"aiProvidersAdd": "Sağlayıcı Ekle",
"aiProvidersSearch": "Sağlayıcıları ara...",
"aiProvidersEmpty": "Henüz AI sağlayıcısı yok",
"aiProviderCreate": "AI Sağlayıcı Oluştur",
"aiProviderCreateDescription": "Bu organizasyon için bir model sağlayıcı ekleyin",
"aiProviderSeeAll": "Tüm Sağlayıcıları Gör",
"aiProviderSetting": "{providerName} için Sağlayıcı Ayarları",
"aiProviderSettingDescription": "Bu AI sağlayıcısını yapılandırın",
"aiProviderGeneral": "Genel",
"aiProviderGeneralDescription": "Bu sağlayıcı için temel ayarlar",
"aiProviderConfiguration": "Yapılandırma",
"aiProviderConfigurationDescription": "Bu sağlayıcının ağ yönlendirme ve kimlik doğrulaması",
"aiProviderNetworkSettings": "Ağ Ayarları",
"aiProviderNetworkSettingsDescription": "Trafiğin bu sağlayıcıya nasıl ulaşacağını seçin",
"aiProviderAuthSettings": "Kimlik Doğrulama",
"aiProviderAuthSettingsDescription": "Bu sağlayıcının URL'sine gelen isteklerde kimlik doğrulamalarını yapılandırın",
"aiProviderBudgetSettings": "Bütçe",
"aiProviderBudgetSettingsDescription": "Bu sağlayıcının harcama veya jeton sınırlamalarına dayalı olarak kullanımını nasıl kısıtlayacağını yapılandırın",
"aiBudgetAdd": "Bütçe Ekle",
"aiBudgetEmpty": "Henüz bütçe ayarlanmamış. Harcama veya jeton sınırını ayarlamak için Bütçe Ekle'ye tıklayın.",
"aiBudgetUnit": "Harcama Türü",
"aiBudgetPeriod": "Sıfırlama Dönemi",
"aiBudgetAmount": "Maksimum Harcama",
"aiBudgetAmountPlaceholder": "Maksimum harcama",
"aiBudgetPeriodHourly": "Saatlik",
"aiBudgetPeriodDaily": "Günlük",
"aiBudgetPeriodWeekly": "Haftalık",
"aiBudgetPeriodMonthly": "Aylık",
"aiBudgetPeriodYearly": "Yıllık",
"aiBudgetPeriodLifetime": "Ömür Boyu",
"aiBudgetUnitUsd": "USD",
"aiBudgetUnitTokens": "Jetonlar",
"aiBudgetConflictError": "Bu sıfırlama dönemi ve harcama türü için zaten bir bütçe var",
"aiBudgetInvalidAmountError": "0'dan büyük bir maksimum harcama girin",
"aiBudgetUpdated": "Bütçeler Güncellendi",
"aiBudgetErrorSave": "Bütçeleri güncelleme başarısız",
"aiProviderType": "Sağlayıcı Türü",
"aiProviderTypeSearch": "Sağlayıcıları ara...",
"aiProviderTypeNotFound": "Hiçbir sağlayıcı türü bulunamadı",
"aiProviderTypeOpenai": "OpenAI",
"aiProviderTypeAnthropic": "Anthropic",
"aiProviderTypeGoogleGemini": "Google Gemini",
"aiProviderTypeVertexAi": "Vertex AI",
"aiProviderTypeBedrock": "Amazon Bedrock",
"aiProviderTypeMicrosoftFoundry": "Microsoft Foundry",
"aiProviderTypeOpenRouter": "OpenRouter",
"aiProviderTypeVercelAiGateway": "Vercel AI Geçidi",
"aiProviderTypeCustom": "Özel",
"aiProviderTypeOpenaiDescription": "OpenAI API",
"aiProviderTypeAnthropicDescription": "Anthropic API",
"aiProviderTypeGoogleGeminiDescription": "Google Gemini API",
"aiProviderTypeVertexAiDescription": "Google Vertex AI API",
"aiProviderTypeBedrockDescription": "Amazon Bedrock Runtime API",
"aiProviderTypeMicrosoftFoundryDescription": "Microsoft Foundry API",
"aiProviderTypeOpenRouterDescription": "OpenRouter API",
"aiProviderTypeVercelAiGatewayDescription": "Vercel AI Geçidi API",
"aiProviderTypeCustomDescription": "Kendi uç noktanızı getirin veya site hedefleri üzerinden yönlendirin",
"aiProviderUpstreamUrl": "Yukarı Akış URL'si",
"aiProviderUpstreamUrlDescription": "Sağlayıcı API için Temel URL",
"aiProviderUpstreamUrlOptionalDescription": "Bu sağlayıcı için varsayılan yukarı akış URL'sini kullanmak için boş bırakın",
"aiProviderEffectiveUpstreamUrl": "Etkili Yukarı Akış URL'si",
"aiProviderApiKey": "API Anahtarı",
"aiProviderApiKeyDescription": "Bu sağlayıcıya yapılan istekleri kimlik doğrulamak için kullanılan API anahtarı",
"aiProviderCustomHeadersDescription": "Bu sağlayıcıya yapılan her istekte gönderilen başlıklar. Yeni satır ile ayrılmış: Başlık-Adı: değer",
"aiProviderApiKeyLastChars": "API Anahtarı",
"aiProviderAuthType": "Kimlik Doğrulama Türü",
"aiProviderAuthTypeSearch": "Kimlik doğrulama türlerini ara...",
"aiProviderAuthTypeNotFound": "Kimlik doğrulama türü bulunamadı",
"aiProviderAuthTypeBearer": "Taşıyıcı",
"aiProviderAuthTypeBearerDescription": "Yetkilendirme: Bearer anahtarı. OpenAI ve çoğu sağlayıcı tarafından kullanılır",
"aiProviderAuthTypeXApiKey": "x-api-key",
"aiProviderAuthTypeXApiKeyDescription": "x-api-key başlığı. Anthropic tarafından kullanılır",
"aiProviderAuthTypeXGoogApiKey": "x-goog-api-key",
"aiProviderAuthTypeXGoogApiKeyDescription": "x-goog-api-key başlığı. Google Gemini tarafından kullanılır",
"aiProviderAuthTypeHec": "Splunk HEC",
"aiProviderAuthTypeHecDescription": "Yetkilendirme: Splunk anahtarı. Splunk HTTP Olay Toplayıcı tarafından kullanılır",
"aiProviderAuthTypeCfAigAuthorization": "Cloudflare AI Geçidi",
"aiProviderAuthTypeCfAigAuthorizationDescription": "cf-aig-yetkilendirme: Bearer anahtarı. Cloudflare AI Gateway tarafından kullanılır",
"aiProviderAuthTypeNone": "Kimlik Doğrulama Yok",
"aiProviderAuthTypePassthrough": "Geçir",
"aiProviderAuthTypeDescription": "Yukarıdaki API, istekleri nasıl kimlik doğrular",
"aiProviderAuthTypePassthroughDescription": "Arayanın API anahtarı başlıklarını yukarı akışa iletin",
"aiProviderAuthTypeNoneDescription": "Yukarı akışa kimlik doğrulama başlıkları göndermeyin",
"aiProviderRoutingMode": "Yönlendirme Modu",
"aiProviderRoutingModeDescription": "Trafiği bir yukarı akış URL'sine veya sitenizdeki HTTP hedeflerine gönderin",
"aiProviderRoutingModeUrl": "Yukarı Akış URL'si",
"aiProviderRoutingModeUrlDescription": "Bir kamuya açık ya da özel API temel URL'si çağırın",
"aiProviderRoutingModeTarget": "Site Hedefleri",
"aiProviderRoutingModeTargetDescription": "Siteniz üzerindeki hedefler üzerinden yönlendirin",
"aiProviderRoutingModeTargetNote": "Bu sağlayıcıyı oluşturduktan sonra, site hedeflerini Ağ Ayarları sekmesinde yapılandırın.",
"aiProviderTargetNoOne": "Bu sağlayıcının herhangi bir hedefi yok. Sitemiz üzerinden istekleri yönlendirmek için bir hedef ekleyin.",
"aiProviderRemoteNodeTargetsWarning": "Uzaktaki düğümlere bağlı siteler, AI Geçidi sağlayıcılarına yönlendirilemez durumda.",
"aiProviderSkipTlsVerification": "TLS Doğrulamayı Atla",
"aiProviderSkipTlsVerificationDescription": "Yukarı akış bağlantısı için TLS sertifika doğrulamasını devre dışı bırakın",
"aiProviderBudget": "Bütçe",
"aiProviderBudgetDescription": "Bu sağlayıcı için isteğe bağlı harcama veya jeton bütçesi",
"aiProviderBudgetAmount": "Bütçe Miktarı",
"aiProviderBudgetUnit": "Bütçe Birimi",
"aiProviderBudgetUnitUsd": "USD",
"aiProviderBudgetUnitTokens": "Jetonlar",
"aiProviderEnabled": "Etkin",
"aiProviderEnabledDescription": "Bu sağlayıcıyı tüm kaynaklarda tamamen devre dışı bırakın",
"aiProviderErrorCreate": "AI sağlayıcı oluşturma başarısız",
"aiProviderErrorUpdate": "AI sağlayıcı güncelleme başarısız",
"aiProviderErrorDelete": "AI sağlayıcı silme başarısız",
"aiProviderErrorLoad": "AI sağlayıcı yükleme başarısız",
"aiProviderErrorUpstreamUrlInvalid": "Geçerli bir yukarı akış URL'si girin",
"aiProviderErrorUpstreamUrlRequired": "Bu sağlayıcı için yukarı akış URL'si gereklidir",
"aiProviderErrorAuthTypeRequired": "Kimlik doğrulama türü zorunludur",
"aiProviderErrorApiKeyRequired": "API anahtarı zorunludur",
"aiProviderErrorRoutingModeTarget": "Site hedefleri yönlendirme sadece özel sağlayıcılar için geçerlidir",
"aiProviderErrorCapabilitiesRequired": "En az bir API yetenek seçin",
"aiProviderCapabilities": "API Yetenekleri",
"aiProviderCapabilitiesDescription": "Bu sağlayıcının hangi API formatlarını kullanabileceğini seçin. Bilinen sağlayıcılar, önerilen varsayılanlarla başlar.",
"aiProviderCapabilitiesCustomDescription": "Bu özel sağlayıcının hangi API formatlarını işleyebileceğini seçin",
"aiProviderCapabilitiesSelect": "Yetenekleri seçin",
"aiProviderCapabilitiesEmpty": "Hiçbir yetenek bulunamadı",
"aiProviderCapabilitiesSearch": "Yetenekleri ara...",
"aiCapabilityOpenaiChat": "OpenAI Sohbet Tamamlama",
"aiCapabilityOpenaiChatDescription": "/v1/chat/tamamlama desteği sağlar",
"aiCapabilityOpenaiResponses": "OpenAI Yanıtları",
"aiCapabilityOpenaiResponsesDescription": "/v1/yanıtlar desteği sağlar",
"aiCapabilityAnthropicMessages": "Anthropic Mesajlar",
"aiCapabilityAnthropicMessagesDescription": "/v1/mesajlar desteği sağlar",
"aiCapabilityV1Models": "Modeller Listesi",
"aiCapabilityV1ModelsDescription": "T /v1/models model keşfini destekler",
"aiCapabilityGeminiGenerateContent": "Gemini İçerik Üret",
"aiCapabilityGeminiGenerateContentDescription": "Doğrudan Gemini API desteği sağlar",
"aiCapabilityBedrockModelInvoke": "Bedrock Modeli Çağır",
"aiCapabilityBedrockModelInvokeDescription": "Amazon Bedrock InvokeModel desteği sağlar",
"aiCapabilityGoogleGenerateContent": "Vertex İçerik Üret",
"aiCapabilityGoogleGenerateContentDescription": "Vertex AI Gemini formatı desteği sağlar",
"aiCapabilityGoogleRawPredict": "Vertex Ham Tahmin",
"aiCapabilityGoogleRawPredictDescription": "Vertex AI HamTahmin desteği sağlar",
"aiCapabilityBedrockConverse": "Bedrock Sohbet",
"aiCapabilityBedrockConverseDescription": "Amazon Bedrock Sohbet API desteği sağlar",
"aiProviderCreated": "AI sağlayıcı yaratıldı",
"aiProviderUpdated": "AI sağlayıcı güncellendi",
"aiProviderDeleted": "AI sağlayıcı silindi",
"aiProviderDelete": "Sağlayıcıyı Sil",
"aiProviderDeleteConfirm": "Sağlayıcıyı Sil",
"aiProviderQuestionRemove": "Bu AI sağlayıcıyı silmek istediğinizden emin misiniz?",
"aiProviderMessageRemove": "Bu, sağlayıcıyı ve modellerini ve hedeflerini kalıcı olarak silecektir. Geri alınamaz.",
"aiProviderErrorNoUpdate": "AI sağlayıcı güncellenemiyor",
"aiProviderModels": "Modeller",
"aiProviderModelsDescription": "Bu sağlayıcı için izin ve engelleme listelerini tanımlayın. İstekler bir izin girişini karşılamalı ve bir engelleme girişini karşılamamalıdır.",
"aiProviderCreateModelsDescription": "Bu sağlayıcının hizmet verebileceği modelleri seçin. Boş bir izin listesi tüm trafiği reddeder. Modelleri daha sonra ekleyebilir veya değiştirebilirsiniz.",
"aiProviderModelsPlaceholder": "Modelleri arayın veya özel bir anahtar yazın",
"aiProviderModelsAllow": "İzin Listesi",
"aiProviderModelsAllowDescription": "Bu sağlayıcı aracılığıyla kullanılabilecek modeller. Boş, tümünü reddet anlamına gelir.",
"aiProviderModelsAllowPlaceholder": "Model anahtarını girin",
"aiProviderModelsAllowEmpty": "Henüz model eklenmedi.",
"aiProviderModelsBlock": "Engelleme Listesi",
"aiProviderModelsBlockDescription": "Bir izin girdisiyle eşleşseler bile reddedilecek modeller.",
"aiProviderModelsBlockPlaceholder": "Model anahtarını girin",
"aiProviderModelsBlockEmpty": "Henüz model eklenmedi.",
"aiProviderModelsAdd": "Modelleri Ekle",
"aiProviderModelsClearAll": "Hepsini Temizle",
"aiProviderModelsAddCustom": "\"{key}\" ekle",
"aiProviderModelsAddCustomHint": "Bu özel model anahtarını eklemek için Enter'a basın.",
"aiProviderModelsAddBulk": "{count} özel anahtar ekle",
"aiProviderModelsAddBulkHint": "{count} özel anahtarı eklemek için Enter'a basın.",
"aiProviderModelsAddOne": "Şimdi {key} ekle",
"aiProviderModelsAddSelected": "Seçileni Ekle",
"aiProviderModelsSelectedCount": "{count} seçildi",
"aiProviderModelsSelectAll": "Hepsini seç",
"aiProviderModelsClearSelected": "Temizle",
"aiProviderModelsBulkHint": "Bilinen modelleri seçin veya özel bir anahtar yazın.",
"aiProviderModelsCatalogEmpty": "Eşleşen katalog modeli yok.",
"aiProviderModelsCatalogHeading": "Bilinen Modeller",
"aiProviderModelsAllLabel": "Tüm modeller",
"aiProviderModelsAllPatternHint": "Genel arama deseni: *",
"aiProviderModelsAddAllAllow": "Tüm modellere izin ver",
"aiProviderModelsAddAllBlock": "Tüm modelleri engelle",
"aiProviderModelsAddAllDescription": "* arama desenini kullanarak her model anahtarı eşleşir.",
"aiProviderModelsViewMore": "Daha fazla göster ({count})",
"aiProviderModelsViewLess": "Daha az göster",
"aiProviderModelsRemove": "Modeli kaldır",
"aiProviderModelsEditHint": "Model ayarlarını düzenlemek için tıklayın",
"aiProviderModelsSourceCatalog": "Bilinen katalog modeli",
"aiProviderModelsSourceCustom": "Özel model anahtarı",
"aiProviderModelsSourcePattern": "Genel arama deseni",
"aiProviderModelsSourceAll": "Her model anahtarı ile eşleşir",
"aiProviderModelsBudgetConfigured": "Bütçe yapılandırılmış",
"aiProviderModelsEditTitle": "Modeli düzenle",
"aiProviderModelsEditDescription": "Model anahtarını güncelleyin veya bütçesini yapılandırın.",
"aiProviderModelsBudgetTab": "Bütçe",
"aiProviderModelsKeyLabel": "Model Anahtarı",
"aiProviderModelsKeyRequired": "Bir model anahtarı girin",
"aiProviderModelsKeyDuplicate": "Bu model anahtarı zaten bir listede var",
"aiProviderModelsOverlapError": "Bu desenler her iki listede de olamaz: {keys}",
"aiProviderModelsUpdated": "Modeller güncellendi",
"aiProviderModelsErrorUpdate": "Modeller güncellenemedi",
"aiResourceProviders": "Sağlayıcılar",
"aiResourceProvidersDescription": "Bu AI ağ geçidi kullanabileceği AI sağlayıcılarını seçin",
"aiResourceProvidersHelp": "Sağlayıcıları ekle ve miras almayı (her sağlayıcının listesini kullanmayı) ya da seçmeyi (bu kaynak için bir izin listesini seç) seçin. Birbirine zıt sağlayıcıların izin desenleri izin verilmez.",
"aiResourceProvidersSelect": "Sağlayıcıları seçin",
"aiResourceProvidersEmpty": "Hiçbir AI sağlayıcı bulunamadı",
"aiResourceProvidersNoneAttached": "Henüz hiç sağlayıcı eklenmedi.",
"aiResourceProvidersAdd": "Sağlayıcı ekle",
"aiResourceProvidersRemove": "Sağlayıcıyı kaldır",
"aiResourceProviderToggleEnabled": "Bu kaynaktaki sağlayıcıyı etkinleştir veya devre dışı bırak",
"aiResourceProviderDisabled": "Devre Dışı",
"aiResourceProvidersUpdated": "Sağlayıcılar güncellendi",
"aiResourceProvidersErrorUpdate": "Sağlayıcılar güncellenemedi",
"aiResourceProviderEditDescription": "Bu sağlayıcının modellerinin bu kaynakta nasıl göstereceğini seçin.",
"viewProviderSettings": "Sağlayıcı Ayarlarını Görüntüle",
"aiResourceProviderMode": "Erişim modu",
"aiResourceProviderModeInherit": "Miras Al",
"aiResourceProviderModeSelect": "Seç",
"aiResourceProviderModeSelectSummary": "Seç · {count} modeller",
"aiResourceProviderModeInheritHelp": "Bu sağlayıcıda yapılandırıldığı gibi izin ve engelleme listelerini kullanın.",
"aiResourceProviderModeSelectHelp": "Bu kaynak için bu sağlayıcının izin listesi modellerinin bir alt kümesini seçin.",
"aiResourceProviderAllowModels": "İzin listesi",
"aiResourceProviderAllowModelsSelect": "Modelleri seçin",
"aiResourceProviderAllowModelsSearch": "Modelleri ara...",
"aiResourceProviderAllowModelsEmpty": "Hiçbir model bulunamadı",
"aiResourceProviderAllowModelsHelp": "Sadece sağlayıcının izin listesindeki modeller seçilebilir.",
"aiResourceAliasRequired": "AI ağ geçitleri için takma ad gereklidir",
"aiResourceDomainConfiguration": "Alan Adı Yapılandırması",
"aiResourceDomainConfigurationDescription": "Bu AI ağ geçidine ulaşmak için müşterilerin kullanacağı alan adını seçin.",
"aiUsageAnalyticsTitle": "AI Kullanım Analizi",
"aiUsageAnalyticsDescription": "AI ağ geçidi maliyetini, token kullanımını ve sağlayıcılar, kaynaklar, roller ve kullanıcılar genelindeki aktiviteyi analiz edin",
"aiUsageTabOverview": "Genel Bakış",
"aiUsageTabProviders": "Sağlayıcılar",
"aiUsageTabResources": "Kaynaklar",
"aiUsageFilterProvider": "Sağlayıcı",
"aiUsageFilterModel": "Model",
"aiUsageFilterResource": "Kaynak",
"aiUsageFilterRole": "Rol",
"aiUsageFilterUser": "Kullanıcı",
"aiUsageFilterAllProviders": "Tüm Sağlayıcılar",
"aiUsageFilterAllModels": "Tüm Modeller",
"aiUsageFilterAllResources": "Tüm Kaynaklar",
"aiUsageFilterAllRoles": "Tüm Roller",
"aiUsageFilterAllUsers": "Tüm Kullanıcılar",
"aiUsageFilterSearch": "Ara...",
"aiUsageFilterNotFound": "Hiçbir seçenek bulunamadı",
"aiUsageResetFilters": "Filtreleri Sıfırla",
"aiUsageRefresh": "Yenile",
"aiUsageTokenTypePrompt": "Yazım",
"aiUsageTokenTypeCacheRead": "Önbellek okuma",
"aiUsageTokenTypeCacheWrite": "Önbellek yazma",
"aiUsageTokenTypeCompletion": "Tamamlama",
"aiUsageTokenTypeReasoning": "Muhakeme",
"aiUsageRequests": "İstekler",
"aiUsageCost": "Maliyet",
"aiUsageTokens": "Jetonlar",
"aiUsageOther": "Diğer",
"aiUsageTotalRequests": "Toplam İstekler",
"aiUsageTotalTokens": "Toplam Jetonlar",
"aiUsageTotalCost": "Toplam Maliyet",
"aiUsageEstimated": "Tahmini",
"aiUsageRequestVolume": "İstek Hacmi",
"aiUsageTokenUsage": "Jeton Kullanımı",
"aiUsageModelCost": "Model Maliyeti",
"aiUsageModelTokens": "Model Jetonları",
"aiUsageTopModels": "En İyi Modeller",
"aiUsageTopProviders": "En İyi Sağlayıcılar",
"aiUsageProviderCost": "Sağlayıcı Maliyeti",
"aiUsageProviderTokenUsage": "Sağlayıcı Jeton Kullanımı",
"aiUsageTopResources": "En İyi Kaynaklar",
"aiUsageResourceCost": "Kaynak Maliyeti",
"aiUsageResourceTokenUsage": "Kaynak Jeton Kullanımı",
"aiUsageResourceTypePublic": "Kamusal Kaynak",
"aiUsageResourceTypeSite": "Özel Kaynak",
"aiUsageNoResource": "Kaynak yok",
"aiUsageRolesTab": "Roller",
"aiUsageUsersTab": "Kullanıcılar",
"aiUsageTopRoles": "En İyi Roller",
"aiUsageRoleCost": "Rol Maliyeti",
"aiUsageRoleTokenUsage": "Rol Jeton Kullanımı",
"aiUsageTopUsers": "En İyi Kullanıcılar",
"aiUsageUserCost": "Kullanıcı Maliyeti",
"aiUsageUserTokenUsage": "Kullanıcı Jeton Kullanımı",
"aiUsageUnknownUser": "Bilinmeyen kullanıcı",
"aiUsageVirtualApiKeysTab": "Sanal API Anahtarları",
"aiUsageFilterVirtualApiKey": "Sanal API Anahtarı",
"aiUsageFilterAllVirtualApiKeys": "Tüm Sanal API Anahtarları",
"aiUsageTopVirtualApiKeys": "En İyi Sanal API Anahtarları",
"aiUsageVirtualApiKeyCost": "Sanal API Anahtar Maliyeti",
"aiUsageVirtualApiKeyTokenUsage": "Sanal API Anahtar Jeton Kullanımı",
"aiUsageUnknownVirtualApiKey": "Sanal API anahtarı yok",
"aiUsageUnnamedVirtualApiKey": "İsimsiz anahtar",
"aiUsageLoading": "Yükleniyor...",
"aiUsageNoData": "Veri yok",
"resourceBudgetSettings": "Bütçe",
"resourceBudgetSettingsDescription": "Bu AI ağ geçidi harcama veya jeton limitlerine göre kullanım nasıl sınırlayacağını yapılandırın",
"sidebarApiKeys": "API Anahtarları",
"sidebarOrgs": "Organizasyonlar",
"sidebarProvisioning": "Tedarik",
"sidebarSettings": "Ayarlar",
"sidebarAllUsers": "Tüm Kullanıcılar",
@@ -1689,6 +2190,8 @@
"alertingRuleSaved": "Uyarı kuralı kaydedildi",
"alertingRuleSavedCreatedDescription": "Yeni uyarı kuralınız oluşturuldu. Bu sayfada düzenlemeye devam edebilirsiniz.",
"alertingRuleSavedUpdatedDescription": "Bu uyarı kuralındaki değişiklikleriniz kaydedildi.",
"alertingTestAlertSent": "Test uyarısı gönderildi",
"alertingTestAlertSentDescription": "Bu kurala yapılandırılmış eylemlere bir test uyarısı gönderildi.",
"alertingEditRule": "Uyarı Kuralını Düzenle",
"alertingCreateRule": "Uyarı Kuralı Oluştur",
"alertingRuleCredenzaDescription": "Ne izlenecek, ne zaman tetiklenecek ve nasıl bildirilecek, bunları seçin",
@@ -1798,6 +2301,12 @@
"alertingRulesBannerDescription": "Her kural neyin izleneceğini (bir site, sağlık kontrolü veya kaynak), ne zaman tetikleneceğini (örneğin çevrimdışı veya sağlıksız) ve ekibinize e-posta, web kancaları veya entegrasyonlar aracılığıyla nasıl bildirileceğini bağlar. Bu listeyi kullanarak kuralları oluşturun, etkinleştirin ve yönetin.",
"alertingHealthChecksBannerTitle": "Sağlık ve Kaynakları İzleyin",
"alertingHealthChecksBannerDescription": "Sağlık kontrolleri bir kez tanımladığınız HTTP veya TCP monitörleridir. Ardından hedef sağlıklı veya sağlıksız olduğunda bildirilmeniz için onları uyarı kurallarında kaynak olarak kullanabilirsiniz. Kaynaklar üzerindeki sağlık kontrolleri de burada görünür.",
"alertingTestRule": "Test Uyarı Kuralı",
"alertingAddActionHeading": "Yeni Eylem Ekle",
"alertingSelectActionType": "Bir Eylem Seçin",
"alertingNoActionsTitle": "Eylem yapılandırılmamış",
"alertingNoActionsSaveDescription": "Bu kuralın tetiklendiğinde birini bilgilendirebilmesi için en az bir eylem ekleyin.",
"alertingNoActionsTestDescription": "Bu kuralı test etmeden önce en az bir eylem ekleyin.",
"standaloneHcTableTitle": "Sağlık Kontrolleri",
"standaloneHcSearchPlaceholder": "Sağlık kontrollerini ara…",
"standaloneHcAddButton": "Sağlık Kontrolü Oluştur",
@@ -1989,6 +2498,9 @@
"domainPickerSubdomain": "Alt Alan: {subdomain}",
"domainPickerNamespace": "Ad Alanı: {namespace}",
"domainPickerShowMore": "Daha Fazla Göster",
"domainPickerNoDomainsAvailableTitle": "Kullanılacak alan adı yok",
"domainPickerNoDomainsAvailableDescription": "Henüz ayarlanmış bir alan adınız yok. Devam etmek için bir alan adı oluşturun.",
"domainPickerNoDomainsAvailableAction": "Alan Adlarına Git",
"regionSelectorTitle": "Bölge Seç",
"domainPickerRemoteExitNodeWarning": "Belirtilen alan adları, siteler uzak çıkış düğümlerine bağlandığında desteklenmez. Kaynakların uzak düğümlerde kullanılabilir olması için özel bir alan adı kullanın.",
"regionSelectorInfo": "Bir bölge seçmek, konumunuz için daha iyi performans sağlamamıza yardımcı olur. Sunucunuzla aynı bölgede olmanıza gerek yoktur.",
@@ -2113,6 +2625,7 @@
"createDomainType": "Tür:",
"createDomainName": "Ad:",
"createDomainValue": "Değer:",
"multiSelectFilterCount": "{count} seçildi",
"createDomainCnameRecords": "CNAME Kayıtları",
"createDomainARecords": "A Kayıtları",
"createDomainRecordNumber": "Kayıt {number}",
@@ -2184,6 +2697,8 @@
"subnetPlaceholder": "Alt ağ",
"addressDescription": "İstemcinin dahili adresi. Organizasyon alt ağı içinde olmalıdır.",
"selectSites": "Siteleri seçin",
"selectResources": "Kaynakları seçin",
"multiResourcesSelectorResourcesCount": "{count, plural, one {# kaynak} other {# kaynaklar}}",
"selectLabels": "Etiketleri seçin",
"sitesDescription": "Müşteri seçilen sitelere bağlantı kuracaktır",
"clientInstallOlm": "Olm Yükle",
@@ -2225,6 +2740,7 @@
"healthScheme": "Yöntem",
"healthSelectScheme": "Yöntem Seç",
"healthCheckPortInvalid": "Bağlantı noktası 1 ile 65535 arasında olmalıdır",
"healthCheckHostnameInvalid": "Ana bilgisayar adı boşluk içermemelidir",
"healthCheckPath": "Yol",
"healthHostname": "IP / Hostname",
"healthPort": "Bağlantı Noktası",
@@ -2236,10 +2752,11 @@
"timeIsInSeconds": "Zaman saniye cinsindendir",
"requireDeviceApproval": "Cihaz Onaylarını Gerektir",
"requireDeviceApprovalDescription": "Bu role sahip kullanıcıların yeni cihazlarının bağlanabilmesi ve kaynaklara erişebilmesi için bir yönetici tarafından onaylanması gerekiyor.",
"sshSettings": "SSH Ayarları",
"sshSettings": "SSH",
"inferenceSettings": "AI Ağ Geçidi",
"sshAccess": "SSH Erişimi",
"rdpSettings": "RDP Ayarları",
"vncSettings": "VNC Ayarları",
"rdpSettings": "RDP",
"vncSettings": "VNC",
"sshServer": "SSH Sunucusu",
"rdpServer": "RDP Sunucusu",
"vncServer": "VNC Sunucusu",
@@ -2352,7 +2869,7 @@
"resourcesTableProxyResources": "Herkese Açık",
"resourcesTableClientResources": "Özel",
"resourcesTableNoProxyResourcesFound": "Hiçbir proxy kaynağı bulunamadı.",
"resourcesTableNoInternalResourcesFound": "Hiçbir dahili kaynak bulunamadı.",
"resourcesTableNoInternalResourcesFound": "Özel kaynak bulunamadı.",
"resourcesTableDestination": "Hedef",
"resourcesTableAlias": "Takma Ad",
"resourcesTableAliasAddress": "Alias Adresi",
@@ -2375,9 +2892,9 @@
"editInternalResourceDialogCancel": "İptal",
"editInternalResourceDialogSaveResource": "Kaynağı Kaydet",
"editInternalResourceDialogSuccess": "Başarı",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Dahili kaynak başarıyla güncellendi",
"editInternalResourceDialogInternalResourceUpdatedSuccessfully": "Özel kaynak başarıyla güncellendi",
"editInternalResourceDialogError": "Hata",
"editInternalResourceDialogFailedToUpdateInternalResource": "Dahili kaynak güncellenemedi",
"editInternalResourceDialogFailedToUpdateInternalResource": "Özel kaynak güncellenemedi",
"editInternalResourceDialogNameRequired": "Ad gerekli",
"editInternalResourceDialogNameMaxLength": "Ad 255 karakterden kısa olmalıdır",
"editInternalResourceDialogProxyPortMin": "Proxy bağlantı noktası en az 1 olmalıdır",
@@ -2392,6 +2909,7 @@
"editInternalResourceDialogModeCidr": "CIDR",
"editInternalResourceDialogModeHttp": "HTTP",
"editInternalResourceDialogModeHttps": "HTTPS",
"editInternalResourceDialogModeInference": "AI Ağ Geçidi",
"editInternalResourceDialogModeSsh": "SSH",
"editInternalResourceDialogScheme": "Şema",
"editInternalResourceDialogEnableSsl": "TLS'yi Etkinleştir",
@@ -2403,7 +2921,7 @@
"editInternalResourceDialogAlias": "Takma Ad",
"editInternalResourceDialogAliasDescription": "Bu kaynak için isteğe bağlı dahili DNS takma adı.",
"createInternalResourceDialogNoSitesAvailable": "Site Bulunamadı",
"createInternalResourceDialogNoSitesAvailableDescription": "Dahili kaynak oluşturmak için en az bir Newt sitesine ve alt ağa sahip olmalısınız.",
"createInternalResourceDialogNoSitesAvailableDescription": "Özel kaynaklar oluşturmak için alt ağı yapılandırılmış en az bir Newt sitesine sahip olmalısınız.",
"createInternalResourceDialogClose": "Kapat",
"createInternalResourceDialogCreateClientResource": "Özel Kaynak Oluştur",
"createInternalResourceDialogCreateClientResourceDescription": "Seçilen siteye bağlı istemcilere erişilebilir olacak yeni bir kaynak oluşturun",
@@ -2412,8 +2930,8 @@
"privateResourceAllowIcmpPing": "ICMP Ping İzne Ver",
"privateResourceNetworkAccess": "Ağ Erişimi",
"privateResourceNetworkAccessDescription": "Bu kaynak için TCP/UDP port erişimini kontrol edin ve ICMP ping'in izin verilip verilmediğini kontrol edin.",
"hostSettings": "Sunucu Ayarları",
"cidrSettings": "CIDR Ayarları",
"hostSettings": "Sunucu",
"cidrSettings": "CIDR",
"createInternalResourceDialogResourceProperties": "Kaynak Özellikleri",
"createInternalResourceDialogName": "Ad",
"createInternalResourceDialogSite": "Site",
@@ -2432,9 +2950,9 @@
"createInternalResourceDialogCancel": "İptal",
"createInternalResourceDialogCreateResource": "Kaynak Oluştur",
"createInternalResourceDialogSuccess": "Başarı",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Dahili kaynak başarıyla oluşturuldu",
"createInternalResourceDialogInternalResourceCreatedSuccessfully": "Özel kaynak başarıyla oluşturuldu",
"createInternalResourceDialogError": "Hata",
"createInternalResourceDialogFailedToCreateInternalResource": "Dahili kaynak oluşturulamadı",
"createInternalResourceDialogFailedToCreateInternalResource": "Özel kaynak oluşturulamadı",
"createInternalResourceDialogNameRequired": "Ad gerekli",
"createInternalResourceDialogNameMaxLength": "Ad 255 karakterden kısa olmalıdır",
"createInternalResourceDialogPleaseSelectSite": "Lütfen bir site seçin",
@@ -2451,6 +2969,7 @@
"createInternalResourceDialogModeHttp": "HTTP",
"createInternalResourceDialogModeHttps": "HTTPS",
"createInternalResourceDialogModeSsh": "SSH",
"createInternalResourceDialogModeInference": "AI Ağ Geçidi",
"scheme": "Şema",
"createInternalResourceDialogScheme": "Şema",
"createInternalResourceDialogEnableSsl": "TLS'yi Etkinleştir",
@@ -2505,7 +3024,7 @@
"remoteExitNodeNetworkingSubnetsPlaceholder": "Bir CIDR aralığı ekle (örneğin, 10.0.0.0/8)",
"remoteExitNodeNetworkingSubnetsLoadError": "Alt ağlar yüklenemedi",
"remoteExitNodeNetworkingLabelsTitle": "Tercih Etiketleri",
"remoteExitNodeNetworkingLabelsDescription": "Bu etiketlere sahip siteler, bu uzak çıkış düğümü üzerinden bağlantı kurmaya zorlanacaktır.",
"remoteExitNodeNetworkingLabelsDescription": "Bu etiketlere sahip siteler, bağlantıyı bu uzak çıkış düğümü üzerinden tercih edecektir.",
"remoteExitNodeNetworkingLabelsButtonText": "Etiketleri seç...",
"remoteExitNodeNetworkingLabelsSearchPlaceholder": "Etiketleri ara...",
"remoteExitNodeNetworkingLabelsLoadError": "Etiketler yüklenemedi",
@@ -2982,6 +3501,7 @@
"priority": "Öncelik",
"priorityDescription": "Daha yüksek öncelikli rotalar önce değerlendirilir. Öncelik = 100, otomatik sıralama anlamına gelir (sistem karar verir). Manuel öncelik uygulamak için başka bir numara kullanın.",
"instanceName": "Örnek İsmi",
"clearInstanceName": "Sunucu Birliği Sıfırla",
"pathMatchModalTitle": "Yol Eşleşmesini Yapılandır",
"pathMatchModalDescription": "Gelen isteklerin yolu temel alarak nasıl eşleştirilmesi gerektiğini ayarlayın.",
"pathMatchType": "Eşleşme Türü",
@@ -2992,7 +3512,7 @@
"clear": "Temizle",
"saveChanges": "Değişiklikleri Kaydet",
"pathMatchRegexPlaceholder": "^/api/.*",
"pathMatchDefaultPlaceholder": "/path",
"pathMatchDefaultPlaceholder": "/yol",
"pathMatchPrefixHelp": "Örnek: /api, /api/users vb.'ni eşleştirir.",
"pathMatchExactHelp": "Örnek: /api yalnızca /api'yi eşleştirir",
"pathMatchRegexHelp": "Örnek: ^/api/.* her şeyi eşleştirir /api/anything",
@@ -3042,6 +3562,7 @@
"validPassword": "Geçerli Şifre",
"validEmail": "Geçerli E-posta",
"validSSO": "Geçerli SSO",
"validVirtualAPIKey": "Geçerli Sanal API Anahtarı",
"view": "Görüntüle",
"configManaged": "Yapılandırma Yönetildi",
"connectedClient": "Bağlı İstemci",
@@ -3062,7 +3583,42 @@
"sidebarLogsAction": "Eylem Günlükleri",
"logRetention": "Kayıt Saklama",
"logRetentionDescription": "Bu organizasyon için farklı türdeki günlüklerin ne kadar süre saklanacağını yönetin veya devre dışı bırakın",
"logRetentionDisabledWarningTitle": "Günlük Saklama Devre Dışı Bırakıldı",
"logRetentionDisabledWarningDescription": "{logType} bu organizasyon için saklanmıyor, bu nedenle yeni etkinlikler burada görünmeyecek. Bu günlükleri toplamak için güvenlik ayarlarında saklamayı etkinleştirin.",
"logRetentionDisabledWarningButton": "Güvenlik Ayarlarına Git",
"requestLogsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek günlüklerini görüntüleyin",
"aiSessionLogs": "AI Ağ Geçidi Oturum Günlükleri",
"aiSessionLogsDescription": "Bu organizasyondaki AI ağ geçidi isteklerinin istem ve yanıt transkriptlerini görüntüleyin",
"sidebarLogsAi": "Oturum Günlükleri",
"commandLogsAi": "Oturum Günlükleri",
"sidebarLogsAiUsage": "Kullanım Analizi",
"commandLogsAiUsage": "Kullanım Analizi",
"provider": "Sağlayıcı",
"capability": "Yetenek",
"model": "Model",
"virtualApiKey": "Sanal API Anahtarı",
"noVirtualApiKey": "Sanal API anahtarı yok",
"stream": "Akış",
"streaming": "Akış",
"nonStreaming": "Akış Dışı",
"statusCode": "Durum Kodu",
"aiSessionId": "Oturum ID",
"aiSessionRequest": "İstek",
"aiSessionResponse": "Yanıt",
"aiSessionNoData": "Hiçbir veri yakalanmadı",
"aiSessionCouldNotParse": "(ham, döküm çözümlenemedi)",
"aiSessionLogTruncated": "Bu oturum saklama öncesi kısaltıldı ve eksik olabilir.",
"aiSessionViewRaw": "Ham JSON Görüntüle",
"aiSessionViewChat": "Sohbet Görüntüle",
"cost": "Maliyet",
"estimated": "Tahmini",
"tokenUsage": "Jeton Kullanımı",
"promptTokens": "Yazım Jetonları",
"cacheReadTokens": "Önbellek Okuma Jetonları",
"cacheWriteTokens": "Önbellek Yazma Jetonları",
"completionTokens": "Tamamlama Jetonları",
"reasoningTokens": "Muhakeme Jetonları",
"totalTokens": "Toplam Jetonlar",
"requestAnalyticsDescription": "Bu organizasyondaki kaynaklar için ayrıntılı istek analizlerini görüntüleyin.",
"logRetentionRequestLabel": "HTTP İstek Günlüğü Saklama",
"logRetentionRequestDescription": "İstek günlüklerini ne kadar süre tutacağını belirle",
@@ -3072,6 +3628,8 @@
"logRetentionActionDescription": "Eylem günlüklerini ne kadar süre tutacağını belirle",
"logRetentionConnectionLabel": "Bağlantı kayıtlarını ne kadar süre saklayacağınız",
"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",
"logRetentionDisabled": "Devre Dışı",
"logRetention3Days": "3 gün",
"logRetention7Days": "7 gün",
@@ -3089,8 +3647,8 @@
"sourceAddress": "Kaynak Adresi",
"destinationAddress": "Hedef Adresi",
"duration": "Süre",
"licenseRequiredToUse": "Bu özelliği kullanmak için bir <enterpriseLicenseLink>Enterprise Edition</enterpriseLicenseLink> lisansı veya <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink> gereklidir. <bookADemoLink>Tanıtım veya POC denemesi ayarlayın</bookADemoLink>.",
"ossEnterpriseEditionRequired": "Bu özelliği kullanmak için <enterpriseEditionLink>Enterprise Edition</enterpriseEditionLink> gereklidir. Bu özellik ayrıca <pangolinCloudLink>Pangolin Cloud</pangolinCloudLink>da da mevcuttur. <bookADemoLink>Tanıtım veya POC denemesi ayarlayın</bookADemoLink>.",
"licenseRequiredToUse": "<enterpriseEditionLink>Kurumsal Sürüm lisansı</enterpriseEditionLink> gereklidir. <bookADemoLink>Bir demo veya deneme rezervasyonu yapın</bookADemoLink>",
"ossEnterpriseEditionRequired": "<enterpriseEditionLink>Kurumsal Sürüm lisansı</enterpriseEditionLink> gereklidir. <bookADemoLink>Bir demo veya deneme rezervasyonu yapın</bookADemoLink>",
"certResolver": "Sertifika Çözücü",
"certResolverDescription": "Bu kaynak için kullanılacak sertifika çözücüsünü seçin.",
"selectCertResolver": "Sertifika Çözücü Seçin",
@@ -3233,6 +3791,7 @@
"noData": "Veri Yok",
"machineClients": "Makine İstemcileri",
"install": "Yükle",
"downloadInstaller": "Yükleyiciyi İndir",
"run": "Çalıştır",
"envFile": "Ortam Dosyası",
"serviceFile": "Servis Dosyası",
@@ -3288,9 +3847,9 @@
"internalResourceFormMultiSiteRoutingHelp": "Birden fazla site seçmek, yüksek kullanılabilirlik için dirençli yönlendirme ve yedeklik sağlar.",
"internalResourceFormMultiSiteRoutingHelpLearnMore": "Daha fazla bilgi",
"editInternalResourceDialogPortRestrictionsDescription": "Belirtilen TCP/UDP portlarına erişimi kısıtlayın veya tüm portlara izin/engelleme verin.",
"createInternalResourceDialogHttpConfiguration": "HTTP yapılandırması",
"createInternalResourceDialogHttpConfiguration": "Alan Adı Yapılandırması",
"createInternalResourceDialogHttpConfigurationDescription": "HTTP veya HTTPS üzerinden bu kaynağa ulaşmak için istemcilerin kullanacağı alan adını seçin.",
"editInternalResourceDialogHttpConfiguration": "HTTP yapılandırması",
"editInternalResourceDialogHttpConfiguration": "Alan Adı Yapılandırması",
"editInternalResourceDialogHttpConfigurationDescription": "HTTP veya HTTPS üzerinden bu kaynağa ulaşmak için istemcilerin kullanacağı alan adını seçin.",
"editInternalResourceDialogTcp": "TCP",
"editInternalResourceDialogUdp": "UDP",
@@ -3440,6 +3999,7 @@
"disconnected": "Bağlantı Kesildi",
"approvalsEmptyStateTitle": "Cihaz Onayları Etkin Değil",
"approvalsEmptyStateDescription": "Kullanıcıların yeni cihazlara bağlanabilmeleri için yönetici onayı gerektiren rol cihaz onaylarını etkinleştirin.",
"approvalsEmptyStateHowToTitle": "Nasıl Etkinleştirilir",
"approvalsEmptyStateStep1Title": "Rollere Git",
"approvalsEmptyStateStep1Description": "Cihaz onaylarını yapılandırmak için kuruluşunuzun rol ayarlarına gidin.",
"approvalsEmptyStateStep2Title": "Cihaz Onaylarını Etkinleştir",
@@ -3561,6 +4121,8 @@
"httpDestConnectionLogsDescription": "Site ve tünel bağlantı olayları, bağlantılar ve bağlantı kesilmeleri dahil.",
"httpDestRequestLogsTitle": "HTTP İstek Günlükleri",
"httpDestRequestLogsDescription": "Yönlendirilmiş kaynaklar için HTTP istek kayıtları, yöntem, yol ve yanıt kodu dahil.",
"httpDestAISessionLogsTitle": "AI Oturum Günlükleri",
"httpDestAISessionLogsDescription": "AI geçidi istek ve yanıt oturumları, istemler, model yanıtları ve token kullanımı dahil.",
"httpDestSaveChanges": "Değişiklikleri Kaydet",
"httpDestCreateDestination": "Hedef Oluştur",
"httpDestUpdatedSuccess": "Hedef başarıyla güncellendi",
@@ -3717,6 +4279,11 @@
"resourceLauncherViewAsAdmin": "Yönetici Olarak Görüntüle",
"resourceLauncherResourceDetailsDescription": "Bu kaynağın bağlantı bilgileri ve durumu.",
"resourceLauncherResourceDetails": "Kaynak Detayları",
"resourceLauncherSitesDescription": "Kaynak, aşağıdaki siteler üzerinden erişilebilir.",
"resourceLauncherViewSiteAsAdmin": "Siteyi Yönetici Olarak Görüntüle",
"resourceLauncherFilterBySite": "Siteye Göre Filtrele",
"resourceLauncherSshCommand": "SSH Komutu",
"resourceLauncherSshCommandDescription": "Pangolin CLI kullanarak bu kaynağa bir SSH oturumu açın.",
"resourceLauncherAuthMethodsDescription": "Bu kaynak için etkin kimlik doğrulama yöntemleri.",
"resourceLauncherPrivateClientRequired": "Bu kaynağa özel olarak erişmek için cihazınızda bir istemci ile bağlanın.",
"resourceLauncherPrivateClientRequiredTitle": "İstemci Bağlantısı Gerekli",
@@ -3726,7 +4293,18 @@
"resourceLauncherTcp": "TCP",
"resourceLauncherUdp": "UDP",
"resourceLauncherUnlabeled": "Etiketsiz",
"resourceLauncherAiGateway": "AI Ağ Geçidi",
"resourceLauncherNoSite": "Site Yok",
"resourceLauncherAvailableModels": "Mevcut Modeller",
"resourceLauncherAvailableModelsDescription": "Bu AI ağ geçidiyle kullanabileceğiniz modeller.",
"resourceLauncherAvailableModelsEmpty": "Bu kaynak için hiçbir model mevcut değil.",
"resourceLauncherAvailableModelsError": "Mevcut modeller yüklenemedi.",
"resourceLauncherApiKeys": "API Anahtarları",
"resourceLauncherApiKeysDescription": "Bu kaynakla kimlik doğrulamak için kimlik anahtarınızı veya atanmış bir anahtarı kullanın.",
"resourceLauncherApiKeysIdentity": "Kimlik Anahtarı",
"resourceLauncherApiKeysManual": "Atanmış Anahtarlar",
"resourceLauncherApiKeysEmpty": "Bu kaynak için hiçbir API anahtarı mevcut değil.",
"resourceLauncherApiKeysError": "API anahtarları yüklenemedi.",
"resourceLauncherNoResourcesInGroup": "Bu grupta kaynak yok",
"resourceLauncherEmptyStateTitle": "Kullanılabilir Kaynak Yok",
"resourceLauncherEmptyStateDescription": "Henüz hiçbir kaynağa erişiminiz yok. Erişim istemek için yöneticinizle iletişime geçin.",
@@ -3753,9 +4331,9 @@
"resourceLauncherViewDeleteFailedDescription": "Başlatıcı görünümü silinemedi. Lütfen tekrar deneyin.",
"memberPortalPrevious": "Önceki",
"memberPortalNext": "Sonraki",
"httpSettings": "HTTP Ayarları",
"tcpSettings": "TCP Ayarları",
"udpSettings": "UDP Ayarları",
"httpSettings": "HTTP",
"tcpSettings": "TCP",
"udpSettings": "UDP",
"sshTitle": "SSH",
"sshConnectingDescription": "Güvenli bir bağlantı kuruluyor…",
"sshConnecting": "Bağlanılıyor…",
@@ -3816,5 +4394,6 @@
"rdpUnicodeKeyboardMode": "Unicode klavye modu",
"sessionToolbarShow": "Araç çubuğunu göster",
"sessionToolbarHide": "Araç çubuğunu gizle",
"actionUpdateSiteApprovals": "Site Onaylarını Güncelle"
"actionUpdateSiteApprovals": "Site Onaylarını Güncelle",
"check": "Kontrol Et"
}
+633 -54
View File
File diff suppressed because it is too large Load Diff
+2213 -788
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -34,6 +34,11 @@ const nextConfig: NextConfig = {
source: "/:orgId/settings/resources/client/:path*",
destination: "/:orgId/settings/resources/private/:path*",
permanent: true
},
{
source: "/:orgId/settings/access/users/:userId/access-controls",
destination: "/:orgId/settings/access/users/:userId/general",
permanent: false
}
];
}
+2352 -3734
View File
File diff suppressed because it is too large Load Diff
+77 -73
View File
@@ -32,49 +32,50 @@
"format": "prettier --write ."
},
"dependencies": {
"@asteasolutions/zod-to-openapi": "8.5.0",
"@asteasolutions/zod-to-openapi": "9.1.0",
"@aws-sdk/client-s3": "3.1121.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.1056.0",
"@headlessui/react": "2.2.10",
"@hookform/resolvers": "5.4.0",
"@hookform/resolvers": "5.9.1",
"@monaco-editor/react": "4.7.0",
"@node-rs/argon2": "2.0.2",
"@node-rs/argon2": "2.2.0",
"@novnc/novnc": "^1.7.0",
"@oslojs/crypto": "1.0.1",
"@oslojs/encoding": "1.1.0",
"@radix-ui/react-avatar": "1.1.11",
"@radix-ui/react-checkbox": "1.3.3",
"@radix-ui/react-collapsible": "1.1.12",
"@radix-ui/react-dialog": "1.1.15",
"@radix-ui/react-dropdown-menu": "2.1.16",
"@radix-ui/react-avatar": "1.2.6",
"@radix-ui/react-checkbox": "1.3.11",
"@radix-ui/react-collapsible": "1.1.20",
"@radix-ui/react-dialog": "1.1.23",
"@radix-ui/react-dropdown-menu": "2.1.24",
"@radix-ui/react-icons": "1.3.2",
"@radix-ui/react-label": "2.1.8",
"@radix-ui/react-popover": "1.1.15",
"@radix-ui/react-progress": "1.1.8",
"@radix-ui/react-radio-group": "1.3.8",
"@radix-ui/react-scroll-area": "1.2.10",
"@radix-ui/react-select": "2.2.6",
"@radix-ui/react-separator": "1.1.8",
"@radix-ui/react-slot": "1.2.4",
"@radix-ui/react-switch": "1.2.6",
"@radix-ui/react-tabs": "1.1.13",
"@radix-ui/react-toast": "1.2.15",
"@radix-ui/react-tooltip": "1.2.8",
"@radix-ui/react-label": "2.1.15",
"@radix-ui/react-popover": "1.1.23",
"@radix-ui/react-progress": "1.1.16",
"@radix-ui/react-radio-group": "1.4.7",
"@radix-ui/react-scroll-area": "1.2.18",
"@radix-ui/react-select": "2.3.7",
"@radix-ui/react-separator": "1.1.15",
"@radix-ui/react-slot": "1.3.3",
"@radix-ui/react-switch": "1.3.7",
"@radix-ui/react-tabs": "1.1.21",
"@radix-ui/react-toast": "1.2.23",
"@radix-ui/react-tooltip": "1.2.16",
"@react-email/body": "0.3.0",
"@react-email/components": "1.0.12",
"@react-email/render": "2.0.8",
"@react-email/render": "2.1.0",
"@react-email/tailwind": "2.0.7",
"@simplewebauthn/browser": "13.3.0",
"@simplewebauthn/server": "13.3.1",
"@simplewebauthn/server": "13.3.3",
"@tailwindcss/forms": "0.5.11",
"@tanstack/react-query": "5.100.14",
"@tanstack/react-query": "5.102.8",
"@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.16.1",
"axios": "1.20.0",
"better-sqlite3": "11.9.1",
"canvas-confetti": "1.9.4",
"class-variance-authority": "0.7.1",
@@ -84,103 +85,106 @@
"cors": "2.8.6",
"crypto-js": "4.2.0",
"d3": "7.9.0",
"dns-packet": "^5.6.1",
"drizzle-orm": "0.45.2",
"express": "5.2.1",
"express-rate-limit": "8.5.2",
"express-rate-limit": "8.7.0",
"glob": "13.0.6",
"helmet": "8.2.0",
"gpt-tokenizer": "^4.0.0",
"helmet": "8.3.0",
"http-errors": "2.0.1",
"input-otp": "1.4.2",
"ioredis": "5.11.0",
"input-otp": "1.5.0",
"ioredis": "6.0.0",
"jmespath": "0.16.0",
"js-yaml": "4.2.0",
"js-yaml": "5.4.1",
"jsonwebtoken": "9.0.3",
"lucide-react": "1.17.0",
"maxmind": "5.0.6",
"lru-cache": "11.5.2",
"lucide-react": "1.38.0",
"maxmind": "5.0.7",
"moment": "2.30.1",
"next": "16.2.6",
"next-intl": "4.13.0",
"next": "16.3.3",
"next-intl": "4.14.1",
"next-themes": "0.4.6",
"nextjs-toploader": "3.9.17",
"node-cache": "5.1.2",
"nodemailer": "9.0.1",
"nodemailer": "9.1.0",
"oslo": "1.2.1",
"pg": "8.21.0",
"posthog-node": "5.35.6",
"pg": "8.23.0",
"posthog-node": "5.51.4",
"qrcode.react": "4.2.0",
"react": "19.2.6",
"react": "19.2.8",
"react-day-picker": "9.14.0",
"react-dom": "19.2.6",
"react-dom": "19.2.8",
"react-easy-sort": "1.8.0",
"react-hook-form": "7.76.1",
"react-icons": "5.6.0",
"recharts": "3.8.1",
"react-hook-form": "7.87.0",
"react-icons": "5.7.0",
"recharts": "3.10.1",
"reodotdev": "1.1.0",
"semver": "7.8.1",
"semver": "7.8.5",
"sshpk": "1.18.0",
"stripe": "22.2.0",
"stripe": "22.6.0",
"swagger-ui-express": "5.0.1",
"tailwind-merge": "3.6.0",
"topojson-client": "3.1.0",
"tw-animate-css": "1.4.0",
"use-debounce": "10.1.1",
"uuid": "14.0.0",
"uuid": "14.0.2",
"vaul": "1.1.2",
"visionscarto-world-atlas": "1.0.0",
"winston": "3.19.0",
"winston-daily-rotate-file": "5.0.0",
"ws": "8.21.0",
"ws": "8.21.3",
"yaml": "2.9.0",
"yargs": "18.0.0",
"zod": "4.4.3",
"yargs": "18.1.0",
"zod": "4.5.4",
"zod-validation-error": "5.0.0"
},
"devDependencies": {
"@dotenvx/dotenvx": "1.69.1",
"@dotenvx/dotenvx": "2.23.0",
"@esbuild-plugins/tsconfig-paths": "0.1.2",
"@react-email/ui": "^6.5.0",
"@tailwindcss/postcss": "4.3.0",
"@tanstack/react-query-devtools": "5.100.14",
"@react-email/ui": "^6.9.3",
"@tailwindcss/postcss": "4.3.3",
"@tanstack/react-query-devtools": "5.102.8",
"@types/better-sqlite3": "7.6.13",
"@types/cookie-parser": "1.4.10",
"@types/cors": "2.8.19",
"@types/crypto-js": "4.2.2",
"@types/d3": "7.4.3",
"@types/dns-packet": "^5.6.5",
"@types/express": "5.0.6",
"@types/express-session": "1.19.0",
"@types/jmespath": "0.15.2",
"@types/js-yaml": "4.0.9",
"@types/jsonwebtoken": "9.0.10",
"@types/node": "25.9.1",
"@types/nodemailer": "8.0.0",
"@types/node": "26.4.0",
"@types/nodemailer": "8.0.1",
"@types/nprogress": "0.2.3",
"@types/pg": "8.20.0",
"@types/react": "19.2.15",
"@types/react-dom": "19.2.3",
"@types/semver": "7.7.1",
"@types/sshpk": "1.17.4",
"@types/pg": "8.23.1",
"@types/react": "19.2.18",
"@types/react-dom": "19.2.5",
"@types/semver": "7.8.0",
"@types/sshpk": "1.17.5",
"@types/swagger-ui-express": "4.1.8",
"@types/topojson-client": "3.1.5",
"@types/ws": "8.18.1",
"@types/yargs": "17.0.35",
"babel-plugin-react-compiler": "1.0.0",
"drizzle-kit": "0.31.10",
"esbuild": "0.28.1",
"esbuild-node-externals": "1.22.0",
"eslint": "10.4.0",
"eslint-config-next": "16.2.6",
"postcss": "8.5.15",
"prettier": "3.8.3",
"react-email": "6.5.0",
"tailwindcss": "4.3.0",
"tsc-alias": "1.8.17",
"tsx": "4.22.3",
"esbuild": "0.28.2",
"esbuild-node-externals": "2.0.0",
"eslint": "10.9.1",
"eslint-config-next": "16.3.3",
"postcss": "8.5.26",
"prettier": "3.9.6",
"react-email": "6.9.3",
"tailwindcss": "4.3.3",
"tsc-alias": "1.9.2",
"tsx": "4.23.13",
"typescript": "6.0.3",
"typescript-eslint": "8.60.0"
"typescript-eslint": "8.68.0"
},
"overrides": {
"esbuild": "0.28.1",
"esbuild": "0.28.2",
"dompurify": "3.4.0",
"postcss": "8.5.15"
"postcss": "8.5.26"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 790 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 532 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 621 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 556 KiB

After

Width:  |  Height:  |  Size: 620 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 410 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 516 KiB

After

Width:  |  Height:  |  Size: 802 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg width="256" height="257" viewBox="0 0 256 257" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_29_2)">
<path d="M50.2278 170.321L100.585 142.064L101.428 139.601L100.585 138.24H98.1225L89.6972 137.722L60.9215 136.944L35.9696 135.907L11.7954 134.611L5.70329 133.314L0 125.796L0.583291 122.037L5.70329 118.603L13.0268 119.251L29.2294 120.352L53.5332 122.037L71.1615 123.074L97.28 125.796H101.428L102.011 124.111L100.585 123.074L99.4835 122.037L74.3372 104.992L47.117 86.9752L32.8587 76.6056L25.1463 71.3559L21.2577 66.4304L19.5727 55.6719L26.5722 47.9595L35.9696 48.6076L38.3676 49.2557L47.8947 56.5792L68.2451 72.3281L94.8172 91.9008L98.7058 95.1413L100.261 94.0395L100.456 93.2618L98.7058 90.3453L84.2532 64.2268L68.8284 37.6547L61.9585 26.637L60.1438 20.0263C59.4957 17.3043 59.042 15.0359 59.042 12.2491L67.0137 1.42582L71.4208 0L82.0496 1.42582L86.5215 5.31443L93.1321 20.4152L103.826 44.2005L120.417 76.5408L125.278 86.1327L127.87 95.0116L128.843 97.7337H130.528V96.1782L131.889 77.9666L134.416 55.6071L136.879 26.8314L137.722 18.7301L141.74 9.00861L149.711 3.75899L155.933 6.74025L161.053 14.0638L160.34 18.7949L157.294 38.562L151.332 69.5413L147.443 90.2805H149.711L152.304 87.6881L162.803 73.7539L180.431 51.7185L188.209 42.9691L197.282 33.3124L203.115 28.7109H214.133L222.234 40.7656L218.605 53.2091L207.263 67.597L197.865 79.7813L184.385 97.9281L175.959 112.446L176.737 113.612L178.746 113.418L209.207 106.937L225.669 103.955L245.306 100.585L254.185 104.733L255.157 108.946L251.658 117.566L230.659 122.75L206.031 127.676L169.349 136.361L168.895 136.685L169.414 137.333L185.94 138.888L193.005 139.277H210.309L242.52 141.675L250.945 147.249L256 154.054L255.157 159.238L242.195 165.849L224.697 161.701L183.866 151.98L169.867 148.48H167.923V149.647L179.589 161.053L200.976 180.367L227.743 205.254L229.104 211.411L225.669 216.271L222.039 215.753L198.513 198.06L189.44 190.088L168.895 172.784H167.534V174.598L172.265 181.533L197.282 219.123L198.578 230.659L196.764 234.418L190.283 236.687L183.153 235.39L168.506 214.846L153.406 191.708L141.221 170.969L139.731 171.812L132.537 249.26L129.167 253.213L121.389 256.194L114.908 251.269L111.473 243.297L114.908 227.548L119.056 207.004L122.426 190.671L125.472 170.386L127.287 163.646L127.157 163.192L125.667 163.386L110.372 184.385L87.1048 215.818L68.6987 235.52L64.2916 237.27L56.6441 233.316L57.357 226.252L61.6344 219.966L87.1048 187.561L102.465 167.469L112.381 155.868L112.316 154.183H111.733L44.0709 198.125L32.0162 199.68L26.8314 194.819L27.4795 186.848L29.9423 184.255L50.2927 170.256L50.2278 170.321Z" fill="#0B0B0B"/>
</g>
<defs>
<clipPath id="clip0_29_2">
<rect width="256" height="257" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+10
View File
@@ -0,0 +1,10 @@
<svg width="256" height="257" viewBox="0 0 256 257" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_29_2)">
<path d="M50.2278 170.321L100.585 142.064L101.428 139.601L100.585 138.24H98.1225L89.6972 137.722L60.9215 136.944L35.9696 135.907L11.7954 134.611L5.70329 133.314L0 125.796L0.583291 122.037L5.70329 118.603L13.0268 119.251L29.2294 120.352L53.5332 122.037L71.1615 123.074L97.28 125.796H101.428L102.011 124.111L100.585 123.074L99.4835 122.037L74.3372 104.992L47.117 86.9752L32.8587 76.6056L25.1463 71.3559L21.2577 66.4304L19.5727 55.6719L26.5722 47.9595L35.9696 48.6076L38.3676 49.2557L47.8947 56.5792L68.2451 72.3281L94.8172 91.9008L98.7058 95.1413L100.261 94.0395L100.456 93.2618L98.7058 90.3453L84.2532 64.2268L68.8284 37.6547L61.9585 26.637L60.1438 20.0263C59.4957 17.3043 59.042 15.0359 59.042 12.2491L67.0137 1.42582L71.4208 0L82.0496 1.42582L86.5215 5.31443L93.1321 20.4152L103.826 44.2005L120.417 76.5408L125.278 86.1327L127.87 95.0116L128.843 97.7337H130.528V96.1782L131.889 77.9666L134.416 55.6071L136.879 26.8314L137.722 18.7301L141.74 9.00861L149.711 3.75899L155.933 6.74025L161.053 14.0638L160.34 18.7949L157.294 38.562L151.332 69.5413L147.443 90.2805H149.711L152.304 87.6881L162.803 73.7539L180.431 51.7185L188.209 42.9691L197.282 33.3124L203.115 28.7109H214.133L222.234 40.7656L218.605 53.2091L207.263 67.597L197.865 79.7813L184.385 97.9281L175.959 112.446L176.737 113.612L178.746 113.418L209.207 106.937L225.669 103.955L245.306 100.585L254.185 104.733L255.157 108.946L251.658 117.566L230.659 122.75L206.031 127.676L169.349 136.361L168.895 136.685L169.414 137.333L185.94 138.888L193.005 139.277H210.309L242.52 141.675L250.945 147.249L256 154.054L255.157 159.238L242.195 165.849L224.697 161.701L183.866 151.98L169.867 148.48H167.923V149.647L179.589 161.053L200.976 180.367L227.743 205.254L229.104 211.411L225.669 216.271L222.039 215.753L198.513 198.06L189.44 190.088L168.895 172.784H167.534V174.598L172.265 181.533L197.282 219.123L198.578 230.659L196.764 234.418L190.283 236.687L183.153 235.39L168.506 214.846L153.406 191.708L141.221 170.969L139.731 171.812L132.537 249.26L129.167 253.213L121.389 256.194L114.908 251.269L111.473 243.297L114.908 227.548L119.056 207.004L122.426 190.671L125.472 170.386L127.287 163.646L127.157 163.192L125.667 163.386L110.372 184.385L87.1048 215.818L68.6987 235.52L64.2916 237.27L56.6441 233.316L57.357 226.252L61.6344 219.966L87.1048 187.561L102.465 167.469L112.381 155.868L112.316 154.183H111.733L44.0709 198.125L32.0162 199.68L26.8314 194.819L27.4795 186.848L29.9423 184.255L50.2927 170.256L50.2278 170.321Z" fill="#F0EFEC"/>
</g>
<defs>
<clipPath id="clip0_29_2">
<rect width="256" height="257" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.7 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 413 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 413 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="716" height="716" viewBox="157 157 402 402" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M508.749 317.399C516.777 287.314 508.991 253.884 485.389 230.282C461.788 206.681 428.36 198.895 398.273 206.923C376.231 184.928 343.39 174.956 311.148 183.596C278.906 192.234 255.45 217.292 247.36 247.361C217.291 255.451 192.233 278.91 183.595 311.149C174.957 343.391 184.927 376.232 206.924 398.274C198.896 428.359 206.683 461.789 230.284 485.391C253.885 508.992 287.313 516.779 317.401 508.75C339.442 530.745 372.286 540.717 404.525 532.079C436.767 523.441 460.223 498.384 468.313 468.315C498.383 460.224 523.44 436.766 532.078 404.526C540.716 372.285 530.747 339.443 508.749 317.402V317.399ZM470.899 244.776C486.892 260.77 493.488 282.601 490.687 303.412L415.577 260.046C412.411 258.218 408.509 258.218 405.345 260.046L317.401 310.82V277.526C317.401 275.191 318.652 273.005 320.676 271.837L387.644 233.174C414.178 218.353 448.346 222.223 470.901 244.776H470.899ZM357.837 311.144L398.275 334.491V381.185L357.837 404.532L317.398 381.185V334.491L357.837 311.144ZM264.776 269.693C265.207 239.305 285.644 211.649 316.453 203.393C338.3 197.54 360.505 202.744 377.127 215.573L302.014 258.937C298.848 260.764 296.898 264.144 296.898 267.798V369.346L268.065 352.699C266.043 351.531 264.776 349.353 264.776 347.017V269.691V269.693ZM203.391 316.454C209.244 294.608 224.854 277.978 244.276 269.999V356.73C244.276 360.384 246.226 363.763 249.392 365.591L337.337 416.365L308.503 433.013C306.481 434.181 303.961 434.188 301.939 433.02L234.971 394.357C208.868 378.789 195.138 347.261 203.391 316.454ZM244.775 470.9C228.781 454.906 222.186 433.075 224.986 412.264L300.096 455.63C303.263 457.457 307.164 457.457 310.328 455.63L398.273 404.856V438.149C398.273 440.485 397.022 442.671 394.997 443.839L328.029 482.502C301.495 497.322 267.327 493.452 244.772 470.9H244.775ZM450.897 445.982C450.466 476.371 430.029 504.027 399.22 512.283C377.373 518.136 355.168 512.932 338.547 500.102L413.659 456.738C416.826 454.911 418.775 451.532 418.775 447.877V346.329L447.609 362.977C449.631 364.145 450.897 366.323 450.897 368.659V445.985V445.982ZM512.282 399.221C506.429 421.068 490.819 437.697 471.397 445.676V358.946C471.397 355.292 469.448 351.912 466.281 350.085L378.336 299.311L407.17 282.663C409.192 281.495 411.712 281.487 413.734 282.655L480.702 321.318C506.805 336.887 520.536 368.415 512.282 399.221Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+3
View File
@@ -0,0 +1,3 @@
<svg width="716" height="716" viewBox="157 157 402 402" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M508.749 317.399C516.777 287.314 508.991 253.884 485.389 230.282C461.788 206.681 428.36 198.895 398.273 206.923C376.231 184.928 343.39 174.956 311.148 183.596C278.906 192.234 255.45 217.292 247.36 247.361C217.291 255.451 192.233 278.91 183.595 311.149C174.957 343.391 184.927 376.232 206.924 398.274C198.896 428.359 206.683 461.789 230.284 485.391C253.885 508.992 287.313 516.779 317.401 508.75C339.442 530.745 372.286 540.717 404.525 532.079C436.767 523.441 460.223 498.384 468.313 468.315C498.383 460.224 523.44 436.766 532.078 404.526C540.716 372.285 530.747 339.443 508.749 317.402V317.399ZM470.899 244.776C486.892 260.77 493.488 282.601 490.687 303.412L415.577 260.046C412.411 258.218 408.509 258.218 405.345 260.046L317.401 310.82V277.526C317.401 275.191 318.652 273.005 320.676 271.837L387.644 233.174C414.178 218.353 448.346 222.223 470.901 244.776H470.899ZM357.837 311.144L398.275 334.491V381.185L357.837 404.532L317.398 381.185V334.491L357.837 311.144ZM264.776 269.693C265.207 239.305 285.644 211.649 316.453 203.393C338.3 197.54 360.505 202.744 377.127 215.573L302.014 258.937C298.848 260.764 296.898 264.144 296.898 267.798V369.346L268.065 352.699C266.043 351.531 264.776 349.353 264.776 347.017V269.691V269.693ZM203.391 316.454C209.244 294.608 224.854 277.978 244.276 269.999V356.73C244.276 360.384 246.226 363.763 249.392 365.591L337.337 416.365L308.503 433.013C306.481 434.181 303.961 434.188 301.939 433.02L234.971 394.357C208.868 378.789 195.138 347.261 203.391 316.454ZM244.775 470.9C228.781 454.906 222.186 433.075 224.986 412.264L300.096 455.63C303.263 457.457 307.164 457.457 310.328 455.63L398.273 404.856V438.149C398.273 440.485 397.022 442.671 394.997 443.839L328.029 482.502C301.495 497.322 267.327 493.452 244.772 470.9H244.775ZM450.897 445.982C450.466 476.371 430.029 504.027 399.22 512.283C377.373 518.136 355.168 512.932 338.547 500.102L413.659 456.738C416.826 454.911 418.775 451.532 418.775 447.877V346.329L447.609 362.977C449.631 364.145 450.897 366.323 450.897 368.659V445.985V445.982ZM512.282 399.221C506.429 421.068 490.819 437.697 471.397 445.676V358.946C471.397 355.292 469.448 351.912 466.281 350.085L378.336 299.311L407.17 282.663C409.192 281.495 411.712 281.487 413.734 282.655L480.702 321.318C506.805 336.887 520.536 368.415 512.282 399.221Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

+1
View File
@@ -0,0 +1 @@
<svg width='240' height='300' viewBox='0 0 240 300' fill='none' xmlns='http://www.w3.org/2000/svg'><g clip-path='url(#clip0_1401_86274)'><mask id='mask0_1401_86274' style='mask-type:luminance' maskUnits='userSpaceOnUse' x='0' y='0' width='240' height='300'><path d='M240 0H0V300H240V0Z' fill='white'/></mask><g mask='url(#mask0_1401_86274)'><path d='M180 240H60V120H180V240Z' fill='#CFCECD'/><path d='M180 60H60V240H180V60ZM240 300H0V0H240V300Z' fill='#211E1E'/></g></g><defs><clipPath id='clip0_1401_86274'><rect width='240' height='300' fill='white'/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 577 B

+1
View File
@@ -0,0 +1 @@
<svg width='240' height='300' viewBox='0 0 240 300' fill='none' xmlns='http://www.w3.org/2000/svg'><g clip-path='url(#clip0_1401_86283)'><mask id='mask0_1401_86283' style='mask-type:luminance' maskUnits='userSpaceOnUse' x='0' y='0' width='240' height='300'><path d='M240 0H0V300H240V0Z' fill='white'/></mask><g mask='url(#mask0_1401_86283)'><path d='M180 240H60V120H180V240Z' fill='#4B4646'/><path d='M180 60H60V240H180V60ZM240 300H0V0H240V300Z' fill='#F1ECEC'/></g></g><defs><clipPath id='clip0_1401_86283'><rect width='240' height='300' fill='white'/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 577 B

+41
View File
@@ -0,0 +1,41 @@
import express from "express";
import helmet from "helmet";
import cors from "cors";
import config from "@server/lib/config";
import logger from "@server/logger";
import {
errorHandlerMiddleware,
notFoundMiddleware
} from "@server/middlewares";
import { createAiGatewayRouter } from "@server/routers/aiGateway";
const aiGatewayPort = config.getRawConfig().server.ai_gateway_port;
export function createAiGatewayServer() {
const aiGatewayServer = express();
const trustProxy = config.getRawConfig().server.trust_proxy;
if (trustProxy) {
aiGatewayServer.set("trust proxy", trustProxy);
}
aiGatewayServer.use(helmet());
aiGatewayServer.use(cors());
// AI requests can carry large payloads (long conversation history, tool
// results, embedded documents), well beyond express.json()'s 100kb default.
aiGatewayServer.use(express.json({ limit: "50mb" }));
aiGatewayServer.use(createAiGatewayRouter());
aiGatewayServer.use(notFoundMiddleware);
aiGatewayServer.use(errorHandlerMiddleware);
aiGatewayServer.listen(aiGatewayPort, (err?: any) => {
if (err) throw err;
logger.info(
`AI gateway server is running on http://localhost:${aiGatewayPort}`
);
});
return aiGatewayServer;
}
+24 -1
View File
@@ -50,6 +50,8 @@ export enum ActionsEnum {
setResourceUsers = "setResourceUsers",
setResourceRoles = "setResourceRoles",
listResourceUsers = "listResourceUsers",
listResourceAiModels = "listResourceAiModels",
setResourceAiModels = "setResourceAiModels",
// removeRoleSite = "removeRoleSite",
// addRoleAction = "addRoleAction",
// removeRoleAction = "removeRoleAction",
@@ -151,6 +153,7 @@ export enum ActionsEnum {
createAlertRule = "createAlertRule",
updateAlertRule = "updateAlertRule",
deleteAlertRule = "deleteAlertRule",
testAlertRule = "testAlertRule",
listAlertRules = "listAlertRules",
listOrgLabels = "listOrgLabels",
createOrgLabel = "createOrgLabel",
@@ -182,7 +185,27 @@ export enum ActionsEnum {
setResourcePolicyHeaderAuth = "setResourcePolicyHeaderAuth",
setResourcePolicyWhitelist = "setResourcePolicyWhitelist",
setResourcePolicyRules = "setResourcePolicyRules",
createOrgWideLauncherView = "createOrgWideLauncherView"
createOrgWideLauncherView = "createOrgWideLauncherView",
createAiProvider = "createAiProvider",
deleteAiProvider = "deleteAiProvider",
getAiProvider = "getAiProvider",
listAiProviders = "listAiProviders",
updateAiProvider = "updateAiProvider",
createAiModel = "createAiModel",
deleteAiModel = "deleteAiModel",
getAiModel = "getAiModel",
listAiModels = "listAiModels",
updateAiModel = "updateAiModel",
createAiBudget = "createAiBudget",
deleteAiBudget = "deleteAiBudget",
getAiBudget = "getAiBudget",
listAiBudgets = "listAiBudgets",
updateAiBudget = "updateAiBudget",
createVirtualApiKey = "createVirtualApiKey",
deleteVirtualApiKey = "deleteVirtualApiKey",
getVirtualApiKey = "getVirtualApiKey",
listVirtualApiKeys = "listVirtualApiKeys",
updateVirtualApiKey = "updateVirtualApiKey"
}
export async function checkUserActionPermission(
+311
View File
@@ -0,0 +1,311 @@
import { canUserAccessResource } from "@server/auth/canUserAccessResource";
import {
db,
users,
virtualApiKeyResources,
virtualApiKeys,
type VirtualApiKey
} from "@server/db";
import config from "@server/lib/config";
import {
decryptVirtualApiKeyToken,
VIRTUAL_API_KEY_PREFIX,
looksLikeVirtualApiKeyCredential
} from "@server/lib/virtualApiKey";
import { getUserOrgRoles } from "@server/lib/userOrgRoles";
import { and, eq } from "drizzle-orm";
import { isWithinExpirationDate } from "oslo";
export type VirtualApiKeyCredential = {
virtualApiKeyId: string;
secret: string;
};
export type VirtualApiKeyUserData = {
userId: string;
username: string;
email: string | null;
name: string | null;
role: string | null;
};
function getHeader(
headers: Record<string, string> | undefined,
name: string
): string | undefined {
if (!headers) {
return undefined;
}
if (headers[name] !== undefined) {
return headers[name];
}
const lower = name.toLowerCase();
for (const [key, value] of Object.entries(headers)) {
if (key.toLowerCase() === lower) {
return value;
}
}
return undefined;
}
function parseVkCredential(
raw: string | undefined
): VirtualApiKeyCredential | null {
if (!raw || !looksLikeVirtualApiKeyCredential(raw)) {
return null;
}
const withoutPrefix = raw.trim().slice(VIRTUAL_API_KEY_PREFIX.length);
const dot = withoutPrefix.indexOf(".");
return {
virtualApiKeyId: withoutPrefix.slice(0, dot),
secret: withoutPrefix.slice(dot + 1)
};
}
export function extractVirtualApiKeyCredential(
headers: Record<string, string> | undefined
): VirtualApiKeyCredential | null {
if (!headers) {
return null;
}
const authorization = getHeader(headers, "authorization");
if (authorization) {
const bearerMatch = authorization.match(/^Bearer\s+(.+)$/i);
if (bearerMatch) {
const credential = parseVkCredential(bearerMatch[1]);
if (credential) {
return credential;
}
}
const splunkMatch = authorization.match(/^Splunk\s+(.+)$/i);
if (splunkMatch) {
const credential = parseVkCredential(splunkMatch[1]);
if (credential) {
return credential;
}
}
}
const cfAig = getHeader(headers, "cf-aig-authorization");
if (cfAig) {
const bearerMatch = cfAig.match(/^Bearer\s+(.+)$/i);
const credential = parseVkCredential(
bearerMatch ? bearerMatch[1] : cfAig
);
if (credential) {
return credential;
}
}
for (const name of ["x-api-key", "x-goog-api-key"] as const) {
const credential = parseVkCredential(getHeader(headers, name));
if (credential) {
return credential;
}
}
return null;
}
async function buildUserData(
userId: string,
orgId: string
): Promise<VirtualApiKeyUserData | undefined> {
const [user] = await db
.select()
.from(users)
.where(eq(users.userId, userId))
.limit(1);
if (!user) {
return undefined;
}
if (
config.getRawConfig().flags?.require_email_verification &&
!user.emailVerified
) {
return undefined;
}
const userOrgRoles = await getUserOrgRoles(user.userId, orgId);
if (userOrgRoles.length === 0) {
return undefined;
}
return {
userId: user.userId,
username: user.username,
email: user.email,
name: user.name,
role: userOrgRoles.map((r) => r.roleName).join(", ") || null
};
}
async function userHasResourceAccess(
userId: string,
resourceId: number,
orgId: string
): Promise<{ allowed: boolean; userData?: VirtualApiKeyUserData }> {
const [user] = await db
.select()
.from(users)
.where(eq(users.userId, userId))
.limit(1);
if (!user) {
return { allowed: false };
}
if (
config.getRawConfig().flags?.require_email_verification &&
!user.emailVerified
) {
return { allowed: false };
}
const userOrgRoles = await getUserOrgRoles(user.userId, orgId);
if (userOrgRoles.length === 0) {
return { allowed: false };
}
const allowed = await canUserAccessResource({
userId,
resourceId,
roleIds: userOrgRoles.map((r) => r.roleId)
});
if (!allowed) {
return { allowed: false };
}
return {
allowed: true,
userData: {
userId: user.userId,
username: user.username,
email: user.email,
name: user.name,
role: userOrgRoles.map((r) => r.roleName).join(", ") || null
}
};
}
async function manualKeyHasResourceAccess(
key: VirtualApiKey,
resourceId: number
): Promise<boolean> {
if (key.allResources) {
return true;
}
const [row] = await db
.select({ resourceId: virtualApiKeyResources.resourceId })
.from(virtualApiKeyResources)
.where(
and(
eq(virtualApiKeyResources.virtualApiKeyId, key.virtualApiKeyId),
eq(virtualApiKeyResources.resourceId, resourceId)
)
)
.limit(1);
return Boolean(row);
}
async function touchLastUsedAt(virtualApiKeyId: string): Promise<void> {
try {
await db
.update(virtualApiKeys)
.set({ lastUsedAt: Date.now() })
.where(eq(virtualApiKeys.virtualApiKeyId, virtualApiKeyId));
} catch {
// Best-effort; do not fail auth on audit timestamp updates.
}
}
export async function verifyVirtualApiKey({
credential,
resourceId,
orgId
}: {
credential: VirtualApiKeyCredential;
resourceId: number;
orgId: string;
}): Promise<{
valid: boolean;
error?: string;
key?: VirtualApiKey;
userData?: VirtualApiKeyUserData;
}> {
const [key] = await db
.select()
.from(virtualApiKeys)
.where(eq(virtualApiKeys.virtualApiKeyId, credential.virtualApiKeyId))
.limit(1);
if (!key) {
return { valid: false, error: "Virtual API key not found" };
}
if (key.orgId !== orgId) {
return { valid: false, error: "Virtual API key org mismatch" };
}
let plaintext: string;
try {
plaintext = decryptVirtualApiKeyToken(key.token);
} catch {
return { valid: false, error: "Virtual API key secret is invalid" };
}
if (plaintext !== credential.secret) {
return { valid: false, error: "Invalid virtual API key secret" };
}
if (key.expiresAt && !isWithinExpirationDate(new Date(key.expiresAt))) {
return { valid: false, error: "Virtual API key has expired" };
}
if (key.kind === "manual") {
const scoped = await manualKeyHasResourceAccess(key, resourceId);
if (!scoped) {
return {
valid: false,
error: "Virtual API key is not scoped to this resource"
};
}
let userData: VirtualApiKeyUserData | undefined;
if (key.userId) {
userData = await buildUserData(key.userId, orgId);
}
await touchLastUsedAt(key.virtualApiKeyId);
return { valid: true, key, userData };
}
if (key.kind === "user") {
if (!key.userId) {
return { valid: false, error: "User virtual API key has no user" };
}
const access = await userHasResourceAccess(
key.userId,
resourceId,
orgId
);
if (!access.allowed || !access.userData) {
return {
valid: false,
error: "User is not allowed to access this resource"
};
}
await touchLastUsedAt(key.virtualApiKeyId);
return { valid: true, key, userData: access.userData };
}
return { valid: false, error: "Unknown virtual API key kind" };
}
+8
View File
@@ -0,0 +1,8 @@
export async function startCertificateManager() {
// No-op: ACME certificate generation/management is only available in
// builds that include the private/enterprise feature set.
}
export async function stopCertificateManager() {
// No-op counterpart to startCertificateManager.
}
+4
View File
@@ -3,12 +3,16 @@ import { flushConnectionLogToDb } from "#dynamic/routers/newt";
import { flushSiteBandwidthToDb } from "@server/routers/gerbil/receiveBandwidth";
import { stopPingAccumulator } from "@server/routers/newt/pingAccumulator";
import { cleanup as wsCleanup } from "#dynamic/routers/ws";
import { shutdownUsageRecorder } from "@server/lib/aiBudgetEnforcement";
import { shutdownAiSessionLogger } from "@server/routers/aiGateway/logAiSession";
async function cleanup() {
await stopPingAccumulator();
await flushBandwidthToDb();
await flushConnectionLogToDb();
await flushSiteBandwidthToDb();
await shutdownUsageRecorder();
await shutdownAiSessionLogger();
await wsCleanup();
process.exit(0);
+27
View File
@@ -1,6 +1,7 @@
import { join } from "path";
import { readFileSync } from "fs";
import {
aiProviders,
clients,
db,
resourcePolicies,
@@ -113,6 +114,32 @@ export async function getUniqueResourceName(orgId: string): Promise<string> {
}
}
export async function getUniqueProviderName(orgId: string): Promise<string> {
let loops = 0;
while (true) {
if (loops > 100) {
throw new Error("Could not generate a unique name");
}
const name = generateName();
const aiProviderCount = await db
.select({
niceId: aiProviders.niceId,
orgId: aiProviders.orgId
})
.from(aiProviders)
.where(
and(eq(aiProviders.niceId, name), eq(aiProviders.orgId, orgId))
);
if (aiProviderCount.length === 0) {
return name;
}
loops++;
}
}
export async function getUniqueResourcePolicyName(
orgId: string
): Promise<string> {
+90 -22
View File
@@ -26,28 +26,11 @@ import {
sites,
clients,
sessions,
labels
labels,
aiProviders,
virtualApiKeys
} from "./schema";
export const certificates = pgTable("certificates", {
certId: serial("certId").primaryKey(),
domain: varchar("domain", { length: 255 }).notNull().unique(),
domainId: varchar("domainId").references(() => domains.domainId, {
onDelete: "cascade"
}),
wildcard: boolean("wildcard").default(false),
status: varchar("status", { length: 50 }).notNull().default("pending"), // pending, requested, valid, expired, failed
expiresAt: bigint("expiresAt", { mode: "number" }),
lastRenewalAttempt: bigint("lastRenewalAttempt", { mode: "number" }),
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
updatedAt: bigint("updatedAt", { mode: "number" }).notNull(),
orderId: varchar("orderId", { length: 500 }),
errorMessage: text("errorMessage"),
renewalCount: integer("renewalCount").default(0),
certFile: text("certFile"),
keyFile: text("keyFile")
});
export const dnsChallenge = pgTable("dnsChallenges", {
dnsChallengeId: serial("dnsChallengeId").primaryKey(),
domain: varchar("domain", { length: 255 }).notNull(),
@@ -95,7 +78,8 @@ export const subscriptions = pgTable("subscriptions", {
billingCycleAnchor: bigint("billingCycleAnchor", { mode: "number" }),
expiresAt: bigint("expiresAt", { mode: "number" }),
trial: boolean("trial").default(false),
type: varchar("type", { length: 50 }) // tier1, tier2, tier3, or license
type: varchar("type", { length: 50 }), // tier1, tier2, tier3, or license
override: boolean("override").default(false)
});
export const subscriptionItems = pgTable("subscriptionItems", {
@@ -486,6 +470,9 @@ export const eventStreamingDestinations = pgTable(
sendRequestLogs: boolean("sendRequestLogs").notNull().default(false),
sendActionLogs: boolean("sendActionLogs").notNull().default(false),
sendAccessLogs: boolean("sendAccessLogs").notNull().default(false),
sendAISessionLogs: boolean("sendAISessionLogs")
.notNull()
.default(false),
type: varchar("type", { length: 50 }).notNull(), // e.g. "http", "kafka", etc.
config: text("config").notNull(), // JSON string with the configuration for the destination
enabled: boolean("enabled").notNull().default(true),
@@ -629,10 +616,90 @@ export const trialNotifications = pgTable("trialNotifications", {
sentAt: bigint("sentAt", { mode: "number" }).notNull()
});
// Logs the aggregated prompt + response for a single AI gateway request, for
// session replay. One row per request (not per streaming chunk). `sessionId`
// is a fresh random id per row for now - no cross-request correlation yet,
// but the column exists so a future pass can link multiple rows into a real
// multi-turn session.
export const aiSessionLog = pgTable(
"aiSessionLog",
{
id: serial("id").primaryKey(),
sessionId: varchar("sessionId").notNull(),
orgId: varchar("orgId").references(() => orgs.orgId, {
onDelete: "cascade"
}),
providerId: integer("providerId").references(
() => aiProviders.providerId,
{ onDelete: "set null" }
),
capability: varchar("capability").notNull(),
resourceId: integer("resourceId").references(
() => resources.resourceId,
{ onDelete: "set null" }
),
siteResourceId: integer("siteResourceId").references(
() => siteResources.siteResourceId,
{ onDelete: "set null" }
),
userId: varchar("userId").references(() => users.userId, {
onDelete: "set null"
}),
virtualApiKeyId: varchar("virtualApiKeyId").references(
() => virtualApiKeys.virtualApiKeyId,
{ onDelete: "set null" }
),
requestedModel: varchar("requestedModel"),
isStream: boolean("isStream").notNull().default(false),
requestBody: text("requestBody"),
responseBody: text("responseBody"),
// Capability-agnostic message transcript (JSON-encoded
// NormalizedAiMessage[] from server/lib/aiMessageNormalization.ts),
// computed at write time so search/display never need per-capability
// parsing logic. Null when normalization couldn't recognize the
// shape - callers fall back to requestBody/responseBody.
normalizedRequest: text("normalizedRequest"),
normalizedResponse: text("normalizedResponse"),
// True if any of the request/response (raw or normalized) fields
// were cut short at AI_SESSION_LOG_MAX_BODY_CHARS before storage.
truncated: boolean("truncated").notNull().default(false),
statusCode: integer("statusCode"),
createdAt: bigint("createdAt", { mode: "number" }).notNull() // epoch seconds
},
(t) => [
index("idx_ai_session_log_org_created").on(t.orgId, t.createdAt),
index("idx_ai_session_log_org_provider_created").on(
t.orgId,
t.providerId,
t.createdAt
),
index("idx_ai_session_log_org_resource_created").on(
t.orgId,
t.resourceId,
t.createdAt
),
index("idx_ai_session_log_org_site_resource_created").on(
t.orgId,
t.siteResourceId,
t.createdAt
),
index("idx_ai_session_log_org_user_created").on(
t.orgId,
t.userId,
t.createdAt
),
index("idx_ai_session_log_org_virtual_api_key_created").on(
t.orgId,
t.virtualApiKeyId,
t.createdAt
),
index("idx_ai_session_log_session").on(t.sessionId)
]
);
export type Approval = InferSelectModel<typeof approvals>;
export type Limit = InferSelectModel<typeof limits>;
export type Account = InferSelectModel<typeof account>;
export type Certificate = InferSelectModel<typeof certificates>;
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
export type Customer = InferSelectModel<typeof customers>;
export type Subscription = InferSelectModel<typeof subscriptions>;
@@ -676,3 +743,4 @@ export type AlertEmailRecipients = InferSelectModel<
>;
export type AlertWebhookActions = InferSelectModel<typeof alertWebhookActions>;
export type TrialNotification = InferSelectModel<typeof trialNotifications>;
export type AiSessionLog = InferSelectModel<typeof aiSessionLog>;
+512 -57
View File
@@ -3,6 +3,7 @@ import { InferSelectModel, sql } from "drizzle-orm";
import {
bigint,
boolean,
check,
index,
integer,
pgTable,
@@ -11,6 +12,7 @@ import {
serial,
text,
unique,
uniqueIndex,
varchar
} from "drizzle-orm/pg-core";
@@ -18,7 +20,7 @@ export const domains = pgTable("domains", {
domainId: varchar("domainId").primaryKey(),
baseDomain: varchar("baseDomain").notNull(),
configManaged: boolean("configManaged").notNull().default(false),
type: varchar("type"), // "ns", "cname", "wildcard"
type: varchar("type").$type<"ns" | "cname" | "wildcard">(),
verified: boolean("verified").notNull().default(false),
failed: boolean("failed").notNull().default(false),
tries: integer("tries").notNull().default(0),
@@ -63,6 +65,11 @@ export const orgs = pgTable("orgs", {
) // where 0 = dont keep logs and -1 = keep forever and 9001 = end of the following year
.notNull()
.default(0),
settingsLogRetentionDaysAISessions: integer(
"settingsLogRetentionDaysAISessions"
) // 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"),
@@ -98,7 +105,7 @@ export const sites = pgTable(
}),
name: varchar("name").notNull(),
pubKey: varchar("pubKey"),
subnet: varchar("subnet"),
exitNodeSubnet: text("exitNodeSubnet"), // this is the subnet when connecting to an exit node and INCLUDES THE CIDR
megabytesIn: real("bytesIn").default(0),
megabytesOut: real("bytesOut").default(0),
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
@@ -194,7 +201,12 @@ export const resources = pgTable(
postAuthPath: text("postAuthPath"),
health: varchar("health").default("unknown"), // "healthy", "unhealthy", "unknown"
wildcard: boolean("wildcard").notNull().default(false),
mode: text("mode").default("http").notNull(), // rdp, ssh, http, vnc
mode: text("mode")
.default("http")
.$type<
"rdp" | "ssh" | "http" | "vnc" | "inference" | "tcp" | "udp"
>()
.notNull(),
pamMode: varchar("pamMode", { length: 32 })
.$type<"passthrough" | "push">()
.default("passthrough"),
@@ -215,16 +227,55 @@ export const resources = pgTable(
]
);
export const labels = pgTable("labels", {
labelId: serial("labelId").primaryKey(),
name: varchar("name").notNull(),
color: varchar("color").notNull(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull()
});
export const resourceAiProviders = pgTable(
"resourceAiProviders",
{
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
providerId: integer("providerId")
.notNull()
.references(() => aiProviders.providerId, { onDelete: "cascade" }),
accessMode: varchar("accessMode")
.$type<"inherit" | "select">()
.notNull()
.default("inherit"),
enabled: boolean("enabled").notNull().default(true)
},
(t) => [primaryKey({ columns: [t.resourceId, t.providerId] })]
);
export const resourceAiModels = pgTable(
"resourceAiModels",
{
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" }),
modelId: integer("modelId")
.notNull()
.references(() => aiModels.modelId, { onDelete: "cascade" }),
listType: varchar("listType")
.$type<"allow" | "block">()
.notNull()
.default("allow")
},
(t) => [primaryKey({ columns: [t.resourceId, t.modelId] })]
);
export const labels = pgTable(
"labels",
{
labelId: serial("labelId").primaryKey(),
name: varchar("name").notNull(),
color: varchar("color").notNull(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull()
},
(t) => [index("idx_labels_orgid").on(t.orgId)]
);
export const launcherViews = pgTable("launcherViews", {
viewId: serial("viewId").primaryKey(),
@@ -317,11 +368,18 @@ export const targets = pgTable(
"targets",
{
targetId: serial("targetId").primaryKey(),
resourceId: integer("resourceId")
.references(() => resources.resourceId, {
resourceId: integer("resourceId").references(
() => resources.resourceId,
{
onDelete: "cascade"
})
.notNull(),
}
),
providerId: integer("providerId").references(
() => aiProviders.providerId,
{
onDelete: "cascade"
}
),
siteId: integer("siteId")
.references(() => sites.siteId, {
onDelete: "cascade"
@@ -345,6 +403,7 @@ export const targets = pgTable(
},
(t) => [
index("idx_targets_resourceid_siteid").on(t.resourceId, t.siteId),
index("idx_targets_providerid_siteid").on(t.providerId, t.siteId),
index("idx_targets_site_enabled_priority_target_resource")
.on(t.siteId, t.priority.desc(), t.targetId, t.resourceId)
.where(sql`${t.enabled} = true`)
@@ -424,11 +483,14 @@ export const siteResources = pgTable(
onDelete: "restrict"
}
),
requiresExitNodeConnection: boolean("requiresExitNodeConnection")
.notNull()
.default(false),
niceId: varchar("niceId").notNull(),
name: varchar("name").notNull(),
ssl: boolean("ssl").notNull().default(false),
mode: varchar("mode")
.$type<"host" | "cidr" | "http" | "ssh">()
.$type<"host" | "cidr" | "http" | "ssh" | "inference">()
.notNull(), // "host" | "cidr" | "http"
scheme: varchar("scheme").$type<"http" | "https">(), // only for when we are doing https or http mode
proxyPort: integer("proxyPort"), // only for port mode
@@ -463,6 +525,45 @@ export const siteResources = pgTable(
(t) => [index("idx_siteresources_orgid_niceid").on(t.orgId, t.niceId)]
);
export const siteResourceAiProviders = pgTable(
"siteResourceAiProviders",
{
siteResourceId: integer("siteResourceId")
.notNull()
.references(() => siteResources.siteResourceId, {
onDelete: "cascade"
}),
providerId: integer("providerId")
.notNull()
.references(() => aiProviders.providerId, { onDelete: "cascade" }),
accessMode: varchar("accessMode")
.$type<"inherit" | "select">()
.notNull()
.default("inherit"),
enabled: boolean("enabled").notNull().default(true)
},
(t) => [primaryKey({ columns: [t.siteResourceId, t.providerId] })]
);
export const siteResourceAiModels = pgTable(
"siteResourceAiModels",
{
siteResourceId: integer("siteResourceId")
.notNull()
.references(() => siteResources.siteResourceId, {
onDelete: "cascade"
}),
modelId: integer("modelId")
.notNull()
.references(() => aiModels.modelId, { onDelete: "cascade" }),
listType: varchar("listType")
.$type<"allow" | "block">()
.notNull()
.default("allow")
},
(t) => [primaryKey({ columns: [t.siteResourceId, t.modelId] })]
);
export const networks = pgTable(
"networks",
{
@@ -581,6 +682,8 @@ export const newts = pgTable(
secretHash: varchar("secretHash").notNull(),
dateCreated: varchar("dateCreated").notNull(),
version: varchar("version"),
agent: varchar("agent"), // either newt or cli
agentVersion: varchar("agentVersion"),
siteId: integer("siteId").references(() => sites.siteId, {
onDelete: "cascade"
})
@@ -596,15 +699,19 @@ export const twoFactorBackupCodes = pgTable("twoFactorBackupCodes", {
codeHash: varchar("codeHash").notNull()
});
export const sessions = pgTable("session", {
sessionId: varchar("id").primaryKey(),
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
issuedAt: bigint("issuedAt", { mode: "number" }),
deviceAuthUsed: boolean("deviceAuthUsed").notNull().default(false)
});
export const sessions = pgTable(
"session",
{
sessionId: varchar("id").primaryKey(),
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
expiresAt: bigint("expiresAt", { mode: "number" }).notNull(),
issuedAt: bigint("issuedAt", { mode: "number" }),
deviceAuthUsed: boolean("deviceAuthUsed").notNull().default(false)
},
(t) => [index("idx_sessions_userid").on(t.userId)]
);
export const newtSessions = pgTable("newtSession", {
sessionId: varchar("id").primaryKey(),
@@ -614,19 +721,26 @@ export const newtSessions = pgTable("newtSession", {
expiresAt: bigint("expiresAt", { mode: "number" }).notNull()
});
export const userOrgs = pgTable("userOrgs", {
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
isOwner: boolean("isOwner").notNull().default(false),
autoProvisioned: boolean("autoProvisioned").default(false),
pamUsername: varchar("pamUsername") // cleaned username for ssh and such
});
export const userOrgs = pgTable(
"userOrgs",
{
userId: varchar("userId")
.notNull()
.references(() => users.userId, { onDelete: "cascade" }),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
isOwner: boolean("isOwner").notNull().default(false),
autoProvisioned: boolean("autoProvisioned").default(false),
pamUsername: varchar("pamUsername") // cleaned username for ssh and such
},
(t) => [
index("idx_userOrgs_userid").on(t.userId),
index("idx_userOrgs_orgid").on(t.orgId)
]
);
export const emailVerificationCodes = pgTable("emailVerificationCodes", {
codeId: serial("id").primaryKey(),
@@ -654,22 +768,26 @@ export const actions = pgTable("actions", {
description: varchar("description")
});
export const roles = pgTable("roles", {
roleId: serial("roleId").primaryKey(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
isAdmin: boolean("isAdmin"),
name: varchar("name").notNull(),
description: varchar("description"),
requireDeviceApproval: boolean("requireDeviceApproval").default(false),
sshSudoMode: varchar("sshSudoMode", { length: 32 }).default("none"), // "none" | "full" | "commands"
sshSudoCommands: text("sshSudoCommands").default("[]"),
sshCreateHomeDir: boolean("sshCreateHomeDir").default(true),
sshUnixGroups: text("sshUnixGroups").default("[]")
});
export const roles = pgTable(
"roles",
{
roleId: serial("roleId").primaryKey(),
orgId: varchar("orgId")
.references(() => orgs.orgId, {
onDelete: "cascade"
})
.notNull(),
isAdmin: boolean("isAdmin"),
name: varchar("name").notNull(),
description: varchar("description"),
requireDeviceApproval: boolean("requireDeviceApproval").default(false),
sshSudoMode: varchar("sshSudoMode", { length: 32 }).default("full"), // "none" | "full" | "commands"
sshSudoCommands: text("sshSudoCommands").default("[]"),
sshCreateHomeDir: boolean("sshCreateHomeDir").default(true),
sshUnixGroups: text("sshUnixGroups").default("[]")
},
(t) => [index("idx_roles_orgid").on(t.orgId)]
);
export const userOrgRoles = pgTable(
"userOrgRoles",
@@ -1150,6 +1268,52 @@ export const apiKeyOrg = pgTable("apiKeyOrg", {
.notNull()
});
export const virtualApiKeys = pgTable(
"virtualApiKeys",
{
virtualApiKeyId: varchar("virtualApiKeyId").primaryKey(),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
kind: varchar("kind").$type<"user" | "manual">().notNull(),
userId: varchar("userId").references(() => users.userId, {
onDelete: "cascade"
}),
name: varchar("name"),
description: varchar("description"),
token: varchar("token").notNull(),
lastChars: varchar("lastChars").notNull(),
allResources: boolean("allResources").notNull().default(false),
expiresAt: bigint("expiresAt", { mode: "number" }),
lastUsedAt: bigint("lastUsedAt", { mode: "number" }),
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
createdByUserId: varchar("createdByUserId").references(
() => users.userId,
{ onDelete: "set null" }
)
},
(t) => [
uniqueIndex("virtual_api_key_user_identity_uniq")
.on(t.orgId, t.userId)
.where(sql`${t.kind} = 'user'`)
]
);
export const virtualApiKeyResources = pgTable(
"virtualApiKeyResources",
{
virtualApiKeyId: varchar("virtualApiKeyId")
.notNull()
.references(() => virtualApiKeys.virtualApiKeyId, {
onDelete: "cascade"
}),
resourceId: integer("resourceId")
.notNull()
.references(() => resources.resourceId, { onDelete: "cascade" })
},
(t) => [primaryKey({ columns: [t.virtualApiKeyId, t.resourceId] })]
);
export const idpOrg = pgTable("idpOrg", {
idpId: integer("idpId")
.notNull()
@@ -1182,6 +1346,7 @@ export const clients = pgTable(
name: varchar("name").notNull(),
pubKey: varchar("pubKey"),
subnet: varchar("subnet").notNull(),
exitNodeSubnet: varchar("exitNodeSubnet"), // INCLUDES THE CIDR
megabytesIn: real("bytesIn"),
megabytesOut: real("bytesOut"),
lastBandwidthUpdate: varchar("lastBandwidthUpdate"),
@@ -1265,7 +1430,10 @@ export const olms = pgTable(
}),
archived: boolean("archived").notNull().default(false)
},
(t) => [index("idx_olms_clientid").on(t.clientId)]
(t) => [
index("idx_olms_clientid").on(t.clientId),
index("idx_olms_userid").on(t.userId)
]
);
export const currentFingerprint = pgTable("currentFingerprint", {
@@ -1540,6 +1708,277 @@ export const statusHistory = pgTable(
]
);
export const aiProviders = pgTable(
"aiProviders",
{
providerId: serial("providerId").primaryKey(),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
name: varchar("name").notNull(),
niceId: varchar("niceId").notNull(),
type: varchar("type")
.$type<
| "openai"
| "anthropic"
| "googleGemini"
| "vertexAi"
| "bedrock"
| "microsoftFoundry"
| "openRouter"
| "vercelAiGateway"
| "custom"
>()
.notNull(),
upstreamUrl: text("upstreamUrl"),
apiKey: text("apiKey"),
apiKeyLastChars: varchar("apiKeyLastChars"),
authType: varchar("authType")
.$type<
| "bearer"
| "x-api-key"
| "x-goog-api-key"
| "hec"
| "cf-aig-authorization"
| "none"
| "passthrough"
>()
.notNull(),
routingMode: varchar("routingMode")
.$type<"url" | "target">()
.notNull()
.default("url"),
capabilities: text("capabilities").notNull().default("[]"),
headers: text("headers"), // JSON array of { name, value }
skipTlsVerification: boolean("skipTlsVerification")
.notNull()
.default(false),
enabled: boolean("enabled").notNull().default(true),
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
},
(t) => [index("idx_aiProviders_orgId_niceId").on(t.orgId, t.niceId)]
);
export const aiModels = pgTable(
"aiModels",
{
modelId: serial("modelId").primaryKey(),
providerId: integer("providerId")
.notNull()
.references(() => aiProviders.providerId, { onDelete: "cascade" }),
modelKey: varchar("modelKey").notNull(),
name: varchar("name").notNull(),
listType: varchar("listType")
.$type<"allow" | "block">()
.notNull()
.default("allow"),
enabled: boolean("enabled").notNull().default(true),
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
},
(t) => [unique("ai_model_provider_key_uniq").on(t.providerId, t.modelKey)]
);
export const aiBudgets = pgTable(
"aiBudgets",
{
budgetId: serial("budgetId").primaryKey(),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
providerId: integer("providerId").references(
() => aiProviders.providerId,
{ onDelete: "cascade" }
),
modelId: integer("modelId").references(() => aiModels.modelId, {
onDelete: "cascade"
}),
resourceId: integer("resourceId").references(
() => resources.resourceId,
{ onDelete: "cascade" }
),
siteResourceId: integer("siteResourceId").references(
() => siteResources.siteResourceId,
{ onDelete: "cascade" }
),
roleId: integer("roleId").references(() => roles.roleId, {
onDelete: "cascade"
}),
virtualApiKeyId: varchar("virtualApiKeyId").references(
() => virtualApiKeys.virtualApiKeyId,
{ onDelete: "cascade" }
),
amount: real("amount").notNull(),
unit: varchar("unit").$type<"usd" | "tokens">().notNull(),
period: varchar("period")
.$type<
| "monthly"
| "yearly"
| "lifetime"
| "daily"
| "hourly"
| "weekly"
>()
.notNull()
.default("monthly"),
enforcement: varchar("enforcement")
.$type<"hard" | "soft">()
.notNull()
.default("hard"),
enabled: boolean("enabled").notNull().default(true),
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
updatedAt: bigint("updatedAt", { mode: "number" }).notNull()
},
(t) => [
unique("ai_budget_provider_uniq").on(t.providerId, t.unit, t.period),
unique("ai_budget_model_uniq").on(t.modelId, t.unit, t.period),
unique("ai_budget_resource_uniq").on(t.resourceId, t.unit, t.period),
unique("ai_budget_site_resource_uniq").on(
t.siteResourceId,
t.unit,
t.period
),
unique("ai_budget_role_uniq").on(t.roleId, t.unit, t.period),
unique("ai_budget_virtual_api_key_uniq").on(
t.virtualApiKeyId,
t.unit,
t.period
)
]
);
export const aiUsageRecords = pgTable(
"aiUsageRecords",
{
id: serial("id").primaryKey(),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
providerId: integer("providerId").references(
() => aiProviders.providerId,
{ onDelete: "set null" }
),
resourceId: integer("resourceId").references(
() => resources.resourceId,
{ onDelete: "set null" }
),
siteResourceId: integer("siteResourceId").references(
() => siteResources.siteResourceId,
{ onDelete: "set null" }
),
userId: varchar("userId").references(() => users.userId, {
onDelete: "set null"
}),
virtualApiKeyId: varchar("virtualApiKeyId").references(
() => virtualApiKeys.virtualApiKeyId,
{ onDelete: "set null" }
),
// Links this usage record back to the aiSessionLog row for the same
// request (aiSessionLog.sessionId), so token/cost usage can be shown
// alongside the session transcript. Not a DB-level FK - aiSessionLog
// lives in the separate logs database. Nullable because the session
// log may be disabled (retention set to 0) while usage tracking
// stays on.
sessionId: varchar("sessionId"),
requestedModel: varchar("requestedModel").notNull(),
promptTokens: integer("promptTokens").notNull().default(0),
cacheReadTokens: integer("cacheReadTokens").notNull().default(0),
cacheWriteTokens: integer("cacheWriteTokens").notNull().default(0),
completionTokens: integer("completionTokens").notNull().default(0),
reasoningTokens: integer("reasoningTokens").notNull().default(0),
totalTokens: integer("totalTokens").notNull().default(0),
costUsd: real("costUsd"),
estimated: boolean("estimated").notNull().default(false),
createdAt: bigint("createdAt", { mode: "number" }).notNull()
},
(t) => [
index("idx_ai_usage_records_org_provider_created").on(
t.orgId,
t.providerId,
t.createdAt
),
index("idx_ai_usage_records_org_resource_created").on(
t.orgId,
t.resourceId,
t.createdAt
),
index("idx_ai_usage_records_org_site_resource_created").on(
t.orgId,
t.siteResourceId,
t.createdAt
),
index("idx_ai_usage_records_org_user_created").on(
t.orgId,
t.userId,
t.createdAt
),
index("idx_ai_usage_records_org_virtual_api_key_created").on(
t.orgId,
t.virtualApiKeyId,
t.createdAt
),
index("idx_ai_usage_records_session").on(t.sessionId)
]
);
export const aiBudgetBreachEvents = pgTable(
"aiBudgetBreachEvents",
{
id: serial("id").primaryKey(),
orgId: varchar("orgId")
.notNull()
.references(() => orgs.orgId, { onDelete: "cascade" }),
budgetId: integer("budgetId")
.notNull()
.references(() => aiBudgets.budgetId, { onDelete: "cascade" }),
enforcement: varchar("enforcement").$type<"hard" | "soft">().notNull(),
unit: varchar("unit").$type<"usd" | "tokens">().notNull(),
period: varchar("period")
.$type<
| "monthly"
| "yearly"
| "lifetime"
| "daily"
| "hourly"
| "weekly"
>()
.notNull(),
amount: real("amount").notNull(),
usageAmount: real("usageAmount").notNull(),
blocked: boolean("blocked").notNull(),
requestUserId: varchar("requestUserId").references(() => users.userId, {
onDelete: "set null"
}),
createdAt: bigint("createdAt", { mode: "number" }).notNull()
},
(t) => [
index("idx_ai_budget_breach_events_budget_created").on(
t.budgetId,
t.createdAt
)
]
);
export const certificates = pgTable("certificates", {
certId: serial("certId").primaryKey(),
domain: varchar("domain", { length: 255 }).notNull().unique(),
domainId: varchar("domainId").references(() => domains.domainId, {
onDelete: "cascade"
}),
wildcard: boolean("wildcard").default(false),
status: varchar("status", { length: 50 }).notNull().default("pending"), // pending, requested, valid, expired, failed
expiresAt: bigint("expiresAt", { mode: "number" }),
lastRenewalAttempt: bigint("lastRenewalAttempt", { mode: "number" }),
createdAt: bigint("createdAt", { mode: "number" }).notNull(),
updatedAt: bigint("updatedAt", { mode: "number" }).notNull(),
orderId: varchar("orderId", { length: 500 }),
errorMessage: text("errorMessage"),
renewalCount: integer("renewalCount").default(0),
certFile: text("certFile"),
keyFile: text("keyFile")
});
export type Org = InferSelectModel<typeof orgs>;
export type User = InferSelectModel<typeof users>;
export type Site = InferSelectModel<typeof sites>;
@@ -1595,6 +2034,10 @@ export type Idp = InferSelectModel<typeof idp>;
export type ApiKey = InferSelectModel<typeof apiKeys>;
export type ApiKeyAction = InferSelectModel<typeof apiKeyActions>;
export type ApiKeyOrg = InferSelectModel<typeof apiKeyOrg>;
export type VirtualApiKey = InferSelectModel<typeof virtualApiKeys>;
export type VirtualApiKeyResource = InferSelectModel<
typeof virtualApiKeyResources
>;
export type Client = InferSelectModel<typeof clients>;
export type ClientSite = InferSelectModel<typeof clientSitesAssociationsCache>;
export type Olm = InferSelectModel<typeof olms>;
@@ -1624,3 +2067,15 @@ export type ResourcePolicy = InferSelectModel<typeof resourcePolicies>;
export type RolePolicy = InferSelectModel<typeof rolePolicies>;
export type UserPolicy = InferSelectModel<typeof userPolicies>;
export type ResourcePolicyRule = InferSelectModel<typeof resourcePolicyRules>;
export type AiProvider = InferSelectModel<typeof aiProviders>;
export type AiModel = InferSelectModel<typeof aiModels>;
export type AiBudget = InferSelectModel<typeof aiBudgets>;
export type AiUsageRecord = InferSelectModel<typeof aiUsageRecords>;
export type AiBudgetBreachEvent = InferSelectModel<typeof aiBudgetBreachEvents>;
export type ResourceAiProvider = InferSelectModel<typeof resourceAiProviders>;
export type SiteResourceAiProvider = InferSelectModel<
typeof siteResourceAiProviders
>;
export type ResourceAiModel = InferSelectModel<typeof resourceAiModels>;
export type SiteResourceAiModel = InferSelectModel<typeof siteResourceAiModels>;
export type Certificate = InferSelectModel<typeof certificates>;
+33 -1
View File
@@ -33,7 +33,9 @@ import {
resourcePolicyPassword,
ResourcePolicyPassword,
resourcePolicyHeaderAuth,
ResourcePolicyHeaderAuth
ResourcePolicyHeaderAuth,
resourceWhitelist,
resourcePolicyWhiteList
} from "@server/db";
import { alias } from "@server/db";
import { and, eq, inArray, isNull, or, sql } from "drizzle-orm";
@@ -448,6 +450,36 @@ export async function getResourceRules(
return [...directRules, ...offsetPolicyRules] as ResourceRule[];
}
/**
* Get the whitelisted email associated with a resource session's whitelist
* match (either a direct resource whitelist entry or a resource policy
* whitelist entry).
*/
export async function getWhitelistEmail(
whitelistId?: number | null,
policyWhitelistId?: number | null
): Promise<string | null> {
if (whitelistId) {
const [row] = await db
.select({ email: resourceWhitelist.email })
.from(resourceWhitelist)
.where(eq(resourceWhitelist.whitelistId, whitelistId))
.limit(1);
return row?.email ?? null;
}
if (policyWhitelistId) {
const [row] = await db
.select({ email: resourcePolicyWhiteList.email })
.from(resourcePolicyWhiteList)
.where(eq(resourcePolicyWhiteList.whitelistId, policyWhitelistId))
.limit(1);
return row?.email ?? null;
}
return null;
}
/**
* Get organization login page
*/
+6 -1
View File
@@ -5,6 +5,7 @@ import path from "path";
import fs from "fs";
import { APP_PATH } from "@server/lib/consts";
import { existsSync, mkdirSync } from "fs";
import logger from "@server/logger";
export const location = path.join(APP_PATH, "db", "db.sqlite");
export const exists = checkFileExists(location);
@@ -12,7 +13,11 @@ export const exists = checkFileExists(location);
bootstrapVolume();
function createDb() {
const sqlite = new Database(location);
const verbose =
process.env.QUERY_LOGGING == "true"
? (message: unknown) => logger.debug(String(message))
: undefined;
const sqlite = new Database(location, { verbose });
if (process.env.ENABLE_SQLITE_WAL_MODE == "true") {
// Enable WAL mode — allows concurrent readers + single writer, preventing
+94 -22
View File
@@ -9,6 +9,7 @@ import {
uniqueIndex
} from "drizzle-orm/sqlite-core";
import {
aiProviders,
clients,
domains,
exitNodes,
@@ -20,28 +21,10 @@ import {
siteResources,
sites,
targetHealthCheck,
users
users,
virtualApiKeys
} from "./schema";
export const certificates = sqliteTable("certificates", {
certId: integer("certId").primaryKey({ autoIncrement: true }),
domain: text("domain").notNull().unique(),
domainId: text("domainId").references(() => domains.domainId, {
onDelete: "cascade"
}),
wildcard: integer("wildcard", { mode: "boolean" }).default(false),
status: text("status").notNull().default("pending"), // pending, requested, valid, expired, failed
expiresAt: integer("expiresAt"),
lastRenewalAttempt: integer("lastRenewalAttempt"),
createdAt: integer("createdAt").notNull(),
updatedAt: integer("updatedAt").notNull(),
orderId: text("orderId"),
errorMessage: text("errorMessage"),
renewalCount: integer("renewalCount").default(0),
certFile: text("certFile"),
keyFile: text("keyFile")
});
export const dnsChallenge = sqliteTable("dnsChallenges", {
dnsChallengeId: integer("dnsChallengeId").primaryKey({
autoIncrement: true
@@ -89,7 +72,8 @@ export const subscriptions = sqliteTable("subscriptions", {
expiresAt: integer("expiresAt"),
trial: integer("trial", { mode: "boolean" }).default(false),
billingCycleAnchor: integer("billingCycleAnchor"),
type: text("type") // tier1, tier2, tier3, or license
type: text("type"), // tier1, tier2, tier3, or license
override: integer("override", { mode: "boolean" }).default(false)
});
export const subscriptionItems = sqliteTable("subscriptionItems", {
@@ -477,6 +461,9 @@ export const eventStreamingDestinations = sqliteTable(
sendAccessLogs: integer("sendAccessLogs", { mode: "boolean" })
.notNull()
.default(false),
sendAISessionLogs: integer("sendAISessionLogs", { mode: "boolean" })
.notNull()
.default(false),
type: text("type").notNull(), // e.g. "http", "kafka", etc.
config: text("config").notNull(), // JSON string with the configuration for the destination
enabled: integer("enabled", { mode: "boolean" })
@@ -624,10 +611,94 @@ export const trialNotifications = sqliteTable("trialNotifications", {
sentAt: integer("sentAt").notNull()
});
// Logs the aggregated prompt + response for a single AI gateway request, for
// session replay. One row per request (not per streaming chunk). `sessionId`
// is a fresh random id per row for now - no cross-request correlation yet,
// but the column exists so a future pass can link multiple rows into a real
// multi-turn session.
export const aiSessionLog = sqliteTable(
"aiSessionLog",
{
id: integer("id").primaryKey({ autoIncrement: true }),
sessionId: text("sessionId").notNull(),
orgId: text("orgId").references(() => orgs.orgId, {
onDelete: "cascade"
}),
providerId: integer("providerId").references(
() => aiProviders.providerId,
{ onDelete: "set null" }
),
capability: text("capability").notNull(),
resourceId: integer("resourceId").references(
() => resources.resourceId,
{ onDelete: "set null" }
),
siteResourceId: integer("siteResourceId").references(
() => siteResources.siteResourceId,
{ onDelete: "set null" }
),
userId: text("userId").references(() => users.userId, {
onDelete: "set null"
}),
virtualApiKeyId: text("virtualApiKeyId").references(
() => virtualApiKeys.virtualApiKeyId,
{ onDelete: "set null" }
),
requestedModel: text("requestedModel"),
isStream: integer("isStream", { mode: "boolean" })
.notNull()
.default(false),
requestBody: text("requestBody"),
responseBody: text("responseBody"),
// Capability-agnostic message transcript (JSON-encoded
// NormalizedAiMessage[] from server/lib/aiMessageNormalization.ts),
// computed at write time so search/display never need per-capability
// parsing logic. Null when normalization couldn't recognize the
// shape - callers fall back to requestBody/responseBody.
normalizedRequest: text("normalizedRequest"),
normalizedResponse: text("normalizedResponse"),
// True if any of the request/response (raw or normalized) fields
// were cut short at AI_SESSION_LOG_MAX_BODY_CHARS before storage.
truncated: integer("truncated", { mode: "boolean" })
.notNull()
.default(false),
statusCode: integer("statusCode"),
createdAt: integer("createdAt").notNull() // epoch seconds
},
(t) => [
index("idx_ai_session_log_org_created").on(t.orgId, t.createdAt),
index("idx_ai_session_log_org_provider_created").on(
t.orgId,
t.providerId,
t.createdAt
),
index("idx_ai_session_log_org_resource_created").on(
t.orgId,
t.resourceId,
t.createdAt
),
index("idx_ai_session_log_org_site_resource_created").on(
t.orgId,
t.siteResourceId,
t.createdAt
),
index("idx_ai_session_log_org_user_created").on(
t.orgId,
t.userId,
t.createdAt
),
index("idx_ai_session_log_org_virtual_api_key_created").on(
t.orgId,
t.virtualApiKeyId,
t.createdAt
),
index("idx_ai_session_log_session").on(t.sessionId)
]
);
export type Approval = InferSelectModel<typeof approvals>;
export type Limit = InferSelectModel<typeof limits>;
export type Account = InferSelectModel<typeof account>;
export type Certificate = InferSelectModel<typeof certificates>;
export type DnsChallenge = InferSelectModel<typeof dnsChallenge>;
export type Customer = InferSelectModel<typeof customers>;
export type Subscription = InferSelectModel<typeof subscriptions>;
@@ -663,3 +734,4 @@ export type AlertEmailAction = InferSelectModel<typeof alertEmailActions>;
export type AlertEmailRecipient = InferSelectModel<typeof alertEmailRecipients>;
export type AlertWebhookAction = InferSelectModel<typeof alertWebhookActions>;
export type TrialNotification = InferSelectModel<typeof trialNotifications>;
export type AiSessionLog = InferSelectModel<typeof aiSessionLog>;
File diff suppressed because it is too large Load Diff
+8
View File
@@ -0,0 +1,8 @@
export async function startDnsServer() {
// No-op: the authoritative DNS server is only available in builds
// that include the private/enterprise feature set.
}
export async function stopDnsServer() {
// No-op counterpart to startDnsServer.
}
+30 -3
View File
@@ -31,9 +31,24 @@ export type AlertNotificationProps = {
orgId: string;
data: Record<string, unknown>;
dashboardLink: string;
isTestAlert?: boolean;
};
function getEventMeta(eventType: AlertEventType): {
function getEventMeta(
eventType: AlertEventType,
isTestAlert: boolean = false
): {
heading: string;
previewText: string;
summary: string;
statusLabel: string | null;
statusColor: string | null;
} {
const meta = getBaseEventMeta(eventType);
return isTestAlert ? { ...meta, heading: `[TEST] ${meta.heading}` } : meta;
}
function getBaseEventMeta(eventType: AlertEventType): {
heading: string;
previewText: string;
summary: string;
@@ -180,8 +195,14 @@ function formatDataItems(
}
export const AlertNotification = (props: AlertNotificationProps) => {
const { eventType, orgId, data, dashboardLink } = props;
const meta = getEventMeta(eventType);
const {
eventType,
orgId,
data,
dashboardLink,
isTestAlert = false
} = props;
const meta = getEventMeta(eventType, isTestAlert);
const dataItems = formatDataItems(data);
const isToggle =
@@ -242,6 +263,12 @@ export const AlertNotification = (props: AlertNotificationProps) => {
Open your dashboard to view more details and manage
your alert rules.
</EmailText>
{isTestAlert && (
<EmailText>
This is a test alert. No action is required,
and no real event has occurred.
</EmailText>
)}
<EmailSection>
<ButtonLink href={dashboardLink}>
@@ -0,0 +1,78 @@
import React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import { themeColors } from "./lib/theme";
import {
EmailContainer,
EmailFooter,
EmailGreeting,
EmailHeading,
EmailInfoSection,
EmailLetterHead,
EmailSection,
EmailSignature,
EmailText
} from "./components/Email";
type IdentityApiKeyGeneratedProps = {
orgName: string;
accountLabel?: string | null;
credential: string;
resourceUrls: string[];
hasMoreResources: boolean;
};
export const IdentityApiKeyGenerated = ({
orgName,
accountLabel,
credential,
resourceUrls,
hasMoreResources
}: IdentityApiKeyGeneratedProps) => {
const previewText = `Your personal identity key for ${orgName}`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind config={themeColors}>
<Body className="font-sans bg-gray-50">
<EmailContainer>
<EmailLetterHead />
<EmailGreeting>Hi there,</EmailGreeting>
<EmailText>
This is your personal identity key for{" "}
<strong>{orgName}</strong>. It belongs to your
account and identifies you when you use public AI
gateways.
</EmailText>
<EmailText>
Use it with resources your administrator has granted
you, or that your role has access to. Treat this key
like a password and do not share it.
</EmailText>
<EmailSection>
<EmailText>Your identity key:</EmailText>
<div className="inline-block max-w-full">
<div className="bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 mx-auto text-left">
<span className="text-sm font-mono text-gray-900 break-all">
{credential}
</span>
</div>
</div>
</EmailSection>
<EmailFooter>
<EmailSignature />
</EmailFooter>
</EmailContainer>
</Body>
</Tailwind>
</Html>
);
};
export default IdentityApiKeyGenerated;
+13 -11
View File
@@ -30,14 +30,14 @@ export const NotifyTrialExpiring = ({
const isLastDay = daysRemaining === 1;
const previewText = hasEnded
? `Your trial for ${orgName} has ended.`
? `Your cloud trial for ${orgName} has ended.`
: isLastDay
? `Your trial for ${orgName} ends tomorrow.`
: `Your trial for ${orgName} ends in ${daysRemaining} days.`;
? `Your cloud trial for ${orgName} ends tomorrow.`
: `Your cloud trial for ${orgName} ends in ${daysRemaining} days.`;
const heading = hasEnded
? "Your Trial Ended"
: "Your Trial is Ending Soon";
? "Your Cloud Trial Ended"
: "Your Cloud Trial is Ending Soon";
return (
<Html>
@@ -55,7 +55,7 @@ export const NotifyTrialExpiring = ({
{hasEnded ? (
<>
<EmailText>
Your free trial for{" "}
Your cloud free trial for{" "}
<strong>{orgName}</strong> ended on{" "}
<strong>{trialEndsAt}</strong>. Your account
has been moved to the free plan, which
@@ -64,10 +64,11 @@ export const NotifyTrialExpiring = ({
<EmailText>
Some features and resources may now be
restricted. To restore full
access and continue using all the features
you had during your trial, please upgrade to
a paid plan.
restricted. To restore full access and
continue using all the features you had
during your trial, please upgrade to a paid
plan. This does not effect any self hosted
licenses.
</EmailText>
<EmailText>
@@ -93,7 +94,8 @@ export const NotifyTrialExpiring = ({
<EmailText>
After your trial ends, your account will be
moved to the free plan and some
functionality may be restricted.
functionality may be restricted. This does
not effect any self hosted licenses.
</EmailText>
<EmailText>
@@ -0,0 +1,119 @@
import React from "react";
import { Body, Head, Html, Preview, Tailwind } from "@react-email/components";
import { themeColors } from "./lib/theme";
import {
EmailContainer,
EmailFooter,
EmailGreeting,
EmailInfoSection,
EmailLetterHead,
EmailSection,
EmailSignature,
EmailText
} from "./components/Email";
type VirtualApiKeyGeneratedProps = {
orgName: string;
keyName: string | null;
credential: string;
resourceUrls: string[];
hasMoreResources: boolean;
};
export const VirtualApiKeyGenerated = ({
orgName,
keyName,
credential,
resourceUrls,
hasMoreResources
}: VirtualApiKeyGeneratedProps) => {
const previewText = `A virtual API key for ${orgName} has been shared with you`;
return (
<Html>
<Head />
<Preview>{previewText}</Preview>
<Tailwind config={themeColors}>
<Body className="font-sans bg-gray-50">
<EmailContainer>
<EmailLetterHead />
<EmailGreeting>Hi there,</EmailGreeting>
<EmailText>
A virtual API key for <strong>{orgName}</strong> has
been shared with you. This key grants access to the
public AI gateways it was created for. Treat this
key like a password and do not share it.
</EmailText>
<EmailSection>
<EmailText>Your virtual API key:</EmailText>
<div className="inline-block max-w-full">
<div className="bg-gray-50 border border-gray-200 rounded-lg px-4 py-3 mx-auto text-left">
<span className="text-sm font-mono text-gray-900 break-all">
{credential}
</span>
</div>
</div>
</EmailSection>
<EmailInfoSection
title="Key details"
items={[
{
label: "Organization",
value: orgName
},
...(keyName
? [
{
label: "Name",
value: keyName
}
]
: [])
]}
/>
{resourceUrls.length > 0 && (
<>
<EmailText>
This key can be used to authenticate to the
following AI gateway resources:
</EmailText>
<div className="px-6 pb-2">
{resourceUrls.map((url) => (
<p
key={url}
className="text-base text-gray-700 leading-relaxed"
>
<a
href={url}
className="text-primary font-medium break-all"
>
{url}
</a>
</p>
))}
</div>
{hasMoreResources && (
<EmailText>
Contact your administrator to get the
full list.
</EmailText>
)}
</>
)}
<EmailFooter>
<EmailSignature />
</EmailFooter>
</EmailContainer>
</Body>
</Tailwind>
</Html>
);
};
export default VirtualApiKeyGenerated;
+1 -1
View File
@@ -18,7 +18,7 @@ export function EmailLetterHead() {
<Img
src="https://fossorial-public-assets.s3.us-east-1.amazonaws.com/word_mark_black.png"
alt="Pangolin Logo"
width="180"
width="135"
height="auto"
className="mx-auto"
/>
+22 -9
View File
@@ -5,26 +5,31 @@ import { runSetupFunctions } from "./setup";
import { createApiServer } from "./apiServer";
import { createNextServer } from "./nextServer";
import { createInternalServer } from "./internalServer";
import { createAiGatewayServer } from "./aiGatewayServer";
import { createIntegrationApiServer } from "./integrationApiServer";
import {
ApiKey,
ApiKeyOrg,
AiBudget,
AiModel,
AiProvider,
RemoteExitNode,
Session,
SiteResource,
User,
UserOrg
UserOrg,
VirtualApiKey
} from "@server/db";
import config from "@server/lib/config";
import { setHostMeta } from "@server/lib/hostMeta";
import { initTelemetryClient } from "@server/lib/telemetry";
import { TraefikConfigManager } from "@server/lib/traefik/TraefikConfigManager";
import { initCleanup } from "#dynamic/cleanup";
import { startSchedulers } from "#dynamic/startSchedulers";
import { startDnsServer } from "#dynamic/dns";
import { startCertificateManager } from "#dynamic/certificates";
import license from "#dynamic/license/license";
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
import { initAcmeCertSync } from "#dynamic/lib/acmeCertSync";
import { fetchServerIp } from "@server/lib/serverIpService";
import { startRebuildQueueProcessor } from "@server/lib/rebuildClientAssociations";
import { initAiModelCatalog } from "@server/lib/aiModelCatalog";
async function startServers() {
await setHostMeta();
@@ -38,15 +43,18 @@ async function startServers() {
await fetchServerIp();
initTelemetryClient();
await initAiModelCatalog();
initLogCleanupInterval();
initAcmeCertSync();
startRebuildQueueProcessor();
startSchedulers();
await startDnsServer();
await startCertificateManager();
// Start all servers
const apiServer = createApiServer();
const internalServer = createInternalServer();
const aiGatewayServer = createAiGatewayServer();
const nextServer = await createNextServer();
if (config.getRawConfig().traefik.file_mode) {
@@ -65,6 +73,7 @@ async function startServers() {
apiServer,
nextServer,
internalServer,
aiGatewayServer,
integrationServer
};
}
@@ -83,6 +92,10 @@ declare global {
userOrgIds?: string[];
remoteExitNode?: RemoteExitNode;
siteResource?: SiteResource;
aiProvider?: AiProvider;
aiModel?: AiModel;
aiBudget?: AiBudget;
virtualApiKey?: VirtualApiKey;
orgPolicyAllowed?: boolean;
}
}
+4 -3
View File
@@ -12,11 +12,11 @@ import { logIncomingMiddleware } from "./middlewares/logIncoming";
import helmet from "helmet";
import swaggerUi from "swagger-ui-express";
import { OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi";
import { registry } from "./openApi";
import { registry, openApiTags } from "./openApi";
import fs from "fs";
import path from "path";
import { APP_PATH } from "./lib/consts";
import yaml from "js-yaml";
import * as yaml from "js-yaml";
import { z } from "zod";
const dev = process.env.ENVIRONMENT !== "prod";
@@ -181,7 +181,8 @@ function getOpenApiDocumentation() {
version: "v1",
title: "Pangolin Integration API"
},
servers: [{ url: "/v1" }]
servers: [{ url: "/v1" }],
tags: openApiTags
});
if (!process.env.DISABLE_GEN_OPENAPI) {
+865 -2
View File
@@ -1,3 +1,866 @@
import fs from "fs";
import path from "path";
import crypto from "crypto";
import {
certificates,
clients,
clientSiteResourcesAssociationsCache,
db,
domains,
newts,
siteNetworks,
SiteResource,
siteResources
} from "@server/db";
import { and, eq } from "drizzle-orm";
import { encrypt, decrypt } from "@server/lib/crypto";
import logger from "@server/logger";
import config from "@server/lib/config";
import {
generateSubnetProxyTargetV2,
SubnetProxyTargetV2
} from "@server/lib/ip";
import { updateTargets } from "@server/routers/client/targets";
import cache from "#dynamic/lib/cache";
import { build } from "@server/build";
interface AcmeCert {
domain: { main: string; sans?: string[] };
certificate: string;
key: string;
Store: string;
}
interface AcmeJson {
[resolver: string]: {
Certificates: AcmeCert[];
};
}
export async function pushCertUpdateToAffectedNewts(
domain: string,
domainId: string | null,
oldCertPem: string | null,
oldKeyPem: string | null
): Promise<void> {
// Find all SSL-enabled HTTP site resources that use this cert's domain
let affectedResources: SiteResource[] = [];
if (domainId) {
affectedResources = await db
.select()
.from(siteResources)
.where(
and(
eq(siteResources.domainId, domainId),
eq(siteResources.ssl, true)
)
);
} else {
// Fallback: match by exact fullDomain when no domainId is available
affectedResources = await db
.select()
.from(siteResources)
.where(
and(
eq(siteResources.fullDomain, domain),
eq(siteResources.ssl, true)
)
);
}
if (affectedResources.length === 0) {
logger.debug(
`acmeCertSync: no affected site resources for cert domain "${domain}"`
);
return;
}
logger.debug(
`acmeCertSync: pushing cert update to ${affectedResources.length} affected site resource(s) for domain "${domain}"`
);
for (const resource of affectedResources) {
try {
// Get all sites for this resource via siteNetworks
const resourceSiteRows = resource.networkId
? await db
.select({ siteId: siteNetworks.siteId })
.from(siteNetworks)
.where(eq(siteNetworks.networkId, resource.networkId))
: [];
if (resourceSiteRows.length === 0) {
logger.debug(
`acmeCertSync: no sites for resource ${resource.siteResourceId}, skipping`
);
continue;
}
// Get all clients with access to this resource
const resourceClients = await db
.select({
clientId: clients.clientId,
pubKey: clients.pubKey,
subnet: clients.subnet
})
.from(clients)
.innerJoin(
clientSiteResourcesAssociationsCache,
eq(
clients.clientId,
clientSiteResourcesAssociationsCache.clientId
)
)
.where(
eq(
clientSiteResourcesAssociationsCache.siteResourceId,
resource.siteResourceId
)
);
if (resourceClients.length === 0) {
logger.debug(
`acmeCertSync: no clients for resource ${resource.siteResourceId}, skipping`
);
continue;
}
// Invalidate the cert cache so generateSubnetProxyTargetV2 fetches fresh data
if (resource.fullDomain) {
await cache.del(`cert:${resource.fullDomain}`);
}
// Generate target once - same cert applies to all sites for this resource
const newTargets = await generateSubnetProxyTargetV2(
resource,
resourceClients
);
if (!newTargets) {
logger.debug(
`acmeCertSync: could not generate target for resource ${resource.siteResourceId}, skipping`
);
continue;
}
// Construct the old targets - same routing shape but with the previous cert/key.
// The newt only uses destPrefix/sourcePrefixes for removal, but we keep the
// semantics correct so the update message accurately reflects what changed.
const oldTargets: SubnetProxyTargetV2[] = newTargets.map((t) => ({
...t,
tlsCert: oldCertPem ?? undefined,
tlsKey: oldKeyPem ?? undefined
}));
// Push update to each site's newt
for (const { siteId } of resourceSiteRows) {
const [newt] = await db
.select()
.from(newts)
.where(eq(newts.siteId, siteId))
.limit(1);
if (!newt) {
logger.debug(
`acmeCertSync: no newt found for site ${siteId}, skipping resource ${resource.siteResourceId}`
);
continue;
}
await updateTargets(
newt.newtId,
{ oldTargets: oldTargets, newTargets: newTargets },
newt.version
);
logger.debug(
`acmeCertSync: pushed cert update to newt for site ${siteId}, resource ${resource.siteResourceId}`
);
}
} catch (err) {
logger.error(
`acmeCertSync: error pushing cert update for resource ${resource?.siteResourceId}: ${err}`
);
}
}
}
async function findDomainId(certDomain: string): Promise<string | null> {
// Strip wildcard prefix before lookup (*.example.com -> example.com)
const lookupDomain = certDomain.startsWith("*.")
? certDomain.slice(2)
: certDomain;
// 1. Exact baseDomain match (any domain type)
const exactMatch = await db
.select({ domainId: domains.domainId })
.from(domains)
.where(eq(domains.baseDomain, lookupDomain))
.limit(1);
if (exactMatch.length > 0) {
return exactMatch[0].domainId;
}
// 2. Walk up the domain hierarchy looking for a wildcard-type domain whose
// baseDomain is a suffix of the cert domain. e.g. cert "sub.example.com"
// matches a wildcard domain with baseDomain "example.com".
const parts = lookupDomain.split(".");
for (let i = 1; i < parts.length; i++) {
const candidate = parts.slice(i).join(".");
if (!candidate) continue;
const wildcardMatch = await db
.select({ domainId: domains.domainId })
.from(domains)
.where(
and(
eq(domains.baseDomain, candidate),
eq(domains.type, "wildcard")
)
)
.limit(1);
if (wildcardMatch.length > 0) {
return wildcardMatch[0].domainId;
}
}
return null;
}
function extractFirstCert(pemBundle: string): string | null {
const match = pemBundle.match(
/-----BEGIN CERTIFICATE-----[\s\S]+?-----END CERTIFICATE-----/
);
return match ? match[0] : null;
}
/**
* Determine whether an ACME cert entry represents a wildcard cert by checking
* both the primary domain (`main`) and the SANs. Some ACME clients (notably
* Traefik) store the bare apex in `main` and only put the wildcard form in
* `sans` (e.g. main="access.example.com", sans=["*.access.example.com"]).
*/
function detectWildcard(
main: string,
sans: string[] | undefined
): { wildcard: boolean; wildcardSan: string | null } {
if (main.startsWith("*.")) {
return { wildcard: true, wildcardSan: null };
}
if (Array.isArray(sans)) {
for (const san of sans) {
if (typeof san !== "string") continue;
if (san === `*.${main}` || san.startsWith("*.")) {
return { wildcard: true, wildcardSan: san };
}
}
}
return { wildcard: false, wildcardSan: null };
}
interface HttpCert {
wildcard: boolean;
altName: string;
certName: string;
commonName: string;
certFile: string;
keyFile: string;
}
async function syncAcmeCertsFromHttp(endpoint: string): Promise<void> {
let response: Response;
try {
response = await fetch(endpoint);
} catch (err) {
logger.debug(
`acmeCertSync: could not reach HTTP endpoint ${endpoint}: ${err}`
);
return;
}
if (!response.ok) {
logger.debug(
`acmeCertSync: HTTP endpoint returned status ${response.status}`
);
return;
}
let httpCerts: HttpCert[];
try {
httpCerts = await response.json();
} catch (err) {
logger.debug(
`acmeCertSync: could not parse JSON from HTTP endpoint: ${err}`
);
return;
}
if (!Array.isArray(httpCerts) || httpCerts.length === 0) {
logger.debug(
`acmeCertSync: no certificates returned from HTTP endpoint`
);
return;
}
for (const cert of httpCerts) {
const domain = cert?.certName;
if (!domain || typeof domain !== "string") {
logger.debug(
`acmeCertSync: skipping HTTP cert with missing certName`
);
continue;
}
const certPem = cert.certFile;
const keyPem = cert.keyFile;
if (!certPem?.trim() || !keyPem?.trim()) {
logger.debug(
`acmeCertSync: skipping HTTP cert for ${domain} - empty certFile or keyFile`
);
continue;
}
const firstCertPemForValidation = extractFirstCert(certPem);
if (!firstCertPemForValidation) {
logger.debug(
`acmeCertSync: skipping HTTP cert for ${domain} - no PEM certificate block found`
);
continue;
}
let validatedX509: crypto.X509Certificate;
try {
validatedX509 = new crypto.X509Certificate(
firstCertPemForValidation
);
} catch (err) {
logger.debug(
`acmeCertSync: skipping HTTP cert for ${domain} - invalid X.509 certificate: ${err}`
);
continue;
}
try {
crypto.createPrivateKey(keyPem);
} catch (err) {
logger.debug(
`acmeCertSync: skipping HTTP cert for ${domain} - invalid private key: ${err}`
);
continue;
}
const wildcard = cert.wildcard ?? false;
const existing = await db
.select()
.from(certificates)
.where(eq(certificates.domain, domain))
.limit(1);
let oldCertPem: string | null = null;
let oldKeyPem: string | null = null;
if (existing.length > 0 && existing[0].certFile) {
try {
const storedCertPem = decrypt(
existing[0].certFile,
config.getRawConfig().server.secret!
);
const wildcardUnchanged = existing[0].wildcard === wildcard;
if (storedCertPem === certPem && wildcardUnchanged) {
continue;
}
oldCertPem = storedCertPem;
if (existing[0].keyFile) {
try {
oldKeyPem = decrypt(
existing[0].keyFile,
config.getRawConfig().server.secret!
);
} catch (keyErr) {
logger.debug(
`acmeCertSync: could not decrypt stored key for ${domain}: ${keyErr}`
);
}
}
} catch (err) {
logger.debug(
`acmeCertSync: could not decrypt stored cert for ${domain}, will update: ${err}`
);
}
}
let expiresAt: number | null = null;
try {
expiresAt = Math.floor(
new Date(validatedX509.validTo).getTime() / 1000
);
} catch (err) {
logger.debug(
`acmeCertSync: could not parse cert expiry for ${domain}: ${err}`
);
}
const encryptedCert = encrypt(
certPem,
config.getRawConfig().server.secret!
);
const encryptedKey = encrypt(
keyPem,
config.getRawConfig().server.secret!
);
const now = Math.floor(Date.now() / 1000);
const domainId = await findDomainId(domain);
if (domainId) {
logger.debug(
`acmeCertSync: resolved domainId "${domainId}" for HTTP cert domain "${domain}"`
);
} else {
logger.debug(
`acmeCertSync: no matching domain record found for HTTP cert domain "${domain}"`
);
}
if (existing.length > 0) {
logger.debug(
`acmeCertSync: updating existing certificate (HTTP) for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
);
await db
.update(certificates)
.set({
certFile: encryptedCert,
keyFile: encryptedKey,
status: "valid",
expiresAt,
updatedAt: now,
wildcard,
...(domainId !== null && { domainId })
})
.where(eq(certificates.domain, domain));
await pushCertUpdateToAffectedNewts(
domain,
domainId,
oldCertPem,
oldKeyPem
);
} else {
logger.debug(
`acmeCertSync: inserting new certificate (HTTP) for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
);
await db.insert(certificates).values({
domain,
domainId,
certFile: encryptedCert,
keyFile: encryptedKey,
status: "valid",
expiresAt,
createdAt: now,
updatedAt: now,
wildcard
});
await pushCertUpdateToAffectedNewts(domain, domainId, null, null);
}
}
}
async function storeCertForDomain(
domain: string,
certPem: string,
keyPem: string,
validatedX509: crypto.X509Certificate
): Promise<void> {
const wildcard = domain.startsWith("*.");
const existing = await db
.select()
.from(certificates)
.where(eq(certificates.domain, domain))
.limit(1);
let oldCertPem: string | null = null;
let oldKeyPem: string | null = null;
if (existing.length > 0 && existing[0].certFile) {
try {
const storedCertPem = decrypt(
existing[0].certFile,
config.getRawConfig().server.secret!
);
const wildcardUnchanged = existing[0].wildcard === wildcard;
if (storedCertPem === certPem && wildcardUnchanged) {
return;
}
oldCertPem = storedCertPem;
if (existing[0].keyFile) {
try {
oldKeyPem = decrypt(
existing[0].keyFile,
config.getRawConfig().server.secret!
);
} catch (keyErr) {
logger.debug(
`acmeCertSync: could not decrypt stored key for ${domain}: ${keyErr}`
);
}
}
} catch (err) {
logger.debug(
`acmeCertSync: could not decrypt stored cert for ${domain}, will update: ${err}`
);
}
}
let expiresAt: number | null = null;
try {
expiresAt = Math.floor(
new Date(validatedX509.validTo).getTime() / 1000
);
} catch (err) {
logger.debug(
`acmeCertSync: could not parse cert expiry for ${domain}: ${err}`
);
}
const encryptedCert = encrypt(
certPem,
config.getRawConfig().server.secret!
);
const encryptedKey = encrypt(keyPem, config.getRawConfig().server.secret!);
const now = Math.floor(Date.now() / 1000);
const domainId = await findDomainId(domain);
if (domainId) {
logger.debug(
`acmeCertSync: resolved domainId "${domainId}" for cert domain "${domain}"`
);
} else {
logger.debug(
`acmeCertSync: no matching domain record found for cert domain "${domain}"`
);
}
if (existing.length > 0) {
logger.debug(
`acmeCertSync: updating existing certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
);
await db
.update(certificates)
.set({
certFile: encryptedCert,
keyFile: encryptedKey,
status: "valid",
expiresAt,
updatedAt: now,
wildcard,
...(domainId !== null && { domainId })
})
.where(eq(certificates.domain, domain));
logger.debug(
`acmeCertSync: updated certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
);
await pushCertUpdateToAffectedNewts(
domain,
domainId,
oldCertPem,
oldKeyPem
);
} else {
logger.debug(
`acmeCertSync: inserting new certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
);
await db.insert(certificates).values({
domain,
domainId,
certFile: encryptedCert,
keyFile: encryptedKey,
status: "valid",
expiresAt,
createdAt: now,
updatedAt: now,
wildcard
});
logger.debug(
`acmeCertSync: inserted new certificate for ${domain} (expires ${expiresAt ? new Date(expiresAt * 1000).toISOString() : "unknown"})`
);
await pushCertUpdateToAffectedNewts(domain, domainId, null, null);
}
}
function findAcmeJsonFiles(dirPath: string): string[] {
const results: string[] = [];
let entries: fs.Dirent[];
try {
entries = fs.readdirSync(dirPath, { withFileTypes: true });
} catch (err) {
logger.warn(
`acmeCertSync: could not read directory "${dirPath}": ${err}`
);
return results;
}
for (const entry of entries) {
const fullPath = path.join(dirPath, entry.name);
if (entry.isDirectory()) {
results.push(...findAcmeJsonFiles(fullPath));
} else if (entry.isFile()) {
// check if it is a json file
if (entry.name.endsWith(".json")) {
let raw: string;
try {
raw = fs.readFileSync(fullPath, "utf8");
} catch (err) {
logger.warn(
`acmeCertSync: could not read file "${fullPath}": ${err}`
);
continue;
}
let parsed: any;
try {
parsed = JSON.parse(raw);
} catch (err) {
logger.warn(
`acmeCertSync: could not parse "${fullPath}" as JSON: ${err}`
);
continue;
}
}
results.push(fullPath);
}
}
return results;
}
async function syncAcmeCerts(acmeJsonPath: string): Promise<void> {
let raw: string;
try {
raw = fs.readFileSync(acmeJsonPath, "utf8");
} catch (err) {
logger.warn(`acmeCertSync: could not read "${acmeJsonPath}": ${err}`);
return;
}
let acmeJson: AcmeJson;
try {
acmeJson = JSON.parse(raw);
} catch (err) {
logger.warn(
`acmeCertSync: could not parse "${acmeJsonPath}" as JSON: ${err}`
);
return;
}
const resolvers = Object.keys(acmeJson || {});
if (resolvers.length === 0) {
logger.debug(`acmeCertSync: no resolvers found in acme.json`);
return;
}
// Collect certificates from every resolver. If the same domain appears in
// multiple resolvers, the last one wins (resolvers iterated in object order).
const allCerts: AcmeCert[] = [];
for (const resolver of resolvers) {
const resolverData = acmeJson[resolver];
if (!resolverData || !Array.isArray(resolverData.Certificates)) {
logger.debug(
`acmeCertSync: no certificates found for resolver "${resolver}"`
);
continue;
}
// logger.debug(
// `acmeCertSync: found ${resolverData.Certificates.length} certificate(s) for resolver "${resolver}"`
// );
for (const cert of resolverData.Certificates) {
allCerts.push(cert);
}
}
for (const cert of allCerts) {
const mainDomain = cert?.domain?.main;
if (!mainDomain || typeof mainDomain !== "string") {
logger.debug(`acmeCertSync: skipping cert with missing domain`);
continue;
}
if (!cert.certificate || !cert.key) {
logger.debug(
`acmeCertSync: skipping cert for ${mainDomain} - empty certificate or key field`
);
continue;
}
let certPem: string;
let keyPem: string;
try {
certPem = Buffer.from(cert.certificate, "base64").toString("utf8");
keyPem = Buffer.from(cert.key, "base64").toString("utf8");
} catch (err) {
logger.debug(
`acmeCertSync: skipping cert for ${mainDomain} - failed to base64-decode cert/key: ${err}`
);
continue;
}
if (!certPem.trim() || !keyPem.trim()) {
logger.debug(
`acmeCertSync: skipping cert for ${mainDomain} - blank PEM after base64 decode`
);
continue;
}
// Validate that the decoded data actually parses as a real X.509 cert
// before we touch the database. This prevents importing partially-written
// or corrupted entries from acme.json.
const firstCertPemForValidation = extractFirstCert(certPem);
if (!firstCertPemForValidation) {
logger.debug(
`acmeCertSync: skipping cert for ${mainDomain} - no PEM certificate block found`
);
continue;
}
let validatedX509: crypto.X509Certificate;
try {
validatedX509 = new crypto.X509Certificate(
firstCertPemForValidation
);
} catch (err) {
logger.debug(
`acmeCertSync: skipping cert for ${mainDomain} - invalid X.509 certificate: ${err}`
);
continue;
}
// Sanity-check the private key parses too
try {
crypto.createPrivateKey(keyPem);
} catch (err) {
logger.debug(
`acmeCertSync: skipping cert for ${mainDomain} - invalid private key: ${err}`
);
continue;
}
// Collect all domains covered by this cert: main + every SAN.
// Each domain gets its own row in the certificates table so that
// lookups by any hostname on the cert succeed independently.
const allDomains = new Set<string>([mainDomain]);
if (Array.isArray(cert.domain?.sans)) {
for (const san of cert.domain.sans) {
if (typeof san === "string" && san.trim()) {
allDomains.add(san.trim());
}
}
}
// logger.debug(
// `acmeCertSync: cert for ${mainDomain} covers ${allDomains.size} domain(s): ${[...allDomains].join(", ")}`
// );
for (const domain of allDomains) {
try {
await storeCertForDomain(
domain,
certPem,
keyPem,
validatedX509
);
} catch (err) {
logger.error(
`acmeCertSync: error storing cert for domain "${domain}": ${err}`
);
}
}
}
}
export function initAcmeCertSync(): void {
// stub
}
if (build == "saas") {
logger.debug(`acmeCertSync: skipping ACME cert sync in SaaS build`);
return;
}
const configData = config.getRawConfig();
if (!configData.flags?.enable_acme_cert_sync) {
logger.debug(
`acmeCertSync: ACME cert sync is disabled by config flag, skipping`
);
return;
}
const acmeJsonPath =
configData.acme?.acme_json_path ?? "config/letsencrypt/acme.json";
const intervalMs = configData.acme?.sync_interval_ms ?? 5000;
const httpEndpoint = configData.acme?.acme_http_endpoint;
logger.debug(
`acmeCertSync: starting ACME cert sync from "${acmeJsonPath}" across all resolvers every ${intervalMs}ms`
);
if (httpEndpoint) {
logger.debug(
`acmeCertSync: also syncing from HTTP endpoint "${httpEndpoint}" every ${intervalMs}ms`
);
}
const runSync = () => {
if (httpEndpoint) {
syncAcmeCertsFromHttp(httpEndpoint).catch((err) => {
logger.error(`acmeCertSync: error during HTTP sync: ${err}`);
});
} else {
// only run the file-based sync if the HTTP endpoint is not configured, to avoid doubling up
let stat: fs.Stats | null = null;
try {
stat = fs.statSync(acmeJsonPath);
} catch (err) {
logger.warn(
`acmeCertSync: cannot stat path "${acmeJsonPath}": ${err}`
);
return;
}
if (stat.isDirectory()) {
const files = findAcmeJsonFiles(acmeJsonPath);
if (files.length === 0) {
logger.debug(
`acmeCertSync: no acme.json files found in directory "${acmeJsonPath}"`
);
return;
}
// logger.debug(
// `acmeCertSync: found ${files.length} acme.json file(s) in directory "${acmeJsonPath}"`
// );
for (const file of files) {
syncAcmeCerts(file).catch((err) => {
logger.error(
`acmeCertSync: error during sync of "${file}": ${err}`
);
});
}
} else {
syncAcmeCerts(acmeJsonPath).catch((err) => {
logger.error(`acmeCertSync: error during sync: ${err}`);
});
}
}
};
// Run immediately on init, then on the configured interval
runSync();
setInterval(runSync, intervalMs);
}
+616
View File
@@ -0,0 +1,616 @@
import {
and,
eq,
gte,
inArray,
isNull,
or,
sql,
SQL,
type InferInsertModel
} from "drizzle-orm";
import {
AiBudget,
aiBudgetBreachEvents,
aiBudgets,
aiModels,
aiUsageRecords,
db,
logsDb,
userOrgRoles
} from "@server/db";
import { modelKeyMatches } from "@server/lib/aiModelKeyMatch";
import type { AiUsage } from "@server/lib/aiUsageExtraction";
import { regionalCache as cache } from "#dynamic/lib/cache";
import logger from "@server/logger";
type BudgetPeriod = AiBudget["period"];
const PERIOD_DURATIONS_MS: Record<Exclude<BudgetPeriod, "lifetime">, number> = {
hourly: 60 * 60 * 1000,
daily: 24 * 60 * 60 * 1000,
weekly: 7 * 24 * 60 * 60 * 1000,
monthly: 30 * 24 * 60 * 60 * 1000,
yearly: 365 * 24 * 60 * 60 * 1000
};
// Budgets are cheap to be a little stale about (enforcement is already
// check-then-act, not transactional). Re-derive each budget's usage sum
// from aiUsageRecords at most this often; in between, completed requests
// just add their own contribution onto the cached sum instead of
// re-querying/re-aggregating from scratch.
const BUDGET_CACHE_REFRESH_MS = 8_000;
// Redis-level TTL is only a safety net for eviction if a budget stops
// seeing traffic - the actual staleness check is the computedAt timestamp
// stored in the cached value, compared against BUDGET_CACHE_REFRESH_MS.
const BUDGET_CACHE_SAFETY_TTL_SEC = 60;
function applicableBudgetsCacheKey(ctx: BudgetScopeContext): string {
const roleKey = [...ctx.roleIds].sort((a, b) => a - b).join(",");
return [
"aiBudget:applicable",
ctx.orgId,
ctx.providerId,
ctx.requestedModel,
ctx.resourceId ?? "",
ctx.siteResourceId ?? "",
roleKey,
ctx.virtualApiKeyId ?? ""
].join(":");
}
function budgetUsageCacheKey(budgetId: number): string {
return `aiBudget:usage:${budgetId}`;
}
type CachedBudgetUsage = {
sum: number;
computedAt: number;
};
// Budget periods are trailing windows from "now", not calendar-aligned
// (e.g. "daily" = last 24h). "lifetime" has no lower bound.
function windowStart(period: BudgetPeriod, now: number): number {
if (period === "lifetime") {
return 0;
}
return now - PERIOD_DURATIONS_MS[period];
}
export type BudgetScopeContext = {
orgId: string;
providerId: number;
requestedModel: string;
resourceId: number | null;
siteResourceId: number | null;
roleIds: number[];
requestUserId: string | null;
virtualApiKeyId: string | null;
};
/**
* Every budget that could apply to this request: the provider itself, any
* model on that provider whose (possibly wildcarded) modelKey matches the
* requested model, the target resource/site-resource, and any role the
* requesting user holds in the org. Cached for BUDGET_CACHE_REFRESH_MS since
* budget/model config changes are rare and a request-scoped org/provider/
* model/resource/role combination repeats constantly under real traffic.
*/
export async function resolveApplicableBudgets(
ctx: BudgetScopeContext
): Promise<AiBudget[]> {
const cacheKey = applicableBudgetsCacheKey(ctx);
const cached = await cache.get<AiBudget[]>(cacheKey);
if (cached !== undefined) {
return cached;
}
const budgets = await fetchApplicableBudgets(ctx);
await cache.set(cacheKey, budgets, BUDGET_CACHE_REFRESH_MS / 1000);
return budgets;
}
async function fetchApplicableBudgets(
ctx: BudgetScopeContext
): Promise<AiBudget[]> {
const providerModels = await db
.select({ modelId: aiModels.modelId, modelKey: aiModels.modelKey })
.from(aiModels)
.where(
and(
eq(aiModels.providerId, ctx.providerId),
eq(aiModels.enabled, true)
)
);
const matchingModelIds = providerModels
.filter((m) => modelKeyMatches(m.modelKey, ctx.requestedModel))
.map((m) => m.modelId);
const scopeConditions: SQL[] = [
and(
eq(aiBudgets.providerId, ctx.providerId),
isNull(aiBudgets.modelId)
)!
];
if (matchingModelIds.length > 0) {
scopeConditions.push(inArray(aiBudgets.modelId, matchingModelIds));
}
if (ctx.resourceId != null) {
scopeConditions.push(eq(aiBudgets.resourceId, ctx.resourceId));
}
if (ctx.siteResourceId != null) {
scopeConditions.push(eq(aiBudgets.siteResourceId, ctx.siteResourceId));
}
if (ctx.roleIds.length > 0) {
scopeConditions.push(inArray(aiBudgets.roleId, ctx.roleIds));
}
if (ctx.virtualApiKeyId != null) {
scopeConditions.push(
eq(aiBudgets.virtualApiKeyId, ctx.virtualApiKeyId)
);
}
return db
.select()
.from(aiBudgets)
.where(
and(
eq(aiBudgets.orgId, ctx.orgId),
eq(aiBudgets.enabled, true),
or(...scopeConditions)
)
);
}
async function sumUsageAmount(
where: SQL,
unit: AiBudget["unit"]
): Promise<number> {
const column =
unit === "usd" ? aiUsageRecords.costUsd : aiUsageRecords.totalTokens;
const [row] = await logsDb
.select({ total: sql<number>`coalesce(sum(${column}), 0)` })
.from(aiUsageRecords)
.where(where);
return Number(row?.total ?? 0);
}
/**
* Sums recorded usage for a single budget's scope + rolling window. Model
* budgets can't be pushed down to SQL because the model's key may itself be
* a glob, so those rows are fetched for the provider+window and matched in
* JS the same way access-control matching does.
*/
export async function sumUsageForBudget(
budget: AiBudget,
ctx: BudgetScopeContext,
now: number
): Promise<number> {
const start = windowStart(budget.period, now);
if (budget.modelId != null) {
const [model] = await db
.select({
providerId: aiModels.providerId,
modelKey: aiModels.modelKey
})
.from(aiModels)
.where(eq(aiModels.modelId, budget.modelId))
.limit(1);
if (!model) {
return 0;
}
const rows = await logsDb
.select({
requestedModel: aiUsageRecords.requestedModel,
costUsd: aiUsageRecords.costUsd,
totalTokens: aiUsageRecords.totalTokens
})
.from(aiUsageRecords)
.where(
and(
eq(aiUsageRecords.orgId, ctx.orgId),
eq(aiUsageRecords.providerId, model.providerId),
gte(aiUsageRecords.createdAt, start)
)
);
return rows
.filter((r) => modelKeyMatches(model.modelKey, r.requestedModel))
.reduce(
(sum, r) =>
sum +
(budget.unit === "usd" ? (r.costUsd ?? 0) : r.totalTokens),
0
);
}
if (budget.providerId != null) {
return sumUsageAmount(
and(
eq(aiUsageRecords.orgId, ctx.orgId),
eq(aiUsageRecords.providerId, budget.providerId),
gte(aiUsageRecords.createdAt, start)
)!,
budget.unit
);
}
if (budget.resourceId != null) {
return sumUsageAmount(
and(
eq(aiUsageRecords.orgId, ctx.orgId),
eq(aiUsageRecords.resourceId, budget.resourceId),
gte(aiUsageRecords.createdAt, start)
)!,
budget.unit
);
}
if (budget.siteResourceId != null) {
return sumUsageAmount(
and(
eq(aiUsageRecords.orgId, ctx.orgId),
eq(aiUsageRecords.siteResourceId, budget.siteResourceId),
gte(aiUsageRecords.createdAt, start)
)!,
budget.unit
);
}
if (budget.roleId != null) {
const members = await db
.select({ userId: userOrgRoles.userId })
.from(userOrgRoles)
.where(
and(
eq(userOrgRoles.roleId, budget.roleId),
eq(userOrgRoles.orgId, ctx.orgId)
)
);
const userIds = members.map((m) => m.userId);
if (userIds.length === 0) {
return 0;
}
return sumUsageAmount(
and(
eq(aiUsageRecords.orgId, ctx.orgId),
inArray(aiUsageRecords.userId, userIds),
gte(aiUsageRecords.createdAt, start)
)!,
budget.unit
);
}
if (budget.virtualApiKeyId != null) {
return sumUsageAmount(
and(
eq(aiUsageRecords.orgId, ctx.orgId),
eq(aiUsageRecords.virtualApiKeyId, budget.virtualApiKeyId),
gte(aiUsageRecords.createdAt, start)
)!,
budget.unit
);
}
return 0;
}
/**
* Cached wrapper around sumUsageForBudget. Reuses a per-budget cached sum
* for up to BUDGET_CACHE_REFRESH_MS, and otherwise falls through to the DB
* aggregation and reseeds the cache. Completed requests within that window
* top the cached sum up via applyUsageToBudgetCache below rather than
* forcing a re-aggregation on every request.
*/
async function getBudgetUsage(
budget: AiBudget,
ctx: BudgetScopeContext,
now: number
): Promise<number> {
const cacheKey = budgetUsageCacheKey(budget.budgetId);
const cached = await cache.get<CachedBudgetUsage>(cacheKey);
if (cached && now - cached.computedAt < BUDGET_CACHE_REFRESH_MS) {
return cached.sum;
}
const sum = await sumUsageForBudget(budget, ctx, now);
await cache.set(
cacheKey,
{ sum, computedAt: now } satisfies CachedBudgetUsage,
BUDGET_CACHE_SAFETY_TTL_SEC
);
return sum;
}
/**
* Called once a request's actual usage is known, for every budget that was
* resolved as applicable to it (i.e. checkBudgets' returned `budgets`).
* Adds this request's contribution directly onto each budget's cached sum
* so the next request in the same refresh window doesn't need to re-query
* or re-aggregate. If there's no warm cache entry, or it's already due for
* a refresh, this is a no-op - the next reader re-derives from the DB,
* which by then already includes this request's row via recordUsage.
*/
export async function applyUsageToBudgetCache(
budgets: AiBudget[],
usage: { usd: number; tokens: number }
): Promise<void> {
await Promise.all(
budgets.map(async (budget) => {
const delta = budget.unit === "usd" ? usage.usd : usage.tokens;
if (!delta) {
return;
}
const cacheKey = budgetUsageCacheKey(budget.budgetId);
const cached = await cache.get<CachedBudgetUsage>(cacheKey);
if (
!cached ||
Date.now() - cached.computedAt >= BUDGET_CACHE_REFRESH_MS
) {
return;
}
await cache.set(
cacheKey,
{
sum: cached.sum + delta,
computedAt: cached.computedAt
} satisfies CachedBudgetUsage,
BUDGET_CACHE_SAFETY_TTL_SEC
);
})
);
}
// Throttled to one durable event per budget per breach window, so a soft
// budget being exceeded doesn't write a row on every subsequent request
// while it stays over.
async function recordBreachEventIfNew(
budget: AiBudget,
ctx: BudgetScopeContext,
usageAmount: number,
now: number
): Promise<void> {
try {
const start = windowStart(budget.period, now);
const [existing] = await db
.select({ id: aiBudgetBreachEvents.id })
.from(aiBudgetBreachEvents)
.where(
and(
eq(aiBudgetBreachEvents.budgetId, budget.budgetId),
gte(aiBudgetBreachEvents.createdAt, start)
)
)
.limit(1);
if (existing) {
return;
}
await db.insert(aiBudgetBreachEvents).values({
orgId: ctx.orgId,
budgetId: budget.budgetId,
enforcement: budget.enforcement,
unit: budget.unit,
period: budget.period,
amount: budget.amount,
usageAmount,
blocked: budget.enforcement === "hard",
requestUserId: ctx.requestUserId,
createdAt: now
});
} catch (error) {
logger.error("Failed to record AI budget breach event", {
error,
budgetId: budget.budgetId
});
}
}
export type BudgetCheckResult = {
blocked: boolean;
blockingBudget?: AiBudget;
// Every budget resolved as applicable to this request, regardless of
// whether it was breached - pass to applyUsageToBudgetCache once this
// request's actual usage is known.
budgets: AiBudget[];
};
export async function checkBudgets(
ctx: BudgetScopeContext
): Promise<BudgetCheckResult> {
const budgets = await resolveApplicableBudgets(ctx);
if (budgets.length === 0) {
return { blocked: false, budgets: [] };
}
const now = Date.now();
let blockingBudget: AiBudget | undefined;
for (const budget of budgets) {
const usage = await getBudgetUsage(budget, ctx, now);
if (usage < budget.amount) {
continue;
}
await recordBreachEventIfNew(budget, ctx, usage, now);
if (budget.enforcement === "hard" && !blockingBudget) {
blockingBudget = budget;
}
}
return blockingBudget
? { blocked: true, blockingBudget, budgets }
: { blocked: false, budgets };
}
export type UsageRecordInput = {
orgId: string;
providerId: number;
resourceId: number | null;
siteResourceId: number | null;
userId: string | null;
virtualApiKeyId: string | null;
requestedModel: string;
usage: AiUsage;
costUsd: number | null;
createdAt?: number;
// Same id as the aiSessionLog row logged for this request, so the two
// can be joined to show token/cost usage alongside the session
// transcript. Undefined when the session wasn't logged (e.g. session
// log retention disabled for the org).
sessionId?: string;
};
type AiUsageRecordInsert = InferInsertModel<typeof aiUsageRecords>;
// In-memory buffer for batching AI usage record inserts, mirroring the
// approach in server/routers/badger/logRequestAudit.ts. Usage rows are read
// back on every budget-cache miss (see getBudgetUsage above), which happens
// at least every BUDGET_CACHE_REFRESH_MS, so this buffer is flushed much
// more aggressively than the request audit log to keep the table from
// lagging behind what budget enforcement needs. Unlike the audit log, there
// is no retention/cleanup job for this table - usage history is kept
// indefinitely for billing and historical reporting.
const usageRecordBuffer: AiUsageRecordInsert[] = [];
const USAGE_BATCH_SIZE = 20; // Write to DB every 20 records
const USAGE_BATCH_INTERVAL_MS = 1000; // Or every 1 second, whichever comes first
const USAGE_MAX_BUFFER_SIZE = 5000; // Prevent unbounded memory growth
let usageFlushTimer: NodeJS.Timeout | null = null;
let isUsageFlushInProgress = false;
async function flushUsageRecords() {
if (usageRecordBuffer.length === 0 || isUsageFlushInProgress) {
return;
}
isUsageFlushInProgress = true;
const recordsToWrite = usageRecordBuffer.splice(
0,
usageRecordBuffer.length
);
try {
// Use a transaction to ensure all inserts succeed or fail together
await logsDb.transaction(async (tx) => {
// Batch insert in groups to avoid overwhelming the database
const DB_BATCH_SIZE = 25;
for (let i = 0; i < recordsToWrite.length; i += DB_BATCH_SIZE) {
const batch = recordsToWrite.slice(i, i + DB_BATCH_SIZE);
await tx.insert(aiUsageRecords).values(batch);
}
});
logger.debug(
`Flushed ${recordsToWrite.length} AI usage records to database`
);
} catch (error) {
logger.error("Error flushing AI usage records:", error);
// On transaction error, put records back at the front of the buffer
// to retry, but only if the buffer isn't too large
if (
usageRecordBuffer.length <
USAGE_MAX_BUFFER_SIZE - recordsToWrite.length
) {
usageRecordBuffer.unshift(...recordsToWrite);
logger.info(
`Re-queued ${recordsToWrite.length} AI usage records for retry`
);
} else {
logger.error(
`Buffer full, dropped ${recordsToWrite.length} AI usage records`
);
}
} finally {
isUsageFlushInProgress = false;
// If buffer filled up while we were flushing, flush again
if (usageRecordBuffer.length >= USAGE_BATCH_SIZE) {
flushUsageRecords().catch((err) =>
logger.error("Error in follow-up AI usage flush:", err)
);
}
}
}
function scheduleUsageFlush() {
if (usageFlushTimer === null) {
usageFlushTimer = setTimeout(() => {
usageFlushTimer = null;
flushUsageRecords().catch((err) =>
logger.error("Error in scheduled AI usage flush:", err)
);
}, USAGE_BATCH_INTERVAL_MS);
}
}
/**
* Gracefully flush all pending AI usage records (call this on shutdown).
*/
export async function shutdownUsageRecorder() {
if (usageFlushTimer) {
clearTimeout(usageFlushTimer);
usageFlushTimer = null;
}
// Force flush even if one is in progress by waiting and retrying
while (isUsageFlushInProgress) {
await new Promise((resolve) => setTimeout(resolve, 100));
}
await flushUsageRecords();
}
export async function recordUsage(input: UsageRecordInput): Promise<void> {
try {
const { usage } = input;
const totalTokens =
usage.promptTokens +
usage.cacheReadTokens +
usage.cacheWriteTokens +
usage.completionTokens +
usage.reasoningTokens;
// Prevent unbounded buffer growth - drop oldest entries if buffer is too large
if (usageRecordBuffer.length >= USAGE_MAX_BUFFER_SIZE) {
const dropped = usageRecordBuffer.splice(0, USAGE_BATCH_SIZE);
logger.warn(
`AI usage record buffer exceeded max size (${USAGE_MAX_BUFFER_SIZE}), dropped ${dropped.length} oldest entries`
);
}
const timestamp = Math.floor(Date.now() / 1000);
usageRecordBuffer.push({
orgId: input.orgId,
providerId: input.providerId,
resourceId: input.resourceId,
siteResourceId: input.siteResourceId,
userId: input.userId,
virtualApiKeyId: input.virtualApiKeyId,
sessionId: input.sessionId,
requestedModel: input.requestedModel,
promptTokens: usage.promptTokens,
cacheReadTokens: usage.cacheReadTokens,
cacheWriteTokens: usage.cacheWriteTokens,
completionTokens: usage.completionTokens,
reasoningTokens: usage.reasoningTokens,
totalTokens,
costUsd: input.costUsd,
estimated: usage.estimated,
createdAt: input.createdAt ?? timestamp
});
// Flush immediately if buffer is full, otherwise schedule a flush
if (usageRecordBuffer.length >= USAGE_BATCH_SIZE) {
flushUsageRecords().catch((err) =>
logger.error("Error flushing AI usage records:", err)
);
} else {
scheduleUsageFlush();
}
} catch (error) {
logger.error("Failed to record AI usage", { error });
}
}
+354
View File
@@ -0,0 +1,354 @@
import type { Request } from "express";
import { AI_CAPABILITIES, type AiCapability } from "@app/lib/aiCapabilities";
export { AI_CAPABILITIES, type AiCapability };
export type AiCapabilityRoute = {
method: "GET" | "POST";
path: string;
};
export type AiProtocolFamily = "openai" | "anthropic" | "google" | "bedrock";
export type AiCapabilityDefinition = {
id: AiCapability;
protocolFamily: AiProtocolFamily;
routes: AiCapabilityRoute[];
extractModel: (req: Request) => string | undefined;
resolveUpstreamUrl: (
baseUrl: string,
req: Request,
model: string
) => string;
isStreaming: (req: Request, contentType: string) => boolean;
};
function bodyModel(req: Request): string | undefined {
return typeof req.body?.model === "string" ? req.body.model : undefined;
}
function paramModel(req: Request): string | undefined {
const model = req.params?.model;
return typeof model === "string" && model.length > 0 ? model : undefined;
}
export function joinUpstreamUrl(baseUrl: string, path: string): string {
const base = baseUrl.replace(/\/+$/, "");
let suffix = path.startsWith("/") ? path : `/${path}`;
let basePathname = "/";
try {
basePathname = new URL(base).pathname.replace(/\/+$/, "") || "/";
} catch {
// Fall through with "/" non-absolute bases are not expected in
// production, but keep joining usable for malformed input.
}
if (basePathname !== "/") {
const baseSegs = basePathname.split("/").filter(Boolean);
const pathSegs = suffix.split("/").filter(Boolean);
const max = Math.min(baseSegs.length, pathSegs.length);
let overlap = 0;
for (let n = max; n >= 1; n--) {
const baseSuffix = baseSegs.slice(-n);
const pathPrefix = pathSegs.slice(0, n);
if (baseSuffix.every((seg, i) => seg === pathPrefix[i])) {
overlap = n;
break;
}
}
if (overlap > 0) {
const remaining = pathSegs.slice(overlap);
suffix = remaining.length > 0 ? `/${remaining.join("/")}` : "/";
}
}
if (suffix === "/") {
return base;
}
return `${base}${suffix}`;
}
function pathFromRequest(req: Request): string {
const raw = req.originalUrl || req.url || req.path;
return raw.startsWith("/") ? raw : `/${raw}`;
}
function bodyRequestsStream(req: Request): boolean {
return req.body?.stream === true;
}
function contentTypeIsSse(contentType: string): boolean {
return contentType.includes("text/event-stream");
}
function contentTypeIsAmazonEventStream(contentType: string): boolean {
return contentType.includes("application/vnd.amazon.eventstream");
}
function pathIncludes(req: Request, fragment: string): boolean {
return pathFromRequest(req).includes(fragment);
}
function isBodyOrSseStreaming(req: Request, contentType: string): boolean {
return bodyRequestsStream(req) || contentTypeIsSse(contentType);
}
function isGeminiStyleStreaming(req: Request, contentType: string): boolean {
return (
pathIncludes(req, "streamGenerateContent") ||
pathIncludes(req, "alt=sse") ||
contentTypeIsSse(contentType)
);
}
export const AI_CAPABILITY_DEFS: Record<AiCapability, AiCapabilityDefinition> =
{
openai_chat: {
id: "openai_chat",
protocolFamily: "openai",
routes: [
{ method: "POST", path: "/v1/chat/completions" },
{ method: "POST", path: "/chat/completions" }
],
extractModel: bodyModel,
resolveUpstreamUrl: (base, req) =>
joinUpstreamUrl(base, pathFromRequest(req)),
isStreaming: isBodyOrSseStreaming
},
openai_responses: {
id: "openai_responses",
protocolFamily: "openai",
routes: [{ method: "POST", path: "/v1/responses" }],
extractModel: bodyModel,
resolveUpstreamUrl: (base, req) =>
joinUpstreamUrl(base, pathFromRequest(req)),
isStreaming: isBodyOrSseStreaming
},
anthropic_messages: {
id: "anthropic_messages",
protocolFamily: "anthropic",
routes: [{ method: "POST", path: "/v1/messages" }],
extractModel: bodyModel,
resolveUpstreamUrl: (base, req) =>
joinUpstreamUrl(base, pathFromRequest(req)),
isStreaming: isBodyOrSseStreaming
},
v1_models: {
id: "v1_models",
protocolFamily: "anthropic",
routes: [
{ method: "GET", path: "/v1/models" },
{ method: "GET", path: "/v1/models/:model" }
],
extractModel: paramModel,
resolveUpstreamUrl: (base, req) =>
joinUpstreamUrl(base, pathFromRequest(req)),
// Model listings are answered from the gateway's own view of the
// provider allow/block lists rather than proxied upstream, so
// there is never a stream to detect.
isStreaming: () => false
},
gemini_generate_content: {
id: "gemini_generate_content",
protocolFamily: "google",
routes: [
{
method: "POST",
path: "/v1beta/models/:model\\:generateContent"
},
{
method: "POST",
path: "/v1beta/models/:model\\:streamGenerateContent"
}
],
extractModel: paramModel,
resolveUpstreamUrl: (base, req) =>
joinUpstreamUrl(base, pathFromRequest(req)),
isStreaming: isGeminiStyleStreaming
},
google_generate_content: {
id: "google_generate_content",
protocolFamily: "google",
routes: [
{
method: "POST",
// Vertex publisher model generateContent
path: "/v1/projects/:project/locations/:location/publishers/:publisher/models/:model\\:generateContent"
},
{
method: "POST",
path: "/v1/projects/:project/locations/:location/publishers/:publisher/models/:model\\:streamGenerateContent"
}
],
extractModel: paramModel,
resolveUpstreamUrl: (base, req) =>
joinUpstreamUrl(base, pathFromRequest(req)),
isStreaming: isGeminiStyleStreaming
},
google_raw_predict: {
id: "google_raw_predict",
protocolFamily: "google",
routes: [
{
method: "POST",
path: "/v1/projects/:project/locations/:location/publishers/:publisher/models/:model\\:rawPredict"
},
{
method: "POST",
path: "/v1/projects/:project/locations/:location/publishers/:publisher/models/:model\\:streamRawPredict"
}
],
extractModel: paramModel,
resolveUpstreamUrl: (base, req) =>
joinUpstreamUrl(base, pathFromRequest(req)),
isStreaming: (req, contentType) =>
pathIncludes(req, "streamRawPredict") ||
pathIncludes(req, "alt=sse") ||
contentTypeIsSse(contentType)
},
bedrock_model_invoke: {
id: "bedrock_model_invoke",
protocolFamily: "bedrock",
routes: [
{ method: "POST", path: "/model/:model/invoke" },
{
method: "POST",
path: "/model/:model/invoke-with-response-stream"
}
],
extractModel: paramModel,
resolveUpstreamUrl: (base, req) =>
joinUpstreamUrl(base, pathFromRequest(req)),
isStreaming: (req, contentType) =>
pathIncludes(req, "invoke-with-response-stream") ||
contentTypeIsAmazonEventStream(contentType) ||
contentTypeIsSse(contentType)
},
bedrock_converse: {
id: "bedrock_converse",
protocolFamily: "bedrock",
routes: [
{ method: "POST", path: "/model/:model/converse" },
{ method: "POST", path: "/model/:model/converse-stream" }
],
extractModel: paramModel,
resolveUpstreamUrl: (base, req) =>
joinUpstreamUrl(base, pathFromRequest(req)),
isStreaming: (req, contentType) =>
pathIncludes(req, "converse-stream") ||
contentTypeIsAmazonEventStream(contentType) ||
contentTypeIsSse(contentType)
}
};
export function isAiCapability(value: unknown): value is AiCapability {
return (
typeof value === "string" &&
(AI_CAPABILITIES as readonly string[]).includes(value)
);
}
/**
* Convert an Express-style route path from AI_CAPABILITY_DEFS into a RegExp.
* Handles `:param` segments and escaped literal colons (`\:`).
*/
export function routePatternToRegExp(routePath: string): RegExp {
let pattern = "";
for (let i = 0; i < routePath.length; i++) {
const ch = routePath[i];
if (
ch === "\\" &&
i + 1 < routePath.length &&
routePath[i + 1] === ":"
) {
pattern += ":";
i++;
continue;
}
if (ch === ":") {
// Named param: consume until next / or end
i++;
while (
i < routePath.length &&
routePath[i] !== "/" &&
!(routePath[i] === "\\" && routePath[i + 1] === ":")
) {
i++;
}
i--; // loop will ++
pattern += "[^/]+";
continue;
}
// Escape regex special chars
if (/[.*+?^${}()|[\]\\]/.test(ch)) {
pattern += "\\" + ch;
} else {
pattern += ch;
}
}
return new RegExp(`^${pattern}$`);
}
export function resolveAiCapabilityFromPath(path: string): AiCapability | null {
const pathname = path.split("?")[0] || "/";
const normalized = pathname.startsWith("/") ? pathname : `/${pathname}`;
for (const def of Object.values(AI_CAPABILITY_DEFS)) {
for (const route of def.routes) {
if (routePatternToRegExp(route.path).test(normalized)) {
return def.id;
}
}
}
return null;
}
export function parseCapabilities(raw: unknown): AiCapability[] {
if (raw == null) {
return [];
}
let parsed: unknown = raw;
if (typeof raw === "string") {
const trimmed = raw.trim();
if (!trimmed) {
return [];
}
try {
parsed = JSON.parse(trimmed);
} catch {
return [];
}
}
if (!Array.isArray(parsed)) {
return [];
}
const out: AiCapability[] = [];
const seen = new Set<AiCapability>();
for (const item of parsed) {
if (isAiCapability(item) && !seen.has(item)) {
seen.add(item);
out.push(item);
}
}
return out;
}
export function serializeCapabilities(capabilities: AiCapability[]): string {
return JSON.stringify(capabilities);
}
export function providerHasCapability(
capabilities: AiCapability[] | string | null | undefined,
capability: AiCapability
): boolean {
const list =
typeof capabilities === "string" || capabilities == null
? parseCapabilities(capabilities)
: capabilities;
return list.includes(capability);
}
+139
View File
@@ -0,0 +1,139 @@
import {
AI_CAPABILITY_DEFS,
type AiCapability,
type AiProtocolFamily
} from "@server/lib/aiCapabilities";
import HttpCode from "@server/types/HttpCode";
export type ClientErrorResponse = {
statusCode?: number;
contentType?: string;
body: string;
};
export type AiCapabilityErrorKind =
| "authentication"
| "invalid_request"
| "not_found"
| "permission"
| "rate_limit"
| "internal";
const AUTH_MESSAGE = "Invalid API key provided.";
type KindFields = {
openaiType: string;
openaiCode: string | null;
anthropicType: string;
googleStatus: string;
};
const KIND_FIELDS: Record<AiCapabilityErrorKind, KindFields> = {
authentication: {
openaiType: "authentication_error",
openaiCode: "invalid_api_key",
anthropicType: "authentication_error",
googleStatus: "UNAUTHENTICATED"
},
invalid_request: {
openaiType: "invalid_request_error",
openaiCode: null,
anthropicType: "invalid_request_error",
googleStatus: "INVALID_ARGUMENT"
},
not_found: {
openaiType: "invalid_request_error",
openaiCode: null,
anthropicType: "not_found_error",
googleStatus: "NOT_FOUND"
},
permission: {
openaiType: "invalid_request_error",
openaiCode: null,
anthropicType: "permission_error",
googleStatus: "PERMISSION_DENIED"
},
rate_limit: {
openaiType: "rate_limit_error",
openaiCode: "rate_limit_exceeded",
anthropicType: "rate_limit_error",
googleStatus: "RESOURCE_EXHAUSTED"
},
internal: {
openaiType: "api_error",
openaiCode: null,
anthropicType: "api_error",
googleStatus: "INTERNAL"
}
};
function resolveProtocolFamily(
capability: AiCapability | null
): AiProtocolFamily {
if (capability == null) {
return "openai";
}
return AI_CAPABILITY_DEFS[capability].protocolFamily;
}
/**
* Build a protocol-native error body for the given capability.
* Message stays contextual; only the envelope/machine fields follow the
* capability's native API shape.
*/
export function buildAiCapabilityErrorBody(
capability: AiCapability | null,
kind: AiCapabilityErrorKind,
message: string,
httpStatus?: number
): Record<string, unknown> {
const family = resolveProtocolFamily(capability);
const fields = KIND_FIELDS[kind];
switch (family) {
case "openai":
return {
error: {
message,
type: fields.openaiType,
param: null,
code: fields.openaiCode
}
};
case "anthropic":
return {
type: "error",
error: {
type: fields.anthropicType,
message
}
};
case "google":
return {
error: {
code: httpStatus ?? HttpCode.BAD_REQUEST,
message,
status: fields.googleStatus
}
};
case "bedrock":
return { message };
}
}
export function buildInferenceAuthClientError(
capability: AiCapability | null
): ClientErrorResponse {
return {
statusCode: HttpCode.UNAUTHORIZED,
contentType: "application/json",
body: JSON.stringify(
buildAiCapabilityErrorBody(
capability,
"authentication",
AUTH_MESSAGE,
HttpCode.UNAUTHORIZED
)
)
};
}
+69
View File
@@ -0,0 +1,69 @@
import { createHash } from "crypto";
import config from "@server/lib/config";
export const AI_GATEWAY_TRUST_HEADER = "X-Pangolin-Ai-Gateway-Auth";
// Injected by the same Traefik trust middleware as AI_GATEWAY_TRUST_HEADER,
// but its value differs per router (public inference resource vs. private
// siteResource) so the gateway can tell which kind of resource a trusted
// request arrived on without re-deriving it from resourceId/siteResourceId.
export const AI_GATEWAY_RESOURCE_TYPE_HEADER =
"X-Pangolin-Ai-Gateway-Resource-Type";
export type AiGatewayResourceType = "resource" | "site-resource";
// Opt-in (server.enable_ai_gateway_client_ip_header): carries the client IP
// that Badger resolved at the Traefik hop, so it survives an intermediary
// proxy between Traefik and the AI gateway that overwrites
// X-Forwarded-For/X-Real-Ip instead of appending to them. Set by a
// disableForwardAuth Badger middleware instance (see getTraefikConfig.ts)
// on the site-resource inference router only, since that's the sole path
// that resolves request identity from the client IP.
export const AI_GATEWAY_CLIENT_IP_HEADER = "X-Pangolin-Client-Ip";
/**
* Derive a Traefik-injected trust token from the server secret.
* Traefik overwrites this header on inference routes so the AI gateway can
* trust Badger-injected Remote-* identity without re-validating credentials.
*/
export function deriveAiGatewayTrustToken(secret: string): string {
return createHash("sha256")
.update(`ai-gateway-trust:${secret}`)
.digest("hex");
}
export function getAiGatewayTrustToken(): string {
const secret = config.getRawConfig().server.secret;
if (!secret) {
throw new Error("Server secret is required for AI gateway trust token");
}
return deriveAiGatewayTrustToken(secret);
}
export function isAiGatewayTrustHeaderValid(
headers: Record<string, string | string[] | undefined> | undefined,
expectedToken?: string
): boolean {
if (!headers) {
return false;
}
const expected = expectedToken ?? getAiGatewayTrustToken();
const raw =
headers[AI_GATEWAY_TRUST_HEADER] ??
headers[AI_GATEWAY_TRUST_HEADER.toLowerCase()];
const value = Array.isArray(raw) ? raw[0] : raw;
return typeof value === "string" && value === expected;
}
export function getAiGatewayResourceType(
headers: Record<string, string | string[] | undefined> | undefined
): AiGatewayResourceType | null {
if (!headers) {
return null;
}
const raw =
headers[AI_GATEWAY_RESOURCE_TYPE_HEADER] ??
headers[AI_GATEWAY_RESOURCE_TYPE_HEADER.toLowerCase()];
const value = Array.isArray(raw) ? raw[0] : raw;
return value === "resource" || value === "site-resource" ? value : null;
}
+82
View File
@@ -0,0 +1,82 @@
import http from "node:http";
import https from "node:https";
import { Readable } from "node:stream";
type UpstreamFetchInit = {
method: string;
headers: Record<string, string>;
body?: string;
skipTlsVerification?: boolean;
signal?: AbortSignal;
};
const insecureHttpsAgent = new https.Agent({
rejectUnauthorized: false,
keepAlive: true
});
export function aiGatewayUpstreamFetch(
url: string,
init: UpstreamFetchInit
): Promise<Response> {
const parsed = new URL(url);
const isHttps = parsed.protocol === "https:";
const lib = isHttps ? https : http;
const agent =
isHttps && init.skipTlsVerification ? insecureHttpsAgent : undefined;
return new Promise((resolve, reject) => {
if (init.signal?.aborted) {
reject(init.signal.reason ?? new Error("Request aborted"));
return;
}
const req = lib.request(
url,
{
method: init.method,
headers: init.headers,
agent
},
(res) => {
const headers = new Headers();
for (const [key, value] of Object.entries(res.headers)) {
if (value === undefined) {
continue;
}
if (Array.isArray(value)) {
for (const entry of value) {
headers.append(key, entry);
}
} else {
headers.set(key, value);
}
}
const body = Readable.toWeb(res) as ReadableStream<Uint8Array>;
resolve(
new Response(body, {
status: res.statusCode ?? 502,
statusText: res.statusMessage,
headers
})
);
}
);
req.on("error", reject);
if (init.signal) {
const onAbort = () => req.destroy(init.signal!.reason);
init.signal.addEventListener("abort", onAbort, { once: true });
req.on("close", () =>
init.signal!.removeEventListener("abort", onAbort)
);
}
if (init.body !== undefined) {
req.write(init.body);
}
req.end();
});
}
+721
View File
@@ -0,0 +1,721 @@
import { and, eq, inArray } from "drizzle-orm";
import {
aiModels,
aiProviders,
db,
resourceAiModels,
resourceAiProviders,
siteResourceAiModels,
siteResourceAiProviders,
type Transaction
} from "@server/db";
import { z } from "zod";
type DbOrTrx = Transaction | typeof db;
export const modelListTypeSchema = z.enum(["allow", "block"]);
export type ModelListType = z.infer<typeof modelListTypeSchema>;
export const accessModeSchema = z.enum(["inherit", "select"]);
export type AccessMode = z.infer<typeof accessModeSchema>;
export const resourceAiProviderAttachmentSchema = z.strictObject({
providerId: z.number().int().positive(),
accessMode: accessModeSchema.optional().default("inherit"),
enabled: z.boolean().optional().default(true)
});
export type ResourceAiProviderInput = z.infer<
typeof resourceAiProviderAttachmentSchema
>;
export type ResourceAiProviderAttachment = {
providerId: number;
accessMode: AccessMode;
enabled: boolean;
};
export const resourceAiModelEntrySchema = z.strictObject({
modelId: z.number().int().positive(),
listType: modelListTypeSchema
});
export type ResourceAiModelEntry = z.infer<typeof resourceAiModelEntrySchema>;
export type InferenceFieldsError = {
error: string;
};
export function isInferenceFieldsError(
value: { error: string } | object
): value is InferenceFieldsError {
return "error" in value;
}
/**
* Resolve which allow/block patterns apply for an attachment.
* inherit provider lists; select resource-selected lists (replace).
*/
export function resolveEffectiveLists(input: {
accessMode: AccessMode;
providerAllows: string[];
providerBlocks: string[];
resourceAllows: string[];
resourceBlocks: string[];
}): { allows: string[]; blocks: string[] } {
if (input.accessMode === "select") {
return {
allows: input.resourceAllows,
blocks: input.resourceBlocks
};
}
return {
allows: input.providerAllows,
blocks: input.providerBlocks
};
}
function normalizeAttachments(
inputs: ResourceAiProviderInput[]
): ResourceAiProviderAttachment[] {
const byProviderId = new Map<
number,
{ accessMode: AccessMode; enabled: boolean }
>();
for (const input of inputs) {
byProviderId.set(input.providerId, {
accessMode: input.accessMode ?? "inherit",
enabled: input.enabled ?? true
});
}
return [...byProviderId.entries()].map(
([providerId, { accessMode, enabled }]) => ({
providerId,
accessMode,
enabled
})
);
}
/**
* Validate provider attachments for an org.
*/
export async function resolveProviderAttachments(input: {
orgId: string;
attachments: ResourceAiProviderInput[];
requireAtLeastOne: boolean;
}): Promise<ResourceAiProviderAttachment[] | InferenceFieldsError> {
const attachments = normalizeAttachments(input.attachments);
if (input.requireAtLeastOne && attachments.length === 0) {
return {
error: "At least one AI provider is required for inference-mode resources"
};
}
if (attachments.length === 0) {
return [];
}
const providerIds = attachments.map((a) => a.providerId);
const providers = await db
.select({
providerId: aiProviders.providerId,
orgId: aiProviders.orgId,
enabled: aiProviders.enabled
})
.from(aiProviders)
.where(
and(
inArray(aiProviders.providerId, providerIds),
eq(aiProviders.orgId, input.orgId)
)
);
if (providers.length !== providerIds.length) {
return {
error: "One or more AI providers were not found in this organization"
};
}
const disabled = providers.find((p) => !p.enabled);
if (disabled) {
return {
error: `AI provider with ID ${disabled.providerId} is disabled`
};
}
return attachments;
}
export async function assertInferenceModeAllowsProviderFields(input: {
mode: string;
hasProviderAttachments: boolean;
}): Promise<InferenceFieldsError | null> {
if (input.mode === "inference") {
return null;
}
if (input.hasProviderAttachments) {
return {
error: "AI providers can only be attached to inference-mode resources"
};
}
return null;
}
/**
* Attach providers to a resource. Inherit attachments use the provider lists
* as-is (resource model rows for those providers are pruned). Select
* attachments keep resource-selected allow/block subsets.
*/
export async function setPublicResourceAiProviders(
resourceId: number,
attachments: ResourceAiProviderAttachment[],
trx: DbOrTrx = db
): Promise<void> {
await trx
.delete(resourceAiProviders)
.where(eq(resourceAiProviders.resourceId, resourceId));
if (attachments.length > 0) {
await trx.insert(resourceAiProviders).values(
attachments.map((a) => ({
resourceId,
providerId: a.providerId,
accessMode: a.accessMode,
enabled: a.enabled
}))
);
}
await prunePublicResourceModelsToSelectProviders(
resourceId,
attachments,
trx
);
}
export async function setSiteResourceAiProviders(
siteResourceId: number,
attachments: ResourceAiProviderAttachment[],
trx: DbOrTrx = db
): Promise<void> {
await trx
.delete(siteResourceAiProviders)
.where(eq(siteResourceAiProviders.siteResourceId, siteResourceId));
if (attachments.length > 0) {
await trx.insert(siteResourceAiProviders).values(
attachments.map((a) => ({
siteResourceId,
providerId: a.providerId,
accessMode: a.accessMode,
enabled: a.enabled
}))
);
}
await pruneSiteResourceModelsToSelectProviders(
siteResourceId,
attachments,
trx
);
}
/**
* Keep resource model rows only for providers in select mode.
*/
async function prunePublicResourceModelsToSelectProviders(
resourceId: number,
attachments: ResourceAiProviderAttachment[],
trx: DbOrTrx
): Promise<void> {
const selectProviderIds = attachments
.filter((a) => a.accessMode === "select")
.map((a) => a.providerId);
if (selectProviderIds.length === 0) {
await trx
.delete(resourceAiModels)
.where(eq(resourceAiModels.resourceId, resourceId));
return;
}
const existing = await trx
.select({
modelId: resourceAiModels.modelId,
providerId: aiModels.providerId
})
.from(resourceAiModels)
.innerJoin(aiModels, eq(resourceAiModels.modelId, aiModels.modelId))
.where(eq(resourceAiModels.resourceId, resourceId));
const allowed = new Set(selectProviderIds);
const toRemove = existing
.filter((row) => !allowed.has(row.providerId))
.map((row) => row.modelId);
if (toRemove.length > 0) {
await trx
.delete(resourceAiModels)
.where(
and(
eq(resourceAiModels.resourceId, resourceId),
inArray(resourceAiModels.modelId, toRemove)
)
);
}
}
async function pruneSiteResourceModelsToSelectProviders(
siteResourceId: number,
attachments: ResourceAiProviderAttachment[],
trx: DbOrTrx
): Promise<void> {
const selectProviderIds = attachments
.filter((a) => a.accessMode === "select")
.map((a) => a.providerId);
if (selectProviderIds.length === 0) {
await trx
.delete(siteResourceAiModels)
.where(eq(siteResourceAiModels.siteResourceId, siteResourceId));
return;
}
const existing = await trx
.select({
modelId: siteResourceAiModels.modelId,
providerId: aiModels.providerId
})
.from(siteResourceAiModels)
.innerJoin(aiModels, eq(siteResourceAiModels.modelId, aiModels.modelId))
.where(eq(siteResourceAiModels.siteResourceId, siteResourceId));
const allowed = new Set(selectProviderIds);
const toRemove = existing
.filter((row) => !allowed.has(row.providerId))
.map((row) => row.modelId);
if (toRemove.length > 0) {
await trx
.delete(siteResourceAiModels)
.where(
and(
eq(siteResourceAiModels.siteResourceId, siteResourceId),
inArray(siteResourceAiModels.modelId, toRemove)
)
);
}
}
export async function clearPublicResourceAiConfig(
resourceId: number,
trx: DbOrTrx = db
): Promise<void> {
await trx
.delete(resourceAiModels)
.where(eq(resourceAiModels.resourceId, resourceId));
await trx
.delete(resourceAiProviders)
.where(eq(resourceAiProviders.resourceId, resourceId));
}
export async function clearSiteResourceAiConfig(
siteResourceId: number,
trx: DbOrTrx = db
): Promise<void> {
await trx
.delete(siteResourceAiModels)
.where(eq(siteResourceAiModels.siteResourceId, siteResourceId));
await trx
.delete(siteResourceAiProviders)
.where(eq(siteResourceAiProviders.siteResourceId, siteResourceId));
}
export async function listPublicResourceAiProviders(resourceId: number) {
return db
.select({
providerId: resourceAiProviders.providerId,
niceId: aiProviders.niceId,
name: aiProviders.name,
type: aiProviders.type,
enabled: resourceAiProviders.enabled,
providerEnabled: aiProviders.enabled,
accessMode: resourceAiProviders.accessMode
})
.from(resourceAiProviders)
.innerJoin(
aiProviders,
eq(resourceAiProviders.providerId, aiProviders.providerId)
)
.where(eq(resourceAiProviders.resourceId, resourceId));
}
export async function listSiteResourceAiProviders(siteResourceId: number) {
return db
.select({
providerId: siteResourceAiProviders.providerId,
niceId: aiProviders.niceId,
name: aiProviders.name,
type: aiProviders.type,
enabled: siteResourceAiProviders.enabled,
providerEnabled: aiProviders.enabled,
accessMode: siteResourceAiProviders.accessMode
})
.from(siteResourceAiProviders)
.innerJoin(
aiProviders,
eq(siteResourceAiProviders.providerId, aiProviders.providerId)
)
.where(eq(siteResourceAiProviders.siteResourceId, siteResourceId));
}
export type EffectiveAllowModel = {
modelId: number;
modelKey: string;
name: string;
providerId: number;
providerName: string;
};
export async function listEffectiveAllowModels(options: {
resourceId?: number;
siteResourceId?: number;
}): Promise<EffectiveAllowModel[]> {
if (
options.resourceId === undefined &&
options.siteResourceId === undefined
) {
return [];
}
const attachments =
options.resourceId !== undefined
? await listPublicResourceAiProviders(options.resourceId)
: await listSiteResourceAiProviders(options.siteResourceId!);
const activeAttachments = attachments.filter(
(a) => a.enabled && a.providerEnabled
);
if (activeAttachments.length === 0) {
return [];
}
const inheritProviderIds = activeAttachments
.filter((a) => a.accessMode === "inherit")
.map((a) => a.providerId);
const selectProviderIds = activeAttachments
.filter((a) => a.accessMode === "select")
.map((a) => a.providerId);
const providerNameById = new Map(
activeAttachments.map((a) => [a.providerId, a.name] as const)
);
const models: EffectiveAllowModel[] = [];
if (inheritProviderIds.length > 0) {
const rows = await db
.select({
modelId: aiModels.modelId,
modelKey: aiModels.modelKey,
name: aiModels.name,
providerId: aiModels.providerId
})
.from(aiModels)
.where(
and(
inArray(aiModels.providerId, inheritProviderIds),
eq(aiModels.enabled, true),
eq(aiModels.listType, "allow")
)
);
for (const row of rows) {
models.push({
...row,
providerName: providerNameById.get(row.providerId) ?? ""
});
}
}
if (selectProviderIds.length > 0) {
if (options.resourceId !== undefined) {
const rows = await db
.select({
modelId: aiModels.modelId,
modelKey: aiModels.modelKey,
name: aiModels.name,
providerId: aiModels.providerId
})
.from(resourceAiModels)
.innerJoin(
aiModels,
eq(resourceAiModels.modelId, aiModels.modelId)
)
.where(
and(
eq(resourceAiModels.resourceId, options.resourceId),
inArray(aiModels.providerId, selectProviderIds),
eq(resourceAiModels.listType, "allow"),
eq(aiModels.enabled, true)
)
);
for (const row of rows) {
models.push({
...row,
providerName: providerNameById.get(row.providerId) ?? ""
});
}
} else if (options.siteResourceId !== undefined) {
const rows = await db
.select({
modelId: aiModels.modelId,
modelKey: aiModels.modelKey,
name: aiModels.name,
providerId: aiModels.providerId
})
.from(siteResourceAiModels)
.innerJoin(
aiModels,
eq(siteResourceAiModels.modelId, aiModels.modelId)
)
.where(
and(
eq(
siteResourceAiModels.siteResourceId,
options.siteResourceId
),
inArray(aiModels.providerId, selectProviderIds),
eq(siteResourceAiModels.listType, "allow"),
eq(aiModels.enabled, true)
)
);
for (const row of rows) {
models.push({
...row,
providerName: providerNameById.get(row.providerId) ?? ""
});
}
}
}
models.sort((a, b) => {
const byProvider = a.providerName.localeCompare(
b.providerName,
undefined,
{
sensitivity: "base"
}
);
if (byProvider !== 0) {
return byProvider;
}
return a.name.localeCompare(b.name, undefined, { sensitivity: "base" });
});
return models;
}
/**
* Model list APIs require an inference resource with at least one select-mode
* attached provider.
*/
export async function assertPublicModelListApiEligible(resource: {
resourceId: number;
mode: string;
}): Promise<string | null> {
if (resource.mode !== "inference") {
return "AI model lists are only supported on inference-mode resources";
}
const [row] = await db
.select({ providerId: resourceAiProviders.providerId })
.from(resourceAiProviders)
.where(
and(
eq(resourceAiProviders.resourceId, resource.resourceId),
eq(resourceAiProviders.accessMode, "select")
)
)
.limit(1);
if (!row) {
return "Set at least one attached AI provider to select mode before managing model lists";
}
return null;
}
export async function assertSiteModelListApiEligible(siteResource: {
siteResourceId: number;
mode: string;
}): Promise<string | null> {
if (siteResource.mode !== "inference") {
return "AI model lists are only supported on inference-mode resources";
}
const [row] = await db
.select({ providerId: siteResourceAiProviders.providerId })
.from(siteResourceAiProviders)
.where(
and(
eq(
siteResourceAiProviders.siteResourceId,
siteResource.siteResourceId
),
eq(siteResourceAiProviders.accessMode, "select")
)
)
.limit(1);
if (!row) {
return "Set at least one attached AI provider to select mode before managing model lists";
}
return null;
}
/**
* Resource model entries must belong to select-mode attached providers, and
* listType must match the provider catalog entry (allowallow, blockblock).
*/
export async function assertPublicResourceModelEntriesValid(input: {
orgId: string;
resourceId: number;
models: ResourceAiModelEntry[];
}): Promise<string | null> {
const uniqueModels = dedupeModelEntries(input.models);
if (uniqueModels.length === 0) {
return null;
}
const attachments = await db
.select({
providerId: resourceAiProviders.providerId,
accessMode: resourceAiProviders.accessMode,
enabled: resourceAiProviders.enabled
})
.from(resourceAiProviders)
.innerJoin(
aiProviders,
eq(resourceAiProviders.providerId, aiProviders.providerId)
)
.where(
and(
eq(resourceAiProviders.resourceId, input.resourceId),
eq(aiProviders.orgId, input.orgId)
)
);
return assertModelEntriesValid({
orgId: input.orgId,
modelEntries: uniqueModels,
attachments,
resourceLabel: "resource"
});
}
export async function assertSiteResourceModelEntriesValid(input: {
orgId: string;
siteResourceId: number;
models: ResourceAiModelEntry[];
}): Promise<string | null> {
const uniqueModels = dedupeModelEntries(input.models);
if (uniqueModels.length === 0) {
return null;
}
const attachments = await db
.select({
providerId: siteResourceAiProviders.providerId,
accessMode: siteResourceAiProviders.accessMode,
enabled: siteResourceAiProviders.enabled
})
.from(siteResourceAiProviders)
.innerJoin(
aiProviders,
eq(siteResourceAiProviders.providerId, aiProviders.providerId)
)
.where(
and(
eq(
siteResourceAiProviders.siteResourceId,
input.siteResourceId
),
eq(aiProviders.orgId, input.orgId)
)
);
return assertModelEntriesValid({
orgId: input.orgId,
modelEntries: uniqueModels,
attachments,
resourceLabel: "site resource"
});
}
function dedupeModelEntries(
models: ResourceAiModelEntry[]
): ResourceAiModelEntry[] {
const byModelId = new Map(
models.map((m) => [m.modelId, m.listType] as const)
);
return [...byModelId.entries()].map(([modelId, listType]) => ({
modelId,
listType
}));
}
async function assertModelEntriesValid(input: {
orgId: string;
modelEntries: ResourceAiModelEntry[];
attachments: ResourceAiProviderAttachment[];
resourceLabel: string;
}): Promise<string | null> {
const selectProviderIds = input.attachments
.filter((a) => a.accessMode === "select")
.map((a) => a.providerId);
if (selectProviderIds.length === 0) {
return "Set at least one attached AI provider to select mode before managing model lists";
}
const modelIds = input.modelEntries.map((m) => m.modelId);
const catalogRows = await db
.select({
modelId: aiModels.modelId,
listType: aiModels.listType,
providerId: aiModels.providerId,
enabled: aiModels.enabled
})
.from(aiModels)
.innerJoin(aiProviders, eq(aiModels.providerId, aiProviders.providerId))
.where(
and(
inArray(aiModels.modelId, modelIds),
inArray(aiModels.providerId, selectProviderIds),
eq(aiProviders.orgId, input.orgId)
)
);
if (catalogRows.length !== modelIds.length) {
return `One or more model IDs do not exist or do not belong to a select-mode provider on this ${input.resourceLabel}`;
}
const catalogById = new Map(catalogRows.map((row) => [row.modelId, row]));
for (const entry of input.modelEntries) {
const catalog = catalogById.get(entry.modelId);
if (!catalog) {
return `One or more model IDs do not exist or do not belong to a select-mode provider on this ${input.resourceLabel}`;
}
if (catalog.listType !== entry.listType) {
return `Model ${entry.modelId} must use listType "${catalog.listType}" to match the provider catalog entry`;
}
if (!catalog.enabled) {
return `Model ${entry.modelId} is disabled on its provider`;
}
}
return null;
}

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