Skip to main content

rpc/natives/
user.rs

1//! User-domain natives. Wraps `server::command::VerifyUserPassword` only;
2//! account-creation/registration is reserved for the auth-flow path, not the
3//! eval channel.
4//!
5//! `verify-user-password` is exposed verbatim from the server command —
6//! takes (email, password), returns the matching UUID on success or nil
7//! on bad creds. The session is already authenticated by the time forms
8//! reach the eval channel, so this native exists for "confirm current
9//! identity before sensitive action" workflows the emacs client may
10//! drive (e.g. re-prompting on session-reauth UX).
11
12use scripting::runtime::{alloc_string_ref, read_string_arg};
13use server::command::CmdResult;
14use server::command::user::VerifyUserPassword;
15use wasmtime::{ArrayRef, Caller, Linker, Rooted};
16
17use crate::session::SessionData;
18
19pub const REGISTERED_COMMANDS: &[&str] = &["verify-user-password"];
20
21pub fn register(linker: &mut Linker<SessionData>) -> wasmtime::Result<()> {
22    linker.func_wrap_async(
23        "nomi",
24        "user_verify_user_password",
25        |mut caller: Caller<'_, SessionData>,
26         (email_arg, password_arg): (Option<Rooted<ArrayRef>>, Option<Rooted<ArrayRef>>)|
27         -> Box<
28            dyn std::future::Future<Output = wasmtime::Result<Option<Rooted<ArrayRef>>>> + Send,
29        > {
30            Box::new(async move {
31                let email = read_string_arg(&mut caller, email_arg)?;
32                let password = read_string_arg(&mut caller, password_arg)?;
33                match run_verify_user_password(email, password).await? {
34                    Some(id) => Ok(Some(alloc_string_ref(&mut caller, id.as_bytes())?)),
35                    None => Ok(None),
36                }
37            })
38        },
39    )?;
40    Ok(())
41}
42
43/// On success returns `Some(<uuid-string>)`; bad credentials surface as
44/// `Ok(None)` (null `ref null $i8_array` on the wire). Validation/runtime
45/// failures trap via `wasmtime::Error` and the rpc envelope renders them
46/// as `:error` records.
47async fn run_verify_user_password(
48    email_arg: Option<String>,
49    password_arg: Option<String>,
50) -> wasmtime::Result<Option<String>> {
51    let email = email_arg
52        .filter(|s| !s.is_empty())
53        .ok_or_else(|| wasmtime::Error::msg("verify-user-password: missing or empty :email arg"))?;
54    let password = password_arg
55        .ok_or_else(|| wasmtime::Error::msg("verify-user-password: missing :password arg"))?;
56    match VerifyUserPassword::new()
57        .email(email)
58        .password(password)
59        .run()
60        .await
61    {
62        Ok(Some(CmdResult::Uuid(id))) => Ok(Some(id.to_string())),
63        Ok(Some(CmdResult::Bool(false))) | Ok(None) => Ok(None),
64        Ok(Some(other)) => Err(wasmtime::Error::msg(format!(
65            "verify-user-password: unexpected variant {other:?}"
66        ))),
67        Err(err) => Err(wasmtime::Error::msg(format!("verify-user-password: {err}"))),
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[tokio::test]
76    async fn run_verify_user_password_missing_email_emits_error() {
77        let err = run_verify_user_password(None, Some("pw".into()))
78            .await
79            .unwrap_err();
80        assert!(err.to_string().contains(":email"), "got: {err}");
81    }
82
83    #[tokio::test]
84    async fn run_verify_user_password_empty_email_emits_error() {
85        let err = run_verify_user_password(Some(String::new()), Some("pw".into()))
86            .await
87            .unwrap_err();
88        assert!(err.to_string().contains(":email"), "got: {err}");
89    }
90
91    #[tokio::test]
92    async fn run_verify_user_password_missing_password_emits_error() {
93        let err = run_verify_user_password(Some("u@example.com".into()), None)
94            .await
95            .unwrap_err();
96        assert!(err.to_string().contains(":password"), "got: {err}");
97    }
98}