@livx.cc/appwrap 0.42.3 → 0.43.0
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 +4 -2
- package/runtime/app/app.ts +15 -0
- package/runtime/app/main-page.ts +11 -2
- package/runtime/app/shell/custom-webview.android.ts +81 -46
- package/runtime/app/shell/custom-webview.ios.ts +18 -8
- package/runtime/app/shell/handlers-android.ts +25 -6
- package/runtime/app/shell/handlers-background.ts +1 -1
- package/runtime/app/shell/web-quirks.ts +63 -0
- package/runtime/webpack.config.js +20 -0
- package/runtime-desktop/autotest/index.html +77 -0
- package/runtime-desktop/src-tauri/Cargo.lock +5459 -0
- package/runtime-desktop/src-tauri/Cargo.toml +38 -0
- package/runtime-desktop/src-tauri/build.rs +3 -0
- package/runtime-desktop/src-tauri/capabilities/default.json +6 -0
- package/runtime-desktop/src-tauri/icons/icon.png +0 -0
- package/runtime-desktop/src-tauri/shell_config.json +10 -0
- package/runtime-desktop/src-tauri/src/biometrics_mac.rs +138 -0
- package/runtime-desktop/src-tauri/src/main.rs +683 -0
- package/runtime-desktop/src-tauri/src/notifications_mac.rs +202 -0
- package/runtime-desktop/src-tauri/src/oauth_mac.rs +139 -0
- package/runtime-desktop/src-tauri/src/popup_mac.rs +466 -0
- package/runtime-desktop/src-tauri/src/push_mac.rs +193 -0
- package/runtime-desktop/src-tauri/src/sidecar.rs +398 -0
- package/runtime-desktop/src-tauri/tauri.conf.json +13 -0
- package/scripts/stage-assets.mjs +4 -2
- package/src/cli.ts +621 -48
- package/src/config.ts +40 -1
- package/src/desktop.ts +246 -0
- package/src/handlers.ts +71 -0
- package/src/icon.ts +207 -0
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[package]
|
|
2
|
+
name = "appwrap-desktop-shell"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
edition = "2021"
|
|
5
|
+
|
|
6
|
+
[build-dependencies]
|
|
7
|
+
tauri-build = { version = "2", features = [] }
|
|
8
|
+
|
|
9
|
+
[dependencies]
|
|
10
|
+
tauri = { version = "2", features = [] }
|
|
11
|
+
tauri-plugin-deep-link = "2"
|
|
12
|
+
tauri-plugin-window-state = "2"
|
|
13
|
+
tauri-plugin-notification = "2"
|
|
14
|
+
tauri-plugin-autostart = "2"
|
|
15
|
+
serde = { version = "1", features = ["derive"] }
|
|
16
|
+
serde_json = "1"
|
|
17
|
+
arboard = "3"
|
|
18
|
+
|
|
19
|
+
# macOS oauth.authorize — ASWebAuthenticationSession (system-browser auth sheet, exact parity with the
|
|
20
|
+
# iOS shell path in runtime/app/shell/handlers-oauth.ts: auto-close on redirect, user-cancel signal).
|
|
21
|
+
[target.'cfg(target_os = "macos")'.dependencies]
|
|
22
|
+
objc2 = "0.6"
|
|
23
|
+
objc2-foundation = "0.3"
|
|
24
|
+
objc2-app-kit = "0.3"
|
|
25
|
+
objc2-authentication-services = "0.3"
|
|
26
|
+
objc2-web-kit = "0.3"
|
|
27
|
+
objc2-local-authentication = "0.3" # Touch ID — LAContext (biometrics.available / .authenticate)
|
|
28
|
+
block2 = "0.6"
|
|
29
|
+
# local notifications (notifications.schedule): UNUserNotificationCenter + a delegate so a tapped
|
|
30
|
+
# notification carrying a deepLink routes through deeplink.open (tauri-plugin-notification has no click callback).
|
|
31
|
+
objc2-user-notifications = { version = "0.3", default-features = false, features = [
|
|
32
|
+
"std", "block2",
|
|
33
|
+
"UNUserNotificationCenter", "UNNotificationContent", "UNNotificationRequest",
|
|
34
|
+
"UNNotificationResponse", "UNNotificationTrigger", "UNNotification",
|
|
35
|
+
] }
|
|
36
|
+
|
|
37
|
+
[profile.dev]
|
|
38
|
+
incremental = true
|
|
Binary file
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// macOS Touch ID / biometrics — LocalAuthentication LAContext, exact parity with the iOS shell path
|
|
2
|
+
// (runtime/app/shell/handlers-extended.ts):
|
|
3
|
+
// biometrics.available → { available: bool, type: 'face' | 'touch' | 'none' }
|
|
4
|
+
// biometrics.authenticate → { success: true } OR rejects with code 'DENIED'
|
|
5
|
+
//
|
|
6
|
+
// Policy: LAPolicyDeviceOwnerAuthenticationWithBiometrics — biometrics ONLY, no passcode fallback.
|
|
7
|
+
// The kit's biometrics.authenticate(reason) contract carries NO "allow passcode" flag (see
|
|
8
|
+
// packages/native-kit/src/modules/biometrics.ts) and the iOS handler uses the WithBiometrics policy,
|
|
9
|
+
// so we mirror that exactly. (DeviceOwnerAuthentication — biometry-or-password — is intentionally not
|
|
10
|
+
// wired: it would over-deliver vs the mobile contract.)
|
|
11
|
+
//
|
|
12
|
+
// Threading: `available` (canEvaluatePolicy) is a pure query, safe off the main thread — computed on
|
|
13
|
+
// the worker thread and delivered inline. `authenticate` is initiated on the main thread (mirrors the
|
|
14
|
+
// other native lanes); its reply block runs on a private LA queue and delivers via the window. The
|
|
15
|
+
// LAContext MUST stay alive for the whole evaluation (ARC would otherwise cancel it mid-prompt), so it
|
|
16
|
+
// is parked in a static for the flight's lifetime — single-flight, like oauth/push.
|
|
17
|
+
use std::sync::atomic::{AtomicBool, Ordering};
|
|
18
|
+
use std::sync::Mutex;
|
|
19
|
+
|
|
20
|
+
use block2::RcBlock;
|
|
21
|
+
use objc2::rc::Retained;
|
|
22
|
+
use objc2::runtime::Bool;
|
|
23
|
+
use objc2_foundation::{NSError, NSString};
|
|
24
|
+
use objc2_local_authentication::{LABiometryType, LAContext, LAPolicy};
|
|
25
|
+
use serde_json::{json, Value};
|
|
26
|
+
|
|
27
|
+
use crate::deliver;
|
|
28
|
+
|
|
29
|
+
/// Human-readable diagnostic for an LAError availability code (kit shape only carries `available:bool`,
|
|
30
|
+
/// so this is for the shell log, not the response). Codes: LocalAuthentication LAError.
|
|
31
|
+
fn availability_reason(code: isize) -> &'static str {
|
|
32
|
+
match code {
|
|
33
|
+
-5 => "passcode not set",
|
|
34
|
+
-6 => "no biometric hardware",
|
|
35
|
+
-7 => "biometrics not enrolled",
|
|
36
|
+
-8 => "biometrics locked out (too many attempts)",
|
|
37
|
+
_ => "unavailable",
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
fn biometry_type_str(t: LABiometryType) -> &'static str {
|
|
42
|
+
match t {
|
|
43
|
+
LABiometryType::FaceID => "face",
|
|
44
|
+
LABiometryType::TouchID => "touch",
|
|
45
|
+
_ => "none",
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// `biometrics.available` — { available, type }. Synchronous; safe on the worker thread.
|
|
50
|
+
pub fn available() -> Value {
|
|
51
|
+
let ctx = unsafe { LAContext::new() };
|
|
52
|
+
// biometryType is only populated AFTER a canEvaluatePolicy call, so evaluate first.
|
|
53
|
+
let available = match unsafe { ctx.canEvaluatePolicy_error(LAPolicy::DeviceOwnerAuthenticationWithBiometrics) } {
|
|
54
|
+
Ok(()) => true,
|
|
55
|
+
Err(e) => {
|
|
56
|
+
let code = e.code();
|
|
57
|
+
eprintln!("[biometrics] unavailable: {} (code {code})", availability_reason(code));
|
|
58
|
+
false
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
let ty = biometry_type_str(unsafe { ctx.biometryType() });
|
|
62
|
+
json!({ "available": available, "type": ty })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Strong ref to the in-flight context, held for the evaluation's lifetime. Only ever touched here;
|
|
66
|
+
/// the Send impl exists purely so the (framework-confined) context can live in a static Mutex.
|
|
67
|
+
struct ActiveFlow(Option<Retained<LAContext>>);
|
|
68
|
+
unsafe impl Send for ActiveFlow {}
|
|
69
|
+
static ACTIVE: Mutex<ActiveFlow> = Mutex::new(ActiveFlow(None));
|
|
70
|
+
|
|
71
|
+
/// Synchronous single-flight reservation, claimed on the INVOKE thread before the main-thread hop
|
|
72
|
+
/// (mirrors main.rs OAUTH_IN_FLIGHT). Guarding on ACTIVE alone is a TOCTOU: ACTIVE is only set
|
|
73
|
+
/// inside the run_on_main_thread closure, so two quick authenticate calls could both pass the check
|
|
74
|
+
/// and the second context would overwrite (and drop) the first mid-prompt.
|
|
75
|
+
static IN_FLIGHT: AtomicBool = AtomicBool::new(false);
|
|
76
|
+
|
|
77
|
+
fn deliver_success(window: &tauri::WebviewWindow, id: &str) {
|
|
78
|
+
let resp = json!({ "v": 1, "id": id, "kind": "response", "result": { "success": true } });
|
|
79
|
+
deliver(window, &resp);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
fn deliver_denied(window: &tauri::WebviewWindow, id: &str, msg: &str) {
|
|
83
|
+
// Kit contract: authenticate rejects with 'DENIED' on user cancel/failure.
|
|
84
|
+
let resp = json!({ "v": 1, "id": id, "kind": "response", "error": { "code": "DENIED", "message": msg } });
|
|
85
|
+
deliver(window, &resp);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// `biometrics.authenticate` — prompt Touch ID; reply delivered from the LA completion block.
|
|
89
|
+
/// Single-flight: a second prompt while one is up is refused (only one LA sheet at a time).
|
|
90
|
+
pub fn authenticate(window: tauri::WebviewWindow, id: String, reason: String) {
|
|
91
|
+
// Atomic reservation BEFORE the main-thread hop — see IN_FLIGHT doc for why ACTIVE can't guard.
|
|
92
|
+
if IN_FLIGHT.swap(true, Ordering::SeqCst) {
|
|
93
|
+
return deliver_denied(&window, &id, "a biometric prompt is already in progress");
|
|
94
|
+
}
|
|
95
|
+
let reason = if reason.trim().is_empty() { "Authenticate".to_string() } else { reason };
|
|
96
|
+
let w = window.clone();
|
|
97
|
+
let fail_id = id.clone();
|
|
98
|
+
let res = window.run_on_main_thread(move || {
|
|
99
|
+
let ctx = unsafe { LAContext::new() };
|
|
100
|
+
|
|
101
|
+
let cb_window = w.clone();
|
|
102
|
+
let cb_id = id.clone();
|
|
103
|
+
let block = RcBlock::new(move |success: Bool, error: *mut NSError| {
|
|
104
|
+
// Release the parked context + reservation first — a follow-up authenticate must be
|
|
105
|
+
// legal from here.
|
|
106
|
+
ACTIVE.lock().unwrap().0 = None;
|
|
107
|
+
IN_FLIGHT.store(false, Ordering::SeqCst);
|
|
108
|
+
if success.as_bool() {
|
|
109
|
+
eprintln!("[biometrics] authenticate succeeded");
|
|
110
|
+
deliver_success(&cb_window, &cb_id);
|
|
111
|
+
} else {
|
|
112
|
+
let msg = if error.is_null() {
|
|
113
|
+
"authentication failed".to_string()
|
|
114
|
+
} else {
|
|
115
|
+
unsafe { (*error).localizedDescription() }.to_string()
|
|
116
|
+
};
|
|
117
|
+
eprintln!("[biometrics] authenticate failed: {msg}");
|
|
118
|
+
deliver_denied(&cb_window, &cb_id, &msg);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// Park the context BEFORE evaluate — the reply can fire from a private queue at any moment.
|
|
123
|
+
ACTIVE.lock().unwrap().0 = Some(ctx.clone());
|
|
124
|
+
unsafe {
|
|
125
|
+
ctx.evaluatePolicy_localizedReason_reply(
|
|
126
|
+
LAPolicy::DeviceOwnerAuthenticationWithBiometrics,
|
|
127
|
+
&NSString::from_str(&reason),
|
|
128
|
+
&block,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
eprintln!("[biometrics] evaluatePolicy started");
|
|
132
|
+
});
|
|
133
|
+
if let Err(e) = res {
|
|
134
|
+
ACTIVE.lock().unwrap().0 = None;
|
|
135
|
+
IN_FLIGHT.store(false, Ordering::SeqCst);
|
|
136
|
+
deliver_denied(&window, &fail_id, &format!("failed to reach main thread: {e}"));
|
|
137
|
+
}
|
|
138
|
+
}
|