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//! There is one driver interface per desktop process, each implemented by the user of this
5//! crate (for example, within the Bitwarden desktop app):
6//! - `AutotypeDriver` handles requests in the main process, registered with
7//!   `registerAutotypeHandlers`.
8//! - `AutotypeRenderDriver` handles requests in the render process, registered with
9//!   `registerAutotypeRenderHandlers`.
10
11use bitwarden_ipc::{
12    Endpoint, IpcClientExt, RequestError, RpcHandler, RpcRequest, RpcRequestInfo, Source,
13    wasm::JsIpcClient,
14};
15use bitwarden_threading::{
16    ThreadBoundRunner,
17    cancellation_token::wasm::{AbortSignal, AbortSignalExt},
18};
19use serde::{Deserialize, Serialize};
20use tsify::serde_wasm_bindgen;
21use wasm_bindgen::{JsCast, JsValue, prelude::wasm_bindgen};
22use wasm_bindgen_futures::js_sys;
23
24#[wasm_bindgen(typescript_custom_section)]
25/// `TS_AUTOTYPE_TYPES` defines the TypeScript types for bindgen.
26/// The AutotypeDriver and AutotypeRenderDriver interfaces are TS interfaces that will be
27/// implemented by the user of this crate (e.g. in the Bitwarden desktop app).
28const TS_AUTOTYPE_TYPES: &'static str = r#"
29export interface AutotypeDriver {
30    /**
31     * Enable or disable autotype. Returns if setting
32     * the enabled state was successful or not.
33     */
34    set_autotype_enabled(enabled: boolean): Promise<boolean>;
35
36    /**
37     * Set the keyboard shortcut. Returns a boolean representing
38     * if setting the shortcut was successful or not.
39     */
40    set_autotype_keyboard_shortcut(shortcut: string[]): Promise<boolean>;
41
42    /**
43     * List the applications currently running. Returns an empty
44     * array if none could be determined. Entries that do not match
45     * `AutotypeRunningApp` are skipped.
46     */
47    list_running_apps(): Promise<AutotypeRunningApp[]>;
48}
49
50/**
51 * Applies Autotype requests in the desktop render process.
52 *
53 * Implement each method as an `async` function. A method that throws synchronously or returns
54 * a non-Promise value leaves the request without a response.
55 */
56export interface AutotypeRenderDriver {
57    /**
58     * Find the credentials to autotype into the focused app, as requested
59     * by the main process when the user presses the autotype shortcut.
60     * Returns the matching vault item's username and password, or
61     * `undefined` if nothing matches.
62     */
63    get_autotype_credentials(app: AutotypeRunningApp): Promise<AutotypeCredentials | undefined>;
64}
65"#;
66
67#[bitwarden_ffi::wasm_import]
68extern "C" {
69    /// JavaScript implementation of the various Autotype operations.
70    #[wasm_bindgen(js_name = AutotypeDriver, typescript_type = "AutotypeDriver")]
71    pub type RawJsAutotypeDriver;
72
73    /// Enable or disable autotype. Returns if setting the enabled state was successful or not.
74    #[wasm_bindgen(method, catch)]
75    async fn set_autotype_enabled(
76        this: &RawJsAutotypeDriver,
77        enabled: bool,
78    ) -> Result<JsValue, JsValue>;
79
80    /// Set the autotype keyboard shortcut. Returns a boolean
81    /// representing if setting the shortcut was successful or not.
82    #[wasm_bindgen(method, catch)]
83    async fn set_autotype_keyboard_shortcut(
84        this: &RawJsAutotypeDriver,
85        shortcut: Vec<String>,
86    ) -> Result<JsValue, JsValue>;
87
88    /// List the applications currently running. The driver returns an `AutotypeRunningApp[]`,
89    /// received here as a raw `JsValue` so `JsAutotypeDriver::list_running_apps` can skip
90    /// entries that don't match `AutotypeRunningApp`.
91    #[wasm_bindgen(method, catch)]
92    async fn list_running_apps(this: &RawJsAutotypeDriver) -> Result<JsValue, JsValue>;
93
94    /// JavaScript implementation of the Autotype operations applied in the render process.
95    #[wasm_bindgen(js_name = AutotypeRenderDriver, typescript_type = "AutotypeRenderDriver")]
96    pub type RawJsAutotypeRenderDriver;
97
98    /// Find the credentials to autotype into the focused app, as requested by the main process.
99    /// The driver returns an `AutotypeCredentials` or `undefined`, received here as a raw
100    /// `JsValue` so `JsAutotypeRenderDriver::get_autotype_credentials` can reject malformed
101    /// credentials.
102    #[wasm_bindgen(method, catch)]
103    async fn get_autotype_credentials(
104        this: &RawJsAutotypeRenderDriver,
105        app: AutotypeRunningApp,
106    ) -> Result<JsValue, JsValue>;
107}
108
109/// A running application.
110#[derive(Debug, Clone, Serialize, Deserialize)]
111#[bitwarden_ffi::wasm_record]
112pub struct AutotypeRunningApp {
113    /// Human-readable name for the app, e.g. `Google Chrome`.
114    pub name: String,
115    /// Path to the app's executable.
116    pub path: String,
117}
118
119/// The credentials the render process found for an [`AutotypeGetCredentialsRequest`].
120///
121/// `Debug` is implemented by hand so the username and password never reach logs.
122#[derive(Clone, Serialize, Deserialize)]
123#[bitwarden_ffi::wasm_record]
124pub struct AutotypeCredentials {
125    /// The matching vault item's username.
126    pub username: String,
127    /// The matching vault item's password.
128    pub password: String,
129}
130
131impl std::fmt::Debug for AutotypeCredentials {
132    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133        f.debug_struct("AutotypeCredentials")
134            .field("username", &"[REDACTED]")
135            .field("password", &"[REDACTED]")
136            .finish()
137    }
138}
139
140/// Wraps the JavaScript driver in a [`ThreadBoundRunner`] so it can be used from the `Send`
141/// futures an [`RpcHandler`] is required to return.
142struct JsAutotypeDriver {
143    runner: ThreadBoundRunner<RawJsAutotypeDriver>,
144}
145
146impl JsAutotypeDriver {
147    fn new(runner: ThreadBoundRunner<RawJsAutotypeDriver>) -> Self {
148        Self { runner }
149    }
150
151    /// Sets the Autotype enabled state, and returns if setting the
152    /// enabled state was successful or not.
153    async fn set_autotype_enabled(&self, enabled: bool) -> bool {
154        self.runner
155            .run_in_thread(move |driver| async move {
156                match driver.set_autotype_enabled(enabled).await {
157                    Ok(value) => value.as_bool().unwrap_or_else(|| {
158                        tracing::error!(
159                            "Autotype driver returned a non-boolean set-enabled result"
160                        );
161                        false
162                    }),
163                    Err(error) => {
164                        tracing::error!(?error, "Autotype driver failed to set the enabled state");
165                        false
166                    }
167                }
168            })
169            .await
170            .unwrap_or_else(|error| {
171                tracing::error!(?error, "Autotype runner failed to set the enabled state");
172                false
173            })
174    }
175
176    /// Sets the keyboard shortcut used for executing Autotype.
177    /// Returns if setting the keyboard shortcut was successful or not.
178    async fn set_autotype_keyboard_shortcut(&self, shortcut: Vec<String>) -> bool {
179        self.runner
180            .run_in_thread(move |driver| async move {
181                match driver.set_autotype_keyboard_shortcut(shortcut).await {
182                    Ok(value) => value.as_bool().unwrap_or_else(|| {
183                        tracing::error!("Autotype driver returned a non-boolean shortcut result");
184                        false
185                    }),
186                    Err(error) => {
187                        tracing::error!(?error, "Autotype driver failed to set the shortcut");
188                        false
189                    }
190                }
191            })
192            .await
193            .unwrap_or_else(|error| {
194                tracing::error!(?error, "Autotype runner failed to set the shortcut");
195                false
196            })
197    }
198
199    /// Retrieves the currently running apps.
200    /// Returns a Vec of `AutotypeRunningApp`. Apps that don't match the
201    /// `AutotypeRunningApp` model are filtered out.
202    async fn list_running_apps(&self) -> Vec<AutotypeRunningApp> {
203        self.runner
204            .run_in_thread(|driver| async move {
205                let value = match driver.list_running_apps().await {
206                    Ok(value) => value,
207                    Err(error) => {
208                        tracing::error!(?error, "Autotype driver failed to list running apps");
209                        return Vec::new();
210                    }
211                };
212
213                // This converts value (a `JsValue`) into an Array.
214                // This is critical for rejecting `null`, `undefined`, and other non-array values
215                // that could have been passed from the JavaScript side.
216                let Ok(array) = value.dyn_into::<js_sys::Array>() else {
217                    tracing::warn!("Autotype driver returned a non-array of running apps");
218                    return Vec::new();
219                };
220
221                // Filter through `array`, keeping entries that deserialize correctly
222                // into the `AutotypeRunningApp` type. This filters out (skips) bad entries.
223                array
224                    .iter()
225                    .filter_map(|entry| match serde_wasm_bindgen::from_value(entry) {
226                        Ok(app) => Some(app),
227                        Err(error) => {
228                            tracing::warn!(
229                                ?error,
230                                "Autotype driver returned a malformed running app; skipping it"
231                            );
232                            None
233                        }
234                    })
235                    .collect()
236            })
237            .await
238            .unwrap_or_else(|error| {
239                tracing::error!(?error, "Autotype runner failed to list running apps");
240                Vec::new()
241            })
242    }
243}
244
245/// Wraps the JavaScript render driver in a [`ThreadBoundRunner`] so it can be used from the
246/// `Send` futures an [`RpcHandler`] is required to return.
247struct JsAutotypeRenderDriver {
248    runner: ThreadBoundRunner<RawJsAutotypeRenderDriver>,
249}
250
251impl JsAutotypeRenderDriver {
252    fn new(runner: ThreadBoundRunner<RawJsAutotypeRenderDriver>) -> Self {
253        Self { runner }
254    }
255
256    /// Finds the credentials to autotype into the focused app, as requested by the main process.
257    /// Returns `None` if nothing matches, or if the driver fails or returns malformed credentials.
258    async fn get_autotype_credentials(
259        &self,
260        app: AutotypeRunningApp,
261    ) -> Option<AutotypeCredentials> {
262        self.runner
263            .run_in_thread(move |driver| async move {
264                let value = match driver.get_autotype_credentials(app).await {
265                    Ok(value) => value,
266                    Err(error) => {
267                        tracing::error!(
268                            ?error,
269                            "Autotype render driver failed to get autotype credentials"
270                        );
271                        return None;
272                    }
273                };
274
275                // `undefined` (or `null`) is the normal reply when no vault item matches.
276                if value.is_undefined() || value.is_null() {
277                    return None;
278                }
279
280                // The deserialization error isn't logged, because it can quote the value it
281                // failed on, which may hold the credentials.
282                match serde_wasm_bindgen::from_value(value) {
283                    Ok(credentials) => Some(credentials),
284                    Err(_) => {
285                        tracing::warn!("Autotype render driver returned malformed credentials");
286                        None
287                    }
288                }
289            })
290            .await
291            .unwrap_or_else(|error| {
292                tracing::error!(
293                    ?error,
294                    "Autotype render runner failed to get autotype credentials"
295                );
296                None
297            })
298    }
299}
300
301/// Determines if a request came from the `expected` source.
302///
303/// A rejection is logged with the request's
304/// [`RpcRequest::NAME`], the source it came from, and the source that was expected.
305fn from_source(info: &RpcRequestInfo, expected: Source, request: &str) -> bool {
306    let allowed = info.source == expected;
307
308    if !allowed {
309        tracing::warn!(
310            request,
311            source = ?info.source,
312            ?expected,
313            "Autotype rejected a request from an unexpected source"
314        );
315    }
316    allowed
317}
318
319/// A request asking the receiving client to enable or disable Autotype.
320#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct AutotypeSetEnabledRequest {
322    /// Whether autotype should be enabled.
323    pub enabled: bool,
324}
325
326/// A response to an [`AutotypeSetEnabledRequest`].
327#[derive(Debug, Clone, Serialize, Deserialize)]
328#[bitwarden_ffi::wasm_record]
329pub struct AutotypeSetEnabledResponse {
330    /// Whether the requested change for enabling/disabling Autotype was applied.
331    pub success: bool,
332}
333
334impl RpcRequest for AutotypeSetEnabledRequest {
335    type Response = AutotypeSetEnabledResponse;
336
337    const NAME: &str = "AutotypeSetEnabledRequest";
338}
339
340/// An [`RpcHandler`] that applies the requested state through a [`RawJsAutotypeDriver`].
341///
342/// Only requests from the desktop renderer are applied; any other source is refused with
343/// `success: false` before the driver is called. See [`from_source`].
344struct AutotypeSetEnabledHandler {
345    driver: JsAutotypeDriver,
346}
347
348impl AutotypeSetEnabledHandler {
349    fn new(driver: JsAutotypeDriver) -> Self {
350        Self { driver }
351    }
352}
353
354impl RpcHandler for AutotypeSetEnabledHandler {
355    type Request = AutotypeSetEnabledRequest;
356
357    async fn handle(
358        &self,
359        request: Self::Request,
360        info: RpcRequestInfo,
361    ) -> AutotypeSetEnabledResponse {
362        if !from_source(&info, Source::DesktopRenderer, Self::Request::NAME) {
363            return AutotypeSetEnabledResponse { success: false };
364        }
365
366        AutotypeSetEnabledResponse {
367            success: self.driver.set_autotype_enabled(request.enabled).await,
368        }
369    }
370}
371
372/// A request asking the receiving client to change the keyboard shortcut Autotype is triggered by.
373#[derive(Debug, Clone, Serialize, Deserialize)]
374pub struct AutotypeSetKeyboardShortcutRequest {
375    /// The shortcut used to execute Autotype.
376    pub shortcut: Vec<String>,
377}
378
379/// A response to an [`AutotypeSetKeyboardShortcutRequest`].
380#[derive(Debug, Clone, Serialize, Deserialize)]
381#[bitwarden_ffi::wasm_record]
382pub struct AutotypeSetKeyboardShortcutResponse {
383    /// Whether the requested keyboard shortcut was applied.
384    pub success: bool,
385}
386
387impl RpcRequest for AutotypeSetKeyboardShortcutRequest {
388    type Response = AutotypeSetKeyboardShortcutResponse;
389
390    const NAME: &str = "AutotypeSetKeyboardShortcutRequest";
391}
392
393/// An [`RpcHandler`] that applies the requested shortcut through a [`RawJsAutotypeDriver`].
394///
395/// Only requests from the desktop renderer are applied; any other source is refused with
396/// `success: false` before the shortcut reaches the driver. See [`from_source`].
397struct AutotypeSetKeyboardShortcutHandler {
398    driver: JsAutotypeDriver,
399}
400
401impl AutotypeSetKeyboardShortcutHandler {
402    fn new(driver: JsAutotypeDriver) -> Self {
403        Self { driver }
404    }
405}
406
407impl RpcHandler for AutotypeSetKeyboardShortcutHandler {
408    type Request = AutotypeSetKeyboardShortcutRequest;
409
410    async fn handle(
411        &self,
412        request: Self::Request,
413        info: RpcRequestInfo,
414    ) -> AutotypeSetKeyboardShortcutResponse {
415        if !from_source(&info, Source::DesktopRenderer, Self::Request::NAME) {
416            return AutotypeSetKeyboardShortcutResponse { success: false };
417        }
418
419        AutotypeSetKeyboardShortcutResponse {
420            success: self
421                .driver
422                .set_autotype_keyboard_shortcut(request.shortcut)
423                .await,
424        }
425    }
426}
427
428/// A request asking the receiving client for the applications that are currently running.
429#[derive(Debug, Clone, Serialize, Deserialize)]
430pub struct AutotypeListRunningAppsRequest;
431
432/// A response to an [`AutotypeListRunningAppsRequest`].
433#[derive(Debug, Clone, Serialize, Deserialize)]
434#[bitwarden_ffi::wasm_record]
435pub struct AutotypeListRunningAppsResponse {
436    /// The applications currently running.
437    pub apps: Vec<AutotypeRunningApp>,
438}
439
440impl RpcRequest for AutotypeListRunningAppsRequest {
441    type Response = AutotypeListRunningAppsResponse;
442
443    const NAME: &str = "AutotypeListRunningAppsRequest";
444}
445
446/// An [`RpcHandler`] that lists running applications through a [`RawJsAutotypeDriver`].
447///
448/// Only requests from the desktop renderer are answered; any other source gets an empty list
449/// without the driver being asked to enumerate anything. See [`from_source`].
450struct AutotypeListRunningAppsHandler {
451    driver: JsAutotypeDriver,
452}
453
454impl AutotypeListRunningAppsHandler {
455    fn new(driver: JsAutotypeDriver) -> Self {
456        Self { driver }
457    }
458}
459
460impl RpcHandler for AutotypeListRunningAppsHandler {
461    type Request = AutotypeListRunningAppsRequest;
462
463    async fn handle(
464        &self,
465        _: Self::Request,
466        info: RpcRequestInfo,
467    ) -> AutotypeListRunningAppsResponse {
468        if !from_source(&info, Source::DesktopRenderer, Self::Request::NAME) {
469            return AutotypeListRunningAppsResponse { apps: Vec::new() };
470        }
471
472        AutotypeListRunningAppsResponse {
473            apps: self.driver.list_running_apps().await,
474        }
475    }
476}
477
478/// A request from the main process, sent when the user presses the Autotype shortcut, asking the
479/// render process for the credentials to autotype into the focused app.
480#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct AutotypeGetCredentialsRequest {
482    /// The focused app.
483    pub app: AutotypeRunningApp,
484}
485
486/// A response to an [`AutotypeGetCredentialsRequest`].
487#[derive(Debug, Clone, Serialize, Deserialize)]
488#[bitwarden_ffi::wasm_record]
489pub struct AutotypeGetCredentialsResponse {
490    /// The matching credentials, or `None` if no vault item matched.
491    #[tsify(optional)]
492    pub credentials: Option<AutotypeCredentials>,
493}
494
495impl RpcRequest for AutotypeGetCredentialsRequest {
496    type Response = AutotypeGetCredentialsResponse;
497
498    const NAME: &str = "AutotypeGetCredentialsRequest";
499}
500
501/// An [`RpcHandler`] that finds the credentials to autotype through a
502/// [`RawJsAutotypeRenderDriver`].
503///
504/// Only requests from the desktop main process are answered; any other source (including
505/// native-messaging peers that main forwards to the render process) gets no match without the
506/// driver being called, so it can't request credentials. See [`from_source`].
507struct AutotypeGetCredentialsHandler {
508    driver: JsAutotypeRenderDriver,
509}
510
511impl AutotypeGetCredentialsHandler {
512    fn new(driver: JsAutotypeRenderDriver) -> Self {
513        Self { driver }
514    }
515}
516
517impl RpcHandler for AutotypeGetCredentialsHandler {
518    type Request = AutotypeGetCredentialsRequest;
519
520    async fn handle(
521        &self,
522        request: Self::Request,
523        info: RpcRequestInfo,
524    ) -> AutotypeGetCredentialsResponse {
525        if !from_source(&info, Source::DesktopMain, Self::Request::NAME) {
526            return AutotypeGetCredentialsResponse { credentials: None };
527        }
528
529        AutotypeGetCredentialsResponse {
530            credentials: self.driver.get_autotype_credentials(request.app).await,
531        }
532    }
533}
534
535/// This registers the Autotype handlers so the client responds to Autotype requests through
536/// the `RawJsAutotypeDriver`. Call this on the desktop main process's IPC client.
537#[bitwarden_ffi::wasm_export(js_name = registerAutotypeHandlers)]
538pub async fn register_autotype_handlers(ipc_client: &JsIpcClient, driver: RawJsAutotypeDriver) {
539    let runner = ThreadBoundRunner::new(driver);
540
541    ipc_client
542        .client
543        .register_rpc_handler(AutotypeSetEnabledHandler::new(JsAutotypeDriver::new(
544            runner.clone(),
545        )))
546        .await;
547    ipc_client
548        .client
549        .register_rpc_handler(AutotypeSetKeyboardShortcutHandler::new(
550            JsAutotypeDriver::new(runner.clone()),
551        ))
552        .await;
553    ipc_client
554        .client
555        .register_rpc_handler(AutotypeListRunningAppsHandler::new(JsAutotypeDriver::new(
556            runner,
557        )))
558        .await;
559}
560
561/// This registers the Autotype handlers so the client responds to Autotype requests from the
562/// main process through the `RawJsAutotypeRenderDriver`. Call this on the desktop render
563/// process's IPC client.
564#[bitwarden_ffi::wasm_export(js_name = registerAutotypeRenderHandlers)]
565pub async fn register_autotype_render_handlers(
566    ipc_client: &JsIpcClient,
567    driver: RawJsAutotypeRenderDriver,
568) {
569    let runner = ThreadBoundRunner::new(driver);
570
571    ipc_client
572        .client
573        .register_rpc_handler(AutotypeGetCredentialsHandler::new(
574            JsAutotypeRenderDriver::new(runner),
575        ))
576        .await;
577}
578
579/// Sends an `AutotypeSetEnabledRequest` to the desktop main process and reports whether the
580/// requested change was successful.
581///
582/// There is no built-in timeout: the request waits until main responds or `abort_signal` aborts,
583/// so pass a signal (for example `AbortSignal.timeout(ms)`) when the wait needs a bound.
584#[bitwarden_ffi::wasm_export(js_name = autotypeRequestSetEnabled)]
585pub async fn autotype_request_set_enabled(
586    ipc_client: &JsIpcClient,
587    enabled: bool,
588    abort_signal: Option<AbortSignal>,
589) -> Result<AutotypeSetEnabledResponse, RequestError> {
590    ipc_client
591        .client
592        .request(
593            AutotypeSetEnabledRequest { enabled },
594            Endpoint::DesktopMain,
595            abort_signal.map(|signal| signal.to_cancellation_token()),
596        )
597        .await
598}
599
600/// Sends an `AutotypeSetKeyboardShortcutRequest` to the desktop main process and reports whether
601/// the requested shortcut was applied.
602///
603/// There is no built-in timeout: the request waits until main responds or `abort_signal` aborts,
604/// so pass a signal (for example `AbortSignal.timeout(ms)`) when the wait needs a bound.
605#[bitwarden_ffi::wasm_export(js_name = autotypeRequestSetKeyboardShortcut)]
606pub async fn autotype_request_set_keyboard_shortcut(
607    ipc_client: &JsIpcClient,
608    shortcut: Vec<String>,
609    abort_signal: Option<AbortSignal>,
610) -> Result<AutotypeSetKeyboardShortcutResponse, RequestError> {
611    ipc_client
612        .client
613        .request(
614            AutotypeSetKeyboardShortcutRequest { shortcut },
615            Endpoint::DesktopMain,
616            abort_signal.map(|signal| signal.to_cancellation_token()),
617        )
618        .await
619}
620
621/// Asks the desktop main process which applications are currently running.
622///
623/// An empty list could mean there are no apps running, or that the apps running
624/// didn't have the matching data needed for use with Autotype.
625///
626/// There is no built-in timeout: the request waits until main responds or `abort_signal` aborts,
627/// so pass a signal (for example `AbortSignal.timeout(ms)`) when the wait needs a bound.
628#[bitwarden_ffi::wasm_export(js_name = autotypeRequestListRunningApps)]
629pub async fn autotype_request_list_running_apps(
630    ipc_client: &JsIpcClient,
631    abort_signal: Option<AbortSignal>,
632) -> Result<AutotypeListRunningAppsResponse, RequestError> {
633    ipc_client
634        .client
635        .request(
636            AutotypeListRunningAppsRequest,
637            Endpoint::DesktopMain,
638            abort_signal.map(|signal| signal.to_cancellation_token()),
639        )
640        .await
641}
642
643/// Sends an `AutotypeGetCredentialsRequest` to the desktop render process, asking for the
644/// credentials to autotype into the focused app. The response's `credentials` is empty when no
645/// vault item matches.
646///
647/// The response doesn't identify the app it's for. Keep the focused app's identity from before
648/// sending the request, and confirm that app is still focused before typing. If it isn't, the user
649/// has switched apps, and nothing should be typed.
650///
651/// The render process must have called `registerAutotypeRenderHandlers` first. Otherwise the
652/// request fails (for example, with a "no handler found" error) or waits until `abort_signal`
653/// fires.
654///
655/// There is no built-in timeout, so always pass `abort_signal` (for example
656/// `AbortSignal.timeout(ms)`). Without one, the request never completes if the render process
657/// doesn't respond.
658#[bitwarden_ffi::wasm_export(js_name = autotypeRequestGetCredentials)]
659pub async fn autotype_request_get_credentials(
660    ipc_client: &JsIpcClient,
661    app: AutotypeRunningApp,
662    abort_signal: Option<AbortSignal>,
663) -> Result<AutotypeGetCredentialsResponse, RequestError> {
664    ipc_client
665        .client
666        .request(
667            AutotypeGetCredentialsRequest { app },
668            Endpoint::DesktopRenderer,
669            abort_signal.map(|signal| signal.to_cancellation_token()),
670        )
671        .await
672}