Skip to main content

systemprompt_security/
error.rs

1//! Error types raised by the security infrastructure.
2//!
3//! Public APIs in this crate return `thiserror`-derived error enums:
4//!
5//! - [`AuthError`] — request validation, JWT decoding, claim extraction.
6//! - [`JwtError`] — JWT minting (admin tokens, session tokens).
7//! - [`ManifestSigningError`] — Ed25519 signing of bridge manifests.
8//!
9//! All three implement `std::error::Error` and can be composed into larger
10//! `thiserror` enums via `#[from]`.
11//!
12//! Copyright (c) systemprompt.io — Business Source License 1.1.
13//! See <https://systemprompt.io> for licensing details.
14
15use systemprompt_identifiers::PluginId;
16use thiserror::Error;
17
18#[derive(Debug, Error)]
19pub enum AuthError {
20    #[error("missing authorization header")]
21    MissingAuthorization,
22
23    #[error("invalid JWT token: {0}")]
24    InvalidToken(#[source] jsonwebtoken::errors::Error),
25
26    #[error("missing session_id in token")]
27    MissingSessionId,
28
29    #[error("hook token: missing or non-`hook` audience")]
30    HookAudienceMissing,
31
32    #[error("hook token: required scope `{0}` not present")]
33    HookScopeMissing(&'static str),
34
35    #[error("hook token: missing `plugin_id` claim")]
36    HookPluginIdMissing,
37
38    #[error(
39        "hook token: plugin_id `{actual}` in claim does not match request plugin_id `{expected}`"
40    )]
41    HookPluginIdMismatch {
42        expected: PluginId,
43        actual: PluginId,
44    },
45
46    #[error("token has unsupported algorithm `{got}`; only RS256 is accepted")]
47    UnsupportedAlgorithm { got: String },
48
49    #[error("audience policy is empty; token decoding requires at least one expected audience")]
50    EmptyAudiencePolicy,
51
52    #[error("token is missing `kid` header")]
53    MissingKid,
54
55    #[error("token `kid` `{0}` does not match any known signing key")]
56    UnknownKid(String),
57
58    #[error("signing key lookup failed: {0}")]
59    KeyLookup(#[source] crate::keys::authority::TokenAuthorityError),
60
61    #[error("issuer `{0}` is not trusted")]
62    UntrustedIssuer(String),
63
64    #[error("JWKS fetch failed for issuer `{issuer}`: {source}")]
65    JwksFetch {
66        issuer: String,
67        #[source]
68        source: crate::keys::JwksClientError,
69    },
70
71    #[error("token `act` delegation chain exceeds maximum depth of {max} (got {depth})")]
72    ActChainTooDeep { depth: usize, max: usize },
73
74    #[error("token is missing the `scope` claim")]
75    MissingScope,
76
77    #[error("token `sub` is not a valid user id: {0}")]
78    InvalidSubject(#[source] systemprompt_identifiers::error::IdValidationError),
79
80    #[error("token `user_type` claim `{claimed}` does not match permissions (derived `{derived}`)")]
81    UserTypeMismatch {
82        claimed: systemprompt_models::auth::UserType,
83        derived: systemprompt_models::auth::UserType,
84    },
85}
86
87impl AuthError {
88    #[must_use]
89    pub fn is_issuer_mismatch(&self) -> bool {
90        matches!(
91            self,
92            Self::InvalidToken(inner)
93                if matches!(inner.kind(), jsonwebtoken::errors::ErrorKind::InvalidIssuer)
94        )
95    }
96}
97
98#[derive(Debug, Error)]
99pub enum JwtError {
100    #[error("jwt encoding failed: {0}")]
101    Encoding(#[from] jsonwebtoken::errors::Error),
102
103    #[error("jwt signing key unavailable: {0}")]
104    Signing(#[source] crate::keys::authority::TokenAuthorityError),
105}
106
107#[derive(Debug, Error)]
108pub enum ManifestSigningError {
109    #[error("manifest signing seed unavailable: {0}")]
110    SeedUnavailable(#[source] systemprompt_config::SecretsBootstrapError),
111
112    #[error("jcs canonicalize: {0}")]
113    Canonicalize(#[source] serde_json::Error),
114
115    #[error("signing key missing after initialization")]
116    KeyMissing,
117
118    #[error("invalid base64 in {field}: {source}")]
119    InvalidBase64 {
120        field: &'static str,
121        #[source]
122        source: base64::DecodeError,
123    },
124
125    #[error("invalid ed25519 public key: {0}")]
126    InvalidPublicKey(#[source] ed25519_dalek::SignatureError),
127
128    #[error("{field} decoded to {actual} bytes, expected {expected}")]
129    InvalidKeyLength {
130        field: &'static str,
131        expected: usize,
132        actual: usize,
133    },
134
135    #[error("ed25519 signature verification failed")]
136    SignatureInvalid,
137}
138
139pub type AuthResult<T> = Result<T, AuthError>;
140
141pub type JwtResult<T> = Result<T, JwtError>;
142
143pub type ManifestSigningResult<T> = Result<T, ManifestSigningError>;