Skip to main content

auths_keri/
events.rs

1//! Canonical KERI event types: Inception (ICP), Rotation (ROT), Interaction (IXN).
2//!
3//! These types are the single authoritative definition of KERI events for the
4//! entire workspace. All other crates import from here.
5
6use serde::ser::SerializeMap;
7use serde::{Deserialize, Serialize, Serializer};
8use std::fmt;
9
10use crate::capability::Capability;
11use crate::types::{CesrKey, ConfigTrait, Prefix, Said, Threshold, VersionString};
12
13/// KERI protocol version prefix string.
14pub const KERI_VERSION_PREFIX: &str = "KERI10JSON";
15
16// ── KeriSequence ─────────────────────────────────────────────────────────────
17
18/// A KERI sequence number, stored internally as u64 and serialized as a hex string.
19///
20/// Sequence numbers are spec-compliant hex strings: "0", "1", "a", "ff", etc.
21///
22/// Usage:
23/// ```ignore
24/// let seq = KeriSequence::new(10);
25/// assert_eq!(seq.value(), 10);
26/// assert_eq!(serde_json::to_string(&seq).unwrap(), "\"a\"");
27/// ```
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
29pub struct KeriSequence(u128);
30
31#[cfg(feature = "schema")]
32impl schemars::JsonSchema for KeriSequence {
33    fn schema_name() -> String {
34        "KeriSequence".to_string()
35    }
36
37    fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
38        schemars::schema::SchemaObject {
39            instance_type: Some(schemars::schema::InstanceType::String.into()),
40            ..Default::default()
41        }
42        .into()
43    }
44}
45
46impl KeriSequence {
47    /// Create a new sequence number.
48    pub fn new(value: u128) -> Self {
49        Self(value)
50    }
51
52    /// Return the full u128 value.
53    pub fn value(self) -> u128 {
54        self.0
55    }
56}
57
58impl fmt::Display for KeriSequence {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        write!(f, "{:x}", self.0)
61    }
62}
63
64impl Serialize for KeriSequence {
65    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
66        serializer.serialize_str(&format!("{:x}", self.0))
67    }
68}
69
70impl<'de> Deserialize<'de> for KeriSequence {
71    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
72        let s = String::deserialize(deserializer)?;
73        let value = u128::from_str_radix(&s, 16)
74            .map_err(|_| serde::de::Error::custom(format!("invalid hex sequence: {s:?}")))?;
75        Ok(KeriSequence(value))
76    }
77}
78
79// ── Seal ─────────────────────────────────────────────────────────────────────
80
81/// KERI seal — anchors external data in an event's `a` field.
82///
83/// Variants are distinguished by field shape (untagged), not by a "type" discriminator.
84/// Per the spec, seal fields MUST appear in the specified order.
85///
86/// Usage:
87/// ```
88/// use auths_keri::Seal;
89/// let seal = Seal::digest("EDigest123");
90/// assert!(seal.digest_value().is_some());
91/// ```
92#[derive(Debug, Clone, PartialEq, Eq)]
93#[non_exhaustive]
94pub enum Seal {
95    /// Digest seal: `{"d": "<SAID>"}`
96    Digest {
97        /// SAID of the anchored data.
98        d: Said,
99    },
100    /// Source event seal: `{"s": "<hex-sn>", "d": "<SAID>"}`
101    SourceEvent {
102        /// Sequence number.
103        s: KeriSequence,
104        /// Event SAID.
105        d: Said,
106    },
107    /// Key event seal: `{"i": "<AID>", "s": "<hex-sn>", "d": "<SAID>"}`
108    KeyEvent {
109        /// AID.
110        i: Prefix,
111        /// Sequence number.
112        s: KeriSequence,
113        /// Event SAID.
114        d: Said,
115    },
116    /// Event-location seal (KERI v1.1 §7): `{"i","s","p","t","d"}`.
117    EventLocation {
118        /// AID.
119        i: Prefix,
120        /// Sequence number.
121        s: KeriSequence,
122        /// Prior event SAID.
123        p: Said,
124        /// Event type (ilk).
125        t: String,
126        /// Event SAID.
127        d: Said,
128    },
129    /// Latest establishment event seal: `{"i": "<AID>"}`
130    LatestEstablishment {
131        /// AID.
132        i: Prefix,
133    },
134    /// Merkle tree root digest seal: `{"rd": "<digest>"}` (non-spec extension).
135    #[cfg(feature = "seal-extensions")]
136    MerkleRoot {
137        /// Root digest.
138        rd: Said,
139    },
140    /// Registrar backer seal: `{"bi": "<AID>", "d": "<SAID>"}` (non-spec extension).
141    #[cfg(feature = "seal-extensions")]
142    RegistrarBacker {
143        /// Backer AID.
144        bi: Prefix,
145        /// Metadata SAID.
146        d: Said,
147    },
148}
149
150impl Seal {
151    /// Create a digest seal from a SAID.
152    pub fn digest(said: impl Into<String>) -> Self {
153        Self::Digest {
154            d: Said::new_unchecked(said.into()),
155        }
156    }
157
158    /// Create a key event seal.
159    pub fn key_event(prefix: Prefix, sequence: KeriSequence, said: Said) -> Self {
160        Self::KeyEvent {
161            i: prefix,
162            s: sequence,
163            d: said,
164        }
165    }
166
167    /// Get the digest from this seal, if it has one.
168    pub fn digest_value(&self) -> Option<&Said> {
169        match self {
170            Seal::Digest { d } => Some(d),
171            Seal::SourceEvent { d, .. } => Some(d),
172            Seal::KeyEvent { d, .. } => Some(d),
173            Seal::EventLocation { d, .. } => Some(d),
174            Seal::LatestEstablishment { .. } => None,
175            #[cfg(feature = "seal-extensions")]
176            Seal::RegistrarBacker { d, .. } => Some(d),
177            #[cfg(feature = "seal-extensions")]
178            Seal::MerkleRoot { rd } => Some(rd),
179        }
180    }
181}
182
183impl Serialize for Seal {
184    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
185        match self {
186            Seal::Digest { d } => {
187                let mut map = serializer.serialize_map(Some(1))?;
188                map.serialize_entry("d", d)?;
189                map.end()
190            }
191            Seal::SourceEvent { s, d } => {
192                let mut map = serializer.serialize_map(Some(2))?;
193                map.serialize_entry("s", s)?;
194                map.serialize_entry("d", d)?;
195                map.end()
196            }
197            Seal::KeyEvent { i, s, d } => {
198                let mut map = serializer.serialize_map(Some(3))?;
199                map.serialize_entry("i", i)?;
200                map.serialize_entry("s", s)?;
201                map.serialize_entry("d", d)?;
202                map.end()
203            }
204            Seal::EventLocation { i, s, p, t, d } => {
205                let mut map = serializer.serialize_map(Some(5))?;
206                map.serialize_entry("i", i)?;
207                map.serialize_entry("s", s)?;
208                map.serialize_entry("p", p)?;
209                map.serialize_entry("t", t)?;
210                map.serialize_entry("d", d)?;
211                map.end()
212            }
213            Seal::LatestEstablishment { i } => {
214                let mut map = serializer.serialize_map(Some(1))?;
215                map.serialize_entry("i", i)?;
216                map.end()
217            }
218            #[cfg(feature = "seal-extensions")]
219            Seal::MerkleRoot { rd } => {
220                let mut map = serializer.serialize_map(Some(1))?;
221                map.serialize_entry("rd", rd)?;
222                map.end()
223            }
224            #[cfg(feature = "seal-extensions")]
225            Seal::RegistrarBacker { bi, d } => {
226                let mut map = serializer.serialize_map(Some(2))?;
227                map.serialize_entry("bi", bi)?;
228                map.serialize_entry("d", d)?;
229                map.end()
230            }
231        }
232    }
233}
234
235impl<'de> Deserialize<'de> for Seal {
236    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
237        let map: serde_json::Map<String, serde_json::Value> =
238            serde_json::Map::deserialize(deserializer)?;
239
240        let want_str = |k: &str| -> Result<String, D::Error> {
241            map.get(k)
242                .and_then(|v| v.as_str())
243                .map(|v| v.to_string())
244                .ok_or_else(|| {
245                    serde::de::Error::custom(format!("seal field `{k}` must be a string"))
246                })
247        };
248        let want_seq =
249            |k: &str| -> Result<KeriSequence, D::Error> {
250                serde_json::from_value(map.get(k).cloned().ok_or_else(|| {
251                    serde::de::Error::custom(format!("seal field `{k}` required"))
252                })?)
253                .map_err(serde::de::Error::custom)
254            };
255
256        // Match on the EXACT sorted field set — no silent field-dropping (F-37).
257        let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
258        keys.sort_unstable();
259
260        match keys.as_slice() {
261            ["d"] => Ok(Seal::Digest {
262                d: Said::new_unchecked(want_str("d")?),
263            }),
264            ["d", "s"] => Ok(Seal::SourceEvent {
265                s: want_seq("s")?,
266                d: Said::new_unchecked(want_str("d")?),
267            }),
268            ["d", "i", "s"] => Ok(Seal::KeyEvent {
269                i: Prefix::new_unchecked(want_str("i")?),
270                s: want_seq("s")?,
271                d: Said::new_unchecked(want_str("d")?),
272            }),
273            ["d", "i", "p", "s", "t"] => Ok(Seal::EventLocation {
274                i: Prefix::new_unchecked(want_str("i")?),
275                s: want_seq("s")?,
276                p: Said::new_unchecked(want_str("p")?),
277                t: want_str("t")?,
278                d: Said::new_unchecked(want_str("d")?),
279            }),
280            ["i"] => Ok(Seal::LatestEstablishment {
281                i: Prefix::new_unchecked(want_str("i")?),
282            }),
283            #[cfg(feature = "seal-extensions")]
284            ["rd"] => Ok(Seal::MerkleRoot {
285                rd: Said::new_unchecked(want_str("rd")?),
286            }),
287            #[cfg(feature = "seal-extensions")]
288            ["bi", "d"] => Ok(Seal::RegistrarBacker {
289                bi: Prefix::new_unchecked(want_str("bi")?),
290                d: Said::new_unchecked(want_str("d")?),
291            }),
292            other => Err(serde::de::Error::custom(format!(
293                "unrecognized seal field set: {other:?}"
294            ))),
295        }
296    }
297}
298
299// ── Event Types ───────────────────────────────────────────────────────────────
300
301/// Inception event — creates a new KERI identity.
302///
303/// The inception event establishes the identifier prefix and commits
304/// to the first rotation key via the `n` (next) field.
305///
306/// Spec field order: `[v, t, d, i, s, kt, k, nt, n, bt, b, c, a]`
307#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
308#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
309pub struct IcpEvent {
310    /// Version string
311    pub v: VersionString,
312    /// SAID (Self-Addressing Identifier) — Blake3 hash of event
313    pub d: Said,
314    /// Identifier prefix (same as `d` for self-addressing inception)
315    pub i: Prefix,
316    /// Sequence number (always 0 for inception)
317    pub s: KeriSequence,
318    /// Key signing threshold (hex integer or fractional weight list)
319    pub kt: Threshold,
320    /// Current public key(s), CESR-encoded
321    pub k: Vec<CesrKey>,
322    /// Next key signing threshold
323    pub nt: Threshold,
324    /// Next key commitment(s) — Blake3 digests of next public key(s)
325    pub n: Vec<Said>,
326    /// Witness/backer threshold
327    pub bt: Threshold,
328    /// Witness/backer list (ordered AIDs)
329    #[serde(default)]
330    pub b: Vec<Prefix>,
331    /// Configuration traits (e.g., EstablishmentOnly, DoNotDelegate)
332    #[serde(default)]
333    pub c: Vec<ConfigTrait>,
334    /// Anchored seals
335    #[serde(default)]
336    pub a: Vec<Seal>,
337}
338
339/// Parameter struct for [`IcpEvent::new`]. Mirrors the existing
340/// field set 1-for-1 so call-site migration is a mechanical prefix/
341/// suffix wrap. Future wire-format additions land on `IcpEvent` via
342/// a `with_*` method, not by widening this `Init` struct — that's
343/// what makes the pattern future-proof.
344pub struct IcpEventInit {
345    /// Version string (`KERI10JSON…`); a placeholder until finalized.
346    pub v: VersionString,
347    /// Self-addressing identifier (the event SAID); blank until finalized.
348    pub d: Said,
349    /// Identifier prefix; for inception this equals the SAID (self-addressing).
350    pub i: Prefix,
351    /// Sequence number (0 for inception).
352    pub s: KeriSequence,
353    /// Signing threshold over the current keys `k`.
354    pub kt: Threshold,
355    /// Current signing keys (CESR-encoded verkeys).
356    pub k: Vec<CesrKey>,
357    /// Next-key threshold over the pre-rotation commitments `n`.
358    pub nt: Threshold,
359    /// Pre-rotation commitments (digests of the next keys).
360    pub n: Vec<Said>,
361    /// Backer (witness) threshold.
362    pub bt: Threshold,
363    /// Backer (witness) prefixes.
364    pub b: Vec<Prefix>,
365    /// Configuration traits (e.g. establishment-only, do-not-delegate).
366    pub c: Vec<ConfigTrait>,
367    /// Anchored seals.
368    pub a: Vec<Seal>,
369}
370
371impl IcpEvent {
372    /// Construct an `IcpEvent` from its required fields.
373    pub fn new(init: IcpEventInit) -> Self {
374        Self {
375            v: init.v,
376            d: init.d,
377            i: init.i,
378            s: init.s,
379            kt: init.kt,
380            k: init.k,
381            nt: init.nt,
382            n: init.n,
383            bt: init.bt,
384            b: init.b,
385            c: init.c,
386            a: init.a,
387        }
388    }
389}
390
391/// Spec field order: v, t, d, i, s, kt, k, nt, n, bt, b, c, a
392impl Serialize for IcpEvent {
393    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
394        let field_count = 13;
395        let mut map = serializer.serialize_map(Some(field_count))?;
396        map.serialize_entry("v", &self.v)?;
397        map.serialize_entry("t", "icp")?;
398        map.serialize_entry("d", &self.d)?;
399        map.serialize_entry("i", &self.i)?;
400        map.serialize_entry("s", &self.s)?;
401        map.serialize_entry("kt", &self.kt)?;
402        map.serialize_entry("k", &self.k)?;
403        map.serialize_entry("nt", &self.nt)?;
404        map.serialize_entry("n", &self.n)?;
405        map.serialize_entry("bt", &self.bt)?;
406        map.serialize_entry("b", &self.b)?;
407        map.serialize_entry("c", &self.c)?;
408        map.serialize_entry("a", &self.a)?;
409        map.end()
410    }
411}
412
413/// Rotation event — rotates to pre-committed key.
414///
415/// Spec field order: `[v, t, d, i, s, p, kt, k, nt, n, bt, br, ba, a]`
416#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
417#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
418pub struct RotEvent {
419    /// Version string
420    pub v: VersionString,
421    /// SAID of this event
422    pub d: Said,
423    /// Identifier prefix
424    pub i: Prefix,
425    /// Sequence number (increments with each event)
426    pub s: KeriSequence,
427    /// Previous event SAID (creates the hash chain)
428    pub p: Said,
429    /// Key signing threshold
430    pub kt: Threshold,
431    /// New current key(s), CESR-encoded
432    pub k: Vec<CesrKey>,
433    /// Next key signing threshold
434    pub nt: Threshold,
435    /// New next key commitment(s) — Blake3 digests
436    pub n: Vec<Said>,
437    /// Witness/backer threshold
438    pub bt: Threshold,
439    /// List of backers to remove (processed first)
440    #[serde(default)]
441    pub br: Vec<Prefix>,
442    /// List of backers to add (processed after removals)
443    #[serde(default)]
444    pub ba: Vec<Prefix>,
445    /// Configuration traits
446    #[serde(default)]
447    pub c: Vec<ConfigTrait>,
448    /// Anchored seals
449    #[serde(default)]
450    pub a: Vec<Seal>,
451}
452
453/// Parameter struct for [`RotEvent::new`]. See [`IcpEventInit`] for
454/// the pattern rationale.
455pub struct RotEventInit {
456    /// Version string (`KERI10JSON…`); a placeholder until finalized.
457    pub v: VersionString,
458    /// Self-addressing identifier (the event SAID); blank until finalized.
459    pub d: Said,
460    /// Identifier prefix of the rotating identity.
461    pub i: Prefix,
462    /// Sequence number (strictly greater than the prior event's).
463    pub s: KeriSequence,
464    /// Prior event SAID (the back-link establishing the chain).
465    pub p: Said,
466    /// Signing threshold over the new current keys `k`.
467    pub kt: Threshold,
468    /// New current signing keys (CESR-encoded verkeys) revealed by this rotation.
469    pub k: Vec<CesrKey>,
470    /// Next-key threshold over the new pre-rotation commitments `n`.
471    pub nt: Threshold,
472    /// New pre-rotation commitments (digests of the next keys).
473    pub n: Vec<Said>,
474    /// Backer (witness) threshold after applying the backer deltas.
475    pub bt: Threshold,
476    /// Backer (witness) prefixes to remove.
477    pub br: Vec<Prefix>,
478    /// Backer (witness) prefixes to add.
479    pub ba: Vec<Prefix>,
480    /// Configuration traits (e.g. `RB`/`NRB`) updated by this rotation.
481    pub c: Vec<ConfigTrait>,
482    /// Anchored seals carried in this event's `a[]`.
483    pub a: Vec<Seal>,
484}
485
486impl RotEvent {
487    /// Construct a `RotEvent` from its required fields.
488    pub fn new(init: RotEventInit) -> Self {
489        Self {
490            v: init.v,
491            d: init.d,
492            i: init.i,
493            s: init.s,
494            p: init.p,
495            kt: init.kt,
496            k: init.k,
497            nt: init.nt,
498            n: init.n,
499            bt: init.bt,
500            br: init.br,
501            ba: init.ba,
502            c: init.c,
503            a: init.a,
504        }
505    }
506}
507
508/// Spec field order: v, t, d, i, s, p, kt, k, nt, n, bt, br, ba, a
509impl Serialize for RotEvent {
510    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
511        let field_count = 14;
512        let mut map = serializer.serialize_map(Some(field_count))?;
513        map.serialize_entry("v", &self.v)?;
514        map.serialize_entry("t", "rot")?;
515        map.serialize_entry("d", &self.d)?;
516        map.serialize_entry("i", &self.i)?;
517        map.serialize_entry("s", &self.s)?;
518        map.serialize_entry("p", &self.p)?;
519        map.serialize_entry("kt", &self.kt)?;
520        map.serialize_entry("k", &self.k)?;
521        map.serialize_entry("nt", &self.nt)?;
522        map.serialize_entry("n", &self.n)?;
523        map.serialize_entry("bt", &self.bt)?;
524        map.serialize_entry("br", &self.br)?;
525        map.serialize_entry("ba", &self.ba)?;
526        map.serialize_entry("a", &self.a)?;
527        map.end()
528    }
529}
530
531/// Interaction event — anchors data without key rotation.
532///
533/// Spec field order: `[v, t, d, i, s, p, a]`
534#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
535#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
536pub struct IxnEvent {
537    /// Version string
538    pub v: VersionString,
539    /// SAID of this event
540    pub d: Said,
541    /// Identifier prefix
542    pub i: Prefix,
543    /// Sequence number
544    pub s: KeriSequence,
545    /// Previous event SAID
546    pub p: Said,
547    /// Anchored seals (the main purpose of IXN events)
548    pub a: Vec<Seal>,
549}
550
551/// Parameter struct for [`IxnEvent::new`].
552pub struct IxnEventInit {
553    pub v: VersionString,
554    pub d: Said,
555    pub i: Prefix,
556    pub s: KeriSequence,
557    pub p: Said,
558    pub a: Vec<Seal>,
559}
560
561impl IxnEvent {
562    /// Construct an `IxnEvent` from its required fields.
563    pub fn new(init: IxnEventInit) -> Self {
564        Self {
565            v: init.v,
566            d: init.d,
567            i: init.i,
568            s: init.s,
569            p: init.p,
570            a: init.a,
571        }
572    }
573}
574
575/// Spec field order: v, t, d, i, s, p, a
576impl Serialize for IxnEvent {
577    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
578        let field_count = 7;
579        let mut map = serializer.serialize_map(Some(field_count))?;
580        map.serialize_entry("v", &self.v)?;
581        map.serialize_entry("t", "ixn")?;
582        map.serialize_entry("d", &self.d)?;
583        map.serialize_entry("i", &self.i)?;
584        map.serialize_entry("s", &self.s)?;
585        map.serialize_entry("p", &self.p)?;
586        map.serialize_entry("a", &self.a)?;
587        map.end()
588    }
589}
590
591/// Unified event enum for processing any KERI event type.
592///
593/// Uses serde's tagged enum feature to deserialize based on the `t` field.
594///
595/// Usage:
596/// ```ignore
597/// let event: Event = serde_json::from_str(json)?;
598/// match event {
599///     Event::Icp(icp) => { /* inception */ }
600///     Event::Rot(rot) => { /* rotation */ }
601///     Event::Ixn(ixn) => { /* interaction */ }
602/// }
603/// ```
604/// Delegated inception event — creates a delegated KERI identity.
605///
606/// Same as ICP plus the `di` (delegator identifier prefix) field.
607/// Spec field order: `[v, t, d, i, s, kt, k, nt, n, bt, b, c, a, di]`
608#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
609#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
610pub struct DipEvent {
611    /// Version string
612    pub v: VersionString,
613    /// SAID
614    pub d: Said,
615    /// Identifier prefix (same as `d` for self-addressing)
616    pub i: Prefix,
617    /// Sequence number (always 0)
618    pub s: KeriSequence,
619    /// Key signing threshold
620    pub kt: Threshold,
621    /// Current public key(s)
622    pub k: Vec<CesrKey>,
623    /// Next key signing threshold
624    pub nt: Threshold,
625    /// Next key commitment(s)
626    pub n: Vec<Said>,
627    /// Witness/backer threshold
628    pub bt: Threshold,
629    /// Witness/backer list
630    #[serde(default)]
631    pub b: Vec<Prefix>,
632    /// Configuration traits
633    #[serde(default)]
634    pub c: Vec<ConfigTrait>,
635    /// Anchored seals
636    #[serde(default)]
637    pub a: Vec<Seal>,
638    /// Delegator identifier prefix
639    pub di: Prefix,
640    /// Delegate-side source seal (`-G` `SealSourceCouple`): a back-reference to
641    /// the delegator's anchoring event. Carried in the CESR **attachment**, never
642    /// in the event body — so it is `#[serde(skip)]` (absent from the JSON and from
643    /// SAID computation) and populated after the delegator anchors (the cooperative
644    /// double-anchor). `None` until/unless the anchoring event is known.
645    #[serde(skip)]
646    pub source_seal: Option<SourceSeal>,
647}
648
649/// Parameter struct for [`DipEvent::new`].
650pub struct DipEventInit {
651    /// Version string (KERI version + serialization + byte count).
652    pub v: VersionString,
653    /// Self-addressing identifier (SAID) of this event.
654    pub d: Said,
655    /// Identifier prefix (AID); self-addressing for a delegated AID (`i == d`).
656    pub i: Prefix,
657    /// Sequence number (0 for inception).
658    pub s: KeriSequence,
659    /// Current signing threshold.
660    pub kt: Threshold,
661    /// Current signing public keys (CESR-encoded).
662    pub k: Vec<CesrKey>,
663    /// Next (rotation) threshold.
664    pub nt: Threshold,
665    /// Next-key commitments (pre-rotation digests).
666    pub n: Vec<Said>,
667    /// Backer (witness) threshold.
668    pub bt: Threshold,
669    /// Backer (witness) list.
670    pub b: Vec<Prefix>,
671    /// Configuration traits.
672    pub c: Vec<ConfigTrait>,
673    /// Anchored seals.
674    pub a: Vec<Seal>,
675    /// Delegator prefix — the AID delegating this identifier.
676    pub di: Prefix,
677}
678
679impl DipEvent {
680    /// Construct a `DipEvent` from its required fields.
681    pub fn new(init: DipEventInit) -> Self {
682        Self {
683            v: init.v,
684            d: init.d,
685            i: init.i,
686            s: init.s,
687            kt: init.kt,
688            k: init.k,
689            nt: init.nt,
690            n: init.n,
691            bt: init.bt,
692            b: init.b,
693            c: init.c,
694            a: init.a,
695            di: init.di,
696            source_seal: None,
697        }
698    }
699}
700
701/// Spec field order: v, t, d, i, s, kt, k, nt, n, bt, b, c, a, di
702impl Serialize for DipEvent {
703    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
704        let field_count = 14;
705        let mut map = serializer.serialize_map(Some(field_count))?;
706        map.serialize_entry("v", &self.v)?;
707        map.serialize_entry("t", "dip")?;
708        map.serialize_entry("d", &self.d)?;
709        map.serialize_entry("i", &self.i)?;
710        map.serialize_entry("s", &self.s)?;
711        map.serialize_entry("kt", &self.kt)?;
712        map.serialize_entry("k", &self.k)?;
713        map.serialize_entry("nt", &self.nt)?;
714        map.serialize_entry("n", &self.n)?;
715        map.serialize_entry("bt", &self.bt)?;
716        map.serialize_entry("b", &self.b)?;
717        map.serialize_entry("c", &self.c)?;
718        map.serialize_entry("a", &self.a)?;
719        map.serialize_entry("di", &self.di)?;
720        map.end()
721    }
722}
723
724/// Delegated rotation event — rotates keys for a delegated identity.
725///
726/// Same field set as ROT but type `drt`. Validation requires checking the
727/// delegator's KEL for an anchoring seal.
728/// Spec field order: `[v, t, d, i, s, p, kt, k, nt, n, bt, br, ba, a]`
729#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
730#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
731pub struct DrtEvent {
732    /// Version string
733    pub v: VersionString,
734    /// SAID
735    pub d: Said,
736    /// Identifier prefix
737    pub i: Prefix,
738    /// Sequence number
739    pub s: KeriSequence,
740    /// Previous event SAID
741    pub p: Said,
742    /// Key signing threshold
743    pub kt: Threshold,
744    /// New current key(s)
745    pub k: Vec<CesrKey>,
746    /// Next key signing threshold
747    pub nt: Threshold,
748    /// New next key commitment(s)
749    pub n: Vec<Said>,
750    /// Witness/backer threshold
751    pub bt: Threshold,
752    /// Backers to remove
753    #[serde(default)]
754    pub br: Vec<Prefix>,
755    /// Backers to add
756    #[serde(default)]
757    pub ba: Vec<Prefix>,
758    /// Configuration traits
759    #[serde(default)]
760    pub c: Vec<ConfigTrait>,
761    /// Anchored seals
762    #[serde(default)]
763    pub a: Vec<Seal>,
764    /// Delegator identifier prefix (KERI §11).
765    pub di: Prefix,
766    /// Delegate-side source seal (`-G` `SealSourceCouple`) — see
767    /// [`DipEvent::source_seal`]. Carried in the attachment, set after the
768    /// delegator anchors this rotation.
769    #[serde(skip)]
770    pub source_seal: Option<SourceSeal>,
771}
772
773/// Parameter struct for [`DrtEvent::new`].
774pub struct DrtEventInit {
775    /// Version string (KERI version + serialization + byte count).
776    pub v: VersionString,
777    /// Self-addressing identifier (SAID) of this event.
778    pub d: Said,
779    /// Identifier prefix (AID) — the existing delegated AID (not self-addressing).
780    pub i: Prefix,
781    /// Sequence number.
782    pub s: KeriSequence,
783    /// Prior event SAID (chains to the previous event).
784    pub p: Said,
785    /// Current signing threshold.
786    pub kt: Threshold,
787    /// Current signing public keys (CESR-encoded) — the revealed pre-rotated keys.
788    pub k: Vec<CesrKey>,
789    /// Next (rotation) threshold.
790    pub nt: Threshold,
791    /// Next-key commitments (pre-rotation digests).
792    pub n: Vec<Said>,
793    /// Backer (witness) threshold.
794    pub bt: Threshold,
795    /// Backers (witnesses) to remove.
796    pub br: Vec<Prefix>,
797    /// Backers (witnesses) to add.
798    pub ba: Vec<Prefix>,
799    /// Configuration traits.
800    pub c: Vec<ConfigTrait>,
801    /// Anchored seals.
802    pub a: Vec<Seal>,
803    /// Delegator prefix — the AID delegating this identifier.
804    pub di: Prefix,
805}
806
807impl DrtEvent {
808    /// Construct a `DrtEvent` from its required fields.
809    pub fn new(init: DrtEventInit) -> Self {
810        Self {
811            v: init.v,
812            d: init.d,
813            i: init.i,
814            s: init.s,
815            p: init.p,
816            kt: init.kt,
817            k: init.k,
818            nt: init.nt,
819            n: init.n,
820            bt: init.bt,
821            br: init.br,
822            ba: init.ba,
823            c: init.c,
824            a: init.a,
825            di: init.di,
826            source_seal: None,
827        }
828    }
829}
830
831/// Spec field order (KERI §11): v, t, d, i, s, p, kt, k, nt, n, bt, br, ba, a, di
832impl Serialize for DrtEvent {
833    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
834        let field_count = 15;
835        let mut map = serializer.serialize_map(Some(field_count))?;
836        map.serialize_entry("v", &self.v)?;
837        map.serialize_entry("t", "drt")?;
838        map.serialize_entry("d", &self.d)?;
839        map.serialize_entry("i", &self.i)?;
840        map.serialize_entry("s", &self.s)?;
841        map.serialize_entry("p", &self.p)?;
842        map.serialize_entry("kt", &self.kt)?;
843        map.serialize_entry("k", &self.k)?;
844        map.serialize_entry("nt", &self.nt)?;
845        map.serialize_entry("n", &self.n)?;
846        map.serialize_entry("bt", &self.bt)?;
847        map.serialize_entry("br", &self.br)?;
848        map.serialize_entry("ba", &self.ba)?;
849        map.serialize_entry("a", &self.a)?;
850        map.serialize_entry("di", &self.di)?;
851        map.end()
852    }
853}
854
855/// Unified event enum for processing any KERI event type.
856#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
857#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
858#[serde(tag = "t")]
859pub enum Event {
860    /// Inception event
861    #[serde(rename = "icp")]
862    Icp(IcpEvent),
863    /// Rotation event
864    #[serde(rename = "rot")]
865    Rot(RotEvent),
866    /// Interaction event
867    #[serde(rename = "ixn")]
868    Ixn(IxnEvent),
869    /// Delegated inception event
870    #[serde(rename = "dip")]
871    Dip(DipEvent),
872    /// Delegated rotation event
873    #[serde(rename = "drt")]
874    Drt(DrtEvent),
875}
876
877impl Serialize for Event {
878    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
879        match self {
880            Event::Icp(e) => e.serialize(serializer),
881            Event::Rot(e) => e.serialize(serializer),
882            Event::Ixn(e) => e.serialize(serializer),
883            Event::Dip(e) => e.serialize(serializer),
884            Event::Drt(e) => e.serialize(serializer),
885        }
886    }
887}
888
889impl Event {
890    /// Get the SAID of this event.
891    pub fn said(&self) -> &Said {
892        match self {
893            Event::Icp(e) => &e.d,
894            Event::Rot(e) => &e.d,
895            Event::Ixn(e) => &e.d,
896            Event::Dip(e) => &e.d,
897            Event::Drt(e) => &e.d,
898        }
899    }
900
901    /// Get the sequence number of this event.
902    pub fn sequence(&self) -> KeriSequence {
903        match self {
904            Event::Icp(e) => e.s,
905            Event::Rot(e) => e.s,
906            Event::Ixn(e) => e.s,
907            Event::Dip(e) => e.s,
908            Event::Drt(e) => e.s,
909        }
910    }
911
912    /// Get the identifier prefix.
913    pub fn prefix(&self) -> &Prefix {
914        match self {
915            Event::Icp(e) => &e.i,
916            Event::Rot(e) => &e.i,
917            Event::Ixn(e) => &e.i,
918            Event::Dip(e) => &e.i,
919            Event::Drt(e) => &e.i,
920        }
921    }
922
923    /// Get the previous event SAID (None for inception/delegated inception).
924    pub fn previous(&self) -> Option<&Said> {
925        match self {
926            Event::Icp(_) | Event::Dip(_) => None,
927            Event::Rot(e) => Some(&e.p),
928            Event::Ixn(e) => Some(&e.p),
929            Event::Drt(e) => Some(&e.p),
930        }
931    }
932
933    /// Get the current keys (only for establishment events).
934    pub fn keys(&self) -> Option<&[CesrKey]> {
935        match self {
936            Event::Icp(e) => Some(&e.k),
937            Event::Rot(e) => Some(&e.k),
938            Event::Dip(e) => Some(&e.k),
939            Event::Drt(e) => Some(&e.k),
940            Event::Ixn(_) => None,
941        }
942    }
943
944    /// Get the next key commitments (only for establishment events).
945    pub fn next_commitments(&self) -> Option<&[Said]> {
946        match self {
947            Event::Icp(e) => Some(&e.n),
948            Event::Rot(e) => Some(&e.n),
949            Event::Dip(e) => Some(&e.n),
950            Event::Drt(e) => Some(&e.n),
951            Event::Ixn(_) => None,
952        }
953    }
954
955    /// Get the anchored seals.
956    pub fn anchors(&self) -> &[Seal] {
957        match self {
958            Event::Icp(e) => &e.a,
959            Event::Rot(e) => &e.a,
960            Event::Ixn(e) => &e.a,
961            Event::Dip(e) => &e.a,
962            Event::Drt(e) => &e.a,
963        }
964    }
965
966    /// Get the delegator AID (only for delegated inception).
967    pub fn delegator(&self) -> Option<&Prefix> {
968        match self {
969            Event::Dip(e) => Some(&e.di),
970            _ => None,
971        }
972    }
973
974    /// Check if this is an inception event (including delegated).
975    pub fn is_inception(&self) -> bool {
976        matches!(self, Event::Icp(_) | Event::Dip(_))
977    }
978
979    /// Check if this is a rotation event (including delegated).
980    pub fn is_rotation(&self) -> bool {
981        matches!(self, Event::Rot(_) | Event::Drt(_))
982    }
983
984    /// Check if this is an interaction event.
985    pub fn is_interaction(&self) -> bool {
986        matches!(self, Event::Ixn(_))
987    }
988
989    /// Check if this is a delegated event.
990    pub fn is_delegated(&self) -> bool {
991        matches!(self, Event::Dip(_) | Event::Drt(_))
992    }
993
994    /// Get the delegate-side source seal (`-G` back-reference to the delegator's
995    /// anchoring event), if this is a delegated event that has been anchored.
996    pub fn source_seal(&self) -> Option<&SourceSeal> {
997        match self {
998            Event::Dip(e) => e.source_seal.as_ref(),
999            Event::Drt(e) => e.source_seal.as_ref(),
1000            _ => None,
1001        }
1002    }
1003}
1004
1005// ── Signed Event (externalized signatures) ──────────────────────────────────
1006
1007/// A single indexed controller signature.
1008///
1009/// The `index` maps to the position in the key list (`k` field) of the
1010/// signing key. Per the CESR spec, indexed signatures carry their key
1011/// index in the derivation code.
1012///
1013/// Usage:
1014/// ```
1015/// use auths_keri::IndexedSignature;
1016/// let sig = IndexedSignature { index: 0, prior_index: None, sig: vec![0u8; 64] };
1017/// assert_eq!(sig.index, 0);
1018/// ```
1019#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1020pub struct IndexedSignature {
1021    /// Index into the new key list `k[]` (which current key signed).
1022    pub index: u32,
1023    /// For a rotation signature, the index into the prior event's next-key
1024    /// commitment list `n[]` that this signature reveals (CESR *ondex*).
1025    ///
1026    /// `Some(j)` emits a dual-index ("Big", `2A`) siger; `None` emits the
1027    /// single-index code (`A`) with ondex == index — preserving the wire
1028    /// bytes of every single-index signer (inception, symmetric rotation).
1029    #[serde(default, skip_serializing_if = "Option::is_none")]
1030    pub prior_index: Option<u32>,
1031    /// Raw signature bytes (64 bytes for Ed25519).
1032    #[serde(with = "hex::serde")]
1033    pub sig: Vec<u8>,
1034}
1035
1036/// A delegate-side source seal — the parsed form of a CESR `-G` `SealSourceCouple`.
1037///
1038/// A delegated event (`dip`/`drt`) carries one of these as an **attachment** (never
1039/// in the body) to bind itself back to the *specific* delegator event that anchored
1040/// it: `s` is that anchoring event's sequence number (CESR `Seqner`), `d` is its
1041/// SAID (CESR `Saider`). Together with the delegator-side `Seal::KeyEvent` (which
1042/// points the other way) this forms the **bilateral** delegation binding keripy
1043/// requires. Byte-aligned with keripy 1.3.4's `-G` couple.
1044#[derive(Debug, Clone, PartialEq, Eq)]
1045#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1046pub struct SourceSeal {
1047    /// Sequence number of the delegator's anchoring event.
1048    pub s: KeriSequence,
1049    /// SAID of the delegator's anchoring event.
1050    pub d: Said,
1051}
1052
1053/// Delegator-anchored scope/expiry for a delegated agent (Epic E.7).
1054///
1055/// Carried as a `Seal::Digest` in the **delegator's** anchoring `ixn` — authority
1056/// comes from the party that controls the delegator key, never agent-self-asserted.
1057/// The digest value is a structured marker string (not a SAID) namespaced under
1058/// `agentscope:`, so it never collides with a real digest, a revocation (bare
1059/// prefix), or an `agent:` role marker. Advisory authorization; the principled
1060/// upgrade is a targeted ACDC (Epic F).
1061#[derive(Debug, Clone, PartialEq, Eq, Default)]
1062#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
1063pub struct AgentScope {
1064    /// Capabilities granted to the agent (empty = unrestricted). Capability strings
1065    /// may contain `:` (e.g. `repo:foo`) but not `,` (the list separator).
1066    pub capabilities: Vec<Capability>,
1067    /// Expiry as Unix epoch seconds; `None` = never expires.
1068    pub expires_at: Option<i64>,
1069}
1070
1071/// Marker prefix for an agent-scope `Seal::Digest` value.
1072const AGENT_SCOPE_MARKER: &str = "agentscope:";
1073
1074/// Encode an agent-scope seal value: `agentscope:{prefix}:{expires_or_0}:{caps_csv}`.
1075///
1076/// Args:
1077/// * `agent_prefix`: The agent's KEL prefix the scope applies to.
1078/// * `scope`: The granted capabilities + optional expiry.
1079///
1080/// Usage:
1081/// ```
1082/// use auths_keri::{AgentScope, Capability, encode_agent_scope};
1083/// let s = AgentScope {
1084///     capabilities: vec![Capability::sign_commit()],
1085///     expires_at: Some(99),
1086/// };
1087/// assert_eq!(encode_agent_scope("Eabc", &s), "agentscope:Eabc:99:sign_commit");
1088/// ```
1089pub fn encode_agent_scope(agent_prefix: &str, scope: &AgentScope) -> String {
1090    let expires = scope.expires_at.unwrap_or(0);
1091    let caps = scope
1092        .capabilities
1093        .iter()
1094        .map(Capability::as_str)
1095        .collect::<Vec<_>>()
1096        .join(",");
1097    format!("{AGENT_SCOPE_MARKER}{agent_prefix}:{expires}:{caps}")
1098}
1099
1100/// Decode an agent-scope seal value into `(agent_prefix, AgentScope)`, or `None` if
1101/// the value is not an `agentscope:` marker or any capability fails validation.
1102/// Inverse of [`encode_agent_scope`].
1103///
1104/// Args:
1105/// * `value`: A `Seal::Digest` value to interpret.
1106pub fn decode_agent_scope(value: &str) -> Option<(String, AgentScope)> {
1107    // {prefix}:{expires}:{caps_csv} — prefix is a `:`-free KERI prefix; caps may
1108    // contain `:`, so the 3-way split keeps the caps tail intact.
1109    let rest = value.strip_prefix(AGENT_SCOPE_MARKER)?;
1110    let mut parts = rest.splitn(3, ':');
1111    let prefix = parts.next()?.to_string();
1112    let expires: i64 = parts.next()?.parse().ok()?;
1113    let caps_csv = parts.next().unwrap_or("");
1114    let capabilities = if caps_csv.is_empty() {
1115        Vec::new()
1116    } else {
1117        caps_csv
1118            .split(',')
1119            .map(Capability::parse)
1120            .collect::<Result<Vec<_>, _>>()
1121            .ok()?
1122    };
1123    Some((
1124        prefix,
1125        AgentScope {
1126            capabilities,
1127            expires_at: (expires != 0).then_some(expires),
1128        },
1129    ))
1130}
1131
1132/// Serialize source seals to a CESR text-domain `-G##<Seqner><Saider>…`
1133/// `SealSourceCouples` group, byte-aligned with keripy 1.3.4.
1134///
1135/// Each couple is a `Seqner` (`0A`-coded 128-bit sequence) followed by a `Saider`
1136/// (the anchoring event's SAID). Reads back via [`parse_source_seal_couples`].
1137///
1138/// Args:
1139/// * `couples`: The delegate-side source seals to encode.
1140///
1141/// Usage:
1142/// ```ignore
1143/// let bytes = serialize_source_seal_couples(&[SourceSeal { s, d }])?;
1144/// ```
1145pub fn serialize_source_seal_couples(couples: &[SourceSeal]) -> Result<Vec<u8>, AttachmentError> {
1146    use cesride::{Counter, Matter, Saider, Seqner, counter};
1147
1148    let count = u32::try_from(couples.len())
1149        .map_err(|_| AttachmentError::Encode("too many source seal couples".into()))?;
1150    let mut out = Counter::new_with_code_and_count(counter::Codex::SealSourceCouples, count)
1151        .and_then(|c| c.qb64())
1152        .map_err(|e| AttachmentError::Encode(e.to_string()))?;
1153
1154    for couple in couples {
1155        let seqner = Seqner::new_with_sn(couple.s.value())
1156            .and_then(|s| s.qb64())
1157            .map_err(|e| AttachmentError::Encode(e.to_string()))?;
1158        let saider = Saider::new_with_qb64(couple.d.as_str())
1159            .and_then(|s| s.qb64())
1160            .map_err(|e| AttachmentError::Encode(e.to_string()))?;
1161        out.push_str(&seqner);
1162        out.push_str(&saider);
1163    }
1164
1165    Ok(out.into_bytes())
1166}
1167
1168/// Parse a CESR `-G##` `SealSourceCouples` group into [`SourceSeal`]s.
1169///
1170/// Inverse of [`serialize_source_seal_couples`]; code-directed so each `Seqner`
1171/// and `Saider` width is read from its CESR hard code.
1172///
1173/// Args:
1174/// * `bytes`: The `-G`-prefixed couple-group bytes (e.g. from an attachment tail).
1175pub fn parse_source_seal_couples(bytes: &[u8]) -> Result<Vec<SourceSeal>, AttachmentError> {
1176    let s = std::str::from_utf8(bytes)
1177        .map_err(|e| AttachmentError::Decode(format!("non-utf8 source-seal group: {e}")))?;
1178    let (couples, rest) = parse_source_seal_group(s)?;
1179    if !rest.is_empty() {
1180        return Err(AttachmentError::Decode(format!(
1181            "trailing bytes after source-seal group: {rest:?}"
1182        )));
1183    }
1184    Ok(couples)
1185}
1186
1187/// Parse one `-G` `SealSourceCouples` group at the head of `s`, returning the
1188/// couples and the unconsumed remainder.
1189fn parse_source_seal_group(s: &str) -> Result<(Vec<SourceSeal>, &str), AttachmentError> {
1190    use cesride::{Matter, Saider, Seqner};
1191
1192    // CESR qb64 is ASCII; reject non-ASCII at the boundary so the byte-offset slices
1193    // below cannot split a multi-byte char and panic.
1194    if !s.is_ascii() {
1195        return Err(AttachmentError::Decode("non-ASCII CESR attachment".into()));
1196    }
1197    let rest = s.strip_prefix("-G").ok_or_else(|| {
1198        AttachmentError::Decode("source-seal group must start with -G counter code".into())
1199    })?;
1200    if rest.len() < 2 {
1201        return Err(AttachmentError::Decode(
1202            "truncated -G counter header".into(),
1203        ));
1204    }
1205    let (count_b64, mut cursor) = rest.split_at(2);
1206    let count = decode_count_b64(count_b64)?;
1207
1208    let mut out = Vec::with_capacity(count);
1209    for _ in 0..count {
1210        // Seqner is fixed-width (`0A` + 22 base64 = 24 chars; a 128-bit number).
1211        if cursor.len() < SEQNER_QB64_LEN {
1212            return Err(AttachmentError::Decode(
1213                "truncated Seqner in -G couple".into(),
1214            ));
1215        }
1216        let (seqner_qb64, after_seqner) = cursor.split_at(SEQNER_QB64_LEN);
1217        let seqner = Seqner::new_with_qb64(seqner_qb64)
1218            .map_err(|e| AttachmentError::Decode(format!("Seqner: {e}")))?;
1219        let sn = seqner
1220            .sn()
1221            .map_err(|e| AttachmentError::Decode(format!("Seqner sn: {e}")))?;
1222
1223        // Saider is code-directed; the leading char fixes its full width.
1224        let first = *after_seqner.as_bytes().first().ok_or_else(|| {
1225            AttachmentError::Decode("truncated -G couple: expected a Saider".into())
1226        })?;
1227        let fs = saider_qb64_len(first).ok_or_else(|| {
1228            AttachmentError::Decode(format!("unsupported Saider code byte {first:?}"))
1229        })?;
1230        if after_seqner.len() < fs {
1231            return Err(AttachmentError::Decode(
1232                "truncated Saider in -G couple".into(),
1233            ));
1234        }
1235        let (saider_qb64, remainder) = after_seqner.split_at(fs);
1236        let saider = Saider::new_with_qb64(saider_qb64)
1237            .and_then(|s| s.qb64())
1238            .map_err(|e| AttachmentError::Decode(format!("Saider: {e}")))?;
1239        cursor = remainder;
1240
1241        out.push(SourceSeal {
1242            s: KeriSequence::new(sn),
1243            d: Said::new_unchecked(saider),
1244        });
1245    }
1246    Ok((out, cursor))
1247}
1248
1249/// Parse a delegated event's combined attachment — the controller signature group
1250/// (`-A`) followed by an optional source-seal group (`-G`).
1251///
1252/// Returns the indexed signatures and any source seals. Used by storage read paths
1253/// to re-attach a delegated event's `-G` back-reference after a JSON round-trip.
1254///
1255/// Args:
1256/// * `bytes`: The full attachment bytes (`-A…` then optional `-G…`).
1257pub fn parse_delegated_attachment(
1258    bytes: &[u8],
1259) -> Result<(Vec<IndexedSignature>, Vec<SourceSeal>), AttachmentError> {
1260    let s = std::str::from_utf8(bytes)
1261        .map_err(|e| AttachmentError::Decode(format!("non-utf8 attachment: {e}")))?;
1262    if s.is_empty() {
1263        return Ok((vec![], vec![]));
1264    }
1265    let (sigs, rest) = parse_sig_group(s)?;
1266    if rest.is_empty() {
1267        return Ok((sigs, vec![]));
1268    }
1269    let (couples, tail) = parse_source_seal_group(rest)?;
1270    if !tail.is_empty() {
1271        return Err(AttachmentError::Decode(format!(
1272            "trailing bytes after delegated attachment: {tail:?}"
1273        )));
1274    }
1275    Ok((sigs, couples))
1276}
1277
1278/// A device-signed delegated inception serialized for a single wire field.
1279///
1280/// Carried as base64url-no-pad(JSON) wherever a transport needs a signed `dip`
1281/// in one string (e.g. a pairing response's `responder_inception_event`). The
1282/// CESR signature attachment travels alongside the event so the receiver can
1283/// replay the signed dip exactly as the device emitted it.
1284///
1285/// This is the ONE wire form for a signed dip — every producer (CLI joiner,
1286/// mobile FFI) and consumer (the anchoring initiator) goes through
1287/// [`encode_signed_dip`] / [`decode_signed_dip`].
1288#[derive(Debug, Clone, Serialize, Deserialize)]
1289pub struct WireSignedDip {
1290    /// The device's delegated-inception event.
1291    pub event: DipEvent,
1292    /// base64url-no-pad of the device's CESR signature attachment over the dip.
1293    pub attachment_b64: String,
1294}
1295
1296/// Encode a device-signed dip into its single-string wire form.
1297///
1298/// Args:
1299/// * `dip`: The finalized, device-signed delegated-inception event.
1300/// * `attachment`: The device's CESR signature attachment bytes (from
1301///   [`serialize_attachment`]).
1302pub fn encode_signed_dip(dip: &DipEvent, attachment: &[u8]) -> Result<String, AttachmentError> {
1303    use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
1304    let wire = WireSignedDip {
1305        event: dip.clone(),
1306        attachment_b64: URL_SAFE_NO_PAD.encode(attachment),
1307    };
1308    let json =
1309        serde_json::to_vec(&wire).map_err(|e| AttachmentError::Encode(format!("dip json: {e}")))?;
1310    Ok(URL_SAFE_NO_PAD.encode(json))
1311}
1312
1313/// Decode a single-string wire form back into the dip and its attachment bytes.
1314/// Inverse of [`encode_signed_dip`].
1315///
1316/// Args:
1317/// * `encoded`: The base64url-no-pad(JSON) wire string.
1318pub fn decode_signed_dip(encoded: &str) -> Result<(DipEvent, Vec<u8>), AttachmentError> {
1319    use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
1320    let json = URL_SAFE_NO_PAD
1321        .decode(encoded)
1322        .map_err(|e| AttachmentError::Decode(format!("dip envelope: {e}")))?;
1323    let wire: WireSignedDip = serde_json::from_slice(&json)
1324        .map_err(|e| AttachmentError::Decode(format!("dip json: {e}")))?;
1325    let attachment = URL_SAFE_NO_PAD
1326        .decode(&wire.attachment_b64)
1327        .map_err(|e| AttachmentError::Decode(format!("dip attachment: {e}")))?;
1328    Ok((wire.event, attachment))
1329}
1330
1331/// Single-string wire form of a signed rotation: base64url-no-pad of the
1332/// JSON `{event, attachment_b64}` pair.
1333///
1334/// This is the ONE wire form for a signed `rot` in transit — every producer
1335/// (the mobile FFI's shared-KEL rotation assembler) and consumer (the daemon
1336/// endpoint that receives a co-authored rotation) goes through
1337/// [`encode_signed_rot`] / [`decode_signed_rot`]. Mirrors [`WireSignedDip`].
1338#[derive(Debug, Clone, Serialize, Deserialize)]
1339pub struct WireSignedRot {
1340    /// The rotation event.
1341    pub event: RotEvent,
1342    /// base64url-no-pad of the CESR indexed-signature attachment over the rot.
1343    pub attachment_b64: String,
1344}
1345
1346/// Encode a signed rot into its single-string wire form.
1347///
1348/// Args:
1349/// * `rot`: The finalized, signed rotation event.
1350/// * `attachment`: The CESR indexed-signature attachment bytes (from
1351///   [`serialize_attachment`]).
1352pub fn encode_signed_rot(rot: &RotEvent, attachment: &[u8]) -> Result<String, AttachmentError> {
1353    use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
1354    let wire = WireSignedRot {
1355        event: rot.clone(),
1356        attachment_b64: URL_SAFE_NO_PAD.encode(attachment),
1357    };
1358    let json =
1359        serde_json::to_vec(&wire).map_err(|e| AttachmentError::Encode(format!("rot json: {e}")))?;
1360    Ok(URL_SAFE_NO_PAD.encode(json))
1361}
1362
1363/// Decode a single-string wire form back into the rot and its attachment bytes.
1364/// Inverse of [`encode_signed_rot`].
1365///
1366/// Args:
1367/// * `encoded`: The base64url-no-pad(JSON) wire string.
1368pub fn decode_signed_rot(encoded: &str) -> Result<(RotEvent, Vec<u8>), AttachmentError> {
1369    use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
1370    let json = URL_SAFE_NO_PAD
1371        .decode(encoded)
1372        .map_err(|e| AttachmentError::Decode(format!("rot envelope: {e}")))?;
1373    let wire: WireSignedRot = serde_json::from_slice(&json)
1374        .map_err(|e| AttachmentError::Decode(format!("rot json: {e}")))?;
1375    let attachment = URL_SAFE_NO_PAD
1376        .decode(&wire.attachment_b64)
1377        .map_err(|e| AttachmentError::Decode(format!("rot attachment: {e}")))?;
1378    Ok((wire.event, attachment))
1379}
1380
1381/// CESR `Seqner` qb64 width: `0A` hard code (2) + 22 base64 chars = 24.
1382const SEQNER_QB64_LEN: usize = 24;
1383
1384/// qb64 full width of a 32-byte digest `Saider` from its leading code char.
1385/// Blake3-256 (`E`), Blake2b-256 (`F`), Blake2s-256 (`G`), SHA3-256 (`H`),
1386/// SHA2-256 (`I`) are all single-char-coded 44-char primitives. Mirrors cesride
1387/// `matter` sizage for the digest family auths emits.
1388fn saider_qb64_len(first: u8) -> Option<usize> {
1389    matches!(first, b'E' | b'F' | b'G' | b'H' | b'I').then_some(44)
1390}
1391
1392/// An event paired with its detached signature(s).
1393///
1394/// Per the KERI spec, signatures are NOT part of the event body. They are
1395/// attached externally (CESR attachment codes in streams, or stored alongside
1396/// in databases). The event body is what gets hashed for the SAID.
1397///
1398/// Usage:
1399/// ```ignore
1400/// use auths_keri::{SignedEvent, IndexedSignature, Event};
1401///
1402/// // After creating and finalizing an event:
1403/// let signed = SignedEvent::new(event, vec![IndexedSignature { index: 0, sig: sig_bytes }]);
1404/// ```
1405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1406pub struct SignedEvent {
1407    /// The event body (no signature data).
1408    pub event: Event,
1409    /// Controller-indexed signatures (detached from body).
1410    pub signatures: Vec<IndexedSignature>,
1411}
1412
1413impl SignedEvent {
1414    /// Create a new signed event from an event and its signatures.
1415    pub fn new(event: Event, signatures: Vec<IndexedSignature>) -> Self {
1416        Self { event, signatures }
1417    }
1418
1419    /// Get the SAID of the inner event.
1420    pub fn said(&self) -> &Said {
1421        self.event.said()
1422    }
1423
1424    /// Get the sequence number of the inner event.
1425    pub fn sequence(&self) -> KeriSequence {
1426        self.event.sequence()
1427    }
1428
1429    /// Get the identifier prefix of the inner event.
1430    pub fn prefix(&self) -> &Prefix {
1431        self.event.prefix()
1432    }
1433}
1434
1435/// Serialize a list of externalized signatures to CESR text-domain
1436/// `-A##<siger1><siger2>…` indexed-signature group bytes.
1437///
1438/// Wire format: `-A` prefix + 2-char base64url count + each signature as
1439/// a CESR `Siger` (qb64). Reads back via `parse_attachment`.
1440pub fn serialize_attachment(signatures: &[IndexedSignature]) -> Result<Vec<u8>, AttachmentError> {
1441    use cesride::{Indexer, Siger, indexer};
1442
1443    let mut out = String::new();
1444    out.push_str("-A");
1445    out.push_str(&encode_count_b64(signatures.len())?);
1446
1447    for sig in signatures {
1448        // Ed25519 indexed signature (auths' sign paths produce Ed25519). A
1449        // distinct prior-commitment index (`prior_index != index`) needs the
1450        // dual-index "Big" code `2A`, which carries both indices; otherwise the
1451        // single-index `A` (ondex == index) keeps the legacy 88-char bytes.
1452        let (code, ondex) = if sig.prior_index.is_some_and(|j| j != sig.index) {
1453            (indexer::Codex::Ed25519_Big, sig.prior_index)
1454        } else {
1455            (indexer::Codex::Ed25519, None)
1456        };
1457        let siger = Siger::new(
1458            None,
1459            Some(sig.index),
1460            ondex,
1461            Some(code),
1462            Some(&sig.sig),
1463            None,
1464            None,
1465            None,
1466        )
1467        .map_err(|e| AttachmentError::Encode(e.to_string()))?;
1468        let qb64 = siger
1469            .qb64()
1470            .map_err(|e| AttachmentError::Encode(e.to_string()))?;
1471        out.push_str(&qb64);
1472    }
1473
1474    Ok(out.into_bytes())
1475}
1476
1477/// CESR Indexer hard-code length: codes whose first char is a digit (`0`–`4`)
1478/// are multi-char selectors; all others are single-char. Mirrors cesride's
1479/// `indexer` hardage (whose tables are `pub(crate)`).
1480fn indexer_hard_len(first: u8) -> usize {
1481    if first.is_ascii_digit() { 2 } else { 1 }
1482}
1483
1484/// `(full qb64 size, ondex size)` for a CESR Indexer signature code, mirroring
1485/// cesride `indexer/tables.rs` (its `sizage` is `pub(crate)`, not callable here).
1486///
1487/// `os > 0` marks a dual-index ("Big") code carrying a *distinct* prior-commitment
1488/// index (ondex); single-index codes have `os == 0` (ondex echoes index).
1489fn indexer_sizage(code: &str) -> Option<(usize, usize)> {
1490    Some(match code {
1491        // single-index: Ed25519 A/B, secp256k1 C/D, P-256 E/F (+ Crt variants)
1492        "A" | "B" | "C" | "D" | "E" | "F" => (88, 0),
1493        // dual-index ("Big"): Ed25519 2A/2B, secp256k1 2C/2D, P-256 2E/2F
1494        "2A" | "2B" | "2C" | "2D" | "2E" | "2F" => (92, 2),
1495        // large/huge index variants (completeness; not emitted by auths)
1496        "0A" | "0B" => (156, 1),
1497        "3A" | "3B" => (160, 3),
1498        _ => return None,
1499    })
1500}
1501
1502/// Parse a CESR `-A##` indexed-signature group into the constituent
1503/// `IndexedSignature`s.
1504///
1505/// Code-directed: each siger's width is read from its CESR hard code (so a group
1506/// may mix single-index `A` (88 ch) and dual-index `2A` (92 ch), or curves). A
1507/// signature under a dual-index code populates `prior_index` from its ondex.
1508pub fn parse_attachment(bytes: &[u8]) -> Result<Vec<IndexedSignature>, AttachmentError> {
1509    let s = std::str::from_utf8(bytes)
1510        .map_err(|e| AttachmentError::Decode(format!("non-utf8 attachment: {e}")))?;
1511
1512    if s.is_empty() {
1513        return Ok(vec![]);
1514    }
1515
1516    let (sigs, rest) = parse_sig_group(s)?;
1517    if !rest.is_empty() {
1518        return Err(AttachmentError::Decode(format!(
1519            "trailing bytes after signature group: {rest:?}"
1520        )));
1521    }
1522    Ok(sigs)
1523}
1524
1525/// Parse one `-A` indexed-signature group at the head of `s`, returning the
1526/// signatures and the unconsumed remainder (e.g. a trailing `-G` source-seal
1527/// group on a delegated event).
1528fn parse_sig_group(s: &str) -> Result<(Vec<IndexedSignature>, &str), AttachmentError> {
1529    use cesride::{Indexer, Siger};
1530
1531    // CESR qb64 is ASCII; reject non-ASCII at the boundary so the byte-offset slices
1532    // below cannot split a multi-byte char and panic.
1533    if !s.is_ascii() {
1534        return Err(AttachmentError::Decode("non-ASCII CESR attachment".into()));
1535    }
1536    let rest = s.strip_prefix("-A").ok_or_else(|| {
1537        AttachmentError::Decode("attachment must start with -A counter code".into())
1538    })?;
1539    if rest.len() < 2 {
1540        return Err(AttachmentError::Decode("truncated counter header".into()));
1541    }
1542    let (count_b64, body) = rest.split_at(2);
1543    let count = decode_count_b64(count_b64)?;
1544
1545    let mut out = Vec::with_capacity(count);
1546    let mut cursor = body;
1547    for _ in 0..count {
1548        let first = *cursor.as_bytes().first().ok_or_else(|| {
1549            AttachmentError::Decode("truncated group: expected another signature".into())
1550        })?;
1551        let hs = indexer_hard_len(first);
1552        if cursor.len() < hs {
1553            return Err(AttachmentError::Decode("truncated siger hard code".into()));
1554        }
1555        let code = &cursor[..hs];
1556        let (fs, os) = indexer_sizage(code)
1557            .ok_or_else(|| AttachmentError::Decode(format!("unknown indexer code {code:?}")))?;
1558        if cursor.len() < fs {
1559            return Err(AttachmentError::Decode(format!(
1560                "insufficient bytes for {code} siger: need {fs}, have {}",
1561                cursor.len()
1562            )));
1563        }
1564        let (siger_qb64, remainder) = cursor.split_at(fs);
1565        cursor = remainder;
1566
1567        let siger = Siger::new(None, None, None, None, None, None, Some(siger_qb64), None)
1568            .map_err(|e| AttachmentError::Decode(format!("Siger: {e}")))?;
1569        // A distinct prior-commitment index is carried only by dual-index codes
1570        // (os > 0); single-index codes report ondex == index, which is not a binding.
1571        let prior_index = (os > 0).then(|| siger.ondex());
1572
1573        out.push(IndexedSignature {
1574            index: siger.index(),
1575            prior_index,
1576            sig: siger.raw(),
1577        });
1578    }
1579
1580    Ok((out, cursor))
1581}
1582
1583/// Pair an ordered KEL with its per-event CESR signature attachments,
1584/// producing the [`SignedEvent`]s an authenticated replay
1585/// ([`validate_signed_kel`](crate::validate_signed_kel)) consumes.
1586///
1587/// FAIL-CLOSED on count mismatch: a KEL whose attachment list is absent or
1588/// short is an *unauthenticated* KEL — it is refused here rather than letting
1589/// any caller fall back to a structural-only replay (the RT-002 forge). Every
1590/// stateless verify entrypoint (WASM, mobile FFI) routes through this one
1591/// definition so the refusal rule cannot drift between transports.
1592///
1593/// Args:
1594/// * `events`: The ordered KEL events, e.g. from [`parse_kel_json`](crate::parse_kel_json).
1595/// * `attachments`: One CESR `-A##` indexed-signature group per event, as raw
1596///   bytes (the text-domain `.cesr` form; transport encodings like hex are the
1597///   caller's adapter concern).
1598///
1599/// Usage:
1600/// ```ignore
1601/// let events = auths_keri::parse_kel_json(kel_json)?;
1602/// let signed = auths_keri::pair_kel_attachments(events, &attachment_bytes)?;
1603/// let state = auths_keri::validate_signed_kel(&signed, None)?;
1604/// ```
1605pub fn pair_kel_attachments(
1606    events: Vec<Event>,
1607    attachments: &[impl AsRef<[u8]>],
1608) -> Result<Vec<SignedEvent>, AttachmentError> {
1609    if attachments.len() != events.len() {
1610        return Err(AttachmentError::CountMismatch {
1611            events: events.len(),
1612            attachments: attachments.len(),
1613        });
1614    }
1615    events
1616        .into_iter()
1617        .zip(attachments)
1618        .map(|(event, att)| {
1619            // A delegated (`dip`/`drt`) event's attachment is `-A <sig> ++ -G
1620            // <source seal>`; a plain event's is just `-A <sig>`. The JSON body
1621            // of a delegated event carries no source seal (it lives in the
1622            // attachment), so restore it here — the bilateral delegation
1623            // binding then re-verifies it against the delegator KEL; it is
1624            // never trusted from the attachment alone.
1625            let (signatures, seals) = parse_delegated_attachment(att.as_ref())?;
1626            let event = rehydrate_source_seal(event, seals.into_iter().next());
1627            Ok(SignedEvent::new(event, signatures))
1628        })
1629        .collect()
1630}
1631
1632/// Re-attach a delegated event's source seal from its parsed attachment; plain
1633/// events and sealless attachments pass through unchanged.
1634fn rehydrate_source_seal(event: Event, seal: Option<SourceSeal>) -> Event {
1635    let Some(seal) = seal else {
1636        return event;
1637    };
1638    match event {
1639        Event::Dip(mut e) => {
1640            e.source_seal = Some(seal);
1641            Event::Dip(e)
1642        }
1643        Event::Drt(mut e) => {
1644            e.source_seal = Some(seal);
1645            Event::Drt(e)
1646        }
1647        other => other,
1648    }
1649}
1650
1651/// Error shape for attachment encode/decode.
1652#[derive(Debug, thiserror::Error)]
1653#[non_exhaustive]
1654pub enum AttachmentError {
1655    /// Encoding an indexed signature into CESR failed.
1656    #[error("attachment encode: {0}")]
1657    Encode(String),
1658    /// Decoding a CESR attachment stream failed (bad counter, malformed Siger, etc.).
1659    #[error("attachment decode: {0}")]
1660    Decode(String),
1661    /// The KEL and its attachment list disagree in length: at least one event
1662    /// has no signature group, so the KEL is unauthenticated. Refused outright
1663    /// instead of degrading to a structural-only replay.
1664    #[error(
1665        "KEL/attachment count mismatch ({events} events vs {attachments} attachments): the KEL is unauthenticated, refusing"
1666    )]
1667    CountMismatch {
1668        /// Number of events in the KEL.
1669        events: usize,
1670        /// Number of signature attachments supplied.
1671        attachments: usize,
1672    },
1673}
1674
1675/// CESR base64url alphabet, ordered so `B64_ALPHA[n]` is the char for n.
1676const B64_ALPHA: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
1677
1678fn encode_count_b64(count: usize) -> Result<String, AttachmentError> {
1679    if count >= 64 * 64 {
1680        return Err(AttachmentError::Encode(format!(
1681            "count {count} exceeds 2-char base64url max (4095)"
1682        )));
1683    }
1684    let hi = B64_ALPHA[(count >> 6) & 0x3f] as char;
1685    let lo = B64_ALPHA[count & 0x3f] as char;
1686    Ok(format!("{hi}{lo}"))
1687}
1688
1689fn decode_count_b64(s: &str) -> Result<usize, AttachmentError> {
1690    let mut it = s.chars();
1691    let hi = it
1692        .next()
1693        .and_then(b64_index)
1694        .ok_or_else(|| AttachmentError::Decode(format!("invalid count hi char: {s:?}")))?;
1695    let lo = it
1696        .next()
1697        .and_then(b64_index)
1698        .ok_or_else(|| AttachmentError::Decode(format!("invalid count lo char: {s:?}")))?;
1699    Ok((hi << 6) | lo)
1700}
1701
1702fn b64_index(c: char) -> Option<usize> {
1703    B64_ALPHA.iter().position(|&b| b as char == c)
1704}
1705
1706#[cfg(test)]
1707#[allow(clippy::unwrap_used)]
1708mod tests {
1709    use super::*;
1710
1711    #[test]
1712    fn attachment_roundtrip_single_sig() {
1713        let sigs = vec![IndexedSignature {
1714            index: 0,
1715            prior_index: None,
1716            sig: vec![0x42u8; 64],
1717        }];
1718        let bytes = serialize_attachment(&sigs).unwrap();
1719        let s = std::str::from_utf8(&bytes).unwrap();
1720        assert!(s.starts_with("-AAB"), "expected -AAB prefix, got {s:?}");
1721        let back = parse_attachment(&bytes).unwrap();
1722        assert_eq!(back.len(), 1);
1723        assert_eq!(back[0].index, 0);
1724        assert_eq!(back[0].sig, vec![0x42u8; 64]);
1725    }
1726
1727    #[test]
1728    fn attachment_parsers_reject_multibyte_without_panicking() {
1729        // qb64 is ASCII; a multi-byte UTF-8 char in a -A signature or -G seal group
1730        // must fail closed, not panic on a non-char-boundary slice.
1731        assert!(parse_attachment("-AAB\u{42c}".as_bytes()).is_err());
1732        assert!(parse_delegated_attachment("-AAB\u{42c}".as_bytes()).is_err());
1733
1734        // -G source-seal group with a multi-byte char straddling the Seqner slice.
1735        let mut seal = String::from("-GAB");
1736        seal.push_str(&"A".repeat(23));
1737        seal.push('\u{42c}');
1738        assert!(parse_source_seal_couples(seal.as_bytes()).is_err());
1739    }
1740
1741    #[test]
1742    fn cesr_length_tables_match_cesride_encoding() {
1743        use cesride::{Diger, Matter, matter};
1744        // saider_qb64_len: pin the Blake3-256 SAID code auths emits to cesride's width.
1745        let digest = Diger::new(
1746            None,
1747            Some(matter::Codex::Blake3_256),
1748            Some(&[0u8; 32]),
1749            None,
1750            None,
1751            None,
1752        )
1753        .and_then(|d| d.qb64())
1754        .unwrap();
1755        assert_eq!(saider_qb64_len(digest.as_bytes()[0]), Some(digest.len()));
1756
1757        // indexer_sizage: pin the single-index (`A`) and dual-index (`2A`) Ed25519 siger
1758        // widths auths emits, by measuring a real encoded siger (after the 4-char counter).
1759        for prior_index in [None, Some(1u32)] {
1760            let att = serialize_attachment(&[IndexedSignature {
1761                index: 0,
1762                prior_index,
1763                sig: vec![0u8; 64],
1764            }])
1765            .unwrap();
1766            let att = std::str::from_utf8(&att).unwrap();
1767            let siger = &att[4..];
1768            let code = &siger[..indexer_hard_len(siger.as_bytes()[0])];
1769            assert_eq!(
1770                indexer_sizage(code).map(|(fs, _)| fs),
1771                Some(siger.len()),
1772                "indexer_sizage({code:?}) drifted from cesride's {} chars",
1773                siger.len()
1774            );
1775        }
1776    }
1777
1778    #[test]
1779    fn attachment_roundtrip_three_sigs() {
1780        let sigs = vec![
1781            IndexedSignature {
1782                index: 0,
1783                prior_index: None,
1784                sig: vec![0x01u8; 64],
1785            },
1786            IndexedSignature {
1787                index: 1,
1788                prior_index: None,
1789                sig: vec![0x02u8; 64],
1790            },
1791            IndexedSignature {
1792                index: 2,
1793                prior_index: None,
1794                sig: vec![0x03u8; 64],
1795            },
1796        ];
1797        let bytes = serialize_attachment(&sigs).unwrap();
1798        let s = std::str::from_utf8(&bytes).unwrap();
1799        assert!(s.starts_with("-AAD"), "expected -AAD prefix, got {s:?}");
1800        let back = parse_attachment(&bytes).unwrap();
1801        assert_eq!(back.len(), 3);
1802        for (i, sig) in back.iter().enumerate() {
1803            assert_eq!(sig.index, i as u32);
1804        }
1805    }
1806
1807    #[test]
1808    fn attachment_empty() {
1809        let bytes = serialize_attachment(&[]).unwrap();
1810        assert_eq!(bytes, b"-AAA");
1811        let back = parse_attachment(&bytes).unwrap();
1812        assert!(back.is_empty());
1813    }
1814
1815    #[test]
1816    fn keri_sequence_serializes_as_hex() {
1817        assert_eq!(
1818            serde_json::to_string(&KeriSequence::new(0)).unwrap(),
1819            "\"0\""
1820        );
1821        assert_eq!(
1822            serde_json::to_string(&KeriSequence::new(10)).unwrap(),
1823            "\"a\""
1824        );
1825        assert_eq!(
1826            serde_json::to_string(&KeriSequence::new(255)).unwrap(),
1827            "\"ff\""
1828        );
1829    }
1830
1831    #[test]
1832    fn keri_sequence_deserializes_from_hex() {
1833        let s: KeriSequence = serde_json::from_str("\"0\"").unwrap();
1834        assert_eq!(s.value(), 0);
1835        let s: KeriSequence = serde_json::from_str("\"a\"").unwrap();
1836        assert_eq!(s.value(), 10);
1837        let s: KeriSequence = serde_json::from_str("\"ff\"").unwrap();
1838        assert_eq!(s.value(), 255);
1839    }
1840
1841    #[test]
1842    fn keri_sequence_rejects_invalid_hex() {
1843        assert!(serde_json::from_str::<KeriSequence>("\"not_hex\"").is_err());
1844    }
1845
1846    #[test]
1847    fn icp_event_always_serializes_d_a_c() {
1848        use crate::types::{CesrKey, Threshold, VersionString};
1849        let icp = IcpEvent {
1850            v: VersionString::placeholder(),
1851            d: Said::default(),
1852            i: Prefix::new_unchecked("ETest123".to_string()),
1853            s: KeriSequence::new(0),
1854            kt: Threshold::Simple(1),
1855            k: vec![CesrKey::new_unchecked("DKey123".to_string())],
1856            nt: Threshold::Simple(1),
1857            n: vec![Said::new_unchecked("ENext456".to_string())],
1858            bt: Threshold::Simple(0),
1859            b: vec![],
1860            c: vec![],
1861            a: vec![],
1862        };
1863        let json = serde_json::to_string(&icp).unwrap();
1864        // d, a, c are always serialized (spec requires all fields)
1865        assert!(json.contains("\"d\":"), "d must always be present");
1866        assert!(json.contains("\"a\":"), "a must always be present");
1867        assert!(json.contains("\"c\":"), "c must always be present");
1868        // x is still conditionally omitted (empty)
1869        assert!(!json.contains("\"x\":"), "empty x must be omitted");
1870        assert!(json.contains("\"s\":\"0\""), "s must serialize as hex");
1871    }
1872
1873    #[test]
1874    fn event_enum_deserializes_by_t_field() {
1875        let json = r#"{"v":"KERI10JSON000000_","t":"icp","d":"ETestSaid","i":"E123","s":"0","kt":"1","k":["DKey"],"nt":"1","n":["ENext"],"bt":"0","b":[]}"#;
1876        let event: Event = serde_json::from_str(json).unwrap();
1877        assert!(event.is_inception());
1878        assert_eq!(event.sequence().value(), 0);
1879    }
1880
1881    #[test]
1882    fn digest_seal_roundtrips() {
1883        let seal = Seal::digest("EDigest123");
1884        let json = serde_json::to_string(&seal).unwrap();
1885        assert_eq!(json, r#"{"d":"EDigest123"}"#);
1886        let parsed: Seal = serde_json::from_str(&json).unwrap();
1887        assert_eq!(seal, parsed);
1888    }
1889
1890    #[test]
1891    fn seal_event_location_roundtrips() {
1892        // A.8 (F-36): the KERI §7 event-location seal {i,s,p,t,d} round-trips.
1893        let seal = Seal::EventLocation {
1894            i: Prefix::new_unchecked("EPrefix".to_string()),
1895            s: KeriSequence::new(3),
1896            p: Said::new_unchecked("EPrior".to_string()),
1897            t: "ixn".to_string(),
1898            d: Said::new_unchecked("EDigest".to_string()),
1899        };
1900        let json = serde_json::to_string(&seal).unwrap();
1901        let parsed: Seal = serde_json::from_str(&json).unwrap();
1902        assert_eq!(seal, parsed);
1903        assert_eq!(parsed.digest_value().map(|d| d.as_str()), Some("EDigest"));
1904    }
1905
1906    #[test]
1907    fn seal_rejects_malformed_i_s() {
1908        // A.8 (F-37): a malformed {i,s} seal (no d) must error, not silently
1909        // collapse to a LatestEstablishment {i} that drops `s`.
1910        let r: Result<Seal, _> = serde_json::from_str(r#"{"i":"EPrefix","s":"3"}"#);
1911        assert!(r.is_err(), "malformed {{i,s}} seal must be rejected");
1912        // and an entirely unknown field set errors too.
1913        let r2: Result<Seal, _> = serde_json::from_str(r#"{"zz":"x"}"#);
1914        assert!(r2.is_err());
1915    }
1916
1917    #[test]
1918    fn key_event_seal_roundtrips() {
1919        let seal = Seal::key_event(
1920            Prefix::new_unchecked("EPrefix".to_string()),
1921            KeriSequence::new(1),
1922            Said::new_unchecked("ESaid".to_string()),
1923        );
1924        let json = serde_json::to_string(&seal).unwrap();
1925        let parsed: Seal = serde_json::from_str(&json).unwrap();
1926        assert_eq!(seal, parsed);
1927    }
1928
1929    #[test]
1930    fn indexed_signature_serde_roundtrip() {
1931        let sig = IndexedSignature {
1932            index: 2,
1933            prior_index: None,
1934            sig: vec![0xab; 64],
1935        };
1936        let json = serde_json::to_string(&sig).unwrap();
1937        assert!(json.contains("\"index\":2"));
1938        assert!(
1939            !json.contains("prior_index"),
1940            "a None prior_index must be omitted from the wire: {json}"
1941        );
1942        let parsed: IndexedSignature = serde_json::from_str(&json).unwrap();
1943        assert_eq!(parsed, sig);
1944    }
1945
1946    #[test]
1947    fn signed_event_accessors() {
1948        use crate::types::{CesrKey, Threshold, VersionString};
1949        let icp = IcpEvent {
1950            v: VersionString::placeholder(),
1951            d: Said::new_unchecked("ESAID123".to_string()),
1952            i: Prefix::new_unchecked("EPrefix".to_string()),
1953            s: KeriSequence::new(0),
1954            kt: Threshold::Simple(1),
1955            k: vec![CesrKey::new_unchecked("DKey".to_string())],
1956            nt: Threshold::Simple(1),
1957            n: vec![Said::new_unchecked("ENext".to_string())],
1958            bt: Threshold::Simple(0),
1959            b: vec![],
1960            c: vec![],
1961            a: vec![],
1962        };
1963        let signed = SignedEvent::new(
1964            Event::Icp(icp),
1965            vec![IndexedSignature {
1966                index: 0,
1967                prior_index: None,
1968                sig: vec![0u8; 64],
1969            }],
1970        );
1971        assert_eq!(signed.said().as_str(), "ESAID123");
1972        assert_eq!(signed.sequence().value(), 0);
1973        assert_eq!(signed.prefix().as_str(), "EPrefix");
1974        assert_eq!(signed.signatures.len(), 1);
1975        assert_eq!(signed.signatures[0].index, 0);
1976    }
1977
1978    #[test]
1979    fn seal_digest_value() {
1980        let seal = Seal::digest("ETest123");
1981        assert_eq!(seal.digest_value().unwrap().as_str(), "ETest123");
1982        let latest = Seal::LatestEstablishment {
1983            i: Prefix::new_unchecked("EPrefix".to_string()),
1984        };
1985        assert!(latest.digest_value().is_none());
1986    }
1987}