use std::sync::Arc;
use serde_json::Value as JsonValue;
use thiserror::Error;
use tokio::sync::RwLock;
use tracing::{info, warn};
use vta_sdk::error::VtaError;
use vti_common::telemetry::{TelemetryEvent, TelemetryKind};
use crate::auth::AuthClaims;
use crate::config::AppConfig;
use crate::error::AppError;
use crate::operations::did_webvh::UpdateDidWebvhError;
use crate::operations::protocol::document::DocumentPatchError;
use crate::operations::protocol::passkey_vm_cleanup::{self, CleanupSummary};
use crate::operations::protocol::service_lifecycle::{
DisableMutationError, ServiceLifecycle, WebauthnService, check_disable_preconditions,
publish_patch,
};
use crate::operations::protocol::{OpContext, PROTOCOL_LOCK, ServiceOpDeps, snapshot};
#[derive(Debug, Clone, Default)]
pub struct DisableWebauthnParams {}
#[derive(Debug, Clone)]
pub struct DisableWebauthnResult {
pub new_version_id: String,
pub vta_did: String,
pub serverless: bool,
pub cleanup: CleanupSummary,
}
#[derive(Debug, Error)]
pub enum DisableWebauthnError {
#[error("WebAuthn is not currently enabled. Use `services webauthn enable --url <url>` first.")]
ServiceNotPresent,
#[error(
"refusing to disable — at least one transport (REST, DIDComm, or WebAuthn) must remain advertised"
)]
LastServiceRefused,
#[error("VTA DID is not configured — run `vta setup` first")]
VtaDidNotConfigured,
#[error("VTA DID `{0}` has no webvh record")]
VtaDidRecordMissing(String),
#[error("VTA DID `{0}` has no published log")]
VtaDidLogMissing(String),
#[error("VTA DID log is empty")]
EmptyLog,
#[error("DID document patch failed: {0}")]
DocumentPatch(#[from] DocumentPatchError),
#[error("WebVH update failed: {0}")]
WebVHUpdate(#[from] UpdateDidWebvhError),
#[error("config persistence failed: {0}")]
ConfigPersistence(String),
#[error("auth: {0}")]
Auth(String),
#[error("storage error: {0}")]
Storage(String),
}
impl From<VtaError> for DisableWebauthnError {
fn from(value: VtaError) -> Self {
match value {
VtaError::LastServiceRefused => Self::LastServiceRefused,
other => Self::Storage(other.to_string()),
}
}
}
impl From<AppError> for DisableWebauthnError {
fn from(value: AppError) -> Self {
Self::Storage(value.to_string())
}
}
impl From<crate::operations::protocol::preconditions::ProtocolPreconditionError>
for DisableWebauthnError
{
fn from(value: crate::operations::protocol::preconditions::ProtocolPreconditionError) -> Self {
use crate::operations::protocol::preconditions::ProtocolPreconditionError as E;
match value {
E::VtaDidNotConfigured => Self::VtaDidNotConfigured,
E::VtaDidRecordMissing(s) => Self::VtaDidRecordMissing(s),
E::VtaDidLogMissing(s) => Self::VtaDidLogMissing(s),
E::EmptyLog => Self::EmptyLog,
E::Storage(s) | E::DocumentParse(s) => Self::Storage(s),
}
}
}
impl DisableMutationError for DisableWebauthnError {
fn not_present() -> Self {
Self::ServiceNotPresent
}
}
pub async fn disable_webauthn(
deps: &ServiceOpDeps<'_>,
auth: &AuthClaims,
_params: DisableWebauthnParams,
ctx: OpContext,
channel: &str,
) -> Result<DisableWebauthnResult, DisableWebauthnError> {
auth.require_super_admin()
.map_err(|e| DisableWebauthnError::Auth(e.to_string()))?;
let _guard = PROTOCOL_LOCK.lock().await;
let (state, prior_url) = check_disable_preconditions::<WebauthnService, DisableWebauthnError>(
deps.config,
deps.webvh_ks,
)
.await?;
snapshot::write(
deps.snapshot_ks,
WebauthnService::snapshot_enabled(prior_url.clone()),
)
.await
.map_err(|e| DisableWebauthnError::Storage(format!("snapshot write: {e}")))?;
let cleanup = passkey_vm_cleanup::strip_all_passkey_vms(
deps.config,
deps.keys_ks,
deps.imported_ks,
deps.contexts_ks,
deps.webvh_ks,
deps.audit_ks,
deps.seed_store,
deps.did_resolver,
deps.didcomm_bridge,
auth,
deps.webvh_auth_locks,
channel,
)
.await?;
if cleanup.failed > 0 {
warn!(
channel,
failed = cleanup.failed,
succeeded = cleanup.succeeded,
"passkey-VM cleanup had per-DID failures; surface to operator",
);
}
let patched = WebauthnService::without_service(state.current_doc);
let update_result = publish_patch::<DisableWebauthnError>(
deps,
auth,
&state.scid,
&state.vta_did,
patched,
channel,
)
.await?;
persist_webauthn_disabled(deps.config).await?;
let mut event = TelemetryEvent::new(TelemetryKind::ServicesWebauthnDisable)
.with_field("channel", JsonValue::from(channel))
.with_field(
"new_version_id",
JsonValue::from(update_result.new_version_id.clone()),
)
.with_field("prior_url", JsonValue::from(prior_url))
.with_field(
"passkey_vm_cleanup_succeeded",
JsonValue::from(cleanup.succeeded),
)
.with_field("passkey_vm_cleanup_failed", JsonValue::from(cleanup.failed));
if let Some(tag) = ctx.telemetry_triggered_by() {
event = event.with_field("triggered_by", JsonValue::from(tag));
}
let _ = deps.telemetry.record(event).await;
info!(
channel,
new_version_id = %update_result.new_version_id,
vta_did = %state.vta_did,
passkey_vm_cleanup_succeeded = cleanup.succeeded,
passkey_vm_cleanup_failed = cleanup.failed,
"WebAuthn disabled"
);
Ok(DisableWebauthnResult {
new_version_id: update_result.new_version_id,
vta_did: state.vta_did,
serverless: update_result.serverless,
cleanup,
})
}
async fn persist_webauthn_disabled(
config: &Arc<RwLock<AppConfig>>,
) -> Result<(), DisableWebauthnError> {
let (contents, path) = {
let mut cfg = config.write().await;
cfg.services.webauthn = false;
let contents = toml::to_string_pretty(&*cfg)
.map_err(|e| DisableWebauthnError::ConfigPersistence(e.to_string()))?;
let path = cfg.config_path.clone();
(contents, path)
};
std::fs::write(&path, contents)
.map_err(|e| DisableWebauthnError::ConfigPersistence(e.to_string()))?;
Ok(())
}