systemprompt_api/routes/gateway/
bridge_error.rs1use axum::extract::rejection::JsonRejection;
13use axum::http::HeaderMap;
14use systemprompt_identifiers::{JwtToken, ManagedResourceId, UserId};
15use systemprompt_marketplace::managed::ManagedError;
16use systemprompt_models::api::ApiError;
17use systemprompt_models::bridge::host::HostKind;
18use systemprompt_models::execution::ContextExtractionError;
19use systemprompt_oauth::OauthError;
20use systemprompt_traits::{AuthUser, BoxedSource};
21use systemprompt_users::UserError;
22
23use super::messages::extract_credential;
24use crate::error::ApiHttpError;
25use crate::services::middleware::JwtContextExtractor;
26use crate::services::middleware::jwt::JwtUserContext;
27
28#[derive(Debug, thiserror::Error)]
29pub enum BridgeError {
30 #[error("Missing Authorization or x-api-key credential")]
31 MissingCredential,
32 #[error("Bridge credential rejected")]
33 CredentialRejected(#[source] ContextExtractionError),
34 #[error("heartbeat session_id must match the authenticated session")]
35 SessionMismatch,
36 #[error("invalid request body: {0}")]
37 InvalidBody(#[source] JsonRejection),
38 #[error("unknown API surface: {0}")]
39 UnknownSurface(String),
40 #[error("host '{0}' is disabled on this installation")]
41 HostDisabled(HostKind),
42 #[error("Gateway not enabled")]
43 GatewayDisabled,
44 #[error("User not found: {0}")]
45 UserNotFound(UserId),
46 #[error("Invalid path")]
47 InvalidPath,
48 #[error("Plugin not found")]
49 PluginNotFound,
50 #[error("File not found")]
51 FileNotFound,
52 #[error("{0}")]
53 DeviceForeignUser(String),
54 #[error("{0}")]
55 DeviceRejected(String),
56 #[error("manifest: catalogue grant not recorded for {resource}")]
57 CatalogGrant {
58 resource: ManagedResourceId,
59 #[source]
60 source: ManagedError,
61 },
62 #[error("{context}")]
63 Unavailable {
64 context: &'static str,
65 #[source]
66 source: BoxedSource,
67 },
68 #[error("{context}")]
69 Internal {
70 context: &'static str,
71 #[source]
72 source: BoxedSource,
73 },
74 #[error(transparent)]
75 Users(#[from] UserError),
76 #[error(transparent)]
77 Oauth(#[from] OauthError),
78}
79
80impl BridgeError {
81 pub fn internal(context: &'static str, source: impl Into<BoxedSource>) -> Self {
82 Self::Internal {
83 context,
84 source: source.into(),
85 }
86 }
87
88 pub fn unavailable(context: &'static str, source: impl Into<BoxedSource>) -> Self {
89 Self::Unavailable {
90 context,
91 source: source.into(),
92 }
93 }
94}
95
96impl From<BridgeError> for ApiHttpError {
97 fn from(err: BridgeError) -> Self {
98 let message = err.to_string();
99 let api = match err {
100 BridgeError::MissingCredential => {
101 ApiError::unauthorized(message).with_error_key("missing_credential")
102 },
103 BridgeError::CredentialRejected(source) => ApiError::unauthorized(message)
104 .with_error_key("invalid_credential")
105 .with_source(source),
106 BridgeError::SessionMismatch => {
107 ApiError::unauthorized(message).with_error_key("session_mismatch")
108 },
109 BridgeError::InvalidBody(_) => {
110 ApiError::bad_request(message).with_error_key("invalid_body")
111 },
112 BridgeError::UnknownSurface(_) => {
113 ApiError::bad_request(message).with_error_key("unknown_api_surface")
114 },
115 BridgeError::HostDisabled(_) => {
116 ApiError::validation_error(message, Vec::new()).with_error_key("host_disabled")
117 },
118 BridgeError::GatewayDisabled => {
119 ApiError::not_found(message).with_error_key("gateway_disabled")
120 },
121 BridgeError::UserNotFound(_)
122 | BridgeError::PluginNotFound
123 | BridgeError::FileNotFound => ApiError::not_found(message),
124 BridgeError::InvalidPath | BridgeError::DeviceRejected(_) => {
125 ApiError::bad_request(message)
126 },
127 BridgeError::DeviceForeignUser(_) => {
128 ApiError::conflict(message).with_error_key("device_fingerprint_foreign_user")
129 },
130 err @ BridgeError::CatalogGrant { .. } => {
131 ApiError::internal("manifest: catalogue grant not recorded", err)
132 },
133 BridgeError::Unavailable { context, source } => {
134 ApiError::service_unavailable(context).with_source(source)
135 },
136 BridgeError::Internal { context, source } => ApiError::internal(context, source),
137 BridgeError::Users(inner) => return Self::from(inner),
138 BridgeError::Oauth(inner) => return Self::from(inner),
139 };
140 Self::from(api)
141 }
142}
143
144pub async fn authenticate_bridge(
145 jwt_extractor: &JwtContextExtractor,
146 headers: &HeaderMap,
147) -> Result<(JwtUserContext, AuthUser), BridgeError> {
148 let credential = extract_credential(headers).ok_or(BridgeError::MissingCredential)?;
149 jwt_extractor
150 .decode_for_gateway(&JwtToken::new(credential))
151 .await
152 .map_err(BridgeError::CredentialRejected)
153}