Skip to main content

bitwarden_autotype/
wasm.rs

1//! WASM bindings for the autotype IPC requests, exposed to the clients through
2//! `@bitwarden/sdk-internal`.
3//!
4//! The `AutotypeDriver` interface is implemented by the user of this crate (for
5//! example, within the Bitwarden desktop app).
6
7use bitwarden_ipc::{
8    Endpoint, IpcClientExt, RequestError, RpcHandler, RpcRequest, RpcRequestInfo, Source,
9    wasm::JsIpcClient,
10};
11use bitwarden_threading::{
12    ThreadBoundRunner,
13    cancellation_token::wasm::{AbortSignal, AbortSignalExt},
14};
15use serde::{Deserialize, Serialize};
16use tsify::serde_wasm_bindgen;
17use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen};
18use wasm_bindgen_futures::js_sys;
19
20#[wasm_bindgen(typescript_custom_section)]
21/// `TS_AUTOTYPE_TYPES` defines the TypeScript types for bindgen.
22/// The AutotypeDriver interface is a TS interface that will be implemented
23/// by the user of this crate (e.g. in the Bitwarden desktop app).
24const TS_AUTOTYPE_TYPES: &'static str = r#"
25export interface AutotypeDriver {
26    /**
27     * Enable or disable autotype. Returns if setting
28     * the enabled state was successful or not.
29     */
30    set_autotype_enabled(enabled: boolean): Promise<boolean>;
31
32    /**
33     * Set the keyboard shortcut. Returns a boolean representing
34     * if setting the shortcut was successful or not.
35     */
36    set_autotype_keyboard_shortcut(shortcut: string[]): Promise<boolean>;
37
38    /**
39     * List the applications currently running. Returns an empty
40     * array if none could be determined. Entries that do not match
41     * `AutotypeRunningApp` are skipped.
42     */
43    list_running_apps(): Promise<AutotypeRunningApp[]>;
44}
45"#;
46
47#[wasm_bindgen]
48extern "C" {
49    /// JavaScript implementation of the various Autotype operations.
50    #[wasm_bindgen(js_name = AutotypeDriver, typescript_type = "AutotypeDriver")]
51    pub type RawJsAutotypeDriver;
52
53    /// Enable or disable autotype. Returns if setting the enabled state was successful or not.
54    #[wasm_bindgen(method, catch)]
55    async fn set_autotype_enabled(
56        this: &RawJsAutotypeDriver,
57        enabled: bool,
58    ) -> Result<JsValue, JsValue>;
59
60    /// Set the autotype keyboard shortcut. Returns a boolean
61    /// representing if setting the shortcut was successful or not.
62    #[wasm_bindgen(method, catch)]
63    async fn set_autotype_keyboard_shortcut(
64        this: &RawJsAutotypeDriver,
65        shortcut: Vec<String>,
66    ) -> Result<JsValue, JsValue>;
67
68    /// List the applications currently running. The driver returns an `AutotypeRunningApp[]`,
69    /// received here as a raw `JsValue` so `JsAutotypeDriver::list_running_apps` can skip
70    /// entries that don't match `AutotypeRunningApp`.
71    #[wasm_bindgen(method, catch)]
72    async fn list_running_apps(this: &RawJsAutotypeDriver) -> Result<JsValue, JsValue>;
73}
74
75/// A running application.
76#[derive(Debug, Clone, Serialize, Deserialize)]
77#[bitwarden_ffi::wasm_record]
78pub struct AutotypeRunningApp {
79    /// Human-readable name for the app, e.g. `Google Chrome`.
80    pub name: String,
81    /// Path to the app's executable.
82    pub path: String,
83}
84
85/// Wraps the JavaScript driver in a [`ThreadBoundRunner`] so it can be used from the `Send`
86/// futures an [`RpcHandler`] is required to return.
87struct JsAutotypeDriver {
88    runner: ThreadBoundRunner<RawJsAutotypeDriver>,
89}
90
91impl JsAutotypeDriver {
92    fn new(runner: ThreadBoundRunner<RawJsAutotypeDriver>) -> Self {
93        Self { runner }
94    }
95
96    /// Sets the Autotype enabled state, and returns if setting the
97    /// enabled state was successful or not.
98    async fn set_autotype_enabled(&self, enabled: bool) -> bool {
99        self.runner
100            .run_in_thread(move |driver| async move {
101                match driver.set_autotype_enabled(enabled).await {
102                    Ok(value) => value.as_bool().unwrap_or_else(|| {
103                        tracing::error!(
104                            "Autotype driver returned a non-boolean set-enabled result"
105                        );
106                        false
107                    }),
108                    Err(error) => {
109                        tracing::error!(?error, "Autotype driver failed to set the enabled state");
110                        false
111                    }
112                }
113            })
114            .await
115            .unwrap_or_else(|error| {
116                tracing::error!(?error, "Autotype runner failed to set the enabled state");
117                false
118            })
119    }
120
121    /// Sets the keyboard shortcut used for executing Autotype.
122    /// Returns if setting the keyboard shortcut was successful or not.
123    async fn set_autotype_keyboard_shortcut(&self, shortcut: Vec<String>) -> bool {
124        self.runner
125            .run_in_thread(move |driver| async move {
126                match driver.set_autotype_keyboard_shortcut(shortcut).await {
127                    Ok(value) => value.as_bool().unwrap_or_else(|| {
128                        tracing::error!("Autotype driver returned a non-boolean shortcut result");
129                        false
130                    }),
131                    Err(error) => {
132                        tracing::error!(?error, "Autotype driver failed to set the shortcut");
133                        false
134                    }
135                }
136            })
137            .await
138            .unwrap_or_else(|error| {
139                tracing::error!(?error, "Autotype runner failed to set the shortcut");
140                false
141            })
142    }
143
144    /// Retrieves the currently running apps.
145    /// Returns a Vec of `AutotypeRunningApp`. Apps that don't match the
146    /// `AutotypeRunningApp` model are filtered out.
147    async fn list_running_apps(&self) -> Vec<AutotypeRunningApp> {
148        self.runner
149            .run_in_thread(|driver| async move {
150                let value = match driver.list_running_apps().await {
151                    Ok(value) => value,
152                    Err(error) => {
153                        tracing::error!(?error, "Autotype driver failed to list running apps");
154                        return Vec::new();
155                    }
156                };
157
158                // This converts value (a `JsValue`) into an Array.
159                // This is critical for rejecting `null`, `undefined`, and other non-array values
160                // that could have been passed from the JavaScript side.
161                let Ok(array) = value.dyn_into::<js_sys::Array>() else {
162                    tracing::warn!("Autotype driver returned a non-array of running apps");
163                    return Vec::new();
164                };
165
166                // Filter through `array`, keeping entries that deserialize correctly
167                // into the `AutotypeRunningApp` type. This filters out (skips) bad entries.
168                array
169                    .iter()
170                    .filter_map(|entry| match serde_wasm_bindgen::from_value(entry) {
171                        Ok(app) => Some(app),
172                        Err(error) => {
173                            tracing::warn!(
174                                ?error,
175                                "Autotype driver returned a malformed running app; skipping it"
176                            );
177                            None
178                        }
179                    })
180                    .collect()
181            })
182            .await
183            .unwrap_or_else(|error| {
184                tracing::error!(?error, "Autotype runner failed to list running apps");
185                Vec::new()
186            })
187    }
188}
189
190/// Determines if a request came from the desktop render process.
191///
192/// A rejection is logged with the request's
193/// [`RpcRequest::NAME`] and the source it came from.
194fn from_renderer(info: &RpcRequestInfo, request: &str) -> bool {
195    let allowed = matches!(info.source, Source::DesktopRenderer);
196
197    if !allowed {
198        tracing::warn!(
199            request,
200            source = ?info.source,
201            "Autotype rejected a request from a non-renderer source"
202        );
203    }
204    allowed
205}
206
207/// A request asking the receiving client to enable or disable Autotype.
208#[derive(Debug, Clone, Serialize, Deserialize)]
209pub struct AutotypeSetEnabledRequest {
210    /// Whether autotype should be enabled.
211    pub enabled: bool,
212}
213
214/// A response to an [`AutotypeSetEnabledRequest`].
215#[derive(Debug, Clone, Serialize, Deserialize)]
216#[bitwarden_ffi::wasm_record]
217pub struct AutotypeSetEnabledResponse {
218    /// Whether the requested change for enabling/disabling Autotype was applied.
219    pub success: bool,
220}
221
222impl RpcRequest for AutotypeSetEnabledRequest {
223    type Response = AutotypeSetEnabledResponse;
224
225    const NAME: &str = "AutotypeSetEnabledRequest";
226}
227
228/// An [`RpcHandler`] that applies the requested state through a [`RawJsAutotypeDriver`].
229///
230/// Only requests from the desktop renderer are applied; any other source is refused with
231/// `success: false` before the driver is called. See [`from_renderer`].
232struct AutotypeSetEnabledHandler {
233    driver: JsAutotypeDriver,
234}
235
236impl AutotypeSetEnabledHandler {
237    fn new(driver: JsAutotypeDriver) -> Self {
238        Self { driver }
239    }
240}
241
242impl RpcHandler for AutotypeSetEnabledHandler {
243    type Request = AutotypeSetEnabledRequest;
244
245    async fn handle(
246        &self,
247        request: Self::Request,
248        info: RpcRequestInfo,
249    ) -> AutotypeSetEnabledResponse {
250        if !from_renderer(&info, Self::Request::NAME) {
251            return AutotypeSetEnabledResponse { success: false };
252        }
253
254        AutotypeSetEnabledResponse {
255            success: self.driver.set_autotype_enabled(request.enabled).await,
256        }
257    }
258}
259
260/// A request asking the receiving client to change the keyboard shortcut Autotype is triggered by.
261#[derive(Debug, Clone, Serialize, Deserialize)]
262pub struct AutotypeSetKeyboardShortcutRequest {
263    /// The shortcut used to execute Autotype.
264    pub shortcut: Vec<String>,
265}
266
267/// A response to an [`AutotypeSetKeyboardShortcutRequest`].
268#[derive(Debug, Clone, Serialize, Deserialize)]
269#[bitwarden_ffi::wasm_record]
270pub struct AutotypeSetKeyboardShortcutResponse {
271    /// Whether the requested keyboard shortcut was applied.
272    pub success: bool,
273}
274
275impl RpcRequest for AutotypeSetKeyboardShortcutRequest {
276    type Response = AutotypeSetKeyboardShortcutResponse;
277
278    const NAME: &str = "AutotypeSetKeyboardShortcutRequest";
279}
280
281/// An [`RpcHandler`] that applies the requested shortcut through a [`RawJsAutotypeDriver`].
282///
283/// Only requests from the desktop renderer are applied; any other source is refused with
284/// `success: false` before the shortcut reaches the driver. See [`from_renderer`].
285struct AutotypeSetKeyboardShortcutHandler {
286    driver: JsAutotypeDriver,
287}
288
289impl AutotypeSetKeyboardShortcutHandler {
290    fn new(driver: JsAutotypeDriver) -> Self {
291        Self { driver }
292    }
293}
294
295impl RpcHandler for AutotypeSetKeyboardShortcutHandler {
296    type Request = AutotypeSetKeyboardShortcutRequest;
297
298    async fn handle(
299        &self,
300        request: Self::Request,
301        info: RpcRequestInfo,
302    ) -> AutotypeSetKeyboardShortcutResponse {
303        if !from_renderer(&info, Self::Request::NAME) {
304            return AutotypeSetKeyboardShortcutResponse { success: false };
305        }
306
307        AutotypeSetKeyboardShortcutResponse {
308            success: self
309                .driver
310                .set_autotype_keyboard_shortcut(request.shortcut)
311                .await,
312        }
313    }
314}
315
316/// A request asking the receiving client for the applications that are currently running.
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct AutotypeListRunningAppsRequest;
319
320/// A response to an [`AutotypeListRunningAppsRequest`].
321#[derive(Debug, Clone, Serialize, Deserialize)]
322#[bitwarden_ffi::wasm_record]
323pub struct AutotypeListRunningAppsResponse {
324    /// The applications currently running.
325    pub apps: Vec<AutotypeRunningApp>,
326}
327
328impl RpcRequest for AutotypeListRunningAppsRequest {
329    type Response = AutotypeListRunningAppsResponse;
330
331    const NAME: &str = "AutotypeListRunningAppsRequest";
332}
333
334/// An [`RpcHandler`] that lists running applications through a [`RawJsAutotypeDriver`].
335///
336/// Only requests from the desktop renderer are answered; any other source gets an empty list
337/// without the driver being asked to enumerate anything. See [`from_renderer`].
338struct AutotypeListRunningAppsHandler {
339    driver: JsAutotypeDriver,
340}
341
342impl AutotypeListRunningAppsHandler {
343    fn new(driver: JsAutotypeDriver) -> Self {
344        Self { driver }
345    }
346}
347
348impl RpcHandler for AutotypeListRunningAppsHandler {
349    type Request = AutotypeListRunningAppsRequest;
350
351    async fn handle(
352        &self,
353        _: Self::Request,
354        info: RpcRequestInfo,
355    ) -> AutotypeListRunningAppsResponse {
356        if !from_renderer(&info, Self::Request::NAME) {
357            return AutotypeListRunningAppsResponse { apps: Vec::new() };
358        }
359
360        AutotypeListRunningAppsResponse {
361            apps: self.driver.list_running_apps().await,
362        }
363    }
364}
365
366/// This registers the Autotype handlers so the client responds to Autotype requests through
367/// the `RawJsAutotypeDriver`.
368#[bitwarden_ffi::wasm_export(js_name = registerAutotypeHandlers)]
369pub async fn register_autotype_handlers(ipc_client: &JsIpcClient, driver: RawJsAutotypeDriver) {
370    let runner = ThreadBoundRunner::new(driver);
371
372    ipc_client
373        .client
374        .register_rpc_handler(AutotypeSetEnabledHandler::new(JsAutotypeDriver::new(
375            runner.clone(),
376        )))
377        .await;
378    ipc_client
379        .client
380        .register_rpc_handler(AutotypeSetKeyboardShortcutHandler::new(
381            JsAutotypeDriver::new(runner.clone()),
382        ))
383        .await;
384    ipc_client
385        .client
386        .register_rpc_handler(AutotypeListRunningAppsHandler::new(JsAutotypeDriver::new(
387            runner,
388        )))
389        .await;
390}
391
392/// Sends an `AutotypeSetEnabledRequest` to the desktop main process and reports whether the
393/// requested change was successful.
394///
395/// There is no built-in timeout: the request waits until main responds or `abort_signal` aborts,
396/// so pass a signal (for example `AbortSignal.timeout(ms)`) when the wait needs a bound.
397#[bitwarden_ffi::wasm_export(js_name = autotypeRequestSetEnabled)]
398pub async fn autotype_request_set_enabled(
399    ipc_client: &JsIpcClient,
400    enabled: bool,
401    abort_signal: Option<AbortSignal>,
402) -> Result<AutotypeSetEnabledResponse, RequestError> {
403    ipc_client
404        .client
405        .request(
406            AutotypeSetEnabledRequest { enabled },
407            Endpoint::DesktopMain,
408            abort_signal.map(|signal| signal.to_cancellation_token()),
409        )
410        .await
411}
412
413/// Sends an `AutotypeSetKeyboardShortcutRequest` to the desktop main process and reports whether
414/// the requested shortcut was applied.
415///
416/// There is no built-in timeout: the request waits until main responds or `abort_signal` aborts,
417/// so pass a signal (for example `AbortSignal.timeout(ms)`) when the wait needs a bound.
418#[bitwarden_ffi::wasm_export(js_name = autotypeRequestSetKeyboardShortcut)]
419pub async fn autotype_request_set_keyboard_shortcut(
420    ipc_client: &JsIpcClient,
421    shortcut: Vec<String>,
422    abort_signal: Option<AbortSignal>,
423) -> Result<AutotypeSetKeyboardShortcutResponse, RequestError> {
424    ipc_client
425        .client
426        .request(
427            AutotypeSetKeyboardShortcutRequest { shortcut },
428            Endpoint::DesktopMain,
429            abort_signal.map(|signal| signal.to_cancellation_token()),
430        )
431        .await
432}
433
434/// Asks the desktop main process which applications are currently running.
435///
436/// An empty list could mean there are no apps running, or that the apps running
437/// didn't have the matching data needed for use with Autotype.
438///
439/// There is no built-in timeout: the request waits until main responds or `abort_signal` aborts,
440/// so pass a signal (for example `AbortSignal.timeout(ms)`) when the wait needs a bound.
441#[bitwarden_ffi::wasm_export(js_name = autotypeRequestListRunningApps)]
442pub async fn autotype_request_list_running_apps(
443    ipc_client: &JsIpcClient,
444    abort_signal: Option<AbortSignal>,
445) -> Result<AutotypeListRunningAppsResponse, RequestError> {
446    ipc_client
447        .client
448        .request(
449            AutotypeListRunningAppsRequest,
450            Endpoint::DesktopMain,
451            abort_signal.map(|signal| signal.to_cancellation_token()),
452        )
453        .await
454}