Compare commits
727 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b982639dc4 | |||
| 56e758a656 | |||
| a84db679b2 | |||
| fb726cc6b2 | |||
| 5d0ceebe0d | |||
| c964821195 | |||
| 5c44c6da73 | |||
| 6e52758d6e | |||
| 0af194550f | |||
| 0d12402662 | |||
| 3fb2e34256 | |||
| ec2b1fbd37 | |||
| 806ae93a12 | |||
| 5d99e0a2f6 | |||
| a75f335791 | |||
| 153d29c47c | |||
| ee8749c292 | |||
| 4f905ddae5 | |||
| 74e686ff3f | |||
| a4ade43380 | |||
| 68821514a0 | |||
| a2652cf692 | |||
| 957144ac3a | |||
| 20c23cdfc5 | |||
| c03e1c5781 | |||
| e69d3eded5 | |||
| 34084cc3a6 | |||
| 91776ad2e4 | |||
| 77eabf41e7 | |||
| d68895e64a | |||
| 1c2d81d77c | |||
| 998778eb72 | |||
| 7be206947a | |||
| 125d33c071 | |||
| 410d26f2df | |||
| 6aa1dafa10 | |||
| 8f1344a665 | |||
| d23028d38e | |||
| 7506a2614b | |||
| b3e5de4a4d | |||
| f9f38fb3f0 | |||
| 888ccce397 | |||
| 49e0a8bd47 | |||
| f59aed8482 | |||
| b6c048c805 | |||
| 018ee17304 | |||
| 5cad796023 | |||
| 5a445817e0 | |||
| fd2c397bfc | |||
| 6c794e0b0c | |||
| 7acb98e245 | |||
| d59ab30c22 | |||
| e4a8e9ac8c | |||
| a874a0b745 | |||
| 9ba2cfdef2 | |||
| bba8d4e7a2 | |||
| 350f8c012a | |||
| 8a97ed5200 | |||
| e66f7fe71b | |||
| a37a59f39a | |||
| d0bf553f6f | |||
| 87e005d32f | |||
| ad343a7453 | |||
| 7e4d38548f | |||
| 3421441635 | |||
| 073bfb32e9 | |||
| 2c5b1e5f0f | |||
| 294830db08 | |||
| fb8d531435 | |||
| 11d947f4ef | |||
| aad18e6501 | |||
| dac8b5a132 | |||
| 162dade852 | |||
| dfe7f60244 | |||
| 07bea71cb9 | |||
| 557c398d0b | |||
| 0a385d1e44 | |||
| 521f78c2f3 | |||
| 0ea4a5fb3d | |||
| e2609f1a0d | |||
| 8b20a88838 | |||
| 7aae51ca9d | |||
| 1d6d885f30 | |||
| fef1476f67 | |||
| e371f26d73 | |||
| 9c8b93d6cc | |||
| aed325f273 | |||
| c7645e5c5b | |||
| c0eed078c8 | |||
| 0f69012c4d | |||
| ba2eb87f20 | |||
| c0ea32863c | |||
| 4d71fcbac8 | |||
| 1d3dcec4c8 | |||
| 17375348b0 | |||
| ea9017ac06 | |||
| 88770ff97b | |||
| 8e75425887 | |||
| 44b0186044 | |||
| 063f6b5ca9 | |||
| e7fdbf9e85 | |||
| 778a840ed7 | |||
| 9d19195089 | |||
| 54bbe82504 | |||
| cddb5ecc3d | |||
| de57df2520 | |||
| 9e392a967d | |||
| 52f7364c35 | |||
| 1bc5fbbf0f | |||
| be0dd65557 | |||
| 11be7b8ee4 | |||
| 9373cc0408 | |||
| e50763c340 | |||
| 8826240548 | |||
| 6d83614482 | |||
| c319b7a65f | |||
| aa7fe7ee0e | |||
| 5fd71df2ae | |||
| c7e1462e46 | |||
| cfb73c9e21 | |||
| 72a9040c2e | |||
| ce53b8c73d | |||
| 6ff2b3e2fe | |||
| 4dada38cb0 | |||
| 732ea034f8 | |||
| 581137c486 | |||
| d75c6da3be | |||
| 11b51202b8 | |||
| c35de18b9b | |||
| 353273a9f2 | |||
| 7c20a292e1 | |||
| 6c13d6e343 | |||
| e641a8ce1b | |||
| a3419f60d6 | |||
| 2db3051a1a | |||
| 3c49b6f3d6 | |||
| 9ad4e36fa9 | |||
| c489ed5724 | |||
| fca3a1e5fa | |||
| b29348d004 | |||
| 337d9a33a3 | |||
| c61e96b1f0 | |||
| 35bc692892 | |||
| 714e7e0ba4 | |||
| 49d5b0ec34 | |||
| e0937a3afa | |||
| 8d7e73afa8 | |||
| a5ce56ea89 | |||
| 2f013335f9 | |||
| b4f6ae74d7 | |||
| f1711ee0b0 | |||
| 780d767a65 | |||
| d6d923e972 | |||
| 41139f2fd0 | |||
| ebcdeab414 | |||
| 2cc7d03ace | |||
| c36cf698c1 | |||
| 34671c6b13 | |||
| 8dfc95347f | |||
| 22db0319c2 | |||
| 39722d30af | |||
| 8aef14cf9f | |||
| 0dece5fef1 | |||
| e7f38c089f | |||
| dd0a5a359a | |||
| 1650ece0c3 | |||
| 7f94d99455 | |||
| 1f9e99219d | |||
| bc56a2bed0 | |||
| 49dcc590ce | |||
| da3e3ff33f | |||
| 69d539f107 | |||
| 872e0f9ae1 | |||
| 5b3713a72f | |||
| f02be1fdbf | |||
| 0bf04cf0cd | |||
| 7cda28d685 | |||
| 60bc74c4df | |||
| 48ab6c501f | |||
| 7a95e543d8 | |||
| b87b7c7e80 | |||
| a47a68d8e1 | |||
| ed0d6fb6b9 | |||
| a02d16fd58 | |||
| 331fee24d4 | |||
| 5bdb12dafe | |||
| e57826d6e0 | |||
| 3d4e143c1f | |||
| 10a25c184d | |||
| 906099d1e1 | |||
| 9a5824900d | |||
| f3474dac98 | |||
| 72d2c79793 | |||
| 23764feb4f | |||
| d2809fbfd1 | |||
| 2d18db3597 | |||
| d00b9478a2 | |||
| 4ddf36ebcc | |||
| 77cab56fa9 | |||
| 2051e5df37 | |||
| 26f9026621 | |||
| 85b40b7164 | |||
| 753cbd45d0 | |||
| 1c2fe44c54 | |||
| 5b782a842c | |||
| 935410b15e | |||
| 89066aa5b1 | |||
| ddf89d0afa | |||
| e4ec6f7cbe | |||
| c1e5769003 | |||
| 4b31326b34 | |||
| 10d2c6438b | |||
| c3140c5da3 | |||
| 0f00a2337d | |||
| 1831b1af58 | |||
| 85257b941b | |||
| 4aa43fd14d | |||
| 90a77ee450 | |||
| d237d6545e | |||
| 262aaa2756 | |||
| 547ac2284a | |||
| 7d2af1837e | |||
| 8ae42e1852 | |||
| 7319bf84f7 | |||
| 9ec9908ed7 | |||
| 9b0e049a21 | |||
| 28b32fe6f7 | |||
| adfb6003d9 | |||
| 19ce236262 | |||
| 6a5ecab013 | |||
| eca1c9044c | |||
| 2c197fab9f | |||
| 4229ef9173 | |||
| d5ea0ecbc1 | |||
| 59f286299e | |||
| 442cefda84 | |||
| e3e1508e8a | |||
| 2e87927b83 | |||
| 65e4fe91b9 | |||
| e65a79cc48 | |||
| 365a905e69 | |||
| bafbf6e096 | |||
| df7e26a444 | |||
| c1051db4a5 | |||
| c1caa30cb9 | |||
| fd0a0818c1 | |||
| 887e13888d | |||
| 114592add8 | |||
| ef051c2f8c | |||
| 5c6da72ec1 | |||
| acd64a049f | |||
| 967deaff88 | |||
| ef8985b0af | |||
| 1f80bdd361 | |||
| 929acc5b1c | |||
| 437ec50019 | |||
| a7bbafd2f0 | |||
| 6824ec6e3e | |||
| 71348f45b2 | |||
| 664b7d52af | |||
| fb6f9c91c6 | |||
| 051c873fdf | |||
| 12224458e7 | |||
| b6719d2a3e | |||
| 7843b56f39 | |||
| 19be6c4b3b | |||
| 4a283710ad | |||
| 1af7c5ee73 | |||
| 77dd47fa8a | |||
| ccfe91f08d | |||
| 624fb8322f | |||
| 6bc3242a29 | |||
| 33f528ef79 | |||
| 6c1afc4453 | |||
| 2a14447f8c | |||
| ad50f7473a | |||
| 5f1218f7b1 | |||
| e377afe9f6 | |||
| 21eb4d2876 | |||
| bb824a7070 | |||
| f98de98288 | |||
| 350de6ad2e | |||
| dc8e724482 | |||
| fb5b564c00 | |||
| 52c078a489 | |||
| 18270381c1 | |||
| b32dc30050 | |||
| 667804043c | |||
| 3fd3d90c10 | |||
| 6e3619fb1a | |||
| 987c0e992b | |||
| bdd7f40688 | |||
| 809bc662e0 | |||
| 195f67c6eb | |||
| a74e1e765a | |||
| c356af3eda | |||
| a4c7121b93 | |||
| 2a3c00045f | |||
| 8e938a2723 | |||
| 197f8f7ba5 | |||
| 10b528642d | |||
| 60d6fff085 | |||
| c664b3da91 | |||
| 492282e758 | |||
| 47f4aefc25 | |||
| 7c0ff9ede7 | |||
| 44e81ea979 | |||
| 56dc10330a | |||
| 6c6e5c0fdf | |||
| 4be73558d5 | |||
| 87ff7650a1 | |||
| ae5af013b4 | |||
| 5adfe65aba | |||
| d3ae06deeb | |||
| ee4a72bcb7 | |||
| f941c23927 | |||
| 3ae6a08b71 | |||
| 620eeaed4c | |||
| 9b08228398 | |||
| eb102a0d23 | |||
| 18317e0028 | |||
| 2ec93c6710 | |||
| 5933eeb05f | |||
| 3fae2d687f | |||
| 285d7c6030 | |||
| 0fd57f5843 | |||
| a296460124 | |||
| 35104f7b29 | |||
| 51375ed8b7 | |||
| 1bb4f604e9 | |||
| 8fd990df93 | |||
| 1f390cabc6 | |||
| 7f1f8c54c1 | |||
| b1515db229 | |||
| d78c226734 | |||
| 52865bc1f5 | |||
| efbb6c9814 | |||
| 1e3dea3dad | |||
| 25f84482ba | |||
| 3f2a116fdb | |||
| dce002c6cc | |||
| dcfbbdc79d | |||
| f8cac37eef | |||
| cb37713c4c | |||
| 7ae4082d7e | |||
| aa8d339d35 | |||
| 7a61b182e0 | |||
| 91b697b80b | |||
| 7d10b63dc5 | |||
| 92916cd9db | |||
| 075d112861 | |||
| 7e9407fbc7 | |||
| 313f777e44 | |||
| 3ba9eb7ad7 | |||
| 328a2d52da | |||
| 697bea814e | |||
| 83cda218b7 | |||
| 76435440af | |||
| 0a8b5e46ca | |||
| a74b0934fc | |||
| c05d2aeafb | |||
| 87a3e5ceea | |||
| 555b36e13e | |||
| 021866011f | |||
| 749abdcb56 | |||
| 7d6f3a2925 | |||
| af515f1072 | |||
| c90c67eab3 | |||
| 51c9507d08 | |||
| 11daf4f927 | |||
| 58195b5959 | |||
| b51aecf45a | |||
| eb8ad6a181 | |||
| 4edd2e4d32 | |||
| 813c3abe54 | |||
| ca7ea72ff1 | |||
| 53b1d8a9f3 | |||
| a3dfb30a42 | |||
| 668a04bcd2 | |||
| 111d4b1b8c | |||
| 32d76ee2ac | |||
| 98ad6fb31b | |||
| 3a18a3994f | |||
| 6d35b20880 | |||
| 173d4a536f | |||
| ea740e12d1 | |||
| d7bff686c0 | |||
| 193f6da8d1 | |||
| 954dc8d1d9 | |||
| 234819f8f2 | |||
| b34a8d116b | |||
| 7375b9efae | |||
| c8f170d197 | |||
| 4989d1e31a | |||
| c568251d8d | |||
| db2de4af89 | |||
| 574ae8f5f9 | |||
| 125091d719 | |||
| 5d37f44241 | |||
| f0f5e9219b | |||
| ee75a09f8c | |||
| c49c6f5837 | |||
| 03118f1ede | |||
| 0ff5ea4f6a | |||
| 978fa8bfc6 | |||
| 36c7c5dc34 | |||
| 8f3628d4ec | |||
| fcd3c72cac | |||
| 3f0be4a39d | |||
| f9ff3a5715 | |||
| ed71c90d73 | |||
| e580c7db7c | |||
| 4a9b0117a2 | |||
| 9b10292e02 | |||
| d9a9ae14fd | |||
| db716f781e | |||
| 46f341f7fd | |||
| 9bb413bb1e | |||
| 2878d5690c | |||
| 1143404a65 | |||
| e92bb9043b | |||
| a2f5d830d6 | |||
| 77e3422e88 | |||
| 9a551c91e5 | |||
| c35ac493ed | |||
| e100645a00 | |||
| 96bfc66a94 | |||
| e21c9eec31 | |||
| a9f32102b5 | |||
| 7291628f86 | |||
| 5fbb205044 | |||
| 02ab24d01e | |||
| 3cfbd66a80 | |||
| 048e4fc73c | |||
| 860fa47b7c | |||
| 71d9d8f010 | |||
| cc58e28e54 | |||
| 653dd920ad | |||
| 49b4fcf063 | |||
| dd78c2cc08 | |||
| 47ae017f94 | |||
| 4b61e12ca6 | |||
| c6bd657ee6 | |||
| c33fa8782b | |||
| 93cba1d098 | |||
| d3a1f9798d | |||
| 115c3cbf07 | |||
| bed5817da3 | |||
| 83035753af | |||
| 379b53bcca | |||
| 355294a2d4 | |||
| 833b27ab4a | |||
| 8a04f13dd4 | |||
| 49020fa6ea | |||
| ac3402a8b3 | |||
| 9369e60695 | |||
| 295e38d2af | |||
| 923371e5b4 | |||
| 81430ba3d3 | |||
| 11595f5f96 | |||
| b08e875b37 | |||
| 0016b8fce7 | |||
| f5b10df7cf | |||
| 1782f31075 | |||
| 21032bc22b | |||
| e4aaadc9f9 | |||
| f7aca85417 | |||
| c42df737b0 | |||
| 75c6af3b11 | |||
| e734cc93a1 | |||
| e3ccc4f8d4 | |||
| 896c362dce | |||
| 864ab97293 | |||
| 899c47e9a3 | |||
| bc2f291352 | |||
| 98f5e39a7f | |||
| e0a66e79bb | |||
| c781be4e70 | |||
| 48c4b44f72 | |||
| 68a84181d9 | |||
| b60390d805 | |||
| b33e776072 | |||
| 2187f23588 | |||
| 732fd4eba1 | |||
| 495c772d6b | |||
| 02e4fe8b48 | |||
| 6d45486bb5 | |||
| 4c1f7f6243 | |||
| 1e3a9fb921 | |||
| 8b8e7913dc | |||
| 88515a7dad | |||
| e012063f5b | |||
| e76377d3c7 | |||
| a357f42c48 | |||
| 2becb15916 | |||
| 186eeed784 | |||
| 216c932cb9 | |||
| 52de5eb539 | |||
| eec3119297 | |||
| f2e7f83b3d | |||
| 93dea0525e | |||
| 7e6e0a8a9c | |||
| 211d3a53f5 | |||
| 187936e5dd | |||
| b180d064d1 | |||
| ed46afd81a | |||
| 0050fad70d | |||
| 3dc9c100e9 | |||
| 969e7c9296 | |||
| 591caab45a | |||
| 02e97d6ae4 | |||
| 996160fadc | |||
| dc1923ab1f | |||
| fe5831eb48 | |||
| ca79abc9d4 | |||
| 9eafa067b9 | |||
| 403b8a12e4 | |||
| 3f305e4d5c | |||
| 5e5e172d39 | |||
| bc7a883f6c | |||
| 12056aebc6 | |||
| 6689a8d93e | |||
| 297cb9c8f2 | |||
| d699455b38 | |||
| 9e7b4afaec | |||
| e91c344e64 | |||
| 07f628b928 | |||
| 184e1425a4 | |||
| 22f2990f56 | |||
| 6c28c5f383 | |||
| e9f9cf54f4 | |||
| 751d1b083d | |||
| a4d77a4fd3 | |||
| f72252552f | |||
| c5d68675c9 | |||
| d04740fede | |||
| 36b8ef5fba | |||
| 6cca5e0472 | |||
| 4048fa274a | |||
| b4d2974e19 | |||
| fb896d6c0f | |||
| 82b86263dc | |||
| 6564bfe8ae | |||
| 75ce7e91d7 | |||
| 656eea5bb1 | |||
| aba27a7bbf | |||
| 3d7e322bf9 | |||
| 1f3fff4a9d | |||
| 39e06f2b6d | |||
| b775c5b674 | |||
| 796bf37141 | |||
| c5b0e1e876 | |||
| 2e8bd7a8c7 | |||
| 790daba796 | |||
| bcf6b86b84 | |||
| 2e9bd50172 | |||
| 4677a0d501 | |||
| c673dce484 | |||
| e1dd8965dc | |||
| 346a0bdc98 | |||
| 8c1169738e | |||
| bc80f91a45 | |||
| 3d062389e9 | |||
| 7afddb5eb5 | |||
| 425a99e5ee | |||
| a5e9339af9 | |||
| b4463f0e1a | |||
| b7c0669c38 | |||
| 835a30cffe | |||
| 83e20c2dfa | |||
| f85d41945a | |||
| 149eb17b27 | |||
| 18b90da6ab | |||
| 72d469b19c | |||
| 6526d7f225 | |||
| 36c0edc62e | |||
| aad26b9ae4 | |||
| 80dcdfe251 | |||
| 1696fc37a8 | |||
| 7759d87835 | |||
| c085de1e9e | |||
| e99cd52e99 | |||
| e38359c74f | |||
| 2cfd7e867b | |||
| ec5a2b0cbe | |||
| 2bf426bc22 | |||
| ed8545f8a2 | |||
| 9811492a0b | |||
| 973925b35d | |||
| 0b30cfc341 | |||
| a7e44944fb | |||
| 9b25ebd6cd | |||
| 33b775e263 | |||
| b0edc6302e | |||
| 1a644b131d | |||
| f7689b7a5a | |||
| 1073011a2a | |||
| 6fa0009ebf | |||
| 42c0abedb7 | |||
| 56e59a93f3 | |||
| 9d581f3897 | |||
| e5ac6ec7cd | |||
| 694fe98131 | |||
| 730078597e | |||
| 32ac8db803 | |||
| aad2ed2719 | |||
| a790fed297 | |||
| deb2d5ce2a | |||
| 093097c619 | |||
| 2cdeb7c104 | |||
| 4dbb04bfb8 | |||
| fa8b921635 | |||
| 33dd10c670 | |||
| ba24e1c4f5 | |||
| f079714caf | |||
| 152d2fb1d6 | |||
| d374b4f66e | |||
| efd2792197 | |||
| efe22c889c | |||
| 7f2b3eb481 | |||
| 81be4a35d9 | |||
| e84da6a8df | |||
| 3ef3ede7df | |||
| 1e521b0b54 | |||
| 59ea701304 | |||
| 7d7c54107d | |||
| f0f6673d69 | |||
| 71561d0e65 | |||
| af87edf3a6 | |||
| 13caad18c7 | |||
| 47522b7e3a | |||
| c8c8d74452 | |||
| e7098963d6 | |||
| f015fb592b | |||
| c099167905 | |||
| b0e274f5a9 | |||
| cb3f0b49a8 | |||
| 146c287aba | |||
| 95ee56217c | |||
| 7625cc208d | |||
| f2dfd939bc | |||
| 699bc8ddb4 | |||
| e658e007e1 | |||
| 3103265450 | |||
| 97789d9e2e | |||
| a1aa3d96d4 | |||
| ae39bc0ecd | |||
| e4d81aa610 | |||
| f59b524266 | |||
| 58c499acae | |||
| f22301a1eb | |||
| d6264fb39a | |||
| 08b5a9b34b | |||
| 263976bf41 | |||
| 0e4cd3a5ba | |||
| 192542629f | |||
| aeed271f3e | |||
| d41e9bbe68 | |||
| 904a5520b8 | |||
| 0f43ae4871 | |||
| bd0cf98319 | |||
| fc7d6bbaf9 | |||
| 1b89c328de | |||
| e7f147d4ca | |||
| 41dbc7d5ed | |||
| 9f297317f2 | |||
| e0a8721207 | |||
| a69b310109 | |||
| 864a6a39cd | |||
| 1882d4df88 | |||
| ef3f71e9b5 | |||
| 9981c4c035 | |||
| bc267b7107 | |||
| 5cbb767e5f | |||
| ac79621cae | |||
| f47c94d05b | |||
| 10773432bb | |||
| d5a56fb71d | |||
| b5518d029e | |||
| 9a9ae649ef | |||
| aa6dc67015 | |||
| 0da96b06ea | |||
| 6649f15498 | |||
| 49854f31a5 | |||
| 762c79511b | |||
| 6e29ebd649 | |||
| e128b8c282 | |||
| 832e382888 | |||
| 84d5a4b86c | |||
| 8a68b899f5 | |||
| 71f45b8a80 | |||
| ecf008a8d9 | |||
| b3880e5c02 | |||
| 58004a8ec9 | |||
| b0ff7d2707 | |||
| 262f7bd090 | |||
| b1558b09b1 | |||
| a33de8268b | |||
| c92d5096c4 | |||
| 9867d3c876 | |||
| 70bddba55b | |||
| 23181f4019 | |||
| 19c1c2042b | |||
| 9cc3190e3a | |||
| 72f179578b | |||
| 4c873e7c48 | |||
| 56fcb80b23 | |||
| 41c68148a9 | |||
| 5d38059b95 | |||
| df0198df9c | |||
| 26713b53f6 | |||
| 2bc6b28978 | |||
| f1ed4da8a4 | |||
| adeefb9dbd | |||
| bf1cc705a5 | |||
| 0b82dae01e | |||
| 75afe6ece2 | |||
| 70e2fe1e4e | |||
| 17e03457e1 | |||
| 4d8cb7e231 | |||
| 9561d23f1e | |||
| 0275f44056 | |||
| a2e1c7b751 | |||
| 9e2ec72ced | |||
| 02fe1f3abd | |||
| 1580b7abff | |||
| bb5547a157 |
@@ -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.
|
||||
@@ -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,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
|
||||
|
||||
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -46,7 +46,6 @@ public/branding
|
||||
server/db/index.ts
|
||||
server/build.ts
|
||||
postgres/
|
||||
dynamic/
|
||||
*.mmdb
|
||||
scratch/
|
||||
tsconfig.json
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
FROM node:24-alpine
|
||||
FROM node:24.18.1-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.`
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,3 +1,3 @@
|
||||
files:
|
||||
- source: /messages/en-US.json
|
||||
translation: /messages/%locale%.json
|
||||
translation: /messages/%locale%.json
|
||||
|
||||
@@ -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"`
|
||||
|
||||
1–5 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.
|
||||
@@ -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/`.
|
||||
@@ -0,0 +1,2 @@
|
||||
tls:
|
||||
certificates: []
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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": "Проверка"
|
||||
}
|
||||
|
||||
@@ -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 can’t be changed later.",
|
||||
"resourceTypeDescription": "This controls the resource protocol and can’t 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 can’t 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"
|
||||
}
|
||||
|
||||
@@ -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": "확인"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
After Width: | Height: | Size: 790 KiB |
|
Before Width: | Height: | Size: 621 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 532 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 621 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 556 KiB After Width: | Height: | Size: 620 KiB |
|
Before Width: | Height: | Size: 574 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 410 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 516 KiB After Width: | Height: | Size: 802 KiB |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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;
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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" };
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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,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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
)
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
@@ -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 (allow→allow, block→block).
|
||||
*/
|
||||
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;
|
||||
}
|
||||