From c050e073b68205bae7389fb86958cb9331e1a3be Mon Sep 17 00:00:00 2001 From: Tony <68118705+Legend-Master@users.noreply.github.com> Date: Wed, 16 Sep 2026 10:49:36 +0800 Subject: [PATCH 01/10] fix(store): `apply_pending_auto_save` can deadlock (#3572) --- .changes/store-pending-audo-save-deadlock.md | 6 ++++++ plugins/store/src/store.rs | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .changes/store-pending-audo-save-deadlock.md diff --git a/.changes/store-pending-audo-save-deadlock.md b/.changes/store-pending-audo-save-deadlock.md new file mode 100644 index 000000000..ef8d1a452 --- /dev/null +++ b/.changes/store-pending-audo-save-deadlock.md @@ -0,0 +1,6 @@ +--- +"store": patch +"store-js": patch +--- + +Fix `apply_pending_auto_save` can deadlock the store diff --git a/plugins/store/src/store.rs b/plugins/store/src/store.rs index 088dcc240..b597a4369 100644 --- a/plugins/store/src/store.rs +++ b/plugins/store/src/store.rs @@ -600,7 +600,8 @@ impl Store { fn apply_pending_auto_save(&self) { // Cancel and save if auto save is pending - if let Some(sender) = self.auto_save_debounce_sender.lock().unwrap().take() { + let auto_save_debounce_sender = self.auto_save_debounce_sender.lock().unwrap().take(); + if let Some(sender) = auto_save_debounce_sender { let _ = sender.send(AutoSaveMessage::Cancel); let _ = self.save(); }; From 3c5d2677677dc7ebea618514af56fc951449cc1b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 12:17:22 +0800 Subject: [PATCH 02/10] publish new versions (#3567) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changes/http-floating-cleanup-rejections.md | 6 ------ .changes/store-pending-audo-save-deadlock.md | 6 ------ Cargo.lock | 6 +++--- examples/api/CHANGELOG.md | 7 +++++++ examples/api/package.json | 2 +- examples/api/src-tauri/CHANGELOG.md | 7 +++++++ examples/api/src-tauri/Cargo.toml | 6 +++--- plugins/http/CHANGELOG.md | 4 ++++ plugins/http/Cargo.toml | 2 +- plugins/http/package.json | 2 +- plugins/store/CHANGELOG.md | 4 ++++ plugins/store/Cargo.toml | 2 +- plugins/store/package.json | 2 +- 13 files changed, 33 insertions(+), 23 deletions(-) delete mode 100644 .changes/http-floating-cleanup-rejections.md delete mode 100644 .changes/store-pending-audo-save-deadlock.md diff --git a/.changes/http-floating-cleanup-rejections.md b/.changes/http-floating-cleanup-rejections.md deleted file mode 100644 index 17da05d7b..000000000 --- a/.changes/http-floating-cleanup-rejections.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"http": patch -"http-js": patch ---- - -Fix unhandled promise rejections on every `fetch` teardown: the request/body cleanup commands were fired as floating promises, and releasing an already-released resource rejects with `The resource id N is invalid.`. `dropBody` is now idempotent and both cleanup calls handle their own rejection. diff --git a/.changes/store-pending-audo-save-deadlock.md b/.changes/store-pending-audo-save-deadlock.md deleted file mode 100644 index ef8d1a452..000000000 --- a/.changes/store-pending-audo-save-deadlock.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"store": patch -"store-js": patch ---- - -Fix `apply_pending_auto_save` can deadlock the store diff --git a/Cargo.lock b/Cargo.lock index 22fbc476f..b8bb9d45c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,7 +207,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "api" -version = "2.0.47" +version = "2.0.48" dependencies = [ "log", "serde", @@ -6605,7 +6605,7 @@ dependencies = [ [[package]] name = "tauri-plugin-http" -version = "2.6.0" +version = "2.6.1" dependencies = [ "bytes", "cookie_store", @@ -6818,7 +6818,7 @@ dependencies = [ [[package]] name = "tauri-plugin-store" -version = "2.4.4" +version = "2.4.5" dependencies = [ "dunce", "serde", diff --git a/examples/api/CHANGELOG.md b/examples/api/CHANGELOG.md index 59e03e9ad..70baa50b6 100644 --- a/examples/api/CHANGELOG.md +++ b/examples/api/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.0.44] + +### Dependencies + +- Upgraded to `http-js@2.6.1` +- Upgraded to `store-js@2.4.5` + ## [2.0.43] ### Dependencies diff --git a/examples/api/package.json b/examples/api/package.json index 77f3bc189..81cac8b9a 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -1,7 +1,7 @@ { "name": "api", "private": true, - "version": "2.0.43", + "version": "2.0.44", "type": "module", "scripts": { "dev": "vite --clearScreen false", diff --git a/examples/api/src-tauri/CHANGELOG.md b/examples/api/src-tauri/CHANGELOG.md index b45209f3e..1d6def5a3 100644 --- a/examples/api/src-tauri/CHANGELOG.md +++ b/examples/api/src-tauri/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.0.48] + +### Dependencies + +- Upgraded to `http@2.6.1` +- Upgraded to `store@2.4.5` + ## [2.0.47] ### Dependencies diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index 29f0234aa..e9c963870 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "api" publish = false -version = "2.0.47" +version = "2.0.48" description = "An example Tauri Application showcasing the api" edition = "2021" rust-version = { workspace = true } @@ -29,7 +29,7 @@ tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.7.3" } tauri-plugin-http = { path = "../../../plugins/http", features = [ "multipart", "cookies", -], version = "2.6.0" } +], version = "2.6.1" } tauri-plugin-notification = { path = "../../../plugins/notification", version = "2.4.0", features = [ "windows7-compat", ] } @@ -37,7 +37,7 @@ tauri-plugin-os = { path = "../../../plugins/os", version = "2.3.2" } tauri-plugin-process = { path = "../../../plugins/process", version = "2.3.1" } tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.5.5" } tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.3.6" } -tauri-plugin-store = { path = "../../../plugins/store", version = "2.4.4" } +tauri-plugin-store = { path = "../../../plugins/store", version = "2.4.5" } tauri-plugin-upload = { path = "../../../plugins/upload", version = "2.3.0" } [dependencies.tauri] diff --git a/plugins/http/CHANGELOG.md b/plugins/http/CHANGELOG.md index 604050dba..b4f3d8c01 100644 --- a/plugins/http/CHANGELOG.md +++ b/plugins/http/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [2.6.1] + +- [`a21555dd`](https://github.com/tauri-apps/plugins-workspace/commit/a21555ddd2eaadfed23848912fe2802c2ba7579e) ([#3566](https://github.com/tauri-apps/plugins-workspace/pull/3566) by [@followdarko](https://github.com/tauri-apps/plugins-workspace/../../followdarko)) Fix unhandled promise rejections on every `fetch` teardown: the request/body cleanup commands were fired as floating promises, and releasing an already-released resource rejects with `The resource id N is invalid.`. `dropBody` is now idempotent and both cleanup calls handle their own rejection. + ## [2.6.0] - [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. diff --git a/plugins/http/Cargo.toml b/plugins/http/Cargo.toml index 16f67bcbf..6c707f4d2 100644 --- a/plugins/http/Cargo.toml +++ b/plugins/http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-http" -version = "2.6.0" +version = "2.6.1" description = "Access an HTTP client written in Rust." edition = { workspace = true } authors = { workspace = true } diff --git a/plugins/http/package.json b/plugins/http/package.json index bc93fda91..5d612e6a8 100644 --- a/plugins/http/package.json +++ b/plugins/http/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-http", - "version": "2.6.0", + "version": "2.6.1", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" diff --git a/plugins/store/CHANGELOG.md b/plugins/store/CHANGELOG.md index dcc84abe9..beb8d85f2 100644 --- a/plugins/store/CHANGELOG.md +++ b/plugins/store/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [2.4.5] + +- [`c050e073`](https://github.com/tauri-apps/plugins-workspace/commit/c050e073b68205bae7389fb86958cb9331e1a3be) ([#3572](https://github.com/tauri-apps/plugins-workspace/pull/3572) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix `apply_pending_auto_save` can deadlock the store + ## \[2.4.4] - [`2ed6d6c1`](https://github.com/tauri-apps/plugins-workspace/commit/2ed6d6c1de4f5051b87cb6a2c5ac45320c444989) ([#3499](https://github.com/tauri-apps/plugins-workspace/pull/3499) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Fix `StoreOptions` requires `defaults` field diff --git a/plugins/store/Cargo.toml b/plugins/store/Cargo.toml index 1bbbdcf98..3d71d0fef 100644 --- a/plugins/store/Cargo.toml +++ b/plugins/store/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-store" -version = "2.4.4" +version = "2.4.5" description = "Simple, persistent key-value store." authors = { workspace = true } license = { workspace = true } diff --git a/plugins/store/package.json b/plugins/store/package.json index 8a85ca192..79f7924f4 100644 --- a/plugins/store/package.json +++ b/plugins/store/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-store", - "version": "2.4.4", + "version": "2.4.5", "description": "Simple, persistent key-value store.", "license": "MIT OR Apache-2.0", "authors": [ From 4b5c9549c96f099bbb3f77e4f3fac0f01748902e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 16 Sep 2026 14:23:04 +0800 Subject: [PATCH 03/10] chore(deps): update dependency typescript-eslint to v8.70.0 (#3536) * chore(deps): update dependency typescript-eslint to v8.70.0 * fix rust audit * dedupe --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Tony --- Cargo.lock | 8 +-- package.json | 2 +- pnpm-lock.yaml | 150 ++++++++++++++++++++++++------------------------- 3 files changed, 79 insertions(+), 81 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8bb9d45c..67d7e8a7a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5186,9 +5186,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.28" +version = "0.23.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7160e3e10bf4535308537f3c4e1641468cd0e485175d6163087c0393c7d46643" +checksum = "0d41d731c7d2f962d1ccc364cec258de3c0e93b38c2fb3ba97ac74513048d634" dependencies = [ "once_cell", "ring", @@ -5249,9 +5249,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.13" +version = "0.103.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" dependencies = [ "ring", "rustls-pki-types", diff --git a/package.json b/package.json index f4aac3d8e..7fe38ce34 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "rollup": "4.63.1", "tslib": "2.8.1", "typescript": "6.0.3", - "typescript-eslint": "8.66.0" + "typescript-eslint": "8.70.0" }, "minimumReleaseAge": 4320, "packageManager": "pnpm@12.3.4" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7d530f329..863f28e48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -143,8 +143,8 @@ importers: specifier: 6.0.3 version: 6.0.3 typescript-eslint: - specifier: 8.66.0 - version: 8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) + specifier: 8.70.0 + version: 8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) examples/api: dependencies: @@ -210,7 +210,7 @@ importers: version: link:../../plugins/upload '@zerodevx/svelte-json-view': specifier: 2.0.0 - version: 2.0.0(svelte@5.55.7(@typescript-eslint/types@8.66.0)) + version: 2.0.0(svelte@5.55.7) devDependencies: '@iconify-json/codicon': specifier: ^1.2.49 @@ -220,7 +220,7 @@ importers: version: 1.2.2 '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.7(@typescript-eslint/types@8.66.0))(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)) + version: 7.0.0(svelte@5.55.7)(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)) '@tauri-apps/cli': specifier: 2.11.4 version: 2.11.4 @@ -229,7 +229,7 @@ importers: version: 66.6.7 svelte: specifier: ^5.55.7 - version: 5.55.7(@typescript-eslint/types@8.66.0) + version: 5.55.7 unocss: specifier: ^66.6.7 version: 66.6.7(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)) @@ -1253,63 +1253,63 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} - '@typescript-eslint/eslint-plugin@8.66.0': - resolution: {integrity: sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==} + '@typescript-eslint/eslint-plugin@8.70.0': + resolution: {integrity: sha512-/v8HZt6RlyIZxB3ntehELOcUcfxKPVGWXnQdJuHRmzrqgF8nQypcC/oxGW+Ot4VGKDq81XugPKxx0n5PBtf9PA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^8.66.0 + '@typescript-eslint/parser': ^8.70.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@8.66.0': - resolution: {integrity: sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==} + '@typescript-eslint/parser@8.70.0': + resolution: {integrity: sha512-zYvrmj9Yxd63UGaXw+kdt6A0F0s0qveJyuatIM77bYC2DE4pgmg7a50u8LR7PRtXd0x+h+Tl3eXabGm06SWd3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/project-service@8.66.0': - resolution: {integrity: sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==} + '@typescript-eslint/project-service@8.70.0': + resolution: {integrity: sha512-hFHbTNqhU9G+2eKFXCBVb1tjFT/LceiJ4+HfLO4pTpDI0KHi6iajpcFFkaSQ9gXmCh7n82A0PthaayEdN6mspQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@8.66.0': - resolution: {integrity: sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==} + '@typescript-eslint/scope-manager@8.70.0': + resolution: {integrity: sha512-8nP3Kwh5hlgZ4FicGvmznAmJe8UL4sdU8tLukrPaMuQmDuk4Y8xYfzu/aYZW4xT2JCgc7H/TpDI5cGlxcWJSqQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/tsconfig-utils@8.66.0': - resolution: {integrity: sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==} + '@typescript-eslint/tsconfig-utils@8.70.0': + resolution: {integrity: sha512-adnkeeNq9Sq1sUf4+FRVc0KdgYghzsgFpZSQVZVvY0LCuUuN0FnQgyGzCJeC4fW1cdXseBAjU2EOqUIjbNcZUw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/type-utils@8.66.0': - resolution: {integrity: sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==} + '@typescript-eslint/type-utils@8.70.0': + resolution: {integrity: sha512-NUMKIhYVaVIVLnRL9CRt+VVcuLgSHUCpXn4/+K8wql+vdInUzvx8BjUO1oJ7cG9shjFJKtF8F8Hh2kCh3/KBVw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.66.0': - resolution: {integrity: sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==} + '@typescript-eslint/types@8.70.0': + resolution: {integrity: sha512-asTOIYhDg4zdzOScCyaytrsV3cR6B4ecPQlXw/dJIm7J/MZTtCtfVII9JD8Geh4jTCrK/Xe6cg5UevoleMcoJQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@8.66.0': - resolution: {integrity: sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==} + '@typescript-eslint/typescript-estree@8.70.0': + resolution: {integrity: sha512-d9NmHMPEKQ7QCLLm1jI3zmoQBwT5KwFYjXBJ9ymZfKCUU+5rmTRykKAFvH5Qn/ZCds3CEAFS9OC9M/jkl0X2bA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@8.66.0': - resolution: {integrity: sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==} + '@typescript-eslint/utils@8.70.0': + resolution: {integrity: sha512-oZmtKJz/4fufZ2p3+Cn3ijEojcdfR+1zYDH2xKYrEly0dR/Q/1xUPRCOlKGxod78nWlU2UnDe09GZ3TaknBFGA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.66.0': - resolution: {integrity: sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==} + '@typescript-eslint/visitor-keys@8.70.0': + resolution: {integrity: sha512-BoC8PiO4Hkdo0TVJh9Ntxr5MxPDI7/oFsrygN5ADelFSeXG/qgNuucIGA+L5Z6JpPTE/uRfcTWtscjbUaufepQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unocss/cli@66.6.7': @@ -1973,8 +1973,8 @@ packages: type-level-regexp@0.1.17: resolution: {integrity: sha512-wTk4DH3cxwk196uGLK/E9pE45aLfeKJacKmcEgEOA/q5dnPGNxXt0cfYdFxb57L+sEpf1oJH4Dnx/pnRcku9jg==} - typescript-eslint@8.66.0: - resolution: {integrity: sha512-QlEbBPz/RuJ1XUHj29nm3t0F/O/cSlEnntozqPOYHnnTGAXFamnMBu5i9Vn6vhUPHGAjR+Vl+5J8vPN/BMUrJw==} + typescript-eslint@8.70.0: + resolution: {integrity: sha512-P/W5cz70/cQAuKfY3xwQMWWTV7BvJ0mAQmi+9mBcsVPaBUpd6Ohpa+fECv9rBFrQcig86jAiNBFNWUqnTjr4pw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -2531,12 +2531,12 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.7(@typescript-eslint/types@8.66.0))(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1))': + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.7)(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 - svelte: 5.55.7(@typescript-eslint/types@8.66.0) + svelte: 5.55.7 vite: 8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1) vitefu: 1.1.2(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)) @@ -2604,14 +2604,14 @@ snapshots: '@types/trusted-types@2.0.7': {} - '@typescript-eslint/eslint-plugin@8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)': + '@typescript-eslint/eslint-plugin@8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) - '@typescript-eslint/scope-manager': 8.66.0 - '@typescript-eslint/type-utils': 8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) - '@typescript-eslint/utils': 8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.66.0 + '@typescript-eslint/parser': 8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/type-utils': 8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.70.0 eslint: 10.4.0(jiti@2.6.1) ignore: 7.0.5 natural-compare: 1.4.0 @@ -2620,41 +2620,41 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)': + '@typescript-eslint/parser@8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 8.66.0 - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) - '@typescript-eslint/visitor-keys': 8.66.0 + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.70.0 debug: 4.4.3 eslint: 10.4.0(jiti@2.6.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.66.0(typescript@6.0.3)': + '@typescript-eslint/project-service@8.70.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3) - '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3) + '@typescript-eslint/types': 8.70.0 debug: 4.4.3 typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@8.66.0': + '@typescript-eslint/scope-manager@8.70.0': dependencies: - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/visitor-keys': 8.66.0 + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/visitor-keys': 8.70.0 - '@typescript-eslint/tsconfig-utils@8.66.0(typescript@6.0.3)': + '@typescript-eslint/tsconfig-utils@8.70.0(typescript@6.0.3)': dependencies: typescript: 6.0.3 - '@typescript-eslint/type-utils@8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)': + '@typescript-eslint/type-utils@8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) debug: 4.4.3 eslint: 10.4.0(jiti@2.6.1) ts-api-utils: 2.5.0(typescript@6.0.3) @@ -2662,14 +2662,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/types@8.66.0': {} + '@typescript-eslint/types@8.70.0': {} - '@typescript-eslint/typescript-estree@8.66.0(typescript@6.0.3)': + '@typescript-eslint/typescript-estree@8.70.0(typescript@6.0.3)': dependencies: - '@typescript-eslint/project-service': 8.66.0(typescript@6.0.3) - '@typescript-eslint/tsconfig-utils': 8.66.0(typescript@6.0.3) - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/visitor-keys': 8.66.0 + '@typescript-eslint/project-service': 8.70.0(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.70.0(typescript@6.0.3) + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/visitor-keys': 8.70.0 debug: 4.4.3 minimatch: 10.2.4 semver: 7.7.4 @@ -2679,20 +2679,20 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)': + '@typescript-eslint/utils@8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3)': dependencies: '@eslint-community/eslint-utils': 4.9.1(eslint@10.4.0(jiti@2.6.1)) - '@typescript-eslint/scope-manager': 8.66.0 - '@typescript-eslint/types': 8.66.0 - '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.70.0 + '@typescript-eslint/types': 8.70.0 + '@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3) eslint: 10.4.0(jiti@2.6.1) typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/visitor-keys@8.66.0': + '@typescript-eslint/visitor-keys@8.70.0': dependencies: - '@typescript-eslint/types': 8.66.0 + '@typescript-eslint/types': 8.70.0 eslint-visitor-keys: 5.0.1 '@unocss/cli@66.6.7': @@ -2829,9 +2829,9 @@ snapshots: unplugin-utils: 0.3.1 vite: 8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1) - '@zerodevx/svelte-json-view@2.0.0(svelte@5.55.7(@typescript-eslint/types@8.66.0))': + '@zerodevx/svelte-json-view@2.0.0(svelte@5.55.7)': optionalDependencies: - svelte: 5.55.7(@typescript-eslint/types@8.66.0) + svelte: 5.55.7 acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -3003,11 +3003,9 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.2.8(@typescript-eslint/types@8.66.0): + esrap@2.2.8: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - optionalDependencies: - '@typescript-eslint/types': 8.66.0 esrecurse@4.3.0: dependencies: @@ -3389,7 +3387,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte@5.55.7(@typescript-eslint/types@8.66.0): + svelte@5.55.7: dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -3402,7 +3400,7 @@ snapshots: clsx: 2.1.1 devalue: 5.8.1 esm-env: 1.2.2 - esrap: 2.2.8(@typescript-eslint/types@8.66.0) + esrap: 2.2.8 is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 @@ -3445,12 +3443,12 @@ snapshots: type-level-regexp@0.1.17: {} - typescript-eslint@8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3): + typescript-eslint@8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.66.0(@typescript-eslint/parser@8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) - '@typescript-eslint/parser': 8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) - '@typescript-eslint/typescript-estree': 8.66.0(typescript@6.0.3) - '@typescript-eslint/utils': 8.66.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/eslint-plugin': 8.70.0(@typescript-eslint/parser@8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3))(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/parser': 8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) + '@typescript-eslint/typescript-estree': 8.70.0(typescript@6.0.3) + '@typescript-eslint/utils': 8.70.0(eslint@10.4.0(jiti@2.6.1))(typescript@6.0.3) eslint: 10.4.0(jiti@2.6.1) typescript: 6.0.3 transitivePeerDependencies: From 2df3d9368187e1d2ce736d3e0b2d4bdcae35b011 Mon Sep 17 00:00:00 2001 From: Tony <68118705+Legend-Master@users.noreply.github.com> Date: Thu, 17 Sep 2026 16:33:47 +0800 Subject: [PATCH 04/10] refactor(log): log webview location after double colon (#3574) --- .changes/log-webview-double-colon.md | 6 ++++++ plugins/log/src/commands.rs | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 .changes/log-webview-double-colon.md diff --git a/.changes/log-webview-double-colon.md b/.changes/log-webview-double-colon.md new file mode 100644 index 000000000..f59bf1b52 --- /dev/null +++ b/.changes/log-webview-double-colon.md @@ -0,0 +1,6 @@ +--- +"log": patch +"log-js": patch +--- + +Log webview log target locations after 2 colons instead of 1. (e.g. `webview:myFunction@http://localhost:5173/src/myFile.ts?t=1787480723620:47:4` to `webview::myFunction@http://localhost:5173/src/myFile.ts?t=1787480723620:47:4`) diff --git a/plugins/log/src/commands.rs b/plugins/log/src/commands.rs index c402db76e..e64c30a88 100644 --- a/plugins/log/src/commands.rs +++ b/plugins/log/src/commands.rs @@ -20,9 +20,9 @@ pub fn log( let level = log::Level::from(level); let target = if let Some(location) = location { - format!("{WEBVIEW_TARGET}:{location}") + format!("{WEBVIEW_TARGET}::{location}") } else { - WEBVIEW_TARGET.to_string() + WEBVIEW_TARGET.to_owned() }; let mut builder = RecordBuilder::new(); From 67cd25a10ce3deb1b935e725228e528766f24ad7 Mon Sep 17 00:00:00 2001 From: PathGao <42336971+PathGao@users.noreply.github.com> Date: Fri, 18 Sep 2026 01:23:13 +0800 Subject: [PATCH 05/10] fix(single-instance): let the first instance come to front on Windows (#3592) * fix(single-instance): let the first instance come to front on Windows * move the hand-over right after the window lookup --- .changes/single-instance-foreground-rights.md | 5 +++++ .../src/platform_impl/windows.rs | 19 +++++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 .changes/single-instance-foreground-rights.md diff --git a/.changes/single-instance-foreground-rights.md b/.changes/single-instance-foreground-rights.md new file mode 100644 index 000000000..57794e69b --- /dev/null +++ b/.changes/single-instance-foreground-rights.md @@ -0,0 +1,5 @@ +--- +"single-instance": patch +--- + +On Windows, the second instance now allows the first instance to bring its window to the front before exiting, so focusing a window from the callback no longer gets refused by Windows. diff --git a/plugins/single-instance/src/platform_impl/windows.rs b/plugins/single-instance/src/platform_impl/windows.rs index d55ec079c..196f7851f 100644 --- a/plugins/single-instance/src/platform_impl/windows.rs +++ b/plugins/single-instance/src/platform_impl/windows.rs @@ -19,10 +19,11 @@ use windows_sys::Win32::{ Threading::{CreateMutexW, ReleaseMutex}, }, UI::WindowsAndMessaging::{ - self as w32wm, CreateWindowExW, DefWindowProcW, DestroyWindow, FindWindowW, - RegisterClassExW, SendMessageW, CREATESTRUCTW, GWLP_USERDATA, GWL_STYLE, - WINDOW_LONG_PTR_INDEX, WM_COPYDATA, WM_CREATE, WM_DESTROY, WNDCLASSEXW, WS_EX_LAYERED, - WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, + self as w32wm, AllowSetForegroundWindow, CreateWindowExW, DefWindowProcW, DestroyWindow, + FindWindowW, GetWindowThreadProcessId, RegisterClassExW, SendMessageW, CREATESTRUCTW, + GWLP_USERDATA, GWL_STYLE, WINDOW_LONG_PTR_INDEX, WM_COPYDATA, WM_CREATE, WM_DESTROY, + WNDCLASSEXW, WS_EX_LAYERED, WS_EX_NOACTIVATE, WS_EX_TOOLWINDOW, WS_EX_TRANSPARENT, + WS_OVERLAPPED, WS_POPUP, WS_VISIBLE, }, }; @@ -74,6 +75,16 @@ pub fn init(callback: Box>) -> TauriPlugin let hwnd = FindWindowW(class_name.as_ptr(), window_name.as_ptr()); if !hwnd.is_null() { + // Windows lets us bring a window to the front, but not the first + // instance. Hand that right over before we exit, so focusing a window + // from the callback works. Windows takes it back if the user switches + // to another app in the meantime. + let mut pid = 0; + GetWindowThreadProcessId(hwnd, &mut pid); + if pid != 0 { + AllowSetForegroundWindow(pid); + } + let cwd = std::env::current_dir().unwrap_or_default(); let cwd = cwd.to_str().unwrap_or_default(); From 690dcfd694388a40bf7ed8b0f8797942238f6f28 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Sat, 19 Sep 2026 20:57:51 -0300 Subject: [PATCH 06/10] Merge commit from fork * feat(updater): verify the version an update was signed for The endpoint response is fetched over TLS but is not signed, and the signature only covers the artifact, so a crafted response could pair an inflated version with an older release's url and signature to force a downgrade to a genuine but outdated build. Read the version back from the signature's trusted comment and reject an update whose announced version differs. Signatures carrying no version are only rejected under the new requireSignedVersion option, since older CLIs did not record one. * fix tests --- .changes/updater-require-signed-version.md | 23 ++ plugins/updater/src/config.rs | 22 ++ plugins/updater/src/error.rs | 16 + plugins/updater/src/updater.rs | 129 ++++++- .../updater/tests/app-updater/tests/update.rs | 351 ++++++++++++++++-- 5 files changed, 507 insertions(+), 34 deletions(-) create mode 100644 .changes/updater-require-signed-version.md diff --git a/.changes/updater-require-signed-version.md b/.changes/updater-require-signed-version.md new file mode 100644 index 000000000..be0e47d77 --- /dev/null +++ b/.changes/updater-require-signed-version.md @@ -0,0 +1,23 @@ +--- +"updater": minor +--- + +Add a `requireSignedVersion` configuration option that binds an update to the version it was signed for. + +The update endpoint response is fetched over TLS but is not itself signed, and the signature only covers the downloaded artifact. Anyone able to serve a crafted response could therefore pair an inflated `version` field with the `url` and `signature` of an older release and force a downgrade to a genuine but outdated build, because that older artifact carries a valid signature. + +The Tauri CLI records the version in the signature's trusted comment, which the signature covers. With this option enabled the updater compares that signed version against the one announced by the endpoint and rejects the update when they differ: + +```json +{ + "plugins": { + "updater": { + "requireSignedVersion": true + } + } +} +``` + +It defaults to `false` because releases signed before the CLI started recording the version carry none and would be rejected. Re-sign and re-publish every release your users can still update from before enabling it, otherwise an older signature can still be served to bypass the check. When the signature does carry a version, a mismatch is rejected whether or not the option is enabled. + +This is checked independently of `allowDowngrades`: it constrains which artifact a given version number may resolve to, not whether that version is newer than the running one. diff --git a/plugins/updater/src/config.rs b/plugins/updater/src/config.rs index 411956b9b..0fa97e8de 100644 --- a/plugins/updater/src/config.rs +++ b/plugins/updater/src/config.rs @@ -114,6 +114,25 @@ pub struct Config { pub endpoints: Vec, /// Signature public key. pub pubkey: String, + /// Require the update signature to carry the version it was signed for, and reject the + /// update when that version differs from the one announced by the update endpoint. + /// + /// The endpoint response is fetched over TLS but is not itself signed, and the signature + /// only covers the downloaded artifact. Without this flag, anyone able to serve a crafted + /// response can pair an inflated `version` field with the `url` and `signature` of an + /// older release and force a downgrade to a genuine but outdated build, since that older + /// artifact carries a valid signature. + /// + /// The signed version is read from the signature's trusted comment, which is covered by + /// the signature. Releases signed before the Tauri CLI started recording it carry no + /// version, so enabling this rejects them. Re-sign and re-publish every release your users + /// can still update from before turning this on. + /// + /// This is checked independently of the version comparison: it constrains which artifact a + /// given version number may resolve to, not whether that version is newer. + /// + /// The default value of this flag is `false`. + pub require_signed_version: bool, /// The Windows configuration for the updater. pub windows: Option, } @@ -135,6 +154,8 @@ impl<'de> Deserialize<'de> for Config { #[serde(default)] pub endpoints: Vec, pub pubkey: String, + #[serde(default, alias = "require-signed-version")] + pub require_signed_version: bool, pub windows: Option, } @@ -152,6 +173,7 @@ impl<'de> Deserialize<'de> for Config { dangerous_accept_invalid_hostnames: config.dangerous_accept_invalid_hostnames, endpoints: config.endpoints, pubkey: config.pubkey, + require_signed_version: config.require_signed_version, windows: config.windows, }) } diff --git a/plugins/updater/src/error.rs b/plugins/updater/src/error.rs index c44f98627..6621fe553 100644 --- a/plugins/updater/src/error.rs +++ b/plugins/updater/src/error.rs @@ -89,6 +89,22 @@ pub enum Error { /// The configured updater endpoint must use a secure protocol like `https` #[error("The configured updater endpoint must use a secure protocol like `https`.")] InsecureTransportProtocol, + /// The version the artifact was signed for does not match the version announced by the + /// update endpoint. + #[error( + "The update was signed for version {signed} but the update endpoint announced version {announced}. The endpoint response may have been tampered with to force installing a different release." + )] + SignedVersionMismatch { + /// The version read from the signature's trusted comment. + signed: String, + /// The version announced by the update endpoint. + announced: String, + }, + /// `requireSignedVersion` is enabled but the signature does not carry a version. + #[error( + "The update signature does not specify the version it was signed for, which `requireSignedVersion` requires. Re-sign and re-publish this release, or disable `requireSignedVersion`." + )] + MissingSignedVersion, #[error(transparent)] Tauri(#[from] tauri::Error), } diff --git a/plugins/updater/src/updater.rs b/plugins/updater/src/updater.rs index e877820a7..cf65df501 100644 --- a/plugins/updater/src/updater.rs +++ b/plugins/updater/src/updater.rs @@ -737,7 +737,13 @@ impl Update { } on_download_finish(); - verify_signature(&buffer, &self.signature, &self.context.config.pubkey)?; + verify_signature( + &buffer, + &self.signature, + &self.context.config.pubkey, + &self.version, + self.context.config.require_signed_version, + )?; Ok(buffer) } @@ -1521,7 +1527,13 @@ where } // Validate signature -fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Result<()> { +fn verify_signature( + data: &[u8], + release_signature: &str, + pub_key: &str, + announced_version: &str, + require_signed_version: bool, +) -> Result<()> { // we need to convert the pub key let pub_key_decoded = base64_to_string(pub_key)?; let public_key = PublicKey::decode(&pub_key_decoded)?; @@ -1530,7 +1542,65 @@ fn verify_signature(data: &[u8], release_signature: &str, pub_key: &str) -> Resu // Validate signature or bail out public_key.verify(data, &signature, true)?; - Ok(()) + + // Only now is the trusted comment usable: minisign's global signature covers it, and + // `verify` above is what checks that global signature. Reading it before this point would + // be trusting attacker controlled data. + verify_signed_version( + signature.trusted_comment(), + announced_version, + require_signed_version, + ) +} + +/// Checks the version the artifact was signed for against the version the update endpoint +/// announced. +/// +/// The endpoint response is not signed, so its `version` field on its own does not prove which +/// release the `url` and `signature` actually point at. Comparing it against the signed version +/// is what stops a tampered response from pairing a new version number with an older release. +fn verify_signed_version( + trusted_comment: &str, + announced_version: &str, + require_signed_version: bool, +) -> Result<()> { + let Some(signed_version) = signed_version(trusted_comment) else { + // Signatures produced before the Tauri CLI started recording the version carry none, so + // this can only be enforced when the app opts in. Note that leaving it off means an + // attacker can bypass the check outright by serving one of those older signatures. + return if require_signed_version { + Err(Error::MissingSignedVersion) + } else { + Ok(()) + }; + }; + + // compare as semver so that equivalent spellings like `1.2.3` and `v1.2.3` match, falling + // back to a literal comparison for versions that are not valid semver + let matches = match ( + Version::from_str(signed_version.trim_start_matches('v')), + Version::from_str(announced_version.trim_start_matches('v')), + ) { + (Ok(signed), Ok(announced)) => signed == announced, + _ => signed_version == announced_version, + }; + + if matches { + Ok(()) + } else { + Err(Error::SignedVersionMismatch { + signed: signed_version.to_string(), + announced: announced_version.to_string(), + }) + } +} + +/// Reads the `version` field out of a signature's trusted comment, which the Tauri CLI writes as +/// tab separated `key:value` pairs, e.g. `timestamp:1700000000\tfile:app.tar.gz\tversion:1.2.3`. +fn signed_version(trusted_comment: &str) -> Option<&str> { + trusted_comment + .split('\t') + .find_map(|field| field.strip_prefix("version:")) } fn base64_to_string(base64_string: &str) -> Result { @@ -1636,6 +1706,59 @@ fn escape_msi_property_arg(arg: impl AsRef) -> String { #[cfg(test)] mod tests { + use super::{signed_version, verify_signed_version}; + use crate::error::Error; + + const CURRENT: &str = "timestamp:1700000000\tfile:app_1.2.3_x64.msi.zip\tversion:1.2.3"; + // signatures produced before the CLI started embedding the version + const LEGACY: &str = "timestamp:1600000000\tfile:app_1.0.0_x64.msi.zip"; + + #[test] + fn reads_the_signed_version() { + assert_eq!(signed_version(CURRENT), Some("1.2.3")); + assert_eq!(signed_version(LEGACY), None); + // must not match on a field that merely ends in `version:` + assert_eq!(signed_version("timestamp:1\tfile:app-version:2.zip"), None); + } + + #[test] + fn accepts_a_matching_version() { + assert!(verify_signed_version(CURRENT, "1.2.3", true).is_ok()); + assert!(verify_signed_version(CURRENT, "1.2.3", false).is_ok()); + // the endpoint and the CLI may spell the same version differently + assert!(verify_signed_version(CURRENT, "v1.2.3", true).is_ok()); + } + + #[test] + fn rejects_a_version_the_artifact_was_not_signed_for() { + // the rollback the flag exists to stop: an old artifact announced as a new version + let err = verify_signed_version(CURRENT, "9.9.9", false).unwrap_err(); + assert!( + matches!(err, Error::SignedVersionMismatch { ref signed, ref announced } + if signed == "1.2.3" && announced == "9.9.9"), + "unexpected error: {err}" + ); + // rejected regardless of whether the app opted in, since the signature does say + // which version it covers + assert!(verify_signed_version(CURRENT, "9.9.9", true).is_err()); + assert!(verify_signed_version(CURRENT, "1.2.4", true).is_err()); + } + + #[test] + fn only_requires_a_signed_version_when_configured() { + assert!(verify_signed_version(LEGACY, "9.9.9", false).is_ok()); + assert!(matches!( + verify_signed_version(LEGACY, "9.9.9", true).unwrap_err(), + Error::MissingSignedVersion + )); + } + + #[test] + fn compares_non_semver_versions_literally() { + let comment = "timestamp:1700000000\tfile:app.zip\tversion:2024-01-01"; + assert!(verify_signed_version(comment, "2024-01-01", true).is_ok()); + assert!(verify_signed_version(comment, "2024-01-02", true).is_err()); + } #[test] #[cfg(windows)] diff --git a/plugins/updater/tests/app-updater/tests/update.rs b/plugins/updater/tests/app-updater/tests/update.rs index acaee63c8..34020578d 100644 --- a/plugins/updater/tests/app-updater/tests/update.rs +++ b/plugins/updater/tests/app-updater/tests/update.rs @@ -9,12 +9,16 @@ use std::{ fs::File, path::{Path, PathBuf}, process::Command, - sync::Arc, + sync::{Arc, Mutex}, }; use serde::Serialize; use tauri::utils::config::{Updater, V1Compatible}; +/// Every test here drives `cargo tauri build` against the same crate and target directory, and +/// binds a fixed port, so they cannot overlap. +static BUILD_LOCK: Mutex<()> = Mutex::new(()); + const UPDATER_PRIVATE_KEY: &str = "dW50cnVzdGVkIGNvbW1lbnQ6IHJzaWduIGVuY3J5cHRlZCBzZWNyZXQga2V5ClJXUlRZMEl5TlFOMFpXYzJFOUdjeHJEVXY4WE1TMUxGNDJVUjNrMmk1WlR3UVJVUWwva0FBQkFBQUFBQUFBQUFBQUlBQUFBQUpVK3ZkM3R3eWhyN3hiUXhQb2hvWFVzUW9FbEs3NlNWYjVkK1F2VGFRU1FEaGxuRUtlell5U0gxYS9DbVRrS0YyZVJGblhjeXJibmpZeGJjS0ZKSUYwYndYc2FCNXpHalM3MHcrODMwN3kwUG9SOWpFNVhCSUd6L0E4TGRUT096TEtLR1JwT1JEVFU9Cg=="; const UPDATED_EXIT_CODE: i32 = 0; const ERROR_EXIT_CODE: i32 = 1; @@ -24,6 +28,10 @@ const UP_TO_DATE_EXIT_CODE: i32 = 2; struct Config { version: &'static str, bundle: BundleConfig, + /// Merged into `plugins` of the app's `tauri.conf.json`. Only set by the signed version + /// tests, which need to flip `requireSignedVersion` and point at their own server. + #[serde(skip_serializing_if = "Option::is_none")] + plugins: Option, } #[derive(Serialize)] @@ -35,13 +43,13 @@ struct BundleConfig { #[derive(Serialize)] struct PlatformUpdate { signature: String, - url: &'static str, + url: String, with_elevated_task: bool, } #[derive(Serialize)] struct Update { - version: &'static str, + version: String, date: String, platforms: HashMap, } @@ -56,6 +64,12 @@ fn build_app(cwd: &Path, config: &Config, target: Option) { .env("TAURI_SIGNING_PRIVATE_KEY_PASSWORD", "") .current_dir(cwd); + // linuxdeploy ships a `strip` that does not know the `.relr.dyn` sections recent distros emit, + // and it aborts the whole bundle when a strip call fails. Nothing here benefits from a smaller + // AppImage, so skip it. + #[cfg(target_os = "linux")] + command.env("NO_STRIP", "1"); + if let Some(target) = target { command.arg("--bundles").arg(target.name()); } else { @@ -107,6 +121,61 @@ impl Default for BundleTarget { } } +/// Copies a bundle out of the bundler output directory, returning the path to run. +/// +/// Every bundler wipes its output before writing — the AppImage one removes `bundle/appimage` +/// entirely, the macOS one the `.app` — so the app built for the initial version only survives +/// until the next `build_app` call. On Linux and macOS the update is also installed over the very +/// bundle being run, which would otherwise leave a 1.0.0 app behind in the bundler output. +/// +/// Windows drives the executable `cargo build` leaves in `target/release`, which no bundler +/// touches, so there is nothing to copy there. +fn stage_app_under_test(root_dir: &Path, target: &str) -> PathBuf { + #[cfg(windows)] + { + let _ = target; + return root_dir.join("target/release/app-updater.exe"); + } + + #[cfg(not(windows))] + { + let bundle_path = test_cases(root_dir, "0.1.0", target.to_string()) + .first() + .unwrap() + .1 + .clone(); + + let staging_dir = root_dir.join("target/release/app-under-test"); + let _ = std::fs::remove_dir_all(&staging_dir); + std::fs::create_dir_all(&staging_dir).expect("failed to create the staging directory"); + + let staged = staging_dir.join(bundle_path.file_name().unwrap()); + copy_recursively(&bundle_path, &staged).unwrap_or_else(|e| { + panic!( + "failed to copy {} to {}: {e}", + bundle_path.display(), + staged.display() + ) + }); + + return staged; + } +} + +/// Copies a file, or a directory such as a macOS `.app`, preserving permissions. +fn copy_recursively(from: &Path, to: &Path) -> std::io::Result<()> { + if from.is_dir() { + std::fs::create_dir_all(to)?; + for entry in std::fs::read_dir(from)? { + let entry = entry?; + copy_recursively(&entry.path(), &to.join(entry.file_name()))?; + } + Ok(()) + } else { + std::fs::copy(from, to).map(|_| ()) + } +} + fn target_to_platforms( update_platform: Option, signature: String, @@ -117,7 +186,7 @@ fn target_to_platforms( platform, PlatformUpdate { signature, - url: "http://localhost:3007/download", + url: "http://localhost:3007/download".into(), with_elevated_task: false, }, ); @@ -270,6 +339,8 @@ fn test_cases( #[test] fn update_app() { + let _lock = BUILD_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + let target = tauri_plugin_updater::target().expect("running updater test in an unsupported platform"); let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); @@ -281,12 +352,14 @@ fn update_app() { bundle: BundleConfig { create_updater_artifacts: Updater::Bool(true), }, + plugins: None, }, Config { version: "1.0.0", bundle: BundleConfig { create_updater_artifacts: Updater::String(V1Compatible::V1Compatible), }, + plugins: None, }, ] { let v1_compatible = matches!( @@ -359,7 +432,7 @@ fn update_app() { target_to_platforms(update_platform.clone(), signature.clone()); let body = serde_json::to_vec(&Update { - version: "1.0.0", + version: "1.0.0".into(), date: time::OffsetDateTime::now_utc() .format(&time::format_description::well_known::Rfc3339) .unwrap(), @@ -393,41 +466,38 @@ fn update_app() { config.version = "0.1.0"; - // bundle initial app version - build_app(&manifest_dir, &config, None); + // bundle initial app version; Linux and macOS run the bundle itself, so it cannot be + // skipped there + build_app( + &manifest_dir, + &config, + if cfg!(windows) { + None + } else { + Some(bundle_target) + }, + ); + + let app_path = stage_app_under_test(&root_dir, &target); for expected_exit_code in status_checks { - let mut binary_cmd = if cfg!(windows) { - Command::new(root_dir.join("target/release/app-updater.exe")) - } else if cfg!(target_os = "macos") { - Command::new( - test_cases(&root_dir, "0.1.0", target.clone()) - .first() - .unwrap() - .1 - .join("Contents/MacOS/app-updater"), - ) - } else if std::env::var("CI").map(|v| v == "true").unwrap_or_default() { + let mut binary_cmd = if cfg!(target_os = "macos") { + Command::new(app_path.join("Contents/MacOS/app-updater")) + } else if cfg!(target_os = "linux") + && std::env::var("CI").map(|v| v == "true").unwrap_or_default() + { let mut c = Command::new("xvfb-run"); - c.arg("--auto-servernum").arg( - &test_cases(&root_dir, "0.1.0", target.clone()) - .first() - .unwrap() - .1, - ); + c.arg("--auto-servernum").arg(&app_path); c } else { - Command::new( - &test_cases(&root_dir, "0.1.0", target.clone()) - .first() - .unwrap() - .1, - ) + Command::new(&app_path) }; binary_cmd.env("TARGET", bundle_target.name()); - let status = binary_cmd.status().expect("failed to run app"); + let status = binary_cmd + .status() + .unwrap_or_else(|e| panic!("failed to run {}: {e}", app_path.display())); let code = status.code().unwrap_or(-1); if code != expected_exit_code { @@ -447,3 +517,222 @@ fn update_app() { } } } + +const SIGNED_VERSION_PORT: u16 = 3008; +/// Fragment of `Error::SignedVersionMismatch`. The app prints the updater error before exiting, +/// and a rejected update exits with the same code as a failed install, so the message is what +/// tells them apart. +const MISMATCH_ERROR: &str = "was signed for version"; + +struct ServedUpdate { + version: String, +} + +fn app_binary(root_dir: &Path) -> PathBuf { + root_dir.join(if cfg!(windows) { + "target/release/app-updater.exe" + } else { + "target/release/app-updater" + }) +} + +/// Runs the app against the update server, restoring the binary from `pristine` first. +/// +/// A case that clears the version check goes on to actually install, and on Linux installing means +/// writing the downloaded bytes over the running executable. Every case therefore has to start +/// from an untouched binary, or the first one that passes leaves the update behind as the app. +fn run_app(root_dir: &Path, pristine: &Path) -> String { + let binary = app_binary(root_dir); + std::fs::copy(pristine, &binary).expect("failed to restore the app binary"); + + let mut command = if cfg!(target_os = "linux") + && std::env::var("CI").map(|v| v == "true").unwrap_or_default() + { + let mut c = Command::new("xvfb-run"); + c.arg("--auto-servernum").arg(&binary); + c + } else { + Command::new(&binary) + }; + + let output = command.output().expect("failed to run app"); + String::from_utf8_lossy(&output.stdout).into_owned() +} + +/// The update endpoint response is not signed, so its `version` field alone does not prove which +/// release the `url` and `signature` point at. This bundles a genuine 1.0.0 release and then varies +/// only the version the endpoint announces for it, which is the shape of a forced downgrade: a +/// tampered response pairing a new version number with an older release's signature. +/// +/// The check runs in `Update::download`, before anything is installed, so a rejected case never +/// reaches the installer while an accepted one does. Both exit non-zero, which is why these assert +/// on the error message rather than the exit code. +#[test] +fn update_validates_signed_version() { + let _lock = BUILD_LOCK.lock().unwrap_or_else(|e| e.into_inner()); + + let target = + tauri_plugin_updater::target().expect("running updater test in an unsupported platform"); + let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let root_dir = manifest_dir.join("../../../.."); + + // bundle the release the endpoint will point at; the CLI records 1.0.0 in its signature + let bundle_target = BundleTarget::default(); + build_app( + &manifest_dir, + &Config { + version: "1.0.0", + bundle: BundleConfig { + create_updater_artifacts: Updater::Bool(true), + }, + plugins: None, + }, + Some(bundle_target), + ); + + let out_bundle_path = test_cases(&root_dir, "1.0.0", target.clone()) + .first() + .unwrap() + .1 + .clone(); + let updater_extension = { + let bundle_ext = out_bundle_path + .extension() + .unwrap() + .to_str() + .unwrap() + .to_string(); + if cfg!(target_os = "macos") { + format!("{bundle_ext}.tar.gz") + } else { + bundle_ext + } + }; + let signature = + std::fs::read_to_string(out_bundle_path.with_extension(format!("{updater_extension}.sig"))) + .expect("failed to read signature file"); + + // move it aside so building the running app cannot clobber it + let out_updater_path = out_bundle_path.with_extension(&updater_extension); + let updater_path = root_dir.join(format!( + "target/release/{}", + out_updater_path.file_name().unwrap().to_str().unwrap() + )); + std::fs::rename(&out_updater_path, &updater_path).expect("failed to rename bundle"); + + let served = Arc::new(Mutex::new(ServedUpdate { + version: "1.0.0".into(), + })); + + let server = Arc::new( + tiny_http::Server::http(format!("localhost:{SIGNED_VERSION_PORT}")) + .expect("failed to start updater server"), + ); + + let server_ = server.clone(); + let served_ = served.clone(); + let target_ = target.clone(); + let updater_path_ = updater_path.clone(); + std::thread::spawn(move || { + for request in server_.incoming_requests() { + match request.url() { + "/" => { + let mut platforms = HashMap::new(); + platforms.insert( + target_.clone(), + PlatformUpdate { + // always the genuine 1.0.0 signature; only the version above it moves + signature: signature.clone(), + url: format!("http://localhost:{SIGNED_VERSION_PORT}/download"), + with_elevated_task: false, + }, + ); + + let body = serde_json::to_vec(&Update { + version: served_.lock().unwrap().version.clone(), + date: time::OffsetDateTime::now_utc() + .format(&time::format_description::well_known::Rfc3339) + .unwrap(), + platforms, + }) + .unwrap(); + let len = body.len(); + let _ = request.respond(tiny_http::Response::new( + tiny_http::StatusCode(200), + Vec::new(), + std::io::Cursor::new(body), + Some(len), + None, + )); + } + "/download" => { + let _ = request.respond(tiny_http::Response::from_file( + File::open(&updater_path_).unwrap_or_else(|_| { + panic!("failed to open updater bundle {}", updater_path_.display()) + }), + )); + } + _ => (), + } + } + }); + + // `requireSignedVersion` is baked in by `generate_context!`, so each value needs its own build + let pristine = root_dir.join("target/release/app-updater.pristine"); + let build = |require_signed_version: bool| { + build_app( + &manifest_dir, + &Config { + version: "0.1.0", + bundle: BundleConfig { + create_updater_artifacts: Updater::Bool(true), + }, + plugins: Some(serde_json::json!({ + "updater": { + "endpoints": [format!("http://localhost:{SIGNED_VERSION_PORT}")], + "requireSignedVersion": require_signed_version, + } + })), + }, + None, + ); + std::fs::copy(app_binary(&root_dir), &pristine) + .expect("failed to keep a pristine copy of the app binary"); + }; + + let check = |announced: &str, expected: Option<&str>, unexpected: Option<&str>| { + served.lock().unwrap().version = announced.to_string(); + let output = run_app(&root_dir, &pristine); + + if let Some(expected) = expected { + assert!( + output.contains(expected), + "expected {expected:?} when announcing {announced} for a 1.0.0 signature, got: {output}" + ); + } + if let Some(unexpected) = unexpected { + assert!( + !output.contains(unexpected), + "unexpected {unexpected:?} when announcing {announced} for a 1.0.0 signature, got: {output}" + ); + } + }; + + build(true); + + // the rollback this exists to stop: a 1.0.0 release dressed up as a newer one + check("1.5.0", Some(MISMATCH_ERROR), None); + // the announced version is what the artifact was signed for, so it must go through + check("1.0.0", None, Some(MISMATCH_ERROR)); + + build(false); + + // a signature that names a version is held to it whether or not the option is on + check("1.5.0", Some(MISMATCH_ERROR), None); + + server.unblock(); + + // leave a runnable binary behind rather than whichever update was installed last + let _ = std::fs::copy(&pristine, app_binary(&root_dir)); + let _ = std::fs::remove_file(&pristine); +} From 1308bfa399b962b3977c767100a6339d1cbfdd20 Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Sat, 19 Sep 2026 21:00:36 -0300 Subject: [PATCH 07/10] Merge commit from fork --- .changes/updater-allow-downgrades-config.md | 20 ++++++++++++++++++++ plugins/updater/guest-js/index.ts | 4 ---- plugins/updater/src/commands.rs | 4 ---- plugins/updater/src/config.rs | 16 ++++++++++++++++ plugins/updater/src/lib.rs | 5 +++++ 5 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 .changes/updater-allow-downgrades-config.md diff --git a/.changes/updater-allow-downgrades-config.md b/.changes/updater-allow-downgrades-config.md new file mode 100644 index 000000000..365cd956c --- /dev/null +++ b/.changes/updater-allow-downgrades-config.md @@ -0,0 +1,20 @@ +--- +"updater": minor +"updater-js": minor +--- + +**Breaking change:** the `allowDowngrades` option was removed from the `check` command and is now read from the plugin configuration instead. + +Previously any code running in the webview could pass `allowDowngrades: true` to `plugin:updater|check` and relax the version check from "the update must be newer" to "the update must be different", overriding the comparator the application had configured on the Rust side. The flag is now an application-level setting: + +```json +{ + "plugins": { + "updater": { + "allowDowngrades": true + } + } +} +``` + +It defaults to `false`, and is ignored when the application provides its own `Builder::default_version_comparator`, which continues to take precedence. diff --git a/plugins/updater/guest-js/index.ts b/plugins/updater/guest-js/index.ts index d53165eb2..c0f023bbb 100644 --- a/plugins/updater/guest-js/index.ts +++ b/plugins/updater/guest-js/index.ts @@ -22,10 +22,6 @@ interface CheckOptions { * Target identifier for the running application. This is sent to the backend. */ target?: string - /** - * Allow downgrades to previous versions by not checking if the current version is greater than the available version. - */ - allowDowngrades?: boolean } /** Options used when downloading an update */ diff --git a/plugins/updater/src/commands.rs b/plugins/updater/src/commands.rs index edc1b88fb..72684e6d8 100644 --- a/plugins/updater/src/commands.rs +++ b/plugins/updater/src/commands.rs @@ -46,7 +46,6 @@ pub(crate) async fn check( timeout: Option, proxy: Option, target: Option, - allow_downgrades: Option, ) -> Result> { let mut builder = webview.updater_builder(); if let Some(headers) = headers { @@ -64,9 +63,6 @@ pub(crate) async fn check( if let Some(target) = target { builder = builder.target(target); } - if allow_downgrades.unwrap_or(false) { - builder = builder.version_comparator(|current, update| update.version != current); - } let updater = builder.build()?; let update = updater.check().await?; diff --git a/plugins/updater/src/config.rs b/plugins/updater/src/config.rs index 0fa97e8de..9bd941ad5 100644 --- a/plugins/updater/src/config.rs +++ b/plugins/updater/src/config.rs @@ -133,6 +133,19 @@ pub struct Config { /// /// The default value of this flag is `false`. pub require_signed_version: bool, + /// Allow the updater to install a release whose version is not newer than the + /// currently running one, changing the version check from "must be newer" to + /// "must be different". + /// + /// Note that the updater only verifies the signature of the downloaded artifact, + /// not the version advertised by the update endpoint, so enabling this removes the + /// only guard against installing a previously released (and validly signed) version. + /// + /// Ignored when the application sets a custom + /// [`Builder::default_version_comparator`](crate::Builder::default_version_comparator). + /// + /// The default value of this flag is `false`. + pub allow_downgrades: bool, /// The Windows configuration for the updater. pub windows: Option, } @@ -156,6 +169,8 @@ impl<'de> Deserialize<'de> for Config { pub pubkey: String, #[serde(default, alias = "require-signed-version")] pub require_signed_version: bool, + #[serde(default, alias = "allow-downgrades")] + pub allow_downgrades: bool, pub windows: Option, } @@ -174,6 +189,7 @@ impl<'de> Deserialize<'de> for Config { endpoints: config.endpoints, pubkey: config.pubkey, require_signed_version: config.require_signed_version, + allow_downgrades: config.allow_downgrades, windows: config.windows, }) } diff --git a/plugins/updater/src/lib.rs b/plugins/updater/src/lib.rs index adb4c01ce..18f2a11a5 100644 --- a/plugins/updater/src/lib.rs +++ b/plugins/updater/src/lib.rs @@ -98,6 +98,11 @@ impl> UpdaterExt for T { builder.version_comparator = version_comparator.clone(); + // a comparator set by the application takes precedence over the configuration + if builder.version_comparator.is_none() && config.allow_downgrades { + builder = builder.version_comparator(|current, update| update.version != current); + } + #[cfg(any( target_os = "linux", target_os = "dragonfly", From 1198a524b710abf2abeb1d9bd7b252402d26ca6d Mon Sep 17 00:00:00 2001 From: Lucas Fernandes Nogueira Date: Sat, 19 Sep 2026 21:01:48 -0300 Subject: [PATCH 08/10] Merge commit from fork Checks the URL scope on every hop of a redirect chain instead of only on the URL requested by the frontend. Without it, a server on an allowed origin can redirect the request to any other origin - including `localhost` services, internal hosts and cloud metadata endpoints - and the plugin follows it, returning the response to the webview. --- .changes/http-redirect-scope.md | 19 ++ plugins/http/guest-js/index.ts | 4 + plugins/http/src/commands.rs | 430 ++++++++++++++++++++++++-------- plugins/http/src/config.rs | 47 ++++ plugins/http/src/lib.rs | 25 +- plugins/http/src/scope.rs | 53 ++-- 6 files changed, 448 insertions(+), 130 deletions(-) create mode 100644 .changes/http-redirect-scope.md create mode 100644 plugins/http/src/config.rs diff --git a/.changes/http-redirect-scope.md b/.changes/http-redirect-scope.md new file mode 100644 index 000000000..58e4616c1 --- /dev/null +++ b/.changes/http-redirect-scope.md @@ -0,0 +1,19 @@ +--- +"http": minor +--- + +**Security:** Added the `scopeRedirects` plugin configuration option, which checks the URL scope on every hop of a redirect chain instead of only on the URL requested by the frontend. Without it, a server on an allowed origin can redirect the request to any other origin - including `localhost` services, internal hosts and cloud metadata endpoints - and the plugin follows it, returning the response to the webview. + +```json +{ + "plugins": { + "http": { + "scopeRedirects": true + } + } +} +``` + +It is opt-in because a redirect to a URL that is not allowed by the scope now fails with `url not allowed on the configured scope` instead of being followed, so applications that rely on being redirected outside of their scope must add the redirect target to the scope. **This will become the default in v3.** + +Note that `tauri_plugin_http::init()` now returns `TauriPlugin>` instead of `TauriPlugin`. diff --git a/plugins/http/guest-js/index.ts b/plugins/http/guest-js/index.ts index 9426e35fe..5406e03f2 100644 --- a/plugins/http/guest-js/index.ts +++ b/plugins/http/guest-js/index.ts @@ -76,6 +76,10 @@ export interface ClientOptions { /** * Defines the maximum number of redirects the client should follow. * If set to 0, no redirects will be followed. + * + * When the `scopeRedirects` plugin configuration is enabled, every redirect must + * also be allowed by the configured scope, otherwise the request fails + * instead of being followed. */ maxRedirections?: number /** Timeout in milliseconds */ diff --git a/plugins/http/src/commands.rs b/plugins/http/src/commands.rs index 08df1509d..4387bae30 100644 --- a/plugins/http/src/commands.rs +++ b/plugins/http/src/commands.rs @@ -22,6 +22,10 @@ use crate::{ const HTTP_USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION"),); +/// Matches the default [`reqwest`] redirect policy, used when the frontend does not +/// configure `maxRedirections`. +const DEFAULT_MAX_REDIRECTIONS: usize = 10; + struct ReqwestResponse(reqwest::Response); impl tauri::Resource for ReqwestResponse {} @@ -174,6 +178,56 @@ fn attach_proxy( Ok(builder) } +/// Builds the redirect policy for the request. +/// +/// When `scope` is [`Some`], **every** hop is checked against it. Validating only the URL the +/// frontend asked for is not enough: a server on an allowed origin can answer with a redirect to +/// any other origin (an open redirect, or a server the attacker controls), and following it would +/// give the webview access to a URL the scope denies. +/// +/// That check is opt-in through the `scopeRedirects` configuration because it breaks applications +/// that rely on being redirected outside of their scope. +// TODO(v3): always check the scope and take it by value instead of `Option` +fn redirect_policy(scope: Option, max_redirections: Option) -> Policy { + if max_redirections == Some(0) { + return Policy::none(); + } + + let max_redirections = max_redirections.unwrap_or(DEFAULT_MAX_REDIRECTIONS); + + let Some(scope) = scope else { + return Policy::limited(max_redirections); + }; + + Policy::custom(move |attempt| { + // the first URL in `previous` is the initial request, so it must be excluded + if attempt.previous().len() > max_redirections { + attempt.error("too many redirects") + } else if scope.is_allowed(attempt.url()) { + attempt.follow() + } else { + let url = attempt.url().clone(); + attempt.error(Error::UrlNotAllowed(url)) + } + }) +} + +/// [`reqwest`] wraps the error returned by the redirect policy, and its `Display` impl does not +/// include the source, so we unwrap our own scope error to keep the reason visible to the frontend. +fn map_request_error(error: reqwest::Error) -> Error { + if error.is_redirect() { + let mut source = std::error::Error::source(&error); + while let Some(err) = source { + if let Some(Error::UrlNotAllowed(url)) = err.downcast_ref::() { + return Error::UrlNotAllowed(url.clone()); + } + source = err.source(); + } + } + + Error::Network(error) +} + #[command] pub async fn fetch( webview: Webview, @@ -214,118 +268,115 @@ pub async fn fetch( match scheme { "http" | "https" => { - if Scope::new( + let scope = Scope::new( command_scope .allows() .iter() .chain(global_scope.allows()) + .cloned() .collect(), command_scope .denies() .iter() .chain(global_scope.denies()) + .cloned() .collect(), - ) - .is_allowed(&url) - { - let mut builder = reqwest::ClientBuilder::new(); + ); - if let Some(danger_config) = danger { - #[cfg(not(feature = "dangerous-settings"))] - { - #[cfg(debug_assertions)] - { - eprintln!("[\x1b[33mWARNING\x1b[0m] using dangerous settings requires `dangerous-settings` feature flag in your Cargo.toml"); - } - let _ = danger_config; - return Err(Error::DangerousSettings); - } - #[cfg(feature = "dangerous-settings")] - { - builder = builder - .danger_accept_invalid_certs(danger_config.accept_invalid_certs) - .danger_accept_invalid_hostnames(danger_config.accept_invalid_hostnames) - } - } - - if let Some(timeout) = connect_timeout { - builder = builder.connect_timeout(Duration::from_millis(timeout)); - } - - if let Some(max_redirections) = max_redirections { - builder = builder.redirect(if max_redirections == 0 { - Policy::none() - } else { - Policy::limited(max_redirections) - }); - } - - if let Some(proxy_config) = proxy { - builder = attach_proxy(proxy_config, builder)?; - } - - #[cfg(feature = "cookies")] - { - builder = builder.cookie_provider(state.cookies_jar.clone()); - } - - let mut request = builder.build()?.request(method.clone(), url); - - // POST and PUT requests should always have a 0 length content-length, - // if there is no body. https://fetch.spec.whatwg.org/#http-network-or-cache-fetch - if data.is_none() && matches!(method, Method::POST | Method::PUT) { - headers.append(header::CONTENT_LENGTH, HeaderValue::from_str("0")?); - } - - if headers.contains_key(header::RANGE) { - // https://fetch.spec.whatwg.org/#http-network-or-cache-fetch step 18 - // If httpRequest's header list contains `Range`, then append (`Accept-Encoding`, `identity`) - headers.append(header::ACCEPT_ENCODING, HeaderValue::from_str("identity")?); - } - - if !headers.contains_key(header::USER_AGENT) { - headers.append(header::USER_AGENT, HeaderValue::from_str(HTTP_USER_AGENT)?); - } - - // ensure we have an Origin header set - if cfg!(not(feature = "unsafe-headers")) || !headers.contains_key(header::ORIGIN) { - if let Ok(url) = webview.url() { - // The url crate returns OpaqueOrigin for tauri://localhost which serializes to "null" - let origin = if url.scheme() == "tauri" { - "tauri://localhost".to_string() - } else { - url.origin().ascii_serialization() - }; - headers.append(header::ORIGIN, HeaderValue::from_str(&origin)?); - } - } - - // In case empty origin is passed, remove it. Some services do not like Origin header - // so this way we can remove it in explicit way. The default behaviour is still to set it - if cfg!(feature = "unsafe-headers") - && headers.get(header::ORIGIN) == Some(&HeaderValue::from_static("")) - { - headers.remove(header::ORIGIN); - }; - - if let Some(data) = data { - request = request.body(data); - } - - request = request.headers(headers); - - #[cfg(feature = "tracing")] - tracing::trace!("{:?}", request); - - let fut = async move { request.send().await.map_err(Into::into) }; - - let mut resources_table = webview.resources_table(); - let rid = resources_table.add_request(Box::pin(fut)); - - Ok(rid) - } else { - Err(Error::UrlNotAllowed(url)) + if !scope.is_allowed(&url) { + return Err(Error::UrlNotAllowed(url)); } + + let mut builder = reqwest::ClientBuilder::new(); + + if let Some(danger_config) = danger { + #[cfg(not(feature = "dangerous-settings"))] + { + #[cfg(debug_assertions)] + { + eprintln!("[\x1b[33mWARNING\x1b[0m] using dangerous settings requires `dangerous-settings` feature flag in your Cargo.toml"); + } + let _ = danger_config; + return Err(Error::DangerousSettings); + } + #[cfg(feature = "dangerous-settings")] + { + builder = builder + .danger_accept_invalid_certs(danger_config.accept_invalid_certs) + .danger_accept_invalid_hostnames(danger_config.accept_invalid_hostnames) + } + } + + if let Some(timeout) = connect_timeout { + builder = builder.connect_timeout(Duration::from_millis(timeout)); + } + + let scope = state.config.scope_redirects.then_some(scope); + builder = builder.redirect(redirect_policy(scope, max_redirections)); + + if let Some(proxy_config) = proxy { + builder = attach_proxy(proxy_config, builder)?; + } + + #[cfg(feature = "cookies")] + { + builder = builder.cookie_provider(state.cookies_jar.clone()); + } + + let mut request = builder.build()?.request(method.clone(), url); + + // POST and PUT requests should always have a 0 length content-length, + // if there is no body. https://fetch.spec.whatwg.org/#http-network-or-cache-fetch + if data.is_none() && matches!(method, Method::POST | Method::PUT) { + headers.append(header::CONTENT_LENGTH, HeaderValue::from_str("0")?); + } + + if headers.contains_key(header::RANGE) { + // https://fetch.spec.whatwg.org/#http-network-or-cache-fetch step 18 + // If httpRequest's header list contains `Range`, then append (`Accept-Encoding`, `identity`) + headers.append(header::ACCEPT_ENCODING, HeaderValue::from_str("identity")?); + } + + if !headers.contains_key(header::USER_AGENT) { + headers.append(header::USER_AGENT, HeaderValue::from_str(HTTP_USER_AGENT)?); + } + + // ensure we have an Origin header set + if cfg!(not(feature = "unsafe-headers")) || !headers.contains_key(header::ORIGIN) { + if let Ok(url) = webview.url() { + // The url crate returns OpaqueOrigin for tauri://localhost which serializes to "null" + let origin = if url.scheme() == "tauri" { + "tauri://localhost".to_string() + } else { + url.origin().ascii_serialization() + }; + headers.append(header::ORIGIN, HeaderValue::from_str(&origin)?); + } + } + + // In case empty origin is passed, remove it. Some services do not like Origin header + // so this way we can remove it in explicit way. The default behaviour is still to set it + if cfg!(feature = "unsafe-headers") + && headers.get(header::ORIGIN) == Some(&HeaderValue::from_static("")) + { + headers.remove(header::ORIGIN); + }; + + if let Some(data) = data { + request = request.body(data); + } + + request = request.headers(headers); + + #[cfg(feature = "tracing")] + tracing::trace!("{:?}", request); + + let fut = async move { request.send().await.map_err(map_request_error) }; + + let mut resources_table = webview.resources_table(); + let rid = resources_table.add_request(Box::pin(fut)); + + Ok(rid) } "data" => { let data_url = @@ -486,3 +537,182 @@ fn is_unsafe_header(header: &HeaderName) -> bool { lower.starts_with("proxy-") || lower.starts_with("sec-") } } + +#[cfg(test)] +mod tests { + use std::{ + io::{BufRead, BufReader, Write}, + net::TcpListener, + sync::Arc, + }; + + use super::*; + + /// Test server with an open redirect: + /// + /// - `/redirect-external` answers a 302 to `http://127.0.0.1:{port}/secret`, which is a + /// different origin as far as the scope is concerned - the tests allow `localhost`; + /// - `/redirect/{n}` answers a 302 to `/redirect/{n-1}` on `localhost`, and `/redirect/0` + /// answers a 302 to `/target`, so the whole chain stays in scope; + /// - anything else answers a 200, so a bypass shows up as a successful response. + fn spawn_server() -> u16 { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + + std::thread::spawn(move || { + for stream in listener.incoming() { + let mut stream = stream.unwrap(); + + // the whole request must be consumed, otherwise closing the connection + // with unread data queued resets it before the client reads the response + let mut reader = BufReader::new(stream.try_clone().unwrap()); + let mut request_line = String::new(); + reader.read_line(&mut request_line).unwrap(); + loop { + let mut header = String::new(); + reader.read_line(&mut header).unwrap(); + if matches!(header.as_str(), "" | "\r\n" | "\n") { + break; + } + } + + let path = request_line.split(' ').nth(1).unwrap_or("/"); + let location = match path.strip_prefix("/redirect/") { + Some("0") => Some(format!("http://localhost:{port}/target")), + Some(remaining) => Some(format!( + "http://localhost:{port}/redirect/{}", + remaining.parse::().unwrap() - 1 + )), + None if path == "/redirect-external" => { + Some(format!("http://127.0.0.1:{port}/secret")) + } + None => None, + }; + + let response = match location { + Some(location) => format!( + "HTTP/1.1 302 Found\r\nLocation: {location}\r\nContent-Length: 0\r\nConnection: close\r\n\r\n" + ), + None => "HTTP/1.1 200 OK\r\nContent-Length: 6\r\nConnection: close\r\n\r\nsecret" + .to_string(), + }; + + let _ = stream.write_all(response.as_bytes()); + let _ = stream.flush(); + } + }); + + port + } + + fn get( + url: &str, + scope: Option, + max_redirections: Option, + ) -> Result { + let client = reqwest::ClientBuilder::new() + .redirect(redirect_policy(scope, max_redirections)) + .build() + .unwrap(); + let request = client.get(url); + tauri::async_runtime::block_on( + async move { request.send().await.map_err(map_request_error) }, + ) + } + + fn localhost_scope(port: u16) -> Option { + let entry = Arc::new(format!("http://localhost:{port}/*").parse().unwrap()); + Some(Scope::new(vec![entry], Vec::new())) + } + + #[test] + fn redirect_outside_of_scope_is_denied() { + let port = spawn_server(); + + let err = get( + &format!("http://localhost:{port}/redirect-external"), + localhost_scope(port), + None, + ) + .unwrap_err(); + + match err { + Error::UrlNotAllowed(url) => { + assert_eq!(url.as_str(), format!("http://127.0.0.1:{port}/secret")) + } + e => panic!("expected the redirect to be denied by the scope, got {e:?}"), + } + } + + #[test] + fn redirect_outside_of_scope_is_followed_when_not_configured() { + let port = spawn_server(); + + // `scopeRedirects` is disabled, so only the URL requested by the frontend is checked + let response = get( + &format!("http://localhost:{port}/redirect-external"), + None, + None, + ) + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + } + + #[test] + fn redirect_denied_by_the_scope_is_denied() { + let port = spawn_server(); + let allow = Arc::new(format!("http://localhost:{port}/*").parse().unwrap()); + let deny = Arc::new(format!("http://localhost:{port}/target").parse().unwrap()); + let scope = Some(Scope::new(vec![allow], vec![deny])); + + let err = get(&format!("http://localhost:{port}/redirect/0"), scope, None).unwrap_err(); + + assert!(matches!(err, Error::UrlNotAllowed(_))); + } + + #[test] + fn redirect_inside_of_scope_is_followed() { + let port = spawn_server(); + + let response = get( + &format!("http://localhost:{port}/redirect/2"), + localhost_scope(port), + None, + ) + .unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!(response.url().path(), "/target"); + } + + #[test] + fn max_redirections_is_enforced() { + let port = spawn_server(); + + for scope in [localhost_scope(port), None] { + let err = get( + &format!("http://localhost:{port}/redirect/5"), + scope, + Some(2), + ) + .unwrap_err(); + + assert!(matches!(err, Error::Network(e) if e.is_redirect())); + } + } + + #[test] + fn zero_max_redirections_does_not_follow() { + let port = spawn_server(); + + let response = get( + &format!("http://localhost:{port}/redirect/0"), + localhost_scope(port), + Some(0), + ) + .unwrap(); + + assert_eq!(response.status(), StatusCode::FOUND); + } +} diff --git a/plugins/http/src/config.rs b/plugins/http/src/config.rs new file mode 100644 index 000000000..02f4e9285 --- /dev/null +++ b/plugins/http/src/config.rs @@ -0,0 +1,47 @@ +// Copyright 2019-2023 Tauri Programme within The Commons Conservancy +// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT + +use serde::Deserialize; + +/// HTTP plugin configuration, defined on the `plugins > http` object of your `tauri.conf.json`. +#[derive(Debug, Clone, Default, Deserialize)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct Config { + /// Whether the scope is checked on every hop of a redirect chain instead of only on the + /// URL requested by the frontend. Defaults to `false`. + /// + /// When disabled, a server on an allowed origin can answer with a redirect to any other + /// origin - an open redirect, or a server an attacker controls - and the plugin follows it, + /// handing the webview a response from a URL the scope denies, such as a `localhost` + /// service, an internal host or a cloud metadata endpoint. + /// + /// When enabled, a redirect to a URL that is not allowed by the scope fails with + /// [`Error::UrlNotAllowed`](crate::Error::UrlNotAllowed) instead of being followed, so every + /// redirect target must also be part of the scope. This is opt-in because it breaks + /// applications that rely on being redirected outside of their configured scope. + // TODO(v3): enforce the scope on redirects by default and remove this option + #[serde(default)] + pub scope_redirects: bool, +} + +#[cfg(test)] +mod tests { + use super::Config; + + #[test] + fn deserializes_the_plugin_configuration() { + // the plugin configuration is optional, and the scope check is opt-in + let config: Option = serde_json::from_value(serde_json::Value::Null).unwrap(); + assert!(config.is_none()); + + let config: Config = serde_json::from_str("{}").unwrap(); + assert!(!config.scope_redirects); + + let config: Config = serde_json::from_str(r#"{ "scopeRedirects": true }"#).unwrap(); + assert!(config.scope_redirects); + + // a typo must not silently disable the scope check + assert!(serde_json::from_str::(r#"{ "scopeRedirect": true }"#).is_err()); + } +} diff --git a/plugins/http/src/lib.rs b/plugins/http/src/lib.rs index e5f5c2b4e..e33275476 100644 --- a/plugins/http/src/lib.rs +++ b/plugins/http/src/lib.rs @@ -43,6 +43,21 @@ //! - **tracing**: Adds request, response, and cookie-store diagnostics through `tracing`. //! - **unsafe-headers**: Allows webview requests to send any headers. //! - **dangerous-settings**: Allows dangerous client settings such as accepting invalid certificates or hostnames. +//! +//! ## Configuration +//! +//! See [`Config`] for the options that can be set on the `plugins > http` object of your +//! `tauri.conf.json`: +//! +//! ```json +//! { +//! "plugins": { +//! "http": { +//! "scopeRedirects": true +//! } +//! } +//! } +//! ``` pub use reqwest; use tauri::{ @@ -50,9 +65,11 @@ use tauri::{ Manager, Runtime, }; +pub use config::Config; pub use error::{Error, Result}; mod commands; +mod config; mod error; #[cfg(feature = "cookies")] mod reqwest_cookie_store; @@ -62,13 +79,14 @@ mod scope; const COOKIES_FILENAME: &str = ".cookies"; pub(crate) struct Http { + pub(crate) config: Config, #[cfg(feature = "cookies")] cookies_jar: std::sync::Arc, } -pub fn init() -> TauriPlugin { - Builder::::new("http") - .setup(|app, _| { +pub fn init() -> TauriPlugin> { + Builder::>::new("http") + .setup(|app, api| { #[cfg(feature = "cookies")] let cookies_jar = { use crate::reqwest_cookie_store::*; @@ -96,6 +114,7 @@ pub fn init() -> TauriPlugin { }; let state = Http { + config: api.config().clone().unwrap_or_default(), #[cfg(feature = "cookies")] cookies_jar: std::sync::Arc::new(cookies_jar), }; diff --git a/plugins/http/src/scope.rs b/plugins/http/src/scope.rs index e7638c2ce..a7745c2cd 100644 --- a/plugins/http/src/scope.rs +++ b/plugins/http/src/scope.rs @@ -59,16 +59,16 @@ impl<'de> Deserialize<'de> for Entry { } } -/// Scope for filesystem access. +/// Scope for HTTP access. #[derive(Debug)] -pub struct Scope<'a> { - allowed: Vec<&'a Arc>, - denied: Vec<&'a Arc>, +pub struct Scope { + allowed: Vec>, + denied: Vec>, } -impl<'a> Scope<'a> { +impl Scope { /// Creates a new scope from the scope configuration. - pub(crate) fn new(allowed: Vec<&'a Arc>, denied: Vec<&'a Arc>) -> Self { + pub(crate) fn new(allowed: Vec>, denied: Vec>) -> Self { Self { allowed, denied } } @@ -94,25 +94,24 @@ impl<'a> Scope<'a> { } #[cfg(test)] -mod tests { - use std::{str::FromStr, sync::Arc}; +impl std::str::FromStr for Entry { + type Err = urlpattern::quirks::Error; - use super::Entry; - - impl FromStr for Entry { - type Err = urlpattern::quirks::Error; - - fn from_str(s: &str) -> Result { - let pattern = super::parse_url_pattern(s)?; - Ok(Self { url: pattern }) - } + fn from_str(s: &str) -> Result { + let url = parse_url_pattern(s)?; + Ok(Self { url }) } +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; #[test] fn denied_takes_precedence() { let allow = Arc::new("http://localhost:8080/file.png".parse().unwrap()); let deny = Arc::new("http://localhost:8080/*".parse().unwrap()); - let scope = super::Scope::new(vec![&allow], vec![&deny]); + let scope = super::Scope::new(vec![allow], vec![deny]); assert!(!scope.is_allowed(&"http://localhost:8080/file.png".parse().unwrap())); assert!(!scope.is_allowed(&"http://localhost:8080?framework=tauri".parse().unwrap())); } @@ -121,7 +120,7 @@ mod tests { fn fixed_url() { // plain URL let entry = Arc::new("http://localhost:8080".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"http://localhost:8080".parse().unwrap())); assert!(scope.is_allowed(&"http://localhost:8080/".parse().unwrap())); assert!(scope.is_allowed(&"http://localhost:8080/file".parse().unwrap())); @@ -137,7 +136,7 @@ mod tests { fn fixed_path() { // URL with fixed path let entry = Arc::new("http://localhost:8080/file.png".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"http://localhost:8080/file.png".parse().unwrap())); assert!(scope.is_allowed(&"http://localhost:8080/file.png?q=1".parse().unwrap())); @@ -150,7 +149,7 @@ mod tests { #[test] fn pattern_wildcard() { let entry = Arc::new("http://localhost:8080/*.png".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"http://localhost:8080/file.png".parse().unwrap())); assert!(scope.is_allowed(&"http://localhost:8080/file.png#head".parse().unwrap())); @@ -167,7 +166,7 @@ mod tests { #[test] fn domain_wildcard() { let entry = Arc::new("http://*".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); assert!(scope.is_allowed(&"http://something.else#tauri".parse().unwrap())); @@ -182,7 +181,7 @@ mod tests { assert!(!scope.is_allowed(&"https://something.else".parse().unwrap())); let entry = Arc::new("http://*/*".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); @@ -191,7 +190,7 @@ mod tests { #[test] fn scheme_wildcard() { let entry = Arc::new("*://*".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); @@ -201,7 +200,7 @@ mod tests { assert!(scope.is_allowed(&"https://something.else?x=1#frag".parse().unwrap())); let entry = Arc::new("*://*/*".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"http://something.else".parse().unwrap())); assert!(scope.is_allowed(&"http://something.else/path/to/file".parse().unwrap())); @@ -212,7 +211,7 @@ mod tests { #[test] fn validate_query() { let entry = Arc::new("https://tauri.app/path?x=*".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"https://tauri.app/path?x=5".parse().unwrap())); @@ -222,7 +221,7 @@ mod tests { #[test] fn validate_hash() { let entry = Arc::new("https://tauri.app/path#frame*".parse().unwrap()); - let scope = super::Scope::new(vec![&entry], Vec::new()); + let scope = super::Scope::new(vec![entry], Vec::new()); assert!(scope.is_allowed(&"https://tauri.app/path#frame".parse().unwrap())); From 2393188deac907725e42e834f9961e2a072e8e0c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 22:23:23 -0300 Subject: [PATCH 09/10] publish new versions (#3591) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- .changes/http-redirect-scope.md | 19 --------- .changes/log-webview-double-colon.md | 6 --- .changes/single-instance-foreground-rights.md | 5 --- .changes/updater-allow-downgrades-config.md | 20 ---------- .changes/updater-require-signed-version.md | 23 ----------- Cargo.lock | 10 ++--- examples/api/CHANGELOG.md | 7 ++++ examples/api/package.json | 2 +- examples/api/src-tauri/CHANGELOG.md | 8 ++++ examples/api/src-tauri/Cargo.toml | 8 ++-- plugins/http/CHANGELOG.md | 18 +++++++++ plugins/http/Cargo.toml | 2 +- plugins/log/CHANGELOG.md | 4 ++ plugins/log/Cargo.toml | 2 +- plugins/log/package.json | 2 +- plugins/single-instance/CHANGELOG.md | 4 ++ plugins/single-instance/Cargo.toml | 2 +- plugins/updater/CHANGELOG.md | 40 +++++++++++++++++++ plugins/updater/Cargo.toml | 2 +- plugins/updater/package.json | 2 +- 20 files changed, 97 insertions(+), 89 deletions(-) delete mode 100644 .changes/http-redirect-scope.md delete mode 100644 .changes/log-webview-double-colon.md delete mode 100644 .changes/single-instance-foreground-rights.md delete mode 100644 .changes/updater-allow-downgrades-config.md delete mode 100644 .changes/updater-require-signed-version.md diff --git a/.changes/http-redirect-scope.md b/.changes/http-redirect-scope.md deleted file mode 100644 index 58e4616c1..000000000 --- a/.changes/http-redirect-scope.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -"http": minor ---- - -**Security:** Added the `scopeRedirects` plugin configuration option, which checks the URL scope on every hop of a redirect chain instead of only on the URL requested by the frontend. Without it, a server on an allowed origin can redirect the request to any other origin - including `localhost` services, internal hosts and cloud metadata endpoints - and the plugin follows it, returning the response to the webview. - -```json -{ - "plugins": { - "http": { - "scopeRedirects": true - } - } -} -``` - -It is opt-in because a redirect to a URL that is not allowed by the scope now fails with `url not allowed on the configured scope` instead of being followed, so applications that rely on being redirected outside of their scope must add the redirect target to the scope. **This will become the default in v3.** - -Note that `tauri_plugin_http::init()` now returns `TauriPlugin>` instead of `TauriPlugin`. diff --git a/.changes/log-webview-double-colon.md b/.changes/log-webview-double-colon.md deleted file mode 100644 index f59bf1b52..000000000 --- a/.changes/log-webview-double-colon.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -"log": patch -"log-js": patch ---- - -Log webview log target locations after 2 colons instead of 1. (e.g. `webview:myFunction@http://localhost:5173/src/myFile.ts?t=1787480723620:47:4` to `webview::myFunction@http://localhost:5173/src/myFile.ts?t=1787480723620:47:4`) diff --git a/.changes/single-instance-foreground-rights.md b/.changes/single-instance-foreground-rights.md deleted file mode 100644 index 57794e69b..000000000 --- a/.changes/single-instance-foreground-rights.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"single-instance": patch ---- - -On Windows, the second instance now allows the first instance to bring its window to the front before exiting, so focusing a window from the callback no longer gets refused by Windows. diff --git a/.changes/updater-allow-downgrades-config.md b/.changes/updater-allow-downgrades-config.md deleted file mode 100644 index 365cd956c..000000000 --- a/.changes/updater-allow-downgrades-config.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -"updater": minor -"updater-js": minor ---- - -**Breaking change:** the `allowDowngrades` option was removed from the `check` command and is now read from the plugin configuration instead. - -Previously any code running in the webview could pass `allowDowngrades: true` to `plugin:updater|check` and relax the version check from "the update must be newer" to "the update must be different", overriding the comparator the application had configured on the Rust side. The flag is now an application-level setting: - -```json -{ - "plugins": { - "updater": { - "allowDowngrades": true - } - } -} -``` - -It defaults to `false`, and is ignored when the application provides its own `Builder::default_version_comparator`, which continues to take precedence. diff --git a/.changes/updater-require-signed-version.md b/.changes/updater-require-signed-version.md deleted file mode 100644 index be0e47d77..000000000 --- a/.changes/updater-require-signed-version.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -"updater": minor ---- - -Add a `requireSignedVersion` configuration option that binds an update to the version it was signed for. - -The update endpoint response is fetched over TLS but is not itself signed, and the signature only covers the downloaded artifact. Anyone able to serve a crafted response could therefore pair an inflated `version` field with the `url` and `signature` of an older release and force a downgrade to a genuine but outdated build, because that older artifact carries a valid signature. - -The Tauri CLI records the version in the signature's trusted comment, which the signature covers. With this option enabled the updater compares that signed version against the one announced by the endpoint and rejects the update when they differ: - -```json -{ - "plugins": { - "updater": { - "requireSignedVersion": true - } - } -} -``` - -It defaults to `false` because releases signed before the CLI started recording the version carry none and would be rejected. Re-sign and re-publish every release your users can still update from before enabling it, otherwise an older signature can still be served to bypass the check. When the signature does carry a version, a mismatch is rejected whether or not the option is enabled. - -This is checked independently of `allowDowngrades`: it constrains which artifact a given version number may resolve to, not whether that version is newer than the running one. diff --git a/Cargo.lock b/Cargo.lock index 67d7e8a7a..9ede4439e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -207,7 +207,7 @@ checksum = "2a4385e2e34eb35d6b3efe798b9eb88096925d87726c0798709bf56d9ed84af3" [[package]] name = "api" -version = "2.0.48" +version = "2.0.49" dependencies = [ "log", "serde", @@ -6605,7 +6605,7 @@ dependencies = [ [[package]] name = "tauri-plugin-http" -version = "2.6.1" +version = "2.7.0" dependencies = [ "bytes", "cookie_store", @@ -6641,7 +6641,7 @@ dependencies = [ [[package]] name = "tauri-plugin-log" -version = "2.9.1" +version = "2.9.2" dependencies = [ "android_logger", "fern", @@ -6783,7 +6783,7 @@ dependencies = [ [[package]] name = "tauri-plugin-single-instance" -version = "2.4.4" +version = "2.4.5" dependencies = [ "semver", "serde", @@ -6853,7 +6853,7 @@ dependencies = [ [[package]] name = "tauri-plugin-updater" -version = "2.11.0" +version = "2.12.0" dependencies = [ "base64 0.22.1", "dirs 6.0.0", diff --git a/examples/api/CHANGELOG.md b/examples/api/CHANGELOG.md index 70baa50b6..77b1396aa 100644 --- a/examples/api/CHANGELOG.md +++ b/examples/api/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [2.0.45] + +### Dependencies + +- Upgraded to `log-js@2.9.2` +- Upgraded to `updater-js@2.12.0` + ## [2.0.44] ### Dependencies diff --git a/examples/api/package.json b/examples/api/package.json index 81cac8b9a..98ed4b6ab 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -1,7 +1,7 @@ { "name": "api", "private": true, - "version": "2.0.44", + "version": "2.0.45", "type": "module", "scripts": { "dev": "vite --clearScreen false", diff --git a/examples/api/src-tauri/CHANGELOG.md b/examples/api/src-tauri/CHANGELOG.md index 1d6def5a3..cb78a36e2 100644 --- a/examples/api/src-tauri/CHANGELOG.md +++ b/examples/api/src-tauri/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.0.49] + +### Dependencies + +- Upgraded to `http@2.7.0` +- Upgraded to `log@2.9.2` +- Upgraded to `updater@2.12.0` + ## [2.0.48] ### Dependencies diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index e9c963870..dc15465c3 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "api" publish = false -version = "2.0.48" +version = "2.0.49" description = "An example Tauri Application showcasing the api" edition = "2021" rust-version = { workspace = true } @@ -20,7 +20,7 @@ serde = { workspace = true } tiny_http = "0.12" time = "0.3" log = { workspace = true } -tauri-plugin-log = { path = "../../../plugins/log", version = "2.9.1" } +tauri-plugin-log = { path = "../../../plugins/log", version = "2.9.2" } tauri-plugin-fs = { path = "../../../plugins/fs", version = "2.5.2", features = [ "watch", ] } @@ -29,7 +29,7 @@ tauri-plugin-dialog = { path = "../../../plugins/dialog", version = "2.7.3" } tauri-plugin-http = { path = "../../../plugins/http", features = [ "multipart", "cookies", -], version = "2.6.1" } +], version = "2.7.0" } tauri-plugin-notification = { path = "../../../plugins/notification", version = "2.4.0", features = [ "windows7-compat", ] } @@ -57,7 +57,7 @@ features = [ [target."cfg(any(target_os = \"macos\", windows, target_os = \"linux\", target_os = \"dragonfly\", target_os = \"freebsd\", target_os = \"openbsd\", target_os = \"netbsd\"))".dependencies] tauri-plugin-cli = { path = "../../../plugins/cli", version = "2.4.1" } tauri-plugin-global-shortcut = { path = "../../../plugins/global-shortcut", version = "2.3.2" } -tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.11.0" } +tauri-plugin-updater = { path = "../../../plugins/updater", version = "2.12.0" } tauri-plugin-window-state = { path = "../../../plugins/window-state", version = "2.2.0" } [target."cfg(any(target_os = \"android\", target_os = \"ios\"))".dependencies] diff --git a/plugins/http/CHANGELOG.md b/plugins/http/CHANGELOG.md index b4f3d8c01..ed6b948ac 100644 --- a/plugins/http/CHANGELOG.md +++ b/plugins/http/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [2.7.0] + +- [`1198a524`](https://github.com/tauri-apps/plugins-workspace/commit/1198a524b710abf2abeb1d9bd7b252402d26ca6d) **Security:** Added the `scopeRedirects` plugin configuration option, which checks the URL scope on every hop of a redirect chain instead of only on the URL requested by the frontend. Without it, a server on an allowed origin can redirect the request to any other origin - including `localhost` services, internal hosts and cloud metadata endpoints - and the plugin follows it, returning the response to the webview. + + ```json + { + "plugins": { + "http": { + "scopeRedirects": true + } + } + } + ``` + + It is opt-in because a redirect to a URL that is not allowed by the scope now fails with `url not allowed on the configured scope` instead of being followed, so applications that rely on being redirected outside of their scope must add the redirect target to the scope. **This will become the default in v3.** + + Note that `tauri_plugin_http::init()` now returns `TauriPlugin>` instead of `TauriPlugin`. + ## [2.6.1] - [`a21555dd`](https://github.com/tauri-apps/plugins-workspace/commit/a21555ddd2eaadfed23848912fe2802c2ba7579e) ([#3566](https://github.com/tauri-apps/plugins-workspace/pull/3566) by [@followdarko](https://github.com/tauri-apps/plugins-workspace/../../followdarko)) Fix unhandled promise rejections on every `fetch` teardown: the request/body cleanup commands were fired as floating promises, and releasing an already-released resource rejects with `The resource id N is invalid.`. `dropBody` is now idempotent and both cleanup calls handle their own rejection. diff --git a/plugins/http/Cargo.toml b/plugins/http/Cargo.toml index 6c707f4d2..f0eeadd3f 100644 --- a/plugins/http/Cargo.toml +++ b/plugins/http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-http" -version = "2.6.1" +version = "2.7.0" description = "Access an HTTP client written in Rust." edition = { workspace = true } authors = { workspace = true } diff --git a/plugins/log/CHANGELOG.md b/plugins/log/CHANGELOG.md index abf71431e..11aea9e12 100644 --- a/plugins/log/CHANGELOG.md +++ b/plugins/log/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [2.9.2] + +- [`2df3d936`](https://github.com/tauri-apps/plugins-workspace/commit/2df3d9368187e1d2ce736d3e0b2d4bdcae35b011) ([#3574](https://github.com/tauri-apps/plugins-workspace/pull/3574)) Log webview log target locations after 2 colons instead of 1. (e.g. `webview:myFunction@http://localhost:5173/src/myFile.ts?t=1787480723620:47:4` to `webview::myFunction@http://localhost:5173/src/myFile.ts?t=1787480723620:47:4`) + ## [2.9.1] - [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. diff --git a/plugins/log/Cargo.toml b/plugins/log/Cargo.toml index bd655a0ae..1bf62bb18 100644 --- a/plugins/log/Cargo.toml +++ b/plugins/log/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-log" -version = "2.9.1" +version = "2.9.2" description = "Configurable logging for your Tauri app." authors = { workspace = true } license = { workspace = true } diff --git a/plugins/log/package.json b/plugins/log/package.json index bb2db12e8..b21ff6cec 100644 --- a/plugins/log/package.json +++ b/plugins/log/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-log", - "version": "2.9.1", + "version": "2.9.2", "description": "Configurable logging for your Tauri app.", "license": "MIT OR Apache-2.0", "authors": [ diff --git a/plugins/single-instance/CHANGELOG.md b/plugins/single-instance/CHANGELOG.md index c2bb43881..3e4c94aab 100644 --- a/plugins/single-instance/CHANGELOG.md +++ b/plugins/single-instance/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [2.4.5] + +- [`67cd25a1`](https://github.com/tauri-apps/plugins-workspace/commit/67cd25a10ce3deb1b935e725228e528766f24ad7) ([#3592](https://github.com/tauri-apps/plugins-workspace/pull/3592)) On Windows, the second instance now allows the first instance to bring its window to the front before exiting, so focusing a window from the callback no longer gets refused by Windows. + ## [2.4.4] - [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. diff --git a/plugins/single-instance/Cargo.toml b/plugins/single-instance/Cargo.toml index 9e3404743..e636a7f57 100644 --- a/plugins/single-instance/Cargo.toml +++ b/plugins/single-instance/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-single-instance" -version = "2.4.4" +version = "2.4.5" description = "Ensure a single instance of your tauri app is running." authors = { workspace = true } license = { workspace = true } diff --git a/plugins/updater/CHANGELOG.md b/plugins/updater/CHANGELOG.md index 9527f65d3..6a5484a8b 100644 --- a/plugins/updater/CHANGELOG.md +++ b/plugins/updater/CHANGELOG.md @@ -1,5 +1,23 @@ # Changelog +## [2.12.0] + +- [`1308bfa3`](https://github.com/tauri-apps/plugins-workspace/commit/1308bfa399b962b3977c767100a6339d1cbfdd20) **Breaking change:** the `allowDowngrades` option was removed from the `check` command and is now read from the plugin configuration instead. + + Previously any code running in the webview could pass `allowDowngrades: true` to `plugin:updater|check` and relax the version check from "the update must be newer" to "the update must be different", overriding the comparator the application had configured on the Rust side. The flag is now an application-level setting: + + ```json + { + "plugins": { + "updater": { + "allowDowngrades": true + } + } + } + ``` + + It defaults to `false`, and is ignored when the application provides its own `Builder::default_version_comparator`, which continues to take precedence. + ## [2.11.0] - [`f8053e65`](https://github.com/tauri-apps/plugins-workspace/commit/f8053e659e4ccd85c1f52833411ff8417cbc5e69) ([#3527](https://github.com/tauri-apps/plugins-workspace/pull/3527) by [@Legend-Master](https://github.com/tauri-apps/plugins-workspace/../../Legend-Master)) Documented Cargo feature flags in each plugin's crate-level documentation. @@ -215,3 +233,25 @@ ## \[2.0.0-alpha.0] - [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! +7c2f9e0ce4c75c07ffa3fd76391a25b58f5daf)([#802](https://github.com/tauri-apps/plugins-workspace/pull/802)) Update to @tauri-apps/api v2.0.0-alpha.12. + +## \[2.0.0-alpha.3] + +- [`e438e0a`](https://github.com/tauri-apps/plugins-workspace/commit/e438e0a62d4b430a5159f05f13ecd397dd891a0d)([#676](https://github.com/tauri-apps/plugins-workspace/pull/676)) Update to @tauri-apps/api v2.0.0-alpha.11. + +## \[2.0.0-alpha.2] + +- [`5c13736`](https://github.com/tauri-apps/plugins-workspace/commit/5c137365c60790e8d4037d449e8237aa3fffdab0)([#673](https://github.com/tauri-apps/plugins-workspace/pull/673)) Update to @tauri-apps/api v2.0.0-alpha.9. + +## \[2.0.0-alpha.2] + +- [`4e2cef9`](https://github.com/tauri-apps/plugins-workspace/commit/4e2cef9b702bbbb9cf4ee17de50791cb21f1b2a4)([#593](https://github.com/tauri-apps/plugins-workspace/pull/593)) Update to alpha.12. + +## \[2.0.0-alpha.1] + +- [`d74fc0a`](https://github.com/tauri-apps/plugins-workspace/commit/d74fc0a097996e90a37be8f57d50b7d1f6ca616f)([#555](https://github.com/tauri-apps/plugins-workspace/pull/555)) Update to alpha.11. +- [`4ab90f0`](https://github.com/tauri-apps/plugins-workspace/commit/4ab90f048eab2918344f97dc8e04413a404e392d)([#431](https://github.com/tauri-apps/plugins-workspace/pull/431)) The updater plugin is recieving a few changes to improve consistency and ergonomics of the Rust and JS APIs + +## \[2.0.0-alpha.0] + +- [`717ae67`](https://github.com/tauri-apps/plugins-workspace/commit/717ae670978feb4492fac1f295998b93f2b9347f)([#371](https://github.com/tauri-apps/plugins-workspace/pull/371)) First v2 alpha release! diff --git a/plugins/updater/Cargo.toml b/plugins/updater/Cargo.toml index 4ca645e92..01d777aa9 100644 --- a/plugins/updater/Cargo.toml +++ b/plugins/updater/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tauri-plugin-updater" -version = "2.11.0" +version = "2.12.0" description = "In-app updates for Tauri applications." edition = { workspace = true } authors = { workspace = true } diff --git a/plugins/updater/package.json b/plugins/updater/package.json index 44547c299..820b3a1b6 100644 --- a/plugins/updater/package.json +++ b/plugins/updater/package.json @@ -1,6 +1,6 @@ { "name": "@tauri-apps/plugin-updater", - "version": "2.11.0", + "version": "2.12.0", "license": "MIT OR Apache-2.0", "authors": [ "Tauri Programme within The Commons Conservancy" From 5baf71a47292d8490f0ba3d53f15224e55c48327 Mon Sep 17 00:00:00 2001 From: Lucas Nogueira Date: Sat, 19 Sep 2026 22:23:56 -0300 Subject: [PATCH 10/10] fix: pnpm audit --- pnpm-lock.yaml | 33 +++++++++++++++++++-------------- pnpm-workspace.yaml | 3 +++ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 863f28e48..707335b59 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -105,6 +105,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + devalue@<5.9.1: ^5.9.1 + importers: .: @@ -210,7 +213,7 @@ importers: version: link:../../plugins/upload '@zerodevx/svelte-json-view': specifier: 2.0.0 - version: 2.0.0(svelte@5.55.7) + version: 2.0.0(svelte@5.55.7(@typescript-eslint/types@8.70.0)) devDependencies: '@iconify-json/codicon': specifier: ^1.2.49 @@ -220,7 +223,7 @@ importers: version: 1.2.2 '@sveltejs/vite-plugin-svelte': specifier: ^7.0.0 - version: 7.0.0(svelte@5.55.7)(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)) + version: 7.0.0(svelte@5.55.7(@typescript-eslint/types@8.70.0))(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)) '@tauri-apps/cli': specifier: 2.11.4 version: 2.11.4 @@ -229,7 +232,7 @@ importers: version: 66.6.7 svelte: specifier: ^5.55.7 - version: 5.55.7 + version: 5.55.7(@typescript-eslint/types@8.70.0) unocss: specifier: ^66.6.7 version: 66.6.7(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)) @@ -1483,8 +1486,8 @@ packages: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} - devalue@5.8.1: - resolution: {integrity: sha512-4CXDYRBGqN+57wVJkuXBYmpAVUSg3L6JAQa/DFqm238G73E1wuyc/JhGQJzN7vUf/CMphYau2zXbfWzDR5aTEw==} + devalue@5.9.4: + resolution: {integrity: sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==} duplexer@0.1.2: resolution: {integrity: sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==} @@ -2531,12 +2534,12 @@ snapshots: dependencies: acorn: 8.16.0 - '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.7)(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1))': + '@sveltejs/vite-plugin-svelte@7.0.0(svelte@5.55.7(@typescript-eslint/types@8.70.0))(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1))': dependencies: deepmerge: 4.3.1 magic-string: 0.30.21 obug: 2.1.1 - svelte: 5.55.7 + svelte: 5.55.7(@typescript-eslint/types@8.70.0) vite: 8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1) vitefu: 1.1.2(vite@8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1)) @@ -2829,9 +2832,9 @@ snapshots: unplugin-utils: 0.3.1 vite: 8.0.16(esbuild@0.28.1)(jiti@2.6.1)(terser@5.46.1)(tsx@4.23.1) - '@zerodevx/svelte-json-view@2.0.0(svelte@5.55.7)': + '@zerodevx/svelte-json-view@2.0.0(svelte@5.55.7(@typescript-eslint/types@8.70.0))': optionalDependencies: - svelte: 5.55.7 + svelte: 5.55.7(@typescript-eslint/types@8.70.0) acorn-jsx@5.3.2(acorn@8.16.0): dependencies: @@ -2899,7 +2902,7 @@ snapshots: detect-libc@2.1.2: {} - devalue@5.8.1: {} + devalue@5.9.4: {} duplexer@0.1.2: {} @@ -3003,9 +3006,11 @@ snapshots: dependencies: estraverse: 5.3.0 - esrap@2.2.8: + esrap@2.2.8(@typescript-eslint/types@8.70.0): dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + optionalDependencies: + '@typescript-eslint/types': 8.70.0 esrecurse@4.3.0: dependencies: @@ -3387,7 +3392,7 @@ snapshots: supports-preserve-symlinks-flag@1.0.0: {} - svelte@5.55.7: + svelte@5.55.7(@typescript-eslint/types@8.70.0): dependencies: '@jridgewell/remapping': 2.3.5 '@jridgewell/sourcemap-codec': 1.5.5 @@ -3398,9 +3403,9 @@ snapshots: aria-query: 5.3.1 axobject-query: 4.1.0 clsx: 2.1.1 - devalue: 5.8.1 + devalue: 5.9.4 esm-env: 1.2.2 - esrap: 2.2.8 + esrap: 2.2.8(@typescript-eslint/types@8.70.0) is-reference: 3.0.3 locate-character: 3.0.0 magic-string: 0.30.21 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 186e599d6..c4c15365f 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,3 +5,6 @@ packages: allowBuilds: esbuild: true + +overrides: + devalue@<5.9.1: ^5.9.1