Something went wrong. Try again.
A downstream fork of Tranquil PDS for use in margin.cafe
Something went wrong. Try again.
40 kB · 899 lines
Rust
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900use axum::{ Json, http::{HeaderValue, StatusCode}, response::{IntoResponse, Response},};use serde::Serialize;use std::borrow::Cow;
#[derive(Debug, Serialize)]struct ErrorBody<'a> { error: Cow<'a, str>, message: String,}
#[derive(Debug)]pub enum ApiError { InternalError(Option<String>), RepoCorruption, AuthenticationRequired, AuthenticationFailed(Option<String>), InvalidRequest(String), InvalidToken(Option<String>), ExpiredToken(Option<String>), OAuthExpiredToken(Option<String>), UseDpopNonce(String), InvalidDpopProof(String), TokenRequired, AccountDeactivated, AccountTakedown, AccountNotFound, RepoNotFound(Option<String>), RepoTakendown, RepoDeactivated, RecordNotFound, BlobNotFound(Option<String>), InvalidHandle(Option<String>), HandleNotAvailable(Option<String>), HandleTaken, InvalidEmail, EmailTaken, InvalidInviteCode, DuplicateCreate, DuplicateAppPassword, AppPasswordNotFound, SessionNotFound, InvalidSwap(Option<String>), InvalidPassword(String), InvalidRepo(String), AccountMigrated, AccountNotVerified, InvalidCollection, InvalidRecord(String), Forbidden, AdminRequired, InsufficientScope(Option<String>), InvitesDisabled, RateLimitExceeded(Option<String>), RateLimitExceededWithStatus { message: Option<String>, kind: crate::state::RateLimitKind, status: crate::cache::RateLimitStatus, }, PayloadTooLarge(String), TotpAlreadyEnabled, TotpNotEnabled, InvalidCode(Option<String>), IdentifierMismatch, NoPasskeys, NoChallengeInProgress, InvalidCredential, PasskeyCounterAnomaly, NoRegistrationInProgress, RegistrationFailed, PasskeyNotFound, InvalidId, InvalidScopes(String), ControllerNotFound, InvalidDelegation(String), DelegationNotFound, InviteCodeRequired, RepoNotReady, DeviceNotFound, NoEmail, MfaVerificationRequired, AuthorizationError(String), InvalidDid(String), InvalidSigningKey, SetupExpired, InvalidAccount, InvalidRecoveryLink, RecoveryLinkExpired, MissingEmail, MissingDiscordId, MissingTelegramUsername, MissingSignalNumber, InvalidVerificationChannel, SelfHostedDidWebDisabled, AccountAlreadyExists, HandleNotFound, SubjectNotFound, NotFoundMsg(String), ServiceUnavailable(Option<String>), UpstreamErrorMsg(String), DatabaseError, UpstreamFailure, UpstreamTimeout, UpstreamUnavailable(String), UpstreamError { status: StatusCode, error: Option<String>, message: Option<String>, }, SsoProviderNotFound, SsoProviderNotEnabled, SsoInvalidAction, SsoNotAuthenticated, SsoSessionExpired, SsoAlreadyLinked, SsoLinkNotFound, AuthFactorTokenRequired, LegacyLoginBlocked, ReauthRequired { methods: Vec<String>, }, MfaVerificationRequiredWithMethods { methods: Vec<String>, },}
const MST_NODE_MISSING_MARKER: &str = "MST node not found";
impl ApiError { pub fn is_repo_corruption(&self) -> bool { matches!(self, Self::RepoCorruption) }
pub fn detail_is_repo_corruption(detail: &str) -> bool { detail.contains(tranquil_store::blockstore::BLOCK_CORRUPTION_MARKER) || detail.contains(MST_NODE_MISSING_MARKER) }
pub fn from_mst_error(context: &str, e: &jacquard_repo::error::RepoError) -> Self { let detail = format!("{e:#}"); if Self::detail_is_repo_corruption(&detail) { tracing::warn!("{context}: repairable MST damage: {detail}"); Self::RepoCorruption } else { tracing::error!("{context}: {detail}"); Self::InternalError(None) } }
fn status_code(&self) -> StatusCode { match self { Self::InternalError(_) | Self::RepoCorruption | Self::DatabaseError => { StatusCode::INTERNAL_SERVER_ERROR } Self::UpstreamFailure | Self::UpstreamUnavailable(_) | Self::UpstreamErrorMsg(_) => { StatusCode::BAD_GATEWAY } Self::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE, Self::UpstreamTimeout => StatusCode::GATEWAY_TIMEOUT, Self::UpstreamError { status, .. } => *status, Self::AuthenticationRequired | Self::AuthenticationFailed(_) | Self::AccountDeactivated | Self::AccountTakedown | Self::InvalidPassword(_) | Self::InvalidToken(_) | Self::PasskeyCounterAnomaly | Self::OAuthExpiredToken(_) | Self::UseDpopNonce(_) | Self::InvalidDpopProof(_) | Self::ReauthRequired { .. } => StatusCode::UNAUTHORIZED, Self::InvalidCode(_) => StatusCode::BAD_REQUEST, Self::ExpiredToken(_) => StatusCode::BAD_REQUEST, Self::Forbidden | Self::AdminRequired | Self::InsufficientScope(_) | Self::InvitesDisabled | Self::InvalidRepo(_) | Self::AccountMigrated | Self::AccountNotVerified | Self::MfaVerificationRequired | Self::MfaVerificationRequiredWithMethods { .. } | Self::AuthorizationError(_) => StatusCode::FORBIDDEN, Self::RateLimitExceeded(_) | Self::RateLimitExceededWithStatus { .. } => { StatusCode::TOO_MANY_REQUESTS } Self::PayloadTooLarge(_) => StatusCode::PAYLOAD_TOO_LARGE, Self::AccountNotFound | Self::RecordNotFound | Self::AppPasswordNotFound | Self::SessionNotFound | Self::DeviceNotFound | Self::ControllerNotFound | Self::DelegationNotFound | Self::InvalidRecoveryLink | Self::HandleNotFound | Self::SubjectNotFound | Self::BlobNotFound(_) | Self::NotFoundMsg(_) => StatusCode::NOT_FOUND, Self::RepoTakendown | Self::RepoDeactivated | Self::RepoNotFound(_) => { StatusCode::BAD_REQUEST } Self::TotpAlreadyEnabled => StatusCode::CONFLICT, Self::InvalidSwap(_) => StatusCode::BAD_REQUEST, Self::InvalidRequest(_) | Self::InvalidHandle(_) | Self::HandleNotAvailable(_) | Self::HandleTaken | Self::InvalidEmail | Self::EmailTaken | Self::InvalidInviteCode | Self::DuplicateCreate | Self::DuplicateAppPassword | Self::InvalidCollection | Self::InvalidRecord(_) | Self::TotpNotEnabled | Self::IdentifierMismatch | Self::NoPasskeys | Self::NoChallengeInProgress | Self::InvalidCredential | Self::NoEmail | Self::NoRegistrationInProgress | Self::RegistrationFailed | Self::InvalidId | Self::InvalidScopes(_) | Self::InvalidDelegation(_) | Self::InviteCodeRequired | Self::RepoNotReady | Self::InvalidDid(_) | Self::InvalidSigningKey | Self::SetupExpired | Self::InvalidAccount | Self::RecoveryLinkExpired | Self::MissingEmail | Self::MissingDiscordId | Self::MissingTelegramUsername | Self::MissingSignalNumber | Self::InvalidVerificationChannel | Self::SelfHostedDidWebDisabled | Self::AccountAlreadyExists | Self::TokenRequired | Self::SsoProviderNotFound | Self::SsoProviderNotEnabled | Self::SsoInvalidAction | Self::SsoNotAuthenticated | Self::SsoSessionExpired | Self::SsoAlreadyLinked | Self::AuthFactorTokenRequired | Self::LegacyLoginBlocked => StatusCode::BAD_REQUEST, Self::PasskeyNotFound | Self::SsoLinkNotFound => StatusCode::NOT_FOUND, } } fn error_name(&self) -> Cow<'static, str> { match self { Self::InternalError(_) | Self::RepoCorruption | Self::DatabaseError => { Cow::Borrowed("InternalServerError") } Self::UpstreamFailure | Self::UpstreamUnavailable(_) | Self::UpstreamErrorMsg(_) => { Cow::Borrowed("UpstreamError") } Self::ServiceUnavailable(_) => Cow::Borrowed("ServiceUnavailable"), Self::NotFoundMsg(_) => Cow::Borrowed("NotFound"), Self::UpstreamTimeout => Cow::Borrowed("UpstreamTimeout"), Self::UpstreamError { error, .. } => { if let Some(e) = error { return Cow::Owned(e.clone()); } Cow::Borrowed("UpstreamError") } Self::AuthenticationRequired => Cow::Borrowed("AuthenticationRequired"), Self::AuthenticationFailed(_) => Cow::Borrowed("AuthenticationFailed"), Self::InvalidToken(_) => Cow::Borrowed("InvalidToken"), Self::ExpiredToken(_) | Self::OAuthExpiredToken(_) => Cow::Borrowed("ExpiredToken"), Self::UseDpopNonce(_) => Cow::Borrowed("use_dpop_nonce"), Self::InvalidDpopProof(_) => Cow::Borrowed("invalid_dpop_proof"), Self::TokenRequired => Cow::Borrowed("TokenRequired"), Self::AccountDeactivated => Cow::Borrowed("AccountDeactivated"), Self::AccountTakedown => Cow::Borrowed("AccountTakedown"), Self::Forbidden => Cow::Borrowed("Forbidden"), Self::AdminRequired => Cow::Borrowed("AdminRequired"), Self::InsufficientScope(_) => Cow::Borrowed("InsufficientScope"), Self::InvitesDisabled => Cow::Borrowed("InvitesDisabled"), Self::AccountNotFound => Cow::Borrowed("AccountNotFound"), Self::RepoNotFound(_) => Cow::Borrowed("RepoNotFound"), Self::RepoTakendown => Cow::Borrowed("RepoTakendown"), Self::RepoDeactivated => Cow::Borrowed("RepoDeactivated"), Self::RecordNotFound => Cow::Borrowed("RecordNotFound"), Self::BlobNotFound(_) => Cow::Borrowed("BlobNotFound"), Self::AppPasswordNotFound => Cow::Borrowed("AppPasswordNotFound"), Self::SessionNotFound => Cow::Borrowed("SessionNotFound"), Self::InvalidRequest(_) => Cow::Borrowed("InvalidRequest"), Self::InvalidHandle(_) => Cow::Borrowed("InvalidHandle"), Self::HandleNotAvailable(_) => Cow::Borrowed("HandleNotAvailable"), Self::HandleTaken => Cow::Borrowed("HandleTaken"), Self::InvalidEmail => Cow::Borrowed("InvalidEmail"), Self::EmailTaken => Cow::Borrowed("EmailTaken"), Self::InvalidInviteCode => Cow::Borrowed("InvalidInviteCode"), Self::DuplicateCreate => Cow::Borrowed("DuplicateCreate"), Self::DuplicateAppPassword => Cow::Borrowed("DuplicateAppPassword"), Self::InvalidSwap(_) => Cow::Borrowed("InvalidSwap"), Self::InvalidPassword(_) => Cow::Borrowed("InvalidPassword"), Self::InvalidRepo(_) => Cow::Borrowed("InvalidRepo"), Self::AccountMigrated => Cow::Borrowed("AccountMigrated"), Self::AccountNotVerified => Cow::Borrowed("AccountNotVerified"), Self::InvalidCollection => Cow::Borrowed("InvalidCollection"), Self::InvalidRecord(_) => Cow::Borrowed("InvalidRecord"), Self::TotpAlreadyEnabled => Cow::Borrowed("TotpAlreadyEnabled"), Self::TotpNotEnabled => Cow::Borrowed("TotpNotEnabled"), Self::InvalidCode(_) => Cow::Borrowed("InvalidCode"), Self::IdentifierMismatch => Cow::Borrowed("IdentifierMismatch"), Self::NoPasskeys => Cow::Borrowed("NoPasskeys"), Self::NoChallengeInProgress => Cow::Borrowed("NoChallengeInProgress"), Self::InvalidCredential => Cow::Borrowed("InvalidCredential"), Self::PasskeyCounterAnomaly => Cow::Borrowed("PasskeyCounterAnomaly"), Self::NoRegistrationInProgress => Cow::Borrowed("NoRegistrationInProgress"), Self::RegistrationFailed => Cow::Borrowed("RegistrationFailed"), Self::PasskeyNotFound => Cow::Borrowed("PasskeyNotFound"), Self::InvalidId => Cow::Borrowed("InvalidId"), Self::InvalidScopes(_) => Cow::Borrowed("InvalidScopes"), Self::ControllerNotFound => Cow::Borrowed("ControllerNotFound"), Self::InvalidDelegation(_) => Cow::Borrowed("InvalidDelegation"), Self::DelegationNotFound => Cow::Borrowed("DelegationNotFound"), Self::InviteCodeRequired => Cow::Borrowed("InviteCodeRequired"), Self::RepoNotReady => Cow::Borrowed("RepoNotReady"), Self::MfaVerificationRequired => Cow::Borrowed("MfaVerificationRequired"), Self::RateLimitExceeded(_) | Self::RateLimitExceededWithStatus { .. } => { Cow::Borrowed("RateLimitExceeded") } Self::PayloadTooLarge(_) => Cow::Borrowed("PayloadTooLarge"), Self::DeviceNotFound => Cow::Borrowed("DeviceNotFound"), Self::NoEmail => Cow::Borrowed("NoEmail"), Self::AuthorizationError(_) => Cow::Borrowed("AuthorizationError"), Self::InvalidDid(_) => Cow::Borrowed("InvalidDid"), Self::InvalidSigningKey => Cow::Borrowed("InvalidSigningKey"), Self::SetupExpired => Cow::Borrowed("SetupExpired"), Self::InvalidAccount => Cow::Borrowed("InvalidAccount"), Self::InvalidRecoveryLink => Cow::Borrowed("InvalidRecoveryLink"), Self::RecoveryLinkExpired => Cow::Borrowed("RecoveryLinkExpired"), Self::MissingEmail => Cow::Borrowed("MissingEmail"), Self::MissingDiscordId => Cow::Borrowed("MissingDiscordId"), Self::MissingTelegramUsername => Cow::Borrowed("MissingTelegramUsername"), Self::MissingSignalNumber => Cow::Borrowed("MissingSignalNumber"), Self::InvalidVerificationChannel => Cow::Borrowed("InvalidVerificationChannel"), Self::SelfHostedDidWebDisabled => Cow::Borrowed("SelfHostedDidWebDisabled"), Self::AccountAlreadyExists => Cow::Borrowed("AccountAlreadyExists"), Self::HandleNotFound => Cow::Borrowed("HandleNotFound"), Self::SubjectNotFound => Cow::Borrowed("SubjectNotFound"), Self::SsoProviderNotFound => Cow::Borrowed("SsoProviderNotFound"), Self::SsoProviderNotEnabled => Cow::Borrowed("SsoProviderNotEnabled"), Self::SsoInvalidAction => Cow::Borrowed("SsoInvalidAction"), Self::SsoNotAuthenticated => Cow::Borrowed("SsoNotAuthenticated"), Self::SsoSessionExpired => Cow::Borrowed("SsoSessionExpired"), Self::SsoAlreadyLinked => Cow::Borrowed("SsoAlreadyLinked"), Self::SsoLinkNotFound => Cow::Borrowed("SsoLinkNotFound"), Self::AuthFactorTokenRequired => Cow::Borrowed("AuthFactorTokenRequired"), Self::LegacyLoginBlocked => Cow::Borrowed("MfaRequired"), Self::ReauthRequired { .. } => Cow::Borrowed("ReauthRequired"), Self::MfaVerificationRequiredWithMethods { .. } => { Cow::Borrowed("MfaVerificationRequired") } } } fn message(&self) -> String { match self { Self::InternalError(msg) => msg .clone() .unwrap_or_else(|| "Internal Server Error".into()), Self::RepoCorruption => "Internal Server Error".into(), Self::AuthenticationFailed(msg) => msg .clone() .unwrap_or_else(|| "Authentication failed".into()), Self::InvalidToken(msg) => { msg.clone().unwrap_or_else(|| "Invalid token".into()) } Self::ExpiredToken(msg) | Self::OAuthExpiredToken(msg) => { msg.clone().unwrap_or_else(|| "Token has expired".into()) } Self::UseDpopNonce(_) => "DPoP nonce required".into(), Self::InvalidDpopProof(msg) => msg.clone(), Self::RepoNotFound(msg) => msg .clone() .unwrap_or_else(|| "Repository not found".into()), Self::BlobNotFound(msg) => { msg.clone().unwrap_or_else(|| "Blob not found".into()) } Self::InvalidHandle(msg) => { msg.clone().unwrap_or_else(|| "Invalid handle".into()) } Self::HandleNotAvailable(msg) => msg .clone() .unwrap_or_else(|| "Handle not available".into()), Self::InvalidSwap(msg) => { msg.clone().unwrap_or_else(|| "Invalid swap".into()) } Self::InsufficientScope(msg) => msg .clone() .unwrap_or_else(|| "Insufficient scope".into()), Self::InvalidCode(msg) => { msg.clone().unwrap_or_else(|| "Invalid code".into()) } Self::RateLimitExceeded(msg) => msg .clone() .unwrap_or_else(|| "Rate limit exceeded".into()), Self::RateLimitExceededWithStatus { message, .. } => message .clone() .unwrap_or_else(|| "Rate limit exceeded".into()), Self::ServiceUnavailable(msg) => msg .clone() .unwrap_or_else(|| "Service temporarily unavailable".into()), Self::InvalidRequest(msg) | Self::UpstreamUnavailable(msg) | Self::InvalidPassword(msg) | Self::InvalidRepo(msg) | Self::InvalidRecord(msg) | Self::NotFoundMsg(msg) | Self::UpstreamErrorMsg(msg) | Self::PayloadTooLarge(msg) | Self::InvalidScopes(msg) | Self::InvalidDelegation(msg) | Self::AuthorizationError(msg) | Self::InvalidDid(msg) => msg.clone(), Self::UpstreamError { message, .. } => message .clone() .unwrap_or_else(|| "Upstream error".into()), Self::DatabaseError => "Internal Server Error".into(), Self::AuthenticationRequired => "Authentication required".into(), Self::TokenRequired => "Authentication token required".into(), Self::AccountDeactivated => "Account is deactivated".into(), Self::AccountTakedown => "Account has been taken down".into(), Self::AccountNotFound => "Account not found".into(), Self::RecordNotFound => "Record not found".into(), Self::Forbidden => "Forbidden".into(), Self::InvitesDisabled => "Invite codes are disabled on this server".into(), Self::InvalidCollection => "Invalid collection".into(), Self::TotpAlreadyEnabled => "TOTP is already enabled".into(), Self::TotpNotEnabled => "TOTP is not enabled".into(), Self::DuplicateAppPassword => "An app password with this name already exists".into(), Self::AppPasswordNotFound => "App password not found".into(), Self::SessionNotFound => "Session not found".into(), Self::UpstreamFailure => "Upstream service failed".into(), Self::RepoTakendown => "Repository has been taken down".into(), Self::RepoDeactivated => "Repository is deactivated".into(), Self::AccountMigrated => { "Account has been migrated to another PDS. Repo operations are not allowed.".into() } Self::AccountNotVerified => { "You must verify at least one notification channel before creating records".into() } Self::NoPasskeys => "No passkeys registered for this account".into(), Self::NoChallengeInProgress => { "No passkey authentication in progress or challenge expired".into() } Self::InvalidCredential => "Failed to parse credential response".into(), Self::NoRegistrationInProgress => { "No registration in progress. Call startPasskeyRegistration first.".into() } Self::RegistrationFailed => "Failed to verify passkey registration".into(), Self::PasskeyNotFound => "Passkey not found".into(), Self::InvalidId => "Invalid ID format".into(), Self::ControllerNotFound => "Controller account not found".into(), Self::DelegationNotFound => { "No active delegation found for this controller".into() } Self::InviteCodeRequired => { "An invite code is required to create an account".into() } Self::RepoNotReady => "Repository not ready".into(), Self::PasskeyCounterAnomaly => { "Authentication failed: security key counter anomaly detected. This may indicate a cloned key.".into() } Self::MfaVerificationRequired => { "This sensitive operation requires MFA verification".into() } Self::DeviceNotFound => "Device not found".into(), Self::NoEmail => "Recipient has no email address".into(), Self::InvalidSigningKey => { "Signing key not found, already used, or expired".into() } Self::SetupExpired => "Setup has already been completed or expired".into(), Self::InvalidAccount => "This account is not a passkey-only account".into(), Self::InvalidRecoveryLink => "Invalid recovery link".into(), Self::RecoveryLinkExpired => "Recovery link has expired".into(), Self::MissingEmail => "Email is required when using email verification".into(), Self::MissingDiscordId => { "Discord ID is required when using Discord verification".into() } Self::MissingTelegramUsername => { "Telegram username is required when using Telegram verification".into() } Self::MissingSignalNumber => { "Signal username is required when using Signal verification".into() } Self::InvalidVerificationChannel => "Invalid verification channel".into(), Self::SelfHostedDidWebDisabled => { "Self-hosted did:web accounts are disabled on this server".into() } Self::AccountAlreadyExists => "Account already exists".into(), Self::HandleNotFound => "Unable to resolve handle".into(), Self::SubjectNotFound => "Subject not found".into(), Self::SsoProviderNotFound => "Unknown SSO provider".into(), Self::SsoProviderNotEnabled => "SSO provider is not enabled".into(), Self::SsoInvalidAction => "Action must be login, link, or register".into(), Self::SsoNotAuthenticated => { "Must be authenticated to link SSO account".into() } Self::SsoSessionExpired => "SSO session expired or invalid".into(), Self::SsoAlreadyLinked => { "This SSO account is already linked to a different user".into() } Self::SsoLinkNotFound => "Linked account not found".into(), Self::IdentifierMismatch => { "The identifier does not match the verification token".into() } Self::UpstreamTimeout => "Upstream service timed out".into(), Self::AdminRequired => "This action requires admin privileges".into(), Self::EmailTaken => "This email address is already registered".into(), Self::HandleTaken => "This handle is already taken".into(), Self::InvalidEmail => "Please provide a valid email address".into(), Self::InvalidInviteCode => "The invite code provided is invalid".into(), Self::DuplicateCreate => "Account creation failed: duplicate request".into(), Self::LegacyLoginBlocked => { "This account requires MFA. Please use an OAuth client that supports TOTP verification.".into() } Self::AuthFactorTokenRequired => { "A sign-in code has been sent to your email address".into() } Self::ReauthRequired { .. } => { "Re-authentication required for this action".into() } Self::MfaVerificationRequiredWithMethods { .. } => { "This sensitive operation requires MFA verification".into() } } } pub fn from_upstream_response(status: StatusCode, body: &[u8]) -> Self { if let Ok(parsed) = serde_json::from_slice::<serde_json::Value>(body) { let error = parsed .get("error") .and_then(|v| v.as_str()) .map(String::from); let message = parsed .get("message") .and_then(|v| v.as_str()) .map(String::from); return Self::UpstreamError { status, error, message, }; } Self::UpstreamError { status, error: None, message: None, } }}
impl IntoResponse for ApiError { fn into_response(self) -> Response { match self { Self::ReauthRequired { ref methods } => { return ( self.status_code(), Json(serde_json::json!({ "error": "ReauthRequired", "message": "Re-authentication required for this action", "reauthMethods": methods, })), ) .into_response(); } Self::MfaVerificationRequiredWithMethods { ref methods } => { return ( self.status_code(), Json(serde_json::json!({ "error": "MfaVerificationRequired", "message": "This sensitive operation requires MFA verification", "reauthMethods": methods, })), ) .into_response(); } _ => {} } let body = ErrorBody { error: self.error_name(), message: self.message(), }; let mut response = (self.status_code(), Json(body)).into_response(); match &self { Self::ExpiredToken(_) => { response.headers_mut().insert( http::header::WWW_AUTHENTICATE, HeaderValue::from_static( "Bearer error=\"invalid_token\", error_description=\"Token has expired\"", ), ); } Self::OAuthExpiredToken(_) => { response.headers_mut().insert( http::header::WWW_AUTHENTICATE, HeaderValue::from_static( "DPoP error=\"invalid_token\", error_description=\"Token has expired\"", ), ); } Self::UseDpopNonce(nonce) => { match HeaderValue::from_str(nonce) { Ok(val) => { response .headers_mut() .insert(crate::util::HEADER_DPOP_NONCE, val); } Err(err) => { tracing::error!( ?err, nonce_len = nonce.len(), "generated DPoP nonce is not a valid header value" ); } } response.headers_mut().insert( http::header::WWW_AUTHENTICATE, HeaderValue::from_static( "DPoP error=\"use_dpop_nonce\", error_description=\"Resource server requires nonce in DPoP proof\"", ), ); } Self::InvalidDpopProof(_) => { response.headers_mut().insert( http::header::WWW_AUTHENTICATE, HeaderValue::from_static( "DPoP error=\"invalid_dpop_proof\", error_description=\"Invalid DPoP proof\"", ), ); } Self::RateLimitExceededWithStatus { kind, status, .. } => { return crate::rate_limit::with_rate_limit_headers( response, kind.params(), *status, ); } _ => {} } response }}
impl From<sqlx::Error> for ApiError { fn from(e: sqlx::Error) -> Self { tracing::error!("Database error: {:?}", e); Self::DatabaseError }}
impl From<tranquil_db_traits::DbError> for ApiError { fn from(e: tranquil_db_traits::DbError) -> Self { tracing::error!("Database error: {:?}", e); Self::DatabaseError }}
impl From<crate::auth::TokenValidationError> for ApiError { fn from(e: crate::auth::TokenValidationError) -> Self { match e { crate::auth::TokenValidationError::AccountDeactivated => Self::AccountDeactivated, crate::auth::TokenValidationError::AccountTakedown => Self::AccountTakedown, crate::auth::TokenValidationError::KeyDecryptionFailed => Self::InternalError(None), crate::auth::TokenValidationError::AuthenticationFailed => { Self::AuthenticationFailed(None) } crate::auth::TokenValidationError::TokenExpired => Self::ExpiredToken(None), crate::auth::TokenValidationError::OAuthTokenExpired => { Self::OAuthExpiredToken(Some("Token has expired".to_string())) } crate::auth::TokenValidationError::InvalidToken => { Self::AuthenticationFailed(Some("Invalid token format".to_string())) } crate::auth::TokenValidationError::UseDpopNonce(nonce) => Self::UseDpopNonce(nonce), crate::auth::TokenValidationError::InvalidDpopProof(msg) => Self::InvalidDpopProof(msg), } }}
impl From<crate::auth::extractor::AuthError> for ApiError { fn from(e: crate::auth::extractor::AuthError) -> Self { match e { crate::auth::extractor::AuthError::MissingToken => Self::AuthenticationRequired, crate::auth::extractor::AuthError::InvalidFormat => { Self::AuthenticationFailed(Some("Invalid authorization header format".to_string())) } crate::auth::extractor::AuthError::AuthenticationFailed => { Self::AuthenticationFailed(None) } crate::auth::extractor::AuthError::TokenExpired => { Self::ExpiredToken(Some("Token has expired".to_string())) } crate::auth::extractor::AuthError::AccountDeactivated => Self::AccountDeactivated, crate::auth::extractor::AuthError::AccountTakedown => Self::AccountTakedown, crate::auth::extractor::AuthError::AdminRequired => Self::AdminRequired, crate::auth::extractor::AuthError::ServiceAuthNotAllowed => Self::AuthenticationFailed( Some("Service authentication not allowed for this endpoint".to_string()), ), crate::auth::extractor::AuthError::InsufficientScope(msg) => { Self::InsufficientScope(Some(msg)) } crate::auth::extractor::AuthError::OAuthExpiredToken(msg) => { Self::OAuthExpiredToken(Some(msg)) } crate::auth::extractor::AuthError::UseDpopNonce(nonce) => Self::UseDpopNonce(nonce), crate::auth::extractor::AuthError::InvalidDpopProof(msg) => Self::InvalidDpopProof(msg), } }}
impl From<crate::auth::scope_verified::ScopeVerificationError> for ApiError { fn from(e: crate::auth::scope_verified::ScopeVerificationError) -> Self { Self::InsufficientScope(Some(e.to_string())) }}
impl From<crate::handle::HandleResolutionError> for ApiError { fn from(e: crate::handle::HandleResolutionError) -> Self { match e { crate::handle::HandleResolutionError::NotFound => Self::HandleNotFound, crate::handle::HandleResolutionError::InvalidDid => { Self::InvalidHandle(Some("Invalid DID format in handle record".to_string())) } crate::handle::HandleResolutionError::DidMismatch { expected, actual } => { Self::InvalidHandle(Some(format!( "Handle DID mismatch: expected {}, got {}", expected, actual ))) } crate::handle::HandleResolutionError::DnsError(msg) => { Self::InternalError(Some(format!("DNS resolution failed: {}", msg))) } crate::handle::HandleResolutionError::HttpError(msg) => { Self::InternalError(Some(format!("Handle HTTP resolution failed: {}", msg))) } } }}
impl From<crate::auth::verification_token::VerifyError> for ApiError { fn from(e: crate::auth::verification_token::VerifyError) -> Self { use crate::auth::verification_token::VerifyError; match e { VerifyError::InvalidFormat => { Self::InvalidRequest("The verification code is invalid or malformed".to_string()) } VerifyError::UnsupportedVersion => { Self::InvalidRequest("This verification code version is not supported".to_string()) } VerifyError::Expired => Self::InvalidRequest( "The verification code has expired. Please request a new one.".to_string(), ), VerifyError::InvalidSignature => { Self::InvalidRequest("The verification code is invalid".to_string()) } VerifyError::IdentifierMismatch => Self::IdentifierMismatch, VerifyError::PurposeMismatch => { Self::InvalidRequest("Verification code purpose does not match".to_string()) } VerifyError::ChannelMismatch => { Self::InvalidRequest("Verification code channel does not match".to_string()) } } }}
impl From<crate::api::validation::HandleValidationError> for ApiError { fn from(e: crate::api::validation::HandleValidationError) -> Self { use crate::api::validation::HandleValidationError; match e { HandleValidationError::Reserved => Self::HandleNotAvailable(None), HandleValidationError::BannedWord => { Self::InvalidHandle(Some("Inappropriate language in handle".to_string())) } HandleValidationError::UnusableHandleDomain | HandleValidationError::NoHandleDomains => Self::InternalError(Some(e.to_string())), _ => Self::InvalidHandle(Some(e.to_string())), } }}
impl From<jacquard_common::types::string::AtStrError> for ApiError { fn from(e: jacquard_common::types::string::AtStrError) -> Self { Self::InvalidRequest(format!("Invalid {}: {}", e.spec, e.kind)) }}
impl From<crate::plc::PlcError> for ApiError { fn from(e: crate::plc::PlcError) -> Self { use crate::plc::PlcError; match e { PlcError::NotFound => Self::NotFoundMsg("DID not found in PLC directory".into()), PlcError::Tombstoned => Self::InvalidRequest("DID is tombstoned".into()), PlcError::Timeout => Self::UpstreamTimeout, PlcError::CircuitBreakerOpen => Self::ServiceUnavailable(Some( "PLC directory service temporarily unavailable".into(), )), PlcError::Http(err) => { tracing::error!("PLC HTTP error: {:?}", err); Self::UpstreamErrorMsg("Failed to communicate with PLC directory".into()) } PlcError::InvalidResponse(msg) => { tracing::error!("PLC invalid response: {}", msg); Self::UpstreamErrorMsg(format!("Invalid response from PLC directory: {}", msg)) } PlcError::Serialization(msg) => { tracing::error!("PLC serialization error: {}", msg); Self::InternalError(Some(format!("PLC serialization error: {}", msg))) } PlcError::Signing(msg) => { tracing::error!("PLC signing error: {}", msg); Self::InternalError(Some(format!("PLC signing error: {}", msg))) } } }}
impl From<bcrypt::BcryptError> for ApiError { fn from(e: bcrypt::BcryptError) -> Self { tracing::error!("Bcrypt error: {:?}", e); Self::InternalError(None) }}
impl From<cid::Error> for ApiError { fn from(e: cid::Error) -> Self { Self::InvalidRequest(format!("Invalid CID: {}", e)) }}
impl From<crate::circuit_breaker::CircuitBreakerError<crate::plc::PlcError>> for ApiError { fn from(e: crate::circuit_breaker::CircuitBreakerError<crate::plc::PlcError>) -> Self { use crate::circuit_breaker::CircuitBreakerError; match e { CircuitBreakerError::CircuitOpen(err) => { tracing::warn!("PLC directory circuit breaker open: {}", err); Self::ServiceUnavailable(Some( "PLC directory service temporarily unavailable".into(), )) } CircuitBreakerError::OperationFailed(plc_err) => Self::from(plc_err), } }}
impl From<crate::storage::StorageError> for ApiError { fn from(e: crate::storage::StorageError) -> Self { tracing::error!("Storage error: {:?}", e); Self::InternalError(Some("Storage operation failed".into())) }}
impl From<crate::rate_limit::UserRateLimitError> for ApiError { fn from(e: crate::rate_limit::UserRateLimitError) -> Self { let status = e.status(); if status.backend_error { return Self::InternalError(None); } Self::RateLimitExceededWithStatus { message: e.message, kind: e.kind, status, } }}
#[allow(clippy::result_large_err)]pub fn parse_did(s: &str) -> Result<tranquil_types::Did, Response> { s.parse() .map_err(|_| ApiError::InvalidDid("Invalid DID format".into()).into_response())}
#[allow(clippy::result_large_err)]pub fn parse_did_option(s: Option<&str>) -> Result<Option<tranquil_types::Did>, Response> { s.map(parse_did).transpose()}
pub trait DbResultExt<T> { fn log_db_err(self, ctx: &str) -> Result<T, ApiError>;}
impl<T, E: std::fmt::Debug> DbResultExt<T> for Result<T, E> { fn log_db_err(self, ctx: &str) -> Result<T, ApiError> { self.map_err(|e| { tracing::error!("DB error {}: {:?}", ctx, e); ApiError::DatabaseError }) }}