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    let rest = s.strip_prefix("-G").ok_or_else(|| {
1193        AttachmentError::Decode("source-seal group must start with -G counter code".into())
1194    })?;
1195    if rest.len() < 2 {
1196        return Err(AttachmentError::Decode(
1197            "truncated -G counter header".into(),
1198        ));
1199    }
1200    let (count_b64, mut cursor) = rest.split_at(2);
1201    let count = decode_count_b64(count_b64)?;
1202
1203    let mut out = Vec::with_capacity(count);
1204    for _ in 0..count {
1205        // Seqner is fixed-width (`0A` + 22 base64 = 24 chars; a 128-bit number).
1206        if cursor.len() < SEQNER_QB64_LEN {
1207            return Err(AttachmentError::Decode(
1208                "truncated Seqner in -G couple".into(),
1209            ));
1210        }
1211        let (seqner_qb64, after_seqner) = cursor.split_at(SEQNER_QB64_LEN);
1212        let seqner = Seqner::new_with_qb64(seqner_qb64)
1213            .map_err(|e| AttachmentError::Decode(format!("Seqner: {e}")))?;
1214        let sn = seqner
1215            .sn()
1216            .map_err(|e| AttachmentError::Decode(format!("Seqner sn: {e}")))?;
1217
1218        // Saider is code-directed; the leading char fixes its full width.
1219        let first = *after_seqner.as_bytes().first().ok_or_else(|| {
1220            AttachmentError::Decode("truncated -G couple: expected a Saider".into())
1221        })?;
1222        let fs = saider_qb64_len(first).ok_or_else(|| {
1223            AttachmentError::Decode(format!("unsupported Saider code byte {first:?}"))
1224        })?;
1225        if after_seqner.len() < fs {
1226            return Err(AttachmentError::Decode(
1227                "truncated Saider in -G couple".into(),
1228            ));
1229        }
1230        let (saider_qb64, remainder) = after_seqner.split_at(fs);
1231        let saider = Saider::new_with_qb64(saider_qb64)
1232            .and_then(|s| s.qb64())
1233            .map_err(|e| AttachmentError::Decode(format!("Saider: {e}")))?;
1234        cursor = remainder;
1235
1236        out.push(SourceSeal {
1237            s: KeriSequence::new(sn),
1238            d: Said::new_unchecked(saider),
1239        });
1240    }
1241    Ok((out, cursor))
1242}
1243
1244/// Parse a delegated event's combined attachment — the controller signature group
1245/// (`-A`) followed by an optional source-seal group (`-G`).
1246///
1247/// Returns the indexed signatures and any source seals. Used by storage read paths
1248/// to re-attach a delegated event's `-G` back-reference after a JSON round-trip.
1249///
1250/// Args:
1251/// * `bytes`: The full attachment bytes (`-A…` then optional `-G…`).
1252pub fn parse_delegated_attachment(
1253    bytes: &[u8],
1254) -> Result<(Vec<IndexedSignature>, Vec<SourceSeal>), AttachmentError> {
1255    let s = std::str::from_utf8(bytes)
1256        .map_err(|e| AttachmentError::Decode(format!("non-utf8 attachment: {e}")))?;
1257    if s.is_empty() {
1258        return Ok((vec![], vec![]));
1259    }
1260    let (sigs, rest) = parse_sig_group(s)?;
1261    if rest.is_empty() {
1262        return Ok((sigs, vec![]));
1263    }
1264    let (couples, tail) = parse_source_seal_group(rest)?;
1265    if !tail.is_empty() {
1266        return Err(AttachmentError::Decode(format!(
1267            "trailing bytes after delegated attachment: {tail:?}"
1268        )));
1269    }
1270    Ok((sigs, couples))
1271}
1272
1273/// CESR `Seqner` qb64 width: `0A` hard code (2) + 22 base64 chars = 24.
1274const SEQNER_QB64_LEN: usize = 24;
1275
1276/// qb64 full width of a 32-byte digest `Saider` from its leading code char.
1277/// Blake3-256 (`E`), Blake2b-256 (`F`), Blake2s-256 (`G`), SHA3-256 (`H`),
1278/// SHA2-256 (`I`) are all single-char-coded 44-char primitives. Mirrors cesride
1279/// `matter` sizage for the digest family auths emits.
1280fn saider_qb64_len(first: u8) -> Option<usize> {
1281    matches!(first, b'E' | b'F' | b'G' | b'H' | b'I').then_some(44)
1282}
1283
1284/// An event paired with its detached signature(s).
1285///
1286/// Per the KERI spec, signatures are NOT part of the event body. They are
1287/// attached externally (CESR attachment codes in streams, or stored alongside
1288/// in databases). The event body is what gets hashed for the SAID.
1289///
1290/// Usage:
1291/// ```ignore
1292/// use auths_keri::{SignedEvent, IndexedSignature, Event};
1293///
1294/// // After creating and finalizing an event:
1295/// let signed = SignedEvent::new(event, vec![IndexedSignature { index: 0, sig: sig_bytes }]);
1296/// ```
1297#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1298pub struct SignedEvent {
1299    /// The event body (no signature data).
1300    pub event: Event,
1301    /// Controller-indexed signatures (detached from body).
1302    pub signatures: Vec<IndexedSignature>,
1303}
1304
1305impl SignedEvent {
1306    /// Create a new signed event from an event and its signatures.
1307    pub fn new(event: Event, signatures: Vec<IndexedSignature>) -> Self {
1308        Self { event, signatures }
1309    }
1310
1311    /// Get the SAID of the inner event.
1312    pub fn said(&self) -> &Said {
1313        self.event.said()
1314    }
1315
1316    /// Get the sequence number of the inner event.
1317    pub fn sequence(&self) -> KeriSequence {
1318        self.event.sequence()
1319    }
1320
1321    /// Get the identifier prefix of the inner event.
1322    pub fn prefix(&self) -> &Prefix {
1323        self.event.prefix()
1324    }
1325}
1326
1327/// Serialize a list of externalized signatures to CESR text-domain
1328/// `-A##<siger1><siger2>…` indexed-signature group bytes.
1329///
1330/// Wire format: `-A` prefix + 2-char base64url count + each signature as
1331/// a CESR `Siger` (qb64). Reads back via `parse_attachment`.
1332pub fn serialize_attachment(signatures: &[IndexedSignature]) -> Result<Vec<u8>, AttachmentError> {
1333    use cesride::{Indexer, Siger, indexer};
1334
1335    let mut out = String::new();
1336    out.push_str("-A");
1337    out.push_str(&encode_count_b64(signatures.len())?);
1338
1339    for sig in signatures {
1340        // Ed25519 indexed signature (auths' sign paths produce Ed25519). A
1341        // distinct prior-commitment index (`prior_index != index`) needs the
1342        // dual-index "Big" code `2A`, which carries both indices; otherwise the
1343        // single-index `A` (ondex == index) keeps the legacy 88-char bytes.
1344        let (code, ondex) = if sig.prior_index.is_some_and(|j| j != sig.index) {
1345            (indexer::Codex::Ed25519_Big, sig.prior_index)
1346        } else {
1347            (indexer::Codex::Ed25519, None)
1348        };
1349        let siger = Siger::new(
1350            None,
1351            Some(sig.index),
1352            ondex,
1353            Some(code),
1354            Some(&sig.sig),
1355            None,
1356            None,
1357            None,
1358        )
1359        .map_err(|e| AttachmentError::Encode(e.to_string()))?;
1360        let qb64 = siger
1361            .qb64()
1362            .map_err(|e| AttachmentError::Encode(e.to_string()))?;
1363        out.push_str(&qb64);
1364    }
1365
1366    Ok(out.into_bytes())
1367}
1368
1369/// CESR Indexer hard-code length: codes whose first char is a digit (`0`–`4`)
1370/// are multi-char selectors; all others are single-char. Mirrors cesride's
1371/// `indexer` hardage (whose tables are `pub(crate)`).
1372fn indexer_hard_len(first: u8) -> usize {
1373    if first.is_ascii_digit() { 2 } else { 1 }
1374}
1375
1376/// `(full qb64 size, ondex size)` for a CESR Indexer signature code, mirroring
1377/// cesride `indexer/tables.rs` (its `sizage` is `pub(crate)`, not callable here).
1378///
1379/// `os > 0` marks a dual-index ("Big") code carrying a *distinct* prior-commitment
1380/// index (ondex); single-index codes have `os == 0` (ondex echoes index).
1381fn indexer_sizage(code: &str) -> Option<(usize, usize)> {
1382    Some(match code {
1383        // single-index: Ed25519 A/B, secp256k1 C/D, P-256 E/F (+ Crt variants)
1384        "A" | "B" | "C" | "D" | "E" | "F" => (88, 0),
1385        // dual-index ("Big"): Ed25519 2A/2B, secp256k1 2C/2D, P-256 2E/2F
1386        "2A" | "2B" | "2C" | "2D" | "2E" | "2F" => (92, 2),
1387        // large/huge index variants (completeness; not emitted by auths)
1388        "0A" | "0B" => (156, 1),
1389        "3A" | "3B" => (160, 3),
1390        _ => return None,
1391    })
1392}
1393
1394/// Parse a CESR `-A##` indexed-signature group into the constituent
1395/// `IndexedSignature`s.
1396///
1397/// Code-directed: each siger's width is read from its CESR hard code (so a group
1398/// may mix single-index `A` (88 ch) and dual-index `2A` (92 ch), or curves). A
1399/// signature under a dual-index code populates `prior_index` from its ondex.
1400pub fn parse_attachment(bytes: &[u8]) -> Result<Vec<IndexedSignature>, AttachmentError> {
1401    let s = std::str::from_utf8(bytes)
1402        .map_err(|e| AttachmentError::Decode(format!("non-utf8 attachment: {e}")))?;
1403
1404    if s.is_empty() {
1405        return Ok(vec![]);
1406    }
1407
1408    let (sigs, rest) = parse_sig_group(s)?;
1409    if !rest.is_empty() {
1410        return Err(AttachmentError::Decode(format!(
1411            "trailing bytes after signature group: {rest:?}"
1412        )));
1413    }
1414    Ok(sigs)
1415}
1416
1417/// Parse one `-A` indexed-signature group at the head of `s`, returning the
1418/// signatures and the unconsumed remainder (e.g. a trailing `-G` source-seal
1419/// group on a delegated event).
1420fn parse_sig_group(s: &str) -> Result<(Vec<IndexedSignature>, &str), AttachmentError> {
1421    use cesride::{Indexer, Siger};
1422
1423    let rest = s.strip_prefix("-A").ok_or_else(|| {
1424        AttachmentError::Decode("attachment must start with -A counter code".into())
1425    })?;
1426    if rest.len() < 2 {
1427        return Err(AttachmentError::Decode("truncated counter header".into()));
1428    }
1429    let (count_b64, body) = rest.split_at(2);
1430    let count = decode_count_b64(count_b64)?;
1431
1432    let mut out = Vec::with_capacity(count);
1433    let mut cursor = body;
1434    for _ in 0..count {
1435        let first = *cursor.as_bytes().first().ok_or_else(|| {
1436            AttachmentError::Decode("truncated group: expected another signature".into())
1437        })?;
1438        let hs = indexer_hard_len(first);
1439        if cursor.len() < hs {
1440            return Err(AttachmentError::Decode("truncated siger hard code".into()));
1441        }
1442        let code = &cursor[..hs];
1443        let (fs, os) = indexer_sizage(code)
1444            .ok_or_else(|| AttachmentError::Decode(format!("unknown indexer code {code:?}")))?;
1445        if cursor.len() < fs {
1446            return Err(AttachmentError::Decode(format!(
1447                "insufficient bytes for {code} siger: need {fs}, have {}",
1448                cursor.len()
1449            )));
1450        }
1451        let (siger_qb64, remainder) = cursor.split_at(fs);
1452        cursor = remainder;
1453
1454        let siger = Siger::new(None, None, None, None, None, None, Some(siger_qb64), None)
1455            .map_err(|e| AttachmentError::Decode(format!("Siger: {e}")))?;
1456        // A distinct prior-commitment index is carried only by dual-index codes
1457        // (os > 0); single-index codes report ondex == index, which is not a binding.
1458        let prior_index = (os > 0).then(|| siger.ondex());
1459
1460        out.push(IndexedSignature {
1461            index: siger.index(),
1462            prior_index,
1463            sig: siger.raw(),
1464        });
1465    }
1466
1467    Ok((out, cursor))
1468}
1469
1470/// Error shape for attachment encode/decode.
1471#[derive(Debug, thiserror::Error)]
1472#[non_exhaustive]
1473pub enum AttachmentError {
1474    /// Encoding an indexed signature into CESR failed.
1475    #[error("attachment encode: {0}")]
1476    Encode(String),
1477    /// Decoding a CESR attachment stream failed (bad counter, malformed Siger, etc.).
1478    #[error("attachment decode: {0}")]
1479    Decode(String),
1480}
1481
1482/// CESR base64url alphabet, ordered so `B64_ALPHA[n]` is the char for n.
1483const B64_ALPHA: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
1484
1485fn encode_count_b64(count: usize) -> Result<String, AttachmentError> {
1486    if count >= 64 * 64 {
1487        return Err(AttachmentError::Encode(format!(
1488            "count {count} exceeds 2-char base64url max (4095)"
1489        )));
1490    }
1491    let hi = B64_ALPHA[(count >> 6) & 0x3f] as char;
1492    let lo = B64_ALPHA[count & 0x3f] as char;
1493    Ok(format!("{hi}{lo}"))
1494}
1495
1496fn decode_count_b64(s: &str) -> Result<usize, AttachmentError> {
1497    let mut it = s.chars();
1498    let hi = it
1499        .next()
1500        .and_then(b64_index)
1501        .ok_or_else(|| AttachmentError::Decode(format!("invalid count hi char: {s:?}")))?;
1502    let lo = it
1503        .next()
1504        .and_then(b64_index)
1505        .ok_or_else(|| AttachmentError::Decode(format!("invalid count lo char: {s:?}")))?;
1506    Ok((hi << 6) | lo)
1507}
1508
1509fn b64_index(c: char) -> Option<usize> {
1510    B64_ALPHA.iter().position(|&b| b as char == c)
1511}
1512
1513#[cfg(test)]
1514#[allow(clippy::unwrap_used)]
1515mod tests {
1516    use super::*;
1517
1518    #[test]
1519    fn attachment_roundtrip_single_sig() {
1520        let sigs = vec![IndexedSignature {
1521            index: 0,
1522            prior_index: None,
1523            sig: vec![0x42u8; 64],
1524        }];
1525        let bytes = serialize_attachment(&sigs).unwrap();
1526        let s = std::str::from_utf8(&bytes).unwrap();
1527        assert!(s.starts_with("-AAB"), "expected -AAB prefix, got {s:?}");
1528        let back = parse_attachment(&bytes).unwrap();
1529        assert_eq!(back.len(), 1);
1530        assert_eq!(back[0].index, 0);
1531        assert_eq!(back[0].sig, vec![0x42u8; 64]);
1532    }
1533
1534    #[test]
1535    fn attachment_roundtrip_three_sigs() {
1536        let sigs = vec![
1537            IndexedSignature {
1538                index: 0,
1539                prior_index: None,
1540                sig: vec![0x01u8; 64],
1541            },
1542            IndexedSignature {
1543                index: 1,
1544                prior_index: None,
1545                sig: vec![0x02u8; 64],
1546            },
1547            IndexedSignature {
1548                index: 2,
1549                prior_index: None,
1550                sig: vec![0x03u8; 64],
1551            },
1552        ];
1553        let bytes = serialize_attachment(&sigs).unwrap();
1554        let s = std::str::from_utf8(&bytes).unwrap();
1555        assert!(s.starts_with("-AAD"), "expected -AAD prefix, got {s:?}");
1556        let back = parse_attachment(&bytes).unwrap();
1557        assert_eq!(back.len(), 3);
1558        for (i, sig) in back.iter().enumerate() {
1559            assert_eq!(sig.index, i as u32);
1560        }
1561    }
1562
1563    #[test]
1564    fn attachment_empty() {
1565        let bytes = serialize_attachment(&[]).unwrap();
1566        assert_eq!(bytes, b"-AAA");
1567        let back = parse_attachment(&bytes).unwrap();
1568        assert!(back.is_empty());
1569    }
1570
1571    #[test]
1572    fn keri_sequence_serializes_as_hex() {
1573        assert_eq!(
1574            serde_json::to_string(&KeriSequence::new(0)).unwrap(),
1575            "\"0\""
1576        );
1577        assert_eq!(
1578            serde_json::to_string(&KeriSequence::new(10)).unwrap(),
1579            "\"a\""
1580        );
1581        assert_eq!(
1582            serde_json::to_string(&KeriSequence::new(255)).unwrap(),
1583            "\"ff\""
1584        );
1585    }
1586
1587    #[test]
1588    fn keri_sequence_deserializes_from_hex() {
1589        let s: KeriSequence = serde_json::from_str("\"0\"").unwrap();
1590        assert_eq!(s.value(), 0);
1591        let s: KeriSequence = serde_json::from_str("\"a\"").unwrap();
1592        assert_eq!(s.value(), 10);
1593        let s: KeriSequence = serde_json::from_str("\"ff\"").unwrap();
1594        assert_eq!(s.value(), 255);
1595    }
1596
1597    #[test]
1598    fn keri_sequence_rejects_invalid_hex() {
1599        assert!(serde_json::from_str::<KeriSequence>("\"not_hex\"").is_err());
1600    }
1601
1602    #[test]
1603    fn icp_event_always_serializes_d_a_c() {
1604        use crate::types::{CesrKey, Threshold, VersionString};
1605        let icp = IcpEvent {
1606            v: VersionString::placeholder(),
1607            d: Said::default(),
1608            i: Prefix::new_unchecked("ETest123".to_string()),
1609            s: KeriSequence::new(0),
1610            kt: Threshold::Simple(1),
1611            k: vec![CesrKey::new_unchecked("DKey123".to_string())],
1612            nt: Threshold::Simple(1),
1613            n: vec![Said::new_unchecked("ENext456".to_string())],
1614            bt: Threshold::Simple(0),
1615            b: vec![],
1616            c: vec![],
1617            a: vec![],
1618        };
1619        let json = serde_json::to_string(&icp).unwrap();
1620        // d, a, c are always serialized (spec requires all fields)
1621        assert!(json.contains("\"d\":"), "d must always be present");
1622        assert!(json.contains("\"a\":"), "a must always be present");
1623        assert!(json.contains("\"c\":"), "c must always be present");
1624        // x is still conditionally omitted (empty)
1625        assert!(!json.contains("\"x\":"), "empty x must be omitted");
1626        assert!(json.contains("\"s\":\"0\""), "s must serialize as hex");
1627    }
1628
1629    #[test]
1630    fn event_enum_deserializes_by_t_field() {
1631        let json = r#"{"v":"KERI10JSON000000_","t":"icp","d":"ETestSaid","i":"E123","s":"0","kt":"1","k":["DKey"],"nt":"1","n":["ENext"],"bt":"0","b":[]}"#;
1632        let event: Event = serde_json::from_str(json).unwrap();
1633        assert!(event.is_inception());
1634        assert_eq!(event.sequence().value(), 0);
1635    }
1636
1637    #[test]
1638    fn digest_seal_roundtrips() {
1639        let seal = Seal::digest("EDigest123");
1640        let json = serde_json::to_string(&seal).unwrap();
1641        assert_eq!(json, r#"{"d":"EDigest123"}"#);
1642        let parsed: Seal = serde_json::from_str(&json).unwrap();
1643        assert_eq!(seal, parsed);
1644    }
1645
1646    #[test]
1647    fn seal_event_location_roundtrips() {
1648        // A.8 (F-36): the KERI §7 event-location seal {i,s,p,t,d} round-trips.
1649        let seal = Seal::EventLocation {
1650            i: Prefix::new_unchecked("EPrefix".to_string()),
1651            s: KeriSequence::new(3),
1652            p: Said::new_unchecked("EPrior".to_string()),
1653            t: "ixn".to_string(),
1654            d: Said::new_unchecked("EDigest".to_string()),
1655        };
1656        let json = serde_json::to_string(&seal).unwrap();
1657        let parsed: Seal = serde_json::from_str(&json).unwrap();
1658        assert_eq!(seal, parsed);
1659        assert_eq!(parsed.digest_value().map(|d| d.as_str()), Some("EDigest"));
1660    }
1661
1662    #[test]
1663    fn seal_rejects_malformed_i_s() {
1664        // A.8 (F-37): a malformed {i,s} seal (no d) must error, not silently
1665        // collapse to a LatestEstablishment {i} that drops `s`.
1666        let r: Result<Seal, _> = serde_json::from_str(r#"{"i":"EPrefix","s":"3"}"#);
1667        assert!(r.is_err(), "malformed {{i,s}} seal must be rejected");
1668        // and an entirely unknown field set errors too.
1669        let r2: Result<Seal, _> = serde_json::from_str(r#"{"zz":"x"}"#);
1670        assert!(r2.is_err());
1671    }
1672
1673    #[test]
1674    fn key_event_seal_roundtrips() {
1675        let seal = Seal::key_event(
1676            Prefix::new_unchecked("EPrefix".to_string()),
1677            KeriSequence::new(1),
1678            Said::new_unchecked("ESaid".to_string()),
1679        );
1680        let json = serde_json::to_string(&seal).unwrap();
1681        let parsed: Seal = serde_json::from_str(&json).unwrap();
1682        assert_eq!(seal, parsed);
1683    }
1684
1685    #[test]
1686    fn indexed_signature_serde_roundtrip() {
1687        let sig = IndexedSignature {
1688            index: 2,
1689            prior_index: None,
1690            sig: vec![0xab; 64],
1691        };
1692        let json = serde_json::to_string(&sig).unwrap();
1693        assert!(json.contains("\"index\":2"));
1694        assert!(
1695            !json.contains("prior_index"),
1696            "a None prior_index must be omitted from the wire: {json}"
1697        );
1698        let parsed: IndexedSignature = serde_json::from_str(&json).unwrap();
1699        assert_eq!(parsed, sig);
1700    }
1701
1702    #[test]
1703    fn signed_event_accessors() {
1704        use crate::types::{CesrKey, Threshold, VersionString};
1705        let icp = IcpEvent {
1706            v: VersionString::placeholder(),
1707            d: Said::new_unchecked("ESAID123".to_string()),
1708            i: Prefix::new_unchecked("EPrefix".to_string()),
1709            s: KeriSequence::new(0),
1710            kt: Threshold::Simple(1),
1711            k: vec![CesrKey::new_unchecked("DKey".to_string())],
1712            nt: Threshold::Simple(1),
1713            n: vec![Said::new_unchecked("ENext".to_string())],
1714            bt: Threshold::Simple(0),
1715            b: vec![],
1716            c: vec![],
1717            a: vec![],
1718        };
1719        let signed = SignedEvent::new(
1720            Event::Icp(icp),
1721            vec![IndexedSignature {
1722                index: 0,
1723                prior_index: None,
1724                sig: vec![0u8; 64],
1725            }],
1726        );
1727        assert_eq!(signed.said().as_str(), "ESAID123");
1728        assert_eq!(signed.sequence().value(), 0);
1729        assert_eq!(signed.prefix().as_str(), "EPrefix");
1730        assert_eq!(signed.signatures.len(), 1);
1731        assert_eq!(signed.signatures[0].index, 0);
1732    }
1733
1734    #[test]
1735    fn seal_digest_value() {
1736        let seal = Seal::digest("ETest123");
1737        assert_eq!(seal.digest_value().unwrap().as_str(), "ETest123");
1738        let latest = Seal::LatestEstablishment {
1739            i: Prefix::new_unchecked("EPrefix".to_string()),
1740        };
1741        assert!(latest.digest_value().is_none());
1742    }
1743}