mirror of
https://github.com/bitwarden/browser
synced 2026-02-01 17:23:37 +00:00
Remove some more files
This commit is contained in:
24
apps/desktop/desktop_native/napi/index.d.ts
vendored
24
apps/desktop/desktop_native/napi/index.d.ts
vendored
@@ -213,31 +213,7 @@ export declare namespace autofill {
|
||||
}
|
||||
}
|
||||
export declare namespace passkey_authenticator {
|
||||
export interface PasskeyRequestEvent {
|
||||
requestType: string
|
||||
requestJson: string
|
||||
}
|
||||
export interface SyncedCredential {
|
||||
/** base64url-encoded credential ID. */
|
||||
credentialId: string
|
||||
rpId: string
|
||||
userName: string
|
||||
/** base64url-encoded user ID. */
|
||||
userHandle: string
|
||||
}
|
||||
export interface PasskeySyncRequest {
|
||||
rpId: string
|
||||
}
|
||||
export interface PasskeySyncResponse {
|
||||
credentials: Array<SyncedCredential>
|
||||
}
|
||||
export interface PasskeyErrorResponse {
|
||||
message: string
|
||||
}
|
||||
export function register(): void
|
||||
export function onRequest(callback: (error: null | Error, event: PasskeyRequestEvent) => Promise<string>): Promise<string>
|
||||
export function syncCredentialsToWindows(credentials: Array<SyncedCredential>): void
|
||||
export function getCredentialsFromWindows(): Array<SyncedCredential>
|
||||
}
|
||||
export declare namespace logging {
|
||||
export const enum LogLevel {
|
||||
|
||||
@@ -962,75 +962,12 @@ pub mod autofill {
|
||||
|
||||
#[napi]
|
||||
pub mod passkey_authenticator {
|
||||
use napi::threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction};
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(Debug)]
|
||||
pub struct PasskeyRequestEvent {
|
||||
pub request_type: String,
|
||||
pub request_json: String,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
pub struct SyncedCredential {
|
||||
/// base64url-encoded credential ID.
|
||||
pub credential_id: String, // base64url encoded
|
||||
pub rp_id: String,
|
||||
pub user_name: String,
|
||||
/// base64url-encoded user ID.
|
||||
pub user_handle: String, // base64url encoded
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeySyncRequest {
|
||||
pub rp_id: String,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
pub struct PasskeySyncResponse {
|
||||
pub credentials: Vec<SyncedCredential>,
|
||||
}
|
||||
|
||||
#[napi(object)]
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
pub struct PasskeyErrorResponse {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[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 async fn on_request(
|
||||
#[napi(
|
||||
ts_arg_type = "(error: null | Error, event: PasskeyRequestEvent) => Promise<string>"
|
||||
)]
|
||||
callback: ThreadsafeFunction<PasskeyRequestEvent, CalleeHandled>,
|
||||
) -> napi::Result<String> {
|
||||
crate::passkey_authenticator_internal::on_request(callback).await
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn sync_credentials_to_windows(credentials: Vec<SyncedCredential>) -> napi::Result<()> {
|
||||
crate::passkey_authenticator_internal::sync_credentials_to_windows(credentials)
|
||||
}
|
||||
|
||||
#[napi]
|
||||
pub fn get_credentials_from_windows() -> napi::Result<Vec<SyncedCredential>> {
|
||||
crate::passkey_authenticator_internal::get_credentials_from_windows()
|
||||
}
|
||||
}
|
||||
|
||||
#[napi]
|
||||
|
||||
@@ -1,220 +1,7 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use napi::{
|
||||
bindgen_prelude::Promise,
|
||||
threadsafe_function::{ErrorStrategy::CalleeHandled, ThreadsafeFunction},
|
||||
};
|
||||
use serde_json;
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
// Use the PasskeyRequestEvent from the parent module
|
||||
pub use crate::passkey_authenticator::{PasskeyRequestEvent, SyncedCredential};
|
||||
|
||||
pub fn register() -> Result<()> {
|
||||
windows_plugin_authenticator::register().map_err(|e| anyhow!(e))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn on_request(
|
||||
callback: ThreadsafeFunction<PasskeyRequestEvent, CalleeHandled>,
|
||||
) -> napi::Result<String> {
|
||||
let (tx, mut rx) = mpsc::unbounded_channel();
|
||||
|
||||
// Set the sender in the Windows plugin authenticator
|
||||
windows_plugin_authenticator::set_request_sender(tx);
|
||||
|
||||
// Spawn task to handle incoming events
|
||||
tokio::spawn(async move {
|
||||
while let Some(event) = rx.recv().await {
|
||||
// The request is already serialized as JSON in the event
|
||||
let request_json = event.request_json;
|
||||
|
||||
// Get the request type as a string
|
||||
let request_type = match event.request_type {
|
||||
windows_plugin_authenticator::RequestType::Assertion => "assertion".to_string(),
|
||||
windows_plugin_authenticator::RequestType::Registration => {
|
||||
"registration".to_string()
|
||||
}
|
||||
windows_plugin_authenticator::RequestType::Sync => "sync".to_string(),
|
||||
};
|
||||
|
||||
let napi_event = PasskeyRequestEvent {
|
||||
request_type,
|
||||
request_json,
|
||||
};
|
||||
|
||||
// Call the callback asynchronously and capture the return value
|
||||
let promise_result: Result<Promise<String>, napi::Error> =
|
||||
callback.call_async(Ok(napi_event)).await;
|
||||
// awai promse
|
||||
|
||||
match promise_result {
|
||||
Ok(promise_result) => match promise_result.await {
|
||||
Ok(result) => {
|
||||
// Parse the JSON response directly back to Rust enum
|
||||
let response: windows_plugin_authenticator::PasskeyResponse =
|
||||
match serde_json::from_str(&result) {
|
||||
Ok(resp) => resp,
|
||||
Err(e) => windows_plugin_authenticator::PasskeyResponse::Error {
|
||||
message: format!("JSON parse error: {}\nJSON: {}", e, &result),
|
||||
},
|
||||
};
|
||||
let _ = event.response_sender.send(response);
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Error calling passkey callback inner: {}", e);
|
||||
let _ = event.response_sender.send(
|
||||
windows_plugin_authenticator::PasskeyResponse::Error {
|
||||
message: format!("Inner Callback error: {}", e),
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Error calling passkey callback: {}", e);
|
||||
let _ = event.response_sender.send(
|
||||
windows_plugin_authenticator::PasskeyResponse::Error {
|
||||
message: format!("Callback error: {}", e),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Ok("Event listener registered successfully".to_string())
|
||||
}
|
||||
|
||||
impl From<windows_plugin_authenticator::SyncedCredential> for SyncedCredential {
|
||||
fn from(cred: windows_plugin_authenticator::SyncedCredential) -> Self {
|
||||
use base64::Engine;
|
||||
Self {
|
||||
credential_id: base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.encode(&cred.credential_id),
|
||||
rp_id: cred.rp_id,
|
||||
user_name: cred.user_name,
|
||||
user_handle: base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&cred.user_handle),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SyncedCredential> for windows_plugin_authenticator::SyncedCredential {
|
||||
fn from(cred: SyncedCredential) -> Self {
|
||||
use base64::Engine;
|
||||
Self {
|
||||
credential_id: base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.decode(&cred.credential_id)
|
||||
.unwrap_or_default(),
|
||||
rp_id: cred.rp_id,
|
||||
user_name: cred.user_name,
|
||||
user_handle: base64::engine::general_purpose::URL_SAFE_NO_PAD
|
||||
.decode(&cred.user_handle)
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sync_credentials_to_windows(credentials: Vec<SyncedCredential>) -> napi::Result<()> {
|
||||
const PLUGIN_CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062";
|
||||
|
||||
log::info!(
|
||||
"[NAPI] sync_credentials_to_windows called with {} credentials",
|
||||
credentials.len()
|
||||
);
|
||||
|
||||
// Log each credential being synced (with truncated IDs for security)
|
||||
for (i, cred) in credentials.iter().enumerate() {
|
||||
let truncated_cred_id = if cred.credential_id.len() > 16 {
|
||||
format!("{}...", &cred.credential_id[..16])
|
||||
} else {
|
||||
cred.credential_id.clone()
|
||||
};
|
||||
let truncated_user_id = if cred.user_handle.len() > 16 {
|
||||
format!("{}...", &cred.user_handle[..16])
|
||||
} else {
|
||||
cred.user_handle.clone()
|
||||
};
|
||||
log::info!(
|
||||
"[NAPI] Credential {}: RP={}, User={}, CredID={}, UserID={}",
|
||||
i + 1,
|
||||
cred.rp_id,
|
||||
cred.user_name,
|
||||
truncated_cred_id,
|
||||
truncated_user_id
|
||||
);
|
||||
}
|
||||
|
||||
// Convert NAPI types to internal types using From trait
|
||||
let internal_credentials: Vec<windows_plugin_authenticator::SyncedCredential> =
|
||||
credentials.into_iter().map(|cred| cred.into()).collect();
|
||||
|
||||
log::info!(
|
||||
"[NAPI] Calling Windows Plugin Authenticator sync with CLSID: {}",
|
||||
PLUGIN_CLSID
|
||||
);
|
||||
let result = windows_plugin_authenticator::sync_credentials_to_windows(
|
||||
internal_credentials,
|
||||
PLUGIN_CLSID,
|
||||
);
|
||||
|
||||
match &result {
|
||||
Ok(()) => log::info!("[NAPI] sync_credentials_to_windows completed successfully"),
|
||||
Err(e) => log::error!("[NAPI] sync_credentials_to_windows failed: {}", e),
|
||||
}
|
||||
|
||||
result.map_err(|e| napi::Error::from_reason(format!("Sync credentials failed: {}", e)))
|
||||
}
|
||||
|
||||
pub fn get_credentials_from_windows() -> napi::Result<Vec<SyncedCredential>> {
|
||||
const PLUGIN_CLSID: &str = "0f7dc5d9-69ce-4652-8572-6877fd695062";
|
||||
|
||||
log::info!(
|
||||
"[NAPI] get_credentials_from_windows called with CLSID: {}",
|
||||
PLUGIN_CLSID
|
||||
);
|
||||
|
||||
let result = windows_plugin_authenticator::get_credentials_from_windows(PLUGIN_CLSID);
|
||||
|
||||
let internal_credentials = match &result {
|
||||
Ok(creds) => {
|
||||
log::info!("[NAPI] Retrieved {} credentials from Windows", creds.len());
|
||||
result
|
||||
.map_err(|e| napi::Error::from_reason(format!("Get credentials failed: {}", e)))?
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("[NAPI] get_credentials_from_windows failed: {}", e);
|
||||
return Err(napi::Error::from_reason(format!(
|
||||
"Get credentials failed: {}",
|
||||
e
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
// Convert internal types to NAPI types
|
||||
let napi_credentials: Vec<SyncedCredential> = internal_credentials
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, cred)| {
|
||||
let result_cred: SyncedCredential = cred.into();
|
||||
let truncated_cred_id = if result_cred.credential_id.len() > 16 {
|
||||
format!("{}...", &result_cred.credential_id[..16])
|
||||
} else {
|
||||
result_cred.credential_id.clone()
|
||||
};
|
||||
log::info!(
|
||||
"[NAPI] Retrieved credential {}: RP={}, User={}, CredID={}",
|
||||
i + 1,
|
||||
result_cred.rp_id,
|
||||
result_cred.user_name,
|
||||
truncated_cred_id
|
||||
);
|
||||
result_cred
|
||||
})
|
||||
.collect();
|
||||
|
||||
log::info!(
|
||||
"[NAPI] get_credentials_from_windows completed successfully, returning {} credentials",
|
||||
napi_credentials.len()
|
||||
);
|
||||
Ok(napi_credentials)
|
||||
}
|
||||
|
||||
@@ -385,7 +385,7 @@ pub unsafe fn plugin_get_assertion(
|
||||
passkey_response.user_handle,
|
||||
)
|
||||
.map_err(|err| {
|
||||
format!("Failed to create WebAuthn assertion response: {err}");
|
||||
tracing::error!("Failed to create WebAuthn assertion response: {err}");
|
||||
HRESULT(-1)
|
||||
})?;
|
||||
tracing::debug!("Successfully created WebAuthn assertion response");
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
use std::sync::Mutex;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
use crate::types::*;
|
||||
use crate::util::debug_log;
|
||||
|
||||
/// Global channel sender for request notifications
|
||||
static REQUEST_SENDER: Mutex<Option<mpsc::UnboundedSender<RequestEvent>>> = Mutex::new(None);
|
||||
|
||||
/// Sets the channel sender for request notifications
|
||||
pub fn set_request_sender(sender: mpsc::UnboundedSender<RequestEvent>) {
|
||||
match REQUEST_SENDER.lock() {
|
||||
Ok(mut tx) => {
|
||||
*tx = Some(sender);
|
||||
debug_log("Passkey request callback registered");
|
||||
}
|
||||
Err(e) => {
|
||||
debug_log(&format!("Failed to register passkey callback: {:?}", e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends a passkey request and waits for response
|
||||
pub fn send_passkey_request(
|
||||
request_type: RequestType,
|
||||
request_json: String,
|
||||
rpid: &str,
|
||||
) -> Option<PasskeyResponse> {
|
||||
let request_desc = match &request_type {
|
||||
RequestType::Assertion => format!("assertion request for {}", rpid),
|
||||
RequestType::Registration => format!("registration request for {}", rpid),
|
||||
RequestType::Sync => format!("sync request for {}", rpid),
|
||||
};
|
||||
|
||||
debug_log(&format!("Passkey {}", request_desc));
|
||||
|
||||
if let Ok(tx_guard) = REQUEST_SENDER.lock() {
|
||||
if let Some(sender) = tx_guard.as_ref() {
|
||||
let (response_tx, response_rx) = oneshot::channel();
|
||||
let event = RequestEvent {
|
||||
request_type,
|
||||
request_json,
|
||||
response_sender: response_tx,
|
||||
};
|
||||
|
||||
if let Ok(()) = sender.send(event) {
|
||||
// Wait for response from TypeScript callback
|
||||
match response_rx.blocking_recv() {
|
||||
Ok(response) => {
|
||||
debug_log(&format!("Received callback response {:?}", response));
|
||||
Some(response)
|
||||
}
|
||||
Err(_) => {
|
||||
debug_log("No response from callback");
|
||||
None
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug_log("Failed to send event to callback");
|
||||
None
|
||||
}
|
||||
} else {
|
||||
debug_log("No callback registered for passkey requests");
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,8 @@ mod assert;
|
||||
mod com_buffer;
|
||||
mod com_provider;
|
||||
mod com_registration;
|
||||
mod ipc;
|
||||
mod ipc2;
|
||||
mod make_credential;
|
||||
mod sync;
|
||||
mod types;
|
||||
mod util;
|
||||
mod webauthn;
|
||||
@@ -18,33 +16,23 @@ mod webauthn;
|
||||
// Re-export main functionality
|
||||
pub use assert::WindowsAssertionRequest;
|
||||
pub use com_registration::{add_authenticator, initialize_com_library, register_com_library};
|
||||
pub use ipc::{send_passkey_request, set_request_sender};
|
||||
pub use make_credential::WindowsRegistrationRequest;
|
||||
pub use sync::{get_credentials_from_windows, send_sync_request, sync_credentials_to_windows};
|
||||
pub use types::{
|
||||
PasskeyRequest, PasskeyResponse, RequestEvent, RequestType, SyncedCredential,
|
||||
UserVerificationRequirement,
|
||||
};
|
||||
|
||||
use crate::util::debug_log;
|
||||
pub use types::UserVerificationRequirement;
|
||||
|
||||
/// Handles initialization and registration for the Bitwarden desktop app as a
|
||||
/// For now, also adds the authenticator
|
||||
pub fn register() -> std::result::Result<(), String> {
|
||||
// TODO: Can we spawn a new named thread for debugging?
|
||||
debug_log("register() called...");
|
||||
tracing::debug!("register() called...");
|
||||
|
||||
let r = com_registration::initialize_com_library();
|
||||
debug_log(&format!("Initialized the com library: {:?}", r));
|
||||
tracing::debug!("Initialized the com library: {:?}", r);
|
||||
|
||||
let r = com_registration::register_com_library();
|
||||
debug_log(&format!("Registered the com library: {:?}", r));
|
||||
tracing::debug!("Registered the com library: {:?}", r);
|
||||
|
||||
let r = com_registration::add_authenticator();
|
||||
debug_log(&format!("Added the authenticator: {:?}", r));
|
||||
tracing::debug!("Added the authenticator: {:?}", r);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// This sets up IPC so the plugin can request credentials from Electron.
|
||||
fn setup_ipc() {}
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
use hex;
|
||||
use serde_json;
|
||||
|
||||
use crate::com_registration::parse_clsid_to_guid_str;
|
||||
use crate::ipc::send_passkey_request;
|
||||
use crate::types::*;
|
||||
use crate::util::debug_log;
|
||||
use crate::webauthn::*;
|
||||
|
||||
/// Helper for sync requests - requests credentials from Electron for a specific RP ID
|
||||
pub fn send_sync_request(rpid: &str) -> Option<PasskeyResponse> {
|
||||
debug_log(&format!(
|
||||
"[SYNC] send_sync_request called for RP ID: {}",
|
||||
rpid
|
||||
));
|
||||
|
||||
let request = PasskeySyncRequest {
|
||||
rp_id: rpid.to_string(),
|
||||
};
|
||||
|
||||
debug_log(&format!("[SYNC] Created sync request for RP ID: {}", rpid));
|
||||
|
||||
match serde_json::to_string(&request) {
|
||||
Ok(request_json) => {
|
||||
debug_log(&format!(
|
||||
"[SYNC] Serialized sync request to JSON: {}",
|
||||
request_json
|
||||
));
|
||||
debug_log(&format!("[SYNC] Sending sync request to Electron via IPC"));
|
||||
let response = send_passkey_request(RequestType::Sync, request_json, rpid);
|
||||
match &response {
|
||||
Some(resp) => debug_log(&format!(
|
||||
"[SYNC] Received response from Electron: {:?}",
|
||||
resp
|
||||
)),
|
||||
None => debug_log("[SYNC] No response received from Electron"),
|
||||
}
|
||||
response
|
||||
}
|
||||
Err(e) => {
|
||||
debug_log(&format!(
|
||||
"[SYNC] ERROR: Failed to serialize sync request: {}",
|
||||
e
|
||||
));
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Initiates credential sync from Electron to Windows - called when Electron wants to push credentials to Windows
|
||||
pub fn sync_credentials_to_windows(
|
||||
credentials: Vec<SyncedCredential>,
|
||||
plugin_clsid: &str,
|
||||
) -> Result<(), String> {
|
||||
debug_log(&format!(
|
||||
"[SYNC_TO_WIN] sync_credentials_to_windows called with {} credentials for plugin CLSID: {}",
|
||||
credentials.len(),
|
||||
plugin_clsid
|
||||
));
|
||||
|
||||
// Parse CLSID string to GUID
|
||||
let clsid_guid = parse_clsid_to_guid_str(plugin_clsid)
|
||||
.map_err(|e| format!("Failed to parse CLSID: {}", e))?;
|
||||
|
||||
if credentials.is_empty() {
|
||||
debug_log("[SYNC_TO_WIN] No credentials to sync, proceeding with empty sync");
|
||||
}
|
||||
|
||||
// Convert Bitwarden credentials to Windows credential details
|
||||
let mut win_credentials = Vec::new();
|
||||
|
||||
for (i, cred) in credentials.iter().enumerate() {
|
||||
let truncated_cred_id = if cred.credential_id.len() > 16 {
|
||||
format!("{}...", hex::encode(&cred.credential_id[..16]))
|
||||
} else {
|
||||
hex::encode(&cred.credential_id)
|
||||
};
|
||||
let truncated_user_id = if cred.user_handle.len() > 16 {
|
||||
format!("{}...", hex::encode(&cred.user_handle[..16]))
|
||||
} else {
|
||||
hex::encode(&cred.user_handle)
|
||||
};
|
||||
|
||||
debug_log(&format!("[SYNC_TO_WIN] Converting credential {}: RP ID: {}, User: {}, Credential ID: {} ({} bytes), User ID: {} ({} bytes)",
|
||||
i + 1, cred.rp_id, cred.user_name, truncated_cred_id, cred.credential_id.len(), truncated_user_id, cred.user_handle.len()));
|
||||
|
||||
let win_cred = WebAuthnPluginCredentialDetails::create_from_bytes(
|
||||
cred.credential_id.clone(), // Pass raw bytes
|
||||
cred.rp_id.clone(),
|
||||
cred.rp_id.clone(), // Use RP ID as friendly name for now
|
||||
cred.user_handle.clone(), // Pass raw bytes
|
||||
cred.user_name.clone(),
|
||||
cred.user_name.clone(), // Use user name as display name for now
|
||||
);
|
||||
|
||||
win_credentials.push(win_cred);
|
||||
debug_log(&format!(
|
||||
"[SYNC_TO_WIN] Converted credential {} to Windows format",
|
||||
i + 1
|
||||
));
|
||||
}
|
||||
|
||||
// First try to remove all existing credentials for this plugin
|
||||
debug_log("Attempting to remove all existing credentials before sync...");
|
||||
match remove_all_credentials(clsid_guid) {
|
||||
Ok(()) => {
|
||||
debug_log("Successfully removed existing credentials");
|
||||
}
|
||||
Err(e) if e.contains("can't be loaded") => {
|
||||
debug_log("RemoveAllCredentials function not available - this is expected for some Windows versions");
|
||||
// This is fine, the function might not exist in all versions
|
||||
}
|
||||
Err(e) => {
|
||||
debug_log(&format!(
|
||||
"Warning: Failed to remove existing credentials: {}",
|
||||
e
|
||||
));
|
||||
// Continue anyway, as this might be the first sync or an older Windows version
|
||||
}
|
||||
}
|
||||
|
||||
// Add the new credentials (only if we have any)
|
||||
if credentials.is_empty() {
|
||||
debug_log("No credentials to add to Windows - sync completed successfully");
|
||||
Ok(())
|
||||
} else {
|
||||
debug_log("Adding new credentials to Windows...");
|
||||
match add_credentials(clsid_guid, win_credentials) {
|
||||
Ok(()) => {
|
||||
debug_log("Successfully synced credentials to Windows");
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
debug_log(&format!(
|
||||
"ERROR: Failed to add credentials to Windows: {}",
|
||||
e
|
||||
));
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Gets all credentials from Windows for a specific plugin - used when Electron requests current state
|
||||
pub fn get_credentials_from_windows(plugin_clsid: &str) -> Result<Vec<SyncedCredential>, String> {
|
||||
debug_log(&format!(
|
||||
"Getting all credentials from Windows for plugin CLSID: {}",
|
||||
plugin_clsid
|
||||
));
|
||||
|
||||
// Parse CLSID string to GUID
|
||||
let clsid_guid = parse_clsid_to_guid_str(plugin_clsid)
|
||||
.map_err(|e| format!("Failed to parse CLSID: {}", e))?;
|
||||
|
||||
match get_all_credentials(clsid_guid) {
|
||||
Ok(credentials) => {
|
||||
debug_log(&format!(
|
||||
"Retrieved {} credentials from Windows",
|
||||
credentials.len()
|
||||
));
|
||||
|
||||
let mut bitwarden_credentials = Vec::new();
|
||||
|
||||
// Convert Windows credentials to Bitwarden format
|
||||
for cred in credentials {
|
||||
let synced_cred = SyncedCredential {
|
||||
credential_id: cred.credential_id,
|
||||
rp_id: cred.rpid,
|
||||
user_name: cred.user_name,
|
||||
user_handle: cred.user_id,
|
||||
};
|
||||
|
||||
debug_log(&format!(
|
||||
"Converted Windows credential: RP ID: {}, User: {}, Credential ID: {} bytes",
|
||||
synced_cred.rp_id,
|
||||
synced_cred.user_name,
|
||||
synced_cred.credential_id.len()
|
||||
));
|
||||
|
||||
bitwarden_credentials.push(synced_cred);
|
||||
}
|
||||
|
||||
Ok(bitwarden_credentials)
|
||||
}
|
||||
Err(e) => {
|
||||
debug_log(&format!(
|
||||
"ERROR: Failed to get credentials from Windows: {}",
|
||||
e
|
||||
));
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,3 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
/// User verification requirement as defined by WebAuthn spec
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
@@ -36,107 +33,3 @@ impl Into<String> for UserVerificationRequirement {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// IDENTICAL to napi/lib.rs/PasskeyAssertionRequest
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeyAssertionRequest {
|
||||
pub rp_id: String,
|
||||
pub client_data_hash: Vec<u8>,
|
||||
pub user_verification: UserVerificationRequirement,
|
||||
pub allowed_credentials: Vec<Vec<u8>>,
|
||||
pub window_xy: Position,
|
||||
|
||||
pub transaction_id: String,
|
||||
}
|
||||
|
||||
// Identical to napi/lib.rs/Position
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Position {
|
||||
pub x: i32,
|
||||
pub y: i32,
|
||||
}
|
||||
|
||||
/// IDENTICAL to napi/lib.rs/PasskeyRegistrationRequest
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::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: UserVerificationRequirement,
|
||||
pub supported_algorithms: Vec<i32>,
|
||||
pub window_xy: Position,
|
||||
pub excluded_credentials: Vec<Vec<u8>>,
|
||||
|
||||
pub transaction_id: String,
|
||||
}
|
||||
|
||||
/// Sync request structure
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PasskeySyncRequest {
|
||||
pub rp_id: String,
|
||||
}
|
||||
|
||||
/// Union type for different request types
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PasskeyRequest {
|
||||
AssertionRequest(PasskeyAssertionRequest),
|
||||
RegistrationRequest(PasskeyRegistrationRequest),
|
||||
SyncRequest(PasskeySyncRequest),
|
||||
}
|
||||
|
||||
/// Response types for different operations - kept as tagged enum for JSON compatibility
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
pub enum PasskeyResponse {
|
||||
#[serde(rename = "assertion_response", rename_all = "camelCase")]
|
||||
AssertionResponse {
|
||||
rp_id: String,
|
||||
user_handle: Vec<u8>,
|
||||
signature: Vec<u8>,
|
||||
client_data_hash: Vec<u8>,
|
||||
authenticator_data: Vec<u8>,
|
||||
credential_id: Vec<u8>,
|
||||
},
|
||||
#[serde(rename = "registration_response", rename_all = "camelCase")]
|
||||
RegistrationResponse {
|
||||
rp_id: String,
|
||||
client_data_hash: Vec<u8>,
|
||||
credential_id: Vec<u8>,
|
||||
attestation_object: Vec<u8>,
|
||||
},
|
||||
#[serde(rename = "sync_response", rename_all = "camelCase")]
|
||||
SyncResponse { credentials: Vec<SyncedCredential> },
|
||||
#[serde(rename = "error", rename_all = "camelCase")]
|
||||
Error { message: String },
|
||||
}
|
||||
|
||||
/// Credential data for sync operations
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SyncedCredential {
|
||||
pub credential_id: Vec<u8>,
|
||||
pub rp_id: String,
|
||||
pub user_name: String,
|
||||
pub user_handle: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Request type enumeration for type discrimination
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum RequestType {
|
||||
Assertion,
|
||||
Registration,
|
||||
Sync,
|
||||
}
|
||||
|
||||
/// Internal request event with response channel and serializable request data
|
||||
#[derive(Debug)]
|
||||
pub struct RequestEvent {
|
||||
pub request_type: RequestType,
|
||||
pub request_json: String,
|
||||
pub response_sender: oneshot::Sender<PasskeyResponse>,
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { autofill } from "@bitwarden/desktop-napi";
|
||||
import { autofill, passkey_authenticator } from "@bitwarden/desktop-napi";
|
||||
|
||||
import { WindowMain } from "../../../main/window.main";
|
||||
|
||||
import { CommandDefinition } from "./command";
|
||||
import { NativeAutofillWindowsMain } from "./native-autofill.windows.main";
|
||||
|
||||
type BufferedMessage = {
|
||||
channel: string;
|
||||
@@ -26,15 +25,10 @@ export class NativeAutofillMain {
|
||||
private messageBuffer: BufferedMessage[] = [];
|
||||
private listenerReady = false;
|
||||
|
||||
private windowsIpc: NativeAutofillWindowsMain | null
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private windowMain: WindowMain,
|
||||
) {
|
||||
if (process.platform === "win32") {
|
||||
this.windowsIpc = new NativeAutofillWindowsMain(this.logService, this.windowMain);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,8 +62,16 @@ export class NativeAutofillMain {
|
||||
|
||||
async init() {
|
||||
if (process.platform === "win32") {
|
||||
this.windowsIpc.initWindows();
|
||||
// this.windowsIpc.setupWindowsRendererIPCHandlers();
|
||||
try {
|
||||
passkey_authenticator.register();
|
||||
}
|
||||
catch (err) {
|
||||
this.logService.error("Failed to register windows passkey plugin:", err)
|
||||
return JSON.stringify({
|
||||
"type": "error",
|
||||
"message": "Failed to register windows passkey plugin"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle(
|
||||
|
||||
@@ -1,374 +0,0 @@
|
||||
import { ipcMain } from "electron";
|
||||
|
||||
import { LogService } from "@bitwarden/common/platform/abstractions/log.service";
|
||||
import { autofill, passkey_authenticator } from "@bitwarden/desktop-napi";
|
||||
|
||||
import { WindowMain } from "../../../main/window.main";
|
||||
|
||||
import { CommandDefinition } from "./command";
|
||||
import type { RunCommandParams, RunCommandResult } from "./native-autofill.main";
|
||||
import { NativeAutofillFido2Credential, NativeAutofillSyncParams } from "./sync.command";
|
||||
|
||||
export class NativeAutofillWindowsMain {
|
||||
private pendingPasskeyRequests = new Map<string, (response: any) => void>();
|
||||
|
||||
constructor(
|
||||
private logService: LogService,
|
||||
private windowMain: WindowMain,
|
||||
) {}
|
||||
|
||||
initWindows() {
|
||||
try {
|
||||
passkey_authenticator.register();
|
||||
}
|
||||
catch (err) {
|
||||
this.logService.error("Failed to register windows passkey plugin:", err)
|
||||
return JSON.stringify({
|
||||
"type": "error",
|
||||
"message": "Failed to register windows passkey plugin"
|
||||
})
|
||||
}
|
||||
/*
|
||||
void passkey_authenticator.onRequest(async (error, event) => {
|
||||
this.logService.info("Passkey request received:", { error, event });
|
||||
|
||||
try {
|
||||
const request = JSON.parse(event.requestJson);
|
||||
this.logService.info("Parsed passkey request:", { type: event.requestType, request });
|
||||
|
||||
// Handle different request types based on the requestType field
|
||||
switch (event.requestType) {
|
||||
case "assertion":
|
||||
return await this.handleAssertionRequest(request);
|
||||
case "registration":
|
||||
return await this.handleRegistrationRequest(request);
|
||||
case "sync":
|
||||
return await this.handleSyncRequest(request);
|
||||
default:
|
||||
this.logService.error("Unknown passkey request type:", event.requestType);
|
||||
return JSON.stringify({
|
||||
type: "error",
|
||||
message: `Unknown request type: ${event.requestType}`,
|
||||
});
|
||||
}
|
||||
} catch (parseError) {
|
||||
this.logService.error("Failed to parse passkey request:", parseError);
|
||||
return JSON.stringify({
|
||||
type: "error",
|
||||
message: "Failed to parse request JSON",
|
||||
});
|
||||
}
|
||||
});
|
||||
*/
|
||||
}
|
||||
|
||||
private async handleAssertionRequest(request: autofill.PasskeyAssertionRequest): Promise<string> {
|
||||
this.logService.info("Handling assertion request for rpId:", request.rpId);
|
||||
|
||||
try {
|
||||
// Generate unique identifiers for tracking this request
|
||||
const clientId = Date.now();
|
||||
const sequenceNumber = Math.floor(Math.random() * 1000000);
|
||||
|
||||
// Send request and wait for response
|
||||
const response = await this.sendAndOptionallyWait<autofill.PasskeyAssertionResponse>(
|
||||
"autofill.passkeyAssertion",
|
||||
{
|
||||
clientId,
|
||||
sequenceNumber,
|
||||
request: request,
|
||||
},
|
||||
{ waitForResponse: true, timeout: 60000 },
|
||||
);
|
||||
|
||||
if (response) {
|
||||
// Convert the response to the format expected by the NAPI bridge
|
||||
return JSON.stringify({
|
||||
type: "assertion_response",
|
||||
...response,
|
||||
});
|
||||
} else {
|
||||
return JSON.stringify({
|
||||
type: "error",
|
||||
message: "No response received from renderer",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error("Error in assertion request:", error);
|
||||
return JSON.stringify({
|
||||
type: "error",
|
||||
message: `Assertion request failed: ${error.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleRegistrationRequest(
|
||||
request: autofill.PasskeyRegistrationRequest,
|
||||
): Promise<string> {
|
||||
this.logService.info("Handling registration request for rpId:", request.rpId);
|
||||
|
||||
try {
|
||||
// Generate unique identifiers for tracking this request
|
||||
const clientId = Date.now();
|
||||
const sequenceNumber = Math.floor(Math.random() * 1000000);
|
||||
|
||||
// Send request and wait for response
|
||||
const response = await this.sendAndOptionallyWait<autofill.PasskeyRegistrationResponse>(
|
||||
"autofill.passkeyRegistration",
|
||||
{
|
||||
clientId,
|
||||
sequenceNumber,
|
||||
request: request,
|
||||
},
|
||||
{ waitForResponse: true, timeout: 60000 },
|
||||
);
|
||||
|
||||
this.logService.info("Received response for registration request:", response);
|
||||
|
||||
if (response) {
|
||||
// Convert the response to the format expected by the NAPI bridge
|
||||
return JSON.stringify({
|
||||
type: "registration_response",
|
||||
...response,
|
||||
});
|
||||
} else {
|
||||
return JSON.stringify({
|
||||
type: "error",
|
||||
message: "No response received from renderer",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error("Error in registration request:", error);
|
||||
return JSON.stringify({
|
||||
type: "error",
|
||||
message: `Registration request failed: ${error.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private async handleSyncRequest(
|
||||
request: passkey_authenticator.PasskeySyncRequest,
|
||||
): Promise<string> {
|
||||
this.logService.info("Handling sync request for rpId:", request.rpId);
|
||||
|
||||
try {
|
||||
// Generate unique identifiers for tracking this request
|
||||
const clientId = Date.now();
|
||||
const sequenceNumber = Math.floor(Math.random() * 1000000);
|
||||
|
||||
// Send sync request and wait for response
|
||||
const response = await this.sendAndOptionallyWait<passkey_authenticator.PasskeySyncResponse>(
|
||||
"autofill.passkeySync",
|
||||
{
|
||||
clientId,
|
||||
sequenceNumber,
|
||||
request: { rpId: request.rpId },
|
||||
},
|
||||
{ waitForResponse: true, timeout: 60000 },
|
||||
);
|
||||
|
||||
this.logService.info("Received response for sync request:", response);
|
||||
|
||||
if (response && response.credentials) {
|
||||
// Convert the response to the format expected by the NAPI bridge
|
||||
return JSON.stringify({
|
||||
type: "sync_response",
|
||||
credentials: response.credentials,
|
||||
});
|
||||
} else {
|
||||
return JSON.stringify({
|
||||
type: "error",
|
||||
message: "No credentials received from renderer",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error("Error in sync request:", error);
|
||||
return JSON.stringify({
|
||||
type: "error",
|
||||
message: `Sync request failed: ${error.message}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for webContents.send that optionally waits for a response
|
||||
* @param channel The IPC channel to send to
|
||||
* @param data The data to send
|
||||
* @param options Optional configuration
|
||||
* @returns Promise that resolves with the response if waitForResponse is true
|
||||
*/
|
||||
private async sendAndOptionallyWait<T = any>(
|
||||
channel: string,
|
||||
data: any,
|
||||
options?: { waitForResponse?: boolean; timeout?: number },
|
||||
): Promise<T | void> {
|
||||
if (!options?.waitForResponse) {
|
||||
// Just send without waiting for response (existing behavior)
|
||||
this.logService.info(`Sending fire-and-forget message to ${channel}`);
|
||||
this.windowMain.win.webContents.send(channel, data);
|
||||
return;
|
||||
}
|
||||
|
||||
// Use clientId and sequenceNumber as the tracking key
|
||||
const trackingKey = `${data.clientId}_${data.sequenceNumber}`;
|
||||
const timeout = options.timeout || 30000; // 30 second default timeout
|
||||
|
||||
this.logService.info(`Sending awaitable request ${trackingKey} to ${channel}`, { data });
|
||||
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
// Set up timeout
|
||||
const timeoutId = setTimeout(() => {
|
||||
this.logService.warning(`Request ${trackingKey} timed out after ${timeout}ms`);
|
||||
this.pendingPasskeyRequests.delete(trackingKey);
|
||||
reject(new Error(`Request timeout after ${timeout}ms`));
|
||||
}, timeout);
|
||||
|
||||
// Store the resolver
|
||||
this.pendingPasskeyRequests.set(trackingKey, (response: T) => {
|
||||
this.logService.info(`Request ${trackingKey} resolved with response:`, response);
|
||||
clearTimeout(timeoutId);
|
||||
this.pendingPasskeyRequests.delete(trackingKey);
|
||||
resolve(response);
|
||||
});
|
||||
|
||||
this.logService.info(
|
||||
`Stored resolver for request ${trackingKey}, total pending: ${this.pendingPasskeyRequests.size}`,
|
||||
);
|
||||
|
||||
// Send the request
|
||||
this.windowMain.win.webContents.send(channel, data);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* These Handlers react to requests coming from the electron RENDERER process.
|
||||
*/
|
||||
setupWindowsRendererIPCHandlers() {
|
||||
// This will run a command in windows and return the result.
|
||||
// Only the "sync" command is supported for now.
|
||||
ipcMain.handle(
|
||||
"autofill.runCommand",
|
||||
<C extends CommandDefinition>(
|
||||
_event: any,
|
||||
params: RunCommandParams<C>,
|
||||
): Promise<RunCommandResult<C>> => {
|
||||
this.logService.debug("Received event (windows):", "autofill.runCommand", params)
|
||||
return this.runCommand(params);
|
||||
},
|
||||
);
|
||||
|
||||
ipcMain.on("autofill.completePasskeySync", (event, data) => {
|
||||
this.logService.warning("autofill.completePasskeySync", data);
|
||||
const { clientId, sequenceNumber, response } = data;
|
||||
|
||||
// Handle awaitable passkey requests using clientId and sequenceNumber
|
||||
if (clientId !== undefined && sequenceNumber !== undefined) {
|
||||
const trackingKey = `${clientId}_${sequenceNumber}`;
|
||||
this.handlePasskeyResponse(trackingKey, response);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on("autofill.completePasskeyRegistration", (event, data) => {
|
||||
this.logService.warning("autofill.completePasskeyRegistration", data);
|
||||
const { clientId, sequenceNumber, response } = data;
|
||||
|
||||
// Handle awaitable passkey requests using clientId and sequenceNumber
|
||||
if (clientId !== undefined && sequenceNumber !== undefined) {
|
||||
const trackingKey = `${clientId}_${sequenceNumber}`;
|
||||
this.handlePasskeyResponse(trackingKey, response);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on("autofill.completePasskeyAssertion", (event, data) => {
|
||||
this.logService.warning("autofill.completePasskeyAssertion", data);
|
||||
const { clientId, sequenceNumber, response } = data;
|
||||
|
||||
// Handle awaitable passkey requests using clientId and sequenceNumber
|
||||
if (clientId !== undefined && sequenceNumber !== undefined) {
|
||||
const trackingKey = `${clientId}_${sequenceNumber}`;
|
||||
this.handlePasskeyResponse(trackingKey, response);
|
||||
}
|
||||
});
|
||||
|
||||
ipcMain.on("autofill.completeError", (event, data) => {
|
||||
this.logService.warning("autofill.completeError", data);
|
||||
const { clientId, sequenceNumber, error } = data;
|
||||
|
||||
// Handle awaitable passkey requests using clientId and sequenceNumber
|
||||
if (clientId !== undefined && sequenceNumber !== undefined) {
|
||||
const trackingKey = `${clientId}_${sequenceNumber}`;
|
||||
this.handlePasskeyResponse(trackingKey, { error: String(error) });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private handlePasskeyResponse(trackingKey: string, response: any): void {
|
||||
this.logService.info("Received passkey response for tracking key:", trackingKey, response);
|
||||
|
||||
if (!trackingKey) {
|
||||
this.logService.error("Response missing tracking key:", response);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logService.info(`Looking for pending request with tracking key: ${trackingKey}`);
|
||||
this.logService.info(
|
||||
`Current pending requests: ${Array.from(this.pendingPasskeyRequests.keys())}`,
|
||||
);
|
||||
|
||||
const resolver = this.pendingPasskeyRequests.get(trackingKey);
|
||||
if (resolver) {
|
||||
this.logService.info("Found resolver, calling with response data:", response);
|
||||
resolver(response);
|
||||
} else {
|
||||
this.logService.warning("No pending request found for tracking key:", trackingKey);
|
||||
}
|
||||
}
|
||||
|
||||
private async runCommand<C extends CommandDefinition>(
|
||||
command: RunCommandParams<C>,
|
||||
): Promise<RunCommandResult<C>> {
|
||||
try {
|
||||
this.logService.info("Windows runCommand (sync) is called with command:", command);
|
||||
|
||||
if (command.namespace !== "autofill") {
|
||||
this.logService.error("Invalid command namespace:", command.namespace);
|
||||
return { type: "error", error: "Invalid command namespace" } as RunCommandResult<C>;
|
||||
}
|
||||
|
||||
if (command.command !== "sync") {
|
||||
this.logService.error("Invalid command:", command.command);
|
||||
return { type: "error", error: "Invalid command" } as RunCommandResult<C>;
|
||||
}
|
||||
|
||||
const syncParams = command.params as NativeAutofillSyncParams;
|
||||
// Only sync FIDO2 credentials
|
||||
const fido2Credentials = syncParams.credentials.filter((c) => c.type === "fido2");
|
||||
|
||||
const mappedCredentials = fido2Credentials.map((cred: NativeAutofillFido2Credential) => {
|
||||
const credential: passkey_authenticator.SyncedCredential = {
|
||||
credentialId: cred.credentialId,
|
||||
rpId: cred.rpId,
|
||||
userName: cred.userName,
|
||||
userHandle: cred.userHandle,
|
||||
};
|
||||
this.logService.info("Mapped credential:", credential);
|
||||
return credential;
|
||||
});
|
||||
|
||||
this.logService.info("Syncing passkeys to Windows:", mappedCredentials);
|
||||
|
||||
passkey_authenticator.syncCredentialsToWindows(mappedCredentials);
|
||||
|
||||
const res = { value: { added: mappedCredentials.length } } as RunCommandResult<C>;
|
||||
return res;
|
||||
} catch (e) {
|
||||
this.logService.error(`Error running autofill command '${command.command}':`, e);
|
||||
|
||||
if (e instanceof Error) {
|
||||
return { type: "error", error: e.stack ?? String(e) } as RunCommandResult<C>;
|
||||
}
|
||||
|
||||
return { type: "error", error: String(e) } as RunCommandResult<C>;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user