+102
-1
src-tauri/Cargo.lock
+102
-1
src-tauri/Cargo.lock
···
874
874
checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476"
875
875
876
876
[[package]]
877
+
name = "dbus"
878
+
version = "0.9.9"
879
+
source = "registry+https://github.com/rust-lang/crates.io-index"
880
+
checksum = "190b6255e8ab55a7b568df5a883e9497edc3e4821c06396612048b430e5ad1e9"
881
+
dependencies = [
882
+
"libc",
883
+
"libdbus-sys",
884
+
"windows-sys 0.59.0",
885
+
]
886
+
887
+
[[package]]
888
+
name = "dbus-secret-service"
889
+
version = "4.1.0"
890
+
source = "registry+https://github.com/rust-lang/crates.io-index"
891
+
checksum = "708b509edf7889e53d7efb0ffadd994cc6c2345ccb62f55cfd6b0682165e4fa6"
892
+
dependencies = [
893
+
"dbus",
894
+
"zeroize",
895
+
]
896
+
897
+
[[package]]
877
898
name = "deranged"
878
899
version = "0.4.0"
879
900
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2052
2073
]
2053
2074
2054
2075
[[package]]
2076
+
name = "keyring"
2077
+
version = "3.6.3"
2078
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2079
+
checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c"
2080
+
dependencies = [
2081
+
"byteorder",
2082
+
"dbus-secret-service",
2083
+
"log",
2084
+
"security-framework 2.11.1",
2085
+
"security-framework 3.3.0",
2086
+
"windows-sys 0.60.2",
2087
+
"zeroize",
2088
+
]
2089
+
2090
+
[[package]]
2055
2091
name = "kuchikiki"
2056
2092
version = "0.8.8-speedreader"
2057
2093
source = "registry+https://github.com/rust-lang/crates.io-index"
···
2098
2134
version = "0.2.175"
2099
2135
source = "registry+https://github.com/rust-lang/crates.io-index"
2100
2136
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
2137
+
2138
+
[[package]]
2139
+
name = "libdbus-sys"
2140
+
version = "0.2.6"
2141
+
source = "registry+https://github.com/rust-lang/crates.io-index"
2142
+
checksum = "5cbe856efeb50e4681f010e9aaa2bf0a644e10139e54cde10fc83a307c23bd9f"
2143
+
dependencies = [
2144
+
"pkg-config",
2145
+
]
2101
2146
2102
2147
[[package]]
2103
2148
name = "libloading"
···
2242
2287
"cpal",
2243
2288
"dasp",
2244
2289
"futures-util",
2290
+
"keyring",
2245
2291
"serde",
2246
2292
"serde_json",
2293
+
"strum",
2247
2294
"tauri",
2248
2295
"tauri-build",
2249
2296
"tauri-plugin-opener",
···
2288
2335
"openssl-probe",
2289
2336
"openssl-sys",
2290
2337
"schannel",
2291
-
"security-framework",
2338
+
"security-framework 2.11.1",
2292
2339
"security-framework-sys",
2293
2340
"tempfile",
2294
2341
]
···
3496
3543
]
3497
3544
3498
3545
[[package]]
3546
+
name = "security-framework"
3547
+
version = "3.3.0"
3548
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3549
+
checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c"
3550
+
dependencies = [
3551
+
"bitflags 2.9.3",
3552
+
"core-foundation 0.10.1",
3553
+
"core-foundation-sys",
3554
+
"libc",
3555
+
"security-framework-sys",
3556
+
]
3557
+
3558
+
[[package]]
3499
3559
name = "security-framework-sys"
3500
3560
version = "2.14.0"
3501
3561
source = "registry+https://github.com/rust-lang/crates.io-index"
···
3858
3918
version = "0.11.1"
3859
3919
source = "registry+https://github.com/rust-lang/crates.io-index"
3860
3920
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
3921
+
3922
+
[[package]]
3923
+
name = "strum"
3924
+
version = "0.27.2"
3925
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3926
+
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
3927
+
dependencies = [
3928
+
"strum_macros",
3929
+
]
3930
+
3931
+
[[package]]
3932
+
name = "strum_macros"
3933
+
version = "0.27.2"
3934
+
source = "registry+https://github.com/rust-lang/crates.io-index"
3935
+
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
3936
+
dependencies = [
3937
+
"heck 0.5.0",
3938
+
"proc-macro2",
3939
+
"quote",
3940
+
"syn 2.0.106",
3941
+
]
3861
3942
3862
3943
[[package]]
3863
3944
name = "swift-rs"
···
5673
5754
"quote",
5674
5755
"syn 2.0.106",
5675
5756
"synstructure",
5757
+
]
5758
+
5759
+
[[package]]
5760
+
name = "zeroize"
5761
+
version = "1.8.1"
5762
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5763
+
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
5764
+
dependencies = [
5765
+
"zeroize_derive",
5766
+
]
5767
+
5768
+
[[package]]
5769
+
name = "zeroize_derive"
5770
+
version = "1.4.2"
5771
+
source = "registry+https://github.com/rust-lang/crates.io-index"
5772
+
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
5773
+
dependencies = [
5774
+
"proc-macro2",
5775
+
"quote",
5776
+
"syn 2.0.106",
5676
5777
]
5677
5778
5678
5779
[[package]]
+2
src-tauri/Cargo.toml
+2
src-tauri/Cargo.toml
···
28
28
futures-util = "0.3.31"
29
29
tokio = "1.47.1"
30
30
dasp = { version = "0.11.0", features = ["all"] }
31
+
keyring = { version = "3.6.3", features = ["apple-native", "windows-native", "sync-secret-service"] }
32
+
strum = {version = "0.27.2", features = ["derive"] }
31
33
32
34
[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies]
33
35
tauri-plugin-positioner = "2"
+14
-8
src-tauri/src/cartesia/client.rs
+14
-8
src-tauri/src/cartesia/client.rs
···
1
1
use std::sync::Arc;
2
-
use tauri::{async_runtime::RwLock, Url};
2
+
use tauri::{http::HeaderValue, Url};
3
3
use tokio::net::TcpStream;
4
4
use tokio_tungstenite::{
5
5
connect_async, tungstenite::client::IntoClientRequest, MaybeTlsStream, WebSocketStream,
6
6
};
7
7
8
+
use crate::secrets::{SecretName, SecretsManager};
9
+
8
10
#[derive(Clone)]
9
11
pub struct CartesiaClient {
10
-
api_key: Arc<RwLock<String>>,
12
+
secrets_manager: Arc<SecretsManager>,
11
13
}
12
14
13
15
impl CartesiaClient {
14
-
pub fn new(api_key: String) -> Self {
15
-
Self {
16
-
api_key: Arc::new(RwLock::new(api_key)),
17
-
}
16
+
pub fn new(secrets_manager: Arc<SecretsManager>) -> Self {
17
+
Self { secrets_manager }
18
18
}
19
19
20
20
pub async fn open_stt_connection(&self) -> WebSocketStream<MaybeTlsStream<TcpStream>> {
21
-
let api_key = self.api_key.read().await.clone();
22
21
let mut request = Url::parse_with_params(
23
22
"wss://api.cartesia.ai/stt/websocket",
24
23
&[
···
33
32
.expect("failed to instantiate STT WebSocket request");
34
33
35
34
let headers = request.headers_mut();
35
+
let api_key = self
36
+
.secrets_manager
37
+
.get_secret(SecretName::CartesiaApiKey)
38
+
.expect("failed to retrieve API key");
36
39
37
-
headers.insert("X-API-Key", api_key.parse().unwrap());
40
+
headers.insert(
41
+
"X-API-Key",
42
+
HeaderValue::from_str(api_key.as_str()).expect("could not convert key to header value"),
43
+
);
38
44
headers.insert("Cartesia-Version", "2025-04-16".parse().unwrap());
39
45
40
46
let (stream, _) = connect_async(request)
+4
-1
src-tauri/src/lib.rs
+4
-1
src-tauri/src/lib.rs
···
1
1
use crate::state::AppState;
2
-
use tauri::async_runtime::Mutex;
3
2
use tauri::{Manager, WebviewUrl, WebviewWindowBuilder};
4
3
use tauri_plugin_positioner::{Position, WindowExt};
5
4
use tauri_plugin_window_state::{StateFlags, WindowExt as StateWindowExt};
6
5
7
6
mod cartesia;
8
7
mod devices;
8
+
mod secrets;
9
9
mod state;
10
10
11
11
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
···
67
67
.invoke_handler(tauri::generate_handler![
68
68
cartesia::commands::start_stt,
69
69
cartesia::commands::stop_stt,
70
+
secrets::commands::has_secret,
71
+
secrets::commands::set_secret,
72
+
secrets::commands::delete_secret,
70
73
])
71
74
.run(tauri::generate_context!())
72
75
.expect("error while running tauri application");
+61
src-tauri/src/secrets/commands.rs
+61
src-tauri/src/secrets/commands.rs
···
1
+
use crate::{secrets::SecretName, state::AppState};
2
+
use std::str::FromStr;
3
+
4
+
#[tauri::command]
5
+
pub async fn has_secret(state: tauri::State<'_, AppState>, name: String) -> Result<bool, ()> {
6
+
match SecretName::from_str(&name) {
7
+
Ok(name) => {
8
+
let res = state
9
+
.secrets_manager
10
+
.has_secret(name)
11
+
.expect("failed to check credential existence");
12
+
13
+
Ok(res)
14
+
}
15
+
Err(err) => {
16
+
eprintln!("invalid credential name: {}", err);
17
+
18
+
Ok(false)
19
+
}
20
+
}
21
+
}
22
+
23
+
#[tauri::command]
24
+
pub fn set_secret(
25
+
state: tauri::State<'_, AppState>,
26
+
name: String,
27
+
value: String,
28
+
) -> Result<(), ()> {
29
+
match SecretName::from_str(&name) {
30
+
Ok(name) => {
31
+
state
32
+
.secrets_manager
33
+
.set_secret(name, value)
34
+
.expect("failed to set credential");
35
+
36
+
Ok(())
37
+
}
38
+
Err(err) => {
39
+
eprintln!("failed to decode secret name: {}", err);
40
+
Err(())
41
+
}
42
+
}
43
+
}
44
+
45
+
#[tauri::command]
46
+
pub fn delete_secret(state: tauri::State<'_, AppState>, name: String) -> Result<(), ()> {
47
+
match SecretName::from_str(&name) {
48
+
Ok(name) => {
49
+
state
50
+
.secrets_manager
51
+
.delete_secret(name)
52
+
.expect("failed to delete credential");
53
+
54
+
Ok(())
55
+
}
56
+
Err(err) => {
57
+
eprintln!("failed to decode secret name: {}", err);
58
+
Err(())
59
+
}
60
+
}
61
+
}
+46
src-tauri/src/secrets/mod.rs
+46
src-tauri/src/secrets/mod.rs
···
1
+
use keyring::{Entry, Error};
2
+
use strum::{Display, EnumString};
3
+
4
+
pub mod commands;
5
+
6
+
#[derive(Debug, PartialEq, Display, EnumString)]
7
+
pub enum SecretName {
8
+
#[strum(to_string = "cartesia_api_key")]
9
+
CartesiaApiKey,
10
+
}
11
+
12
+
const SERVICE_NAME: &'static str = "miwiwi";
13
+
14
+
pub struct SecretsManager {}
15
+
impl SecretsManager {
16
+
pub fn new() -> Self {
17
+
Self {}
18
+
}
19
+
20
+
pub fn get_secret(&self, name: SecretName) -> Result<String, Error> {
21
+
let entry = Entry::new(SERVICE_NAME, &name.to_string())?;
22
+
let cred = entry.get_password()?;
23
+
24
+
Ok(cred)
25
+
}
26
+
27
+
pub fn set_secret(&self, name: SecretName, value: String) -> Result<(), Error> {
28
+
let entry = Entry::new(SERVICE_NAME, &name.to_string())?;
29
+
30
+
entry.set_password(&value)
31
+
}
32
+
33
+
pub fn delete_secret(&self, name: SecretName) -> Result<(), Error> {
34
+
let entry = Entry::new(SERVICE_NAME, &name.to_string())?;
35
+
36
+
entry.delete_credential()
37
+
}
38
+
39
+
pub fn has_secret(&self, name: SecretName) -> Result<bool, Error> {
40
+
match self.get_secret(name) {
41
+
Ok(_) => Ok(true),
42
+
Err(Error::NoEntry) => Ok(false),
43
+
Err(err) => Err(err),
44
+
}
45
+
}
46
+
}
+5
-2
src-tauri/src/state.rs
+5
-2
src-tauri/src/state.rs
···
3
3
use crate::{
4
4
cartesia::{client::CartesiaClient, stt::SttManager},
5
5
devices::{input::InputDeviceManager, output::OutputDeviceManager, types::AudioDeviceError},
6
+
secrets::SecretsManager,
6
7
};
7
8
8
9
pub struct AppState {
9
10
pub cartesia_client: Arc<CartesiaClient>,
10
11
pub stt_manager: Arc<SttManager>,
12
+
pub secrets_manager: Arc<SecretsManager>,
11
13
pub input_device_manager: Arc<InputDeviceManager>,
12
14
pub output_device_manager: Arc<OutputDeviceManager>,
13
15
}
···
16
18
pub fn new() -> Result<Self, AudioDeviceError> {
17
19
let input_device_manager = Arc::new(InputDeviceManager::new()?);
18
20
let output_device_manager = Arc::new(OutputDeviceManager::new()?);
19
-
20
-
let cartesia_client = Arc::new(CartesiaClient::new("TODO".into()));
21
+
let secrets_manager = Arc::new(SecretsManager::new());
22
+
let cartesia_client = Arc::new(CartesiaClient::new(secrets_manager.clone()));
21
23
let stt_manager = Arc::new(SttManager::new(
22
24
cartesia_client.clone(),
23
25
input_device_manager.clone(),
···
26
28
Ok(AppState {
27
29
input_device_manager,
28
30
output_device_manager,
31
+
secrets_manager,
29
32
cartesia_client,
30
33
stt_manager,
31
34
})
+29
-2
src/routes/+page.svelte
+29
-2
src/routes/+page.svelte
···
1
1
<script lang="ts">
2
2
import { invoke } from "@tauri-apps/api/core"
3
+
4
+
let keyInvocation = $state(invoke("has_secret", { name: "cartesia_api_key"}))
5
+
let tempCredential = $state("")
6
+
7
+
async function deleteKey() {
8
+
await invoke("delete_secret", { name: "cartesia_api_key"})
9
+
keyInvocation = invoke("has_secret", { name: "cartesia_api_key"})
10
+
}
3
11
</script>
4
12
13
+
{#await keyInvocation}
14
+
<p>checking for cred...</p>
15
+
{:then hasKey}
16
+
{#if hasKey}
17
+
<p>key has been set</p>
18
+
<button onclick={deleteKey}>delete key</button>
19
+
{:else}
20
+
<p>no key yet</p>
21
+
<input placeholder="my key..." bind:value={tempCredential} />
22
+
<button onclick={async () => {
23
+
await invoke("set_secret", { name: "cartesia_api_key", value: tempCredential})
24
+
25
+
keyInvocation = invoke("has_secret", { name: "cartesia_api_key"})
26
+
}}>Save API key</button>
27
+
{/if}
28
+
{:catch err}
29
+
<p>{err}</p>
30
+
{/await}
31
+
5
32
<main>miwiwi</main>
6
-
<button rel="button" on:click={() => invoke("start_stt")}>Start STT</button>
7
-
<button rel="button" on:click={() => invoke("stop_stt")}>Stop STT</button>
33
+
<button onclick={() => invoke("start_stt")}>Start STT</button>
34
+
<button onclick={() => invoke("stop_stt")}>Stop STT</button>
8
35