Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions .github/workflows/native-app-frame-probe.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
name: Verify Linux and macOS app frames

on:
pull_request:
branches: [main]
paths:
- 'src-tauri/src/extension_frames.rs'
- 'src-tauri/src/native_ipc.rs'
- 'scripts/run-extension-probe.mjs'
- 'tests/fixtures/native-frame-probe*'
- '.github/workflows/native-app-frame-probe.yml'

permissions:
contents: read

jobs:
native-isolation:
strategy:
fail-fast: false
matrix:
include:
- runner: ubuntu-22.04
platform: linux
- runner: macos-15
platform: macos
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6
with:
node-version: 24
cache: npm
- name: Linux prerequisites
if: matrix.platform == 'linux'
run: |
sudo apt-get update
sudo apt-get install -y libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libssl-dev libsecret-1-dev xvfb dbus-x11
- run: npm ci
- name: Build the dedicated native probe
run: npm run tauri -- build --debug --no-bundle --config src-tauri/tauri.extension-probe.conf.json
- name: Exercise app frame isolation
env:
PLATFORM: ${{ matrix.platform }}
run: |
if [ "$PLATFORM" = linux ]; then
xvfb-run -a dbus-run-session -- node scripts/run-extension-probe.mjs
else
node scripts/run-extension-probe.mjs
fi
5 changes: 3 additions & 2 deletions scripts/run-extension-probe.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { resolve } from "node:path";
const root = fileURLToPath(new URL("../", import.meta.url));
const result = resolve(root, ".local/native-extension-probe/result.json");
await rm(result, { force: true });
const child = spawn(resolve(root, "target/debug/shellcanvas.exe"), [], {
const binary = resolve(root, `target/debug/shellcanvas${process.platform === "win32" ? ".exe" : ""}`);
const child = spawn(binary, [], {
cwd: root,
windowsHide: true,
env: { ...process.env, SHELLCANVAS_EXTENSION_PROBE: "1" },
Expand All @@ -21,7 +22,7 @@ try {
});
const report = JSON.parse(await readFile(result, "utf8"));
console.log(JSON.stringify(report, null, 2));
// The structured report is authoritative; Windows GUI exit codes are insufficient.
// The structured report is authoritative; GUI exit codes are insufficient.
if (report.success !== true) {
console.error(
await readFile(
Expand Down
19 changes: 13 additions & 6 deletions src-tauri/src/extension_frames.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,11 @@ impl FrameDocuments {
}
documents.insert(id.clone(), document);
Ok(FrameLocation {
url: format!("http://{SCHEME}.localhost/{id}/index.html"),
url: if cfg!(windows) {
format!("http://{SCHEME}.localhost/{id}/index.html")
} else {
format!("{SCHEME}://localhost/{id}/index.html")
},
id,
})
}
Expand Down Expand Up @@ -142,9 +146,6 @@ pub fn publish_app_frame(
style: String,
instance_token: String,
) -> Result<FrameLocation, String> {
if !cfg!(windows) {
return Err("Native runtime app frames are not yet verified on this platform.".into());
}
if webview.label() != "main" {
return Err("Only the desktop can create app frames.".into());
}
Expand All @@ -161,8 +162,9 @@ pub fn release_app_frame(

pub fn plugin<R: Runtime>() -> TauriPlugin<R> {
tauri::plugin::Builder::new("runtime-app-documents")
// Wry's Windows navigation callback handles the top-level WebView. Never promote an
// app resource into that privileged document. Other platforms remain gated above.
// Wry's Windows navigation callback handles the top-level WebView. The
// IPC transport also checks the trusted desktop origin before creating
// its invocation key on every platform.
.on_navigation(|_, url| {
!cfg!(windows)
|| (url.scheme() != SCHEME && url.host_str() != Some("shellcanvas-app.localhost"))
Expand Down Expand Up @@ -193,6 +195,11 @@ mod tests {
"body{color:red}".into(),
)
.unwrap();
assert!(one.url.starts_with(if cfg!(windows) {
"http://shellcanvas-app.localhost/"
} else {
"shellcanvas-app://localhost/"
}));
let other = documents
.publish(
"main",
Expand Down
22 changes: 18 additions & 4 deletions src-tauri/src/native_ipc.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// SPDX-License-Identifier: MPL-2.0
//! Preserve Tauri's IPC transport, but never initialize its secret-bearing closure in a subframe.
//! Preserve Tauri's IPC transport, but initialize its secret-bearing closure
//! only in the desktop's own top-level document.
pub fn initialization_script() -> String {
let transport = include_str!("../vendor/tauri-ipc/ipc-protocol.js")
.replace("__TEMPLATE_invoke_key__", "__INVOKE_KEY__")
Expand All @@ -16,7 +17,20 @@ pub fn initialization_script() -> String {
// Private Tauri 2.11.5 wire constant, kept with the matching vendored templates.
"\"plugin:__TAURI_CHANNEL__|fetch\"",
);
// WebView2 ignores Wry's main-frame-only flag. This check runs before app code, and
// Window.top is browser-owned. Keep the invocation key inside the guarded closure.
format!("if (window === window.top) {{\n{transport}\n}}")
// WebView2 ignores Wry's main-frame-only flag. On WebKit, an app document
// could also become the top-level page after a navigation. Check both the
// browser-owned frame identity and the exact trusted desktop origin before
// constructing the invocation-key closure.
let trusted = if cfg!(dev) {
"http://127.0.0.1:1420"
} else {
if cfg!(windows) {
"http://tauri.localhost"
} else {
"tauri://localhost"
}
};
format!(
"if (window === window.top && window.location.protocol + '//' + window.location.host === {trusted:?}) {{\n{transport}\n}}"
)
}