use systemprompt_identifiers::{PluginId, UserId};
use systemprompt_models::auth::{JwtAudience, Permission};
use crate::error::{AuthError, AuthResult};
use crate::jwt::{ValidationPolicy, decode_rs256_claims};
#[derive(Debug, Clone)]
pub struct ValidatedHookClaims {
pub plugin_id: PluginId,
pub subject: UserId,
pub scopes: Vec<Permission>,
}
#[derive(Debug)]
pub struct HookTokenValidator {
issuer: String,
}
impl HookTokenValidator {
#[must_use]
pub const fn new(issuer: String) -> Self {
Self { 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 policy = ValidationPolicy::issuer_scoped(&self.issuer, &[JwtAudience::Hook]);
let claims = decode_rs256_claims(token, &policy)?;
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.as_str()
{
return Err(AuthError::HookPluginIdMismatch {
expected: expected.to_owned(),
actual: plugin_id,
});
}
Ok(ValidatedHookClaims {
plugin_id: PluginId::new(plugin_id),
subject: UserId::new(claims.sub),
scopes: claims.scope,
})
}
}