@livx.cc/appwrap 0.46.1 → 0.46.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/runtime/App_Resources/Android/src/main/AndroidManifest.xml +1 -0
- package/runtime/app/shell/custom-webview.android.ts +19 -2
- package/runtime-desktop/bridge-shim/shim.ts +12 -197
- package/runtime-desktop/chrome/appwrap-browser-chrome.html +120 -0
- package/runtime-desktop/crates/webview-control/Cargo.lock +330 -0
- package/runtime-desktop/crates/webview-control/Cargo.toml +28 -0
- package/runtime-desktop/crates/webview-control/examples/host_harness.rs +338 -0
- package/runtime-desktop/crates/webview-control/src/embedded.rs +121 -0
- package/runtime-desktop/crates/webview-control/src/lib.rs +99 -0
- package/runtime-desktop/crates/webview-control/src/popup.rs +522 -0
- package/runtime-desktop/crates/webview-control/src/profiles.rs +57 -0
- package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +540 -0
- package/runtime-desktop/src-tauri/Cargo.lock +13 -0
- package/runtime-desktop/src-tauri/Cargo.toml +6 -1
- package/runtime-desktop/src-tauri/capabilities/default.json +1 -1
- package/runtime-desktop/src-tauri/src/bridge_mac.rs +126 -431
- package/runtime-desktop/src-tauri/src/browser_tab.rs +445 -0
- package/runtime-desktop/src-tauri/src/main.rs +90 -2
- package/runtime-desktop/src-tauri/src/popup_mac.rs +5 -519
- package/runtime-desktop/src-tauri/tauri.conf.json +1 -0
- package/src/cli.ts +26 -1
- package/src/config.ts +10 -1
|
@@ -1,351 +1,86 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Control-channel ADAPTER for the browser-bridge shim (bridge-shim/shim.ts).
|
|
2
|
+
//
|
|
3
|
+
// Phase 1 extraction: the WKWebView primitive (per-profile persistent+isolated stores, manual
|
|
4
|
+
// window creation, CSP-immune eval, snapshot, the bridgeShim handler, SSO popups) now lives in the
|
|
5
|
+
// `webview-control` crate. This file is the thin transport adapter that OWNS the bod-browser wire
|
|
6
|
+
// format + the TCP control channel:
|
|
7
|
+
// - dials the shim's TCP control port (line-JSON) and serves window ops,
|
|
8
|
+
// - frames the crate's RAW page payloads into {type:"pageMsg", payload} lines,
|
|
9
|
+
// - frames RAW eval/snapshot results into {id,result}/{id,error} responses,
|
|
10
|
+
// - keeps the legacy tauri-window path (no profileId) for back-compat.
|
|
2
11
|
// Env-gated: fully inert unless APPWRAP_BRIDGE_JS points at the built content bundle.
|
|
3
|
-
// The shell dials the shim's TCP control port (line-JSON) and serves window ops:
|
|
4
|
-
// createWindow { label, url } -> new WebviewWindow with the content bundle injected
|
|
5
|
-
// closeWindow { label }
|
|
6
|
-
// navigate { label, url }
|
|
7
|
-
// screenshot { label } -> WKWebView takeSnapshot -> PNG base64
|
|
8
|
-
// listWindows
|
|
9
12
|
use base64::Engine;
|
|
10
13
|
use serde_json::{json, Value};
|
|
11
14
|
use std::io::{Read, Write};
|
|
12
15
|
use std::net::TcpStream;
|
|
13
|
-
use std::sync::atomic::{AtomicBool, Ordering};
|
|
14
16
|
use std::sync::mpsc::{channel, Sender};
|
|
15
17
|
use std::sync::Mutex;
|
|
16
18
|
use std::time::Duration;
|
|
17
19
|
use tauri::{AppHandle, Manager};
|
|
18
20
|
|
|
19
21
|
use objc2::rc::Retained;
|
|
20
|
-
use objc2::
|
|
21
|
-
use
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
// ---------------------------------------------------------------------------
|
|
37
|
-
// Page -> shim transport: a native WKScriptMessageHandler ("bridgeShim").
|
|
38
|
-
// WebKit blocks ws://127.0.0.1 (and http beacons) from https pages as mixed
|
|
39
|
-
// content — NO loopback exemption like Chrome — so the content layer posts
|
|
40
|
-
// results via webkit.messageHandlers.bridgeShim.postMessage(json) instead,
|
|
41
|
-
// and we forward them to the shim as {type:"pageMsg", payload} lines.
|
|
42
|
-
// Commands travel shim -> evalJs(window.__bridgeDispatch(...)).
|
|
43
|
-
// ---------------------------------------------------------------------------
|
|
44
|
-
|
|
45
|
-
pub struct MsgHandlerIvars {
|
|
46
|
-
tx: Sender<String>,
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
define_class!(
|
|
50
|
-
#[unsafe(super(NSObject))]
|
|
51
|
-
#[thread_kind = MainThreadOnly]
|
|
52
|
-
#[name = "AppwrapBridgeMsgHandler"]
|
|
53
|
-
#[ivars = MsgHandlerIvars]
|
|
54
|
-
struct MsgHandler;
|
|
55
|
-
|
|
56
|
-
unsafe impl NSObjectProtocol for MsgHandler {}
|
|
57
|
-
|
|
58
|
-
unsafe impl WKScriptMessageHandler for MsgHandler {
|
|
59
|
-
#[unsafe(method(userContentController:didReceiveScriptMessage:))]
|
|
60
|
-
fn did_receive(&self, _ucc: &WKUserContentController, message: &WKScriptMessage) {
|
|
61
|
-
let body = unsafe { message.body() };
|
|
62
|
-
if let Ok(s) = body.downcast::<NSString>() {
|
|
63
|
-
let _ = self.ivars().tx.send(
|
|
64
|
-
json!({ "type": "pageMsg", "payload": s.to_string() }).to_string(),
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
// Per-profile persistent + isolated WKWebsiteDataStore (THE CRUX).
|
|
73
|
-
//
|
|
74
|
-
// A "profile" is a named, persistent cookie/localStorage/IndexedDB jar. On macOS
|
|
75
|
-
// 14+ WebKit gives us persistent, isolated stores via
|
|
76
|
-
// `WKWebsiteDataStore(forIdentifier:)` keyed by a stable NSUUID. We persist a
|
|
77
|
-
// profileId->UUID map on disk so the SAME store is reattached across app quits
|
|
78
|
-
// (persistence) while different profiles get different stores (isolation).
|
|
79
|
-
//
|
|
80
|
-
// Such a store can only be selected on a WKWebViewConfiguration BEFORE the
|
|
81
|
-
// webview is created, and tauri::WebviewWindowBuilder exposes no hook for it — so
|
|
82
|
-
// profile windows are created MANUALLY (NSWindow + WKWebView), mirroring
|
|
83
|
-
// popup_mac.rs. They live in their own registry (below); the bridge ops route to
|
|
84
|
-
// it first and fall back to tauri windows (the main shell window) otherwise.
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
mod profiles {
|
|
87
|
-
use objc2_foundation::NSUUID;
|
|
88
|
-
use std::collections::HashMap;
|
|
89
|
-
use std::path::PathBuf;
|
|
90
|
-
|
|
91
|
-
fn map_path() -> PathBuf {
|
|
92
|
-
let base = std::env::var("APPWRAP_PROFILE_DIR")
|
|
93
|
-
.map(PathBuf::from)
|
|
94
|
-
.unwrap_or_else(|_| {
|
|
95
|
-
PathBuf::from(std::env::var("HOME").unwrap_or_default())
|
|
96
|
-
.join("Library/Application Support/appwrap-desktop")
|
|
97
|
-
});
|
|
98
|
-
base.join("browser-profiles.json")
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
fn load() -> HashMap<String, String> {
|
|
102
|
-
std::fs::read_to_string(map_path())
|
|
103
|
-
.ok()
|
|
104
|
-
.and_then(|s| serde_json::from_str(&s).ok())
|
|
105
|
-
.unwrap_or_default()
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/// Stable UUID string for a profile id — generated once and persisted so the
|
|
109
|
-
/// same on-disk data store is reattached on every subsequent launch.
|
|
110
|
-
pub fn uuid_for(profile_id: &str) -> String {
|
|
111
|
-
let mut map = load();
|
|
112
|
-
if let Some(u) = map.get(profile_id) {
|
|
113
|
-
return u.clone();
|
|
114
|
-
}
|
|
115
|
-
let uuid = NSUUID::UUID().UUIDString().to_string();
|
|
116
|
-
map.insert(profile_id.to_string(), uuid.clone());
|
|
117
|
-
let p = map_path();
|
|
118
|
-
if let Some(dir) = p.parent() {
|
|
119
|
-
let _ = std::fs::create_dir_all(dir);
|
|
120
|
-
}
|
|
121
|
-
let _ = std::fs::write(&p, serde_json::to_string_pretty(&map).unwrap_or_default());
|
|
122
|
-
uuid
|
|
123
|
-
}
|
|
22
|
+
use objc2::MainThreadMarker;
|
|
23
|
+
use objc2_web_kit::WKWebView;
|
|
24
|
+
|
|
25
|
+
// The transport-free primitive. This adapter is the ONLY place that knows the bod-browser wire
|
|
26
|
+
// format; the crate stays automation-agnostic.
|
|
27
|
+
use webview_control as wv;
|
|
28
|
+
|
|
29
|
+
/// Build the page->shim message sink for one webview: wrap the crate's RAW payload into the
|
|
30
|
+
/// {type:"pageMsg", payload} envelope the shim expects. This is the seam — the framing lives here,
|
|
31
|
+
/// never in the crate.
|
|
32
|
+
fn page_sink(tx: Sender<String>) -> wv::MessageSink {
|
|
33
|
+
Box::new(move |raw: String| {
|
|
34
|
+
let _ = tx.send(json!({ "type": "pageMsg", "payload": raw }).to_string());
|
|
35
|
+
})
|
|
124
36
|
}
|
|
125
37
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
/// solely so the registry can be a static Mutex (same pattern as popup_mac).
|
|
129
|
-
struct ProfileWindow {
|
|
130
|
-
label: String,
|
|
131
|
-
profile_id: String,
|
|
132
|
-
window: Retained<NSWindow>,
|
|
133
|
-
webview: Retained<WKWebView>,
|
|
134
|
-
/// Set true by the window-close delegate on a USER (red-button) close. Entries
|
|
135
|
-
/// stay in the registry but are filtered out of lookups/listWindows, then reaped
|
|
136
|
-
/// at a safe point (reap_closed, not inside the delegate callback — which would
|
|
137
|
-
/// drop the delegate mid-call → UAF).
|
|
138
|
-
closed: AtomicBool,
|
|
139
|
-
_handler: Retained<MsgHandler>,
|
|
140
|
-
/// Per-window SSO-popup delegate (weak uiDelegate → held here). Opaque; lifetime only.
|
|
141
|
-
_popup_delegate: Retained<AnyObject>,
|
|
142
|
-
/// NSWindowDelegate that flags `closed` on a user close (weak setDelegate → held here).
|
|
143
|
-
_win_delegate: Retained<ProfileWinDelegate>,
|
|
38
|
+
fn page_port() -> u16 {
|
|
39
|
+
std::env::var("APPWRAP_BRIDGE_PAGE_PORT").ok().and_then(|s| s.parse().ok()).unwrap_or(9236)
|
|
144
40
|
}
|
|
145
|
-
unsafe impl Send for ProfileWindow {}
|
|
146
|
-
static PROFILE_WINDOWS: Mutex<Vec<ProfileWindow>> = Mutex::new(Vec::new());
|
|
147
|
-
|
|
148
|
-
// NSWindowDelegate for profile windows — flags the matching registry entry `closed`
|
|
149
|
-
// when the user closes the window (red button), so it drops out of listWindows/getTabs.
|
|
150
|
-
// Matches by NSWindow identity (the notification's object). Main-thread only.
|
|
151
|
-
define_class!(
|
|
152
|
-
#[unsafe(super(NSObject))]
|
|
153
|
-
#[thread_kind = MainThreadOnly]
|
|
154
|
-
#[name = "AppwrapProfileWinDelegate"]
|
|
155
|
-
struct ProfileWinDelegate;
|
|
156
41
|
|
|
157
|
-
|
|
42
|
+
/// The live control-channel page->shim sender, set for the duration of a connected `run()`. Lets a
|
|
43
|
+
/// webview created OUTSIDE the createWindow op (e.g. a browser sub-window in browser_tab.rs) route
|
|
44
|
+
/// its page beacons to the SAME shim, so it registers as a bod-browser tab. None when disconnected.
|
|
45
|
+
static PAGE_TX: Mutex<Option<Sender<String>>> = Mutex::new(None);
|
|
158
46
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
let v = PROFILE_WINDOWS.lock().unwrap();
|
|
165
|
-
if let Some(p) = v.iter().find(|p| Retained::as_ptr(&p.window) as usize == key) {
|
|
166
|
-
eprintln!("[bridge-shim] profile window {} user-closed", p.label);
|
|
167
|
-
p.closed.store(true, Ordering::Relaxed);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
47
|
+
/// Clears PAGE_TX on drop, so every `run()` return path (normal or error) leaves it None.
|
|
48
|
+
struct PageTxGuard;
|
|
49
|
+
impl Drop for PageTxGuard {
|
|
50
|
+
fn drop(&mut self) {
|
|
51
|
+
*PAGE_TX.lock().unwrap() = None;
|
|
170
52
|
}
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
/// Retained clone of a profile window's WKWebView (op routing), or None if the
|
|
174
|
-
/// label is a tauri window / unknown / already closed.
|
|
175
|
-
fn profile_webview(label: &str) -> Option<Retained<WKWebView>> {
|
|
176
|
-
PROFILE_WINDOWS
|
|
177
|
-
.lock()
|
|
178
|
-
.unwrap()
|
|
179
|
-
.iter()
|
|
180
|
-
.find(|p| p.label == label && !p.closed.load(Ordering::Relaxed))
|
|
181
|
-
.map(|p| p.webview.clone())
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
fn profile_labels() -> Vec<(String, String)> {
|
|
185
|
-
PROFILE_WINDOWS
|
|
186
|
-
.lock()
|
|
187
|
-
.unwrap()
|
|
188
|
-
.iter()
|
|
189
|
-
.filter(|p| !p.closed.load(Ordering::Relaxed))
|
|
190
|
-
.map(|p| (p.label.clone(), p.profile_id.clone()))
|
|
191
|
-
.collect()
|
|
192
53
|
}
|
|
193
54
|
|
|
194
|
-
///
|
|
195
|
-
///
|
|
196
|
-
///
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
/// The removed entry is held on the stack across window.close() so its window
|
|
205
|
-
/// delegate stays alive while AppKit fires windowWillClose.
|
|
206
|
-
fn profile_close(label: &str) -> bool {
|
|
207
|
-
let entry = {
|
|
208
|
-
let mut v = PROFILE_WINDOWS.lock().unwrap();
|
|
209
|
-
v.iter().position(|p| p.label == label).map(|i| v.remove(i))
|
|
210
|
-
};
|
|
211
|
-
match entry {
|
|
212
|
-
Some(p) => {
|
|
213
|
-
p.window.close();
|
|
214
|
-
true
|
|
55
|
+
/// Page->shim sink for a webview created outside `createWindow` (browser sub-windows). Unlike a
|
|
56
|
+
/// snapshot of the CURRENT PAGE_TX, this sink reads the LIVE control connection at send-time: a
|
|
57
|
+
/// sub-window opened while the shim is disconnected (PAGE_TX == None) installs this ONE sink and its
|
|
58
|
+
/// beacons are dropped for now, but the moment a later `run()` publishes a PAGE_TX the SAME installed
|
|
59
|
+
/// sink starts delivering — so the window SELF-HEALS into agent-exposure on shim (re)connect, with no
|
|
60
|
+
/// re-injection of the message handler. Framing stays identical to `page_sink`.
|
|
61
|
+
pub fn bridge_sink() -> wv::MessageSink {
|
|
62
|
+
Box::new(move |raw: String| {
|
|
63
|
+
if let Some(tx) = PAGE_TX.lock().unwrap().as_ref() {
|
|
64
|
+
let _ = tx.send(json!({ "type": "pageMsg", "payload": raw }).to_string());
|
|
215
65
|
}
|
|
216
|
-
|
|
217
|
-
}
|
|
66
|
+
})
|
|
218
67
|
}
|
|
219
68
|
|
|
220
|
-
///
|
|
221
|
-
///
|
|
222
|
-
///
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
url: &str,
|
|
228
|
-
profile_id: &str,
|
|
229
|
-
init_js: &str,
|
|
230
|
-
tx: Sender<String>,
|
|
231
|
-
) -> Result<Value, String> {
|
|
232
|
-
let uuid_str = profiles::uuid_for(profile_id);
|
|
233
|
-
let nsuuid = NSUUID::initWithUUIDString(NSUUID::alloc(), &NSString::from_str(&uuid_str))
|
|
234
|
-
.ok_or_else(|| format!("bad uuid for profile {profile_id}: {uuid_str}"))?;
|
|
235
|
-
let store = unsafe { WKWebsiteDataStore::dataStoreForIdentifier(&nsuuid, mtm) };
|
|
236
|
-
let persistent = unsafe { store.isPersistent() };
|
|
237
|
-
|
|
238
|
-
let cfg = unsafe { WKWebViewConfiguration::new(mtm) };
|
|
239
|
-
unsafe { cfg.setWebsiteDataStore(&store) };
|
|
240
|
-
|
|
241
|
-
// bridgeShim message handler + content bundle (documentStart) on this cfg's UCC.
|
|
242
|
-
let ucc = unsafe { cfg.userContentController() };
|
|
243
|
-
let handler = MsgHandler::alloc(mtm).set_ivars(MsgHandlerIvars { tx });
|
|
244
|
-
let handler: Retained<MsgHandler> = unsafe { objc2::msg_send![super(handler), init] };
|
|
245
|
-
unsafe {
|
|
246
|
-
ucc.addScriptMessageHandler_name(
|
|
247
|
-
ProtocolObject::from_ref(&*handler),
|
|
248
|
-
&NSString::from_str("bridgeShim"),
|
|
249
|
-
);
|
|
250
|
-
let script = WKUserScript::initWithSource_injectionTime_forMainFrameOnly(
|
|
251
|
-
WKUserScript::alloc(mtm),
|
|
252
|
-
&NSString::from_str(init_js),
|
|
253
|
-
WKUserScriptInjectionTime::AtDocumentStart,
|
|
254
|
-
true,
|
|
255
|
-
);
|
|
256
|
-
ucc.addUserScript(&script);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
let rect = NSRect::new(NSPoint::new(0.0, 0.0), NSSize::new(1100.0, 800.0));
|
|
260
|
-
let style = NSWindowStyleMask::Titled
|
|
261
|
-
| NSWindowStyleMask::Closable
|
|
262
|
-
| NSWindowStyleMask::Miniaturizable
|
|
263
|
-
| NSWindowStyleMask::Resizable;
|
|
264
|
-
let window = unsafe {
|
|
265
|
-
NSWindow::initWithContentRect_styleMask_backing_defer(
|
|
266
|
-
mtm.alloc::<NSWindow>(),
|
|
267
|
-
rect,
|
|
268
|
-
style,
|
|
269
|
-
NSBackingStoreType::Buffered,
|
|
270
|
-
false,
|
|
271
|
-
)
|
|
272
|
-
};
|
|
273
|
-
unsafe { window.setReleasedWhenClosed(false) };
|
|
274
|
-
|
|
275
|
-
let webview = unsafe {
|
|
276
|
-
WKWebView::initWithFrame_configuration(mtm.alloc::<WKWebView>(), window.frame(), &cfg)
|
|
69
|
+
/// Build the SAME framed init-JS `createWindow` injects, for a webview created elsewhere. Reads the
|
|
70
|
+
/// content bundle from `APPWRAP_BRIDGE_JS`; None when the feature is off (unset/empty) or the read
|
|
71
|
+
/// fails (logged). `label` becomes `window.__BRIDGE_LABEL` so the shim registers this webview by id.
|
|
72
|
+
pub fn bridge_init_js(label: &str) -> Option<String> {
|
|
73
|
+
let path = match std::env::var("APPWRAP_BRIDGE_JS") {
|
|
74
|
+
Ok(p) if !p.is_empty() => p,
|
|
75
|
+
_ => return None,
|
|
277
76
|
};
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
77
|
+
match std::fs::read_to_string(&path) {
|
|
78
|
+
Ok(bundle) => Some(format!(
|
|
79
|
+
"window.__BRIDGE_LABEL={};window.__BRIDGE_PORT={};\n{}",
|
|
80
|
+
serde_json::to_string(&label).unwrap(), page_port(), bundle
|
|
81
|
+
)),
|
|
82
|
+
Err(e) => { eprintln!("[bridge-shim] bridge_init_js read {path}: {e}"); None }
|
|
282
83
|
}
|
|
283
|
-
|
|
284
|
-
// Per-window SSO-popup delegate (window.open → real child NSWindow inheriting this profile's
|
|
285
|
-
// data store) + window-close delegate (drop from registry on user close). Both are weak refs on
|
|
286
|
-
// AppKit/WebKit → retained in the registry entry below.
|
|
287
|
-
let popup_delegate: Retained<AnyObject> = crate::popup_mac::install_for_profile(
|
|
288
|
-
Retained::as_ptr(&webview) as *mut std::ffi::c_void,
|
|
289
|
-
mtm,
|
|
290
|
-
);
|
|
291
|
-
let win_delegate = ProfileWinDelegate::alloc(mtm).set_ivars(());
|
|
292
|
-
let win_delegate: Retained<ProfileWinDelegate> = unsafe { msg_send![super(win_delegate), init] };
|
|
293
|
-
window.setDelegate(Some(ProtocolObject::from_ref(&*win_delegate)));
|
|
294
|
-
window.center();
|
|
295
|
-
window.makeKeyAndOrderFront(None);
|
|
296
|
-
|
|
297
|
-
let nsurl = NSURL::URLWithString(&NSString::from_str(url))
|
|
298
|
-
.ok_or_else(|| format!("bad url: {url}"))?;
|
|
299
|
-
let req = NSURLRequest::requestWithURL(&nsurl);
|
|
300
|
-
let _ = unsafe { webview.loadRequest(&req) };
|
|
301
|
-
|
|
302
|
-
reap_closed();
|
|
303
|
-
PROFILE_WINDOWS.lock().unwrap().push(ProfileWindow {
|
|
304
|
-
label: label.to_string(),
|
|
305
|
-
profile_id: profile_id.to_string(),
|
|
306
|
-
window,
|
|
307
|
-
webview,
|
|
308
|
-
closed: AtomicBool::new(false),
|
|
309
|
-
_handler: handler,
|
|
310
|
-
_popup_delegate: popup_delegate,
|
|
311
|
-
_win_delegate: win_delegate,
|
|
312
|
-
});
|
|
313
|
-
eprintln!(
|
|
314
|
-
"[bridge-shim] profile window {label} profile={profile_id} store={uuid_str} persistent={persistent}"
|
|
315
|
-
);
|
|
316
|
-
Ok(json!({ "ok": true, "label": label, "profileId": profile_id, "storeUuid": uuid_str, "persistent": persistent }))
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
/// callAsyncJavaScript on a raw WKWebView ptr — CSP-immune eval with an awaited
|
|
320
|
-
/// return value. Shared by the tauri and profile-window paths. Main-thread only.
|
|
321
|
-
unsafe fn eval_async_on(wk: *mut WKWebView, js: String, tx: Sender<String>, id: String) {
|
|
322
|
-
let wk = &*wk;
|
|
323
|
-
let mtm = MainThreadMarker::new().expect("eval_async_on runs on main");
|
|
324
|
-
let block = block2::RcBlock::new(move |result: *mut AnyObject, error: *mut NSError| {
|
|
325
|
-
if !error.is_null() {
|
|
326
|
-
respond(&tx, &id, Err((*error).localizedDescription().to_string()));
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
let out = if result.is_null() {
|
|
330
|
-
"null".to_string()
|
|
331
|
-
} else if let Ok(ns) = (&*result).downcast_ref::<NSString>().ok_or(()) {
|
|
332
|
-
ns.to_string()
|
|
333
|
-
} else {
|
|
334
|
-
"null".to_string()
|
|
335
|
-
};
|
|
336
|
-
respond(&tx, &id, Ok(json!({ "json": out })));
|
|
337
|
-
});
|
|
338
|
-
wk.callAsyncJavaScript_arguments_inFrame_inContentWorld_completionHandler(
|
|
339
|
-
&NSString::from_str(&js),
|
|
340
|
-
None,
|
|
341
|
-
None,
|
|
342
|
-
&WKContentWorld::pageWorld(mtm),
|
|
343
|
-
Some(&block),
|
|
344
|
-
);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
fn page_port() -> u16 {
|
|
348
|
-
std::env::var("APPWRAP_BRIDGE_PAGE_PORT").ok().and_then(|s| s.parse().ok()).unwrap_or(9236)
|
|
349
84
|
}
|
|
350
85
|
fn rust_port() -> u16 {
|
|
351
86
|
std::env::var("APPWRAP_BRIDGE_RUST_PORT").ok().and_then(|s| s.parse().ok()).unwrap_or(9237)
|
|
@@ -372,6 +107,14 @@ pub fn init(app: AppHandle) {
|
|
|
372
107
|
fn run(app: &AppHandle, mut stream: TcpStream, js_path: &str) {
|
|
373
108
|
let _ = stream.set_read_timeout(Some(Duration::from_millis(50)));
|
|
374
109
|
let (tx, rx) = channel::<String>();
|
|
110
|
+
// Publish this connection's sender so sub-window webviews (created outside createWindow) can route
|
|
111
|
+
// their page beacons here; the guard clears it on ANY return below (disconnect/error).
|
|
112
|
+
*PAGE_TX.lock().unwrap() = Some(tx.clone());
|
|
113
|
+
let _tx_guard = PageTxGuard;
|
|
114
|
+
// Self-heal sub-windows opened BEFORE this connect: their dynamic sink can now deliver, but a
|
|
115
|
+
// document already loaded won't re-fire its `register` beacon on its own. Nudge every open
|
|
116
|
+
// sub-window webview to re-post `register` so it (re)appears as a bod-browser tab on this shim.
|
|
117
|
+
crate::browser_tab::reregister_subwindows(app);
|
|
375
118
|
let mut acc = String::new();
|
|
376
119
|
let mut buf = [0u8; 65536];
|
|
377
120
|
loop {
|
|
@@ -427,15 +170,16 @@ fn handle_op(app: &AppHandle, tx: &Sender<String>, v: Value, js_path: &str) {
|
|
|
427
170
|
),
|
|
428
171
|
Err(e) => { respond(tx, &id, Err(format!("read bundle {js_path}: {e}"))); return; }
|
|
429
172
|
};
|
|
430
|
-
// Profile windows take the MANUAL creation path (per-profile persistent
|
|
173
|
+
// Profile windows take the crate's MANUAL creation path (per-profile persistent store).
|
|
431
174
|
// Windows without a profileId keep the original tauri path (back-compat).
|
|
432
175
|
if !profile_id.is_empty() {
|
|
433
176
|
let idc = id.clone();
|
|
434
177
|
let init_js2 = init_js.clone();
|
|
435
178
|
let r = app.run_on_main_thread(move || {
|
|
436
179
|
let mtm = MainThreadMarker::new().expect("run_on_main_thread is main");
|
|
437
|
-
let res = create_profile_window(mtm, &label, &url, &profile_id, &init_js2, tx2.clone())
|
|
438
|
-
|
|
180
|
+
let res = wv::create_profile_window(mtm, &label, &url, &profile_id, &init_js2, page_sink(tx2.clone()))
|
|
181
|
+
.map(|c| json!({ "ok": true, "label": label, "profileId": profile_id, "storeUuid": c.store_uuid, "persistent": c.persistent }));
|
|
182
|
+
respond(&tx2, &idc, res);
|
|
439
183
|
});
|
|
440
184
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
441
185
|
return;
|
|
@@ -444,45 +188,36 @@ fn handle_op(app: &AppHandle, tx: &Sender<String>, v: Value, js_path: &str) {
|
|
|
444
188
|
let r = app.run_on_main_thread(move || {
|
|
445
189
|
let parsed = match tauri::Url::parse(&url) {
|
|
446
190
|
Ok(u) => u,
|
|
447
|
-
Err(e) => {
|
|
191
|
+
Err(e) => { respond(&tx2, &idc, Err(format!("bad url: {e}"))); return; }
|
|
448
192
|
};
|
|
449
193
|
let r = tauri::WebviewWindowBuilder::new(&app2, &label, tauri::WebviewUrl::External(parsed))
|
|
450
194
|
.title(&label)
|
|
451
195
|
.inner_size(1100.0, 800.0)
|
|
452
|
-
// Safari-normalized UA at BUILD time (same constraint as the main window
|
|
453
|
-
// first document snapshots navigator.userAgent before any later override).
|
|
196
|
+
// Safari-normalized UA at BUILD time (same constraint as the main window).
|
|
454
197
|
.user_agent(crate::popup_mac::SAFARI_UA)
|
|
455
198
|
.initialization_script(&init_js)
|
|
456
199
|
.build();
|
|
457
|
-
// Install the page->shim message handler on the fresh webview (main thread),
|
|
458
|
-
//
|
|
459
|
-
// real child NSWindow — Google/Firebase SSO popups; silently dropped otherwise).
|
|
200
|
+
// Install the page->shim message handler on the fresh webview (main thread), plus the
|
|
201
|
+
// SAME popup machinery the main window gets (SSO window.open child windows).
|
|
460
202
|
if let Ok(w) = &r {
|
|
461
203
|
let tx4 = tx2.clone();
|
|
462
204
|
let _ = w.with_webview(move |pw| unsafe {
|
|
463
205
|
crate::popup_mac::install(pw.inner() as _, false);
|
|
464
|
-
let
|
|
465
|
-
|
|
466
|
-
let handler = MsgHandler::alloc(mtm).set_ivars(MsgHandlerIvars { tx: tx4 });
|
|
467
|
-
let handler: Retained<MsgHandler> = objc2::msg_send![super(handler), init];
|
|
468
|
-
let ucc = wk.configuration().userContentController();
|
|
469
|
-
ucc.addScriptMessageHandler_name(
|
|
470
|
-
ProtocolObject::from_ref(&*handler),
|
|
471
|
-
&NSString::from_str("bridgeShim"),
|
|
472
|
-
);
|
|
206
|
+
let mtm = MainThreadMarker::new().expect("with_webview runs on main");
|
|
207
|
+
wv::install_message_handler(pw.inner() as *mut WKWebView, mtm, page_sink(tx4));
|
|
473
208
|
});
|
|
474
209
|
}
|
|
475
|
-
|
|
210
|
+
respond(&tx2, &idc, r.map(|_| json!({ "ok": true, "label": label })).map_err(|e| e.to_string()));
|
|
476
211
|
});
|
|
477
212
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
478
213
|
}
|
|
479
214
|
"closeWindow" => {
|
|
480
215
|
let label = v["label"].as_str().unwrap_or_default().to_string();
|
|
481
|
-
if profile_webview(&label).is_some() {
|
|
216
|
+
if wv::profile_webview(&label).is_some() {
|
|
482
217
|
let idc = id.clone();
|
|
483
218
|
let r = app.run_on_main_thread(move || {
|
|
484
|
-
let ok =
|
|
485
|
-
|
|
219
|
+
let ok = wv::close(&label);
|
|
220
|
+
respond(&tx2, &idc, if ok { Ok(json!({ "ok": true })) } else { Err(format!("no window {label}")) });
|
|
486
221
|
});
|
|
487
222
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
488
223
|
return;
|
|
@@ -497,24 +232,18 @@ fn handle_op(app: &AppHandle, tx: &Sender<String>, v: Value, js_path: &str) {
|
|
|
497
232
|
let url = v["url"].as_str().unwrap_or_default().to_string();
|
|
498
233
|
let idc = id.clone();
|
|
499
234
|
let r = app.run_on_main_thread(move || {
|
|
500
|
-
if let Some(wk) = profile_webview(&label) {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
let req = NSURLRequest::requestWithURL(&nsurl);
|
|
504
|
-
let _ = unsafe { wk.loadRequest(&req) };
|
|
505
|
-
respond_owned(&tx2, &idc, Ok(json!({ "ok": true })));
|
|
506
|
-
}
|
|
507
|
-
None => respond_owned(&tx2, &idc, Err(format!("bad url: {url}"))),
|
|
508
|
-
}
|
|
235
|
+
if let Some(wk) = wv::profile_webview(&label) {
|
|
236
|
+
let res = unsafe { wv::navigate(Retained::as_ptr(&wk) as *mut WKWebView, &url) };
|
|
237
|
+
respond(&tx2, &idc, res.map(|_| json!({ "ok": true })));
|
|
509
238
|
return;
|
|
510
239
|
}
|
|
511
240
|
let parsed = match tauri::Url::parse(&url) {
|
|
512
241
|
Ok(u) => u,
|
|
513
|
-
Err(e) => {
|
|
242
|
+
Err(e) => { respond(&tx2, &idc, Err(format!("bad url: {e}"))); return; }
|
|
514
243
|
};
|
|
515
244
|
match app2.get_webview_window(&label) {
|
|
516
|
-
Some(
|
|
517
|
-
None =>
|
|
245
|
+
Some(w) => respond(&tx2, &idc, w.navigate(parsed).map(|_| json!({ "ok": true })).map_err(|e| e.to_string())),
|
|
246
|
+
None => respond(&tx2, &idc, Err(format!("no window {label}"))),
|
|
518
247
|
}
|
|
519
248
|
});
|
|
520
249
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
@@ -523,18 +252,22 @@ fn handle_op(app: &AppHandle, tx: &Sender<String>, v: Value, js_path: &str) {
|
|
|
523
252
|
let label = v["label"].as_str().unwrap_or_default().to_string();
|
|
524
253
|
let idc = id.clone();
|
|
525
254
|
let r = app.run_on_main_thread(move || {
|
|
526
|
-
if let Some(wk) = profile_webview(&label) {
|
|
527
|
-
|
|
255
|
+
if let Some(wk) = wv::profile_webview(&label) {
|
|
256
|
+
let tx3 = tx2.clone();
|
|
257
|
+
let id2 = idc.clone();
|
|
258
|
+
unsafe { wv::snapshot(Retained::as_ptr(&wk) as *mut WKWebView, move |r| respond_png(&tx3, &id2, r)) };
|
|
528
259
|
return;
|
|
529
260
|
}
|
|
530
261
|
let Some(w) = app2.get_webview_window(&label) else {
|
|
531
|
-
|
|
262
|
+
respond(&tx2, &idc, Err(format!("no window {label}")));
|
|
532
263
|
return;
|
|
533
264
|
};
|
|
534
265
|
let tx3 = tx2.clone();
|
|
535
266
|
let id2 = idc.clone();
|
|
536
|
-
let r = w.with_webview(move |pw| unsafe {
|
|
537
|
-
|
|
267
|
+
let r = w.with_webview(move |pw| unsafe {
|
|
268
|
+
wv::snapshot(pw.inner() as *mut WKWebView, move |r| respond_png(&tx3, &id2, r))
|
|
269
|
+
});
|
|
270
|
+
if let Err(e) = r { respond(&tx2, &idc, Err(e.to_string())); }
|
|
538
271
|
});
|
|
539
272
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
540
273
|
}
|
|
@@ -542,16 +275,14 @@ fn handle_op(app: &AppHandle, tx: &Sender<String>, v: Value, js_path: &str) {
|
|
|
542
275
|
// Fire-and-forget JS injection into a window (debug aid + transport fallback probe).
|
|
543
276
|
let label = v["label"].as_str().unwrap_or_default().to_string();
|
|
544
277
|
let js = v["js"].as_str().unwrap_or_default().to_string();
|
|
545
|
-
if profile_webview(&label).is_some() {
|
|
278
|
+
if wv::profile_webview(&label).is_some() {
|
|
546
279
|
let idc = id.clone();
|
|
547
280
|
let r = app.run_on_main_thread(move || {
|
|
548
|
-
if let Some(wk) = profile_webview(&label) {
|
|
549
|
-
unsafe {
|
|
550
|
-
|
|
551
|
-
}
|
|
552
|
-
respond_owned(&tx2, &idc, Ok(json!({ "ok": true })));
|
|
281
|
+
if let Some(wk) = wv::profile_webview(&label) {
|
|
282
|
+
unsafe { wv::inject_script(Retained::as_ptr(&wk) as *mut WKWebView, &js) };
|
|
283
|
+
respond(&tx2, &idc, Ok(json!({ "ok": true })));
|
|
553
284
|
} else {
|
|
554
|
-
|
|
285
|
+
respond(&tx2, &idc, Err(format!("no window {label}")));
|
|
555
286
|
}
|
|
556
287
|
});
|
|
557
288
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
@@ -563,63 +294,52 @@ fn handle_op(app: &AppHandle, tx: &Sender<String>, v: Value, js_path: &str) {
|
|
|
563
294
|
}
|
|
564
295
|
}
|
|
565
296
|
"evalAsync" => {
|
|
566
|
-
// CSP-immune eval with a real (awaited) return value —
|
|
567
|
-
//
|
|
568
|
-
// is exempt, and promises are awaited. `js` is a FUNCTION BODY (use `return`).
|
|
297
|
+
// CSP-immune eval with a real (awaited) return value — callAsyncJavaScript. `js` is a
|
|
298
|
+
// FUNCTION BODY (use `return`). The crate delivers the RAW return string; we frame {json}.
|
|
569
299
|
let label = v["label"].as_str().unwrap_or_default().to_string();
|
|
570
300
|
let js = v["js"].as_str().unwrap_or_default().to_string();
|
|
571
301
|
let idc = id.clone();
|
|
572
302
|
let r = app.run_on_main_thread(move || {
|
|
573
|
-
if let Some(wk) = profile_webview(&label) {
|
|
574
|
-
|
|
303
|
+
if let Some(wk) = wv::profile_webview(&label) {
|
|
304
|
+
let tx3 = tx2.clone();
|
|
305
|
+
let id2 = idc.clone();
|
|
306
|
+
unsafe { wv::eval_async(Retained::as_ptr(&wk) as *mut WKWebView, js.clone(), move |r| respond_eval(&tx3, &id2, r)) };
|
|
575
307
|
return;
|
|
576
308
|
}
|
|
577
309
|
let Some(w) = app2.get_webview_window(&label) else {
|
|
578
|
-
|
|
310
|
+
respond(&tx2, &idc, Err(format!("no window {label}")));
|
|
579
311
|
return;
|
|
580
312
|
};
|
|
581
313
|
let tx3 = tx2.clone();
|
|
582
314
|
let id2 = idc.clone();
|
|
583
315
|
let js2 = js.clone();
|
|
584
316
|
let r = w.with_webview(move |pw| unsafe {
|
|
585
|
-
|
|
317
|
+
wv::eval_async(pw.inner() as *mut WKWebView, js2, move |r| respond_eval(&tx3, &id2, r));
|
|
586
318
|
});
|
|
587
|
-
if let Err(e) = r {
|
|
319
|
+
if let Err(e) = r { respond(&tx2, &idc, Err(e.to_string())); }
|
|
588
320
|
});
|
|
589
321
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
590
322
|
}
|
|
591
323
|
"userClose" => {
|
|
592
|
-
// Verification aid: drive
|
|
593
|
-
//
|
|
594
|
-
// hygiene headlessly (accessibility clicks are TCC-gated in CI/agent contexts).
|
|
324
|
+
// Verification aid: drive the red-button path WITHOUT touching the registry, so the
|
|
325
|
+
// windowWillClose delegate is the sole remover — proves user-close hygiene headlessly.
|
|
595
326
|
let label = v["label"].as_str().unwrap_or_default().to_string();
|
|
596
327
|
let idc = id.clone();
|
|
597
328
|
let r = app.run_on_main_thread(move || {
|
|
598
|
-
let
|
|
599
|
-
|
|
600
|
-
.unwrap()
|
|
601
|
-
.iter()
|
|
602
|
-
.find(|p| p.label == label)
|
|
603
|
-
.map(|p| p.window.clone());
|
|
604
|
-
match win {
|
|
605
|
-
Some(w) => {
|
|
606
|
-
w.performClose(None);
|
|
607
|
-
respond_owned(&tx2, &idc, Ok(json!({ "ok": true })));
|
|
608
|
-
}
|
|
609
|
-
None => respond_owned(&tx2, &idc, Err(format!("no window {label}"))),
|
|
610
|
-
}
|
|
329
|
+
let ok = wv::user_close(&label);
|
|
330
|
+
respond(&tx2, &idc, if ok { Ok(json!({ "ok": true })) } else { Err(format!("no window {label}")) });
|
|
611
331
|
});
|
|
612
332
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
613
333
|
}
|
|
614
334
|
"listWindows" => {
|
|
615
|
-
// Closed windows are filtered by
|
|
616
|
-
//
|
|
335
|
+
// Closed windows are filtered by the crate; actual reaping (which drops objc objects) is
|
|
336
|
+
// deferred to createWindow, which runs on the main thread.
|
|
617
337
|
let mut labels: Vec<String> = app.webview_windows().keys().cloned().collect();
|
|
618
|
-
let profiles: Vec<Value> =
|
|
338
|
+
let profiles: Vec<Value> = wv::list_webviews()
|
|
619
339
|
.into_iter()
|
|
620
|
-
.map(|
|
|
621
|
-
labels.push(label.clone());
|
|
622
|
-
json!({ "label": label, "profileId": profile_id })
|
|
340
|
+
.map(|info| {
|
|
341
|
+
labels.push(info.label.clone());
|
|
342
|
+
json!({ "label": info.label, "profileId": info.profile_id })
|
|
623
343
|
})
|
|
624
344
|
.collect();
|
|
625
345
|
respond(tx, &id, Ok(json!({ "labels": labels, "profiles": profiles })));
|
|
@@ -628,37 +348,12 @@ fn handle_op(app: &AppHandle, tx: &Sender<String>, v: Value, js_path: &str) {
|
|
|
628
348
|
}
|
|
629
349
|
}
|
|
630
350
|
|
|
631
|
-
///
|
|
632
|
-
fn
|
|
633
|
-
respond(tx, id,
|
|
351
|
+
/// Frame a RAW eval return string into the {json} envelope the shim reads (`r.json`).
|
|
352
|
+
fn respond_eval(tx: &Sender<String>, id: &str, r: Result<String, String>) {
|
|
353
|
+
respond(tx, id, r.map(|out| json!({ "json": out })));
|
|
634
354
|
}
|
|
635
355
|
|
|
636
|
-
///
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
let wk = &*webview;
|
|
640
|
-
let block = block2::RcBlock::new(move |image: *mut NSImage, error: *mut NSError| {
|
|
641
|
-
if image.is_null() {
|
|
642
|
-
let msg = if error.is_null() { "snapshot failed".to_string() } else { (*error).localizedDescription().to_string() };
|
|
643
|
-
respond(&tx, &id, Err(msg));
|
|
644
|
-
return;
|
|
645
|
-
}
|
|
646
|
-
let img = &*image;
|
|
647
|
-
let Some(tiff) = img.TIFFRepresentation() else {
|
|
648
|
-
respond(&tx, &id, Err("no TIFF representation".into()));
|
|
649
|
-
return;
|
|
650
|
-
};
|
|
651
|
-
let Some(rep) = NSBitmapImageRep::initWithData(NSBitmapImageRep::alloc(), &tiff) else {
|
|
652
|
-
respond(&tx, &id, Err("bitmap rep failed".into()));
|
|
653
|
-
return;
|
|
654
|
-
};
|
|
655
|
-
let props = NSDictionary::new();
|
|
656
|
-
let Some(png) = rep.representationUsingType_properties(NSBitmapImageFileType::PNG, &props) else {
|
|
657
|
-
respond(&tx, &id, Err("png encode failed".into()));
|
|
658
|
-
return;
|
|
659
|
-
};
|
|
660
|
-
let b64 = base64::engine::general_purpose::STANDARD.encode(png.to_vec());
|
|
661
|
-
respond(&tx, &id, Ok(json!({ "png": b64 })));
|
|
662
|
-
});
|
|
663
|
-
wk.takeSnapshotWithConfiguration_completionHandler(None, &block);
|
|
356
|
+
/// Base64-encode the crate's RAW PNG bytes and frame into the {png} envelope.
|
|
357
|
+
fn respond_png(tx: &Sender<String>, id: &str, r: Result<Vec<u8>, String>) {
|
|
358
|
+
respond(tx, id, r.map(|bytes| json!({ "png": base64::engine::general_purpose::STANDARD.encode(bytes) })));
|
|
664
359
|
}
|