use affinidi_tdk::messaging::ATM;
use serde_json::Value;
use vti_common::vault::{SecretKind, StoredVaultEntry, VaultSecret, put_stored_vault_entry};
use crate::error::AppError;
use crate::store::KeyspaceHandle;
use crate::trust_tasks::wire_v0_2::{WireVersion, camelize_paths};
pub const TTL_CEILING_SECS: u32 = 60;
pub struct ReleasedSecret {
pub jwe: String,
pub secret_kind: SecretKind,
pub ttl_seconds: u32,
}
pub(crate) async fn release_secret(
atm: &ATM,
vault_ks: &KeyspaceHandle,
vta_did: &str,
holder_did: &str,
mut stored: StoredVaultEntry,
ttl_hint: Option<u32>,
wire: WireVersion,
) -> Result<ReleasedSecret, AppError> {
let ttl_seconds = ttl_hint
.map(|t| t.min(TTL_CEILING_SECS))
.unwrap_or(TTL_CEILING_SECS);
let secret_body = secret_cleartext_for_wire(&stored.secret, wire)?;
let jwe = super::authcrypt_to_holder(
atm,
vta_did,
holder_did,
super::RELEASE_INNER_MSG_TYPE,
secret_body,
)
.await?;
let secret_kind = stored.entry.secret_kind;
stored.entry.last_used_at = Some(chrono::Utc::now().to_rfc3339());
if let Err(e) = put_stored_vault_entry(vault_ks, &stored).await {
tracing::warn!(
entry_id = %stored.entry.id,
error = %e,
"vault/release: lastUsedAt update failed; secret release proceeded"
);
}
Ok(ReleasedSecret {
jwe,
secret_kind,
ttl_seconds,
})
}
fn secret_cleartext_for_wire(secret: &VaultSecret, wire: WireVersion) -> Result<Value, AppError> {
let mut body = serde_json::to_value(secret).map_err(|e| {
AppError::Internal(format!("vault/release: failed to serialise secret: {e}"))
})?;
if wire == WireVersion::V0_2 {
camelize_paths(&mut body, &["kind", "loginConfig.format"]);
}
Ok(body)
}
#[cfg(test)]
mod tests {
use super::*;
use vti_common::vault::{PasswordLoginConfig, PasswordLoginFormat};
#[test]
fn v0_1_release_keeps_kebab_kind_and_format() {
let secret = VaultSecret::OauthTokens {
provider: "google".into(),
refresh_token: "r".into(),
access_token: None,
access_token_expires_at: None,
scopes: vec![],
secure_notes: None,
};
let body = secret_cleartext_for_wire(&secret, WireVersion::V0_1).unwrap();
assert_eq!(body["kind"], "oauth-tokens");
}
#[test]
fn v0_2_release_camelizes_kind_and_login_config_format() {
let secret = VaultSecret::Password {
username: Some("u".into()),
password: "p".into(),
totp: None,
login_config: Some(PasswordLoginConfig {
login_url: "https://example.com/login".into(),
format: PasswordLoginFormat::FormUrlencoded,
username_field: None,
password_field: None,
totp_field: None,
extra_fields: None,
success_status: None,
}),
secure_notes: None,
custom_fields: vec![],
};
let v01 = secret_cleartext_for_wire(&secret, WireVersion::V0_1).unwrap();
assert_eq!(v01["kind"], "password");
assert_eq!(v01["loginConfig"]["format"], "form-urlencoded");
let v02 = secret_cleartext_for_wire(&secret, WireVersion::V0_2).unwrap();
assert_eq!(v02["kind"], "password");
assert_eq!(v02["loginConfig"]["format"], "formUrlencoded");
}
#[test]
fn v0_2_release_camelizes_multiword_kind() {
let secret = VaultSecret::DidSelfIssued {
did: "did:webvh:x".into(),
signing_key_id: "did:webvh:x#k".into(),
secure_notes: None,
};
let v02 = secret_cleartext_for_wire(&secret, WireVersion::V0_2).unwrap();
assert_eq!(v02["kind"], "didSelfIssued");
assert_eq!(v02["signingKeyId"], "did:webvh:x#k");
}
}