Skip to main content

arkhe_forge_core/
entry.rs

1//! Entry primitive — persistent content unit.
2
3use arkhe_kernel::abi::{EntityId, Tick};
4use serde::{Deserialize, Serialize};
5
6use crate::actor::ActorId;
7use crate::brand::ShellId;
8use crate::component::BoundedString;
9use crate::pii::{AeadKind, DekId};
10use crate::space::SpaceId;
11use crate::ArkheComponent;
12
13/// Opaque handle into the runtime Entry namespace.
14#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct EntryId(EntityId);
17
18impl EntryId {
19    /// Construct an `EntryId` from a runtime-allocated `EntityId`. Callers
20    /// must hold proof (spawn event, admin scope, or test fixture) that the
21    /// id belongs to the Entry namespace — this constructor does not verify.
22    #[inline]
23    #[must_use]
24    pub fn new(id: EntityId) -> Self {
25        Self(id)
26    }
27
28    /// Underlying entity handle.
29    #[inline]
30    #[must_use]
31    pub fn get(self) -> EntityId {
32        self.0
33    }
34}
35
36/// Relay relation kind — single-level only (E-entry-4).
37#[non_exhaustive]
38#[repr(u8)]
39#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
40pub enum RelayKind {
41    /// Reblog / simple forward with no added content.
42    Plain = 0,
43    /// Quote-relay — relay plus own commentary.
44    Quote = 1,
45}
46
47/// Entry core Component — exactly one per Entry (E-entry-1).
48#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
49#[arkhe(type_code = 0x0003_0301, schema_version = 1)]
50pub struct EntryCore {
51    /// Wire-level schema version tag.
52    pub schema_version: u16,
53    /// Shell identity — immutable.
54    pub shell_id: ShellId,
55    /// Home Space.
56    pub space_id: SpaceId,
57    /// Authoring actor.
58    pub author_id: ActorId,
59    /// Parent (reply target). Immutable after creation (E-entry-7 / P5).
60    pub parent_entry: Option<EntryId>,
61    /// Relay source. Immutable after creation (E-entry-7 / P5).
62    pub relay_of: Option<EntryId>,
63    /// Relay kind — present iff `relay_of.is_some()`.
64    pub relay_kind: Option<RelayKind>,
65    /// Creation tick.
66    pub created_tick: Tick,
67}
68
69/// Entry body Component — content hash plus optional cipher metadata.
70/// `DeleteEntry` removes this Component while preserving `EntryCore` (soft
71/// delete, E-entry-5).
72#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
73#[arkhe(type_code = 0x0003_0302, schema_version = 1)]
74pub struct EntryBody {
75    /// Wire-level schema version tag.
76    pub schema_version: u16,
77    /// Optional title — bounded to 256 UTF-8 bytes.
78    pub title: Option<BoundedString<256>>,
79    /// `BLAKE3(body || user_salt || entry_nonce)`.
80    pub body_hash: [u8; 32],
81    /// Cipher metadata — present iff the body plaintext is AEAD-sealed in a
82    /// shell-side channel.
83    pub body_cipher_meta: Option<BodyCipherMeta>,
84    /// Monotone edit counter (E-entry-6).
85    pub edit_seq: u32,
86}
87
88/// Body encryption metadata — references the DEK and AEAD algorithm used.
89/// Ciphertext itself lives in the shell's side-channel (outside the WAL).
90#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
91pub struct BodyCipherMeta {
92    /// DEK handle (HSM/KMS-managed).
93    pub dek_id: DekId,
94    /// AEAD algorithm family.
95    pub aead_kind: AeadKind,
96    /// 24-byte XChaCha20-Poly1305 nonce (or 12-byte AES-GCM nonce, zero-padded).
97    pub nonce: [u8; 24],
98}
99
100/// Cached parent-chain depth (E-entry-3). 0 = root, max [`MAX_ENTRY_DEPTH`].
101#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, ArkheComponent)]
102#[arkhe(type_code = 0x0003_0303, schema_version = 1)]
103pub struct EntryParentDepth {
104    /// Wire-level schema version tag.
105    pub schema_version: u16,
106    /// Depth value.
107    pub depth: u8,
108}
109
110/// Maximum reply-chain depth (E-entry-3).
111pub const MAX_ENTRY_DEPTH: u8 = 64;
112
113#[cfg(test)]
114#[allow(clippy::unwrap_used, clippy::expect_used)]
115mod tests {
116    use super::*;
117    use crate::component::ArkheComponent;
118
119    fn ent(v: u64) -> EntityId {
120        EntityId::new(v).unwrap()
121    }
122
123    #[test]
124    fn entry_core_serde_roundtrip_postcard() {
125        let ec = EntryCore {
126            schema_version: 1,
127            shell_id: ShellId([0u8; 16]),
128            space_id: SpaceId::new(ent(1)),
129            author_id: ActorId::new(ent(2)),
130            parent_entry: None,
131            relay_of: None,
132            relay_kind: None,
133            created_tick: Tick(10),
134        };
135        let bytes = postcard::to_stdvec(&ec).unwrap();
136        let back: EntryCore = postcard::from_bytes(&bytes).unwrap();
137        assert_eq!(ec, back);
138    }
139
140    #[test]
141    fn entry_body_serde_roundtrip_with_cipher_meta() {
142        let body = EntryBody {
143            schema_version: 1,
144            title: Some(BoundedString::<256>::new("hello").unwrap()),
145            body_hash: [0xABu8; 32],
146            body_cipher_meta: Some(BodyCipherMeta {
147                dek_id: DekId([0x11u8; 16]),
148                aead_kind: AeadKind::XChaCha20Poly1305,
149                nonce: [0x22u8; 24],
150            }),
151            edit_seq: 1,
152        };
153        let bytes = postcard::to_stdvec(&body).unwrap();
154        let back: EntryBody = postcard::from_bytes(&bytes).unwrap();
155        assert_eq!(body, back);
156    }
157
158    #[test]
159    fn entry_component_type_codes() {
160        assert_eq!(EntryCore::TYPE_CODE, 0x0003_0301);
161        assert_eq!(EntryBody::TYPE_CODE, 0x0003_0302);
162        assert_eq!(EntryParentDepth::TYPE_CODE, 0x0003_0303);
163    }
164
165    #[test]
166    fn max_entry_depth_is_sixty_four() {
167        assert_eq!(MAX_ENTRY_DEPTH, 64);
168    }
169}