@livx.cc/appwrap 0.46.1 → 0.46.2
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-desktop/bridge-shim/shim.ts +12 -197
- 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 +245 -0
- package/runtime-desktop/crates/webview-control/src/embedded.rs +100 -0
- package/runtime-desktop/crates/webview-control/src/lib.rs +95 -0
- package/runtime-desktop/crates/webview-control/src/popup.rs +519 -0
- package/runtime-desktop/crates/webview-control/src/profiles.rs +45 -0
- package/runtime-desktop/crates/webview-control/src/wkwebview_backend.rs +497 -0
- package/runtime-desktop/src-tauri/Cargo.lock +13 -0
- package/runtime-desktop/src-tauri/Cargo.toml +3 -0
- package/runtime-desktop/src-tauri/src/bridge_mac.rs +83 -441
- package/runtime-desktop/src-tauri/src/browser_tab.rs +193 -0
- package/runtime-desktop/src-tauri/src/main.rs +8 -1
- 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 +16 -0
- package/src/config.ts +10 -1
|
@@ -1,347 +1,37 @@
|
|
|
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
|
-
use std::sync::Mutex;
|
|
16
17
|
use std::time::Duration;
|
|
17
18
|
use tauri::{AppHandle, Manager};
|
|
18
19
|
|
|
19
20
|
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
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/// A manually-created profile window. Strong refs kept alive until closeWindow
|
|
127
|
-
/// (or windowWillClose). Only touched on the main thread; the unsafe Send exists
|
|
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>,
|
|
144
|
-
}
|
|
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
|
-
|
|
157
|
-
unsafe impl NSObjectProtocol for ProfileWinDelegate {}
|
|
158
|
-
|
|
159
|
-
unsafe impl NSWindowDelegate for ProfileWinDelegate {
|
|
160
|
-
#[unsafe(method(windowWillClose:))]
|
|
161
|
-
fn window_will_close(&self, notification: &NSNotification) {
|
|
162
|
-
let Some(obj) = notification.object() else { return };
|
|
163
|
-
let key = Retained::as_ptr(&obj) as usize;
|
|
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
|
-
}
|
|
170
|
-
}
|
|
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
|
-
}
|
|
193
|
-
|
|
194
|
-
/// Drop entries flagged `closed` (user-closed windows). Called at safe points only
|
|
195
|
-
/// (createWindow/listWindows/closeWindow entry) — never from within a delegate callback,
|
|
196
|
-
/// so the delegate isn't released mid-call.
|
|
197
|
-
fn reap_closed() {
|
|
198
|
-
let mut v = PROFILE_WINDOWS.lock().unwrap();
|
|
199
|
-
v.retain(|p| !p.closed.load(Ordering::Relaxed));
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
/// Close + drop a profile window (releases its NSWindow + WKWebView). Main-thread
|
|
203
|
-
/// only. Returns false if no such profile window (caller falls back to tauri).
|
|
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
|
|
215
|
-
}
|
|
216
|
-
None => false,
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
/// Create a manual NSWindow + WKWebView bound to `profile_id`'s persistent data
|
|
221
|
-
/// store, inject the bridge content bundle + bridgeShim handler, and register it.
|
|
222
|
-
/// Runs on the main thread (called inside run_on_main_thread). Returns the
|
|
223
|
-
/// isPersistent flag of the resolved store so the harness can assert it.
|
|
224
|
-
fn create_profile_window(
|
|
225
|
-
mtm: MainThreadMarker,
|
|
226
|
-
label: &str,
|
|
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)
|
|
277
|
-
};
|
|
278
|
-
unsafe {
|
|
279
|
-
webview.setCustomUserAgent(Some(&NSString::from_str(crate::popup_mac::SAFARI_UA)));
|
|
280
|
-
window.setContentView(Some(&webview));
|
|
281
|
-
window.setTitle(&NSString::from_str(label));
|
|
282
|
-
}
|
|
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
|
-
);
|
|
21
|
+
use objc2::MainThreadMarker;
|
|
22
|
+
use objc2_web_kit::WKWebView;
|
|
23
|
+
|
|
24
|
+
// The transport-free primitive. This adapter is the ONLY place that knows the bod-browser wire
|
|
25
|
+
// format; the crate stays automation-agnostic.
|
|
26
|
+
use webview_control as wv;
|
|
27
|
+
|
|
28
|
+
/// Build the page->shim message sink for one webview: wrap the crate's RAW payload into the
|
|
29
|
+
/// {type:"pageMsg", payload} envelope the shim expects. This is the seam — the framing lives here,
|
|
30
|
+
/// never in the crate.
|
|
31
|
+
fn page_sink(tx: Sender<String>) -> wv::MessageSink {
|
|
32
|
+
Box::new(move |raw: String| {
|
|
33
|
+
let _ = tx.send(json!({ "type": "pageMsg", "payload": raw }).to_string());
|
|
34
|
+
})
|
|
345
35
|
}
|
|
346
36
|
|
|
347
37
|
fn page_port() -> u16 {
|
|
@@ -427,15 +117,16 @@ fn handle_op(app: &AppHandle, tx: &Sender<String>, v: Value, js_path: &str) {
|
|
|
427
117
|
),
|
|
428
118
|
Err(e) => { respond(tx, &id, Err(format!("read bundle {js_path}: {e}"))); return; }
|
|
429
119
|
};
|
|
430
|
-
// Profile windows take the MANUAL creation path (per-profile persistent
|
|
120
|
+
// Profile windows take the crate's MANUAL creation path (per-profile persistent store).
|
|
431
121
|
// Windows without a profileId keep the original tauri path (back-compat).
|
|
432
122
|
if !profile_id.is_empty() {
|
|
433
123
|
let idc = id.clone();
|
|
434
124
|
let init_js2 = init_js.clone();
|
|
435
125
|
let r = app.run_on_main_thread(move || {
|
|
436
126
|
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
|
-
|
|
127
|
+
let res = wv::create_profile_window(mtm, &label, &url, &profile_id, &init_js2, page_sink(tx2.clone()))
|
|
128
|
+
.map(|c| json!({ "ok": true, "label": label, "profileId": profile_id, "storeUuid": c.store_uuid, "persistent": c.persistent }));
|
|
129
|
+
respond(&tx2, &idc, res);
|
|
439
130
|
});
|
|
440
131
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
441
132
|
return;
|
|
@@ -444,45 +135,36 @@ fn handle_op(app: &AppHandle, tx: &Sender<String>, v: Value, js_path: &str) {
|
|
|
444
135
|
let r = app.run_on_main_thread(move || {
|
|
445
136
|
let parsed = match tauri::Url::parse(&url) {
|
|
446
137
|
Ok(u) => u,
|
|
447
|
-
Err(e) => {
|
|
138
|
+
Err(e) => { respond(&tx2, &idc, Err(format!("bad url: {e}"))); return; }
|
|
448
139
|
};
|
|
449
140
|
let r = tauri::WebviewWindowBuilder::new(&app2, &label, tauri::WebviewUrl::External(parsed))
|
|
450
141
|
.title(&label)
|
|
451
142
|
.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).
|
|
143
|
+
// Safari-normalized UA at BUILD time (same constraint as the main window).
|
|
454
144
|
.user_agent(crate::popup_mac::SAFARI_UA)
|
|
455
145
|
.initialization_script(&init_js)
|
|
456
146
|
.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).
|
|
147
|
+
// Install the page->shim message handler on the fresh webview (main thread), plus the
|
|
148
|
+
// SAME popup machinery the main window gets (SSO window.open child windows).
|
|
460
149
|
if let Ok(w) = &r {
|
|
461
150
|
let tx4 = tx2.clone();
|
|
462
151
|
let _ = w.with_webview(move |pw| unsafe {
|
|
463
152
|
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
|
-
);
|
|
153
|
+
let mtm = MainThreadMarker::new().expect("with_webview runs on main");
|
|
154
|
+
wv::install_message_handler(pw.inner() as *mut WKWebView, mtm, page_sink(tx4));
|
|
473
155
|
});
|
|
474
156
|
}
|
|
475
|
-
|
|
157
|
+
respond(&tx2, &idc, r.map(|_| json!({ "ok": true, "label": label })).map_err(|e| e.to_string()));
|
|
476
158
|
});
|
|
477
159
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
478
160
|
}
|
|
479
161
|
"closeWindow" => {
|
|
480
162
|
let label = v["label"].as_str().unwrap_or_default().to_string();
|
|
481
|
-
if profile_webview(&label).is_some() {
|
|
163
|
+
if wv::profile_webview(&label).is_some() {
|
|
482
164
|
let idc = id.clone();
|
|
483
165
|
let r = app.run_on_main_thread(move || {
|
|
484
|
-
let ok =
|
|
485
|
-
|
|
166
|
+
let ok = wv::close(&label);
|
|
167
|
+
respond(&tx2, &idc, if ok { Ok(json!({ "ok": true })) } else { Err(format!("no window {label}")) });
|
|
486
168
|
});
|
|
487
169
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
488
170
|
return;
|
|
@@ -497,24 +179,18 @@ fn handle_op(app: &AppHandle, tx: &Sender<String>, v: Value, js_path: &str) {
|
|
|
497
179
|
let url = v["url"].as_str().unwrap_or_default().to_string();
|
|
498
180
|
let idc = id.clone();
|
|
499
181
|
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
|
-
}
|
|
182
|
+
if let Some(wk) = wv::profile_webview(&label) {
|
|
183
|
+
let res = unsafe { wv::navigate(Retained::as_ptr(&wk) as *mut WKWebView, &url) };
|
|
184
|
+
respond(&tx2, &idc, res.map(|_| json!({ "ok": true })));
|
|
509
185
|
return;
|
|
510
186
|
}
|
|
511
187
|
let parsed = match tauri::Url::parse(&url) {
|
|
512
188
|
Ok(u) => u,
|
|
513
|
-
Err(e) => {
|
|
189
|
+
Err(e) => { respond(&tx2, &idc, Err(format!("bad url: {e}"))); return; }
|
|
514
190
|
};
|
|
515
191
|
match app2.get_webview_window(&label) {
|
|
516
|
-
Some(
|
|
517
|
-
None =>
|
|
192
|
+
Some(w) => respond(&tx2, &idc, w.navigate(parsed).map(|_| json!({ "ok": true })).map_err(|e| e.to_string())),
|
|
193
|
+
None => respond(&tx2, &idc, Err(format!("no window {label}"))),
|
|
518
194
|
}
|
|
519
195
|
});
|
|
520
196
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
@@ -523,18 +199,22 @@ fn handle_op(app: &AppHandle, tx: &Sender<String>, v: Value, js_path: &str) {
|
|
|
523
199
|
let label = v["label"].as_str().unwrap_or_default().to_string();
|
|
524
200
|
let idc = id.clone();
|
|
525
201
|
let r = app.run_on_main_thread(move || {
|
|
526
|
-
if let Some(wk) = profile_webview(&label) {
|
|
527
|
-
|
|
202
|
+
if let Some(wk) = wv::profile_webview(&label) {
|
|
203
|
+
let tx3 = tx2.clone();
|
|
204
|
+
let id2 = idc.clone();
|
|
205
|
+
unsafe { wv::snapshot(Retained::as_ptr(&wk) as *mut WKWebView, move |r| respond_png(&tx3, &id2, r)) };
|
|
528
206
|
return;
|
|
529
207
|
}
|
|
530
208
|
let Some(w) = app2.get_webview_window(&label) else {
|
|
531
|
-
|
|
209
|
+
respond(&tx2, &idc, Err(format!("no window {label}")));
|
|
532
210
|
return;
|
|
533
211
|
};
|
|
534
212
|
let tx3 = tx2.clone();
|
|
535
213
|
let id2 = idc.clone();
|
|
536
|
-
let r = w.with_webview(move |pw| unsafe {
|
|
537
|
-
|
|
214
|
+
let r = w.with_webview(move |pw| unsafe {
|
|
215
|
+
wv::snapshot(pw.inner() as *mut WKWebView, move |r| respond_png(&tx3, &id2, r))
|
|
216
|
+
});
|
|
217
|
+
if let Err(e) = r { respond(&tx2, &idc, Err(e.to_string())); }
|
|
538
218
|
});
|
|
539
219
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
540
220
|
}
|
|
@@ -542,16 +222,14 @@ fn handle_op(app: &AppHandle, tx: &Sender<String>, v: Value, js_path: &str) {
|
|
|
542
222
|
// Fire-and-forget JS injection into a window (debug aid + transport fallback probe).
|
|
543
223
|
let label = v["label"].as_str().unwrap_or_default().to_string();
|
|
544
224
|
let js = v["js"].as_str().unwrap_or_default().to_string();
|
|
545
|
-
if profile_webview(&label).is_some() {
|
|
225
|
+
if wv::profile_webview(&label).is_some() {
|
|
546
226
|
let idc = id.clone();
|
|
547
227
|
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 })));
|
|
228
|
+
if let Some(wk) = wv::profile_webview(&label) {
|
|
229
|
+
unsafe { wv::inject_script(Retained::as_ptr(&wk) as *mut WKWebView, &js) };
|
|
230
|
+
respond(&tx2, &idc, Ok(json!({ "ok": true })));
|
|
553
231
|
} else {
|
|
554
|
-
|
|
232
|
+
respond(&tx2, &idc, Err(format!("no window {label}")));
|
|
555
233
|
}
|
|
556
234
|
});
|
|
557
235
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
@@ -563,63 +241,52 @@ fn handle_op(app: &AppHandle, tx: &Sender<String>, v: Value, js_path: &str) {
|
|
|
563
241
|
}
|
|
564
242
|
}
|
|
565
243
|
"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`).
|
|
244
|
+
// CSP-immune eval with a real (awaited) return value — callAsyncJavaScript. `js` is a
|
|
245
|
+
// FUNCTION BODY (use `return`). The crate delivers the RAW return string; we frame {json}.
|
|
569
246
|
let label = v["label"].as_str().unwrap_or_default().to_string();
|
|
570
247
|
let js = v["js"].as_str().unwrap_or_default().to_string();
|
|
571
248
|
let idc = id.clone();
|
|
572
249
|
let r = app.run_on_main_thread(move || {
|
|
573
|
-
if let Some(wk) = profile_webview(&label) {
|
|
574
|
-
|
|
250
|
+
if let Some(wk) = wv::profile_webview(&label) {
|
|
251
|
+
let tx3 = tx2.clone();
|
|
252
|
+
let id2 = idc.clone();
|
|
253
|
+
unsafe { wv::eval_async(Retained::as_ptr(&wk) as *mut WKWebView, js.clone(), move |r| respond_eval(&tx3, &id2, r)) };
|
|
575
254
|
return;
|
|
576
255
|
}
|
|
577
256
|
let Some(w) = app2.get_webview_window(&label) else {
|
|
578
|
-
|
|
257
|
+
respond(&tx2, &idc, Err(format!("no window {label}")));
|
|
579
258
|
return;
|
|
580
259
|
};
|
|
581
260
|
let tx3 = tx2.clone();
|
|
582
261
|
let id2 = idc.clone();
|
|
583
262
|
let js2 = js.clone();
|
|
584
263
|
let r = w.with_webview(move |pw| unsafe {
|
|
585
|
-
|
|
264
|
+
wv::eval_async(pw.inner() as *mut WKWebView, js2, move |r| respond_eval(&tx3, &id2, r));
|
|
586
265
|
});
|
|
587
|
-
if let Err(e) = r {
|
|
266
|
+
if let Err(e) = r { respond(&tx2, &idc, Err(e.to_string())); }
|
|
588
267
|
});
|
|
589
268
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
590
269
|
}
|
|
591
270
|
"userClose" => {
|
|
592
|
-
// Verification aid: drive
|
|
593
|
-
//
|
|
594
|
-
// hygiene headlessly (accessibility clicks are TCC-gated in CI/agent contexts).
|
|
271
|
+
// Verification aid: drive the red-button path WITHOUT touching the registry, so the
|
|
272
|
+
// windowWillClose delegate is the sole remover — proves user-close hygiene headlessly.
|
|
595
273
|
let label = v["label"].as_str().unwrap_or_default().to_string();
|
|
596
274
|
let idc = id.clone();
|
|
597
275
|
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
|
-
}
|
|
276
|
+
let ok = wv::user_close(&label);
|
|
277
|
+
respond(&tx2, &idc, if ok { Ok(json!({ "ok": true })) } else { Err(format!("no window {label}")) });
|
|
611
278
|
});
|
|
612
279
|
if let Err(e) = r { respond(tx, &id, Err(e.to_string())); }
|
|
613
280
|
}
|
|
614
281
|
"listWindows" => {
|
|
615
|
-
// Closed windows are filtered by
|
|
616
|
-
//
|
|
282
|
+
// Closed windows are filtered by the crate; actual reaping (which drops objc objects) is
|
|
283
|
+
// deferred to createWindow, which runs on the main thread.
|
|
617
284
|
let mut labels: Vec<String> = app.webview_windows().keys().cloned().collect();
|
|
618
|
-
let profiles: Vec<Value> =
|
|
285
|
+
let profiles: Vec<Value> = wv::list_webviews()
|
|
619
286
|
.into_iter()
|
|
620
|
-
.map(|
|
|
621
|
-
labels.push(label.clone());
|
|
622
|
-
json!({ "label": label, "profileId": profile_id })
|
|
287
|
+
.map(|info| {
|
|
288
|
+
labels.push(info.label.clone());
|
|
289
|
+
json!({ "label": info.label, "profileId": info.profile_id })
|
|
623
290
|
})
|
|
624
291
|
.collect();
|
|
625
292
|
respond(tx, &id, Ok(json!({ "labels": labels, "profiles": profiles })));
|
|
@@ -628,37 +295,12 @@ fn handle_op(app: &AppHandle, tx: &Sender<String>, v: Value, js_path: &str) {
|
|
|
628
295
|
}
|
|
629
296
|
}
|
|
630
297
|
|
|
631
|
-
///
|
|
632
|
-
fn
|
|
633
|
-
respond(tx, id,
|
|
298
|
+
/// Frame a RAW eval return string into the {json} envelope the shim reads (`r.json`).
|
|
299
|
+
fn respond_eval(tx: &Sender<String>, id: &str, r: Result<String, String>) {
|
|
300
|
+
respond(tx, id, r.map(|out| json!({ "json": out })));
|
|
634
301
|
}
|
|
635
302
|
|
|
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);
|
|
303
|
+
/// Base64-encode the crate's RAW PNG bytes and frame into the {png} envelope.
|
|
304
|
+
fn respond_png(tx: &Sender<String>, id: &str, r: Result<Vec<u8>, String>) {
|
|
305
|
+
respond(tx, id, r.map(|bytes| json!({ "png": base64::engine::general_purpose::STANDARD.encode(bytes) })));
|
|
664
306
|
}
|