Compare commits
9 Commits
v0.14.0-de
...
v0.14.0-de
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
04fc28c492 | ||
|
|
623a09fd7e | ||
|
|
b00aa7ef84 | ||
|
|
acfa265595 | ||
|
|
35b171764e | ||
|
|
6b53ab2d73 | ||
|
|
1b829094ef | ||
|
|
e28e9f5879 | ||
|
|
cb84547c88 |
367
package-lock.json
generated
367
package-lock.json
generated
@@ -15,6 +15,14 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.11"
|
"baseline-browser-mapping": "^2.9.11"
|
||||||
},
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@rollup/rollup-darwin-arm64": "4.52.5",
|
||||||
|
"@rollup/rollup-darwin-x64": "4.52.5",
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": "4.52.5",
|
||||||
|
"@rollup/rollup-linux-x64-gnu": "4.52.5",
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": "4.52.5",
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "4.52.5"
|
||||||
|
},
|
||||||
"workspaces": {
|
"workspaces": {
|
||||||
"packages": [
|
"packages": [
|
||||||
"packages/server",
|
"packages/server",
|
||||||
@@ -64,6 +72,7 @@
|
|||||||
"version": "7.28.5",
|
"version": "7.28.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.27.1",
|
"@babel/code-frame": "^7.27.1",
|
||||||
"@babel/generator": "^7.28.5",
|
"@babel/generator": "^7.28.5",
|
||||||
@@ -2930,16 +2939,304 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-android-arm64": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
"node_modules/@rollup/rollup-darwin-x64": {
|
"node_modules/@rollup/rollup-darwin-x64": {
|
||||||
"version": "4.52.5",
|
"version": "4.52.5",
|
||||||
"cpu": [
|
"cpu": [
|
||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"darwin"
|
"freebsd"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||||
|
"version": "4.52.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
|
||||||
|
"integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/@shikijs/core": {
|
"node_modules/@shikijs/core": {
|
||||||
@@ -3380,6 +3677,7 @@
|
|||||||
"version": "7.20.5",
|
"version": "7.20.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/parser": "^7.20.7",
|
"@babel/parser": "^7.20.7",
|
||||||
"@babel/types": "^7.20.7",
|
"@babel/types": "^7.20.7",
|
||||||
@@ -3481,6 +3779,7 @@
|
|||||||
"version": "22.19.0",
|
"version": "22.19.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
@@ -3555,6 +3854,7 @@
|
|||||||
"integrity": "sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==",
|
"integrity": "sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cac": "^6.7.14",
|
"cac": "^6.7.14",
|
||||||
"colorette": "^2.0.20",
|
"colorette": "^2.0.20",
|
||||||
@@ -3637,6 +3937,7 @@
|
|||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.1",
|
"fast-deep-equal": "^3.1.1",
|
||||||
"fast-json-stable-stringify": "^2.0.0",
|
"fast-json-stable-stringify": "^2.0.0",
|
||||||
@@ -3839,7 +4140,6 @@
|
|||||||
"version": "5.3.2",
|
"version": "5.3.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^2.1.0",
|
"archiver-utils": "^2.1.0",
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.4",
|
||||||
@@ -3857,7 +4157,6 @@
|
|||||||
"version": "2.1.0",
|
"version": "2.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.1.4",
|
"glob": "^7.1.4",
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
@@ -3878,7 +4177,6 @@
|
|||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
@@ -3892,14 +4190,12 @@
|
|||||||
"node_modules/archiver-utils/node_modules/safe-buffer": {
|
"node_modules/archiver-utils/node_modules/safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/archiver-utils/node_modules/string_decoder": {
|
"node_modules/archiver-utils/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
@@ -4213,7 +4509,6 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer": "^5.5.0",
|
"buffer": "^5.5.0",
|
||||||
"inherits": "^2.0.4",
|
"inherits": "^2.0.4",
|
||||||
@@ -4277,6 +4572,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -4767,7 +5063,6 @@
|
|||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"buffer-crc32": "^0.2.13",
|
"buffer-crc32": "^0.2.13",
|
||||||
"crc32-stream": "^4.0.2",
|
"crc32-stream": "^4.0.2",
|
||||||
@@ -4897,7 +5192,6 @@
|
|||||||
"version": "1.2.2",
|
"version": "1.2.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"crc32": "bin/crc32.njs"
|
"crc32": "bin/crc32.njs"
|
||||||
},
|
},
|
||||||
@@ -4909,7 +5203,6 @@
|
|||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"crc-32": "^1.2.0",
|
"crc-32": "^1.2.0",
|
||||||
"readable-stream": "^3.4.0"
|
"readable-stream": "^3.4.0"
|
||||||
@@ -5275,6 +5568,7 @@
|
|||||||
"version": "24.13.3",
|
"version": "24.13.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "24.13.3",
|
"app-builder-lib": "24.13.3",
|
||||||
"builder-util": "24.13.1",
|
"builder-util": "24.13.1",
|
||||||
@@ -5441,7 +5735,6 @@
|
|||||||
"version": "24.13.3",
|
"version": "24.13.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"app-builder-lib": "24.13.3",
|
"app-builder-lib": "24.13.3",
|
||||||
"archiver": "^5.3.1",
|
"archiver": "^5.3.1",
|
||||||
@@ -5453,7 +5746,6 @@
|
|||||||
"version": "10.1.0",
|
"version": "10.1.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
"jsonfile": "^6.0.1",
|
"jsonfile": "^6.0.1",
|
||||||
@@ -5467,7 +5759,6 @@
|
|||||||
"version": "6.2.0",
|
"version": "6.2.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"universalify": "^2.0.0"
|
"universalify": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -5479,7 +5770,6 @@
|
|||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 10.0.0"
|
"node": ">= 10.0.0"
|
||||||
}
|
}
|
||||||
@@ -6197,8 +6487,7 @@
|
|||||||
"node_modules/fs-constants": {
|
"node_modules/fs-constants": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/fs-extra": {
|
"node_modules/fs-extra": {
|
||||||
"version": "8.1.0",
|
"version": "8.1.0",
|
||||||
@@ -7415,8 +7704,7 @@
|
|||||||
"node_modules/isarray": {
|
"node_modules/isarray": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/isbinaryfile": {
|
"node_modules/isbinaryfile": {
|
||||||
"version": "5.0.6",
|
"version": "5.0.6",
|
||||||
@@ -7466,6 +7754,7 @@
|
|||||||
"version": "1.21.7",
|
"version": "1.21.7",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"jiti": "bin/jiti.js"
|
"jiti": "bin/jiti.js"
|
||||||
}
|
}
|
||||||
@@ -7597,7 +7886,6 @@
|
|||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"readable-stream": "^2.0.5"
|
"readable-stream": "^2.0.5"
|
||||||
},
|
},
|
||||||
@@ -7609,7 +7897,6 @@
|
|||||||
"version": "2.3.8",
|
"version": "2.3.8",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"core-util-is": "~1.0.0",
|
"core-util-is": "~1.0.0",
|
||||||
"inherits": "~2.0.3",
|
"inherits": "~2.0.3",
|
||||||
@@ -7623,14 +7910,12 @@
|
|||||||
"node_modules/lazystream/node_modules/safe-buffer": {
|
"node_modules/lazystream/node_modules/safe-buffer": {
|
||||||
"version": "5.1.2",
|
"version": "5.1.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lazystream/node_modules/string_decoder": {
|
"node_modules/lazystream/node_modules/string_decoder": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.1.0"
|
"safe-buffer": "~5.1.0"
|
||||||
}
|
}
|
||||||
@@ -7695,26 +7980,22 @@
|
|||||||
"node_modules/lodash.defaults": {
|
"node_modules/lodash.defaults": {
|
||||||
"version": "4.2.0",
|
"version": "4.2.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.difference": {
|
"node_modules/lodash.difference": {
|
||||||
"version": "4.5.0",
|
"version": "4.5.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.flatten": {
|
"node_modules/lodash.flatten": {
|
||||||
"version": "4.4.0",
|
"version": "4.4.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.isplainobject": {
|
"node_modules/lodash.isplainobject": {
|
||||||
"version": "4.0.6",
|
"version": "4.0.6",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lodash.sortby": {
|
"node_modules/lodash.sortby": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
@@ -7726,8 +8007,7 @@
|
|||||||
"node_modules/lodash.union": {
|
"node_modules/lodash.union": {
|
||||||
"version": "4.6.0",
|
"version": "4.6.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/lowercase-keys": {
|
"node_modules/lowercase-keys": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
@@ -8531,6 +8811,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -8678,8 +8959,7 @@
|
|||||||
"node_modules/process-nextick-args": {
|
"node_modules/process-nextick-args": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/process-warning": {
|
"node_modules/process-warning": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
@@ -8928,7 +9208,6 @@
|
|||||||
"version": "3.6.2",
|
"version": "3.6.2",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"inherits": "^2.0.3",
|
"inherits": "^2.0.3",
|
||||||
"string_decoder": "^1.1.1",
|
"string_decoder": "^1.1.1",
|
||||||
@@ -8942,7 +9221,6 @@
|
|||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimatch": "^5.1.0"
|
"minimatch": "^5.1.0"
|
||||||
}
|
}
|
||||||
@@ -9245,6 +9523,7 @@
|
|||||||
"version": "4.52.5",
|
"version": "4.52.5",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@@ -9468,6 +9747,7 @@
|
|||||||
"node_modules/seroval": {
|
"node_modules/seroval": {
|
||||||
"version": "1.3.2",
|
"version": "1.3.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
@@ -9791,6 +10071,7 @@
|
|||||||
"node_modules/solid-js": {
|
"node_modules/solid-js": {
|
||||||
"version": "1.9.10",
|
"version": "1.9.10",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"csstype": "^3.1.0",
|
"csstype": "^3.1.0",
|
||||||
"seroval": "~1.3.0",
|
"seroval": "~1.3.0",
|
||||||
@@ -9931,7 +10212,6 @@
|
|||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"safe-buffer": "~5.2.0"
|
"safe-buffer": "~5.2.0"
|
||||||
}
|
}
|
||||||
@@ -10265,7 +10545,6 @@
|
|||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"bl": "^4.0.3",
|
"bl": "^4.0.3",
|
||||||
"end-of-stream": "^1.4.1",
|
"end-of-stream": "^1.4.1",
|
||||||
@@ -10458,6 +10737,7 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -10707,6 +10987,7 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -11054,6 +11335,7 @@
|
|||||||
"version": "5.4.21",
|
"version": "5.4.21",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.21.3",
|
"esbuild": "^0.21.3",
|
||||||
"postcss": "^8.4.43",
|
"postcss": "^8.4.43",
|
||||||
@@ -11538,6 +11820,7 @@
|
|||||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-deep-equal": "^3.1.3",
|
"fast-deep-equal": "^3.1.3",
|
||||||
"fast-uri": "^3.0.1",
|
"fast-uri": "^3.0.1",
|
||||||
@@ -11732,6 +12015,7 @@
|
|||||||
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
"integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"rollup": "dist/bin/rollup"
|
"rollup": "dist/bin/rollup"
|
||||||
},
|
},
|
||||||
@@ -12020,7 +12304,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"archiver-utils": "^3.0.4",
|
"archiver-utils": "^3.0.4",
|
||||||
"compress-commons": "^4.1.2",
|
"compress-commons": "^4.1.2",
|
||||||
@@ -12034,7 +12317,6 @@
|
|||||||
"version": "3.0.4",
|
"version": "3.0.4",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"glob": "^7.2.3",
|
"glob": "^7.2.3",
|
||||||
"graceful-fs": "^4.2.0",
|
"graceful-fs": "^4.2.0",
|
||||||
@@ -12054,6 +12336,7 @@
|
|||||||
"node_modules/zod": {
|
"node_modules/zod": {
|
||||||
"version": "3.25.76",
|
"version": "3.25.76",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,5 +30,13 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.11"
|
"baseline-browser-mapping": "^2.9.11"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@rollup/rollup-darwin-arm64": "4.52.5",
|
||||||
|
"@rollup/rollup-darwin-x64": "4.52.5",
|
||||||
|
"@rollup/rollup-linux-arm64-gnu": "4.52.5",
|
||||||
|
"@rollup/rollup-linux-x64-gnu": "4.52.5",
|
||||||
|
"@rollup/rollup-win32-arm64-msvc": "4.52.5",
|
||||||
|
"@rollup/rollup-win32-x64-msvc": "4.52.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@
|
|||||||
"vite-plugin-solid": "^2.10.0"
|
"vite-plugin-solid": "^2.10.0"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"appId": "ai.opencode.client",
|
"appId": "ai.neuralnomads.codenomad.client",
|
||||||
"productName": "CodeNomad",
|
"productName": "CodeNomad",
|
||||||
"directories": {
|
"directories": {
|
||||||
"output": "release",
|
"output": "release",
|
||||||
|
|||||||
@@ -337,6 +337,16 @@ export interface RemoteServerProbeResponse {
|
|||||||
errorCode?: string
|
errorCode?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface RemoteProxySessionCreateRequest {
|
||||||
|
baseUrl: string
|
||||||
|
skipTlsVerify?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoteProxySessionCreateResponse {
|
||||||
|
sessionId: string
|
||||||
|
windowUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
export type WorkspaceEventType =
|
export type WorkspaceEventType =
|
||||||
| "workspace.created"
|
| "workspace.created"
|
||||||
| "workspace.started"
|
| "workspace.started"
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { launchInBrowser } from "./launcher"
|
|||||||
import { resolveUi } from "./ui/remote-ui"
|
import { resolveUi } from "./ui/remote-ui"
|
||||||
import { AuthManager, BOOTSTRAP_TOKEN_STDOUT_PREFIX, DEFAULT_AUTH_COOKIE_NAME, DEFAULT_AUTH_USERNAME } from "./auth/manager"
|
import { AuthManager, BOOTSTRAP_TOKEN_STDOUT_PREFIX, DEFAULT_AUTH_COOKIE_NAME, DEFAULT_AUTH_USERNAME } from "./auth/manager"
|
||||||
import { resolveHttpsOptions } from "./server/tls"
|
import { resolveHttpsOptions } from "./server/tls"
|
||||||
|
import { RemoteProxySessionManager } from "./server/remote-proxy"
|
||||||
import { resolveNetworkAddresses, resolveRemoteAddresses } from "./server/network-addresses"
|
import { resolveNetworkAddresses, resolveRemoteAddresses } from "./server/network-addresses"
|
||||||
import { startDevReleaseMonitor } from "./releases/dev-release-monitor"
|
import { startDevReleaseMonitor } from "./releases/dev-release-monitor"
|
||||||
import { SpeechService } from "./speech/service"
|
import { SpeechService } from "./speech/service"
|
||||||
@@ -375,14 +376,15 @@ async function main() {
|
|||||||
})
|
})
|
||||||
: null
|
: null
|
||||||
|
|
||||||
if (uiResolution.uiDevServerUrl && options.https) {
|
|
||||||
throw new InvalidArgumentError("UI dev proxy is only supported with --https=false --http=true")
|
|
||||||
}
|
|
||||||
|
|
||||||
const remoteAccessEnabled = options.host === "0.0.0.0" || !isLoopbackHost(options.host)
|
const remoteAccessEnabled = options.host === "0.0.0.0" || !isLoopbackHost(options.host)
|
||||||
|
|
||||||
const clientConnectionManager = new ClientConnectionManager(logger.child({ component: "client-connections" }))
|
const clientConnectionManager = new ClientConnectionManager(logger.child({ component: "client-connections" }))
|
||||||
const pluginChannel = new PluginChannelManager(logger.child({ component: "plugin-channel" }))
|
const pluginChannel = new PluginChannelManager(logger.child({ component: "plugin-channel" }))
|
||||||
|
const remoteProxySessionManager = new RemoteProxySessionManager({
|
||||||
|
authManager,
|
||||||
|
logger: logger.child({ component: "remote-proxy" }),
|
||||||
|
httpsOptions: tlsResolution?.httpsOptions,
|
||||||
|
})
|
||||||
const voiceModeManager = new VoiceModeManager({
|
const voiceModeManager = new VoiceModeManager({
|
||||||
connections: clientConnectionManager,
|
connections: clientConnectionManager,
|
||||||
channel: pluginChannel,
|
channel: pluginChannel,
|
||||||
@@ -422,6 +424,7 @@ async function main() {
|
|||||||
clientConnectionManager,
|
clientConnectionManager,
|
||||||
pluginChannel,
|
pluginChannel,
|
||||||
voiceModeManager,
|
voiceModeManager,
|
||||||
|
remoteProxySessionManager,
|
||||||
uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR,
|
uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR,
|
||||||
uiDevServerUrl: uiResolution.uiDevServerUrl,
|
uiDevServerUrl: uiResolution.uiDevServerUrl,
|
||||||
logger,
|
logger,
|
||||||
@@ -447,6 +450,7 @@ async function main() {
|
|||||||
clientConnectionManager,
|
clientConnectionManager,
|
||||||
pluginChannel,
|
pluginChannel,
|
||||||
voiceModeManager,
|
voiceModeManager,
|
||||||
|
remoteProxySessionManager,
|
||||||
uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR,
|
uiStaticDir: uiResolution.uiStaticDir ?? DEFAULT_UI_STATIC_DIR,
|
||||||
uiDevServerUrl: undefined,
|
uiDevServerUrl: undefined,
|
||||||
logger,
|
logger,
|
||||||
|
|||||||
248
packages/server/src/server/__tests__/remote-proxy.test.ts
Normal file
248
packages/server/src/server/__tests__/remote-proxy.test.ts
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
import assert from "node:assert/strict"
|
||||||
|
import { after, afterEach, describe, it } from "node:test"
|
||||||
|
import fs from "node:fs"
|
||||||
|
import http, { type IncomingMessage, type ServerResponse } from "node:http"
|
||||||
|
import os from "node:os"
|
||||||
|
import path from "node:path"
|
||||||
|
|
||||||
|
import { Agent, fetch } from "undici"
|
||||||
|
|
||||||
|
import type { AuthManager } from "../../auth/manager"
|
||||||
|
import type { Logger } from "../../logger"
|
||||||
|
import { RemoteProxySessionManager } from "../remote-proxy"
|
||||||
|
import { resolveHttpsOptions } from "../tls"
|
||||||
|
|
||||||
|
const sharedTempDir = fs.mkdtempSync(path.join(os.tmpdir(), "codenomad-remote-proxy-test-"))
|
||||||
|
const sharedTls = resolveHttpsOptions({
|
||||||
|
enabled: true,
|
||||||
|
configDir: sharedTempDir,
|
||||||
|
host: "127.0.0.1",
|
||||||
|
logger: createStubLogger(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!sharedTls) {
|
||||||
|
throw new Error("Failed to generate HTTPS options for remote proxy tests")
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharedHttpsOptions = sharedTls.httpsOptions
|
||||||
|
|
||||||
|
const httpsDispatcher = new Agent({ connect: { rejectUnauthorized: false } })
|
||||||
|
const managers = new Set<RemoteProxySessionManager>()
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
for (const manager of managers) {
|
||||||
|
await disposeManager(manager)
|
||||||
|
}
|
||||||
|
managers.clear()
|
||||||
|
})
|
||||||
|
|
||||||
|
after(() => {
|
||||||
|
fs.rmSync(sharedTempDir, { recursive: true, force: true })
|
||||||
|
httpsDispatcher.close().catch(() => {})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe("RemoteProxySessionManager", () => {
|
||||||
|
it("blocks proxying before activation and keeps bootstrap tokens scoped per session", async () => {
|
||||||
|
await withUpstreamServer(async (upstreamBaseUrl) => {
|
||||||
|
const manager = createSessionManager()
|
||||||
|
const session1 = await createSession(manager, `${upstreamBaseUrl}/base`)
|
||||||
|
const session2 = await createSession(manager, `${upstreamBaseUrl}/base`)
|
||||||
|
|
||||||
|
const blocked = await proxyFetch(`${session1.proxyOrigin}/status`)
|
||||||
|
assert.equal(blocked.status, 403)
|
||||||
|
|
||||||
|
const wrongTokenResponse = await proxyFetch(`${session1.proxyOrigin}/__codenomad/api/auth/token`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ token: session2.token }),
|
||||||
|
})
|
||||||
|
assert.equal(wrongTokenResponse.status, 401)
|
||||||
|
|
||||||
|
assert.equal(await activateSession(session1), true)
|
||||||
|
assert.equal(await activateSession(session2), true)
|
||||||
|
}, (req, res) => {
|
||||||
|
res.writeHead(200, { "content-type": "text/plain" })
|
||||||
|
res.end(req.url ?? "")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("preserves remote base paths and rewrites same-origin redirects to the local proxy origin", async () => {
|
||||||
|
await withUpstreamServer(async (upstreamBaseUrl) => {
|
||||||
|
const manager = createSessionManager()
|
||||||
|
const session = await createSession(manager, `${upstreamBaseUrl}/base`)
|
||||||
|
|
||||||
|
await activateSession(session)
|
||||||
|
|
||||||
|
const apiResponse = await proxyFetch(`${session.proxyOrigin}/api/auth/status?foo=bar`)
|
||||||
|
assert.equal(apiResponse.status, 200)
|
||||||
|
assert.equal(await apiResponse.text(), "/base/api/auth/status?foo=bar")
|
||||||
|
|
||||||
|
const redirectResponse = await proxyFetch(`${session.proxyOrigin}/redirect`, { redirect: "manual" })
|
||||||
|
assert.equal(redirectResponse.status, 302)
|
||||||
|
assert.equal(redirectResponse.headers.get("location"), `${session.proxyOrigin}/base/after?ok=1`)
|
||||||
|
}, (req, res) => {
|
||||||
|
const requestUrl = req.url ?? ""
|
||||||
|
if (requestUrl === "/base/redirect") {
|
||||||
|
res.writeHead(302, { location: "/base/after?ok=1" })
|
||||||
|
res.end()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(200, { "content-type": "text/plain" })
|
||||||
|
res.end(requestUrl)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("rewrites set-cookie names for the proxy and restores cookie names on proxied requests", async () => {
|
||||||
|
await withUpstreamServer(async (upstreamBaseUrl) => {
|
||||||
|
const manager = createSessionManager()
|
||||||
|
const session = await createSession(manager, `${upstreamBaseUrl}/base`)
|
||||||
|
|
||||||
|
await activateSession(session)
|
||||||
|
|
||||||
|
const loginResponse = await proxyFetch(`${session.proxyOrigin}/login`)
|
||||||
|
assert.equal(loginResponse.status, 200)
|
||||||
|
const setCookie = getSetCookie(loginResponse)[0]
|
||||||
|
|
||||||
|
assert.match(setCookie, /^cnrp_[0-9a-f]+_session=abc123/i)
|
||||||
|
assert.doesNotMatch(setCookie, /domain=/i)
|
||||||
|
|
||||||
|
const cookieHeader = setCookie.split(";", 1)[0]
|
||||||
|
const whoamiResponse = await proxyFetch(`${session.proxyOrigin}/whoami`, {
|
||||||
|
headers: { cookie: cookieHeader },
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(await whoamiResponse.text(), "session=abc123")
|
||||||
|
}, (req, res) => {
|
||||||
|
const requestUrl = req.url ?? ""
|
||||||
|
if (requestUrl === "/base/login") {
|
||||||
|
res.writeHead(200, {
|
||||||
|
"content-type": "text/plain",
|
||||||
|
"set-cookie": "session=abc123; Path=/; Secure; HttpOnly; Domain=127.0.0.1",
|
||||||
|
})
|
||||||
|
res.end("ok")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestUrl === "/base/whoami") {
|
||||||
|
res.writeHead(200, { "content-type": "text/plain" })
|
||||||
|
res.end(req.headers.cookie ?? "")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res.writeHead(404, { "content-type": "text/plain" })
|
||||||
|
res.end(requestUrl)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("supports explicit deletion and idle cleanup of sessions", async () => {
|
||||||
|
await withUpstreamServer(async (upstreamBaseUrl) => {
|
||||||
|
const manager = createSessionManager()
|
||||||
|
const session = await createSession(manager, `${upstreamBaseUrl}/base`)
|
||||||
|
|
||||||
|
assert.equal(await manager.deleteSession(session.sessionId), true)
|
||||||
|
assert.equal(await manager.deleteSession(session.sessionId), false)
|
||||||
|
|
||||||
|
const session3 = await createSession(manager, `${upstreamBaseUrl}/base`)
|
||||||
|
const internalSessions = (manager as any).sessions as Map<string, { lastAccessAt: number }>
|
||||||
|
const internalCleanup = (manager as any).cleanupExpiredSessions as () => Promise<void>
|
||||||
|
|
||||||
|
internalSessions.get(session3.sessionId)!.lastAccessAt = Date.now() - 31 * 60_000
|
||||||
|
await internalCleanup.call(manager)
|
||||||
|
|
||||||
|
assert.equal(internalSessions.has(session3.sessionId), false)
|
||||||
|
assert.equal(await manager.deleteSession(session3.sessionId), false)
|
||||||
|
}, (_req, res) => {
|
||||||
|
res.writeHead(200, { "content-type": "text/plain" })
|
||||||
|
res.end("ok")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function createSessionManager() {
|
||||||
|
const manager = new RemoteProxySessionManager({
|
||||||
|
authManager: {
|
||||||
|
isLoopbackRequest: () => true,
|
||||||
|
} as unknown as AuthManager,
|
||||||
|
logger: createStubLogger(),
|
||||||
|
httpsOptions: sharedHttpsOptions,
|
||||||
|
})
|
||||||
|
managers.add(manager)
|
||||||
|
return manager
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createSession(manager: RemoteProxySessionManager, baseUrl: string) {
|
||||||
|
const created = await manager.createSession(baseUrl, false)
|
||||||
|
const windowUrl = new URL(created.windowUrl)
|
||||||
|
return {
|
||||||
|
sessionId: created.sessionId,
|
||||||
|
windowUrl,
|
||||||
|
proxyOrigin: windowUrl.origin,
|
||||||
|
token: decodeURIComponent(windowUrl.hash.replace(/^#/, "")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function activateSession(session: { proxyOrigin: string; token: string }) {
|
||||||
|
const response = await proxyFetch(`${session.proxyOrigin}/__codenomad/api/auth/token`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "content-type": "application/json" },
|
||||||
|
body: JSON.stringify({ token: session.token }),
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
const body = (await response.json()) as { ok?: boolean }
|
||||||
|
return body.ok === true
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSetCookie(response: Awaited<ReturnType<typeof fetch>>): string[] {
|
||||||
|
const values = (response.headers as any).getSetCookie?.() as string[] | undefined
|
||||||
|
if (Array.isArray(values) && values.length > 0) {
|
||||||
|
return values
|
||||||
|
}
|
||||||
|
const fallback = response.headers.get("set-cookie")
|
||||||
|
return fallback ? [fallback] : []
|
||||||
|
}
|
||||||
|
|
||||||
|
async function proxyFetch(url: string, init?: Parameters<typeof fetch>[1]) {
|
||||||
|
return fetch(url, { dispatcher: httpsDispatcher, ...init })
|
||||||
|
}
|
||||||
|
|
||||||
|
async function disposeManager(manager: RemoteProxySessionManager) {
|
||||||
|
const sessions = Array.from(((manager as any).sessions as Map<string, unknown>).keys())
|
||||||
|
for (const sessionId of sessions) {
|
||||||
|
await manager.deleteSession(sessionId)
|
||||||
|
}
|
||||||
|
clearInterval((manager as any).cleanupTimer as NodeJS.Timeout)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function withUpstreamServer(
|
||||||
|
callback: (baseUrl: string) => Promise<void>,
|
||||||
|
handler: (req: IncomingMessage, res: ServerResponse<IncomingMessage>) => void,
|
||||||
|
) {
|
||||||
|
const server = http.createServer(handler)
|
||||||
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", () => resolve()))
|
||||||
|
|
||||||
|
try {
|
||||||
|
const address = server.address()
|
||||||
|
if (!address || typeof address === "string") {
|
||||||
|
throw new Error("Failed to resolve upstream server address")
|
||||||
|
}
|
||||||
|
await callback(`http://127.0.0.1:${address.port}`)
|
||||||
|
} finally {
|
||||||
|
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createStubLogger(): Logger {
|
||||||
|
const logger = {
|
||||||
|
info() {},
|
||||||
|
warn() {},
|
||||||
|
error() {},
|
||||||
|
child() {
|
||||||
|
return logger
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
return logger as unknown as Logger
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import { registerBackgroundProcessRoutes } from "./routes/background-processes"
|
|||||||
import { registerWorktreeRoutes } from "./routes/worktrees"
|
import { registerWorktreeRoutes } from "./routes/worktrees"
|
||||||
import { registerSpeechRoutes } from "./routes/speech"
|
import { registerSpeechRoutes } from "./routes/speech"
|
||||||
import { registerRemoteServerRoutes } from "./routes/remote-servers"
|
import { registerRemoteServerRoutes } from "./routes/remote-servers"
|
||||||
|
import { registerRemoteProxyRoutes } from "./routes/remote-proxy"
|
||||||
import { registerSideCarRoutes } from "./routes/sidecars"
|
import { registerSideCarRoutes } from "./routes/sidecars"
|
||||||
import { ServerMeta } from "../api-types"
|
import { ServerMeta } from "../api-types"
|
||||||
import { InstanceStore } from "../storage/instance-store"
|
import { InstanceStore } from "../storage/instance-store"
|
||||||
@@ -38,6 +39,7 @@ import { ClientConnectionManager } from "../clients/connection-manager"
|
|||||||
import { PluginChannelManager } from "../plugins/channel"
|
import { PluginChannelManager } from "../plugins/channel"
|
||||||
import { VoiceModeManager } from "../plugins/voice-mode"
|
import { VoiceModeManager } from "../plugins/voice-mode"
|
||||||
import type { SideCarManager } from "../sidecars/manager"
|
import type { SideCarManager } from "../sidecars/manager"
|
||||||
|
import type { RemoteProxySessionManager } from "./remote-proxy"
|
||||||
|
|
||||||
interface HttpServerDeps {
|
interface HttpServerDeps {
|
||||||
bindHost: string
|
bindHost: string
|
||||||
@@ -58,6 +60,7 @@ interface HttpServerDeps {
|
|||||||
clientConnectionManager: ClientConnectionManager
|
clientConnectionManager: ClientConnectionManager
|
||||||
pluginChannel: PluginChannelManager
|
pluginChannel: PluginChannelManager
|
||||||
voiceModeManager: VoiceModeManager
|
voiceModeManager: VoiceModeManager
|
||||||
|
remoteProxySessionManager: RemoteProxySessionManager
|
||||||
uiStaticDir: string
|
uiStaticDir: string
|
||||||
uiDevServerUrl?: string
|
uiDevServerUrl?: string
|
||||||
logger: Logger
|
logger: Logger
|
||||||
@@ -199,7 +202,12 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
publicPagePaths.add("/auth/token")
|
publicPagePaths.add("/auth/token")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (publicApiPaths.has(pathname) || publicPagePaths.has(pathname)) {
|
const isLoopbackRemoteProxyDelete =
|
||||||
|
request.method === "DELETE" &&
|
||||||
|
pathname.startsWith("/api/remote-proxy/sessions/") &&
|
||||||
|
deps.authManager.isLoopbackRequest(request)
|
||||||
|
|
||||||
|
if (publicApiPaths.has(pathname) || publicPagePaths.has(pathname) || isLoopbackRemoteProxyDelete) {
|
||||||
done()
|
done()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -274,6 +282,7 @@ export function createHttpServer(deps: HttpServerDeps) {
|
|||||||
workspaceManager: deps.workspaceManager,
|
workspaceManager: deps.workspaceManager,
|
||||||
})
|
})
|
||||||
registerRemoteServerRoutes(app, { logger: apiLogger })
|
registerRemoteServerRoutes(app, { logger: apiLogger })
|
||||||
|
registerRemoteProxyRoutes(app, { logger: proxyLogger, sessionManager: deps.remoteProxySessionManager })
|
||||||
registerSpeechRoutes(app, { speechService: deps.speechService })
|
registerSpeechRoutes(app, { speechService: deps.speechService })
|
||||||
registerSideCarRoutes(app, { sidecarManager: deps.sidecarManager })
|
registerSideCarRoutes(app, { sidecarManager: deps.sidecarManager })
|
||||||
registerSideCarProxyRoutes(app, { sidecarManager: deps.sidecarManager, logger: proxyLogger })
|
registerSideCarProxyRoutes(app, { sidecarManager: deps.sidecarManager, logger: proxyLogger })
|
||||||
|
|||||||
566
packages/server/src/server/remote-proxy.ts
Normal file
566
packages/server/src/server/remote-proxy.ts
Normal file
@@ -0,0 +1,566 @@
|
|||||||
|
import Fastify, { type FastifyInstance, type FastifyReply, type FastifyRequest } from "fastify"
|
||||||
|
import { randomBytes, randomUUID } from "crypto"
|
||||||
|
import { Readable } from "stream"
|
||||||
|
import { pipeline } from "stream/promises"
|
||||||
|
import { Agent, fetch } from "undici"
|
||||||
|
import type { AuthManager } from "../auth/manager"
|
||||||
|
import type { Logger } from "../logger"
|
||||||
|
|
||||||
|
const LOOPBACK_HOST = "127.0.0.1"
|
||||||
|
const BOOTSTRAP_PAGE_PATH = "/__codenomad/auth/token"
|
||||||
|
const BOOTSTRAP_EXCHANGE_PATH = "/__codenomad/api/auth/token"
|
||||||
|
const SESSION_IDLE_TTL_MS = 30 * 60_000
|
||||||
|
|
||||||
|
interface RemoteProxySession {
|
||||||
|
id: string
|
||||||
|
bootstrapToken: string
|
||||||
|
targetBaseUrl: URL
|
||||||
|
skipTlsVerify: boolean
|
||||||
|
localBaseUrl: URL
|
||||||
|
entryUrl: URL
|
||||||
|
bootstrapUrl: string
|
||||||
|
activated: boolean
|
||||||
|
cookiePrefix: string
|
||||||
|
app: FastifyInstance
|
||||||
|
dispatcher?: Agent
|
||||||
|
createdAt: number
|
||||||
|
lastAccessAt: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoteProxySessionManagerOptions {
|
||||||
|
authManager: AuthManager
|
||||||
|
logger: Logger
|
||||||
|
httpsOptions?: { key: string | Buffer; cert: string | Buffer; ca?: string | Buffer }
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RemoteProxySessionCreateResult {
|
||||||
|
sessionId: string
|
||||||
|
windowUrl: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RemoteProxySessionManager {
|
||||||
|
private readonly sessions = new Map<string, RemoteProxySession>()
|
||||||
|
private readonly cleanupTimer: NodeJS.Timeout
|
||||||
|
|
||||||
|
constructor(private readonly options: RemoteProxySessionManagerOptions) {
|
||||||
|
this.cleanupTimer = setInterval(() => {
|
||||||
|
void this.cleanupExpiredSessions()
|
||||||
|
}, 60_000)
|
||||||
|
this.cleanupTimer.unref()
|
||||||
|
}
|
||||||
|
|
||||||
|
async createSession(baseUrl: string, skipTlsVerify: boolean): Promise<RemoteProxySessionCreateResult> {
|
||||||
|
if (!this.options.httpsOptions) {
|
||||||
|
throw new Error("Local HTTPS is required for remote proxy sessions")
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetBaseUrl = normalizeBaseUrl(baseUrl)
|
||||||
|
const sessionId = randomUUID()
|
||||||
|
const bootstrapToken = randomBytes(32).toString("base64url")
|
||||||
|
const dispatcher = skipTlsVerify ? new Agent({ connect: { rejectUnauthorized: false } }) : undefined
|
||||||
|
const app = Fastify({ logger: false, https: this.options.httpsOptions })
|
||||||
|
let session: RemoteProxySession | null = null
|
||||||
|
|
||||||
|
app.removeAllContentTypeParsers()
|
||||||
|
// Preserve raw request bodies for proxying while still letting token JSON parse from Buffer.
|
||||||
|
app.addContentTypeParser("*", { parseAs: "buffer" }, (_req, body, done) => done(null, body))
|
||||||
|
|
||||||
|
app.get(BOOTSTRAP_PAGE_PATH, async (request, reply) => {
|
||||||
|
if (!this.options.authManager.isLoopbackRequest(request)) {
|
||||||
|
reply.code(404).send({ error: "Not found" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.header("Cache-Control", "no-store")
|
||||||
|
reply.header("Pragma", "no-cache")
|
||||||
|
reply.header("Expires", "0")
|
||||||
|
reply.type("text/html").send(buildBootstrapPageHtml())
|
||||||
|
})
|
||||||
|
|
||||||
|
app.post(BOOTSTRAP_EXCHANGE_PATH, async (request, reply) => {
|
||||||
|
if (!this.options.authManager.isLoopbackRequest(request)) {
|
||||||
|
reply.code(404).send({ error: "Not found" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session) {
|
||||||
|
reply.code(503).send({ error: "Remote proxy session is unavailable" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = parseTokenBody(request.body)
|
||||||
|
if (body.token !== session.bootstrapToken) {
|
||||||
|
reply.code(401).send({ error: "Invalid token" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.activated = true
|
||||||
|
session.lastAccessAt = Date.now()
|
||||||
|
reply.send({ ok: true })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.all("/*", async (request, reply) => {
|
||||||
|
if (!session) {
|
||||||
|
reply.code(503).send({ error: "Remote proxy session is unavailable" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session.activated) {
|
||||||
|
reply.code(403).send({ error: "Remote proxy session is not activated" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.lastAccessAt = Date.now()
|
||||||
|
await proxyRequest({ request, reply, session, logger: this.options.logger })
|
||||||
|
})
|
||||||
|
|
||||||
|
app.setNotFoundHandler(async (request, reply) => {
|
||||||
|
if (!session) {
|
||||||
|
reply.code(503).send({ error: "Remote proxy session is unavailable" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!session.activated) {
|
||||||
|
reply.code(403).send({ error: "Remote proxy session is not activated" })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
session.lastAccessAt = Date.now()
|
||||||
|
await proxyRequest({ request, reply, session, logger: this.options.logger })
|
||||||
|
})
|
||||||
|
|
||||||
|
const addressInfo = await app.listen({ host: LOOPBACK_HOST, port: 0 })
|
||||||
|
const address = new URL(addressInfo)
|
||||||
|
const localBaseUrl = new URL(`https://${LOOPBACK_HOST}:${address.port}`)
|
||||||
|
const entryUrl = new URL(targetBaseUrl.pathname || "/", localBaseUrl)
|
||||||
|
const returnTo = buildReturnToTarget(entryUrl)
|
||||||
|
|
||||||
|
session = {
|
||||||
|
id: sessionId,
|
||||||
|
bootstrapToken,
|
||||||
|
targetBaseUrl,
|
||||||
|
skipTlsVerify,
|
||||||
|
localBaseUrl,
|
||||||
|
entryUrl,
|
||||||
|
bootstrapUrl: `${localBaseUrl.origin}${BOOTSTRAP_PAGE_PATH}?returnTo=${encodeURIComponent(returnTo)}#${encodeURIComponent(bootstrapToken)}`,
|
||||||
|
activated: false,
|
||||||
|
cookiePrefix: `cnrp_${randomBytes(6).toString("hex")}_`,
|
||||||
|
app,
|
||||||
|
dispatcher,
|
||||||
|
createdAt: Date.now(),
|
||||||
|
lastAccessAt: Date.now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sessions.set(sessionId, session)
|
||||||
|
this.options.logger.info(
|
||||||
|
{ sessionId, targetBaseUrl: targetBaseUrl.toString(), localBaseUrl: localBaseUrl.toString() },
|
||||||
|
"Created remote proxy session",
|
||||||
|
)
|
||||||
|
|
||||||
|
return { sessionId, windowUrl: session.bootstrapUrl }
|
||||||
|
}
|
||||||
|
|
||||||
|
async deleteSession(sessionId: string): Promise<boolean> {
|
||||||
|
return this.disposeSession(sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private async cleanupExpiredSessions() {
|
||||||
|
const now = Date.now()
|
||||||
|
for (const session of Array.from(this.sessions.values())) {
|
||||||
|
if (now - session.lastAccessAt <= SESSION_IDLE_TTL_MS) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
await this.disposeSession(session.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async disposeSession(sessionId: string): Promise<boolean> {
|
||||||
|
const session = this.sessions.get(sessionId)
|
||||||
|
if (!session) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
this.sessions.delete(sessionId)
|
||||||
|
session.dispatcher?.close().catch(() => {})
|
||||||
|
await session.app.close().catch(() => {})
|
||||||
|
this.options.logger.info({ sessionId }, "Disposed remote proxy session")
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeBaseUrl(input: string): URL {
|
||||||
|
const parsed = new URL(input.trim())
|
||||||
|
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
|
||||||
|
throw new Error("Server URL must use http:// or https://")
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed.hash = ""
|
||||||
|
parsed.search = ""
|
||||||
|
parsed.pathname = parsed.pathname === "/" ? "/" : parsed.pathname.replace(/\/+$/, "") || "/"
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildReturnToTarget(entryUrl: URL): string {
|
||||||
|
const query = entryUrl.search ? entryUrl.search : ""
|
||||||
|
return `${entryUrl.pathname || "/"}${query}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildBootstrapPageHtml(): string {
|
||||||
|
return `<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>CodeNomad</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; background: #0b0b0f; color: #fff; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; }
|
||||||
|
.card { width: 420px; max-width: calc(100vw - 32px); background: #14141c; border: 1px solid rgba(255,255,255,0.08); border-radius: 14px; padding: 24px; }
|
||||||
|
h1 { font-size: 18px; margin: 0 0 12px; }
|
||||||
|
p { margin: 0; color: rgba(255,255,255,0.7); font-size: 13px; line-height: 1.4; }
|
||||||
|
.error { margin-top: 12px; color: #ff6b6b; font-size: 13px; display: none; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<h1>Connecting...</h1>
|
||||||
|
<p>Finalizing local authentication.</p>
|
||||||
|
<div id="error" class="error"></div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
const token = decodeURIComponent((location.hash || "").replace(/^#/, "").trim())
|
||||||
|
const params = new URLSearchParams(location.search)
|
||||||
|
const returnTo = sanitizeReturnTo(params.get("returnTo"))
|
||||||
|
const errorEl = document.getElementById("error")
|
||||||
|
|
||||||
|
function sanitizeReturnTo(value) {
|
||||||
|
if (!value || typeof value !== "string") return "/"
|
||||||
|
if (!value.startsWith("/")) return "/"
|
||||||
|
if (value.startsWith("//")) return "/"
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
|
||||||
|
function showError(message) {
|
||||||
|
errorEl.textContent = message
|
||||||
|
errorEl.style.display = "block"
|
||||||
|
}
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
if (!token) {
|
||||||
|
showError("Missing bootstrap token.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch("${BOOTSTRAP_EXCHANGE_PATH}", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ token }),
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
let message = ""
|
||||||
|
try {
|
||||||
|
const json = await res.json()
|
||||||
|
message = json && json.error ? String(json.error) : ""
|
||||||
|
} catch {
|
||||||
|
message = ""
|
||||||
|
}
|
||||||
|
showError(message || "Token exchange failed (" + res.status + ")")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
window.location.replace(returnTo)
|
||||||
|
} catch (error) {
|
||||||
|
showError(error && error.message ? error.message : String(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
run()
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTokenBody(body: unknown): { token: string } {
|
||||||
|
const value = normalizeJsonBody(body) as { token?: unknown } | null | undefined
|
||||||
|
const token = typeof value?.token === "string" ? value.token.trim() : ""
|
||||||
|
if (!token) {
|
||||||
|
throw new Error("Missing bootstrap token")
|
||||||
|
}
|
||||||
|
return { token }
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeJsonBody(body: unknown): unknown {
|
||||||
|
if (Buffer.isBuffer(body)) {
|
||||||
|
return JSON.parse(body.toString("utf-8"))
|
||||||
|
}
|
||||||
|
if (typeof body === "string") {
|
||||||
|
return JSON.parse(body)
|
||||||
|
}
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
|
||||||
|
function toRequestBody(body: unknown): any {
|
||||||
|
if (body == null) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
if (Buffer.isBuffer(body) || typeof body === "string" || body instanceof Uint8Array) {
|
||||||
|
return body
|
||||||
|
}
|
||||||
|
return JSON.stringify(body)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function proxyRequest(args: {
|
||||||
|
request: FastifyRequest
|
||||||
|
reply: FastifyReply
|
||||||
|
session: RemoteProxySession
|
||||||
|
logger: Logger
|
||||||
|
}) {
|
||||||
|
const { request, reply, session, logger } = args
|
||||||
|
const upstreamUrl = buildUpstreamUrl(session.targetBaseUrl, request.raw.url ?? request.url)
|
||||||
|
const headers = filterRequestHeaders(request.headers, session)
|
||||||
|
|
||||||
|
const init: any = {
|
||||||
|
method: request.method,
|
||||||
|
headers,
|
||||||
|
dispatcher: session.dispatcher,
|
||||||
|
redirect: "manual",
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
||||||
|
const body = toRequestBody(request.body)
|
||||||
|
if (body !== undefined) {
|
||||||
|
init.body = body
|
||||||
|
init.duplex = "half"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(upstreamUrl, init as any)
|
||||||
|
reply.code(response.status)
|
||||||
|
applyResponseHeaders(reply, response, session)
|
||||||
|
|
||||||
|
if (!response.body || request.method === "HEAD") {
|
||||||
|
reply.send()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.hijack()
|
||||||
|
reply.raw.writeHead(reply.statusCode, toOutgoingHeaders(reply.getHeaders()))
|
||||||
|
await pipeline(Readable.fromWeb(response.body as any), reply.raw)
|
||||||
|
} catch (error) {
|
||||||
|
logger.error({ err: error, upstreamUrl }, "Failed to proxy remote session request")
|
||||||
|
if (!reply.sent) {
|
||||||
|
reply.code(502).send({ error: "Remote proxy request failed" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildUpstreamUrl(baseUrl: URL, rawUrl: string): string {
|
||||||
|
const parsed = new URL(rawUrl, "https://localhost")
|
||||||
|
const url = new URL(baseUrl.toString())
|
||||||
|
url.pathname = rewriteRequestPath(baseUrl, parsed.pathname)
|
||||||
|
url.search = stripInternalQuery(parsed.search)
|
||||||
|
url.hash = ""
|
||||||
|
return url.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteRequestPath(baseUrl: URL, requestPath: string): string {
|
||||||
|
const basePath = normalizedBasePath(baseUrl)
|
||||||
|
if (basePath === "/") {
|
||||||
|
return requestPath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (requestPath === "/") {
|
||||||
|
return basePath
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathHasBasePrefix(basePath, requestPath)) {
|
||||||
|
return requestPath
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${basePath}${requestPath}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizedBasePath(baseUrl: URL): string {
|
||||||
|
return baseUrl.pathname || "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
function pathHasBasePrefix(basePath: string, requestPath: string): boolean {
|
||||||
|
return requestPath === basePath || requestPath.startsWith(`${basePath}/`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function stripInternalQuery(search: string): string {
|
||||||
|
if (!search || search === "?") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return search
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterRequestHeaders(
|
||||||
|
headers: FastifyRequest["headers"],
|
||||||
|
session: RemoteProxySession,
|
||||||
|
): Record<string, string> {
|
||||||
|
const next: Record<string, string> = {}
|
||||||
|
for (const [key, value] of Object.entries(headers ?? {})) {
|
||||||
|
if (!value) continue
|
||||||
|
const lower = key.toLowerCase()
|
||||||
|
if (
|
||||||
|
isHopByHopHeader(lower) ||
|
||||||
|
lower === "host" ||
|
||||||
|
lower === "content-length" ||
|
||||||
|
lower === "accept-encoding"
|
||||||
|
) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (lower === "origin") {
|
||||||
|
next[key] = session.targetBaseUrl.origin
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (lower === "referer") {
|
||||||
|
const rewritten = rewriteRefererHeader(Array.isArray(value) ? value[0] : value, session.targetBaseUrl)
|
||||||
|
if (rewritten) {
|
||||||
|
next[key] = rewritten
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if (lower === "cookie") {
|
||||||
|
const rewritten = rewriteRequestCookieHeader(Array.isArray(value) ? value.join("; ") : value, session.cookiePrefix)
|
||||||
|
if (rewritten) {
|
||||||
|
next[key] = rewritten
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next[key] = Array.isArray(value) ? value.join(",") : value
|
||||||
|
}
|
||||||
|
|
||||||
|
next.host = session.targetBaseUrl.port ? `${session.targetBaseUrl.hostname}:${session.targetBaseUrl.port}` : session.targetBaseUrl.hostname
|
||||||
|
if (!next.origin) {
|
||||||
|
next.origin = session.targetBaseUrl.origin
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteRefererHeader(referer: string | undefined, targetBaseUrl: URL): string | null {
|
||||||
|
if (!referer) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const parsed = new URL(referer)
|
||||||
|
const rewritten = new URL(targetBaseUrl.toString())
|
||||||
|
rewritten.pathname = rewriteRequestPath(targetBaseUrl, parsed.pathname)
|
||||||
|
rewritten.search = parsed.search
|
||||||
|
rewritten.hash = parsed.hash
|
||||||
|
return rewritten.toString()
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyResponseHeaders(reply: FastifyReply, response: any, session: RemoteProxySession) {
|
||||||
|
const setCookie = (response.headers as any).getSetCookie?.() as string[] | undefined
|
||||||
|
if (Array.isArray(setCookie)) {
|
||||||
|
for (const cookie of setCookie) {
|
||||||
|
reply.header("set-cookie", rewriteSetCookie(cookie, session.cookiePrefix))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
response.headers.forEach((value: string, key: string) => {
|
||||||
|
const lower = key.toLowerCase()
|
||||||
|
if (
|
||||||
|
isHopByHopHeader(lower) ||
|
||||||
|
lower === "set-cookie" ||
|
||||||
|
lower === "content-length" ||
|
||||||
|
lower === "content-encoding"
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lower === "location") {
|
||||||
|
reply.header(key, rewriteLocation(value, session.targetBaseUrl, session.localBaseUrl))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
reply.header(key, value)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function toOutgoingHeaders(headers: ReturnType<FastifyReply["getHeaders"]>): Record<string, string | string[]> {
|
||||||
|
const next: Record<string, string | string[]> = {}
|
||||||
|
for (const [key, value] of Object.entries(headers)) {
|
||||||
|
if (value === undefined) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next[key] = Array.isArray(value) ? value.map(String) : String(value)
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteSetCookie(cookie: string, cookiePrefix: string): string {
|
||||||
|
const parts = cookie.split(";").map((part) => part.trim())
|
||||||
|
const first = parts.shift() ?? ""
|
||||||
|
const separator = first.indexOf("=")
|
||||||
|
if (separator <= 0) {
|
||||||
|
return cookie
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = first.slice(0, separator).trim()
|
||||||
|
const value = first.slice(separator + 1)
|
||||||
|
const rewritten = [`${cookiePrefix}${name}=${value}`]
|
||||||
|
for (const part of parts) {
|
||||||
|
if (part.slice(0, 7).toLowerCase().startsWith("domain=")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rewritten.push(part)
|
||||||
|
}
|
||||||
|
return rewritten.join("; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteRequestCookieHeader(cookieHeader: string, cookiePrefix: string): string {
|
||||||
|
const next: string[] = []
|
||||||
|
for (const rawPart of cookieHeader.split(";")) {
|
||||||
|
const part = rawPart.trim()
|
||||||
|
if (!part) continue
|
||||||
|
const separator = part.indexOf("=")
|
||||||
|
if (separator <= 0) continue
|
||||||
|
const name = part.slice(0, separator).trim()
|
||||||
|
const value = part.slice(separator + 1)
|
||||||
|
if (!name.startsWith(cookiePrefix)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
next.push(`${name.slice(cookiePrefix.length)}=${value}`)
|
||||||
|
}
|
||||||
|
return next.join("; ")
|
||||||
|
}
|
||||||
|
|
||||||
|
function rewriteLocation(location: string, targetBaseUrl: URL, localBaseUrl: URL): string {
|
||||||
|
try {
|
||||||
|
const parsed = new URL(location, targetBaseUrl)
|
||||||
|
if (parsed.origin !== targetBaseUrl.origin) {
|
||||||
|
return location
|
||||||
|
}
|
||||||
|
|
||||||
|
const rewritten = new URL(localBaseUrl.toString())
|
||||||
|
rewritten.pathname = parsed.pathname
|
||||||
|
rewritten.search = parsed.search
|
||||||
|
rewritten.hash = parsed.hash
|
||||||
|
return rewritten.toString()
|
||||||
|
} catch {
|
||||||
|
return location
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isHopByHopHeader(name: string): boolean {
|
||||||
|
return new Set([
|
||||||
|
"connection",
|
||||||
|
"keep-alive",
|
||||||
|
"proxy-authenticate",
|
||||||
|
"proxy-authorization",
|
||||||
|
"te",
|
||||||
|
"trailer",
|
||||||
|
"transfer-encoding",
|
||||||
|
"upgrade",
|
||||||
|
]).has(name)
|
||||||
|
}
|
||||||
54
packages/server/src/server/routes/remote-proxy.ts
Normal file
54
packages/server/src/server/routes/remote-proxy.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { FastifyInstance } from "fastify"
|
||||||
|
import { z } from "zod"
|
||||||
|
import type { RemoteProxySessionCreateResponse } from "../../api-types"
|
||||||
|
import { isLoopbackAddress } from "../../auth/http-auth"
|
||||||
|
import type { Logger } from "../../logger"
|
||||||
|
import type { RemoteProxySessionManager } from "../remote-proxy"
|
||||||
|
|
||||||
|
interface RouteDeps {
|
||||||
|
logger: Logger
|
||||||
|
sessionManager: RemoteProxySessionManager
|
||||||
|
}
|
||||||
|
|
||||||
|
const CreateSessionSchema = z.object({
|
||||||
|
baseUrl: z.string().min(1),
|
||||||
|
skipTlsVerify: z.boolean().optional(),
|
||||||
|
})
|
||||||
|
|
||||||
|
const SessionParamsSchema = z.object({
|
||||||
|
id: z.string().uuid(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export function registerRemoteProxyRoutes(app: FastifyInstance, deps: RouteDeps) {
|
||||||
|
app.post("/api/remote-proxy/sessions", async (request, reply): Promise<RemoteProxySessionCreateResponse | { error: string }> => {
|
||||||
|
try {
|
||||||
|
const body = CreateSessionSchema.parse(request.body ?? {})
|
||||||
|
return await deps.sessionManager.createSession(body.baseUrl, Boolean(body.skipTlsVerify))
|
||||||
|
} catch (error) {
|
||||||
|
deps.logger.warn({ err: error }, "Failed to create remote proxy session")
|
||||||
|
reply.code(400)
|
||||||
|
return { error: error instanceof Error ? error.message : "Failed to create remote proxy session" }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.delete("/api/remote-proxy/sessions/:id", async (request, reply): Promise<{ ok: boolean } | { error: string }> => {
|
||||||
|
if (!isLoopbackAddress(request.socket.remoteAddress)) {
|
||||||
|
reply.code(404)
|
||||||
|
return { error: "Not found" }
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const params = SessionParamsSchema.parse(request.params ?? {})
|
||||||
|
const deleted = await deps.sessionManager.deleteSession(params.id)
|
||||||
|
if (!deleted) {
|
||||||
|
reply.code(404)
|
||||||
|
return { error: "Remote proxy session not found" }
|
||||||
|
}
|
||||||
|
return { ok: true }
|
||||||
|
} catch (error) {
|
||||||
|
deps.logger.warn({ err: error }, "Failed to delete remote proxy session")
|
||||||
|
reply.code(400)
|
||||||
|
return { error: error instanceof Error ? error.message : "Failed to delete remote proxy session" }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
368
packages/tauri-app/Cargo.lock
generated
368
packages/tauri-app/Cargo.lock
generated
@@ -213,6 +213,28 @@ version = "1.5.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aws-lc-rs"
|
||||||
|
version = "1.16.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc"
|
||||||
|
dependencies = [
|
||||||
|
"aws-lc-sys",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "aws-lc-sys"
|
||||||
|
version = "0.39.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cmake",
|
||||||
|
"dunce",
|
||||||
|
"fs_extra",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.21.7"
|
version = "0.21.7"
|
||||||
@@ -408,6 +430,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"find-msvc-tools",
|
"find-msvc-tools",
|
||||||
|
"jobserver",
|
||||||
|
"libc",
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -444,6 +468,12 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg_aliases"
|
||||||
|
version = "0.2.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "chrono"
|
name = "chrono"
|
||||||
version = "0.4.44"
|
version = "0.4.44"
|
||||||
@@ -456,17 +486,28 @@ dependencies = [
|
|||||||
"windows-link 0.2.1",
|
"windows-link 0.2.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cmake"
|
||||||
|
version = "0.1.58"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codenomad-tauri"
|
name = "codenomad-tauri"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"base64 0.22.1",
|
||||||
"dirs 5.0.1",
|
"dirs 5.0.1",
|
||||||
"keepawake",
|
"keepawake",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"regex",
|
"regex",
|
||||||
|
"reqwest 0.12.28",
|
||||||
|
"rustls",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_yaml",
|
"serde_yaml",
|
||||||
@@ -476,8 +517,8 @@ dependencies = [
|
|||||||
"tauri-plugin-global-shortcut",
|
"tauri-plugin-global-shortcut",
|
||||||
"tauri-plugin-notification",
|
"tauri-plugin-notification",
|
||||||
"tauri-plugin-opener",
|
"tauri-plugin-opener",
|
||||||
"thiserror 1.0.69",
|
|
||||||
"url",
|
"url",
|
||||||
|
"webkit2gtk",
|
||||||
"which",
|
"which",
|
||||||
"windows-sys 0.59.0",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
@@ -969,6 +1010,15 @@ version = "1.2.2"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
checksum = "4ef6b89e5b37196644d8796de5268852ff179b44e96276cf4290264843743bb7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "encoding_rs"
|
||||||
|
version = "0.8.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "endi"
|
name = "endi"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
@@ -1139,6 +1189,12 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fs_extra"
|
||||||
|
version = "1.3.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futf"
|
name = "futf"
|
||||||
version = "0.1.5"
|
version = "0.1.5"
|
||||||
@@ -1379,8 +1435,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi 0.11.1+wasi-snapshot-preview1",
|
"wasi 0.11.1+wasi-snapshot-preview1",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1390,9 +1448,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"r-efi 5.3.0",
|
"r-efi 5.3.0",
|
||||||
"wasip2",
|
"wasip2",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -1574,6 +1634,25 @@ dependencies = [
|
|||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h2"
|
||||||
|
version = "0.4.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54"
|
||||||
|
dependencies = [
|
||||||
|
"atomic-waker",
|
||||||
|
"bytes",
|
||||||
|
"fnv",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"http",
|
||||||
|
"indexmap 2.13.0",
|
||||||
|
"slab",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
|
"tracing",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.12.3"
|
version = "0.12.3"
|
||||||
@@ -1699,6 +1778,7 @@ dependencies = [
|
|||||||
"bytes",
|
"bytes",
|
||||||
"futures-channel",
|
"futures-channel",
|
||||||
"futures-core",
|
"futures-core",
|
||||||
|
"h2",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"httparse",
|
"httparse",
|
||||||
@@ -1710,6 +1790,23 @@ dependencies = [
|
|||||||
"want",
|
"want",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hyper-rustls"
|
||||||
|
version = "0.27.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
|
||||||
|
dependencies = [
|
||||||
|
"http",
|
||||||
|
"hyper",
|
||||||
|
"hyper-util",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tower-service",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hyper-util"
|
name = "hyper-util"
|
||||||
version = "0.1.20"
|
version = "0.1.20"
|
||||||
@@ -1999,6 +2096,16 @@ version = "0.3.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jobserver"
|
||||||
|
version = "0.1.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
|
||||||
|
dependencies = [
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"libc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.91"
|
version = "0.3.91"
|
||||||
@@ -2157,6 +2264,12 @@ version = "0.4.29"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lru-slab"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mac"
|
name = "mac"
|
||||||
version = "0.1.1"
|
version = "0.1.1"
|
||||||
@@ -2995,6 +3108,61 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn"
|
||||||
|
version = "0.11.9"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"cfg_aliases",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn-proto",
|
||||||
|
"quinn-udp",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"socket2",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tokio",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-proto"
|
||||||
|
version = "0.11.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"getrandom 0.3.4",
|
||||||
|
"lru-slab",
|
||||||
|
"rand 0.9.2",
|
||||||
|
"ring",
|
||||||
|
"rustc-hash",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"slab",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"tinyvec",
|
||||||
|
"tracing",
|
||||||
|
"web-time",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quinn-udp"
|
||||||
|
version = "0.5.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg_aliases",
|
||||||
|
"libc",
|
||||||
|
"once_cell",
|
||||||
|
"socket2",
|
||||||
|
"tracing",
|
||||||
|
"windows-sys 0.60.2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "quote"
|
name = "quote"
|
||||||
version = "1.0.45"
|
version = "1.0.45"
|
||||||
@@ -3212,6 +3380,50 @@ version = "0.8.10"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "reqwest"
|
||||||
|
version = "0.12.28"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"bytes",
|
||||||
|
"encoding_rs",
|
||||||
|
"futures-core",
|
||||||
|
"futures-util",
|
||||||
|
"h2",
|
||||||
|
"http",
|
||||||
|
"http-body",
|
||||||
|
"http-body-util",
|
||||||
|
"hyper",
|
||||||
|
"hyper-rustls",
|
||||||
|
"hyper-util",
|
||||||
|
"js-sys",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"percent-encoding",
|
||||||
|
"pin-project-lite",
|
||||||
|
"quinn",
|
||||||
|
"rustls",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_urlencoded",
|
||||||
|
"sync_wrapper",
|
||||||
|
"tokio",
|
||||||
|
"tokio-rustls",
|
||||||
|
"tokio-util",
|
||||||
|
"tower",
|
||||||
|
"tower-http",
|
||||||
|
"tower-service",
|
||||||
|
"url",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"wasm-streams 0.4.2",
|
||||||
|
"web-sys",
|
||||||
|
"webpki-roots",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "reqwest"
|
name = "reqwest"
|
||||||
version = "0.13.2"
|
version = "0.13.2"
|
||||||
@@ -3242,7 +3454,7 @@ dependencies = [
|
|||||||
"url",
|
"url",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"wasm-bindgen-futures",
|
"wasm-bindgen-futures",
|
||||||
"wasm-streams",
|
"wasm-streams 0.5.0",
|
||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -3270,6 +3482,20 @@ dependencies = [
|
|||||||
"windows-sys 0.60.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustc-hash"
|
name = "rustc-hash"
|
||||||
version = "2.1.1"
|
version = "2.1.1"
|
||||||
@@ -3311,6 +3537,44 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls"
|
||||||
|
version = "0.23.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4"
|
||||||
|
dependencies = [
|
||||||
|
"aws-lc-rs",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"rustls-webpki",
|
||||||
|
"subtle",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-pki-types"
|
||||||
|
version = "1.14.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd"
|
||||||
|
dependencies = [
|
||||||
|
"web-time",
|
||||||
|
"zeroize",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustls-webpki"
|
||||||
|
version = "0.103.10"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef"
|
||||||
|
dependencies = [
|
||||||
|
"aws-lc-rs",
|
||||||
|
"ring",
|
||||||
|
"rustls-pki-types",
|
||||||
|
"untrusted",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustversion"
|
name = "rustversion"
|
||||||
version = "1.0.22"
|
version = "1.0.22"
|
||||||
@@ -3531,6 +3795,18 @@ dependencies = [
|
|||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_urlencoded"
|
||||||
|
version = "0.7.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
|
||||||
|
dependencies = [
|
||||||
|
"form_urlencoded",
|
||||||
|
"itoa",
|
||||||
|
"ryu",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_with"
|
name = "serde_with"
|
||||||
version = "3.18.0"
|
version = "3.18.0"
|
||||||
@@ -3792,6 +4068,12 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.6.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "swift-rs"
|
name = "swift-rs"
|
||||||
version = "1.0.7"
|
version = "1.0.7"
|
||||||
@@ -3943,7 +4225,7 @@ dependencies = [
|
|||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"plist",
|
"plist",
|
||||||
"raw-window-handle",
|
"raw-window-handle",
|
||||||
"reqwest",
|
"reqwest 0.13.2",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"serde_repr",
|
"serde_repr",
|
||||||
@@ -4367,6 +4649,21 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec"
|
||||||
|
version = "1.11.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
|
||||||
|
dependencies = [
|
||||||
|
"tinyvec_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tinyvec_macros"
|
||||||
|
version = "0.1.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.50.0"
|
version = "1.50.0"
|
||||||
@@ -4381,6 +4678,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-rustls"
|
||||||
|
version = "0.26.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
|
||||||
|
dependencies = [
|
||||||
|
"rustls",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-util"
|
name = "tokio-util"
|
||||||
version = "0.7.18"
|
version = "0.7.18"
|
||||||
@@ -4691,6 +4998,12 @@ version = "0.2.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
@@ -4902,6 +5215,19 @@ dependencies = [
|
|||||||
"wasmparser",
|
"wasmparser",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-streams"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
|
||||||
|
dependencies = [
|
||||||
|
"futures-util",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "wasm-streams"
|
name = "wasm-streams"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -4937,6 +5263,16 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "web-time"
|
||||||
|
version = "1.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "web_atoms"
|
name = "web_atoms"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
@@ -4993,6 +5329,15 @@ dependencies = [
|
|||||||
"system-deps",
|
"system-deps",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "webpki-roots"
|
||||||
|
version = "1.0.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "22cfaf3c063993ff62e73cb4311efde4db1efb31ab78a3e5c457939ad5cc0bed"
|
||||||
|
dependencies = [
|
||||||
|
"rustls-pki-types",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webview2-com"
|
name = "webview2-com"
|
||||||
version = "0.38.2"
|
version = "0.38.2"
|
||||||
@@ -5286,6 +5631,15 @@ dependencies = [
|
|||||||
"windows-targets 0.48.5",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-sys"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets 0.52.6",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "windows-sys"
|
name = "windows-sys"
|
||||||
version = "0.59.0"
|
version = "0.59.0"
|
||||||
@@ -5927,6 +6281,12 @@ dependencies = [
|
|||||||
"synstructure",
|
"synstructure",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zeroize"
|
||||||
|
version = "1.8.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zerotrie"
|
name = "zerotrie"
|
||||||
version = "0.2.3"
|
version = "0.2.3"
|
||||||
|
|||||||
@@ -37,6 +37,12 @@ const braceExpansionPath = path.join(
|
|||||||
"package.json",
|
"package.json",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const serverBuildDependencyPaths = [
|
||||||
|
path.join(serverRoot, "node_modules", "typescript", "package.json"),
|
||||||
|
path.join(serverRoot, "node_modules", "@types", "node-forge", "package.json"),
|
||||||
|
path.join(serverRoot, "node_modules", "@types", "yauzl", "package.json"),
|
||||||
|
]
|
||||||
|
|
||||||
const viteBinPath = path.join(uiRoot, "node_modules", ".bin", "vite")
|
const viteBinPath = path.join(uiRoot, "node_modules", ".bin", "vite")
|
||||||
|
|
||||||
async function ensureMonacoAssets() {
|
async function ensureMonacoAssets() {
|
||||||
@@ -98,7 +104,7 @@ function syncServerUiBundle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureServerDevDependencies() {
|
function ensureServerDevDependencies() {
|
||||||
if (fs.existsSync(braceExpansionPath)) {
|
if (serverBuildDependencyPaths.every((filePath) => fs.existsSync(filePath))) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +148,7 @@ function ensureRollupPlatformBinary() {
|
|||||||
"linux-arm64": "@rollup/rollup-linux-arm64-gnu",
|
"linux-arm64": "@rollup/rollup-linux-arm64-gnu",
|
||||||
"darwin-arm64": "@rollup/rollup-darwin-arm64",
|
"darwin-arm64": "@rollup/rollup-darwin-arm64",
|
||||||
"darwin-x64": "@rollup/rollup-darwin-x64",
|
"darwin-x64": "@rollup/rollup-darwin-x64",
|
||||||
|
"win32-arm64": "@rollup/rollup-win32-arm64-msvc",
|
||||||
"win32-x64": "@rollup/rollup-win32-x64-msvc",
|
"win32-x64": "@rollup/rollup-win32-x64-msvc",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ tauri = { version = "2.5.2", features = [ "devtools"] }
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
|
base64 = "0.22"
|
||||||
|
rustls = { version = "0.23", features = ["ring"] }
|
||||||
|
reqwest = { version = "0.12", default-features = false, features = ["http2", "charset", "json", "stream", "rustls-tls"] }
|
||||||
regex = "1"
|
regex = "1"
|
||||||
once_cell = "1"
|
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
thiserror = "1"
|
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
which = "4"
|
which = "4"
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
@@ -28,4 +29,7 @@ url = "2"
|
|||||||
tauri-plugin-notification = "2"
|
tauri-plugin-notification = "2"
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_UI_Shell", "Win32_Security", "Win32_System_JobObjects"] }
|
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security_Cryptography", "Win32_UI_Shell", "Win32_Security", "Win32_System_JobObjects"] }
|
||||||
|
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
webkit2gtk = "2.0.2"
|
||||||
|
|||||||
2807
packages/tauri-app/src-tauri/gen/schemas/windows-schema.json
Normal file
2807
packages/tauri-app/src-tauri/gen/schemas/windows-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
BIN
packages/tauri-app/src-tauri/icons/linux/128x128.png
Normal file
BIN
packages/tauri-app/src-tauri/icons/linux/128x128.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
BIN
packages/tauri-app/src-tauri/icons/linux/256x256.png
Normal file
BIN
packages/tauri-app/src-tauri/icons/linux/256x256.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 74 KiB |
BIN
packages/tauri-app/src-tauri/icons/linux/32x32.png
Normal file
BIN
packages/tauri-app/src-tauri/icons/linux/32x32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 KiB |
BIN
packages/tauri-app/src-tauri/icons/linux/48x48.png
Normal file
BIN
packages/tauri-app/src-tauri/icons/linux/48x48.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 4.4 KiB |
BIN
packages/tauri-app/src-tauri/icons/linux/512x512.png
Normal file
BIN
packages/tauri-app/src-tauri/icons/linux/512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 322 KiB |
BIN
packages/tauri-app/src-tauri/icons/linux/64x64.png
Normal file
BIN
packages/tauri-app/src-tauri/icons/linux/64x64.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.8 KiB |
@@ -0,0 +1,9 @@
|
|||||||
|
[Desktop Entry]
|
||||||
|
Categories=
|
||||||
|
Exec=codenomad-tauri
|
||||||
|
StartupWMClass=codenomad-tauri
|
||||||
|
Icon=codenomad-tauri
|
||||||
|
Name=CodeNomad
|
||||||
|
NoDisplay=true
|
||||||
|
Terminal=false
|
||||||
|
Type=Application
|
||||||
449
packages/tauri-app/src-tauri/src/cert_manager.rs
Normal file
449
packages/tauri-app/src-tauri/src/cert_manager.rs
Normal file
@@ -0,0 +1,449 @@
|
|||||||
|
use base64::Engine;
|
||||||
|
use std::env;
|
||||||
|
use std::fs;
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG_PATH: &str = "~/.config/codenomad/config.json";
|
||||||
|
const TLS_DIR_NAME: &str = "tls";
|
||||||
|
const CA_CERT_FILE: &str = "ca-cert.pem";
|
||||||
|
const SERVER_CERT_FILE: &str = "server-cert.pem";
|
||||||
|
const SERVER_KEY_FILE: &str = "server-key.pem";
|
||||||
|
const TRUSTED_MARKER: &str = "server-ca.trusted";
|
||||||
|
#[cfg(windows)]
|
||||||
|
const WINDOWS_APP_USER_MODEL_ID: &str = "ai.neuralnomads.codenomad.client";
|
||||||
|
|
||||||
|
/// Holds the PEM-encoded certificate/key pair used by the local HTTPS proxy,
|
||||||
|
/// plus the CA certificate DER used for trust-store installation.
|
||||||
|
pub struct LocalCert {
|
||||||
|
pub cert_pem: String,
|
||||||
|
pub key_pem: String,
|
||||||
|
pub ca_cert_der: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct TlsAssetPaths {
|
||||||
|
cert_path: PathBuf,
|
||||||
|
key_path: PathBuf,
|
||||||
|
trust_path: PathBuf,
|
||||||
|
append_ca_to_cert: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the TLS assets already managed by `packages/server`.
|
||||||
|
pub fn ensure_local_cert() -> Result<LocalCert, String> {
|
||||||
|
let assets = resolve_tls_asset_paths()?;
|
||||||
|
let mut cert_pem = read_pem_file(&assets.cert_path)?;
|
||||||
|
let key_pem = read_pem_file(&assets.key_path)?;
|
||||||
|
let trust_pem = read_pem_file(&assets.trust_path)?;
|
||||||
|
|
||||||
|
if assets.append_ca_to_cert {
|
||||||
|
cert_pem = format!("{}\n{}\n", cert_pem.trim(), trust_pem.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
let ca_cert_der = pem_to_der(&trust_pem)?;
|
||||||
|
|
||||||
|
Ok(LocalCert {
|
||||||
|
cert_pem,
|
||||||
|
key_pem,
|
||||||
|
ca_cert_der,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_pem_file(path: &Path) -> Result<String, String> {
|
||||||
|
fs::read_to_string(path).map_err(|e| format!("Failed to read {}: {e}", path.display()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_tls_dir() -> Result<PathBuf, String> {
|
||||||
|
Ok(resolve_server_config_base_dir()?.join(TLS_DIR_NAME))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_tls_asset_paths() -> Result<TlsAssetPaths, String> {
|
||||||
|
let tls_key_path = env::var("CLI_TLS_KEY")
|
||||||
|
.ok()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.map(|value| resolve_path_like_server(&value))
|
||||||
|
.transpose()?;
|
||||||
|
let tls_cert_path = env::var("CLI_TLS_CERT")
|
||||||
|
.ok()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.map(|value| resolve_path_like_server(&value))
|
||||||
|
.transpose()?;
|
||||||
|
let tls_ca_path = env::var("CLI_TLS_CA")
|
||||||
|
.ok()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.map(|value| resolve_path_like_server(&value))
|
||||||
|
.transpose()?;
|
||||||
|
|
||||||
|
match (tls_key_path, tls_cert_path) {
|
||||||
|
(Some(key_path), Some(cert_path)) => {
|
||||||
|
let append_ca_to_cert = tls_ca_path.is_some();
|
||||||
|
let trust_path = tls_ca_path.unwrap_or_else(|| cert_path.clone());
|
||||||
|
Ok(TlsAssetPaths {
|
||||||
|
cert_path,
|
||||||
|
key_path,
|
||||||
|
trust_path,
|
||||||
|
append_ca_to_cert,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(Some(_), None) | (None, Some(_)) => Err(
|
||||||
|
"CLI_TLS_KEY and CLI_TLS_CERT must both be set when using custom TLS files"
|
||||||
|
.to_string(),
|
||||||
|
),
|
||||||
|
(None, None) => {
|
||||||
|
let tls_dir = server_tls_dir()?;
|
||||||
|
Ok(TlsAssetPaths {
|
||||||
|
cert_path: tls_dir.join(SERVER_CERT_FILE),
|
||||||
|
key_path: tls_dir.join(SERVER_KEY_FILE),
|
||||||
|
trust_path: tls_dir.join(CA_CERT_FILE),
|
||||||
|
append_ca_to_cert: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_server_config_base_dir() -> Result<PathBuf, String> {
|
||||||
|
let raw = env::var("CLI_CONFIG")
|
||||||
|
.ok()
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.unwrap_or_else(|| DEFAULT_CONFIG_PATH.to_string());
|
||||||
|
let expanded = resolve_path_like_server(&raw)?;
|
||||||
|
let lower = raw.trim().to_lowercase();
|
||||||
|
|
||||||
|
if lower.ends_with(".yaml") || lower.ends_with(".yml") || lower.ends_with(".json") {
|
||||||
|
return expanded
|
||||||
|
.parent()
|
||||||
|
.map(Path::to_path_buf)
|
||||||
|
.ok_or_else(|| format!("Failed to determine config base dir from {}", expanded.display()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(expanded)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_path_like_server(path: &str) -> Result<PathBuf, String> {
|
||||||
|
if path.starts_with("~/") {
|
||||||
|
let home = dirs::home_dir().or_else(|| env::var("HOME").ok().map(PathBuf::from));
|
||||||
|
let home = home.ok_or_else(|| "Cannot determine home directory".to_string())?;
|
||||||
|
return Ok(home.join(path.trim_start_matches("~/")));
|
||||||
|
}
|
||||||
|
|
||||||
|
let path = PathBuf::from(path);
|
||||||
|
if path.is_absolute() {
|
||||||
|
return Ok(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cwd = env::current_dir().map_err(|e| format!("Failed to read current dir: {e}"))?;
|
||||||
|
Ok(cwd.join(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trusted_marker_path() -> Result<PathBuf, String> {
|
||||||
|
let base = dirs::data_local_dir()
|
||||||
|
.ok_or_else(|| "Cannot determine local app data directory".to_string())?;
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
return Ok(base.join(WINDOWS_APP_USER_MODEL_ID).join(TRUSTED_MARKER));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
{
|
||||||
|
Ok(base.join("codenomad").join(TRUSTED_MARKER))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trusted_marker_value(cert_der: &[u8]) -> String {
|
||||||
|
cert_der.iter().map(|byte| format!("{byte:02x}")).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trusted_marker_file_suffix(cert_der: &[u8]) -> String {
|
||||||
|
trusted_marker_value(cert_der).chars().take(16).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_matching_trusted_marker(cert_der: &[u8]) -> bool {
|
||||||
|
trusted_marker_path()
|
||||||
|
.ok()
|
||||||
|
.and_then(|path| fs::read_to_string(path).ok())
|
||||||
|
.map(|value| value.trim() == trusted_marker_value(cert_der))
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_trusted_marker(cert_der: &[u8]) -> Result<(), String> {
|
||||||
|
let path = trusted_marker_path()?;
|
||||||
|
if let Some(parent) = path.parent() {
|
||||||
|
fs::create_dir_all(parent)
|
||||||
|
.map_err(|e| format!("Failed to create trust state dir {}: {e}", parent.display()))?;
|
||||||
|
}
|
||||||
|
fs::write(path, trusted_marker_value(cert_der))
|
||||||
|
.map_err(|e| format!("Failed to write trust marker: {e}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn needs_trust_in_store(cert_der: &[u8]) -> Result<bool, String> {
|
||||||
|
Ok(!windows_cert_is_trusted(cert_der)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
pub fn trust_cert_in_store(cert_der: &[u8]) -> Result<(), String> {
|
||||||
|
use windows_sys::Win32::Security::Cryptography::{
|
||||||
|
CertAddEncodedCertificateToStore, CertCloseStore, CertOpenSystemStoreW,
|
||||||
|
CERT_STORE_ADD_REPLACE_EXISTING, PKCS_7_ASN_ENCODING, X509_ASN_ENCODING,
|
||||||
|
};
|
||||||
|
|
||||||
|
if !needs_trust_in_store(cert_der)? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let store_name: Vec<u16> = "Root\0".encode_utf16().collect();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let store = CertOpenSystemStoreW(0, store_name.as_ptr());
|
||||||
|
if store.is_null() {
|
||||||
|
return Err("Failed to open CurrentUser\\Root certificate store".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let encoding = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING;
|
||||||
|
let result = CertAddEncodedCertificateToStore(
|
||||||
|
store,
|
||||||
|
encoding,
|
||||||
|
cert_der.as_ptr(),
|
||||||
|
cert_der.len() as u32,
|
||||||
|
CERT_STORE_ADD_REPLACE_EXISTING,
|
||||||
|
std::ptr::null_mut(),
|
||||||
|
);
|
||||||
|
|
||||||
|
CertCloseStore(store, 0);
|
||||||
|
|
||||||
|
if result == 0 {
|
||||||
|
return Err(
|
||||||
|
"Failed to add certificate to trust store. The user may have declined the security dialog."
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
write_trusted_marker(cert_der)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn needs_trust_in_store(cert_der: &[u8]) -> Result<bool, String> {
|
||||||
|
Ok(!(has_matching_trusted_marker(cert_der) && macos_cert_is_trusted(cert_der)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
pub fn trust_cert_in_store(cert_der: &[u8]) -> Result<(), String> {
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
if !needs_trust_in_store(cert_der)? {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let temp_path = env::temp_dir().join(format!(
|
||||||
|
"codenomad-server-ca-{}.cer",
|
||||||
|
trusted_marker_file_suffix(cert_der)
|
||||||
|
));
|
||||||
|
fs::write(&temp_path, cert_der)
|
||||||
|
.map_err(|e| format!("Failed to write temporary certificate {}: {e}", temp_path.display()))?;
|
||||||
|
|
||||||
|
let keychain_path = resolve_macos_user_keychain()?;
|
||||||
|
|
||||||
|
let mut command = Command::new("/usr/bin/security");
|
||||||
|
command.args(["add-trusted-cert", "-r", "trustRoot", "-k"]);
|
||||||
|
command.arg(&keychain_path);
|
||||||
|
|
||||||
|
let output = command.arg(&temp_path).output().map_err(|e| {
|
||||||
|
format!(
|
||||||
|
"Failed to launch macOS security tool to trust the local CA certificate: {e}"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let _ = fs::remove_file(&temp_path);
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
|
let detail = if stderr.is_empty() {
|
||||||
|
format!("security exited with status {}", output.status)
|
||||||
|
} else {
|
||||||
|
stderr
|
||||||
|
};
|
||||||
|
return Err(format!(
|
||||||
|
"Failed to add the local CodeNomad CA certificate to the macOS trust settings: {detail}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
if !macos_cert_is_trusted(cert_der)? {
|
||||||
|
return Err(format!(
|
||||||
|
"Added the local CodeNomad CA certificate to {} but could not verify that macOS trusts it",
|
||||||
|
keychain_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
write_trusted_marker(cert_der)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(windows)]
|
||||||
|
fn windows_cert_is_trusted(cert_der: &[u8]) -> Result<bool, String> {
|
||||||
|
use windows_sys::Win32::Security::Cryptography::{
|
||||||
|
CertCloseStore, CertEnumCertificatesInStore, CertOpenSystemStoreW,
|
||||||
|
};
|
||||||
|
|
||||||
|
let store_name: Vec<u16> = "Root\0".encode_utf16().collect();
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
let store = CertOpenSystemStoreW(0, store_name.as_ptr());
|
||||||
|
if store.is_null() {
|
||||||
|
return Err("Failed to open CurrentUser\\Root certificate store".into());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut context = CertEnumCertificatesInStore(store, std::ptr::null());
|
||||||
|
while !context.is_null() {
|
||||||
|
let encoded = std::slice::from_raw_parts(
|
||||||
|
(*context).pbCertEncoded,
|
||||||
|
(*context).cbCertEncoded as usize,
|
||||||
|
);
|
||||||
|
if encoded == cert_der {
|
||||||
|
CertCloseStore(store, 0);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
context = CertEnumCertificatesInStore(store, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
CertCloseStore(store, 0);
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn resolve_macos_user_keychain() -> Result<PathBuf, String> {
|
||||||
|
let output = std::process::Command::new("/usr/bin/security")
|
||||||
|
.args(["default-keychain", "-d", "user"])
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to resolve macOS default user keychain: {e}"))?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let trimmed = stdout.trim().trim_matches('"');
|
||||||
|
if !trimmed.is_empty() {
|
||||||
|
return Ok(PathBuf::from(trimmed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let home = dirs::home_dir().or_else(|| env::var("HOME").ok().map(PathBuf::from));
|
||||||
|
let home = home.ok_or_else(|| "Cannot determine home directory for macOS keychain lookup".to_string())?;
|
||||||
|
Ok(home.join("Library/Keychains/login.keychain-db"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn macos_cert_is_trusted(cert_der: &[u8]) -> Result<bool, String> {
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
let temp_path = env::temp_dir().join(format!(
|
||||||
|
"codenomad-server-ca-verify-{}.cer",
|
||||||
|
trusted_marker_file_suffix(cert_der)
|
||||||
|
));
|
||||||
|
fs::write(&temp_path, cert_der)
|
||||||
|
.map_err(|e| format!("Failed to write temporary certificate {}: {e}", temp_path.display()))?;
|
||||||
|
|
||||||
|
let keychain_path = resolve_macos_user_keychain()?;
|
||||||
|
let fingerprint = macos_cert_sha256(&temp_path)?;
|
||||||
|
let find_output = Command::new("/usr/bin/security")
|
||||||
|
.args(["find-certificate", "-a", "-Z", "-c", "CodeNomad Local CA"])
|
||||||
|
.arg(&keychain_path)
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to query macOS keychain certificates: {e}"))?;
|
||||||
|
|
||||||
|
if !find_output.status.success() {
|
||||||
|
let _ = fs::remove_file(&temp_path);
|
||||||
|
let stderr = String::from_utf8_lossy(&find_output.stderr).trim().to_string();
|
||||||
|
let detail = if stderr.is_empty() {
|
||||||
|
format!("security exited with status {}", find_output.status)
|
||||||
|
} else {
|
||||||
|
stderr
|
||||||
|
};
|
||||||
|
return Err(format!(
|
||||||
|
"Failed to inspect the macOS keychain for the local CodeNomad CA certificate: {detail}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&find_output.stdout);
|
||||||
|
if !stdout.to_ascii_uppercase().contains(&fingerprint) {
|
||||||
|
let _ = fs::remove_file(&temp_path);
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
let verify_output = Command::new("/usr/bin/security")
|
||||||
|
.args(["verify-cert", "-q", "-L", "-l", "-p", "basic", "-c"])
|
||||||
|
.arg(&temp_path)
|
||||||
|
.args(["-k"])
|
||||||
|
.arg(&keychain_path)
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to verify macOS trust for the local CodeNomad CA certificate: {e}"))?;
|
||||||
|
|
||||||
|
let _ = fs::remove_file(&temp_path);
|
||||||
|
Ok(verify_output.status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
fn macos_cert_sha256(cert_path: &Path) -> Result<String, String> {
|
||||||
|
let output = std::process::Command::new("/usr/bin/shasum")
|
||||||
|
.args(["-a", "256"])
|
||||||
|
.arg(cert_path)
|
||||||
|
.output()
|
||||||
|
.map_err(|e| format!("Failed to compute SHA-256 for {}: {e}", cert_path.display()))?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
|
let detail = if stderr.is_empty() {
|
||||||
|
format!("shasum exited with status {}", output.status)
|
||||||
|
} else {
|
||||||
|
stderr
|
||||||
|
};
|
||||||
|
return Err(format!(
|
||||||
|
"Failed to compute SHA-256 for {}: {detail}",
|
||||||
|
cert_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
let hash = stdout
|
||||||
|
.split_whitespace()
|
||||||
|
.next()
|
||||||
|
.ok_or_else(|| format!("Failed to parse SHA-256 output for {}", cert_path.display()))?;
|
||||||
|
Ok(hash.to_ascii_uppercase())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(not(windows), not(target_os = "macos")))]
|
||||||
|
pub fn needs_trust_in_store(_cert_der: &[u8]) -> Result<bool, String> {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(not(windows), not(target_os = "macos")))]
|
||||||
|
pub fn trust_cert_in_store(_cert_der: &[u8]) -> Result<(), String> {
|
||||||
|
// Non-Windows platforms use native webview-specific handling instead of OS trust-store writes.
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pem_to_der(pem: &str) -> Result<Vec<u8>, String> {
|
||||||
|
let mut body = String::new();
|
||||||
|
let mut in_block = false;
|
||||||
|
|
||||||
|
for line in pem.lines() {
|
||||||
|
if line.starts_with("-----BEGIN CERTIFICATE-----") {
|
||||||
|
in_block = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if line.starts_with("-----END CERTIFICATE-----") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if in_block {
|
||||||
|
body.push_str(line.trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if body.is_empty() {
|
||||||
|
return Err("No certificate found in PEM file".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
base64::engine::general_purpose::STANDARD
|
||||||
|
.decode(body)
|
||||||
|
.map_err(|e| format!("Failed to decode certificate PEM: {e}"))
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ use windows_sys::Win32::System::JobObjects::{
|
|||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
const CREATE_NO_WINDOW: u32 = 0x08000000;
|
||||||
|
const MISSING_NODE_PREFIX: &str = "CODENOMAD_MISSING_NODE:";
|
||||||
|
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -630,6 +631,13 @@ impl CliProcessManager {
|
|||||||
|
|
||||||
let use_user_shell = supports_user_shell();
|
let use_user_shell = supports_user_shell();
|
||||||
|
|
||||||
|
if !use_user_shell && which::which(&resolution.node_binary).is_err() {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Node binary '{}' not found. CodeNomad desktop currently requires Node.js installed on the system, or set NODE_BINARY to a valid runtime path.",
|
||||||
|
resolution.node_binary
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let command_info = if use_user_shell {
|
let command_info = if use_user_shell {
|
||||||
log_line("spawning via user shell");
|
log_line("spawning via user shell");
|
||||||
ShellCommandType::UserShell(build_shell_command_string(&resolution, &args)?)
|
ShellCommandType::UserShell(build_shell_command_string(&resolution, &args)?)
|
||||||
@@ -641,14 +649,6 @@ impl CliProcessManager {
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
if !use_user_shell {
|
|
||||||
if which::which(&resolution.node_binary).is_err() {
|
|
||||||
return Err(anyhow::anyhow!(
|
|
||||||
"Node binary not found. Make sure Node.js is installed."
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let child = match &command_info {
|
let child = match &command_info {
|
||||||
ShellCommandType::UserShell(cmd) => {
|
ShellCommandType::UserShell(cmd) => {
|
||||||
log_line(&format!("spawn command: {} {:?}", cmd.shell, cmd.args));
|
log_line(&format!("spawn command: {} {:?}", cmd.shell, cmd.args));
|
||||||
@@ -920,6 +920,17 @@ impl CliProcessManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(node_binary) = line.strip_prefix(MISSING_NODE_PREFIX) {
|
||||||
|
let mut locked = status.lock();
|
||||||
|
if locked.error.is_none() {
|
||||||
|
locked.error = Some(format!(
|
||||||
|
"Node binary '{}' not found in the desktop shell environment. CodeNomad desktop currently requires Node.js installed on the system, or set NODE_BINARY to a valid runtime path.",
|
||||||
|
node_binary.trim()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(url) = local_url_regex
|
if let Some(url) = local_url_regex
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(|re| re.captures(line).and_then(|c| c.get(1)))
|
.and_then(|re| re.captures(line).and_then(|c| c.get(1)))
|
||||||
@@ -1083,7 +1094,8 @@ impl CliEntry {
|
|||||||
];
|
];
|
||||||
|
|
||||||
if dev {
|
if dev {
|
||||||
// Dev: plain HTTP + Vite dev server proxy.
|
// Dev: keep loopback HTTP for the Vite proxy, but also enable HTTPS so
|
||||||
|
// remote proxy sessions can still spin up secure local windows.
|
||||||
let ui_dev_server = std::env::var("VITE_DEV_SERVER_URL")
|
let ui_dev_server = std::env::var("VITE_DEV_SERVER_URL")
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|value| !value.trim().is_empty())
|
.filter(|value| !value.trim().is_empty())
|
||||||
@@ -1100,7 +1112,7 @@ impl CliEntry {
|
|||||||
.unwrap_or_else(|| "info".to_string());
|
.unwrap_or_else(|| "info".to_string());
|
||||||
|
|
||||||
args.push("--https".to_string());
|
args.push("--https".to_string());
|
||||||
args.push("false".to_string());
|
args.push("true".to_string());
|
||||||
args.push("--http".to_string());
|
args.push("--http".to_string());
|
||||||
args.push("true".to_string());
|
args.push("true".to_string());
|
||||||
args.push("--http-port".to_string());
|
args.push("--http-port".to_string());
|
||||||
@@ -1248,7 +1260,13 @@ fn build_shell_command_string(
|
|||||||
for arg in entry.runner_args(cli_args) {
|
for arg in entry.runner_args(cli_args) {
|
||||||
quoted.push(shell_escape(&arg));
|
quoted.push(shell_escape(&arg));
|
||||||
}
|
}
|
||||||
let command = format!("ELECTRON_RUN_AS_NODE=1 exec {}", quoted.join(" "));
|
let command = format!(
|
||||||
|
"if command -v {} >/dev/null 2>&1; then ELECTRON_RUN_AS_NODE=1 exec {}; else printf '%s%s\\n' '{}' {} >&2; exit 127; fi",
|
||||||
|
shell_escape(&entry.node_binary),
|
||||||
|
quoted.join(" "),
|
||||||
|
MISSING_NODE_PREFIX,
|
||||||
|
shell_escape(&entry.node_binary),
|
||||||
|
);
|
||||||
let args = build_shell_args(&shell, &command);
|
let args = build_shell_args(&shell, &command);
|
||||||
log_line(&format!("user shell command: {} {:?}", shell, args));
|
log_line(&format!("user shell command: {} {:?}", shell, args));
|
||||||
Ok(ShellCommand { shell, args })
|
Ok(ShellCommand { shell, args })
|
||||||
@@ -1288,8 +1306,11 @@ fn build_shell_args(shell: &str, command: &str) -> Vec<String> {
|
|||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
|
|
||||||
let _ = shell_name;
|
if shell_name.contains("zsh") || shell_name.contains("bash") {
|
||||||
vec!["-l".into(), "-c".into(), command.into()]
|
vec!["-i".into(), "-l".into(), "-c".into(), command.into()]
|
||||||
|
} else {
|
||||||
|
vec!["-l".into(), "-c".into(), command.into()]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn first_existing(paths: Vec<Option<PathBuf>>) -> Option<String> {
|
fn first_existing(paths: Vec<Option<PathBuf>>) -> Option<String> {
|
||||||
|
|||||||
88
packages/tauri-app/src-tauri/src/linux_tls.rs
Normal file
88
packages/tauri-app/src-tauri/src/linux_tls.rs
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
use crate::AppState;
|
||||||
|
use tauri::{AppHandle, Manager, WebviewWindow};
|
||||||
|
use url::Url;
|
||||||
|
use webkit2gtk::{WebContextExt, WebView, WebViewExt};
|
||||||
|
|
||||||
|
pub fn should_bootstrap_tls_navigation(target_url: &Url, allow_tls_certificate: bool) -> bool {
|
||||||
|
allow_tls_certificate && target_url.scheme() == "https"
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn ensure_remote_window_tls_handler(
|
||||||
|
window: &WebviewWindow,
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
window_label: &str,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
{
|
||||||
|
let state = app_handle.state::<AppState>();
|
||||||
|
let mut handlers = state
|
||||||
|
.remote_tls_handlers
|
||||||
|
.lock()
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
if !handlers.insert(window_label.to_string()) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let app_handle = app_handle.clone();
|
||||||
|
let window_label = window_label.to_string();
|
||||||
|
window
|
||||||
|
.with_webview(move |platform_webview| {
|
||||||
|
let webview = platform_webview.inner();
|
||||||
|
let app_handle = app_handle.clone();
|
||||||
|
let window_label = window_label.clone();
|
||||||
|
webview.connect_load_failed_with_tls_errors(move |view, failing_uri, certificate, _| {
|
||||||
|
allow_remote_tls_certificate(
|
||||||
|
&app_handle,
|
||||||
|
&window_label,
|
||||||
|
view,
|
||||||
|
failing_uri,
|
||||||
|
certificate,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn allow_remote_tls_certificate(
|
||||||
|
app_handle: &AppHandle,
|
||||||
|
window_label: &str,
|
||||||
|
view: &WebView,
|
||||||
|
failing_uri: &str,
|
||||||
|
certificate: &webkit2gtk::gio::TlsCertificate,
|
||||||
|
) -> bool {
|
||||||
|
let Ok(parsed_uri) = Url::parse(failing_uri) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
let Some(host) = parsed_uri.host_str() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = app_handle.state::<AppState>();
|
||||||
|
let skip_tls_verify = state
|
||||||
|
.remote_skip_tls_verify
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|values| values.get(window_label).copied())
|
||||||
|
.unwrap_or(false);
|
||||||
|
if !skip_tls_verify {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected_origin = state
|
||||||
|
.remote_origins
|
||||||
|
.lock()
|
||||||
|
.ok()
|
||||||
|
.and_then(|origins| origins.get(window_label).cloned());
|
||||||
|
let parsed_origin = parsed_uri.origin().ascii_serialization();
|
||||||
|
if expected_origin.as_deref() != Some(parsed_origin.as_str()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(context) = view.context() else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
context.allow_tls_certificate_for_host(certificate, host);
|
||||||
|
view.load_uri(failing_uri);
|
||||||
|
true
|
||||||
|
}
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
mod cert_manager;
|
||||||
mod cli_manager;
|
mod cli_manager;
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
mod linux_tls;
|
||||||
|
|
||||||
use cli_manager::{CliProcessManager, CliStatus};
|
use cli_manager::{CliProcessManager, CliStatus};
|
||||||
use keepawake::KeepAwake;
|
use keepawake::KeepAwake;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
@@ -16,6 +20,7 @@ use tauri::webview::Webview;
|
|||||||
use tauri::{
|
use tauri::{
|
||||||
AppHandle, Emitter, Manager, Runtime, WebviewUrl, WebviewWindowBuilder, WindowEvent, Wry,
|
AppHandle, Emitter, Manager, Runtime, WebviewUrl, WebviewWindowBuilder, WindowEvent, Wry,
|
||||||
};
|
};
|
||||||
|
use tauri_plugin_dialog::{DialogExt, MessageDialogButtons, MessageDialogKind};
|
||||||
use tauri_plugin_global_shortcut::{
|
use tauri_plugin_global_shortcut::{
|
||||||
Code as ShortcutCode, GlobalShortcutExt, Shortcut, ShortcutState,
|
Code as ShortcutCode, GlobalShortcutExt, Shortcut, ShortcutState,
|
||||||
};
|
};
|
||||||
@@ -45,6 +50,9 @@ pub struct AppState {
|
|||||||
pub wake_lock: Mutex<Option<KeepAwake>>,
|
pub wake_lock: Mutex<Option<KeepAwake>>,
|
||||||
pub zoom_level: Mutex<f64>,
|
pub zoom_level: Mutex<f64>,
|
||||||
pub remote_origins: Mutex<HashMap<String, String>>,
|
pub remote_origins: Mutex<HashMap<String, String>>,
|
||||||
|
pub remote_proxy_sessions: Mutex<HashMap<String, String>>,
|
||||||
|
pub remote_skip_tls_verify: Mutex<HashMap<String, bool>>,
|
||||||
|
pub remote_tls_handlers: Mutex<HashSet<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -53,9 +61,87 @@ struct RemoteWindowPayload {
|
|||||||
id: String,
|
id: String,
|
||||||
name: String,
|
name: String,
|
||||||
base_url: String,
|
base_url: String,
|
||||||
|
entry_url: Option<String>,
|
||||||
|
proxy_session_id: Option<String>,
|
||||||
|
#[allow(dead_code)]
|
||||||
skip_tls_verify: bool,
|
skip_tls_verify: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn schedule_remote_proxy_session_cleanup(app: AppHandle, session_id: String) {
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
if let Err(err) = cleanup_remote_proxy_session(&app, &session_id).await {
|
||||||
|
eprintln!(
|
||||||
|
"[tauri] failed to clean up remote proxy session {}: {}",
|
||||||
|
session_id, err
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn confirm_local_certificate_install(app: &AppHandle) -> Result<bool, String> {
|
||||||
|
let (sender, receiver) = std::sync::mpsc::sync_channel(1);
|
||||||
|
|
||||||
|
let mut dialog = app
|
||||||
|
.dialog()
|
||||||
|
.message(
|
||||||
|
"CodeNomad needs to install a local certificate to open self-signed HTTPS remote windows. This certificate is only used for local desktop proxy traffic on your machine. Your operating system may show a second certificate prompt after this.",
|
||||||
|
)
|
||||||
|
.title("Install Local Certificate")
|
||||||
|
.kind(MessageDialogKind::Warning)
|
||||||
|
.buttons(MessageDialogButtons::OkCancelCustom(
|
||||||
|
"Continue".into(),
|
||||||
|
"Cancel".into(),
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
dialog = dialog.parent(&window);
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show(move |accepted| {
|
||||||
|
let _ = sender.send(accepted);
|
||||||
|
});
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn_blocking(move || receiver.recv().unwrap_or(false))
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn cleanup_remote_proxy_session(app: &AppHandle, session_id: &str) -> Result<(), String> {
|
||||||
|
let status = app.state::<AppState>().manager.status();
|
||||||
|
let Some(base_url) = status.url else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cleanup_url = Url::parse(&base_url).map_err(|err| err.to_string())?;
|
||||||
|
cleanup_url.set_path(&format!("/api/remote-proxy/sessions/{session_id}"));
|
||||||
|
cleanup_url.set_query(None);
|
||||||
|
cleanup_url.set_fragment(None);
|
||||||
|
|
||||||
|
let client = if cleanup_url.scheme() == "https" {
|
||||||
|
let local_cert = cert_manager::ensure_local_cert()?;
|
||||||
|
let ca_cert = reqwest::Certificate::from_der(&local_cert.ca_cert_der)
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
reqwest::Client::builder()
|
||||||
|
.add_root_certificate(ca_cert)
|
||||||
|
.build()
|
||||||
|
.map_err(|err| err.to_string())?
|
||||||
|
} else {
|
||||||
|
reqwest::Client::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
let response = client
|
||||||
|
.delete(cleanup_url.as_str())
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
if response.status().is_success() || response.status() == reqwest::StatusCode::NOT_FOUND {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
Err(format!("unexpected status {}", response.status()))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize)]
|
#[derive(Debug, Default, Deserialize)]
|
||||||
#[serde(default, rename_all = "camelCase")]
|
#[serde(default, rename_all = "camelCase")]
|
||||||
struct WakeLockConfig {
|
struct WakeLockConfig {
|
||||||
@@ -119,7 +205,7 @@ fn is_dev_mode() -> bool {
|
|||||||
|
|
||||||
fn should_allow_internal(url: &Url) -> bool {
|
fn should_allow_internal(url: &Url) -> bool {
|
||||||
match url.scheme() {
|
match url.scheme() {
|
||||||
"tauri" | "asset" | "file" => true,
|
"tauri" | "asset" | "file" | "about" => true,
|
||||||
// On Windows/WebView2, Tauri serves the app assets from `tauri.localhost`.
|
// On Windows/WebView2, Tauri serves the app assets from `tauri.localhost`.
|
||||||
// This must be treated as an internal origin or the navigation guard will
|
// This must be treated as an internal origin or the navigation guard will
|
||||||
// redirect it to the system browser and the app will appear blank.
|
// redirect it to the system browser and the app will appear blank.
|
||||||
@@ -167,25 +253,61 @@ fn intercept_navigation<R: Runtime>(webview: &Webview<R>, url: &Url) -> bool {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tauri::command]
|
async fn open_remote_window_impl(
|
||||||
fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<(), String> {
|
app: AppHandle,
|
||||||
if payload.skip_tls_verify && payload.base_url.starts_with("https://") {
|
payload: RemoteWindowPayload,
|
||||||
return Err(
|
) -> Result<(), String> {
|
||||||
"Tauri cannot bypass self-signed HTTPS certificates automatically yet. Trust the certificate in your OS first, then reconnect, or use the CodeNomad Electron app."
|
let entry_url = payload.entry_url.as_deref().unwrap_or(payload.base_url.as_str());
|
||||||
.to_string(),
|
let parsed = Url::parse(entry_url).map_err(|err| err.to_string())?;
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed = Url::parse(&payload.base_url).map_err(|err| err.to_string())?;
|
|
||||||
let label = format!("remote-{}", payload.id);
|
let label = format!("remote-{}", payload.id);
|
||||||
let title = format!(
|
let title = format!(
|
||||||
"{} - {}",
|
"{} - {}",
|
||||||
payload.name,
|
payload.name,
|
||||||
parsed.host_str().unwrap_or(payload.base_url.as_str())
|
Url::parse(&payload.base_url)
|
||||||
|
.ok()
|
||||||
|
.and_then(|url| url.host_str().map(str::to_string))
|
||||||
|
.unwrap_or_else(|| payload.base_url.clone())
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let window_url = parsed.clone();
|
||||||
|
|
||||||
|
let allow_linux_tls_certificate =
|
||||||
|
parsed.scheme() == "https" && (payload.proxy_session_id.is_some() || payload.skip_tls_verify);
|
||||||
|
|
||||||
|
app.state::<AppState>()
|
||||||
|
.remote_origins
|
||||||
|
.lock()
|
||||||
|
.map_err(|err| err.to_string())?
|
||||||
|
.insert(label.clone(), window_url.origin().ascii_serialization());
|
||||||
|
app.state::<AppState>()
|
||||||
|
.remote_skip_tls_verify
|
||||||
|
.lock()
|
||||||
|
.map_err(|err| err.to_string())?
|
||||||
|
.insert(label.clone(), allow_linux_tls_certificate);
|
||||||
|
|
||||||
|
let replaced_session = {
|
||||||
|
let state = app.state::<AppState>();
|
||||||
|
let mut sessions = state
|
||||||
|
.remote_proxy_sessions
|
||||||
|
.lock()
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
match payload.proxy_session_id.clone() {
|
||||||
|
Some(session_id) => sessions.insert(label.clone(), session_id),
|
||||||
|
None => sessions.remove(&label),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(previous) = replaced_session {
|
||||||
|
if payload.proxy_session_id.as_deref() != Some(previous.as_str()) {
|
||||||
|
schedule_remote_proxy_session_cleanup(app.clone(), previous);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(existing) = app.get_webview_window(&label) {
|
if let Some(existing) = app.get_webview_window(&label) {
|
||||||
let _ = existing.navigate(parsed.clone());
|
#[cfg(target_os = "linux")]
|
||||||
|
linux_tls::ensure_remote_window_tls_handler(&existing, &app, &label)?;
|
||||||
|
|
||||||
|
let _ = existing.navigate(window_url.clone());
|
||||||
let _ = existing.set_title(&title);
|
let _ = existing.set_title(&title);
|
||||||
let _ = existing.show();
|
let _ = existing.show();
|
||||||
let _ = existing.unminimize();
|
let _ = existing.unminimize();
|
||||||
@@ -193,25 +315,51 @@ fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<()
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
app.state::<AppState>()
|
#[cfg(target_os = "linux")]
|
||||||
.remote_origins
|
let initial_url = if linux_tls::should_bootstrap_tls_navigation(
|
||||||
.lock()
|
&window_url,
|
||||||
.map_err(|err| err.to_string())?
|
allow_linux_tls_certificate,
|
||||||
.insert(label.clone(), parsed.origin().ascii_serialization());
|
) {
|
||||||
|
Url::parse("about:blank").map_err(|err| err.to_string())?
|
||||||
|
} else {
|
||||||
|
window_url.clone()
|
||||||
|
};
|
||||||
|
|
||||||
let window =
|
#[cfg(not(target_os = "linux"))]
|
||||||
WebviewWindowBuilder::new(&app, label.clone(), WebviewUrl::External(parsed.clone()))
|
let initial_url = window_url.clone();
|
||||||
.title(title)
|
|
||||||
.inner_size(1400.0, 900.0)
|
let window = WebviewWindowBuilder::new(&app, label.clone(), WebviewUrl::External(initial_url.clone()))
|
||||||
.min_inner_size(800.0, 600.0)
|
.title(title)
|
||||||
.build()
|
.inner_size(1400.0, 900.0)
|
||||||
.map_err(|err| err.to_string())?;
|
.min_inner_size(800.0, 600.0)
|
||||||
|
.build()
|
||||||
|
.map_err(|err| err.to_string())?;
|
||||||
|
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
{
|
||||||
|
linux_tls::ensure_remote_window_tls_handler(&window, &app, &label)?;
|
||||||
|
if initial_url != window_url {
|
||||||
|
let _ = window.navigate(window_url.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let app_handle = app.clone();
|
let app_handle = app.clone();
|
||||||
|
let label_for_cleanup = label.clone();
|
||||||
window.on_window_event(move |event| {
|
window.on_window_event(move |event| {
|
||||||
if let WindowEvent::Destroyed = event {
|
if let WindowEvent::Destroyed = event {
|
||||||
if let Ok(mut origins) = app_handle.state::<AppState>().remote_origins.lock() {
|
if let Ok(mut origins) = app_handle.state::<AppState>().remote_origins.lock() {
|
||||||
origins.remove(&label);
|
origins.remove(&label_for_cleanup);
|
||||||
|
}
|
||||||
|
if let Ok(mut sessions) = app_handle.state::<AppState>().remote_proxy_sessions.lock() {
|
||||||
|
if let Some(session_id) = sessions.remove(&label_for_cleanup) {
|
||||||
|
schedule_remote_proxy_session_cleanup(app_handle.clone(), session_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Ok(mut values) = app_handle.state::<AppState>().remote_skip_tls_verify.lock() {
|
||||||
|
values.remove(&label_for_cleanup);
|
||||||
|
}
|
||||||
|
if let Ok(mut handlers) = app_handle.state::<AppState>().remote_tls_handlers.lock() {
|
||||||
|
handlers.remove(&label_for_cleanup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -219,6 +367,40 @@ fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<()
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
async fn open_remote_window(app: AppHandle, payload: RemoteWindowPayload) -> Result<(), String> {
|
||||||
|
#[cfg(not(target_os = "linux"))]
|
||||||
|
{
|
||||||
|
let entry_url = payload.entry_url.as_deref().unwrap_or(payload.base_url.as_str());
|
||||||
|
let parsed = Url::parse(entry_url).map_err(|err| err.to_string())?;
|
||||||
|
if payload.proxy_session_id.is_some() && parsed.scheme() == "https" {
|
||||||
|
let local_cert = cert_manager::ensure_local_cert().map_err(|err| {
|
||||||
|
format!(
|
||||||
|
"Failed to load the local HTTPS certificate for the remote proxy window: {err}"
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if cert_manager::needs_trust_in_store(&local_cert.ca_cert_der).map_err(|err| {
|
||||||
|
format!("Failed to inspect the local CodeNomad certificate trust state: {err}")
|
||||||
|
})? {
|
||||||
|
let accepted = confirm_local_certificate_install(&app).await?;
|
||||||
|
if !accepted {
|
||||||
|
return Err(
|
||||||
|
"CodeNomad needs the local certificate to be trusted before it can open self-signed HTTPS remote windows."
|
||||||
|
.to_string(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Err(err) = cert_manager::trust_cert_in_store(&local_cert.ca_cert_der) {
|
||||||
|
return Err(format!(
|
||||||
|
"Failed to trust the local CodeNomad CA certificate. Accept the certificate installation prompt and try again: {err}"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
open_remote_window_impl(app, payload).await
|
||||||
|
}
|
||||||
|
|
||||||
fn collect_directory_paths(paths: &[std::path::PathBuf]) -> Vec<String> {
|
fn collect_directory_paths(paths: &[std::path::PathBuf]) -> Vec<String> {
|
||||||
paths
|
paths
|
||||||
.iter()
|
.iter()
|
||||||
@@ -346,6 +528,8 @@ fn set_windows_app_user_model_id() {
|
|||||||
fn set_windows_app_user_model_id() {}
|
fn set_windows_app_user_model_id() {}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
|
||||||
let navigation_guard: TauriPlugin<Wry, ()> = PluginBuilder::new("external-link-guard")
|
let navigation_guard: TauriPlugin<Wry, ()> = PluginBuilder::new("external-link-guard")
|
||||||
.on_navigation(|webview, url| intercept_navigation(webview, url))
|
.on_navigation(|webview, url| intercept_navigation(webview, url))
|
||||||
.build();
|
.build();
|
||||||
@@ -373,6 +557,9 @@ fn main() {
|
|||||||
wake_lock: Mutex::new(None),
|
wake_lock: Mutex::new(None),
|
||||||
zoom_level: Mutex::new(DEFAULT_ZOOM_LEVEL),
|
zoom_level: Mutex::new(DEFAULT_ZOOM_LEVEL),
|
||||||
remote_origins: Mutex::new(HashMap::new()),
|
remote_origins: Mutex::new(HashMap::new()),
|
||||||
|
remote_proxy_sessions: Mutex::new(HashMap::new()),
|
||||||
|
remote_skip_tls_verify: Mutex::new(HashMap::new()),
|
||||||
|
remote_tls_handlers: Mutex::new(HashSet::new()),
|
||||||
})
|
})
|
||||||
.setup(|app| {
|
.setup(|app| {
|
||||||
set_windows_app_user_model_id();
|
set_windows_app_user_model_id();
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
"frontendDist": "resources/ui-loading"
|
"frontendDist": "resources/ui-loading"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
|
"enableGTKAppId": true,
|
||||||
"withGlobalTauri": true,
|
"withGlobalTauri": true,
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
@@ -41,6 +42,35 @@
|
|||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
|
"linux": {
|
||||||
|
"appimage": {
|
||||||
|
"files": {
|
||||||
|
"/usr/share/applications/ai.neuralnomads.codenomad.client.desktop": "icons/linux/ai.neuralnomads.codenomad.client.desktop"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deb": {
|
||||||
|
"files": {
|
||||||
|
"/usr/share/applications/ai.neuralnomads.codenomad.client.desktop": "icons/linux/ai.neuralnomads.codenomad.client.desktop",
|
||||||
|
"/usr/share/icons/hicolor/32x32/apps/codenomad-tauri.png": "icons/linux/32x32.png",
|
||||||
|
"/usr/share/icons/hicolor/48x48/apps/codenomad-tauri.png": "icons/linux/48x48.png",
|
||||||
|
"/usr/share/icons/hicolor/64x64/apps/codenomad-tauri.png": "icons/linux/64x64.png",
|
||||||
|
"/usr/share/icons/hicolor/128x128/apps/codenomad-tauri.png": "icons/linux/128x128.png",
|
||||||
|
"/usr/share/icons/hicolor/256x256/apps/codenomad-tauri.png": "icons/linux/256x256.png",
|
||||||
|
"/usr/share/icons/hicolor/512x512/apps/codenomad-tauri.png": "icons/linux/512x512.png"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rpm": {
|
||||||
|
"files": {
|
||||||
|
"/usr/share/applications/ai.neuralnomads.codenomad.client.desktop": "icons/linux/ai.neuralnomads.codenomad.client.desktop",
|
||||||
|
"/usr/share/icons/hicolor/32x32/apps/codenomad-tauri.png": "icons/linux/32x32.png",
|
||||||
|
"/usr/share/icons/hicolor/48x48/apps/codenomad-tauri.png": "icons/linux/48x48.png",
|
||||||
|
"/usr/share/icons/hicolor/64x64/apps/codenomad-tauri.png": "icons/linux/64x64.png",
|
||||||
|
"/usr/share/icons/hicolor/128x128/apps/codenomad-tauri.png": "icons/linux/128x128.png",
|
||||||
|
"/usr/share/icons/hicolor/256x256/apps/codenomad-tauri.png": "icons/linux/256x256.png",
|
||||||
|
"/usr/share/icons/hicolor/512x512/apps/codenomad-tauri.png": "icons/linux/512x512.png"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"resources": [
|
"resources": [
|
||||||
"resources/server",
|
"resources/server",
|
||||||
"resources/ui-loading"
|
"resources/ui-loading"
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import { showAlertDialog } from "../stores/alerts"
|
|||||||
import { openSettings, settingsOpen } from "../stores/settings-screen"
|
import { openSettings, settingsOpen } from "../stores/settings-screen"
|
||||||
import { openExternalUrl } from "../lib/external-url"
|
import { openExternalUrl } from "../lib/external-url"
|
||||||
import { serverApi } from "../lib/api-client"
|
import { serverApi } from "../lib/api-client"
|
||||||
|
import { runtimeEnv } from "../lib/runtime-env"
|
||||||
import { openRemoteServerWindow } from "../lib/native/remote-window"
|
import { openRemoteServerWindow } from "../lib/native/remote-window"
|
||||||
|
|
||||||
const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
|
const codeNomadLogo = new URL("../images/CodeNomad-Icon.png", import.meta.url).href
|
||||||
@@ -332,7 +333,23 @@ const FolderSelectionView: Component<FolderSelectionViewProps> = (props) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if (openWindow) {
|
if (openWindow) {
|
||||||
await openRemoteServerWindow(profile)
|
const remoteProxySession =
|
||||||
|
runtimeEnv.host === "tauri" && profile.skipTlsVerify && profile.baseUrl.startsWith("https://")
|
||||||
|
? await serverApi.createRemoteProxySession({
|
||||||
|
baseUrl: profile.baseUrl,
|
||||||
|
skipTlsVerify: profile.skipTlsVerify,
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
try {
|
||||||
|
await openRemoteServerWindow(profile, remoteProxySession?.windowUrl, remoteProxySession?.sessionId)
|
||||||
|
} catch (error) {
|
||||||
|
if (remoteProxySession) {
|
||||||
|
void serverApi.deleteRemoteProxySession(remoteProxySession.sessionId).catch(() => {})
|
||||||
|
}
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
await markRemoteServerConnected(profile.id)
|
await markRemoteServerConnected(profile.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -357,7 +357,11 @@ const InstanceShell2: Component<InstanceShellProps> = (props) => {
|
|||||||
const pill = activeSessionStatusPill()
|
const pill = activeSessionStatusPill()
|
||||||
if (!pill) return null
|
if (!pill) return null
|
||||||
return (
|
return (
|
||||||
<span class={`status-indicator session-status session-status-list ${pill.className}`} title={pill.title}>
|
<span
|
||||||
|
class={`status-indicator session-status session-status-list ${pill.className} notranslate`}
|
||||||
|
title={pill.title}
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
{pill.showAlertIcon ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
|
{pill.showAlertIcon ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
|
||||||
{pill.text}
|
{pill.text}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -638,18 +638,25 @@ export default function MessageSection(props: MessageSectionProps) {
|
|||||||
const autoPinHoldTargetKey = createMemo(() => {
|
const autoPinHoldTargetKey = createMemo(() => {
|
||||||
if (!holdLongAssistantRepliesEnabled()) return null
|
if (!holdLongAssistantRepliesEnabled()) return null
|
||||||
const messageId = lastVisibleMessageId()
|
const messageId = lastVisibleMessageId()
|
||||||
return isAssistantTextMessage(messageId) ? messageId : null
|
return isStreamingAssistantTextMessage(messageId) ? messageId : null
|
||||||
})
|
})
|
||||||
|
|
||||||
function toggleHoldLongAssistantReplies() {
|
function toggleHoldLongAssistantReplies() {
|
||||||
updatePreferences({ holdLongAssistantReplies: !holdLongAssistantRepliesEnabled() })
|
updatePreferences({ holdLongAssistantReplies: !holdLongAssistantRepliesEnabled() })
|
||||||
}
|
}
|
||||||
|
|
||||||
function isAssistantTextMessage(messageId: string | null | undefined) {
|
function isStreamingAssistantTextMessage(messageId: string | null | undefined) {
|
||||||
if (!messageId) return false
|
if (!messageId) return false
|
||||||
const resolvedStore = store()
|
const resolvedStore = store()
|
||||||
const record = resolvedStore.getMessage(messageId)
|
const record = resolvedStore.getMessage(messageId)
|
||||||
if (!record || record.role !== "assistant") return false
|
if (!record || record.role !== "assistant") return false
|
||||||
|
if (record.status !== "streaming") return false
|
||||||
|
|
||||||
|
const info = resolvedStore.getMessageInfo(messageId)
|
||||||
|
if (!info) return false
|
||||||
|
const timeInfo = info?.time as { end?: number } | undefined
|
||||||
|
const isStreaming = timeInfo?.end === undefined || timeInfo.end === 0
|
||||||
|
if (!isStreaming) return false
|
||||||
|
|
||||||
const { orderedParts } = buildRecordDisplayData(props.instanceId, record)
|
const { orderedParts } = buildRecordDisplayData(props.instanceId, record)
|
||||||
return orderedParts.some((part) => {
|
return orderedParts.some((part) => {
|
||||||
|
|||||||
@@ -520,7 +520,11 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
<ChevronDown class={`w-3.5 h-3.5 transition-transform ${rowProps.expanded ? "" : "-rotate-90"}`} />
|
<ChevronDown class={`w-3.5 h-3.5 transition-transform ${rowProps.expanded ? "" : "-rotate-90"}`} />
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
<span class={`status-indicator session-status session-status-list ${statusClassName()}`} title={statusTooltip()}>
|
<span
|
||||||
|
class={`status-indicator session-status session-status-list ${statusClassName()} notranslate`}
|
||||||
|
title={statusTooltip()}
|
||||||
|
translate="no"
|
||||||
|
>
|
||||||
{needsInput() ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
|
{needsInput() ? <ShieldAlert class="w-3.5 h-3.5" aria-hidden="true" /> : <span class="status-dot" />}
|
||||||
{statusText()}
|
{statusText()}
|
||||||
</span>
|
</span>
|
||||||
@@ -736,7 +740,9 @@ const SessionList: Component<SessionListProps> = (props) => {
|
|||||||
<div class="session-list-header p-3 border-b border-base">
|
<div class="session-list-header p-3 border-b border-base">
|
||||||
{props.headerContent ?? (
|
{props.headerContent ?? (
|
||||||
<div class="flex items-center justify-between gap-3">
|
<div class="flex items-center justify-between gap-3">
|
||||||
<h3 class="text-sm font-semibold text-primary">{t("sessionList.header.title")}</h3>
|
<h3 class="text-sm font-semibold text-primary notranslate" translate="no">
|
||||||
|
{t("sessionList.header.title")}
|
||||||
|
</h3>
|
||||||
<KeyboardHint
|
<KeyboardHint
|
||||||
shortcuts={[keyboardRegistry.get("session-prev")!, keyboardRegistry.get("session-next")!].filter(Boolean)}
|
shortcuts={[keyboardRegistry.get("session-prev")!, keyboardRegistry.get("session-next")!].filter(Boolean)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -161,8 +161,9 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
|
const [showScrollTopButton, setShowScrollTopButton] = createSignal(false)
|
||||||
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
|
const [showScrollBottomButton, setShowScrollBottomButton] = createSignal(false)
|
||||||
const [activeKey, setActiveKey] = createSignal<string | null>(null)
|
const [activeKey, setActiveKey] = createSignal<string | null>(null)
|
||||||
const [heldItemCount, setHeldItemCount] = createSignal<number | null>(null)
|
const [activeHoldTargetKey, setActiveHoldTargetKey] = createSignal<string | null>(null)
|
||||||
const effectiveSuspendAutoPinToBottom = () => externalSuspendAutoPinToBottom() || heldItemCount() !== null
|
const [didTriggerHoldForCurrentTarget, setDidTriggerHoldForCurrentTarget] = createSignal(false)
|
||||||
|
const effectiveSuspendAutoPinToBottom = () => externalSuspendAutoPinToBottom() || activeHoldTargetKey() !== null
|
||||||
|
|
||||||
const scrollButtonsCount = createMemo(() => (showScrollTopButton() ? 1 : 0) + (showScrollBottomButton() ? 1 : 0))
|
const scrollButtonsCount = createMemo(() => (showScrollTopButton() ? 1 : 0) + (showScrollBottomButton() ? 1 : 0))
|
||||||
const itemElements = new Map<string, HTMLDivElement>()
|
const itemElements = new Map<string, HTMLDivElement>()
|
||||||
@@ -196,6 +197,17 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
return performance.now() <= userScrollIntentUntil
|
return performance.now() <= userScrollIntentUntil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearAutoPinHold(options?: { resumeBottom?: boolean }) {
|
||||||
|
if (activeHoldTargetKey() === null) return
|
||||||
|
setActiveHoldTargetKey(null)
|
||||||
|
if (options?.resumeBottom && autoScroll()) {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!autoScroll() || activeHoldTargetKey() !== null) return
|
||||||
|
scrollToBottom(false)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function attachScrollIntentListeners(element: HTMLDivElement | undefined) {
|
function attachScrollIntentListeners(element: HTMLDivElement | undefined) {
|
||||||
if (detachScrollIntentListeners) {
|
if (detachScrollIntentListeners) {
|
||||||
detachScrollIntentListeners()
|
detachScrollIntentListeners()
|
||||||
@@ -257,7 +269,7 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
// scrollbar). If follow mode stays enabled, the next render notification
|
// scrollbar). If follow mode stays enabled, the next render notification
|
||||||
// snaps the list straight back to bottom. A real upward viewport move away
|
// snaps the list straight back to bottom. A real upward viewport move away
|
||||||
// from bottom should always break follow unless a hold target is active.
|
// from bottom should always break follow unless a hold target is active.
|
||||||
if (wasPinnedAtBottom && scrolledUp && autoScroll() && !atBottom && heldItemCount() === null) {
|
if (wasPinnedAtBottom && scrolledUp && autoScroll() && !atBottom && activeHoldTargetKey() === null) {
|
||||||
setAutoScroll(false)
|
setAutoScroll(false)
|
||||||
lastObservedPinnedAtBottom = false
|
lastObservedPinnedAtBottom = false
|
||||||
return
|
return
|
||||||
@@ -265,9 +277,7 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
|
|
||||||
// Sync autoScroll state based on scroll position if it was a user scroll
|
// Sync autoScroll state based on scroll position if it was a user scroll
|
||||||
if (hasUserScrollIntent()) {
|
if (hasUserScrollIntent()) {
|
||||||
if (atBottom && heldItemCount() !== null) {
|
clearAutoPinHold()
|
||||||
setHeldItemCount(null)
|
|
||||||
}
|
|
||||||
if (atBottom && !autoScroll()) {
|
if (atBottom && !autoScroll()) {
|
||||||
setAutoScroll(true)
|
setAutoScroll(true)
|
||||||
} else if (!atBottom && autoScroll()) {
|
} else if (!atBottom && autoScroll()) {
|
||||||
@@ -303,7 +313,6 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateScrollButtons()
|
updateScrollButtons()
|
||||||
updateAutoPinHold()
|
|
||||||
props.onScroll?.()
|
props.onScroll?.()
|
||||||
|
|
||||||
// Find active key (roughly the first visible item)
|
// Find active key (roughly the first visible item)
|
||||||
@@ -335,25 +344,14 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
|
|
||||||
function updateAutoPinHold() {
|
function updateAutoPinHold() {
|
||||||
const element = scrollElement()
|
const element = scrollElement()
|
||||||
const itemCount = props.items().length
|
|
||||||
const heldCount = heldItemCount()
|
|
||||||
if (!element) return
|
if (!element) return
|
||||||
|
|
||||||
if (heldCount !== null) {
|
const targetKey = holdTargetKey()
|
||||||
if (itemCount > heldCount) {
|
const heldKey = activeHoldTargetKey()
|
||||||
setHeldItemCount(null)
|
|
||||||
if (autoScroll()) {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (!autoScroll()) return
|
|
||||||
scrollToBottom(false)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemCount < heldCount) {
|
if (heldKey !== null) {
|
||||||
setHeldItemCount(null)
|
if (targetKey !== heldKey) {
|
||||||
return
|
clearAutoPinHold({ resumeBottom: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -361,9 +359,8 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
|
|
||||||
if (!autoScroll()) return
|
if (!autoScroll()) return
|
||||||
if (externalSuspendAutoPinToBottom()) return
|
if (externalSuspendAutoPinToBottom()) return
|
||||||
|
|
||||||
const targetKey = holdTargetKey()
|
|
||||||
if (!targetKey) return
|
if (!targetKey) return
|
||||||
|
if (didTriggerHoldForCurrentTarget()) return
|
||||||
|
|
||||||
const itemWrapper = itemElements.get(targetKey)
|
const itemWrapper = itemElements.get(targetKey)
|
||||||
if (!itemWrapper) return
|
if (!itemWrapper) return
|
||||||
@@ -379,7 +376,8 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
if (Math.abs(alignDelta) > 1) {
|
if (Math.abs(alignDelta) > 1) {
|
||||||
element.scrollTop = Math.max(0, element.scrollTop + alignDelta)
|
element.scrollTop = Math.max(0, element.scrollTop + alignDelta)
|
||||||
}
|
}
|
||||||
setHeldItemCount(itemCount)
|
setActiveHoldTargetKey(targetKey)
|
||||||
|
setDidTriggerHoldForCurrentTarget(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +393,7 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
},
|
},
|
||||||
notifyContentRendered: () => {
|
notifyContentRendered: () => {
|
||||||
updateAutoPinHold()
|
updateAutoPinHold()
|
||||||
if (heldItemCount() !== null) return
|
if (activeHoldTargetKey() !== null) return
|
||||||
if (autoScroll() && !effectiveSuspendAutoPinToBottom()) {
|
if (autoScroll() && !effectiveSuspendAutoPinToBottom()) {
|
||||||
scrollToBottom(true)
|
scrollToBottom(true)
|
||||||
}
|
}
|
||||||
@@ -411,14 +409,23 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
|
|
||||||
createEffect(on(() => props.resetKey?.(), () => {
|
createEffect(on(() => props.resetKey?.(), () => {
|
||||||
itemElements.clear()
|
itemElements.clear()
|
||||||
setHeldItemCount(null)
|
setActiveHoldTargetKey(null)
|
||||||
|
setDidTriggerHoldForCurrentTarget(false)
|
||||||
lastObservedScrollOffset = 0
|
lastObservedScrollOffset = 0
|
||||||
lastObservedPinnedAtBottom = false
|
lastObservedPinnedAtBottom = false
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
createEffect(on(holdTargetKey, (nextTargetKey, prevTargetKey) => {
|
||||||
|
if (nextTargetKey !== prevTargetKey && didTriggerHoldForCurrentTarget()) {
|
||||||
|
setDidTriggerHoldForCurrentTarget(false)
|
||||||
|
}
|
||||||
|
if (activeHoldTargetKey() === null) return
|
||||||
|
if (nextTargetKey === activeHoldTargetKey()) return
|
||||||
|
clearAutoPinHold({ resumeBottom: true })
|
||||||
|
}, { defer: true }))
|
||||||
|
|
||||||
// Handle autoScroll (Follow) on items change
|
// Handle autoScroll (Follow) on items change
|
||||||
createEffect(on(() => props.items().length, (len, prevLen) => {
|
createEffect(on(() => props.items().length, (len, prevLen) => {
|
||||||
updateAutoPinHold()
|
|
||||||
if (len > (prevLen ?? 0) && autoScroll() && !effectiveSuspendAutoPinToBottom() && !suppressAutoScrollOnce) {
|
if (len > (prevLen ?? 0) && autoScroll() && !effectiveSuspendAutoPinToBottom() && !suppressAutoScrollOnce) {
|
||||||
requestAnimationFrame(() => scrollToBottom(true))
|
requestAnimationFrame(() => scrollToBottom(true))
|
||||||
}
|
}
|
||||||
@@ -427,16 +434,11 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
|
|
||||||
// Handle followToken change
|
// Handle followToken change
|
||||||
createEffect(on(() => props.followToken?.(), () => {
|
createEffect(on(() => props.followToken?.(), () => {
|
||||||
updateAutoPinHold()
|
|
||||||
if (autoScroll() && !effectiveSuspendAutoPinToBottom()) {
|
if (autoScroll() && !effectiveSuspendAutoPinToBottom()) {
|
||||||
scrollToBottom(true)
|
scrollToBottom(true)
|
||||||
}
|
}
|
||||||
}, { defer: true }))
|
}, { defer: true }))
|
||||||
|
|
||||||
createEffect(on(() => holdTargetKey(), () => {
|
|
||||||
updateAutoPinHold()
|
|
||||||
}, { defer: true }))
|
|
||||||
|
|
||||||
// Reset state on resetKey change
|
// Reset state on resetKey change
|
||||||
createEffect(on(() => props.resetKey?.(), (nextKey) => {
|
createEffect(on(() => props.resetKey?.(), (nextKey) => {
|
||||||
if (nextKey === lastResetKey) return
|
if (nextKey === lastResetKey) return
|
||||||
@@ -459,13 +461,6 @@ export default function VirtualFollowList<T>(props: VirtualFollowListProps<T>) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (typeof window === "undefined") return
|
|
||||||
const handleResize = () => updateAutoPinHold()
|
|
||||||
window.addEventListener("resize", handleResize)
|
|
||||||
onCleanup(() => window.removeEventListener("resize", handleResize))
|
|
||||||
})
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="virtual-follow-list-shell" ref={shellElement => {
|
<div class="virtual-follow-list-shell" ref={shellElement => {
|
||||||
setShellElement(shellElement)
|
setShellElement(shellElement)
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ import type {
|
|||||||
SpeechTranscriptionResponse,
|
SpeechTranscriptionResponse,
|
||||||
SideCar,
|
SideCar,
|
||||||
ServerMeta,
|
ServerMeta,
|
||||||
|
RemoteProxySessionCreateRequest,
|
||||||
|
RemoteProxySessionCreateResponse,
|
||||||
RemoteServerProbeRequest,
|
RemoteServerProbeRequest,
|
||||||
RemoteServerProbeResponse,
|
RemoteServerProbeResponse,
|
||||||
VoiceModeStateResponse,
|
VoiceModeStateResponse,
|
||||||
@@ -256,6 +258,15 @@ export const serverApi = {
|
|||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
createRemoteProxySession(payload: RemoteProxySessionCreateRequest): Promise<RemoteProxySessionCreateResponse> {
|
||||||
|
return request<RemoteProxySessionCreateResponse>("/api/remote-proxy/sessions", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteRemoteProxySession(id: string): Promise<void> {
|
||||||
|
return request(`/api/remote-proxy/sessions/${encodeURIComponent(id)}`, { method: "DELETE" })
|
||||||
|
},
|
||||||
fetchAuthStatus(): Promise<{ authenticated: boolean; username?: string; passwordUserProvided?: boolean }> {
|
fetchAuthStatus(): Promise<{ authenticated: boolean; username?: string; passwordUserProvided?: boolean }> {
|
||||||
return request<{ authenticated: boolean; username?: string; passwordUserProvided?: boolean }>("/api/auth/status")
|
return request<{ authenticated: boolean; username?: string; passwordUserProvided?: boolean }>("/api/auth/status")
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -6,14 +6,22 @@ export interface RemoteWindowOpenPayload {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
|
entryUrl?: string
|
||||||
|
proxySessionId?: string
|
||||||
skipTlsVerify: boolean
|
skipTlsVerify: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openRemoteServerWindow(profile: Pick<RemoteServerProfile, "id" | "name" | "baseUrl" | "skipTlsVerify">): Promise<void> {
|
export async function openRemoteServerWindow(
|
||||||
|
profile: Pick<RemoteServerProfile, "id" | "name" | "baseUrl" | "skipTlsVerify">,
|
||||||
|
entryUrl?: string,
|
||||||
|
proxySessionId?: string,
|
||||||
|
): Promise<void> {
|
||||||
const payload: RemoteWindowOpenPayload = {
|
const payload: RemoteWindowOpenPayload = {
|
||||||
id: profile.id,
|
id: profile.id,
|
||||||
name: profile.name,
|
name: profile.name,
|
||||||
baseUrl: profile.baseUrl,
|
baseUrl: profile.baseUrl,
|
||||||
|
entryUrl,
|
||||||
|
proxySessionId,
|
||||||
skipTlsVerify: profile.skipTlsVerify,
|
skipTlsVerify: profile.skipTlsVerify,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -397,7 +397,8 @@ function handleMessageUpdate(instanceId: string, event: MessageUpdateEvent | Mes
|
|||||||
|
|
||||||
const role: MessageRole = info.role === "user" ? "user" : "assistant"
|
const role: MessageRole = info.role === "user" ? "user" : "assistant"
|
||||||
const hasError = Boolean((info as any).error)
|
const hasError = Boolean((info as any).error)
|
||||||
const status: MessageStatus = hasError ? "error" : "complete"
|
const hasEnded = typeof timeInfo.end === "number" && timeInfo.end > 0
|
||||||
|
const status: MessageStatus = hasError ? "error" : hasEnded ? "complete" : "streaming"
|
||||||
|
|
||||||
let record = store.getMessage(messageId)
|
let record = store.getMessage(messageId)
|
||||||
if (!record) {
|
if (!record) {
|
||||||
|
|||||||
2
packages/ui/src/types/global.d.ts
vendored
2
packages/ui/src/types/global.d.ts
vendored
@@ -37,6 +37,8 @@ declare global {
|
|||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
baseUrl: string
|
baseUrl: string
|
||||||
|
entryUrl?: string
|
||||||
|
proxySessionId?: string
|
||||||
skipTlsVerify: boolean
|
skipTlsVerify: boolean
|
||||||
}) => Promise<{ ok: boolean }>
|
}) => Promise<{ ok: boolean }>
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user