Skip to main content

hessra_cap_engine/
context.rs

1//! Context token wrapper for the capability engine.
2//!
3//! Delegates to `hessra-context-token` for all token operations,
4//! wrapping with engine types (ObjectId, ExposureLabel, SessionConfig, EngineError).
5
6use hessra_token_core::{KeyPair, PublicKey, TokenTimeConfig};
7
8use crate::error::EngineError;
9use crate::types::{ExposureLabel, ObjectId, SessionConfig};
10
11/// A context token tracking data exposure for an object.
12///
13/// Context tokens are append-only: exposure labels can be added but never removed.
14/// The token is a base64-encoded Biscuit with an authority block identifying
15/// the session and subsequent blocks recording exposure labels.
16#[derive(Debug, Clone)]
17pub struct ContextToken {
18    /// The base64-encoded Biscuit token.
19    token: String,
20    /// Cached exposure labels extracted from the token (kept in sync).
21    exposure_labels: Vec<ExposureLabel>,
22}
23
24impl ContextToken {
25    /// Create a ContextToken from a raw base64 token string and known exposure labels.
26    pub(crate) fn new(token: String, exposure_labels: Vec<ExposureLabel>) -> Self {
27        Self {
28            token,
29            exposure_labels,
30        }
31    }
32
33    /// Get the base64-encoded token string.
34    pub fn token(&self) -> &str {
35        &self.token
36    }
37
38    /// Get the current exposure labels.
39    pub fn exposure_labels(&self) -> &[ExposureLabel] {
40        &self.exposure_labels
41    }
42
43    /// Check if this context has a specific exposure label.
44    pub fn has_exposure(&self, label: &ExposureLabel) -> bool {
45        self.exposure_labels.contains(label)
46    }
47
48    /// Check if this context has any exposure labels.
49    pub fn is_exposed(&self) -> bool {
50        !self.exposure_labels.is_empty()
51    }
52}
53
54/// Builder for creating Hessra context tokens.
55///
56/// Wraps `hessra_context_token::HessraContext` with engine types.
57pub struct HessraContext {
58    subject: ObjectId,
59    session_config: SessionConfig,
60}
61
62impl HessraContext {
63    /// Creates a new context token builder.
64    pub fn new(subject: ObjectId, session_config: SessionConfig) -> Self {
65        Self {
66            subject,
67            session_config,
68        }
69    }
70
71    /// Issues (builds and signs) the context token.
72    pub fn issue(self, keypair: &KeyPair) -> Result<ContextToken, EngineError> {
73        let time_config = TokenTimeConfig {
74            start_time: None,
75            duration: self.session_config.ttl,
76        };
77
78        let token = hessra_context_token::HessraContext::new(
79            self.subject.as_str().to_string(),
80            time_config,
81        )
82        .issue(keypair)
83        .map_err(|e| EngineError::Context(format!("failed to mint context token: {e}")))?;
84
85        Ok(ContextToken::new(token, vec![]))
86    }
87}
88
89/// Add exposure labels to a context token.
90pub fn add_exposure_block(
91    context: &ContextToken,
92    labels: &[ExposureLabel],
93    source: &ObjectId,
94    keypair: &KeyPair,
95) -> Result<ContextToken, EngineError> {
96    if labels.is_empty() {
97        return Ok(context.clone());
98    }
99
100    let label_strings: Vec<String> = labels.iter().map(|l| l.as_str().to_string()).collect();
101
102    let new_token = hessra_context_token::add_exposure(
103        context.token(),
104        keypair.public(),
105        &label_strings,
106        source.as_str().to_string(),
107    )
108    .map_err(|e| EngineError::Context(format!("failed to add exposure: {e}")))?;
109
110    // Merge labels
111    let mut all_labels = context.exposure_labels().to_vec();
112    for label in labels {
113        if !all_labels.contains(label) {
114            all_labels.push(label.clone());
115        }
116    }
117
118    Ok(ContextToken::new(new_token, all_labels))
119}
120
121/// Extract all exposure labels from a context token by re-parsing the Biscuit.
122pub fn extract_exposure_labels(
123    token: &str,
124    public_key: PublicKey,
125) -> Result<Vec<ExposureLabel>, EngineError> {
126    let labels = hessra_context_token::extract_exposure_labels(token, public_key)
127        .map_err(|e| EngineError::Context(format!("failed to extract exposure labels: {e}")))?;
128
129    Ok(labels.into_iter().map(ExposureLabel::new).collect())
130}
131
132/// Fork a context token for a sub-agent, inheriting the parent's exposure.
133pub fn fork_context(
134    parent: &ContextToken,
135    child_subject: &ObjectId,
136    session_config: SessionConfig,
137    keypair: &KeyPair,
138) -> Result<ContextToken, EngineError> {
139    let time_config = TokenTimeConfig {
140        start_time: None,
141        duration: session_config.ttl,
142    };
143
144    let child_token = hessra_context_token::fork_context(
145        parent.token(),
146        keypair.public(),
147        child_subject.as_str().to_string(),
148        time_config,
149        keypair,
150    )
151    .map_err(|e| EngineError::Context(format!("failed to fork context: {e}")))?;
152
153    // Extract exposure labels from the new child token
154    let exposure_labels =
155        hessra_context_token::extract_exposure_labels(&child_token, keypair.public())
156            .map_err(|e| EngineError::Context(format!("failed to extract child exposure: {e}")))?
157            .into_iter()
158            .map(ExposureLabel::new)
159            .collect();
160
161    Ok(ContextToken::new(child_token, exposure_labels))
162}