Skip to main content

bitwarden_importers/
wasm.rs

1//! WASM bindings for the direct 1Password import.
2//!
3//! The import asks the user for a TOTP code mid sign-in, which `wasm_bindgen` cannot express as the
4//! `&dyn OnePasswordTwoFactorUi` the native API takes. JavaScript passes an object implementing
5//! [`OnePasswordTwoFactorUi`](RawJsOnePasswordTwoFactorUi) instead, and
6//! [`JsOnePasswordTwoFactorUi`] adapts it back to the trait.
7
8use bitwarden_threading::ThreadBoundRunner;
9use wasm_bindgen::prelude::*;
10
11use crate::{OnePasswordTotpResult, OnePasswordTwoFactorUi};
12
13#[wasm_bindgen(typescript_custom_section)]
14const TS_CUSTOM_TYPES: &'static str = r#"
15/**
16 * Asks the user for a 1Password two-step verification code during a direct import.
17 */
18export interface OnePasswordTwoFactorUi {
19    /**
20     * Provides a TOTP passcode for the given zero-based attempt. A wrong code restarts the
21     * sign-in, so `attempt` grows as the user retries.
22     *
23     * @returns the passcode, or undefined if the user declined to provide one.
24     */
25    provideTotp(attempt: number): Promise<string | undefined>;
26}
27"#;
28
29#[wasm_bindgen]
30extern "C" {
31    /// The JavaScript object implementing the two-factor prompt.
32    #[wasm_bindgen(js_name = OnePasswordTwoFactorUi, typescript_type = "OnePasswordTwoFactorUi")]
33    pub type RawJsOnePasswordTwoFactorUi;
34
35    #[wasm_bindgen(catch, method, structural, js_name = provideTotp)]
36    async fn provide_totp(
37        this: &RawJsOnePasswordTwoFactorUi,
38        attempt: u32,
39    ) -> Result<JsValue, JsValue>;
40}
41
42/// Adapts the JavaScript prompt to [`OnePasswordTwoFactorUi`].
43///
44/// The JS object is `!Send`, so it stays pinned to its own thread behind a [`ThreadBoundRunner`];
45/// only the runner's channel handle crosses threads, which is what makes this `Send + Sync`.
46pub(crate) struct JsOnePasswordTwoFactorUi(ThreadBoundRunner<RawJsOnePasswordTwoFactorUi>);
47
48impl JsOnePasswordTwoFactorUi {
49    pub(crate) fn new(ui: RawJsOnePasswordTwoFactorUi) -> Self {
50        Self(ThreadBoundRunner::new(ui))
51    }
52}
53
54#[async_trait::async_trait]
55impl OnePasswordTwoFactorUi for JsOnePasswordTwoFactorUi {
56    async fn provide_totp(&self, attempt: u32) -> OnePasswordTotpResult {
57        let code = self
58            .0
59            .run_in_thread(move |ui| async move {
60                ui.provide_totp(attempt)
61                    .await
62                    .ok()
63                    .and_then(|code| code.as_string())
64            })
65            .await;
66
67        // A value that isn't a string, a rejected promise, and a runner whose thread went away all
68        // leave the sign-in without a code, which is what cancelling it means.
69        match code {
70            Ok(Some(code)) => OnePasswordTotpResult::Code(code),
71            Ok(None) | Err(_) => OnePasswordTotpResult::Cancel,
72        }
73    }
74}