Skip to main content

tea_session/
archive.rs

1use std::fmt;
2
3use serde::de::{MapAccess, SeqAccess, Visitor};
4use serde::{Deserialize, Deserializer, Serialize};
5use serde_json::{Map, Number, Value};
6use tea_protocol::{RecordDecodeError, RecordEnvelope, SessionId};
7use thiserror::Error;
8
9use crate::artifact::ArtifactState;
10use crate::{
11    AppendOutcome, AppendTransaction, ApprovalArtifactEntry, GrantJournalEntry, SessionReducer,
12    SessionSnapshot, SessionStore, SessionStoreError, SessionStoreErrorCode, SessionStoreFuture,
13};
14
15/// Current independent JSON archive format version.
16pub const CURRENT_ARCHIVE_FORMAT_VERSION: u32 = 1;
17/// Maximum accepted encoded archive size.
18pub const MAX_ARCHIVE_BYTES: usize = 64 * 1024 * 1024;
19/// Maximum records or side-journal entries in one archive collection.
20pub const MAX_ARCHIVE_ENTRIES: usize = 100_000;
21
22/// Validated interchange/diagnostic representation of one complete session.
23#[derive(Debug, Clone, PartialEq, Serialize)]
24#[serde(rename_all = "camelCase")]
25pub struct SessionArchive {
26    format_version: u32,
27    session_id: SessionId,
28    records: Vec<RecordEnvelope>,
29    approval_artifacts: Vec<ApprovalArtifactEntry>,
30    grant_journal: Vec<GrantJournalEntry>,
31}
32
33impl SessionArchive {
34    /// Builds and validates an archive from complete source facts.
35    ///
36    /// # Errors
37    ///
38    /// Returns an error when records, sequence, references, or typed journals
39    /// do not form one valid session.
40    pub fn new(
41        session_id: SessionId,
42        records: Vec<RecordEnvelope>,
43        approval_artifacts: Vec<ApprovalArtifactEntry>,
44        grant_journal: Vec<GrantJournalEntry>,
45    ) -> Result<Self, SessionArchiveError> {
46        if records.len() > MAX_ARCHIVE_ENTRIES
47            || approval_artifacts.len() > MAX_ARCHIVE_ENTRIES
48            || grant_journal.len() > MAX_ARCHIVE_ENTRIES
49        {
50            return Err(SessionArchiveError::OutOfBounds);
51        }
52        let archive = Self {
53            format_version: CURRENT_ARCHIVE_FORMAT_VERSION,
54            session_id,
55            records,
56            approval_artifacts,
57            grant_journal,
58        };
59        archive.validate()?;
60        Ok(archive)
61    }
62
63    /// Exports one immutable store snapshot after revalidating source facts.
64    ///
65    /// # Errors
66    ///
67    /// Returns an error if the snapshot cannot be represented as a valid
68    /// archive.
69    pub fn from_snapshot(snapshot: &SessionSnapshot) -> Result<Self, SessionArchiveError> {
70        Self::new(
71            snapshot.state().session_id(),
72            snapshot.records().to_vec(),
73            snapshot.approval_artifacts().to_vec(),
74            snapshot.grant_journal().to_vec(),
75        )
76    }
77
78    /// Decodes duplicate-key-safe untrusted JSON and validates the full archive.
79    ///
80    /// # Errors
81    ///
82    /// Returns a stable archive error for oversized input, duplicate keys,
83    /// unsupported format/protocol records, or invalid session semantics.
84    pub fn decode_json(input: &str) -> Result<Self, SessionArchiveError> {
85        if input.len() > MAX_ARCHIVE_BYTES {
86            return Err(SessionArchiveError::OutOfBounds);
87        }
88        let mut deserializer = serde_json::Deserializer::from_str(input);
89        let value = deserialize_unique_value(&mut deserializer)
90            .map_err(|error| SessionArchiveError::Malformed(error.to_string()))?;
91        deserializer
92            .end()
93            .map_err(|error| SessionArchiveError::Malformed(error.to_string()))?;
94        Self::decode_value(value)
95    }
96
97    /// Imports this complete archive into a store as one create transaction.
98    ///
99    /// Destination stores reject an existing session and validate all source
100    /// facts before making any data visible.
101    pub fn import_into(self, store: &dyn SessionStore) -> SessionStoreFuture<'_, AppendOutcome> {
102        Box::pin(async move {
103            self.validate().map_err(SessionStoreError::from)?;
104            let transaction = AppendTransaction::new(self.session_id, None, self.records)
105                .with_expected_journal_revision(0)
106                .with_approval_artifacts(self.approval_artifacts)
107                .with_grant_entries(self.grant_journal);
108            store.append(transaction).await
109        })
110    }
111
112    /// Returns archive format version.
113    #[must_use]
114    pub const fn format_version(&self) -> u32 {
115        self.format_version
116    }
117
118    /// Returns archived session identity.
119    #[must_use]
120    pub const fn session_id(&self) -> SessionId {
121        self.session_id
122    }
123
124    /// Returns canonical records in authoritative order.
125    #[must_use]
126    pub fn records(&self) -> &[RecordEnvelope] {
127        &self.records
128    }
129
130    /// Returns rich approval journal.
131    #[must_use]
132    pub fn approval_artifacts(&self) -> &[ApprovalArtifactEntry] {
133        &self.approval_artifacts
134    }
135
136    /// Returns append-only grant journal.
137    #[must_use]
138    pub fn grant_journal(&self) -> &[GrantJournalEntry] {
139        &self.grant_journal
140    }
141
142    fn decode_value(value: Value) -> Result<Self, SessionArchiveError> {
143        let raw: RawSessionArchive = serde_json::from_value(value)
144            .map_err(|error| SessionArchiveError::Malformed(error.to_string()))?;
145        if raw.format_version != CURRENT_ARCHIVE_FORMAT_VERSION {
146            return Err(SessionArchiveError::UnsupportedFormatVersion(
147                raw.format_version,
148            ));
149        }
150        if raw.records.len() > MAX_ARCHIVE_ENTRIES
151            || raw.approval_artifacts.len() > MAX_ARCHIVE_ENTRIES
152            || raw.grant_journal.len() > MAX_ARCHIVE_ENTRIES
153        {
154            return Err(SessionArchiveError::OutOfBounds);
155        }
156        let records = raw
157            .records
158            .into_iter()
159            .map(RecordEnvelope::decode_value)
160            .collect::<Result<Vec<_>, _>>()?;
161        Self::new(
162            raw.session_id,
163            records,
164            raw.approval_artifacts,
165            raw.grant_journal,
166        )
167    }
168
169    fn validate(&self) -> Result<(), SessionArchiveError> {
170        if self.format_version != CURRENT_ARCHIVE_FORMAT_VERSION {
171            return Err(SessionArchiveError::UnsupportedFormatVersion(
172                self.format_version,
173            ));
174        }
175        if self
176            .records
177            .iter()
178            .any(|record| record.session_id() != self.session_id)
179        {
180            return Err(SessionArchiveError::SessionMismatch);
181        }
182        SessionReducer::replay(self.records.clone())?;
183        ArtifactState::default().apply(
184            self.session_id,
185            &self.records,
186            &self.records,
187            &self.approval_artifacts,
188            &self.grant_journal,
189        )?;
190        Ok(())
191    }
192}
193
194#[derive(Deserialize)]
195#[serde(rename_all = "camelCase")]
196struct RawSessionArchive {
197    format_version: u32,
198    session_id: SessionId,
199    records: Vec<Value>,
200    #[serde(default)]
201    approval_artifacts: Vec<ApprovalArtifactEntry>,
202    #[serde(default)]
203    grant_journal: Vec<GrantJournalEntry>,
204}
205
206/// Error decoding or validating a session interchange archive.
207#[derive(Debug, Error)]
208pub enum SessionArchiveError {
209    /// Independent archive format is unsupported.
210    #[error("unsupported session archive format version: {0}")]
211    UnsupportedFormatVersion(u32),
212    /// Encoded bytes or collection counts exceed archive bounds.
213    #[error("session archive exceeds supported bounds")]
214    OutOfBounds,
215    /// JSON structure is malformed or contains duplicate keys.
216    #[error("malformed session archive: {0}")]
217    Malformed(String),
218    /// One canonical record belongs to another session.
219    #[error("session archive contains another session")]
220    SessionMismatch,
221    /// Canonical record decode failed.
222    #[error("session archive record is invalid: {0}")]
223    Record(#[from] RecordDecodeError),
224    /// Canonical replay failed.
225    #[error("session archive replay failed: {0}")]
226    Replay(#[from] crate::SessionReplayError),
227    /// Typed approval/grant journal validation failed.
228    #[error("session archive policy journal failed: {0}")]
229    Artifact(#[from] crate::ArtifactValidationError),
230}
231
232impl SessionArchiveError {
233    /// Returns stable storage-facing classification.
234    #[must_use]
235    pub const fn store_code(&self) -> SessionStoreErrorCode {
236        match self {
237            Self::UnsupportedFormatVersion(_)
238            | Self::Record(
239                RecordDecodeError::UnsupportedVersion { .. }
240                | RecordDecodeError::UnsupportedType { .. },
241            ) => SessionStoreErrorCode::UnsupportedSchemaVersion,
242            Self::Replay(error) => error.store_code(),
243            Self::Artifact(error) => error.store_code(),
244            Self::OutOfBounds | Self::Malformed(_) | Self::SessionMismatch | Self::Record(_) => {
245                SessionStoreErrorCode::InvalidRecord
246            }
247        }
248    }
249}
250
251impl From<SessionArchiveError> for SessionStoreError {
252    fn from(error: SessionArchiveError) -> Self {
253        Self::new(error.store_code(), error.to_string())
254    }
255}
256
257fn deserialize_unique_value<'de, D>(deserializer: D) -> Result<Value, D::Error>
258where
259    D: Deserializer<'de>,
260{
261    deserializer.deserialize_any(UniqueValueVisitor)
262}
263
264struct UniqueValueVisitor;
265
266impl<'de> Visitor<'de> for UniqueValueVisitor {
267    type Value = Value;
268
269    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
270        formatter.write_str("a JSON value without duplicate object keys")
271    }
272
273    fn visit_bool<E>(self, value: bool) -> Result<Self::Value, E> {
274        Ok(Value::Bool(value))
275    }
276
277    fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E> {
278        Ok(Value::Number(Number::from(value)))
279    }
280
281    fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E> {
282        Ok(Value::Number(Number::from(value)))
283    }
284
285    fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
286    where
287        E: serde::de::Error,
288    {
289        Number::from_f64(value)
290            .map(Value::Number)
291            .ok_or_else(|| E::custom("non-finite JSON number"))
292    }
293
294    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E> {
295        Ok(Value::String(value.to_owned()))
296    }
297
298    fn visit_string<E>(self, value: String) -> Result<Self::Value, E> {
299        Ok(Value::String(value))
300    }
301
302    fn visit_none<E>(self) -> Result<Self::Value, E> {
303        Ok(Value::Null)
304    }
305
306    fn visit_unit<E>(self) -> Result<Self::Value, E> {
307        Ok(Value::Null)
308    }
309
310    fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
311    where
312        D: Deserializer<'de>,
313    {
314        deserialize_unique_value(deserializer)
315    }
316
317    fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
318    where
319        A: SeqAccess<'de>,
320    {
321        let mut values = Vec::with_capacity(sequence.size_hint().unwrap_or(0));
322        while let Some(value) = sequence.next_element_seed(UniqueValueSeed)? {
323            values.push(value);
324        }
325        Ok(Value::Array(values))
326    }
327
328    fn visit_map<A>(self, mut object: A) -> Result<Self::Value, A::Error>
329    where
330        A: MapAccess<'de>,
331    {
332        let mut values = Map::new();
333        while let Some(key) = object.next_key::<String>()? {
334            if values.contains_key(&key) {
335                return Err(serde::de::Error::custom(format_args!(
336                    "duplicate JSON object key: {key}"
337                )));
338            }
339            let value = object.next_value_seed(UniqueValueSeed)?;
340            values.insert(key, value);
341        }
342        Ok(Value::Object(values))
343    }
344}
345
346struct UniqueValueSeed;
347
348impl<'de> serde::de::DeserializeSeed<'de> for UniqueValueSeed {
349    type Value = Value;
350
351    fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
352    where
353        D: Deserializer<'de>,
354    {
355        deserialize_unique_value(deserializer)
356    }
357}