tokens for credentials

This commit is contained in:
Hanashi 2026-08-03 19:28:16 -04:00
parent 1621866cf1
commit e68e9f26d1
19 changed files with 283 additions and 62 deletions

View File

@ -40,6 +40,7 @@ pub async fn add_api_credential_command(
let source_path = creds_dir.next_available_credential_path(service);
let credential = Credential {
token: creds_dir.generate_unique_credential_token(),
service,
name: normalize_name(name),
secret: CredentialSecret::ApiKey(ApiKeyCredential::new(api_key)),

View File

@ -45,6 +45,7 @@ pub fn save_web_credential(
let credential = match existing {
Some(existing) => Credential {
token: existing.token,
service: existing.service,
name: existing.name,
secret: CredentialSecret::Cookies(cookie),
@ -52,6 +53,7 @@ pub fn save_web_credential(
source_path: existing.source_path,
},
None => Credential {
token: creds_dir.generate_unique_credential_token(),
service: save.service,
name: None,
secret: CredentialSecret::Cookies(cookie),

View File

@ -150,6 +150,7 @@ fn save_session_credential(
let creds_dir = app_data_root.credentials_dir();
let credential = Credential {
token: creds_dir.generate_unique_credential_token(),
service,
name: None,
secret: CredentialSecret::Cookies(cookie),

View File

@ -2,12 +2,18 @@ use crate::credentials::credential::Credential;
use crate::credentials::credential_service_type::{CredentialKind, CredentialServiceType};
use chrono::{DateTime, Utc};
use serde_derive::Serialize;
use tokens::tokens::sqlite::credentials::CredentialToken;
/// Frontend-facing view of a stored credential.
#[derive(Serialize)]
pub struct CredentialPayload {
/// The credential's identifier: its file name within the credentials
/// directory (e.g. `fal_api_2.toml`). Pass this back to edit/delete.
/// The credential's stable identity (`credential_{entropy}`). Hidden from
/// users, but the effective primary identifier — pass this back to
/// edit/delete and use it to reference the credential anywhere.
pub token: CredentialToken,
/// The credential's file name within the credentials directory
/// (e.g. `fal_api_2.toml`). Informational; may change if the user renames
/// the file.
pub id: String,
pub service: CredentialServiceType,
pub kind: CredentialKind,
@ -29,6 +35,7 @@ impl CredentialPayload {
let api_key = credential.api_key();
let cookie = credential.cookies();
Self {
token: credential.token.clone(),
id: credential.id(),
service: credential.service,
kind: credential.kind(),

View File

@ -2,21 +2,33 @@ use crate::state::data_dir::app_data_root::AppDataRoot;
use log::{error, info};
use tauri::State;
/// Delete a credential file, identified by its file name within the
/// credentials directory (e.g. `fal_api_2.toml`). The frontend asks the
/// user for confirmation before calling this.
/// Delete a credential, identified by its stable token. The frontend asks
/// the user for confirmation before calling this.
#[tauri::command]
pub async fn delete_credentials_command(
app_data_root: State<'_, AppDataRoot>,
file_name: String,
credential_token: String,
) -> Result<(), String> {
info!("delete_credentials_command called for: {}", file_name);
info!("delete_credentials_command called for: {}", credential_token);
app_data_root
.credentials_dir()
.delete_credential_file(&file_name)
let creds_dir = app_data_root.credentials_dir();
let credential = creds_dir
.find_credential_by_token(&credential_token)
.map_err(|err| {
error!("Error deleting credential {}: {}", file_name, err);
format!("Error deleting credential {}: {}", file_name, err)
error!("Error looking up credential {}: {}", credential_token, err);
format!("Error looking up credential {}: {}", credential_token, err)
})?
.ok_or_else(|| {
let message = format!("No credential found for token {}", credential_token);
error!("{}", message);
message
})?;
creds_dir
.delete_credential_file(&credential.id())
.map_err(|err| {
error!("Error deleting credential {}: {}", credential_token, err);
format!("Error deleting credential {}: {}", credential_token, err)
})
}

View File

@ -11,31 +11,35 @@ pub struct EditApiCredentialResponse {
pub credential: CredentialPayload,
}
/// Edit an existing API-key credential file, identified by its file name
/// within the credentials directory (e.g. `fal_api_2.toml`).
/// Edit an existing API-key credential, identified by its stable token.
///
/// `api_key` replaces the key when provided (non-empty). `name` replaces the
/// label; an empty string clears it. Omitted fields are left unchanged.
#[tauri::command]
pub async fn edit_api_credential_command(
app_data_root: State<'_, AppDataRoot>,
file_name: String,
credential_token: String,
api_key: Option<String>,
name: Option<String>,
) -> Result<EditApiCredentialResponse, String> {
info!("edit_api_credential_command called for: {}", file_name);
info!("edit_api_credential_command called for: {}", credential_token);
let creds_dir = app_data_root.credentials_dir();
let mut credential = creds_dir
.load_credential(&file_name)
.find_credential_by_token(&credential_token)
.map_err(|err| {
error!("Error loading credential {}: {}", file_name, err);
format!("Error loading credential {}: {}", file_name, err)
error!("Error looking up credential {}: {}", credential_token, err);
format!("Error looking up credential {}: {}", credential_token, err)
})?
.ok_or_else(|| {
let message = format!("No credential found for token {}", credential_token);
error!("{}", message);
message
})?;
let CredentialSecret::ApiKey(existing_key) = &credential.secret else {
let message = format!("Credential {} is not an API key credential", file_name);
let message = format!("Credential {} is not an API key credential", credential_token);
error!("{}", message);
return Err(message);
};
@ -55,8 +59,8 @@ pub async fn edit_api_credential_command(
creds_dir
.save_credential(&credential)
.map_err(|err| {
error!("Error saving credential {}: {}", file_name, err);
format!("Error saving credential {}: {}", file_name, err)
error!("Error saving credential {}: {}", credential_token, err);
format!("Error saving credential {}: {}", credential_token, err)
})?;
Ok(EditApiCredentialResponse {

View File

@ -12,8 +12,8 @@ pub struct EditWebCredentialResponse {
pub credential: CredentialPayload,
}
/// Edit an existing web-login (cookie) credential file, identified by its file
/// name within the credentials directory (e.g. `artcraft_cookies_2.toml`).
/// Edit an existing web-login (cookie) credential, identified by its stable
/// token.
///
/// `name` replaces the label; an empty string clears it. `cookie_header`
/// replaces the cookies when provided (non-empty). Omitted fields are left
@ -21,23 +21,28 @@ pub struct EditWebCredentialResponse {
#[tauri::command]
pub async fn edit_web_credential_command(
app_data_root: State<'_, AppDataRoot>,
file_name: String,
credential_token: String,
name: Option<String>,
cookie_header: Option<String>,
) -> Result<EditWebCredentialResponse, String> {
info!("edit_web_credential_command called for: {}", file_name);
info!("edit_web_credential_command called for: {}", credential_token);
let creds_dir = app_data_root.credentials_dir();
let mut credential = creds_dir
.load_credential(&file_name)
.find_credential_by_token(&credential_token)
.map_err(|err| {
error!("Error loading credential {}: {}", file_name, err);
format!("Error loading credential {}: {}", file_name, err)
error!("Error looking up credential {}: {}", credential_token, err);
format!("Error looking up credential {}: {}", credential_token, err)
})?
.ok_or_else(|| {
let message = format!("No credential found for token {}", credential_token);
error!("{}", message);
message
})?;
let CredentialSecret::Cookies(existing_cookie) = &credential.secret else {
let message = format!("Credential {} is not a cookie credential", file_name);
let message = format!("Credential {} is not a cookie credential", credential_token);
error!("{}", message);
return Err(message);
};
@ -65,8 +70,8 @@ pub async fn edit_web_credential_command(
creds_dir
.save_credential(&credential)
.map_err(|err| {
error!("Error saving credential {}: {}", file_name, err);
format!("Error saving credential {}: {}", file_name, err)
error!("Error saving credential {}: {}", credential_token, err);
format!("Error saving credential {}: {}", credential_token, err)
})?;
Ok(EditWebCredentialResponse {

View File

@ -5,6 +5,7 @@ use crate::credentials::credential_service_type::{CredentialKind, CredentialServ
use crate::credentials::credential_user_info::CredentialUserInfo;
use crate::error::artcraftx_credential_error::ArtcraftXCredentialError;
use std::path::{Path, PathBuf};
use tokens::tokens::sqlite::credentials::CredentialToken;
/// A validated, in-app credential.
///
@ -14,6 +15,10 @@ use std::path::{Path, PathBuf};
/// timestamps, etc.)
#[derive(Clone, Debug)]
pub struct Credential {
/// The credential's stable identity (`credential_{entropy}`), stored in the
/// TOML file. Hidden from users, but the effective primary identifier —
/// file names can be freely renamed.
pub token: CredentialToken,
pub service: CredentialServiceType,
/// Optional user-facing label to tell multiple credentials for the same
/// service apart. Empty by default.
@ -41,10 +46,19 @@ impl CredentialSecret {
}
impl Credential {
/// Load a credential, minting (and persisting) a token if the file lacks
/// one so the identity stays stable across loads.
pub fn load_from_file<P: AsRef<Path>>(path: P) -> Result<Self, ArtcraftXCredentialError> {
let path = path.as_ref();
let file = CredentialFile::load(path)?;
file.into_credential(path.to_path_buf())
let needs_token_backfill = file.token.is_none();
let credential = file.into_credential(path.to_path_buf())?;
if needs_token_backfill {
if let Err(err) = credential.save() {
log::warn!("Could not persist backfilled credential token for {:?}: {}", path, err);
}
}
Ok(credential)
}
/// Rewrite this credential's TOML file in place.
@ -90,6 +104,7 @@ mod tests {
let path = dir.path().join("fal_api_key.toml");
let credential = Credential {
token: CredentialToken::generate(),
service: CredentialServiceType::FalApi,
name: Some("work account".to_string()),
secret: CredentialSecret::ApiKey(ApiKeyCredential::new("fal-secret-key-12345")),
@ -99,6 +114,7 @@ mod tests {
credential.save().unwrap();
let loaded = Credential::load_from_file(&path).unwrap();
assert_eq!(loaded.token, credential.token);
assert_eq!(loaded.service, CredentialServiceType::FalApi);
assert_eq!(loaded.name.as_deref(), Some("work account"));
assert_eq!(loaded.api_key().unwrap().api_key, "fal-secret-key-12345");

View File

@ -6,6 +6,7 @@ use crate::credentials::credential_user_info::CredentialUserInfo;
use crate::error::artcraftx_credential_error::ArtcraftXCredentialError;
use serde_derive::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use tokens::tokens::sqlite::credentials::CredentialToken;
/// The on-disk (TOML) shape of a credential file.
///
@ -16,6 +17,7 @@ use std::path::{Path, PathBuf};
/// Example file (`~/Artcraft/artcraftx/credentials/higgsfield.toml`):
///
/// ```toml
/// token = "credential_2f7h9k3mnp5qr8tv0wx4yz6ab"
/// service = "higgsfield_cookies"
///
/// [cookie]
@ -26,6 +28,11 @@ use std::path::{Path, PathBuf};
/// ```
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct CredentialFile {
/// The credential's stable identity. Absent in older or hand-written
/// files; loading backfills a freshly generated token.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token: Option<CredentialToken>,
pub service: CredentialServiceType,
/// Optional user-facing label. Empty by default.
@ -96,6 +103,7 @@ impl CredentialFile {
}
Ok(Credential {
token: self.token.unwrap_or_else(CredentialToken::generate),
service: self.service,
name: self.name,
secret,
@ -110,6 +118,7 @@ impl CredentialFile {
CredentialSecret::ApiKey(api_key) => (None, Some(api_key.clone())),
};
Self {
token: Some(credential.token.clone()),
service: credential.service,
name: credential.name.clone(),
cookie,
@ -225,6 +234,30 @@ mod tests {
}
}
mod token_tests {
use super::*;
#[test]
fn token_in_file_is_preserved() {
let toml_text = r#"
token = "credential_test123"
service = "fal_api"
[api_key]
api_key = "x"
"#;
let file: CredentialFile = toml::from_str(toml_text).unwrap();
let credential = file.into_credential(PathBuf::from("test.toml")).unwrap();
assert_eq!(credential.token.as_str(), "credential_test123");
}
#[test]
fn missing_token_generates_one() {
let file: CredentialFile = toml::from_str(HAND_WRITTEN_API_KEY_FILE).unwrap();
let credential = file.into_credential(PathBuf::from("test.toml")).unwrap();
assert!(credential.token.as_str().starts_with("credential_"));
}
}
mod round_trip_tests {
use super::*;

View File

@ -3,7 +3,9 @@ use crate::credentials::credential_service_type::CredentialServiceType;
use crate::error::artcraftx_credential_error::ArtcraftXCredentialError;
use crate::error::artcraftx_error::ArtcraftXError;
use crate::state::data_dir::subdirectory::trait_data_subdir::DataSubdir;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use tokens::tokens::sqlite::credentials::CredentialToken;
/// The directory holding per-service credential TOML files
/// (by default `~/Artcraft/artcraftx/credentials`).
@ -51,9 +53,24 @@ impl AppCredentialsDir {
paths.sort();
let mut credentials = Vec::new();
let mut seen_tokens: HashSet<CredentialToken> = HashSet::new();
for path in paths {
match Credential::load_from_file(&path) {
Ok(credential) => credentials.push(credential),
Ok(mut credential) => {
// Tokens must be unique; a duplicate (eg. a copied file) gets a
// fresh token, persisted so it stays stable.
if seen_tokens.contains(&credential.token) {
credential.token = generate_token_not_in(&seen_tokens);
if let Err(err) = self.save_credential(&credential) {
log::warn!(
"Could not persist deduplicated credential token for {:?}: {}",
credential.source_path, err,
);
}
}
seen_tokens.insert(credential.token.clone());
credentials.push(credential);
},
Err(err) => {
log::warn!("Skipping bad credential file: {}", err);
},
@ -119,6 +136,25 @@ impl AppCredentialsDir {
Credential::load_from_file(path).map_err(ArtcraftXError::from)
}
/// Find a credential by its token, the stable primary identifier.
pub fn find_credential_by_token(
&self,
token: &str,
) -> Result<Option<Credential>, ArtcraftXError> {
Ok(self.load_credentials()?
.into_iter()
.find(|credential| credential.token.as_str() == token))
}
/// Generate a token no existing credential uses. Collisions are
/// astronomically unlikely; the check guards against them anyway.
pub fn generate_unique_credential_token(&self) -> CredentialToken {
let existing: HashSet<CredentialToken> = self.load_credentials()
.map(|credentials| credentials.into_iter().map(|c| c.token).collect())
.unwrap_or_default();
generate_token_not_in(&existing)
}
/// Delete a credential file by id (file name).
pub fn delete_credential_file(&self, file_name: &str) -> Result<(), ArtcraftXError> {
let path = self.resolve_credential_file(file_name)?;
@ -127,6 +163,15 @@ impl AppCredentialsDir {
}
}
fn generate_token_not_in(taken: &HashSet<CredentialToken>) -> CredentialToken {
loop {
let token = CredentialToken::generate();
if !taken.contains(&token) {
return token;
}
}
}
fn has_toml_extension(path: &Path) -> bool {
path.extension()
.map(|ext| ext.eq_ignore_ascii_case("toml"))
@ -175,6 +220,7 @@ mod tests {
let creds_dir = AppCredentialsDir::new_from(dir.path());
let credential = Credential {
token: CredentialToken::generate(),
service: CredentialServiceType::FalApi,
name: None,
secret: CredentialSecret::ApiKey(ApiKeyCredential::new("fal-key-123")),
@ -206,6 +252,65 @@ mod tests {
assert_eq!(third.file_name().unwrap(), "fal_api_3.toml");
}
#[test]
fn missing_token_is_backfilled_and_stable_across_loads() {
let dir = tempfile::tempdir().unwrap();
write_file(
dir.path(),
"fal_api.toml",
"service = \"fal_api\"\n[api_key]\napi_key = \"fal-key-123\"\n",
);
let creds_dir = AppCredentialsDir::new_from(dir.path());
let first_load = creds_dir.load_credentials().unwrap();
assert_eq!(first_load.len(), 1);
let token = first_load[0].token.clone();
assert!(token.as_str().starts_with("credential_"));
// The generated token was persisted, so a reload sees the same one.
let second_load = creds_dir.load_credentials().unwrap();
assert_eq!(second_load[0].token, token);
}
#[test]
fn duplicate_tokens_are_deduplicated_on_load() {
let dir = tempfile::tempdir().unwrap();
let duplicated = "token = \"credential_copied\"\nservice = \"fal_api\"\n[api_key]\napi_key = \"x\"\n";
write_file(dir.path(), "fal_api.toml", duplicated);
write_file(dir.path(), "fal_api_2.toml", duplicated);
let creds_dir = AppCredentialsDir::new_from(dir.path());
let credentials = creds_dir.load_credentials().unwrap();
assert_eq!(credentials.len(), 2);
assert_ne!(credentials[0].token, credentials[1].token);
// The reassigned token was persisted, so a reload is stable.
let reloaded = creds_dir.load_credentials().unwrap();
let mut tokens: Vec<_> = credentials.iter().map(|c| c.token.clone()).collect();
let mut reloaded_tokens: Vec<_> = reloaded.iter().map(|c| c.token.clone()).collect();
tokens.sort();
reloaded_tokens.sort();
assert_eq!(tokens, reloaded_tokens);
}
#[test]
fn find_credential_by_token_matches() {
let dir = tempfile::tempdir().unwrap();
write_file(
dir.path(),
"fal_api.toml",
"token = \"credential_findme\"\nservice = \"fal_api\"\n[api_key]\napi_key = \"x\"\n",
);
let creds_dir = AppCredentialsDir::new_from(dir.path());
let found = creds_dir.find_credential_by_token("credential_findme").unwrap();
assert_eq!(found.unwrap().id(), "fal_api.toml");
let missing = creds_dir.find_credential_by_token("credential_nope").unwrap();
assert!(missing.is_none());
}
#[test]
fn resolve_rejects_path_traversal() {
let dir = tempfile::tempdir().unwrap();

View File

@ -68,6 +68,7 @@ pub(crate) enum TokenPrefix {
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[cfg_attr(test, derive(EnumIter, EnumCount))]
pub(crate) enum TauriTokenPrefix {
Credential,
Task,
}
@ -162,6 +163,7 @@ impl PrefixGenerator for TokenPrefix {
impl PrefixGenerator for TauriTokenPrefix {
fn prefix(self) -> &'static str {
match self {
TauriTokenPrefix::Credential => "credential_",
TauriTokenPrefix::Task => "task_",
}
}

View File

@ -0,0 +1,20 @@
use std::fmt::Debug;
use serde::Deserialize;
use serde::Serialize;
use crate::prefixes::TauriTokenPrefix;
/// The stable identifier for a stored credential in the Tauri desktop app.
///
/// Unlike most tokens, this is not a database primary key: it lives inside
/// each credential TOML file in the app's credentials directory. It is hidden
/// from users but serves as the effective primary identifier for a credential
/// (file names can be freely renamed).
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "database", derive(sqlx::Type))]
#[cfg_attr(feature = "database", sqlx(transparent))]
pub struct CredentialToken(pub String);
impl_string_token!(CredentialToken);
impl_crockford_generator!(CredentialToken, 32usize, TauriTokenPrefix::Credential, CrockfordLower);

View File

@ -1,3 +1,4 @@
pub mod credentials;
pub mod news_stories;
pub mod tasks;
pub mod tts_render_tasks;

View File

@ -22,11 +22,11 @@ import { useAccountSelectorStore } from "./accountSelectorStore";
*/
export const AccountSelector = () => {
const [credentials, setCredentials] = useState<CredentialPayload[]>([]);
const selectedAccountId = useAccountSelectorStore(
(state) => state.selectedAccountId
const selectedAccountToken = useAccountSelectorStore(
(state) => state.selectedAccountToken
);
const setSelectedAccountId = useAccountSelectorStore(
(state) => state.setSelectedAccountId
const setSelectedAccountToken = useAccountSelectorStore(
(state) => state.setSelectedAccountToken
);
const refresh = useCallback(async () => {
@ -50,21 +50,24 @@ export const AccountSelector = () => {
// clear) when the selected credential is deleted.
useEffect(() => {
if (credentials.length === 0) {
if (selectedAccountId !== null) setSelectedAccountId(null);
if (selectedAccountToken !== null) setSelectedAccountToken(null);
return;
}
if (!credentials.some((credential) => credential.id === selectedAccountId)) {
setSelectedAccountId(credentials[0].id);
const selectionExists = credentials.some(
(credential) => credential.token === selectedAccountToken
);
if (!selectionExists) {
setSelectedAccountToken(credentials[0].token);
}
}, [credentials, selectedAccountId, setSelectedAccountId]);
}, [credentials, selectedAccountToken, setSelectedAccountToken]);
const items: PopoverItem[] =
credentials.length > 0
? credentials.map((credential) => ({
label: accountLabel(credential),
description: accountDescription(credential),
selected: credential.id === selectedAccountId,
action: credential.id,
selected: credential.token === selectedAccountToken,
action: credential.token,
icon: (
<img
src={getServiceLogoPath(credential.service)}
@ -83,11 +86,11 @@ export const AccountSelector = () => {
];
const handleSelect = (item: PopoverItem) => {
if (item.action) setSelectedAccountId(item.action);
if (item.action) setSelectedAccountToken(item.action);
};
const selectedCredential = credentials.find(
(credential) => credential.id === selectedAccountId
(credential) => credential.token === selectedAccountToken
);
return (

View File

@ -2,16 +2,16 @@ import { create } from "zustand";
interface AccountSelectorState {
/**
* Credential id (the credential's file name, e.g. `artcraft_2.toml`) of
* the account selected in the toolbar account pickers. Shared across all
* generation pages. `null` until accounts load (the selector defaults to
* the first account) or when no accounts exist.
* Stable credential token (`credential_{entropy}`) of the account selected
* in the toolbar account pickers. Shared across all generation pages.
* `null` until accounts load (the selector defaults to the first account)
* or when no accounts exist.
*/
selectedAccountId: string | null;
setSelectedAccountId: (id: string | null) => void;
selectedAccountToken: string | null;
setSelectedAccountToken: (token: string | null) => void;
}
export const useAccountSelectorStore = create<AccountSelectorState>((set) => ({
selectedAccountId: null,
setSelectedAccountId: (id) => set({ selectedAccountId: id }),
selectedAccountToken: null,
setSelectedAccountToken: (token) => set({ selectedAccountToken: token }),
}));

View File

@ -79,7 +79,7 @@ export const CredentialsManager = () => {
<div className="flex flex-col gap-1.5">
{credentials.map((credential) => (
<CredentialRow
key={credential.id}
key={credential.token}
credential={credential}
onEdit={() => setEditing(credential)}
onDelete={() => setDeleting(credential)}

View File

@ -39,7 +39,7 @@ export const DeleteCredentialModal = ({
try {
// Deletes by file name (the credential id) so we're sure we're
// removing exactly this one.
await deleteCredential(credential.id);
await deleteCredential(credential.token);
onDeleted();
onClose();
} catch (e) {

View File

@ -47,7 +47,7 @@ export const EditCredentialModal = ({
setErrorMessage("");
try {
await editApiCredential({
fileName: credential.id,
credentialToken: credential.token,
apiKey: apiKey.trim(),
// Empty string clears the name on the Rust side.
name: name.trim(),

View File

@ -3,7 +3,14 @@ import { getServicesBasePath } from "@storyteller/model-list";
// Mirrors the Rust `CredentialPayload` (crates/artcraftx/src/commands/credentials).
export interface CredentialPayload {
/** File name within the credentials directory, e.g. `fal_api_2.toml`. */
/**
* Stable identity (`credential_{entropy}`) stored inside the credential
* file. Hidden from users, but the effective primary identifier — pass it
* back to edit/delete and use it to reference the credential anywhere.
*/
token: string;
/** File name within the credentials directory, e.g. `fal_api_2.toml`.
* Informational; changes if the user renames the file. */
id: string;
service: string;
kind: "cookies" | "api_key";
@ -103,14 +110,14 @@ export const addApiCredential = async (args: {
};
export const editApiCredential = async (args: {
fileName: string;
credentialToken: string;
apiKey?: string;
name?: string;
}): Promise<CredentialPayload> => {
const response = await invoke<{ credential: CredentialPayload }>(
"edit_api_credential_command",
{
fileName: args.fileName,
credentialToken: args.credentialToken,
apiKey: args.apiKey ?? null,
name: args.name ?? null,
}
@ -118,8 +125,10 @@ export const editApiCredential = async (args: {
return response.credential;
};
export const deleteCredential = async (fileName: string): Promise<void> => {
await invoke("delete_credentials_command", { fileName });
export const deleteCredential = async (
credentialToken: string
): Promise<void> => {
await invoke("delete_credentials_command", { credentialToken });
};
/**