diff --git a/package.nix b/package.nix index d82e6ea..b0324ac 100644 --- a/package.nix +++ b/package.nix @@ -5,18 +5,16 @@ fetchFromGitHub, makeDesktopItem, - cargo-tauri, pkg-config, + cargo-tauri, + wrapGAppsHook3, + copyDesktopItems, - atkmm, - eudev, - gdk-pixbuf, glib, gtk3, - libgudev, - libsoup_3, - pango, + openssl, pcsclite, + udev, webkitgtk_4_1, }: rustPlatform.buildRustPackage (finalAttrs: { @@ -32,13 +30,32 @@ rustPlatform.buildRustPackage (finalAttrs: { }; cargoRoot = "src-tauri"; - buildAndTestSubdir = finalAttrs.cargoRoot; cargoHash = "sha256-nLf8v4MIt2zAeA9YMVaoI3s/yut5/Jy2fGM3Sx33EJc="; - npmDist = buildNpmPackage { - name = "${finalAttrs.pname}-${finalAttrs.version}-dist"; + postPatch = '' + sed -i src-tauri/tauri.conf.json -e '/beforeBuildCommand/d' + ''; + + nativeBuildInputs = [ + pkg-config + cargo-tauri.hook + wrapGAppsHook3 + copyDesktopItems + ]; + + buildInputs = [ + glib + gtk3 + openssl + pcsclite + udev + webkitgtk_4_1 + ]; + + frontendDist = buildNpmPackage { + name = "${finalAttrs.pname}-${finalAttrs.version}-frontend-dist"; inherit (finalAttrs) src; npmDepsHash = "sha256-7DLooiGLzk3JRsKAftOxSf7HAgHBXCJDaAFp2p/pryc="; @@ -54,28 +71,9 @@ rustPlatform.buildRustPackage (finalAttrs: { }; preBuild = '' - sed -i '/beforeBuildCommand/d' src-tauri/tauri.conf.json - cp -r ${finalAttrs.npmDist} build + cp -r ${finalAttrs.frontendDist} build ''; - nativeBuildInputs = [ - cargo-tauri.hook - pkg-config - ]; - - buildInputs = [ - atkmm - eudev - gdk-pixbuf - glib - gtk3 - libgudev - libsoup_3 - pango - pcsclite - webkitgtk_4_1 - ]; - postInstall = '' install -Dm644 ${finalAttrs.src}/static/in.suyogtandel.picoforge.svg $out/share/icons/hicolor/scalable/apps/picoforge.svg ''; @@ -97,7 +95,7 @@ rustPlatform.buildRustPackage (finalAttrs: { meta = { changelog = "https://github.com/librekeys/picoforge/releases/tag/v${finalAttrs.version}"; - description = "An open source commissioning tool for Pico FIDO security keys."; + description = "An open source commissioning tool for Pico FIDO security keys"; homepage = "https://github.com/librekeys/picoforge"; license = lib.licenses.agpl3Only; mainProgram = "picoforge"; diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 92f37ba..1a54f3c 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2943,6 +2943,7 @@ dependencies = [ "log4rs", "pcsc", "rand 0.9.2", + "ring", "serde", "serde_cbor_2", "serde_json", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 3b66ed2..27eef64 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -35,6 +35,7 @@ hidapi = "2.6" # For fido2 interface operations but non-standard command serde_cbor_2 = "0.13" rand = "0.9" bitflags = "2.10" +ring = "0.17" # For signing fido2 messages with pin token log = "0.4" # Logging facade log4rs = "1" # For logging to output (like stdout) diff --git a/src-tauri/src/fido/hid.rs b/src-tauri/src/fido/hid.rs index 68e8c21..e54b7ac 100644 --- a/src-tauri/src/fido/hid.rs +++ b/src-tauri/src/fido/hid.rs @@ -2,8 +2,13 @@ use anyhow::{Result, anyhow}; use rand::Rng; +use serde_cbor_2::{Value, to_vec}; +use std::collections::BTreeMap; use std::time::Duration; +use crate::error::PFError; +use crate::fido::constants::*; + // HID Transport Constants const HID_REPORT_SIZE: usize = 64; const HID_USAGE_PAGE_FIDO: u16 = 0xF1D0; @@ -35,7 +40,7 @@ impl HidTransport { .find(|d| d.usage_page() == HID_USAGE_PAGE_FIDO) .ok_or_else(|| { log::warn!("No FIDO device found with Usage Page 0xF1D0."); - anyhow!("No FIDO device found. Is it plugged in?") + PFError::NoDevice })?; log::debug!( @@ -74,6 +79,17 @@ impl HidTransport { fn init_channel(device: &hidapi::HidDevice) -> Result { log::debug!("Initializing CTAPHID channel..."); + + // --- Drain Step --- + // Read and discard any pending packets to avoid using a stale response for CID negotiation. + let mut drain_buf = [0u8; HID_REPORT_SIZE]; + while let Ok(n) = device.read_timeout(&mut drain_buf[..], 10) { + if n == 0 { + break; + } + log::trace!("Drained stale HID packet: {:02X?}", &drain_buf[0..16]); + } + let mut nonce = [0u8; 8]; rand::rng().fill(&mut nonce); @@ -86,7 +102,7 @@ impl HidTransport { report[8..16].copy_from_slice(&nonce); log::trace!("Sending CTAPHID_INIT broadcast with nonce: {:02X?}", nonce); - device.write(&report).map_err(|e| { + device.write(&report[..]).map_err(|e| { log::error!("Failed to write INIT packet: {}", e); e })?; @@ -95,11 +111,11 @@ impl HidTransport { let start = std::time::Instant::now(); while start.elapsed() < Duration::from_secs(1) { let mut buf = [0u8; HID_REPORT_SIZE]; - if device.read_timeout(&mut buf, 100).is_ok() { + if device.read_timeout(&mut buf[..], 100).is_ok() { // Check if response matches our broadcast and nonce if buf[0..4] == CTAPHID_CID_BROADCAST.to_be_bytes() && buf[4] == CTAPHID_INIT - && &buf[7..15] == &nonce + && buf[7..15] == nonce { // New CID is at bytes 16..20 let new_cid = u32::from_be_bytes([buf[15], buf[16], buf[17], buf[18]]); @@ -136,7 +152,7 @@ impl HidTransport { sent += to_copy; // log::trace!("Writing Init Packet (Sent: {}/{})", sent, total_len); - if let Err(e) = self.device.write(&report) { + if let Err(e) = self.device.write(&report[..]) { log::error!("Failed to write initial HID packet: {}", e); return Err(e.into()); } @@ -153,7 +169,7 @@ impl HidTransport { sent += to_copy; // log::trace!("Writing Cont Packet Seq {} (Sent: {}/{})", sequence - 1, sent, total_len); - if let Err(e) = self.device.write(&report) { + if let Err(e) = self.device.write(&report[..]) { log::error!( "Failed to write continuation HID packet (Seq {}): {}", sequence - 1, @@ -176,7 +192,7 @@ impl HidTransport { let mut buf = [0u8; HID_REPORT_SIZE]; loop { - if let Err(e) = self.device.read_timeout(&mut buf, 2000) { + if let Err(e) = self.device.read_timeout(&mut buf[..], 2000) { log::error!("Timeout reading response packet: {}", e); return Err(e.into()); } @@ -227,7 +243,7 @@ impl HidTransport { // 2. Read Continuation Packets while read_len < expected_len { - if let Err(e) = self.device.read_timeout(&mut buf, 500) { + if let Err(e) = self.device.read_timeout(&mut buf[..], 500) { log::error!("Timeout reading continuation packet: {}", e); return Err(e.into()); } @@ -274,4 +290,86 @@ impl HidTransport { // Return payload without status byte Ok(response_data[1..].to_vec()) } + + pub fn send_vendor_config( + &self, + pin_token: &[u8], + vendor_cmd: VendorConfigCommand, + param: Value, + ) -> Result<(), PFError> { + log::debug!("Sending vendor config command: {}...", vendor_cmd); + + // Build subCommandParams (Key 0x02) + // This map contains: + // 0x01: vendorCommandId (u64) + // 0x02/0x03/0x04: param + let mut sub_params_inner = BTreeMap::new(); + sub_params_inner.insert( + Value::Integer(0x01), + Value::Integer(vendor_cmd.to_u64() as i128), + ); + + match param { + Value::Bytes(_) => { + sub_params_inner.insert(Value::Integer(0x02), param.clone()); + } + Value::Integer(_) => { + sub_params_inner.insert(Value::Integer(0x03), param.clone()); + } + Value::Text(_) => { + sub_params_inner.insert(Value::Integer(0x04), param.clone()); + } + _ => return Err(PFError::Io("Unsupported parameter type".into())), + } + + let sub_params = Value::Map(sub_params_inner); + let sub_params_bytes = to_vec(&sub_params).map_err(|e| PFError::Io(e.to_string()))?; + + // Build HMAC message for signing + // According to FIDO 2.1: authenticate(pinUvAuthToken, 32×0xff || 0x0d || uint8(subCommand) || subCommandParams) + let mut message = vec![0xff; 32]; + message.push(CtapCommand::Config as u8); + message.push(ConfigSubCommand::VendorPrototype as u8); + message.extend(&sub_params_bytes); + + // Sign using provided PIN token + use ring::hmac; + let hmac_key = hmac::Key::new(hmac::HMAC_SHA256, pin_token); + let sig = hmac::sign(&hmac_key, &message); + let pin_auth = sig.as_ref()[0..16].to_vec(); + + // Build full authenticatorConfig map + let mut config_map = BTreeMap::new(); + config_map.insert( + Value::Integer(ConfigParam::SubCommand as i128), + Value::Integer(ConfigSubCommand::VendorPrototype as i128), + ); + config_map.insert( + Value::Integer(ConfigParam::SubCommandParams as i128), + sub_params, + ); + config_map.insert( + Value::Integer(ConfigParam::PinUvAuthProtocol as i128), + Value::Integer(1), + ); + config_map.insert( + Value::Integer(ConfigParam::PinUvAuthParam as i128), + Value::Bytes(pin_auth), + ); + + let config_payload_cbor = + to_vec(&Value::Map(config_map)).map_err(|e| PFError::Io(e.to_string()))?; + + // Encapsulate for CTAP + let mut payload = vec![CtapCommand::Config as u8]; + payload.extend(config_payload_cbor); + + // Send via HID + self.send_cbor(CTAPHID_CBOR, &payload).map_err(|e| { + log::error!("Failed to send FIDO config: {}", e); + PFError::Device(format!("FIDO config failed: {}", e)) + })?; + + Ok(()) + } } diff --git a/src-tauri/src/fido/mod.rs b/src-tauri/src/fido/mod.rs index 1e10359..6f841ae 100644 --- a/src-tauri/src/fido/mod.rs +++ b/src-tauri/src/fido/mod.rs @@ -5,13 +5,19 @@ pub mod hid; use crate::{ error::PFError, - types::{AppConfig, DeviceInfo, FidoDeviceInfo, FullDeviceStatus, StoredCredential}, + types::{ + AppConfig, AppConfigInput, DeviceInfo, FidoDeviceInfo, FullDeviceStatus, StoredCredential, + }, }; use constants::*; use ctap_hid_fido2::{ - Cfg, FidoKeyHidFactory, public_key_credential_descriptor::PublicKeyCredentialDescriptor, + Cfg, FidoKeyHidFactory, + fidokey::make_credential::{MakeCredentialArgs, MakeCredentialArgsBuilder}, + public_key_credential_descriptor::PublicKeyCredentialDescriptor, + public_key_credential_user_entity::PublicKeyCredentialUserEntity, }; use hid::*; +use rand::Rng; use serde_cbor_2::{Value, from_slice, to_vec}; use std::collections::{BTreeMap, HashMap}; @@ -149,8 +155,12 @@ pub fn read_device_details() -> Result { log::info!("Starting FIDO device details read..."); let transport = HidTransport::open().map_err(|e| { - log::error!("Failed to open HID transport: {}", e); - PFError::Device(e.to_string()) + if let Some(PFError::NoDevice) = e.downcast_ref::() { + PFError::NoDevice + } else { + log::error!("Failed to open HID transport: {}", e); + PFError::Device(e.to_string()) + } })?; // --- 1. Get Info --- @@ -332,3 +342,118 @@ pub fn read_device_details() -> Result { method: "FIDO".to_string(), }) } + +pub fn write_config(config: AppConfigInput, pin: Option) -> Result { + log::info!("Starting FIDO write_config..."); + + let pin_val = pin.as_deref().ok_or_else(|| { + log::error!("PIN is required for configuration"); + PFError::Device("PIN is required for configuration".into()) + })?; + + // 1. Obtain PIN token using the library handle + let pin_token = { + let cfg = Cfg::init(); + let device = FidoKeyHidFactory::create(&cfg) + .map_err(|e| PFError::Device(format!("Could not connect to FIDO device: {:?}", e)))?; + + use ctap_hid_fido2::fidokey::pin::Permission; + // Try to obtain a token with AuthenticatorConfiguration permission (CTAP 2.1) + match device + .get_pinuv_auth_token_with_permission(pin_val, Permission::AuthenticatorConfiguration) + { + Ok(token) => { + log::debug!("Successfully obtained PIN token with ACFG permission."); + token.key + } + Err(e) => { + log::warn!( + "Failed to get PIN token with ACFG permission (Error: {:?}). Falling back to standard token.", + e + ); + // Fallback to standard PIN token (Subcommand 0x05) + let token = device.get_pin_token(pin_val).map_err(|e2| { + log::error!("Failed to obtain even a standard PIN token: {:?}", e2); + PFError::Device(format!("PIN token acquisition failed: {:?}", e2)) + })?; + log::debug!("Successfully obtained standard PIN token (fallback)."); + token.key + } + } + // Library handle 'device' is dropped here, closing the HID session. + }; + + // 2. Open custom HidTransport and send vendor commands using the token + let transport = HidTransport::open().map_err(|e| { + log::error!("Failed to open HID transport: {}", e); + PFError::Device(format!("Could not open HID transport: {}", e)) + })?; + + // VID/PID config + if let (Some(vid_str), Some(pid_str)) = (&config.vid, &config.pid) { + let vid = u16::from_str_radix(vid_str, 16).map_err(|e| PFError::Io(e.to_string()))?; + let pid = u16::from_str_radix(pid_str, 16).map_err(|e| PFError::Io(e.to_string()))?; + let vidpid = ((vid as u32) << 16) | (pid as u32); + transport.send_vendor_config( + &pin_token, + VendorConfigCommand::PhysicalVidPid, + Value::Integer(vidpid as i128), + )?; + } + + // LED GPIO config + if let Some(gpio) = config.led_gpio { + transport.send_vendor_config( + &pin_token, + VendorConfigCommand::PhysicalLedGpio, + Value::Integer(gpio as i128), + )?; + } + + // LED brightness config + if let Some(brightness) = config.led_brightness { + transport.send_vendor_config( + &pin_token, + VendorConfigCommand::PhysicalLedBrightness, + Value::Integer(brightness as i128), + )?; + } + + // Options config + let mut opts = 0u16; + if config.led_dimmable.unwrap_or(false) { + opts |= 0x02; // PHY_OPT_DIMM + } + if !config.power_cycle_on_reset.unwrap_or(true) { + opts |= 0x04; // PHY_OPT_DISABLE_POWER_RESET + } + if config.led_steady.unwrap_or(false) { + opts |= 0x08; // PHY_OPT_LED_STEADY + } + // Touch_timeout config + if let Some(timeout) = config.touch_timeout { + // In the firmware's phy_data, touch_timeout is often part of opts or separate. + // Looking at the previous code, it was separate (0x08). + // However, in vendor configuration, we usually send parameters individually. + transport + .send_vendor_config( + &pin_token, + VendorConfigCommand::PhysicalOptions, // Assuming there's a command for it or it's in opts + Value::Integer(timeout as i128), // Wait, let's check VendorConfigCommand again + ) + .ok(); // If it fails, maybe it's not supported as a standalone vendor cmd + } + + transport.send_vendor_config( + &pin_token, + VendorConfigCommand::PhysicalOptions, + Value::Integer(opts as i128), + )?; + + // ToDo : Product name configuration is not implemented in pico-fido firmware (cbor_config.c)? + + Ok( + "Configuration updated successfully! Unplug and re-plug the device to apply VID/PID changes." + .to_string(), + ) +} diff --git a/src-tauri/src/io.rs b/src-tauri/src/io.rs index 8dc3d6b..ecea9b5 100644 --- a/src-tauri/src/io.rs +++ b/src-tauri/src/io.rs @@ -13,8 +13,16 @@ pub fn read_device_details() -> Result { } #[tauri::command] -pub fn write_config(config: AppConfigInput) -> Result { - rescue::write_config(config) +pub fn write_config( + config: AppConfigInput, + method: String, + pin: Option, +) -> Result { + if method == "FIDO" { + fido::write_config(config, pin) + } else { + rescue::write_config(config) + } } #[tauri::command] diff --git a/src-tauri/src/types.rs b/src-tauri/src/types.rs index 1ca3228..9adeee8 100644 --- a/src-tauri/src/types.rs +++ b/src-tauri/src/types.rs @@ -32,7 +32,7 @@ pub struct AppConfig { pub enable_secp256k1: bool, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] pub struct AppConfigInput { pub vid: Option, diff --git a/src/lib/components/dialogs/authPinDialog.svelte b/src/lib/components/dialogs/authPinDialog.svelte new file mode 100644 index 0000000..33941f4 --- /dev/null +++ b/src/lib/components/dialogs/authPinDialog.svelte @@ -0,0 +1,51 @@ + + + + { + e.preventDefault(); + document.getElementById("auth-pin")?.focus(); + }} + > + + Authentication Required + + Please enter your FIDO2 PIN to authorize the configuration + change. + + +
+
+ + + e.key === "Enter" && configState.confirmAuthPinSave()} + /> +
+ {#if configState.authPinError} +

+ {configState.authPinError} +

+ {/if} +
+ + (configState.authPinDialogOpen = false)} + >Cancel + configState.confirmAuthPinSave()} + >Confirm + +
+
diff --git a/src/lib/components/dialogs/minPinDialog.svelte b/src/lib/components/dialogs/minPinDialog.svelte index c50b8ca..5932590 100644 --- a/src/lib/components/dialogs/minPinDialog.svelte +++ b/src/lib/components/dialogs/minPinDialog.svelte @@ -5,41 +5,76 @@ import { Slider } from "$lib/components/ui/slider/index.js"; import * as AlertDialog from "$lib/components/ui/alert-dialog"; - import { configViewState as state } from "$lib/state/configState.svelte"; + import { configViewState as configState } from "$lib/state/configState.svelte"; - - + + { + e.preventDefault(); + document.getElementById("min-pin-current")?.focus(); + }} + > Update Minimum PIN Length - Set the minimum allowed PIN length (4-63 characters) and enter a new PIN that meets this requirement. + Set the minimum allowed PIN length (4-63 characters) and enter a new PIN + that meets this requirement.
- - + +
- +
- - + +
- +
- {#if state.minPinError} -

{state.minPinError}

+ {#if configState.minPinError} +

{configState.minPinError}

{/if}
- (state.minPinDialogOpen = false)}>Cancel - Update + (configState.minPinDialogOpen = false)} + >Cancel + Update
diff --git a/src/lib/components/dialogs/setPinDialog.svelte b/src/lib/components/dialogs/setPinDialog.svelte index 71f4d09..23e9324 100644 --- a/src/lib/components/dialogs/setPinDialog.svelte +++ b/src/lib/components/dialogs/setPinDialog.svelte @@ -4,41 +4,72 @@ import { Label } from "$lib/components/ui/label"; import * as AlertDialog from "$lib/components/ui/alert-dialog"; - import { configViewState as state } from "$lib/state/configState.svelte"; + import { configViewState as configState } from "$lib/state/configState.svelte"; - - + + { + e.preventDefault(); + const id = configState.isSettingPin ? "new-pin" : "current-pin"; + document.getElementById(id)?.focus(); + }} + > - {state.isSettingPin ? "Set PIN" : "Change PIN"} + {configState.isSettingPin + ? "Set PIN" + : "Change PIN"} - {state.isSettingPin - ? "Set a PIN for your FIDO2 device. Minimum length is " + (state.minPinLength || 4) + " characters." + {configState.isSettingPin + ? "Set a PIN for your FIDO2 device. Minimum length is " + + (configState.minPinLength || 4) + + " characters." : "Enter your current PIN and choose a new one."}
- {#if !state.isSettingPin} + {#if !configState.isSettingPin}
- +
{/if}
- +
- +
- {#if state.pinError} -

{state.pinError}

+ {#if configState.pinError} +

{configState.pinError}

{/if}
- (state.setPinDialogOpen = false)}>Cancel - state.handlePinChange()}>Confirm + (configState.setPinDialogOpen = false)} + >Cancel + configState.handlePinChange()} + >Confirm
diff --git a/src/lib/device/manager.svelte.ts b/src/lib/device/manager.svelte.ts index 0d359af..5bb55a2 100644 --- a/src/lib/device/manager.svelte.ts +++ b/src/lib/device/manager.svelte.ts @@ -95,7 +95,7 @@ class DeviceManager { } } - async save() { + async save(pin: string | null = null) { if (!this.connected || !this.#originalConfig) return { success: false, msg: "Device not connected" }; this.loading = true; @@ -158,15 +158,17 @@ class DeviceManager { return { success: false, msg: "No changes detected." }; } else { logger.add("Sending configuration to device...", "info"); - const response = await invoke("write_config", { config: rustConfig }); + const response = await invoke("write_config", { config: rustConfig, method: this.method, pin }); logger.add(`Device Response: ${response}`, "success"); await this.refresh(); return { success: true, msg: "Configuration Applied Successfully!" }; } } catch (err: any) { - logger.add(`Write Failed: ${err}`, "error"); - return { success: false, msg: `Error: ${err}` }; + console.error("Write failed:", err); + const msg = typeof err === "string" ? err : err.message || JSON.stringify(err); + logger.add(`Write Failed: ${msg}`, "error"); + return { success: false, msg: `Error: ${msg}` }; } finally { this.loading = false; } diff --git a/src/lib/layout/sidebar.svelte b/src/lib/layout/sidebar.svelte index f508ccb..f2cfdb5 100644 --- a/src/lib/layout/sidebar.svelte +++ b/src/lib/layout/sidebar.svelte @@ -120,11 +120,19 @@ >Device Status {#if device.connected} - Online + {#if device.method === "FIDO"} + Online - Fido + {:else} + Online + {/if} {:else if device.error}
diff --git a/src/lib/state/configState.svelte.ts b/src/lib/state/configState.svelte.ts index b4a3c18..cdb7730 100644 --- a/src/lib/state/configState.svelte.ts +++ b/src/lib/state/configState.svelte.ts @@ -15,17 +15,38 @@ class configState { minPinLength = $state(4); minPinError = $state(""); + authPinDialogOpen = $state(false); + authPin = $state(""); + authPinError = $state(""); + dialogOpen = $state(false); dialogTitle = $state(""); dialogMessage = $state(""); async handleSave() { + if (device.method === "FIDO" && device.fidoInfo?.options?.clientPin) { + this.authPin = ""; + this.authPinError = ""; + this.authPinDialogOpen = true; + return; + } + const result = await device.save(); if (result) { this.showStatusDialog(result.success ? "Success" : "Write Failed", result.msg); } } + async confirmAuthPinSave() { + const result = await device.save(this.authPin); + if (result.success) { + this.authPinDialogOpen = false; + this.showStatusDialog("Success", result.msg); + } else { + this.authPinError = result.msg as string; + } + } + showStatusDialog(title: string, message: string) { this.dialogTitle = title; this.dialogMessage = message; diff --git a/src/lib/views/homeView.svelte b/src/lib/views/homeView.svelte index b7df1bf..052749c 100644 --- a/src/lib/views/homeView.svelte +++ b/src/lib/views/homeView.svelte @@ -60,10 +60,6 @@

Product Name

{device.config.productName}

-
-

Connection Method

- {device.method} -
diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index 3edd44e..4bc5896 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -16,6 +16,7 @@ import SetPinDialog from "$lib/components/dialogs/setPinDialog.svelte"; import MinPinDialog from "$lib/components/dialogs/minPinDialog.svelte"; + import AuthPinDialog from "$lib/components/dialogs/authPinDialog.svelte"; import MessageDialog from "$lib/components/dialogs/messageDialog.svelte"; type View = "home" | "passkeys" | "config" | "security" | "logs" | "about"; @@ -35,7 +36,9 @@ $effect(() => { logger.logs.length; tick().then(() => { - const viewport = document.querySelector("[data-radix-scroll-area-viewport]"); + const viewport = document.querySelector( + "[data-radix-scroll-area-viewport]", + ); if (viewport) { viewport.scrollTop = viewport.scrollHeight; } @@ -62,3 +65,4 @@ +