fix: don't log cloud provider keys (#1145)

* fix: don't log cloud provider keys

* test: make sure keys are redacted

* change to newtype

* change secret struct to public

* test: test secretmap directly

* Update bindings.ts

---------

Co-authored-by: Shaan <shaankhosla@macbook-pro.mynetworksettings.com>
Co-authored-by: CJ Pais <cj@cjpais.com>
This commit is contained in:
Shaan Khosla 2026-03-28 07:05:36 -04:00 committed by GitHub
parent 075fb5f78d
commit 4239cd3584
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 63 additions and 4 deletions

View file

@ -3,6 +3,7 @@ use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer, Serialize};
use specta::Type;
use std::collections::HashMap;
use std::fmt;
use tauri::AppHandle;
use tauri_plugin_store::StoreExt;
@ -304,6 +305,34 @@ impl Default for OrtAcceleratorSetting {
}
}
#[derive(Clone, Serialize, Deserialize, Type)]
#[serde(transparent)]
pub(crate) struct SecretMap(HashMap<String, String>);
impl fmt::Debug for SecretMap {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let redacted: HashMap<&String, &str> = self
.0
.iter()
.map(|(k, v)| (k, if v.is_empty() { "" } else { "[REDACTED]" }))
.collect();
redacted.fmt(f)
}
}
impl std::ops::Deref for SecretMap {
type Target = HashMap<String, String>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for SecretMap {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
/* still handy for composing the initial JSON in the store ------------- */
#[derive(Serialize, Deserialize, Debug, Clone, Type)]
pub struct AppSettings {
@ -365,7 +394,7 @@ pub struct AppSettings {
#[serde(default = "default_post_process_providers")]
pub post_process_providers: Vec<PostProcessProvider>,
#[serde(default = "default_post_process_api_keys")]
pub post_process_api_keys: HashMap<String, String>,
pub post_process_api_keys: SecretMap,
#[serde(default = "default_post_process_models")]
pub post_process_models: HashMap<String, String>,
#[serde(default = "default_post_process_prompts")]
@ -573,12 +602,12 @@ fn default_post_process_providers() -> Vec<PostProcessProvider> {
providers
}
fn default_post_process_api_keys() -> HashMap<String, String> {
fn default_post_process_api_keys() -> SecretMap {
let mut map = HashMap::new();
for provider in default_post_process_providers() {
map.insert(provider.id, String::new());
}
map
SecretMap(map)
}
fn default_model_for_provider(provider_id: &str) -> String {
@ -918,4 +947,33 @@ mod tests {
assert!(!settings.auto_submit);
assert_eq!(settings.auto_submit_key, AutoSubmitKey::Enter);
}
#[test]
fn debug_output_redacts_api_keys() {
let mut settings = get_default_settings();
settings
.post_process_api_keys
.insert("openai".to_string(), "sk-proj-secret-key-12345".to_string());
settings.post_process_api_keys.insert(
"anthropic".to_string(),
"sk-ant-secret-key-67890".to_string(),
);
settings
.post_process_api_keys
.insert("empty_provider".to_string(), "".to_string());
let debug_output = format!("{:?}", settings);
assert!(!debug_output.contains("sk-proj-secret-key-12345"));
assert!(!debug_output.contains("sk-ant-secret-key-67890"));
assert!(debug_output.contains("[REDACTED]"));
}
#[test]
fn secret_map_debug_redacts_values() {
let map = SecretMap(HashMap::from([("key".into(), "secret".into())]));
let out = format!("{:?}", map);
assert!(!out.contains("secret"));
assert!(out.contains("[REDACTED]"));
}
}

View file

@ -827,7 +827,7 @@ historyUpdatePayload: "history-update-payload"
/** user-defined types **/
export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: number; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; auto_submit?: boolean; auto_submit_key?: AutoSubmitKey; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: Partial<{ [key in string]: string }>; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean; append_trailing_space?: boolean; app_language?: string; experimental_enabled?: boolean; lazy_stream_close?: boolean; keyboard_implementation?: KeyboardImplementation; show_tray_icon?: boolean; paste_delay_ms?: number; typing_tool?: TypingTool; external_script_path: string | null; custom_filler_words?: string[] | null; whisper_accelerator?: WhisperAcceleratorSetting; ort_accelerator?: OrtAcceleratorSetting; whisper_gpu_device?: number; extra_recording_buffer_ms?: number }
export type AppSettings = { bindings: Partial<{ [key in string]: ShortcutBinding }>; push_to_talk: boolean; audio_feedback: boolean; audio_feedback_volume?: number; sound_theme?: SoundTheme; start_hidden?: boolean; autostart_enabled?: boolean; update_checks_enabled?: boolean; selected_model?: string; always_on_microphone?: boolean; selected_microphone?: string | null; clamshell_microphone?: string | null; selected_output_device?: string | null; translate_to_english?: boolean; selected_language?: string; overlay_position?: OverlayPosition; debug_mode?: boolean; log_level?: LogLevel; custom_words?: string[]; model_unload_timeout?: ModelUnloadTimeout; word_correction_threshold?: number; history_limit?: number; recording_retention_period?: RecordingRetentionPeriod; paste_method?: PasteMethod; clipboard_handling?: ClipboardHandling; auto_submit?: boolean; auto_submit_key?: AutoSubmitKey; post_process_enabled?: boolean; post_process_provider_id?: string; post_process_providers?: PostProcessProvider[]; post_process_api_keys?: SecretMap; post_process_models?: Partial<{ [key in string]: string }>; post_process_prompts?: LLMPrompt[]; post_process_selected_prompt_id?: string | null; mute_while_recording?: boolean; append_trailing_space?: boolean; app_language?: string; experimental_enabled?: boolean; lazy_stream_close?: boolean; keyboard_implementation?: KeyboardImplementation; show_tray_icon?: boolean; paste_delay_ms?: number; typing_tool?: TypingTool; external_script_path: string | null; custom_filler_words?: string[] | null; whisper_accelerator?: WhisperAcceleratorSetting; ort_accelerator?: OrtAcceleratorSetting; whisper_gpu_device?: number; extra_recording_buffer_ms?: number }
export type AudioDevice = { index: string; name: string; is_default: boolean }
export type AutoSubmitKey = "enter" | "ctrl_enter" | "cmd_enter"
export type AvailableAccelerators = { whisper: string[]; ort: string[]; gpu_devices: GpuDeviceOption[] }
@ -859,6 +859,7 @@ export type PasteMethod = "ctrl_v" | "direct" | "none" | "shift_insert" | "ctrl_
export type PermissionAccess = "allowed" | "denied" | "unknown"
export type PostProcessProvider = { id: string; label: string; base_url: string; allow_base_url_edit?: boolean; models_endpoint?: string | null; supports_structured_output?: boolean }
export type RecordingRetentionPeriod = "never" | "preserve_limit" | "days_3" | "weeks_2" | "months_3"
export type SecretMap = Partial<{ [key in string]: string }>
export type ShortcutBinding = { id: string; name: string; description: string; default_binding: string; current_binding: string }
export type SoundTheme = "marimba" | "pop" | "custom"
export type TypingTool = "auto" | "wtype" | "kwtype" | "dotool" | "ydotool" | "xdotool"