Skip to main content

khive_runtime/
event_store_guard.rs

1//! Attribution-enforcing decorator for token-scoped event stores.
2//!
3//! Runtime and pack callers construct the semantic portion of an event, but
4//! the authorization boundary owns its namespace and actor attribution. A
5//! caller reaching [`crate::KhiveRuntime::events`] therefore cannot select
6//! either persisted field: this decorator replaces them from the sealed
7//! [`crate::NamespaceToken`] on every append path.
8
9use std::collections::BTreeMap;
10use std::sync::Arc;
11
12use async_trait::async_trait;
13use khive_storage::event::{
14    EventGroupBy, EventPageQuery, EventPageWindow, IdempotentEventBatchResult,
15};
16use khive_storage::{
17    BatchWriteSummary, Event, EventFilter, EventStore, Page, PageRequest, StorageResult,
18};
19use uuid::Uuid;
20
21use crate::NamespaceToken;
22
23/// Runtime-resolved event attribution derived from a sealed authorization
24/// token.
25///
26/// This value is the construction helper for event writes that must share a
27/// larger SQL transaction and therefore cannot use [`crate::KhiveRuntime::events`].
28/// Its fields are private, so linked code can obtain one only from an already
29/// authorized token and cannot select a different namespace or actor.
30#[derive(Clone, Debug, Eq, PartialEq)]
31pub struct EventAttribution {
32    namespace: String,
33    actor: String,
34    operation: Option<khive_types::OperationAttribution>,
35}
36
37impl EventAttribution {
38    /// Resolve the canonical namespace and actor stamp from `token`, capturing
39    /// the current operation for explicitly deferred transactional event work.
40    pub fn from_token(token: &NamespaceToken) -> Self {
41        Self {
42            namespace: token.namespace().as_str().to_owned(),
43            actor: format!("{}:{}", token.actor().kind, token.actor().id),
44            operation: khive_storage::operation_context::current_operation_attribution(),
45        }
46    }
47
48    /// Replace both attribution fields while preserving semantic event data.
49    pub fn stamp(&self, mut event: Event) -> Event {
50        event.namespace.clone_from(&self.namespace);
51        event.actor.clone_from(&self.actor);
52        // Explicit capture carries the originating operation through writer-task
53        // closures. Reusable store decorators disable this capture below.
54        if let Some(operation) = self.operation {
55            event.op_index = Some(operation.op_index);
56            event.ref_resolution = Some(operation.ref_resolution);
57        }
58        event
59    }
60}
61
62pub(crate) struct AttributedEventStore {
63    inner: Arc<dyn EventStore>,
64    attribution: EventAttribution,
65}
66
67impl AttributedEventStore {
68    pub(crate) fn wrap(inner: Arc<dyn EventStore>, token: &NamespaceToken) -> Arc<dyn EventStore> {
69        let mut attribution = EventAttribution::from_token(token);
70        // A store may outlive the operation that obtained it. Its authority
71        // remains token-bound, but each event captures its own operation when
72        // constructed; reusing the store must not reuse an earlier position.
73        attribution.operation = None;
74        Arc::new(Self { inner, attribution })
75    }
76
77    fn attribute(&self, event: Event) -> Event {
78        self.attribution.stamp(event)
79    }
80
81    fn attribute_many(&self, events: Vec<Event>) -> Vec<Event> {
82        events
83            .into_iter()
84            .map(|event| self.attribute(event))
85            .collect()
86    }
87}
88
89#[async_trait]
90impl EventStore for AttributedEventStore {
91    async fn append_event(&self, event: Event) -> StorageResult<()> {
92        self.inner.append_event(self.attribute(event)).await
93    }
94
95    async fn append_events(&self, events: Vec<Event>) -> StorageResult<BatchWriteSummary> {
96        self.inner.append_events(self.attribute_many(events)).await
97    }
98
99    async fn get_event(&self, id: Uuid) -> StorageResult<Option<Event>> {
100        self.inner.get_event(id).await
101    }
102
103    async fn query_events(
104        &self,
105        filter: EventFilter,
106        page: PageRequest,
107    ) -> StorageResult<Page<Event>> {
108        self.inner.query_events(filter, page).await
109    }
110
111    async fn count_events(&self, filter: EventFilter) -> StorageResult<u64> {
112        self.inner.count_events(filter).await
113    }
114
115    async fn count_events_grouped(
116        &self,
117        filter: EventFilter,
118        group_by: EventGroupBy,
119    ) -> StorageResult<BTreeMap<String, u64>> {
120        self.inner.count_events_grouped(filter, group_by).await
121    }
122
123    async fn query_event_page(&self, query: EventPageQuery) -> StorageResult<EventPageWindow> {
124        self.inner.query_event_page(query).await
125    }
126
127    fn preflight_event(&self, event: &Event) -> StorageResult<()> {
128        self.inner.preflight_event(&self.attribute(event.clone()))
129    }
130
131    async fn append_events_idempotent(
132        &self,
133        events: Vec<Event>,
134    ) -> StorageResult<IdempotentEventBatchResult> {
135        self.inner
136            .append_events_idempotent(self.attribute_many(events))
137            .await
138    }
139
140    fn supports_idempotent_audit_batch(&self) -> bool {
141        self.inner.supports_idempotent_audit_batch()
142    }
143}
144
145#[cfg(test)]
146mod operation_tests {
147    use super::*;
148    use khive_storage::operation_context::scope_operation_attribution;
149    use khive_types::{EventKind, OperationAttribution, RefResolution, SubstrateKind};
150
151    fn event() -> Event {
152        Event::new(
153            "local",
154            "test.operation",
155            EventKind::Audit,
156            SubstrateKind::Event,
157            "fixture",
158        )
159    }
160
161    #[tokio::test]
162    async fn reusable_store_does_not_reuse_operation_but_explicit_capture_survives_defer() {
163        let runtime = crate::KhiveRuntime::memory().unwrap();
164        let token = runtime.authorize(crate::Namespace::local()).unwrap();
165        let operation = OperationAttribution {
166            op_index: 2,
167            ref_resolution: RefResolution::Resolved,
168        };
169        let (store, captured) = scope_operation_attribution(operation, async {
170            (
171                runtime.events(&token).unwrap(),
172                EventAttribution::from_token(&token),
173            )
174        })
175        .await;
176
177        let outside = event();
178        store.append_event(outside.clone()).await.unwrap();
179        let stored = store.get_event(outside.id).await.unwrap().unwrap();
180        assert_eq!((stored.op_index, stored.ref_resolution), (None, None));
181
182        let deferred = tokio::spawn(async move { captured.stamp(event()) })
183            .await
184            .unwrap();
185        assert_eq!(
186            (deferred.op_index, deferred.ref_resolution),
187            (Some(2), Some(RefResolution::Resolved))
188        );
189        store.append_event(deferred.clone()).await.unwrap();
190        let stored = store.get_event(deferred.id).await.unwrap().unwrap();
191        assert_eq!(
192            (stored.op_index, stored.ref_resolution),
193            (Some(2), Some(RefResolution::Resolved))
194        );
195
196        let next = scope_operation_attribution(
197            OperationAttribution {
198                op_index: 7,
199                ref_resolution: RefResolution::Literal,
200            },
201            async { event() },
202        )
203        .await;
204        store.append_event(next.clone()).await.unwrap();
205        let stored = store.get_event(next.id).await.unwrap().unwrap();
206        assert_eq!(
207            (stored.op_index, stored.ref_resolution),
208            (Some(7), Some(RefResolution::Literal))
209        );
210    }
211}