1
0
mirror of https://github.com/bitwarden/browser synced 2025-12-17 16:53:34 +00:00
Files
browser/apps/desktop/desktop_native/napi/src/lib.rs
Bernd Schoolmann 04ed114e0e [BEEEP/PM-8492] Add autostart for flatpak (#12016)
* Add autostart for flatpak via ashpd

* Fix clippy errors

* Cargo fmt

* Fix clippy
2025-05-26 00:30:52 +02:00

878 lines
30 KiB
Rust

#[macro_use]
extern crate napi_derive;
mod passkey_authenticator_internal;
mod registry;
#[napi]
pub mod passwords {
/// Fetch the stored password from the keychain.
#[napi]
pub async fn get_password(service: String, account: String) -> napi::Result<String> {
desktop_core::password::get_password(&service, &account)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Save the password to the keychain. Adds an entry if none exists otherwise updates the existing entry.
#[napi]
pub async fn set_password(
service: String,
account: String,
password: String,
) -> napi::Result<()> {
desktop_core::password::set_password(&service, &account, &password)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Delete the stored password from the keychain.
#[napi]
pub async fn delete_password(service: String, account: String) -> napi::Result<()> {
desktop_core::password::delete_password(&service, &account)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
// Checks if the os secure storage is available
#[napi]
pub async fn is_available() -> napi::Result<bool> {
desktop_core::password::is_available()
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
}
#[napi]
pub mod biometrics {
use desktop_core::biometric::{Biometric, BiometricTrait};
// Prompt for biometric confirmation
#[napi]
pub async fn prompt(
hwnd: napi::bindgen_prelude::Buffer,
message: String,
) -> napi::Result<bool> {
Biometric::prompt(hwnd.into(), message)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn available() -> napi::Result<bool> {
Biometric::available()
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn set_biometric_secret(
service: String,
account: String,
secret: String,
key_material: Option<KeyMaterial>,
iv_b64: String,
) -> napi::Result<String> {
Biometric::set_biometric_secret(
&service,
&account,
&secret,
key_material.map(|m| m.into()),
&iv_b64,
)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn get_biometric_secret(
service: String,
account: String,
key_material: Option<KeyMaterial>,
) -> napi::Result<String> {
Biometric::get_biometric_secret(&service, &account, key_material.map(|m| m.into()))
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
/// Derives key material from biometric data. Returns a string encoded with a
/// base64 encoded key and the base64 encoded challenge used to create it
/// separated by a `|` character.
///
/// If the iv is provided, it will be used as the challenge. Otherwise a random challenge will be generated.
///
/// `format!("<key_base64>|<iv_base64>")`
#[napi]
pub async fn derive_key_material(iv: Option<String>) -> napi::Result<OsDerivedKey> {
Biometric::derive_key_material(iv.as_deref())
.map(|k| k.into())
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi(object)]
pub struct KeyMaterial {
pub os_key_part_b64: String,
pub client_key_part_b64: Option<String>,
}
impl From<KeyMaterial> for desktop_core::biometric::KeyMaterial {
fn from(km: KeyMaterial) -> Self {
desktop_core::biometric::KeyMaterial {
os_key_part_b64: km.os_key_part_b64,
client_key_part_b64: km.client_key_part_b64,
}
}
}
#[napi(object)]
pub struct OsDerivedKey {
pub key_b64: String,
pub iv_b64: String,
}
impl From<desktop_core::biometric::OsDerivedKey> for OsDerivedKey {
fn from(km: desktop_core::biometric::OsDerivedKey) -> Self {
OsDerivedKey {
key_b64: km.key_b64,
iv_b64: km.iv_b64,
}
}
}
}
#[napi]
pub mod clipboards {
#[napi]
pub async fn read() -> napi::Result<String> {
desktop_core::clipboard::read().map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn write(text: String, password: bool) -> napi::Result<()> {
desktop_core::clipboard::write(&text, password)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
}
#[napi]
pub mod sshagent {
use std::sync::Arc;
use napi::{
bindgen_prelude::Promise,
threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction},
};
use tokio::{self, sync::Mutex};
#[napi]
pub struct SshAgentState {
state: desktop_core::ssh_agent::BitwardenDesktopAgent,
}
#[napi(object)]
pub struct PrivateKey {
pub private_key: String,
pub name: String,
pub cipher_id: String,
}
#[napi(object)]
pub struct SshKey {
pub private_key: String,
pub public_key: String,
pub key_fingerprint: String,
}
#[napi(object)]
pub struct SshUIRequest {
pub cipher_id: Option<String>,
pub is_list: bool,
pub process_name: String,
pub is_forwarding: bool,
pub namespace: Option<String>,
}
#[napi]
pub async fn serve(
callback: ThreadsafeFunction<SshUIRequest, CalleeHandled>,
) -> napi::Result<SshAgentState> {
let (auth_request_tx, mut auth_request_rx) =
tokio::sync::mpsc::channel::<desktop_core::ssh_agent::SshAgentUIRequest>(32);
let (auth_response_tx, auth_response_rx) =
tokio::sync::broadcast::channel::<(u32, bool)>(32);
let auth_response_tx_arc = Arc::new(Mutex::new(auth_response_tx));
tokio::spawn(async move {
let _ = auth_response_rx;
while let Some(request) = auth_request_rx.recv().await {
let cloned_response_tx_arc = auth_response_tx_arc.clone();
let cloned_callback = callback.clone();
tokio::spawn(async move {
let auth_response_tx_arc = cloned_response_tx_arc;
let callback = cloned_callback;
let promise_result: Result<Promise<bool>, napi::Error> = callback
.call_async(Ok(SshUIRequest {
cipher_id: request.cipher_id,
is_list: request.is_list,
process_name: request.process_name,
is_forwarding: request.is_forwarding,
namespace: request.namespace,
}))
.await;
match promise_result {
Ok(promise_result) => match promise_result.await {
Ok(result) => {
let _ = auth_response_tx_arc
.lock()
.await
.send((request.request_id, result))
.expect("should be able to send auth response to agent");
}
Err(e) => {
println!("[SSH Agent Native Module] calling UI callback promise was rejected: {}", e);
let _ = auth_response_tx_arc
.lock()
.await
.send((request.request_id, false))
.expect("should be able to send auth response to agent");
}
},
Err(e) => {
println!("[SSH Agent Native Module] calling UI callback could not create promise: {}", e);
let _ = auth_response_tx_arc
.lock()
.await
.send((request.request_id, false))
.expect("should be able to send auth response to agent");
}
}
});
}
});
match desktop_core::ssh_agent::BitwardenDesktopAgent::start_server(
auth_request_tx,
Arc::new(Mutex::new(auth_response_rx)),
)
.await
{
Ok(state) => Ok(SshAgentState { state }),
Err(e) => Err(napi::Error::from_reason(e.to_string())),
}
}
#[napi]
pub fn stop(agent_state: &mut SshAgentState) -> napi::Result<()> {
let bitwarden_agent_state = &mut agent_state.state;
bitwarden_agent_state.stop();
Ok(())
}
#[napi]
pub fn is_running(agent_state: &mut SshAgentState) -> bool {
let bitwarden_agent_state = agent_state.state.clone();
bitwarden_agent_state.is_running()
}
#[napi]
pub fn set_keys(
agent_state: &mut SshAgentState,
new_keys: Vec<PrivateKey>,
) -> napi::Result<()> {
let bitwarden_agent_state = &mut agent_state.state;
bitwarden_agent_state
.set_keys(
new_keys
.iter()
.map(|k| (k.private_key.clone(), k.name.clone(), k.cipher_id.clone()))
.collect(),
)
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
Ok(())
}
#[napi]
pub fn lock(agent_state: &mut SshAgentState) -> napi::Result<()> {
let bitwarden_agent_state = &mut agent_state.state;
bitwarden_agent_state
.lock()
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> {
let bitwarden_agent_state = &mut agent_state.state;
bitwarden_agent_state
.clear_keys()
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
}
#[napi]
pub mod processisolations {
#[napi]
pub async fn disable_coredumps() -> napi::Result<()> {
desktop_core::process_isolation::disable_coredumps()
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn is_core_dumping_disabled() -> napi::Result<bool> {
desktop_core::process_isolation::is_core_dumping_disabled()
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn disable_memory_access() -> napi::Result<()> {
desktop_core::process_isolation::disable_memory_access()
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
}
#[napi]
pub mod powermonitors {
use napi::{
threadsafe_function::{
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
},
tokio,
};
#[napi]
pub async fn on_lock(callback: ThreadsafeFunction<(), CalleeHandled>) -> napi::Result<()> {
let (tx, mut rx) = tokio::sync::mpsc::channel::<()>(32);
desktop_core::powermonitor::on_lock(tx)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))?;
tokio::spawn(async move {
while let Some(()) = rx.recv().await {
callback.call(Ok(()), ThreadsafeFunctionCallMode::NonBlocking);
}
});
Ok(())
}
#[napi]
pub async fn is_lock_monitor_available() -> napi::Result<bool> {
Ok(desktop_core::powermonitor::is_lock_monitor_available().await)
}
}
#[napi]
pub mod windows_registry {
#[napi]
pub async fn create_key(key: String, subkey: String, value: String) -> napi::Result<()> {
crate::registry::create_key(&key, &subkey, &value)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[napi]
pub async fn delete_key(key: String, subkey: String) -> napi::Result<()> {
crate::registry::delete_key(&key, &subkey)
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
}
#[napi]
pub mod ipc {
use desktop_core::ipc::server::{Message, MessageType};
use napi::threadsafe_function::{
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
};
#[napi(object)]
pub struct IpcMessage {
pub client_id: u32,
pub kind: IpcMessageType,
pub message: Option<String>,
}
impl From<Message> for IpcMessage {
fn from(message: Message) -> Self {
IpcMessage {
client_id: message.client_id,
kind: message.kind.into(),
message: message.message,
}
}
}
#[napi]
pub enum IpcMessageType {
Connected,
Disconnected,
Message,
}
impl From<MessageType> for IpcMessageType {
fn from(message_type: MessageType) -> Self {
match message_type {
MessageType::Connected => IpcMessageType::Connected,
MessageType::Disconnected => IpcMessageType::Disconnected,
MessageType::Message => IpcMessageType::Message,
}
}
}
#[napi]
pub struct IpcServer {
server: desktop_core::ipc::server::Server,
}
#[napi]
impl IpcServer {
/// Create and start the IPC server without blocking.
///
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
/// @param callback This function will be called whenever a message is received from a client.
#[napi(factory)]
pub async fn listen(
name: String,
#[napi(ts_arg_type = "(error: null | Error, message: IpcMessage) => void")]
callback: ThreadsafeFunction<IpcMessage, ErrorStrategy::CalleeHandled>,
) -> napi::Result<Self> {
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
tokio::spawn(async move {
while let Some(message) = recv.recv().await {
callback.call(Ok(message.into()), ThreadsafeFunctionCallMode::NonBlocking);
}
});
let path = desktop_core::ipc::path(&name);
let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| {
napi::Error::from_reason(format!(
"Error listening to server - Path: {path:?} - Error: {e} - {e:?}"
))
})?;
Ok(IpcServer { server })
}
/// Return the path to the IPC server.
#[napi]
pub fn get_path(&self) -> String {
self.server.path.to_string_lossy().to_string()
}
/// Stop the IPC server.
#[napi]
pub fn stop(&self) -> napi::Result<()> {
self.server.stop();
Ok(())
}
/// Send a message over the IPC server to all the connected clients
///
/// @return The number of clients that the message was sent to. Note that the number of messages
/// actually received may be less, as some clients could disconnect before receiving the message.
#[napi]
pub fn send(&self, message: String) -> napi::Result<u32> {
self.server
.send(message)
.map_err(|e| {
napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}"))
})
// NAPI doesn't support u64 or usize, so we need to convert to u32
.map(|u| u32::try_from(u).unwrap_or_default())
}
}
}
#[napi]
pub mod autostart {
#[napi]
pub async fn set_autostart(autostart: bool, params: Vec<String>) -> napi::Result<()> {
desktop_core::autostart::set_autostart(autostart, params)
.await
.map_err(|e| napi::Error::from_reason(format!("Error setting autostart - {e} - {e:?}")))
}
}
#[napi]
pub mod autofill {
use desktop_core::ipc::server::{Message, MessageType};
use napi::threadsafe_function::{
ErrorStrategy, ThreadsafeFunction, ThreadsafeFunctionCallMode,
};
use serde::{de::DeserializeOwned, Deserialize, Serialize};
#[napi]
pub async fn run_command(value: String) -> napi::Result<String> {
desktop_core::autofill::run_command(value)
.await
.map_err(|e| napi::Error::from_reason(e.to_string()))
}
#[derive(Debug, serde::Serialize, serde:: Deserialize)]
pub enum BitwardenError {
Internal(String),
}
#[napi(string_enum)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum UserVerification {
#[napi(value = "preferred")]
Preferred,
#[napi(value = "required")]
Required,
#[napi(value = "discouraged")]
Discouraged,
}
#[derive(Serialize, Deserialize)]
#[serde(bound = "T: Serialize + DeserializeOwned")]
pub struct PasskeyMessage<T: Serialize + DeserializeOwned> {
pub sequence_number: u32,
pub value: Result<T, BitwardenError>,
}
#[napi(object)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Position {
pub x: i32,
pub y: i32,
}
#[napi(object)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyRegistrationRequest {
pub rp_id: String,
pub user_name: String,
pub user_handle: Vec<u8>,
pub client_data_hash: Vec<u8>,
pub user_verification: UserVerification,
pub supported_algorithms: Vec<i32>,
pub window_xy: Position,
}
#[napi(object)]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyRegistrationResponse {
pub rp_id: String,
pub client_data_hash: Vec<u8>,
pub credential_id: Vec<u8>,
pub attestation_object: Vec<u8>,
}
#[napi(object)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyAssertionRequest {
pub rp_id: String,
pub client_data_hash: Vec<u8>,
pub user_verification: UserVerification,
pub allowed_credentials: Vec<Vec<u8>>,
pub window_xy: Position,
//extension_input: Vec<u8>, TODO: Implement support for extensions
}
#[napi(object)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyAssertionWithoutUserInterfaceRequest {
pub rp_id: String,
pub credential_id: Vec<u8>,
pub user_name: String,
pub user_handle: Vec<u8>,
pub record_identifier: Option<String>,
pub client_data_hash: Vec<u8>,
pub user_verification: UserVerification,
pub window_xy: Position,
}
#[napi(object)]
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct PasskeyAssertionResponse {
pub rp_id: String,
pub user_handle: Vec<u8>,
pub signature: Vec<u8>,
pub client_data_hash: Vec<u8>,
pub authenticator_data: Vec<u8>,
pub credential_id: Vec<u8>,
}
#[napi]
pub struct IpcServer {
server: desktop_core::ipc::server::Server,
}
#[napi]
impl IpcServer {
/// Create and start the IPC server without blocking.
///
/// @param name The endpoint name to listen on. This name uniquely identifies the IPC connection and must be the same for both the server and client.
/// @param callback This function will be called whenever a message is received from a client.
#[napi(factory)]
pub async fn listen(
name: String,
// Ideally we'd have a single callback that has an enum containing the request values,
// but NAPI doesn't support that just yet
#[napi(
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyRegistrationRequest) => void"
)]
registration_callback: ThreadsafeFunction<
(u32, u32, PasskeyRegistrationRequest),
ErrorStrategy::CalleeHandled,
>,
#[napi(
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionRequest) => void"
)]
assertion_callback: ThreadsafeFunction<
(u32, u32, PasskeyAssertionRequest),
ErrorStrategy::CalleeHandled,
>,
#[napi(
ts_arg_type = "(error: null | Error, clientId: number, sequenceNumber: number, message: PasskeyAssertionWithoutUserInterfaceRequest) => void"
)]
assertion_without_user_interface_callback: ThreadsafeFunction<
(u32, u32, PasskeyAssertionWithoutUserInterfaceRequest),
ErrorStrategy::CalleeHandled,
>,
) -> napi::Result<Self> {
let (send, mut recv) = tokio::sync::mpsc::channel::<Message>(32);
tokio::spawn(async move {
while let Some(Message {
client_id,
kind,
message,
}) = recv.recv().await
{
match kind {
// TODO: We're ignoring the connection and disconnection messages for now
MessageType::Connected | MessageType::Disconnected => continue,
MessageType::Message => {
let Some(message) = message else {
println!("[ERROR] Message is empty");
continue;
};
match serde_json::from_str::<PasskeyMessage<PasskeyAssertionRequest>>(
&message,
) {
Ok(msg) => {
let value = msg
.value
.map(|value| (client_id, msg.sequence_number, value))
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
assertion_callback
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
continue;
}
Err(e) => {
println!("[ERROR] Error deserializing message1: {e}");
}
}
match serde_json::from_str::<
PasskeyMessage<PasskeyAssertionWithoutUserInterfaceRequest>,
>(&message)
{
Ok(msg) => {
let value = msg
.value
.map(|value| (client_id, msg.sequence_number, value))
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
assertion_without_user_interface_callback
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
continue;
}
Err(e) => {
println!("[ERROR] Error deserializing message1: {e}");
}
}
match serde_json::from_str::<PasskeyMessage<PasskeyRegistrationRequest>>(
&message,
) {
Ok(msg) => {
let value = msg
.value
.map(|value| (client_id, msg.sequence_number, value))
.map_err(|e| napi::Error::from_reason(format!("{e:?}")));
registration_callback
.call(value, ThreadsafeFunctionCallMode::NonBlocking);
continue;
}
Err(e) => {
println!("[ERROR] Error deserializing message2: {e}");
}
}
println!("[ERROR] Received an unknown message2: {message:?}");
}
}
}
});
let path = desktop_core::ipc::path(&name);
let server = desktop_core::ipc::server::Server::start(&path, send).map_err(|e| {
napi::Error::from_reason(format!(
"Error listening to server - Path: {path:?} - Error: {e} - {e:?}"
))
})?;
Ok(IpcServer { server })
}
/// Return the path to the IPC server.
#[napi]
pub fn get_path(&self) -> String {
self.server.path.to_string_lossy().to_string()
}
/// Stop the IPC server.
#[napi]
pub fn stop(&self) -> napi::Result<()> {
self.server.stop();
Ok(())
}
#[napi]
pub fn complete_registration(
&self,
client_id: u32,
sequence_number: u32,
response: PasskeyRegistrationResponse,
) -> napi::Result<u32> {
let message = PasskeyMessage {
sequence_number,
value: Ok(response),
};
self.send(client_id, serde_json::to_string(&message).unwrap())
}
#[napi]
pub fn complete_assertion(
&self,
client_id: u32,
sequence_number: u32,
response: PasskeyAssertionResponse,
) -> napi::Result<u32> {
let message = PasskeyMessage {
sequence_number,
value: Ok(response),
};
self.send(client_id, serde_json::to_string(&message).unwrap())
}
#[napi]
pub fn complete_error(
&self,
client_id: u32,
sequence_number: u32,
error: String,
) -> napi::Result<u32> {
let message: PasskeyMessage<()> = PasskeyMessage {
sequence_number,
value: Err(BitwardenError::Internal(error)),
};
self.send(client_id, serde_json::to_string(&message).unwrap())
}
// TODO: Add a way to send a message to a specific client?
fn send(&self, _client_id: u32, message: String) -> napi::Result<u32> {
self.server
.send(message)
.map_err(|e| {
napi::Error::from_reason(format!("Error sending message - Error: {e} - {e:?}"))
})
// NAPI doesn't support u64 or usize, so we need to convert to u32
.map(|u| u32::try_from(u).unwrap_or_default())
}
}
}
#[napi]
pub mod crypto {
use napi::bindgen_prelude::Buffer;
#[napi]
pub async fn argon2(
secret: Buffer,
salt: Buffer,
iterations: u32,
memory: u32,
parallelism: u32,
) -> napi::Result<Buffer> {
desktop_core::crypto::argon2(&secret, &salt, iterations, memory, parallelism)
.map_err(|e| napi::Error::from_reason(e.to_string()))
.map(|v| v.to_vec())
.map(Buffer::from)
}
}
#[napi]
pub mod passkey_authenticator {
#[napi]
pub fn register() -> napi::Result<()> {
crate::passkey_authenticator_internal::register().map_err(|e| {
napi::Error::from_reason(format!("Passkey registration failed - Error: {e} - {e:?}"))
})
}
}
#[napi]
pub mod logging {
use log::{Level, Metadata, Record};
use napi::threadsafe_function::{
ErrorStrategy::CalleeHandled, ThreadsafeFunction, ThreadsafeFunctionCallMode,
};
use std::sync::OnceLock;
struct JsLogger(OnceLock<ThreadsafeFunction<(LogLevel, String), CalleeHandled>>);
static JS_LOGGER: JsLogger = JsLogger(OnceLock::new());
#[napi]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
impl From<Level> for LogLevel {
fn from(level: Level) -> Self {
match level {
Level::Trace => LogLevel::Trace,
Level::Debug => LogLevel::Debug,
Level::Info => LogLevel::Info,
Level::Warn => LogLevel::Warn,
Level::Error => LogLevel::Error,
}
}
}
#[napi]
pub fn init_napi_log(js_log_fn: ThreadsafeFunction<(LogLevel, String), CalleeHandled>) {
let _ = JS_LOGGER.0.set(js_log_fn);
let _ = log::set_logger(&JS_LOGGER);
log::set_max_level(log::LevelFilter::Debug);
}
impl log::Log for JsLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
metadata.level() <= log::max_level()
}
fn log(&self, record: &Record) {
if !self.enabled(record.metadata()) {
return;
}
let Some(logger) = self.0.get() else {
return;
};
let msg = (record.level().into(), record.args().to_string());
let _ = logger.call(Ok(msg), ThreadsafeFunctionCallMode::NonBlocking);
}
fn flush(&self) {}
}
}