Next (#930)
This commit is contained in:
commit
00cc130414
|
|
@ -11,19 +11,19 @@
|
|||
"dependencies": {
|
||||
"@connectrpc/connect": "^2.1.1",
|
||||
"@connectrpc/connect-web": "^2.1.1",
|
||||
"@hugeicons/core-free-icons": "^3.3.0",
|
||||
"@hugeicons/vue": "^1.0.4",
|
||||
"@hugeicons/core-free-icons": "^4.0.0",
|
||||
"@hugeicons/vue": "^1.0.5",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"iconify-icon": "^3.0.2",
|
||||
"picocrank": "^1.14.0",
|
||||
"picocrank": "^1.14.1",
|
||||
"standard": "^17.1.2",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vue": "^3.5.29",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue": "^3.5.30",
|
||||
"vue-i18n": "^11.3.0",
|
||||
"vue-router": "^5.0.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -906,15 +906,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@hugeicons/core-free-icons": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-3.3.0.tgz",
|
||||
"integrity": "sha512-qYyr4JQ2eQIHTSTbITvnJvs6ERNK64D9gpwZnf2IyuG0exzqfyABLO/oTB71FB3RZPfu1GbwycdiGSo46apjMQ==",
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@hugeicons/core-free-icons/-/core-free-icons-4.0.0.tgz",
|
||||
"integrity": "sha512-bzfbKumv3ke3ajbe2MyXi9i0I/cdsZ6n/mO9EfIPNSL++pHLqs7nSGRIVUtjF4xrrEyVkfhxssv4Jek8DPA6gA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@hugeicons/vue": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@hugeicons/vue/-/vue-1.0.4.tgz",
|
||||
"integrity": "sha512-OtFEXbyW5jYUig98C/n/HygktLvfF5Ga6nN6gK8R0E0jCrVw3EfgoZZVXqo+xGxyIjH5R1wdbg6nJrtf6mzLKQ==",
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@hugeicons/vue/-/vue-1.0.5.tgz",
|
||||
"integrity": "sha512-kaouUZceXtdDfupfiqqfn40tIyRBF/fcEvCfY96hZIXZ3JMsqpwhCDqiqoj+B5bMEUXOuvz3npNyKI5+7iPfYA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"vue": "^2.6.0 || ^3.0.0"
|
||||
|
|
@ -962,13 +962,30 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@intlify/core-base": {
|
||||
"version": "11.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.8.tgz",
|
||||
"integrity": "sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==",
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.3.0.tgz",
|
||||
"integrity": "sha512-NNX5jIwF4TJBe7RtSKDMOA6JD9mp2mRcBHAwt2X+Q8PvnZub0yj5YYXlFu2AcESdgQpEv/5Yx2uOCV/yh7YkZg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/message-compiler": "11.2.8",
|
||||
"@intlify/shared": "11.2.8"
|
||||
"@intlify/devtools-types": "11.3.0",
|
||||
"@intlify/message-compiler": "11.3.0",
|
||||
"@intlify/shared": "11.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/kazupon"
|
||||
}
|
||||
},
|
||||
"node_modules/@intlify/devtools-types": {
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/devtools-types/-/devtools-types-11.3.0.tgz",
|
||||
"integrity": "sha512-G9CNL4WpANWVdUjubOIIS7/D2j/0j+1KJmhBJxHilWNKr9mmt3IjFV3Hq4JoBP23uOoC5ynxz/FHZ42M+YxfGw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.3.0",
|
||||
"@intlify/shared": "11.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
|
|
@ -978,12 +995,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@intlify/message-compiler": {
|
||||
"version": "11.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.8.tgz",
|
||||
"integrity": "sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==",
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.3.0.tgz",
|
||||
"integrity": "sha512-RAJp3TMsqohg/Wa7bVF3cChRhecSYBLrTCQSj7j0UtWVFLP+6iEJoE2zb7GU5fp+fmG5kCbUdzhmlAUCWXiUJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/shared": "11.2.8",
|
||||
"@intlify/shared": "11.3.0",
|
||||
"source-map-js": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -994,9 +1011,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@intlify/shared": {
|
||||
"version": "11.2.8",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.8.tgz",
|
||||
"integrity": "sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw==",
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.3.0.tgz",
|
||||
"integrity": "sha512-LC6P/uay7rXL5zZ5+5iRJfLs/iUN8apu9tm8YqQVmW3Uq3X4A0dOFUIDuAmB7gAC29wTHOS3EiN/IosNSz0eNQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
|
|
@ -1436,53 +1453,53 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.29.tgz",
|
||||
"integrity": "sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw==",
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.30.tgz",
|
||||
"integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@vue/shared": "3.5.29",
|
||||
"@vue/shared": "3.5.30",
|
||||
"entities": "^7.0.1",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz",
|
||||
"integrity": "sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg==",
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz",
|
||||
"integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.29",
|
||||
"@vue/shared": "3.5.29"
|
||||
"@vue/compiler-core": "3.5.30",
|
||||
"@vue/shared": "3.5.30"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz",
|
||||
"integrity": "sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA==",
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz",
|
||||
"integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.29.0",
|
||||
"@vue/compiler-core": "3.5.29",
|
||||
"@vue/compiler-dom": "3.5.29",
|
||||
"@vue/compiler-ssr": "3.5.29",
|
||||
"@vue/shared": "3.5.29",
|
||||
"@vue/compiler-core": "3.5.30",
|
||||
"@vue/compiler-dom": "3.5.30",
|
||||
"@vue/compiler-ssr": "3.5.30",
|
||||
"@vue/shared": "3.5.30",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss": "^8.5.8",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz",
|
||||
"integrity": "sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw==",
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz",
|
||||
"integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.29",
|
||||
"@vue/shared": "3.5.29"
|
||||
"@vue/compiler-dom": "3.5.30",
|
||||
"@vue/shared": "3.5.30"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
|
|
@ -1516,53 +1533,53 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.29.tgz",
|
||||
"integrity": "sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA==",
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.30.tgz",
|
||||
"integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.29"
|
||||
"@vue/shared": "3.5.30"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.29.tgz",
|
||||
"integrity": "sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg==",
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.30.tgz",
|
||||
"integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.29",
|
||||
"@vue/shared": "3.5.29"
|
||||
"@vue/reactivity": "3.5.30",
|
||||
"@vue/shared": "3.5.30"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz",
|
||||
"integrity": "sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg==",
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz",
|
||||
"integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.29",
|
||||
"@vue/runtime-core": "3.5.29",
|
||||
"@vue/shared": "3.5.29",
|
||||
"@vue/reactivity": "3.5.30",
|
||||
"@vue/runtime-core": "3.5.30",
|
||||
"@vue/shared": "3.5.30",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.29.tgz",
|
||||
"integrity": "sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g==",
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.30.tgz",
|
||||
"integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.29",
|
||||
"@vue/shared": "3.5.29"
|
||||
"@vue/compiler-ssr": "3.5.30",
|
||||
"@vue/shared": "3.5.30"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.29"
|
||||
"vue": "3.5.30"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.29.tgz",
|
||||
"integrity": "sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg==",
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.30.tgz",
|
||||
"integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@xterm/addon-fit": {
|
||||
|
|
@ -4875,19 +4892,19 @@
|
|||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picocrank": {
|
||||
"version": "1.14.0",
|
||||
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.14.0.tgz",
|
||||
"integrity": "sha512-ksjqPHFMFE6ENaIXjhund50wocFmaLy22jYgWlWikugHBdd/0YlHfOOuoIMn0wKV8bSrJhcM3pQug/qz45Bc4g==",
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/picocrank/-/picocrank-1.14.1.tgz",
|
||||
"integrity": "sha512-N/aGK/deicXevv+n3zWaKrUe66d4QYQ/7SPinEr4fprljNpXQHSl2EBDlmTNlLoujR3QJHClfRO8Gusj2Hs5MQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@hugeicons/core-free-icons": "^3.1.1",
|
||||
"@hugeicons/vue": "^1.0.4",
|
||||
"@hugeicons/core-free-icons": "^4.0.0",
|
||||
"@hugeicons/vue": "^1.0.5",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"femtocrank": "^2.5.0",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vue": "^3.5.28",
|
||||
"vue-router": "^5.0.2"
|
||||
"vue": "^3.5.30",
|
||||
"vue-router": "^5.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
|
|
@ -5007,9 +5024,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
|
|
@ -6603,16 +6620,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.29",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.29.tgz",
|
||||
"integrity": "sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA==",
|
||||
"version": "3.5.30",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.30.tgz",
|
||||
"integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.29",
|
||||
"@vue/compiler-sfc": "3.5.29",
|
||||
"@vue/runtime-dom": "3.5.29",
|
||||
"@vue/server-renderer": "3.5.29",
|
||||
"@vue/shared": "3.5.29"
|
||||
"@vue/compiler-dom": "3.5.30",
|
||||
"@vue/compiler-sfc": "3.5.30",
|
||||
"@vue/runtime-dom": "3.5.30",
|
||||
"@vue/server-renderer": "3.5.30",
|
||||
"@vue/shared": "3.5.30"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
|
|
@ -6624,13 +6641,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vue-i18n": {
|
||||
"version": "11.2.8",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.8.tgz",
|
||||
"integrity": "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==",
|
||||
"version": "11.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.3.0.tgz",
|
||||
"integrity": "sha512-1J+xDfDJTLhDxElkd3+XUhT7FYSZd2b8pa7IRKGxhWH/8yt6PTvi3xmWhGwhYT5EaXdatui11pF2R6tL73/zPA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@intlify/core-base": "11.2.8",
|
||||
"@intlify/shared": "11.2.8",
|
||||
"@intlify/core-base": "11.3.0",
|
||||
"@intlify/devtools-types": "11.3.0",
|
||||
"@intlify/shared": "11.3.0",
|
||||
"@vue/devtools-api": "^6.5.0"
|
||||
},
|
||||
"engines": {
|
||||
|
|
|
|||
|
|
@ -24,19 +24,19 @@
|
|||
"dependencies": {
|
||||
"@connectrpc/connect": "^2.1.1",
|
||||
"@connectrpc/connect-web": "^2.1.1",
|
||||
"@hugeicons/core-free-icons": "^3.3.0",
|
||||
"@hugeicons/vue": "^1.0.4",
|
||||
"@hugeicons/core-free-icons": "^4.0.0",
|
||||
"@hugeicons/vue": "^1.0.5",
|
||||
"@vitejs/plugin-vue": "^6.0.4",
|
||||
"@xterm/addon-fit": "^0.11.0",
|
||||
"@xterm/addon-web-links": "^0.12.0",
|
||||
"@xterm/xterm": "^6.0.0",
|
||||
"iconify-icon": "^3.0.2",
|
||||
"picocrank": "^1.14.0",
|
||||
"picocrank": "^1.14.1",
|
||||
"standard": "^17.1.2",
|
||||
"unplugin-vue-components": "^31.0.0",
|
||||
"vite": "^7.3.1",
|
||||
"vue": "^3.5.29",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue": "^3.5.30",
|
||||
"vue-i18n": "^11.3.0",
|
||||
"vue-router": "^5.0.3"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
<template>
|
||||
<span id="connection-banner" v-if="!connectionState.connected" class="inline-notification critical user-info-connection">
|
||||
<span class="connection-banner-sr-only" role="status">{{ staticAnnouncement }}</span>
|
||||
<span aria-hidden="true">{{ bannerText }}</span>
|
||||
<span aria-hidden="true">
|
||||
<a :href="websocketDocsUrl" target="_blank" rel="noopener noreferrer" class="connection-banner-link">{{ linkText }}</a>{{ bannerSuffix }}
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
|
|
@ -12,6 +14,10 @@ import { connectionState } from '../stores/connectionState.js'
|
|||
|
||||
const { t } = useI18n()
|
||||
|
||||
const websocketDocsUrl = 'https://docs.olivetin.app/troubleshooting/err-websocket-connection.html'
|
||||
|
||||
const linkText = computed(() => t('disconnected-banner-link-text'))
|
||||
|
||||
function formatShortRelative(ms) {
|
||||
if (ms < 0) return '0s'
|
||||
const secs = Math.floor(ms / 1000)
|
||||
|
|
@ -49,16 +55,16 @@ onUnmounted(() => {
|
|||
|
||||
const staticAnnouncement = computed(() => t('disconnected-banner-announcement'))
|
||||
|
||||
const bannerText = computed(() => {
|
||||
const bannerSuffix = computed(() => {
|
||||
const at = connectionState.disconnectedAt
|
||||
const next = connectionState.nextReconnectAt
|
||||
const n = now.value
|
||||
const disconnectedSince = formatShortTime(at)
|
||||
if (next != null && next > n) {
|
||||
const reconnectIn = formatShortRelative(next - n)
|
||||
return t('disconnected-banner', { disconnectedSince, reconnectIn })
|
||||
return t('disconnected-banner-suffix', { disconnectedSince, reconnectIn })
|
||||
}
|
||||
return t('disconnected-banner-reconnecting', { disconnectedSince })
|
||||
return t('disconnected-banner-suffix-reconnecting', { disconnectedSince })
|
||||
})
|
||||
</script>
|
||||
|
||||
|
|
@ -70,6 +76,10 @@ const bannerText = computed(() => {
|
|||
border: 0;
|
||||
margin: 0;
|
||||
}
|
||||
.connection-banner-link {
|
||||
color: inherit;
|
||||
text-decoration: underline;
|
||||
}
|
||||
.connection-banner-sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
|
|
|
|||
|
|
@ -21,9 +21,10 @@
|
|||
"diagnostics.useragent-data-error": "Fehler beim Abrufen von userAgentData",
|
||||
"diagnostics.where-to-find-help": "Wo Sie Hilfe finden",
|
||||
"disconnected": "Getrennt",
|
||||
"disconnected-banner": "Events-Websocket getrennt seit {disconnectedSince}. Erneuter Verbindungsversuch in {reconnectIn}.",
|
||||
"disconnected-banner-announcement": "Events-Websocket getrennt.",
|
||||
"disconnected-banner-reconnecting": "Events-Websocket getrennt seit {disconnectedSince}. Verbindungsversuch…",
|
||||
"disconnected-banner-link-text": "Events-Websocket getrennt",
|
||||
"disconnected-banner-suffix": " seit {disconnectedSince}. Erneuter Verbindungsversuch in {reconnectIn}.",
|
||||
"disconnected-banner-suffix-reconnecting": " seit {disconnectedSince}. Verbindungsversuch…",
|
||||
"docs": "Dokumentation",
|
||||
"language-dialog.browser-languages": "Browser-Sprachen",
|
||||
"language-dialog.close": "Schließen",
|
||||
|
|
@ -79,9 +80,10 @@
|
|||
"diagnostics.useragent-data-error": "Error retrieving userAgentData",
|
||||
"diagnostics.where-to-find-help": "Where to find help",
|
||||
"disconnected": "Disconnected",
|
||||
"disconnected-banner": "Events websocket disconnected since {disconnectedSince}. Trying reconnect in {reconnectIn}.",
|
||||
"disconnected-banner-announcement": "Events websocket disconnected.",
|
||||
"disconnected-banner-reconnecting": "Events websocket disconnected since {disconnectedSince}. Trying reconnect…",
|
||||
"disconnected-banner-link-text": "Events websocket disconnected",
|
||||
"disconnected-banner-suffix": " since {disconnectedSince}. Trying reconnect in {reconnectIn}.",
|
||||
"disconnected-banner-suffix-reconnecting": " since {disconnectedSince}. Trying reconnect…",
|
||||
"docs": "Documentation",
|
||||
"language-dialog.browser-languages": "Browser languages",
|
||||
"language-dialog.close": "Close",
|
||||
|
|
@ -137,9 +139,10 @@
|
|||
"diagnostics.useragent-data-error": "Error al recuperar userAgentData",
|
||||
"diagnostics.where-to-find-help": "Dónde encontrar ayuda",
|
||||
"disconnected": "Desconectado",
|
||||
"disconnected-banner": "Websocket de eventos desconectado desde {disconnectedSince}. Reintentando conexión en {reconnectIn}.",
|
||||
"disconnected-banner-announcement": "Websocket de eventos desconectado.",
|
||||
"disconnected-banner-reconnecting": "Websocket de eventos desconectado desde {disconnectedSince}. Reintentando conexión…",
|
||||
"disconnected-banner-link-text": "Websocket de eventos desconectado",
|
||||
"disconnected-banner-suffix": " desde {disconnectedSince}. Reintentando conexión en {reconnectIn}.",
|
||||
"disconnected-banner-suffix-reconnecting": " desde {disconnectedSince}. Reintentando conexión…",
|
||||
"docs": "Documentación",
|
||||
"language-dialog.browser-languages": "Idiomas del navegador",
|
||||
"language-dialog.close": "Cerrar",
|
||||
|
|
@ -195,9 +198,10 @@
|
|||
"diagnostics.useragent-data-error": "Errore nel recupero di userAgentData",
|
||||
"diagnostics.where-to-find-help": "Dove trovare aiuto",
|
||||
"disconnected": "Disconnesso",
|
||||
"disconnected-banner": "Websocket eventi disconnesso dalle {disconnectedSince}. Nuovo tentativo tra {reconnectIn}.",
|
||||
"disconnected-banner-announcement": "Websocket eventi disconnesso.",
|
||||
"disconnected-banner-reconnecting": "Websocket eventi disconnesso dalle {disconnectedSince}. Tentativo di connessione…",
|
||||
"disconnected-banner-link-text": "Websocket eventi disconnesso",
|
||||
"disconnected-banner-suffix": " dalle {disconnectedSince}. Nuovo tentativo tra {reconnectIn}.",
|
||||
"disconnected-banner-suffix-reconnecting": " dalle {disconnectedSince}. Tentativo di connessione…",
|
||||
"docs": "Documentazione",
|
||||
"language-dialog.browser-languages": "Lingue del browser",
|
||||
"language-dialog.close": "Chiudi",
|
||||
|
|
@ -253,9 +257,10 @@
|
|||
"diagnostics.useragent-data-error": "检索 userAgentData 时出错",
|
||||
"diagnostics.where-to-find-help": "在哪里找到帮助",
|
||||
"disconnected": "已断开连接",
|
||||
"disconnected-banner": "事件 WebSocket 自 {disconnectedSince} 已断开。{reconnectIn} 后尝试重连。",
|
||||
"disconnected-banner-announcement": "事件 WebSocket 已断开。",
|
||||
"disconnected-banner-reconnecting": "事件 WebSocket 自 {disconnectedSince} 已断开。正在尝试重连…",
|
||||
"disconnected-banner-link-text": "事件 WebSocket 已断开",
|
||||
"disconnected-banner-suffix": "自 {disconnectedSince}。{reconnectIn} 后尝试重连。",
|
||||
"disconnected-banner-suffix-reconnecting": "自 {disconnectedSince}。正在尝试重连…",
|
||||
"docs": "文档",
|
||||
"language-dialog.browser-languages": "浏览器语言",
|
||||
"language-dialog.close": "关闭",
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ translations:
|
|||
connected: Verbunden
|
||||
disconnected: Getrennt
|
||||
reconnecting: Verbinde erneut…
|
||||
disconnected-banner: "Events-Websocket getrennt seit {disconnectedSince}. Erneuter Verbindungsversuch in {reconnectIn}."
|
||||
disconnected-banner-reconnecting: "Events-Websocket getrennt seit {disconnectedSince}. Verbindungsversuch…"
|
||||
disconnected-banner-announcement: Events-Websocket getrennt.
|
||||
disconnected-banner-link-text: "Events-Websocket getrennt"
|
||||
disconnected-banner-suffix: " seit {disconnectedSince}. Erneuter Verbindungsversuch in {reconnectIn}."
|
||||
disconnected-banner-suffix-reconnecting: " seit {disconnectedSince}. Verbindungsversuch…"
|
||||
login-button: Login
|
||||
raise-issue: Ein Problem melden auf GitHub
|
||||
docs: Dokumentation
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ translations:
|
|||
connected: Connected
|
||||
disconnected: Disconnected
|
||||
reconnecting: Reconnecting…
|
||||
disconnected-banner: "Events websocket disconnected since {disconnectedSince}. Trying reconnect in {reconnectIn}."
|
||||
disconnected-banner-reconnecting: "Events websocket disconnected since {disconnectedSince}. Trying reconnect…"
|
||||
disconnected-banner-announcement: Events websocket disconnected.
|
||||
disconnected-banner-link-text: "Events websocket disconnected"
|
||||
disconnected-banner-suffix: " since {disconnectedSince}. Trying reconnect in {reconnectIn}."
|
||||
disconnected-banner-suffix-reconnecting: " since {disconnectedSince}. Trying reconnect…"
|
||||
login-button: Login
|
||||
logs.title: Logs
|
||||
logs.page-description: This is a list of logs from actions that have been executed. You can filter the list by action title.
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ translations:
|
|||
connected: Conectado
|
||||
disconnected: Desconectado
|
||||
reconnecting: Reconectando…
|
||||
disconnected-banner: "Websocket de eventos desconectado desde {disconnectedSince}. Reintentando conexión en {reconnectIn}."
|
||||
disconnected-banner-reconnecting: "Websocket de eventos desconectado desde {disconnectedSince}. Reintentando conexión…"
|
||||
disconnected-banner-announcement: Websocket de eventos desconectado.
|
||||
disconnected-banner-link-text: "Websocket de eventos desconectado"
|
||||
disconnected-banner-suffix: " desde {disconnectedSince}. Reintentando conexión en {reconnectIn}."
|
||||
disconnected-banner-suffix-reconnecting: " desde {disconnectedSince}. Reintentando conexión…"
|
||||
login-button: Iniciar sesión
|
||||
raise-issue: Reportar un problema en GitHub
|
||||
docs: Documentación
|
||||
|
|
|
|||
|
|
@ -9,9 +9,10 @@ translations:
|
|||
connected: Connesso
|
||||
disconnected: Disconnesso
|
||||
reconnecting: Riconnessione…
|
||||
disconnected-banner: "Websocket eventi disconnesso dalle {disconnectedSince}. Nuovo tentativo tra {reconnectIn}."
|
||||
disconnected-banner-reconnecting: "Websocket eventi disconnesso dalle {disconnectedSince}. Tentativo di connessione…"
|
||||
disconnected-banner-announcement: Websocket eventi disconnesso.
|
||||
disconnected-banner-link-text: "Websocket eventi disconnesso"
|
||||
disconnected-banner-suffix: " dalle {disconnectedSince}. Nuovo tentativo tra {reconnectIn}."
|
||||
disconnected-banner-suffix-reconnecting: " dalle {disconnectedSince}. Tentativo di connessione…"
|
||||
login-button: Login
|
||||
raise-issue: Segnala un problema su GitHub
|
||||
logs.title: Registri
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ translations:
|
|||
connected: 已连接
|
||||
disconnected: 已断开连接
|
||||
reconnecting: 正在重新连接…
|
||||
disconnected-banner: "事件 WebSocket 自 {disconnectedSince} 已断开。{reconnectIn} 后尝试重连。"
|
||||
disconnected-banner-reconnecting: "事件 WebSocket 自 {disconnectedSince} 已断开。正在尝试重连…"
|
||||
disconnected-banner-announcement: 事件 WebSocket 已断开。
|
||||
disconnected-banner-link-text: "事件 WebSocket 已断开"
|
||||
disconnected-banner-suffix: "自 {disconnectedSince}。{reconnectIn} 后尝试重连。"
|
||||
disconnected-banner-suffix-reconnecting: "自 {disconnectedSince}。正在尝试重连…"
|
||||
login-button: 登录
|
||||
raise-issue: 在 GitHub 上报告问题
|
||||
docs: 文档
|
||||
|
|
|
|||
|
|
@ -60,6 +60,20 @@ type streamingClient struct {
|
|||
AuthenticatedUser *authpublic.AuthenticatedUser
|
||||
}
|
||||
|
||||
// trySendEventToClient sends msg to the client's channel. Returns false if the channel is full (client should be removed).
|
||||
func (api *oliveTinAPI) trySendEventToClient(client *streamingClient, msg *apiv1.EventStreamResponse) bool {
|
||||
if client == nil || msg == nil {
|
||||
return false
|
||||
}
|
||||
select {
|
||||
case client.channel <- msg:
|
||||
return true
|
||||
default:
|
||||
log.Warnf("EventStream: client channel is full, removing client")
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) KillAction(ctx ctx.Context, req *connect.Request[apiv1.KillActionRequest]) (*connect.Response[apiv1.KillActionResponse], error) {
|
||||
ret := &apiv1.KillActionResponse{
|
||||
ExecutionTrackingId: req.Msg.ExecutionTrackingId,
|
||||
|
|
@ -589,9 +603,20 @@ func isValidLogEntry(e *executor.InternalLogEntry) bool {
|
|||
|
||||
// isLogEntryAllowed checks if a log entry is allowed to be viewed by the user.
|
||||
func (api *oliveTinAPI) isLogEntryAllowed(e *executor.InternalLogEntry, user *authpublic.AuthenticatedUser) bool {
|
||||
if user == nil || !isValidLogEntry(e) {
|
||||
return false
|
||||
}
|
||||
return acl.IsAllowedLogs(api.cfg, user, e.Binding.Action)
|
||||
}
|
||||
|
||||
// mayViewExecutionEvent returns whether the user is allowed to receive this execution event (for EventStream ACL).
|
||||
func (api *oliveTinAPI) mayViewExecutionEvent(entry *executor.InternalLogEntry, user *authpublic.AuthenticatedUser) bool {
|
||||
if user == nil {
|
||||
return false
|
||||
}
|
||||
return isValidLogEntry(entry) && api.isLogEntryAllowed(entry, user)
|
||||
}
|
||||
|
||||
// buildEmptyPageResponse creates a response for an empty page.
|
||||
func buildEmptyPageResponse(page pageInfo) *apiv1.GetActionLogsResponse {
|
||||
return &apiv1.GetActionLogsResponse{
|
||||
|
|
@ -886,6 +911,9 @@ func (api *oliveTinAPI) EventStream(ctx ctx.Context, req *connect.Request[apiv1.
|
|||
}
|
||||
|
||||
func (api *oliveTinAPI) removeClient(clientToRemove *streamingClient) {
|
||||
if clientToRemove == nil {
|
||||
return
|
||||
}
|
||||
api.streamingClientsMutex.Lock()
|
||||
delete(api.streamingClients, clientToRemove)
|
||||
api.streamingClientsMutex.Unlock()
|
||||
|
|
@ -915,50 +943,62 @@ func (api *oliveTinAPI) OnActionMapRebuilt() {
|
|||
|
||||
func (api *oliveTinAPI) OnExecutionStarted(ex *executor.InternalLogEntry) {
|
||||
toRemove := []*streamingClient{}
|
||||
|
||||
for _, client := range api.copyOfStreamingClients() {
|
||||
select {
|
||||
case client.channel <- &apiv1.EventStreamResponse{
|
||||
Event: &apiv1.EventStreamResponse_ExecutionStarted{
|
||||
ExecutionStarted: &apiv1.EventExecutionStarted{
|
||||
LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
|
||||
},
|
||||
},
|
||||
}:
|
||||
default:
|
||||
log.Warnf("EventStream: client channel is full, removing client")
|
||||
toRemove = append(toRemove, client)
|
||||
}
|
||||
api.maybeSendExecutionStarted(client, ex, &toRemove)
|
||||
}
|
||||
|
||||
for _, client := range toRemove {
|
||||
api.removeClient(client)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) maybeSendExecutionStarted(client *streamingClient, ex *executor.InternalLogEntry, toRemove *[]*streamingClient) {
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
if !api.mayViewExecutionEvent(ex, client.AuthenticatedUser) {
|
||||
return
|
||||
}
|
||||
msg := &apiv1.EventStreamResponse{
|
||||
Event: &apiv1.EventStreamResponse_ExecutionStarted{
|
||||
ExecutionStarted: &apiv1.EventExecutionStarted{
|
||||
LogEntry: api.internalLogEntryToPb(ex, client.AuthenticatedUser),
|
||||
},
|
||||
},
|
||||
}
|
||||
if !api.trySendEventToClient(client, msg) {
|
||||
*toRemove = append(*toRemove, client)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) OnExecutionFinished(ile *executor.InternalLogEntry) {
|
||||
toRemove := []*streamingClient{}
|
||||
|
||||
for _, client := range api.copyOfStreamingClients() {
|
||||
select {
|
||||
case client.channel <- &apiv1.EventStreamResponse{
|
||||
Event: &apiv1.EventStreamResponse_ExecutionFinished{
|
||||
ExecutionFinished: &apiv1.EventExecutionFinished{
|
||||
LogEntry: api.internalLogEntryToPb(ile, client.AuthenticatedUser),
|
||||
},
|
||||
},
|
||||
}:
|
||||
default:
|
||||
log.Warnf("EventStream: client channel is full, removing client")
|
||||
toRemove = append(toRemove, client)
|
||||
}
|
||||
api.maybeSendExecutionFinished(client, ile, &toRemove)
|
||||
}
|
||||
|
||||
for _, client := range toRemove {
|
||||
api.removeClient(client)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) maybeSendExecutionFinished(client *streamingClient, ile *executor.InternalLogEntry, toRemove *[]*streamingClient) {
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
if !api.mayViewExecutionEvent(ile, client.AuthenticatedUser) {
|
||||
return
|
||||
}
|
||||
msg := &apiv1.EventStreamResponse{
|
||||
Event: &apiv1.EventStreamResponse_ExecutionFinished{
|
||||
ExecutionFinished: &apiv1.EventExecutionFinished{
|
||||
LogEntry: api.internalLogEntryToPb(ile, client.AuthenticatedUser),
|
||||
},
|
||||
},
|
||||
}
|
||||
if !api.trySendEventToClient(client, msg) {
|
||||
*toRemove = append(*toRemove, client)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) GetDiagnostics(ctx ctx.Context, req *connect.Request[apiv1.GetDiagnosticsRequest]) (*connect.Response[apiv1.GetDiagnosticsResponse], error) {
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
if err := api.checkDashboardAccess(user); err != nil {
|
||||
|
|
@ -1128,29 +1168,47 @@ func buildAdditionalLinks(links []*config.NavigationLink) []*apiv1.AdditionalLin
|
|||
}
|
||||
|
||||
func (api *oliveTinAPI) OnOutputChunk(content []byte, executionTrackingId string) {
|
||||
toRemove := []*streamingClient{}
|
||||
|
||||
for _, client := range api.copyOfStreamingClients() {
|
||||
select {
|
||||
case client.channel <- &apiv1.EventStreamResponse{
|
||||
Event: &apiv1.EventStreamResponse_OutputChunk{
|
||||
OutputChunk: &apiv1.EventOutputChunk{
|
||||
Output: string(content),
|
||||
ExecutionTrackingId: executionTrackingId,
|
||||
},
|
||||
},
|
||||
}:
|
||||
default:
|
||||
log.Warnf("EventStream: client channel is full, removing client")
|
||||
toRemove = append(toRemove, client)
|
||||
}
|
||||
entry := api.getValidLogEntryForStreaming(executionTrackingId)
|
||||
if entry == nil {
|
||||
return
|
||||
}
|
||||
msg := &apiv1.EventStreamResponse{
|
||||
Event: &apiv1.EventStreamResponse_OutputChunk{
|
||||
OutputChunk: &apiv1.EventOutputChunk{
|
||||
Output: string(content),
|
||||
ExecutionTrackingId: executionTrackingId,
|
||||
},
|
||||
},
|
||||
}
|
||||
toRemove := []*streamingClient{}
|
||||
for _, client := range api.copyOfStreamingClients() {
|
||||
api.maybeSendOutputChunk(client, entry, msg, &toRemove)
|
||||
}
|
||||
|
||||
for _, client := range toRemove {
|
||||
api.removeClient(client)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) getValidLogEntryForStreaming(executionTrackingId string) *executor.InternalLogEntry {
|
||||
entry, ok := api.executor.GetLog(executionTrackingId)
|
||||
if !ok || !isValidLogEntry(entry) {
|
||||
return nil
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) maybeSendOutputChunk(client *streamingClient, entry *executor.InternalLogEntry, msg *apiv1.EventStreamResponse, toRemove *[]*streamingClient) {
|
||||
if client == nil {
|
||||
return
|
||||
}
|
||||
if !api.mayViewExecutionEvent(entry, client.AuthenticatedUser) {
|
||||
return
|
||||
}
|
||||
if !api.trySendEventToClient(client, msg) {
|
||||
*toRemove = append(*toRemove, client)
|
||||
}
|
||||
}
|
||||
|
||||
func (api *oliveTinAPI) GetEntities(ctx ctx.Context, req *connect.Request[apiv1.GetEntitiesRequest]) (*connect.Response[apiv1.GetEntitiesResponse], error) {
|
||||
user := auth.UserFromApiCall(ctx, req, api.cfg)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,24 +2,24 @@ package api
|
|||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"connectrpc.com/connect"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
apiv1 "github.com/OliveTin/OliveTin/gen/olivetin/api/v1"
|
||||
apiv1connect "github.com/OliveTin/OliveTin/gen/olivetin/api/v1/apiv1connect"
|
||||
authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
|
||||
config "github.com/OliveTin/OliveTin/internal/config"
|
||||
"github.com/OliveTin/OliveTin/internal/entities"
|
||||
"github.com/OliveTin/OliveTin/internal/executor"
|
||||
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path"
|
||||
)
|
||||
|
||||
func getNewTestServerAndClient(injectedConfig *config.Config) (*httptest.Server, apiv1connect.OliveTinApiServiceClient) {
|
||||
|
|
@ -338,12 +338,13 @@ func testWithEntity(t *testing.T, binding *executor.ActionBinding, rr *Dashboard
|
|||
}
|
||||
|
||||
// buildViewPermissionTestConfig returns config and users for GHSA view-permission tests:
|
||||
// one action "secret_action", ACL "restricted" (view:false) for user "low", ACL "full" (view:true) for user "admin".
|
||||
// one action "secret_action", ACL "restricted" (view:false, logs:false) for user "low", ACL "full" (view:true, logs:true) for user "admin".
|
||||
func buildViewPermissionTestConfig(t *testing.T) (*config.Config, *authpublic.AuthenticatedUser, *authpublic.AuthenticatedUser) {
|
||||
t.Helper()
|
||||
cfg := config.DefaultConfig()
|
||||
cfg.DefaultPermissions.View = false
|
||||
cfg.DefaultPermissions.Exec = false
|
||||
cfg.DefaultPermissions.Logs = false
|
||||
|
||||
cfg.Actions = append(cfg.Actions, &config.Action{
|
||||
ID: "secret_action",
|
||||
|
|
@ -572,3 +573,133 @@ func TestOrderTopLevelDashboardComponents_SortablesSorted(t *testing.T) {
|
|||
assert.Equal(t, "Alpha", out[0].Title, "sortables ordered by title")
|
||||
assert.Equal(t, "Beta", out[1].Title)
|
||||
}
|
||||
|
||||
// TestEventStreamACLNoLeakToUnauthorizedUser (GHSA-228v-wc5r-j8m7) asserts that EventStream
|
||||
// does not send execution events or output chunks to users who are not allowed to view that action's logs.
|
||||
func TestEventStreamACLNoLeakToUnauthorizedUser(t *testing.T) {
|
||||
cfg, lowUser, adminUser := buildViewPermissionTestConfig(t)
|
||||
ex := executor.DefaultExecutor(cfg)
|
||||
ex.RebuildActionMap()
|
||||
api := newServer(ex)
|
||||
|
||||
binding := ex.FindBindingByID("secret_action")
|
||||
require.NotNil(t, binding, "secret_action binding must exist")
|
||||
|
||||
clientLow, clientAdmin := addEventStreamTestClients(t, api, lowUser, adminUser)
|
||||
defer removeEventStreamTestClients(api, clientLow, clientAdmin)
|
||||
|
||||
runEventStreamTestExecution(t, ex, cfg, binding, adminUser)
|
||||
adminEvents := drainEventStreamUntilFinished(clientAdmin.channel, 2*time.Second)
|
||||
lowEvents := drainEventStreamWithTimeout(clientLow.channel, 50*time.Millisecond)
|
||||
|
||||
assertEventStreamLowUserReceivesNothing(t, lowEvents)
|
||||
assertEventStreamAdminReceivesSecretActionEvents(t, adminEvents)
|
||||
}
|
||||
|
||||
func addEventStreamTestClients(t *testing.T, api *oliveTinAPI, lowUser, adminUser *authpublic.AuthenticatedUser) (*streamingClient, *streamingClient) {
|
||||
t.Helper()
|
||||
clientLow := &streamingClient{
|
||||
channel: make(chan *apiv1.EventStreamResponse, 20),
|
||||
AuthenticatedUser: lowUser,
|
||||
}
|
||||
clientAdmin := &streamingClient{
|
||||
channel: make(chan *apiv1.EventStreamResponse, 20),
|
||||
AuthenticatedUser: adminUser,
|
||||
}
|
||||
api.streamingClientsMutex.Lock()
|
||||
api.streamingClients[clientLow] = struct{}{}
|
||||
api.streamingClients[clientAdmin] = struct{}{}
|
||||
api.streamingClientsMutex.Unlock()
|
||||
return clientLow, clientAdmin
|
||||
}
|
||||
|
||||
func removeEventStreamTestClients(api *oliveTinAPI, clientLow, clientAdmin *streamingClient) {
|
||||
api.streamingClientsMutex.Lock()
|
||||
delete(api.streamingClients, clientLow)
|
||||
delete(api.streamingClients, clientAdmin)
|
||||
api.streamingClientsMutex.Unlock()
|
||||
close(clientLow.channel)
|
||||
close(clientAdmin.channel)
|
||||
}
|
||||
|
||||
func runEventStreamTestExecution(t *testing.T, ex *executor.Executor, cfg *config.Config, binding *executor.ActionBinding, adminUser *authpublic.AuthenticatedUser) {
|
||||
t.Helper()
|
||||
execReq := &executor.ExecutionRequest{
|
||||
Binding: binding,
|
||||
Arguments: map[string]string{},
|
||||
TrackingID: uuid.NewString(),
|
||||
Cfg: cfg,
|
||||
AuthenticatedUser: adminUser,
|
||||
}
|
||||
wg, _ := ex.ExecRequest(execReq)
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func drainEventStreamUntilFinished(ch <-chan *apiv1.EventStreamResponse, timeout time.Duration) []*apiv1.EventStreamResponse {
|
||||
var out []*apiv1.EventStreamResponse
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
ev, finished := recvEventStreamOne(ch, 50*time.Millisecond)
|
||||
if ev != nil {
|
||||
out = append(out, ev)
|
||||
}
|
||||
if finished {
|
||||
return out
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func recvEventStreamOne(ch <-chan *apiv1.EventStreamResponse, timeout time.Duration) (*apiv1.EventStreamResponse, bool) {
|
||||
select {
|
||||
case ev, ok := <-ch:
|
||||
if !ok {
|
||||
return nil, true
|
||||
}
|
||||
return ev, ev.GetExecutionFinished() != nil
|
||||
case <-time.After(timeout):
|
||||
return nil, true
|
||||
}
|
||||
}
|
||||
|
||||
func drainEventStreamWithTimeout(ch <-chan *apiv1.EventStreamResponse, timeout time.Duration) []*apiv1.EventStreamResponse {
|
||||
var out []*apiv1.EventStreamResponse
|
||||
for {
|
||||
select {
|
||||
case ev, ok := <-ch:
|
||||
if !ok {
|
||||
return out
|
||||
}
|
||||
out = append(out, ev)
|
||||
case <-time.After(timeout):
|
||||
return out
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func assertEventStreamLowUserReceivesNothing(t *testing.T, lowEvents []*apiv1.EventStreamResponse) {
|
||||
t.Helper()
|
||||
for _, ev := range lowEvents {
|
||||
assert.Nil(t, ev.GetExecutionStarted(), "low-privilege user must not receive ExecutionStarted")
|
||||
assert.Nil(t, ev.GetExecutionFinished(), "low-privilege user must not receive ExecutionFinished")
|
||||
assert.Nil(t, ev.GetOutputChunk(), "low-privilege user must not receive OutputChunk")
|
||||
}
|
||||
assert.Empty(t, lowEvents, "low-privilege user with Logs:false must not receive any execution events")
|
||||
}
|
||||
|
||||
func assertEventStreamAdminReceivesSecretActionEvents(t *testing.T, adminEvents []*apiv1.EventStreamResponse) {
|
||||
t.Helper()
|
||||
var gotStarted, gotFinished bool
|
||||
for _, ev := range adminEvents {
|
||||
if ev.GetExecutionStarted() != nil {
|
||||
gotStarted = true
|
||||
assert.Equal(t, "secret_action", ev.GetExecutionStarted().LogEntry.GetBindingId())
|
||||
}
|
||||
if ev.GetExecutionFinished() != nil {
|
||||
gotFinished = true
|
||||
assert.Equal(t, "secret_action", ev.GetExecutionFinished().LogEntry.GetBindingId())
|
||||
}
|
||||
}
|
||||
assert.True(t, gotStarted, "admin must receive ExecutionStarted for secret_action")
|
||||
assert.True(t, gotFinished, "admin must receive ExecutionFinished for secret_action")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -281,7 +281,7 @@ func DefaultConfigWithBasePort(basePort int) *Config {
|
|||
config.Prometheus.Enabled = false
|
||||
config.Prometheus.DefaultGoMetrics = false
|
||||
config.Security.HeaderContentSecurityPolicy = true
|
||||
config.Security.ContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'"
|
||||
config.Security.ContentSecurityPolicy = ContentSecurityPolicyDefault
|
||||
config.Security.HeaderXContentTypeOptions = true
|
||||
config.Security.HeaderXFrameOptions = true
|
||||
config.Security.XFrameOptions = "DENY"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,3 @@
|
|||
package config
|
||||
|
||||
const ContentSecurityPolicyDefault = "default-src 'self'; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'"
|
||||
|
|
@ -194,7 +194,7 @@ func (cfg *Config) sanitizeSecurityHeadersCSP() {
|
|||
if !cfg.Security.HeaderContentSecurityPolicy || cfg.Security.ContentSecurityPolicy != "" {
|
||||
return
|
||||
}
|
||||
cfg.Security.ContentSecurityPolicy = "default-src 'self'; script-src 'self' 'unsafe-inline' https:; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self' https:; frame-ancestors 'none'; base-uri 'self'"
|
||||
cfg.Security.ContentSecurityPolicy = ContentSecurityPolicyDefault
|
||||
}
|
||||
|
||||
func (cfg *Config) sanitizeSecurityHeadersXFrameOptions() {
|
||||
|
|
|
|||
|
|
@ -250,13 +250,10 @@ func typecheckChoiceEntity(value string, arg *config.ActionArgument) error {
|
|||
|
||||
func typeSafetyCheckEmail(value string) error {
|
||||
_, err := mail.ParseAddress(value)
|
||||
|
||||
log.Errorf("Email check: %v, %v", err, value)
|
||||
|
||||
if err != nil {
|
||||
log.WithField("type", "email").Debugf("Email argument type check failed")
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue