use jsonwebtoken::{Algorithm, DecodingKey, Validation, decode};
use systemprompt_models::auth::{JwtAudience, JwtClaims, Permission};
use crate::error::{AuthError, AuthResult};
#[derive(Debug, Clone)]
pub struct ValidatedHookClaims {
pub plugin_id: String,
pub subject: String,
pub scopes: Vec<Permission>,
}
#[derive(Debug)]
pub struct HookTokenValidator {
secret: String,
issuer: String,
}
impl HookTokenValidator {
#[must_use]
pub const fn new(secret: String, issuer: String) -> Self {
Self { secret, issuer }
}
pub fn validate_govern(
&self,
token: &str,
request_plugin_id: Option<&str>,
) -> AuthResult<ValidatedHookClaims> {
self.validate(
token,
Permission::HookGovern,
"hook:govern",
request_plugin_id,
)
}
pub fn validate_track(
&self,
token: &str,
request_plugin_id: Option<&str>,
) -> AuthResult<ValidatedHookClaims> {
self.validate(
token,
Permission::HookTrack,
"hook:track",
request_plugin_id,
)
}
fn validate(
&self,
token: &str,
required_scope: Permission,
required_scope_name: &'static str,
request_plugin_id: Option<&str>,
) -> AuthResult<ValidatedHookClaims> {
let mut validation = Validation::new(Algorithm::HS256);
validation.set_issuer(&[&self.issuer]);
validation.set_audience(&[JwtAudience::Hook.as_str()]);
let token_data = decode::<JwtClaims>(
token,
&DecodingKey::from_secret(self.secret.as_bytes()),
&validation,
)
.map_err(AuthError::InvalidToken)?;
let claims = token_data.claims;
if !claims.aud.iter().any(|a| matches!(a, JwtAudience::Hook)) {
return Err(AuthError::HookAudienceMissing);
}
if !claims.scope.contains(&required_scope) {
return Err(AuthError::HookScopeMissing(required_scope_name));
}
let plugin_id = claims
.plugin_id
.clone()
.ok_or(AuthError::HookPluginIdMissing)?;
if let Some(expected) = request_plugin_id
&& expected != plugin_id
{
return Err(AuthError::HookPluginIdMismatch {
expected: expected.to_string(),
actual: plugin_id,
});
}
Ok(ValidatedHookClaims {
plugin_id,
subject: claims.sub,
scopes: claims.scope,
})
}
}