From 10a1c29154c28e43d1e31562ae15c70129e02407 Mon Sep 17 00:00:00 2001 From: fran domovic Date: Mon, 13 Jul 2026 13:54:06 +0200 Subject: [PATCH 1/4] fix: browser-style login from app windows dies on IPC scope rejection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Logging out of a remotely-hosted app (e.g. mero-blocks.vercel.app) and navigating in-window to the node's auth page (http://localhost:2528/auth/login) left the user stuck on "No providers available": every fetch from that page was routed through the Tauri IPC proxy, but Tauri v1 matches IPC scopes by exact domain — only the app's own domain was registered, so all IPC calls from the localhost-origin page failed with "Scope not defined for URL", and the proxy re-threw instead of falling back. - proxy_script.js: only proxy fetch/XHR from HTTPS pages (the proxy exists solely to bypass mixed-content blocking; plain-HTTP pages can reach http://localhost natively), and fall back to the native fetch/XHR path as a last resort when the proxy fails - create_app_window: register IPC scopes for the node host and localhost in addition to the app domain, so IPC survives in-window navigation to the auth page - tauri.conf.json: drop the dangerousRemoteDomainIpcAccess wildcard entry — Tauri v1 does exact matching only, so it never matched anything - bump app to 0.0.67 Co-Authored-By: Claude Fable 5 --- apps/desktop/package.json | 2 +- apps/desktop/src-tauri/src/main.rs | 31 +++++++++++--- apps/desktop/src-tauri/src/proxy_script.js | 48 ++++++++++++++++------ apps/desktop/src-tauri/tauri.conf.json | 11 +---- 4 files changed, 64 insertions(+), 28 deletions(-) diff --git a/apps/desktop/package.json b/apps/desktop/package.json index 6669ad0..62e9416 100644 --- a/apps/desktop/package.json +++ b/apps/desktop/package.json @@ -1,6 +1,6 @@ { "name": "desktop", - "version": "0.0.66", + "version": "0.0.67", "description": "Calimero Desktop Application", "type": "module", "scripts": { diff --git a/apps/desktop/src-tauri/src/main.rs b/apps/desktop/src-tauri/src/main.rs index 0d39f79..4ee06e7 100644 --- a/apps/desktop/src-tauri/src/main.rs +++ b/apps/desktop/src-tauri/src/main.rs @@ -790,12 +790,33 @@ async fn create_app_window( // Configure IPC scope BEFORE showing window // This allows windows with unique labels (domain + timestamp) to access Tauri IPC - let remote_access = tauri::ipc::RemoteDomainAccessScope::new(domain) - .add_window(&window_label) - .enable_tauri_api(); - app_handle.ipc_scope().configure_remote_access(remote_access); + // + // Tauri v1 matches IPC scopes by EXACT domain (no wildcards), and the + // browser-style login flow navigates this same window to the node's auth + // page (e.g. http://localhost:2528/auth/login). Without a scope for the + // node's host, every IPC call from that page fails with + // "Scope not defined for URL". Register the app domain, the node host and + // localhost. Note: Url::domain() returns None for IP hosts (127.0.0.1), + // so scopes can never match IP-hosted pages — the injected proxy script + // falls back to native fetch there (plain-HTTP pages don't need the proxy). + let mut scope_domains: Vec = vec![domain.to_string(), "localhost".to_string()]; + if let Some(node_host) = node_url_to_use + .parse::() + .ok() + .and_then(|u| u.host_str().map(str::to_string)) + { + scope_domains.push(node_host); + } + scope_domains.sort(); + scope_domains.dedup(); + for scope_domain in &scope_domains { + let remote_access = tauri::ipc::RemoteDomainAccessScope::new(scope_domain) + .add_window(&window_label) + .enable_tauri_api(); + app_handle.ipc_scope().configure_remote_access(remote_access); + } - info!("[Tauri] Configured IPC scope for domain: {} on window: {}", domain, window_label); + info!("[Tauri] Configured IPC scope for domains: {:?} on window: {}", scope_domains, window_label); // Camera/microphone for WebRTC video calls (e.g. Mero Meet) needs no extra // work here: wry's own WKUIDelegate already grants diff --git a/apps/desktop/src-tauri/src/proxy_script.js b/apps/desktop/src-tauri/src/proxy_script.js index 14d335a..a3a2490 100644 --- a/apps/desktop/src-tauri/src/proxy_script.js +++ b/apps/desktop/src-tauri/src/proxy_script.js @@ -20,6 +20,18 @@ } } + // The proxy exists solely to bypass mixed-content blocking: HTTPS-hosted + // app pages cannot fetch http://localhost directly. When the page itself + // is served over plain HTTP (e.g. the node's auth frontend at + // http://localhost:2528/auth/login after a logout → browser-style login + // navigation), native fetch works fine — and Tauri v1 IPC scopes are + // exact-domain matches registered for the app's original domain, so + // invoking the proxy from a localhost-origin page fails with + // "Scope not defined for URL". Never proxy from non-HTTPS pages. + function pageNeedsProxy() { + return window.location.protocol === 'https:'; + } + // Normalize the first fetch() argument: string | URL | Request → string URL function normalizeUrl(urlArg) { if (typeof urlArg === 'string') return urlArg; @@ -207,7 +219,7 @@ // Proxy any HTTP localhost request (any port) to avoid mixed content blocking. // The Rust backend validates the URL before proxying. - const shouldProxy = isHttpLocalhost(urlStr); + const shouldProxy = pageNeedsProxy() && isHttpLocalhost(urlStr); console.log('[Tauri Proxy] Should proxy?', shouldProxy, 'for URL:', urlStr); if (shouldProxy) { @@ -291,13 +303,15 @@ headers: new Headers(response.headers) }); } catch (error) { - console.error('[Tauri Proxy] Fetch proxy failed:', error, 'URL:', urlStr); - const isTauri = typeof window.__TAURI_INVOKE__ === 'function' || - (typeof window.__TAURI__ !== 'undefined' && typeof window.__TAURI__.invoke === 'function'); - if (!isTauri) { - return originalFetch.apply(this, arguments); + if (error instanceof DOMException && error.name === 'AbortError') { + throw error; } - throw error; + // Last resort: try the native fetch. If the proxy failed for an + // IPC scope error while the page can reach localhost natively, + // this recovers; if mixed content blocks it, it fails the same + // way the proxy just did. + console.error('[Tauri Proxy] Fetch proxy failed, falling back to native fetch:', error, 'URL:', urlStr); + return originalFetch.apply(this, arguments); } } @@ -329,7 +343,7 @@ }; xhr.send = function(body) { - const shouldProxy = isHttpLocalhost(xhrUrl); + const shouldProxy = pageNeedsProxy() && isHttpLocalhost(xhrUrl); if (shouldProxy) { console.log('[Tauri Proxy] XHR intercepted:', xhrMethod, xhrUrl); @@ -365,12 +379,20 @@ xhr.dispatchEvent(new ProgressEvent('loadend')); }) .catch(function(error) { - console.error('[Tauri Proxy] XHR proxy failed:', error, 'URL:', xhrUrl); - if (typeof xhr.onerror === 'function') { - xhr.onerror(new ProgressEvent('error')); + // Same last-resort fallback as fetch: open() already ran + // on the real XHR, so a native send() can still succeed + // (e.g. when only the IPC scope was the problem). + console.error('[Tauri Proxy] XHR proxy failed, falling back to native XHR:', error, 'URL:', xhrUrl); + try { + originalSend(body); + } catch (sendError) { + console.error('[Tauri Proxy] Native XHR fallback failed:', sendError, 'URL:', xhrUrl); + if (typeof xhr.onerror === 'function') { + xhr.onerror(new ProgressEvent('error')); + } + xhr.dispatchEvent(new ProgressEvent('error')); + xhr.dispatchEvent(new ProgressEvent('loadend')); } - xhr.dispatchEvent(new ProgressEvent('error')); - xhr.dispatchEvent(new ProgressEvent('loadend')); }); return; } diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index a7faa92..f573604 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -8,7 +8,7 @@ }, "package": { "productName": "Calimero Desktop", - "version": "0.0.66" + "version": "0.0.67" }, "tauri": { "allowlist": { @@ -89,14 +89,7 @@ }, "security": { "csp": "default-src 'self' 'unsafe-inline' 'unsafe-eval' http://localhost:* http://127.0.0.1:* https://api.github.com; connect-src 'self' https://apps.calimero.network https://*.calimero.network http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* wss://* https: http: ws: wss: https://api.github.com https://github.com; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https: http:; font-src 'self' data:; frame-src 'self' https: http:;", - "dangerousRemoteDomainIpcAccess": [ - { - "domain": "*", - "windows": ["*"], - "plugins": [], - "enableTauriAPI": true - } - ], + "dangerousRemoteDomainIpcAccess": [], "dangerousDisableAssetCspModification": true }, "systemTray": { From 5cf1883dd069c7aedb71e227bfa1853be11dce35 Mon Sep 17 00:00:00 2001 From: fran domovic Date: Mon, 13 Jul 2026 14:03:10 +0200 Subject: [PATCH 2/4] test: cover the plain-HTTP page proxy bypass; tolerate missing location MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The proxy-script test harness injects the IIFE with a bare mock window that has no `location`, so pageNeedsProxy() threw. Give the mock a location (HTTPS by default so proxy-path tests keep exercising the proxy), add a test for the http-page → native-fetch bypass, and make pageNeedsProxy() fall back to the historical always-proxy behavior if location is ever unreadable. Co-Authored-By: Claude Fable 5 --- apps/desktop/src-tauri/src/proxy_script.js | 8 ++++++- apps/desktop/src/utils/proxyScript.test.ts | 26 +++++++++++++++++++++- 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src-tauri/src/proxy_script.js b/apps/desktop/src-tauri/src/proxy_script.js index a3a2490..c71603e 100644 --- a/apps/desktop/src-tauri/src/proxy_script.js +++ b/apps/desktop/src-tauri/src/proxy_script.js @@ -29,7 +29,13 @@ // invoking the proxy from a localhost-origin page fails with // "Scope not defined for URL". Never proxy from non-HTTPS pages. function pageNeedsProxy() { - return window.location.protocol === 'https:'; + try { + return window.location.protocol === 'https:'; + } catch (e) { + // No usable location (shouldn't happen in a real webview) — + // keep the historical always-proxy behavior. + return true; + } } // Normalize the first fetch() argument: string | URL | Request → string URL diff --git a/apps/desktop/src/utils/proxyScript.test.ts b/apps/desktop/src/utils/proxyScript.test.ts index 4a38dac..c680cd5 100644 --- a/apps/desktop/src/utils/proxyScript.test.ts +++ b/apps/desktop/src/utils/proxyScript.test.ts @@ -23,13 +23,16 @@ function injectProxy(mockWindow: any, nodeUrl = "http://localhost:2428") { new Function("window", src)(mockWindow); } -function makeMockWindow() { +function makeMockWindow(pageProtocol = "https:") { // Provide a minimal XMLHttpRequest stub so the script's XHR-wrapping code // doesn't throw on init (we don't exercise XHR in these tests). function FakeXHR() {} FakeXHR.prototype = {}; return { __TAURI_FETCH_PROXY_INJECTED__: undefined as boolean | undefined, + // The proxy only runs on HTTPS-hosted pages (mixed-content bypass); + // default the mock to an HTTPS page so proxy-path tests exercise it. + location: { protocol: pageProtocol }, // Stored by the script as `originalFetch` — returned for non-proxied URLs. fetch: vi.fn().mockResolvedValue(new Response("original", { status: 200 })), XMLHttpRequest: FakeXHR, @@ -131,6 +134,27 @@ describe("proxy_script SSE routing", () => { expect(mockWindow.__TAURI_INVOKE__).not.toHaveBeenCalled(); }); + it("does not invoke Tauri IPC when the page itself is plain HTTP (e.g. the node's auth page)", async () => { + // Pages served over http:// (like http://localhost:2528/auth/login after + // a logout → browser-style login) have no mixed-content problem, and the + // window's IPC scope may not cover their origin — native fetch must be + // used instead of the proxy. + const mockWindow = makeMockWindow("http:"); + mockWindow.__TAURI_INVOKE__ = vi.fn(); + mockWindow.__TAURI__ = { + event: { listen: vi.fn(async () => vi.fn()) }, + }; + + injectProxy(mockWindow); + + const response = await mockWindow.fetch( + "http://localhost:2528/auth/providers", + { headers: { Accept: "application/json" } } + ); + expect(mockWindow.__TAURI_INVOKE__).not.toHaveBeenCalled(); + expect(await response.text()).toBe("original"); + }); + it("does not invoke Tauri IPC for non-SSE localhost requests", async () => { const mockWindow = makeMockWindow(); const tauriInvoke = vi.fn().mockResolvedValue({ From 53a7a516200dde0882dbd44cf1c570cd60d18a37 Mon Sep 17 00:00:00 2001 From: fran domovic Date: Mon, 13 Jul 2026 14:10:27 +0200 Subject: [PATCH 3/4] chore: bump bundled merod to 0.11.0-rc.14 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit rc.14 embeds auth-frontend v1.3.1 (auth-frontend#37), which pairs with the proxy fix in this PR: the login page recovers from a failed providers load instead of dead-ending on "No providers available". NOTE: builds fail until core 0.11.0-rc.14 is released (core#3218) — the prepare-merod script downloads the binary by release tag. Co-Authored-By: Claude Fable 5 --- merod-config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/merod-config.json b/merod-config.json index 4cd7416..9ecca21 100644 --- a/merod-config.json +++ b/merod-config.json @@ -1,4 +1,4 @@ { "name": "merod binary", - "version": "0.11.0-rc.13" + "version": "0.11.0-rc.14" } \ No newline at end of file From 289c44e9373e948992dc78038c49896934607371 Mon Sep 17 00:00:00 2001 From: fran domovic Date: Wed, 15 Jul 2026 18:47:49 +0200 Subject: [PATCH 4/4] ci: re-trigger build now that core 0.11.0-rc.14 binaries are published