use serde::{Deserialize, Serialize};
use crate::{
EngineContextSourceMaterializationRequestV1, EngineContextSourceMaterializationResponseV1,
ProtocolReference, ValidationError,
};
pub const ENGINE_CONTEXT_MATERIALIZATION_V2_SCHEMA_ID: &str =
"leanctx.engine-context-materialization/v2";
pub const ENGINE_PRESENTER_REVOCATION_V1_SCHEMA_ID: &str = "leanctx.engine-presenter-revocation/v1";
pub const ENGINE_PRESENTER_CAPABILITY_MIN_ENGINE_VERSION: &str = "3.11.0";
pub const MAX_ENGINE_PRESENTER_TTL_SECONDS: u32 = 24 * 60 * 60;
pub const ENGINE_PRESENTER_CAPABILITY_PREFIX: &str = "lcp1_";
pub const ENGINE_PRESENTER_REF_PREFIX: &str = "presenter:";
const CAPABILITY_HEX_CHARS: usize = 64;
const PRESENTER_REF_HEX_CHARS: usize = 32;
fn lower_hex(text: &str, length: usize) -> bool {
text.len() == length
&& text
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
}
#[must_use]
pub fn is_engine_presenter_capability_form(text: &str) -> bool {
text.strip_prefix(ENGINE_PRESENTER_CAPABILITY_PREFIX)
.is_some_and(|hex| lower_hex(hex, CAPABILITY_HEX_CHARS))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields, rename_all = "snake_case", tag = "mode")]
pub enum EnginePresenterRequestV1 {
Issue { ttl_seconds: u32 },
Renew,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EngineContextSourceMaterializationRequestV2 {
pub schema_version: u32,
pub materialization: EngineContextSourceMaterializationRequestV1,
pub presenter: EnginePresenterRequestV1,
}
impl EngineContextSourceMaterializationRequestV2 {
pub fn validate_payload(&self) -> Result<(), ValidationError> {
if self.schema_version != 2 {
return Err(invalid());
}
match self.presenter {
EnginePresenterRequestV1::Issue { ttl_seconds }
if !(1..=MAX_ENGINE_PRESENTER_TTL_SECONDS).contains(&ttl_seconds) =>
{
Err(invalid())
}
_ => self.materialization.validate_payload(),
}
}
}
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EnginePresenterGrantV1 {
pub presenter_ref: ProtocolReference,
pub expires_at_unix: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub capability: Option<String>,
}
impl std::fmt::Debug for EnginePresenterGrantV1 {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
formatter
.debug_struct("EnginePresenterGrantV1")
.field("presenter_ref", &self.presenter_ref)
.field("expires_at_unix", &self.expires_at_unix)
.field(
"capability",
&self.capability.as_ref().map(|_| "<redacted>"),
)
.finish()
}
}
impl EnginePresenterGrantV1 {
fn validate(&self) -> Result<(), ValidationError> {
let reference_ok = self
.presenter_ref
.as_str()
.strip_prefix(ENGINE_PRESENTER_REF_PREFIX)
.is_some_and(|hex| lower_hex(hex, PRESENTER_REF_HEX_CHARS));
let capability_ok = self
.capability
.as_deref()
.is_none_or(is_engine_presenter_capability_form);
if reference_ok && capability_ok {
Ok(())
} else {
Err(invalid())
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EngineContextSourceMaterializationResponseV2 {
pub schema_version: u32,
pub materialization: EngineContextSourceMaterializationResponseV1,
pub presenter: EnginePresenterGrantV1,
}
impl EngineContextSourceMaterializationResponseV2 {
pub fn validate(&self) -> Result<(), ValidationError> {
if self.schema_version != 2 {
return Err(invalid());
}
self.presenter.validate()?;
let leaked = self
.presenter
.capability
.as_deref()
.is_some_and(|capability| self.materialization.content.contains(capability));
if leaked {
return Err(invalid());
}
self.materialization.validate()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EnginePresenterRevocationRequestV1 {
pub schema_version: u32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct EnginePresenterRevocationResponseV1 {
pub schema_version: u32,
pub presenter_ref: ProtocolReference,
pub revoked: bool,
}
fn invalid() -> ValidationError {
ValidationError::new("invalid Engine materialization v2 envelope")
}