Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T04:13:05.963Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

issue31_nostr.rs

5065 lines · 197.5 KB · rust
1use std::collections::{BTreeMap, BTreeSet};
2
3use serde::{Deserialize, Serialize};
4use sha2::{Digest, Sha256};
5use thiserror::Error;
6
7pub const ISSUE31_HOST_DISCOVERY_SCHEMA: &str = "openagents.omega.issue31.host_discovery.v1";
8pub const ISSUE31_HOST_DISCOVERY_SCHEMA_V2: &str = "openagents.omega.issue31.host_discovery.v2";
9pub const ISSUE31_PAIRING_SCHEMA: &str = "openagents.omega.issue31.pairing.v1";
10pub const ISSUE31_COMMAND_SCHEMA: &str = "openagents.omega.issue31.command.v1";
11pub const ISSUE31_COMMAND_SCHEMA_V2: &str = "openagents.omega.issue31.command.v2";
12pub const ISSUE31_OWNER_PROJECTION_SCHEMA: &str = "openagents.omega.issue31.owner_projection.v1";
13pub const ISSUE31_WITHHELD_SOURCES_SCHEMA: &str = "openagents.omega.issue31.withheld_sources.v1";
14/// The omega#47 host snapshot, as an owner-private record (omega#49).
15pub const ISSUE31_HOST_ADJUNCT_SCHEMA: &str = "openagents.omega.issue31.host.v1";
16/// The omega#47 Full Auto detail projection, as an owner-private record.
17pub const ISSUE31_FULL_AUTO_ADJUNCT_SCHEMA: &str = "openagents.omega.issue31.fullauto.v1";
18pub const ISSUE31_HOST_ADJUNCT_RECORD_TYPE: &str = "host_snapshot";
19pub const ISSUE31_FULL_AUTO_ADJUNCT_RECORD_TYPE: &str = "full_auto_detail";
20/// The fields the host pump adds when it addresses an adjunct to a device.
21///
22/// An omega#47 adjunct describes a host; on its own it names neither the key
23/// that signed it nor the device it is for, so the device's envelope check has
24/// nothing to compare the seal author and the gift wrap recipient against. The
25/// pump states all five together or the record is not delivered at all.
26pub const ISSUE31_ADJUNCT_DELIVERY_KEYS: [&str; 5] = [
27    "recordType",
28    "hostPublicKeyHex",
29    "devicePublicKeyHex",
30    "grantRef",
31    "expectedGeneration",
32];
33pub const ISSUE31_HOST_DISCOVERY_KIND: u16 = 31_990;
34pub const ISSUE31_PRIVATE_RUMOR_KIND: u16 = 14;
35pub const ISSUE31_PRIVATE_SEAL_KIND: u16 = 13;
36pub const ISSUE31_PRIVATE_GIFT_WRAP_KIND: u16 = 1_059;
37pub const ISSUE31_ACTION_SEND_MESSAGE: &str = "action.issue31.sarah.send";
38pub const ISSUE31_ACTION_INTERRUPT_TURN: &str = "action.issue31.sarah.interrupt";
39pub const ISSUE31_ACTION_ADVANCE_READ_STATE: &str = "action.issue31.read_state.advance";
40pub const ISSUE31_ACTION_CREATE_REMINDER: &str = "action.issue31.reminder.create";
41pub const ISSUE31_ACTION_CHANGE_REMINDER: &str = "action.issue31.reminder.change";
42pub const ISSUE31_ACTION_COMPLETE_REMINDER: &str = "action.issue31.reminder.complete";
43pub const ISSUE31_ACTION_CANCEL_REMINDER: &str = "action.issue31.reminder.cancel";
44pub const SARAH_TURN_RECORD_KIND: u16 = 44_300;
45pub const SARAH_AUTHORITY_RECEIPT_KIND: u16 = 44_301;
46pub const SARAH_ENGRAM_KIND: u16 = 30_174;
47pub const SARAH_READ_STATE_KIND: u16 = 30_078;
48pub const SARAH_REMINDER_KIND: u16 = 30_300;
49
50#[derive(Debug, Error, PartialEq, Eq)]
51pub enum Issue31NostrError {
52    #[error("invalid Issue 31 record: {0}")]
53    Invalid(String),
54    #[error("Issue 31 record decoding failed: {0}")]
55    Decode(String),
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
59#[serde(rename_all = "camelCase", deny_unknown_fields)]
60pub struct Issue31HostDiscovery {
61    pub schema: String,
62    pub host_ref: String,
63    pub host_public_key_hex: String,
64    pub sarah_public_key_hex: String,
65    pub display_name: String,
66    pub protocols: Vec<String>,
67    pub relay_urls: Vec<String>,
68    pub generation: u64,
69    pub issued_at: u64,
70    pub expires_at: u64,
71}
72
73impl Issue31HostDiscovery {
74    pub fn decode(bytes: &[u8]) -> Result<Self, Issue31NostrError> {
75        if bytes.len() > 64 * 1024 {
76            return Err(Issue31NostrError::Invalid(
77                "host discovery exceeds the record budget".into(),
78            ));
79        }
80        let record: Self = serde_json::from_slice(bytes)
81            .map_err(|error| Issue31NostrError::Decode(error.to_string()))?;
82        record.validate()?;
83        Ok(record)
84    }
85
86    pub fn validate(&self) -> Result<(), Issue31NostrError> {
87        if self.schema != ISSUE31_HOST_DISCOVERY_SCHEMA
88            || !valid_ref(&self.host_ref)
89            || !valid_hex64(&self.host_public_key_hex)
90            || !valid_hex64(&self.sarah_public_key_hex)
91            || self.sarah_public_key_hex == self.host_public_key_hex
92            || self.display_name.is_empty()
93            || self.display_name.len() > 80
94            || self.generation == 0
95            || self.expires_at <= self.issued_at
96            || self.protocols.len() != 2
97            || !all_unique(&self.protocols)
98            || !self
99                .protocols
100                .iter()
101                .any(|protocol| protocol == ISSUE31_PAIRING_SCHEMA)
102            || !self
103                .protocols
104                .iter()
105                .any(|protocol| protocol == ISSUE31_COMMAND_SCHEMA)
106            || self.relay_urls.is_empty()
107            || self.relay_urls.len() > 8
108            || !all_unique(&self.relay_urls)
109            || self
110                .relay_urls
111                .iter()
112                .any(|relay_url| !valid_relay_url(relay_url))
113        {
114            return Err(Issue31NostrError::Invalid(
115                "host discovery failed its schema, identity, protocol, relay, or lifetime law"
116                    .into(),
117            ));
118        }
119        Ok(())
120    }
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
124#[serde(rename_all = "camelCase", deny_unknown_fields)]
125pub struct Issue31HostDiscoveryV2 {
126    pub schema: String,
127    pub host_ref: String,
128    pub host_public_key_hex: String,
129    pub sarah_public_key_hex: String,
130    pub conversation: String,
131    pub display_name: String,
132    pub protocols: Vec<String>,
133    pub relay_urls: Vec<String>,
134    pub generation: u64,
135    pub issued_at: u64,
136    pub expires_at: u64,
137}
138
139impl Issue31HostDiscoveryV2 {
140    pub fn decode(bytes: &[u8]) -> Result<Self, Issue31NostrError> {
141        if bytes.len() > 64 * 1024 {
142            return Err(Issue31NostrError::Invalid(
143                "host discovery exceeds the record budget".into(),
144            ));
145        }
146        let record: Self = serde_json::from_slice(bytes)
147            .map_err(|error| Issue31NostrError::Decode(error.to_string()))?;
148        record.validate()?;
149        Ok(record)
150    }
151
152    pub fn validate(&self) -> Result<(), Issue31NostrError> {
153        if self.schema != ISSUE31_HOST_DISCOVERY_SCHEMA_V2
154            || !valid_ref(&self.host_ref)
155            || !valid_hex64(&self.host_public_key_hex)
156            || !valid_hex64(&self.sarah_public_key_hex)
157            || self.sarah_public_key_hex == self.host_public_key_hex
158            || !valid_conversation_tag(&self.conversation)
159            || self.display_name.is_empty()
160            || self.display_name.len() > 80
161            || self.generation == 0
162            || self.expires_at <= self.issued_at
163            || self.protocols.len() != 3
164            || !all_unique(&self.protocols)
165            || !self
166                .protocols
167                .iter()
168                .any(|protocol| protocol == ISSUE31_PAIRING_SCHEMA)
169            || !self
170                .protocols
171                .iter()
172                .any(|protocol| protocol == ISSUE31_COMMAND_SCHEMA)
173            || !self
174                .protocols
175                .iter()
176                .any(|protocol| protocol == ISSUE31_COMMAND_SCHEMA_V2)
177            || self.relay_urls.is_empty()
178            || self.relay_urls.len() > 8
179            || !all_unique(&self.relay_urls)
180            || self
181                .relay_urls
182                .iter()
183                .any(|relay_url| !valid_relay_url(relay_url))
184        {
185            return Err(Issue31NostrError::Invalid(
186                "v2 host discovery failed its schema, identity, protocol, relay, or lifetime law"
187                    .into(),
188            ));
189        }
190        Ok(())
191    }
192}
193
194#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
195#[serde(rename_all = "snake_case")]
196pub enum Issue31PairingScope {
197    ObserveIssue31,
198    SendMessage,
199    InterruptTurn,
200    ControlFullAuto,
201    RequestProviderHandoff,
202    ActInCommunity,
203}
204
205impl Issue31PairingScope {
206    pub fn parse(value: &str) -> Result<Self, Issue31NostrError> {
207        match value {
208            "observe_issue31" => Ok(Self::ObserveIssue31),
209            "send_message" => Ok(Self::SendMessage),
210            "interrupt_turn" => Ok(Self::InterruptTurn),
211            "control_full_auto" => Ok(Self::ControlFullAuto),
212            "request_provider_handoff" => Ok(Self::RequestProviderHandoff),
213            "act_in_community" => Ok(Self::ActInCommunity),
214            _ => Err(Issue31NostrError::Invalid(
215                "unknown Issue 31 pairing scope".into(),
216            )),
217        }
218    }
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
222#[serde(tag = "recordType", rename_all = "snake_case", deny_unknown_fields)]
223pub enum Issue31PairingRecord {
224    PairingRequest {
225        schema: String,
226        #[serde(rename = "hostRef")]
227        host_ref: String,
228        #[serde(rename = "hostPublicKeyHex")]
229        host_public_key_hex: String,
230        #[serde(rename = "devicePublicKeyHex")]
231        device_public_key_hex: String,
232        #[serde(rename = "issuedAt")]
233        issued_at: u64,
234        #[serde(rename = "pairingRequestRef")]
235        pairing_request_ref: String,
236        #[serde(rename = "requestedScopes")]
237        requested_scopes: Vec<Issue31PairingScope>,
238        #[serde(rename = "expiresAt")]
239        expires_at: u64,
240    },
241    PairingChallenge {
242        schema: String,
243        #[serde(rename = "hostRef")]
244        host_ref: String,
245        #[serde(rename = "hostPublicKeyHex")]
246        host_public_key_hex: String,
247        #[serde(rename = "devicePublicKeyHex")]
248        device_public_key_hex: String,
249        #[serde(rename = "issuedAt")]
250        issued_at: u64,
251        #[serde(rename = "pairingChallengeRef")]
252        pairing_challenge_ref: String,
253        #[serde(rename = "pairingRequestEventId")]
254        pairing_request_event_id: String,
255        challenge: String,
256        #[serde(rename = "expiresAt")]
257        expires_at: u64,
258    },
259    PairingResponse {
260        schema: String,
261        #[serde(rename = "hostRef")]
262        host_ref: String,
263        #[serde(rename = "hostPublicKeyHex")]
264        host_public_key_hex: String,
265        #[serde(rename = "devicePublicKeyHex")]
266        device_public_key_hex: String,
267        #[serde(rename = "issuedAt")]
268        issued_at: u64,
269        #[serde(rename = "pairingResponseRef")]
270        pairing_response_ref: String,
271        #[serde(rename = "pairingChallengeEventId")]
272        pairing_challenge_event_id: String,
273        challenge: String,
274        #[serde(rename = "expiresAt")]
275        expires_at: u64,
276    },
277    ScopedGrant {
278        schema: String,
279        #[serde(rename = "hostRef")]
280        host_ref: String,
281        #[serde(rename = "hostPublicKeyHex")]
282        host_public_key_hex: String,
283        #[serde(rename = "sarahPublicKeyHex")]
284        sarah_public_key_hex: String,
285        #[serde(rename = "devicePublicKeyHex")]
286        device_public_key_hex: String,
287        #[serde(rename = "issuedAt")]
288        issued_at: u64,
289        #[serde(rename = "pairingResponseEventId")]
290        pairing_response_event_id: String,
291        #[serde(rename = "grantRef")]
292        grant_ref: String,
293        generation: u64,
294        scopes: Vec<Issue31PairingScope>,
295        #[serde(rename = "expiresAt")]
296        expires_at: u64,
297    },
298    GrantRenewal {
299        schema: String,
300        #[serde(rename = "hostRef")]
301        host_ref: String,
302        #[serde(rename = "hostPublicKeyHex")]
303        host_public_key_hex: String,
304        #[serde(rename = "sarahPublicKeyHex")]
305        sarah_public_key_hex: String,
306        #[serde(rename = "devicePublicKeyHex")]
307        device_public_key_hex: String,
308        #[serde(rename = "issuedAt")]
309        issued_at: u64,
310        #[serde(rename = "grantRef")]
311        grant_ref: String,
312        #[serde(rename = "previousGrantEventId")]
313        previous_grant_event_id: String,
314        #[serde(rename = "priorGeneration")]
315        prior_generation: u64,
316        generation: u64,
317        scopes: Vec<Issue31PairingScope>,
318        #[serde(rename = "expiresAt")]
319        expires_at: u64,
320    },
321    GrantRevocation {
322        schema: String,
323        #[serde(rename = "hostRef")]
324        host_ref: String,
325        #[serde(rename = "hostPublicKeyHex")]
326        host_public_key_hex: String,
327        #[serde(rename = "sarahPublicKeyHex")]
328        sarah_public_key_hex: String,
329        #[serde(rename = "devicePublicKeyHex")]
330        device_public_key_hex: String,
331        #[serde(rename = "issuedAt")]
332        issued_at: u64,
333        #[serde(rename = "grantRef")]
334        grant_ref: String,
335        generation: u64,
336        #[serde(rename = "reasonRef", default, skip_serializing_if = "Option::is_none")]
337        reason_ref: Option<String>,
338    },
339}
340
341impl Issue31PairingRecord {
342    pub fn decode(bytes: &[u8]) -> Result<Self, Issue31NostrError> {
343        if bytes.len() > 64 * 1024 {
344            return Err(Issue31NostrError::Invalid(
345                "pairing record exceeds the record budget".into(),
346            ));
347        }
348        let record: Self = serde_json::from_slice(bytes)
349            .map_err(|error| Issue31NostrError::Decode(error.to_string()))?;
350        record.validate()?;
351        Ok(record)
352    }
353
354    pub fn validate(&self) -> Result<(), Issue31NostrError> {
355        let (schema, host_ref, host_key, device_key, issued_at) = self.base();
356        if schema != ISSUE31_PAIRING_SCHEMA
357            || !valid_ref(host_ref)
358            || !valid_hex64(host_key)
359            || !valid_hex64(device_key)
360        {
361            return Err(Issue31NostrError::Invalid(
362                "invalid pairing identity".into(),
363            ));
364        }
365        match self {
366            Self::PairingRequest {
367                pairing_request_ref,
368                requested_scopes,
369                expires_at,
370                ..
371            } => validate_scopes(requested_scopes, "requested scopes")
372                .and_then(|()| validate_ref_value(pairing_request_ref, "pairing request"))
373                .and_then(|()| validate_lifetime(issued_at, *expires_at)),
374            Self::PairingChallenge {
375                pairing_challenge_ref,
376                pairing_request_event_id,
377                challenge,
378                expires_at,
379                ..
380            } => validate_ref_value(pairing_challenge_ref, "pairing challenge")
381                .and_then(|()| {
382                    validate_hex_value(pairing_request_event_id, "pairing request event")
383                })
384                .and_then(|()| validate_hex_value(challenge, "pairing challenge nonce"))
385                .and_then(|()| validate_lifetime(issued_at, *expires_at)),
386            Self::PairingResponse {
387                pairing_response_ref,
388                pairing_challenge_event_id,
389                challenge,
390                expires_at,
391                ..
392            } => validate_ref_value(pairing_response_ref, "pairing response")
393                .and_then(|()| validate_hex_value(pairing_challenge_event_id, "challenge event"))
394                .and_then(|()| validate_hex_value(challenge, "pairing challenge nonce"))
395                .and_then(|()| validate_lifetime(issued_at, *expires_at)),
396            Self::ScopedGrant {
397                sarah_public_key_hex,
398                pairing_response_event_id,
399                grant_ref,
400                generation,
401                scopes,
402                expires_at,
403                ..
404            } => validate_sarah_binding(host_key, sarah_public_key_hex)
405                .and_then(|()| {
406                    validate_hex_value(pairing_response_event_id, "pairing response event")
407                })
408                .and_then(|()| validate_ref_value(grant_ref, "grant"))
409                .and_then(|()| validate_generation(*generation))
410                .and_then(|()| validate_scopes(scopes, "grant scopes"))
411                .and_then(|()| validate_lifetime(issued_at, *expires_at)),
412            Self::GrantRenewal {
413                sarah_public_key_hex,
414                grant_ref,
415                previous_grant_event_id,
416                prior_generation,
417                generation,
418                scopes,
419                expires_at,
420                ..
421            } => validate_sarah_binding(host_key, sarah_public_key_hex)
422                .and_then(|()| validate_ref_value(grant_ref, "grant"))
423                .and_then(|()| validate_hex_value(previous_grant_event_id, "previous grant event"))
424                .and_then(|()| validate_generation(*prior_generation))
425                .and_then(|()| {
426                    if *generation == prior_generation.saturating_add(1) {
427                        Ok(())
428                    } else {
429                        Err(Issue31NostrError::Invalid(
430                            "grant renewal must advance exactly one generation".into(),
431                        ))
432                    }
433                })
434                .and_then(|()| validate_scopes(scopes, "grant scopes"))
435                .and_then(|()| validate_lifetime(issued_at, *expires_at)),
436            Self::GrantRevocation {
437                sarah_public_key_hex,
438                grant_ref,
439                generation,
440                reason_ref,
441                ..
442            } => validate_sarah_binding(host_key, sarah_public_key_hex)
443                .and_then(|()| validate_ref_value(grant_ref, "grant"))
444                .and_then(|()| validate_generation(*generation))
445                .and_then(|()| match reason_ref {
446                    Some(reason_ref) => validate_ref_value(reason_ref, "revocation reason"),
447                    None => Ok(()),
448                }),
449        }
450    }
451
452    fn base(&self) -> (&str, &str, &str, &str, u64) {
453        match self {
454            Self::PairingRequest {
455                schema,
456                host_ref,
457                host_public_key_hex,
458                device_public_key_hex,
459                issued_at,
460                ..
461            }
462            | Self::PairingChallenge {
463                schema,
464                host_ref,
465                host_public_key_hex,
466                device_public_key_hex,
467                issued_at,
468                ..
469            }
470            | Self::PairingResponse {
471                schema,
472                host_ref,
473                host_public_key_hex,
474                device_public_key_hex,
475                issued_at,
476                ..
477            }
478            | Self::ScopedGrant {
479                schema,
480                host_ref,
481                host_public_key_hex,
482                device_public_key_hex,
483                issued_at,
484                ..
485            }
486            | Self::GrantRenewal {
487                schema,
488                host_ref,
489                host_public_key_hex,
490                device_public_key_hex,
491                issued_at,
492                ..
493            }
494            | Self::GrantRevocation {
495                schema,
496                host_ref,
497                host_public_key_hex,
498                device_public_key_hex,
499                issued_at,
500                ..
501            } => (
502                schema,
503                host_ref,
504                host_public_key_hex,
505                device_public_key_hex,
506                *issued_at,
507            ),
508        }
509    }
510
511    fn lifecycle_binding(&self) -> Option<(&str, &str, &str, &str, &str, u64, u64)> {
512        match self {
513            Self::ScopedGrant {
514                host_ref,
515                host_public_key_hex,
516                sarah_public_key_hex,
517                device_public_key_hex,
518                issued_at,
519                grant_ref,
520                generation,
521                ..
522            }
523            | Self::GrantRenewal {
524                host_ref,
525                host_public_key_hex,
526                sarah_public_key_hex,
527                device_public_key_hex,
528                issued_at,
529                grant_ref,
530                generation,
531                ..
532            }
533            | Self::GrantRevocation {
534                host_ref,
535                host_public_key_hex,
536                sarah_public_key_hex,
537                device_public_key_hex,
538                issued_at,
539                grant_ref,
540                generation,
541                ..
542            } => Some((
543                host_ref,
544                host_public_key_hex,
545                sarah_public_key_hex,
546                device_public_key_hex,
547                grant_ref,
548                *generation,
549                *issued_at,
550            )),
551            Self::PairingRequest { .. }
552            | Self::PairingChallenge { .. }
553            | Self::PairingResponse { .. } => None,
554        }
555    }
556
557    pub fn validate_private_binding(
558        &self,
559        sender_public_key_hex: &str,
560        recipient_public_key_hex: &str,
561    ) -> Result<(), Issue31NostrError> {
562        self.validate()?;
563        let (_, _, host_key, device_key, _) = self.base();
564        let device_authored = matches!(
565            self,
566            Self::PairingRequest { .. } | Self::PairingResponse { .. }
567        );
568        let expected_sender = if device_authored {
569            device_key
570        } else {
571            host_key
572        };
573        let expected_recipient = if device_authored {
574            host_key
575        } else {
576            device_key
577        };
578        if sender_public_key_hex != expected_sender
579            || recipient_public_key_hex != expected_recipient
580        {
581            return Err(Issue31NostrError::Invalid(
582                "pairing record signer or recipient does not match its binding".into(),
583            ));
584        }
585        Ok(())
586    }
587
588    pub fn device_public_key_hex(&self) -> &str {
589        let (_, _, _, device_public_key_hex, _) = self.base();
590        device_public_key_hex
591    }
592}
593
594#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
595pub struct Issue31PairingEvent {
596    pub event_id: String,
597    pub record: Issue31PairingRecord,
598}
599
600#[derive(Debug, Clone, Copy, PartialEq, Eq)]
601pub enum Issue31GrantStatus {
602    Active,
603    Revoked,
604}
605
606#[derive(Debug, Clone, PartialEq, Eq)]
607pub struct Issue31GrantState {
608    pub grant_ref: String,
609    pub host_ref: String,
610    pub host_public_key_hex: String,
611    pub sarah_public_key_hex: String,
612    pub device_public_key_hex: String,
613    pub generation: u64,
614    pub status: Issue31GrantStatus,
615    pub scopes: Vec<Issue31PairingScope>,
616    pub expires_at: Option<u64>,
617    pub issued_at: u64,
618    pub source_event_id: String,
619}
620
621#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
622#[serde(rename_all = "camelCase", deny_unknown_fields)]
623pub struct Issue31GrantProjection {
624    pub grant_ref: String,
625    pub device_fingerprint: String,
626    pub generation: u64,
627    pub status: String,
628    pub scopes: Vec<Issue31PairingScope>,
629    pub expires_at: Option<u64>,
630    pub source_event_id: String,
631}
632
633pub fn fold_issue31_grant(
634    events: &[Issue31PairingEvent],
635    grant_ref: &str,
636) -> Result<Option<Issue31GrantState>, Issue31NostrError> {
637    validate_ref_value(grant_ref, "grant")?;
638    let mut unique_events: BTreeMap<&str, &Issue31PairingRecord> = BTreeMap::new();
639    for event in events {
640        if !valid_hex64(&event.event_id) {
641            return Err(Issue31NostrError::Invalid(
642                "invalid pairing event id".into(),
643            ));
644        }
645        event.record.validate()?;
646        if let Some(prior) = unique_events.get(event.event_id.as_str()) {
647            if **prior != event.record {
648                return Err(Issue31NostrError::Invalid(format!(
649                    "Issue 31 event {} has conflicting records",
650                    event.event_id
651                )));
652            }
653        } else {
654            unique_events.insert(&event.event_id, &event.record);
655        }
656    }
657
658    let mut candidates: Vec<(&str, &Issue31PairingRecord)> = unique_events
659        .iter()
660        .map(|(event_id, record)| (*event_id, *record))
661        .filter(|(_, record)| {
662            record
663                .lifecycle_binding()
664                .is_some_and(|(_, _, _, _, candidate_grant_ref, _, _)| {
665                    candidate_grant_ref == grant_ref
666                })
667        })
668        .collect();
669    candidates.sort_by(|(left_id, left), (right_id, right)| {
670        let left_generation = left
671            .lifecycle_binding()
672            .map(|(_, _, _, _, _, generation, _)| generation)
673            .unwrap_or_default();
674        let right_generation = right
675            .lifecycle_binding()
676            .map(|(_, _, _, _, _, generation, _)| generation)
677            .unwrap_or_default();
678        (left_generation, *left_id).cmp(&(right_generation, *right_id))
679    });
680
681    let Some((_, identity_record)) = candidates.first() else {
682        return Ok(None);
683    };
684    let (identity_host_ref, identity_host_key, identity_sarah_key, identity_device_key, _, _, _) =
685        identity_record
686            .lifecycle_binding()
687            .ok_or_else(|| Issue31NostrError::Invalid("invalid grant lifecycle record".into()))?;
688    if candidates.iter().any(|(_, record)| {
689        record.lifecycle_binding().is_some_and(
690            |(host_ref, host_key, sarah_key, device_key, _, _, _)| {
691                host_ref != identity_host_ref
692                    || host_key != identity_host_key
693                    || sarah_key != identity_sarah_key
694                    || device_key != identity_device_key
695            },
696        )
697    }) {
698        return Err(Issue31NostrError::Invalid(format!(
699            "Issue 31 grant {grant_ref} has an identity fork"
700        )));
701    }
702
703    if let Some((
704        event_id,
705        Issue31PairingRecord::GrantRevocation {
706            host_ref,
707            host_public_key_hex,
708            sarah_public_key_hex,
709            device_public_key_hex,
710            issued_at,
711            generation,
712            ..
713        },
714    )) = candidates
715        .iter()
716        .rev()
717        .find(|(_, record)| matches!(record, Issue31PairingRecord::GrantRevocation { .. }))
718        .copied()
719    {
720        return Ok(Some(Issue31GrantState {
721            grant_ref: grant_ref.to_string(),
722            host_ref: host_ref.clone(),
723            host_public_key_hex: host_public_key_hex.clone(),
724            sarah_public_key_hex: sarah_public_key_hex.clone(),
725            device_public_key_hex: device_public_key_hex.clone(),
726            generation: *generation,
727            status: Issue31GrantStatus::Revoked,
728            scopes: Vec::new(),
729            expires_at: None,
730            issued_at: *issued_at,
731            source_event_id: event_id.to_string(),
732        }));
733    }
734
735    let mut records_by_generation: BTreeMap<u64, Vec<&Issue31PairingRecord>> = BTreeMap::new();
736    for (_, record) in &candidates {
737        let generation = record
738            .lifecycle_binding()
739            .map(|(_, _, _, _, _, generation, _)| generation)
740            .ok_or_else(|| Issue31NostrError::Invalid("invalid grant lifecycle record".into()))?;
741        records_by_generation
742            .entry(generation)
743            .or_default()
744            .push(record);
745    }
746    for (generation, records) in records_by_generation {
747        if records
748            .first()
749            .is_some_and(|first| records.iter().skip(1).any(|record| *record != *first))
750        {
751            return Err(Issue31NostrError::Invalid(format!(
752                "Issue 31 grant {grant_ref} forks at generation {generation}"
753            )));
754        }
755    }
756
757    let mut state: Option<Issue31GrantState> = None;
758    for (event_id, record) in candidates {
759        match (state.as_ref(), record) {
760            (
761                None,
762                Issue31PairingRecord::ScopedGrant {
763                    host_ref,
764                    host_public_key_hex,
765                    sarah_public_key_hex,
766                    device_public_key_hex,
767                    issued_at,
768                    generation,
769                    scopes,
770                    expires_at,
771                    ..
772                },
773            ) => {
774                validate_scoped_grant_pairing_chain(record, &unique_events)?;
775                state = Some(Issue31GrantState {
776                    grant_ref: grant_ref.to_string(),
777                    host_ref: host_ref.clone(),
778                    host_public_key_hex: host_public_key_hex.clone(),
779                    sarah_public_key_hex: sarah_public_key_hex.clone(),
780                    device_public_key_hex: device_public_key_hex.clone(),
781                    generation: *generation,
782                    status: Issue31GrantStatus::Active,
783                    scopes: scopes.clone(),
784                    expires_at: Some(*expires_at),
785                    issued_at: *issued_at,
786                    source_event_id: event_id.to_string(),
787                });
788            }
789            (
790                Some(prior),
791                Issue31PairingRecord::GrantRenewal {
792                    host_ref,
793                    host_public_key_hex,
794                    sarah_public_key_hex,
795                    device_public_key_hex,
796                    issued_at,
797                    previous_grant_event_id,
798                    prior_generation,
799                    generation,
800                    scopes,
801                    expires_at,
802                    ..
803                },
804            ) if previous_grant_event_id == &prior.source_event_id
805                && *prior_generation == prior.generation
806                && *generation == prior.generation.saturating_add(1)
807                && host_ref == &prior.host_ref
808                && host_public_key_hex == &prior.host_public_key_hex
809                && sarah_public_key_hex == &prior.sarah_public_key_hex
810                && device_public_key_hex == &prior.device_public_key_hex =>
811            {
812                state = Some(Issue31GrantState {
813                    grant_ref: grant_ref.to_string(),
814                    host_ref: host_ref.clone(),
815                    host_public_key_hex: host_public_key_hex.clone(),
816                    sarah_public_key_hex: sarah_public_key_hex.clone(),
817                    device_public_key_hex: device_public_key_hex.clone(),
818                    generation: *generation,
819                    status: Issue31GrantStatus::Active,
820                    scopes: scopes.clone(),
821                    expires_at: Some(*expires_at),
822                    issued_at: *issued_at,
823                    source_event_id: event_id.to_string(),
824                });
825            }
826            (None, Issue31PairingRecord::GrantRenewal { .. }) => {
827                return Err(Issue31NostrError::Invalid(format!(
828                    "Issue 31 grant {grant_ref} renewal has no initial grant"
829                )));
830            }
831            (Some(_), Issue31PairingRecord::GrantRenewal { .. }) => {
832                return Err(Issue31NostrError::Invalid(format!(
833                    "Issue 31 grant {grant_ref} renewal lineage is invalid"
834                )));
835            }
836            (Some(_), Issue31PairingRecord::ScopedGrant { .. }) => {
837                return Err(Issue31NostrError::Invalid(format!(
838                    "Issue 31 grant {grant_ref} has more than one initial grant"
839                )));
840            }
841            (_, Issue31PairingRecord::GrantRevocation { .. }) => {}
842            (
843                _,
844                Issue31PairingRecord::PairingRequest { .. }
845                | Issue31PairingRecord::PairingChallenge { .. }
846                | Issue31PairingRecord::PairingResponse { .. },
847            ) => {
848                return Err(Issue31NostrError::Invalid(
849                    "non-lifecycle pairing record entered grant projection".into(),
850                ));
851            }
852        }
853    }
854    Ok(state)
855}
856
857fn validate_scoped_grant_pairing_chain(
858    grant: &Issue31PairingRecord,
859    records_by_event_id: &BTreeMap<&str, &Issue31PairingRecord>,
860) -> Result<(), Issue31NostrError> {
861    let Issue31PairingRecord::ScopedGrant {
862        host_ref,
863        host_public_key_hex,
864        device_public_key_hex,
865        issued_at: grant_issued_at,
866        pairing_response_event_id,
867        scopes,
868        ..
869    } = grant
870    else {
871        return Err(Issue31NostrError::Invalid(
872            "pairing-chain validation requires a scoped grant".into(),
873        ));
874    };
875    let Some(Issue31PairingRecord::PairingResponse {
876        host_ref: response_host_ref,
877        host_public_key_hex: response_host_key,
878        device_public_key_hex: response_device_key,
879        issued_at: response_issued_at,
880        pairing_challenge_event_id,
881        challenge: response_challenge,
882        ..
883    }) = records_by_event_id
884        .get(pairing_response_event_id.as_str())
885        .copied()
886    else {
887        return Err(Issue31NostrError::Invalid(
888            "Issue 31 scoped grant has no pairing response".into(),
889        ));
890    };
891    let Some(Issue31PairingRecord::PairingChallenge {
892        host_ref: challenge_host_ref,
893        host_public_key_hex: challenge_host_key,
894        device_public_key_hex: challenge_device_key,
895        issued_at: challenge_issued_at,
896        pairing_request_event_id,
897        challenge,
898        ..
899    }) = records_by_event_id
900        .get(pairing_challenge_event_id.as_str())
901        .copied()
902    else {
903        return Err(Issue31NostrError::Invalid(
904            "Issue 31 pairing response has no challenge".into(),
905        ));
906    };
907    let Some(Issue31PairingRecord::PairingRequest {
908        host_ref: request_host_ref,
909        host_public_key_hex: request_host_key,
910        device_public_key_hex: request_device_key,
911        issued_at: request_issued_at,
912        requested_scopes,
913        ..
914    }) = records_by_event_id
915        .get(pairing_request_event_id.as_str())
916        .copied()
917    else {
918        return Err(Issue31NostrError::Invalid(
919            "Issue 31 pairing challenge has no request".into(),
920        ));
921    };
922    for (candidate_host_ref, candidate_host_key, candidate_device_key) in [
923        (response_host_ref, response_host_key, response_device_key),
924        (challenge_host_ref, challenge_host_key, challenge_device_key),
925        (request_host_ref, request_host_key, request_device_key),
926    ] {
927        if candidate_host_ref != host_ref
928            || candidate_host_key != host_public_key_hex
929            || candidate_device_key != device_public_key_hex
930        {
931            return Err(Issue31NostrError::Invalid(
932                "Issue 31 pairing chain changes host or device identity".into(),
933            ));
934        }
935    }
936    if response_challenge != challenge {
937        return Err(Issue31NostrError::Invalid(
938            "Issue 31 pairing response does not answer its challenge".into(),
939        ));
940    }
941    if scopes.iter().any(|scope| !requested_scopes.contains(scope)) {
942        return Err(Issue31NostrError::Invalid(
943            "Issue 31 scoped grant exceeds requested scopes".into(),
944        ));
945    }
946    if request_issued_at > challenge_issued_at
947        || challenge_issued_at > response_issued_at
948        || response_issued_at > grant_issued_at
949    {
950        return Err(Issue31NostrError::Invalid(
951            "Issue 31 pairing chain time order is invalid".into(),
952        ));
953    }
954    Ok(())
955}
956
957#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
958#[serde(rename_all = "snake_case")]
959pub enum Issue31CommandStatus {
960    Completed,
961    Failed,
962    Refused,
963    Stopped,
964    Unavailable,
965}
966
967#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
968#[serde(tag = "recordType", rename_all = "snake_case", deny_unknown_fields)]
969pub enum Issue31CommandRecord {
970    CommandIntent {
971        schema: String,
972        #[serde(rename = "hostRef")]
973        host_ref: String,
974        #[serde(rename = "hostPublicKeyHex")]
975        host_public_key_hex: String,
976        #[serde(rename = "devicePublicKeyHex")]
977        device_public_key_hex: String,
978        #[serde(rename = "grantRef")]
979        grant_ref: String,
980        #[serde(rename = "actionRef")]
981        action_ref: String,
982        #[serde(rename = "idempotencyRef")]
983        idempotency_ref: String,
984        #[serde(rename = "expectedGeneration")]
985        expected_generation: u64,
986        #[serde(rename = "argumentsRef")]
987        arguments_ref: String,
988        #[serde(rename = "issuedAt")]
989        issued_at: u64,
990        #[serde(rename = "expiresAt")]
991        expires_at: u64,
992    },
993    CommandResult {
994        schema: String,
995        #[serde(rename = "hostRef")]
996        host_ref: String,
997        #[serde(rename = "hostPublicKeyHex")]
998        host_public_key_hex: String,
999        #[serde(rename = "devicePublicKeyHex")]
1000        device_public_key_hex: String,
1001        #[serde(rename = "grantRef")]
1002        grant_ref: String,
1003        #[serde(rename = "intentEventId")]
1004        intent_event_id: String,
1005        #[serde(rename = "actionRef")]
1006        action_ref: String,
1007        #[serde(rename = "idempotencyRef")]
1008        idempotency_ref: String,
1009        #[serde(rename = "expectedGeneration")]
1010        expected_generation: u64,
1011        status: Issue31CommandStatus,
1012        #[serde(rename = "outcomeRef")]
1013        outcome_ref: String,
1014        #[serde(rename = "reasonRef", default, skip_serializing_if = "Option::is_none")]
1015        reason_ref: Option<String>,
1016        #[serde(rename = "completedAt")]
1017        completed_at: u64,
1018    },
1019}
1020
1021impl Issue31CommandRecord {
1022    pub fn decode(bytes: &[u8]) -> Result<Self, Issue31NostrError> {
1023        if bytes.len() > 64 * 1024 {
1024            return Err(Issue31NostrError::Invalid(
1025                "command record exceeds the record budget".into(),
1026            ));
1027        }
1028        let record: Self = serde_json::from_slice(bytes)
1029            .map_err(|error| Issue31NostrError::Decode(error.to_string()))?;
1030        record.validate()?;
1031        Ok(record)
1032    }
1033
1034    pub fn validate(&self) -> Result<(), Issue31NostrError> {
1035        let (
1036            schema,
1037            host_ref,
1038            host_key,
1039            device_key,
1040            grant_ref,
1041            action_ref,
1042            idempotency_ref,
1043            generation,
1044        ) = self.binding();
1045        if schema != ISSUE31_COMMAND_SCHEMA
1046            || !valid_ref(host_ref)
1047            || !valid_hex64(host_key)
1048            || !valid_hex64(device_key)
1049            || !valid_ref(grant_ref)
1050            || !valid_ref(action_ref)
1051            || !valid_ref(idempotency_ref)
1052            || generation == 0
1053        {
1054            return Err(Issue31NostrError::Invalid("invalid command binding".into()));
1055        }
1056        match self {
1057            Self::CommandIntent {
1058                arguments_ref,
1059                issued_at,
1060                expires_at,
1061                ..
1062            } => {
1063                validate_ref_value(arguments_ref, "arguments")?;
1064                validate_lifetime(*issued_at, *expires_at)
1065            }
1066            Self::CommandResult {
1067                intent_event_id,
1068                outcome_ref,
1069                reason_ref,
1070                ..
1071            } => {
1072                validate_hex_value(intent_event_id, "intent event")?;
1073                validate_ref_value(outcome_ref, "outcome")?;
1074                if let Some(reason_ref) = reason_ref {
1075                    validate_ref_value(reason_ref, "reason")?;
1076                }
1077                Ok(())
1078            }
1079        }
1080    }
1081
1082    fn binding(&self) -> (&str, &str, &str, &str, &str, &str, &str, u64) {
1083        match self {
1084            Self::CommandIntent {
1085                schema,
1086                host_ref,
1087                host_public_key_hex,
1088                device_public_key_hex,
1089                grant_ref,
1090                action_ref,
1091                idempotency_ref,
1092                expected_generation,
1093                ..
1094            }
1095            | Self::CommandResult {
1096                schema,
1097                host_ref,
1098                host_public_key_hex,
1099                device_public_key_hex,
1100                grant_ref,
1101                action_ref,
1102                idempotency_ref,
1103                expected_generation,
1104                ..
1105            } => (
1106                schema,
1107                host_ref,
1108                host_public_key_hex,
1109                device_public_key_hex,
1110                grant_ref,
1111                action_ref,
1112                idempotency_ref,
1113                *expected_generation,
1114            ),
1115        }
1116    }
1117
1118    pub fn validate_private_binding(
1119        &self,
1120        sender_public_key_hex: &str,
1121        recipient_public_key_hex: &str,
1122    ) -> Result<(), Issue31NostrError> {
1123        self.validate()?;
1124        let (_, _, host_key, device_key, _, _, _, _) = self.binding();
1125        let device_authored = matches!(self, Self::CommandIntent { .. });
1126        let expected_sender = if device_authored {
1127            device_key
1128        } else {
1129            host_key
1130        };
1131        let expected_recipient = if device_authored {
1132            host_key
1133        } else {
1134            device_key
1135        };
1136        if sender_public_key_hex != expected_sender
1137            || recipient_public_key_hex != expected_recipient
1138        {
1139            return Err(Issue31NostrError::Invalid(
1140                "command record signer or recipient does not match its binding".into(),
1141            ));
1142        }
1143        Ok(())
1144    }
1145
1146    pub fn device_public_key_hex(&self) -> &str {
1147        let (_, _, _, device_public_key_hex, _, _, _, _) = self.binding();
1148        device_public_key_hex
1149    }
1150}
1151
1152#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1153#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1154pub enum Issue31CommandArguments {
1155    SendMessage {
1156        #[serde(rename = "actionRef")]
1157        action_ref: String,
1158        conversation: String,
1159        text: String,
1160    },
1161    InterruptTurn {
1162        #[serde(rename = "actionRef")]
1163        action_ref: String,
1164        conversation: String,
1165        #[serde(rename = "turnRef")]
1166        turn_ref: String,
1167    },
1168    ReadStatePatch {
1169        #[serde(rename = "actionRef")]
1170        action_ref: String,
1171        #[serde(rename = "slotId")]
1172        slot_id: String,
1173        #[serde(rename = "clientId")]
1174        client_id: String,
1175        #[serde(rename = "contextRef")]
1176        context_ref: String,
1177        #[serde(rename = "readAt")]
1178        read_at: u64,
1179    },
1180    ReminderCreate {
1181        #[serde(rename = "actionRef")]
1182        action_ref: String,
1183        #[serde(rename = "reminderId")]
1184        reminder_id: String,
1185        #[serde(default, skip_serializing_if = "Option::is_none")]
1186        note: Option<String>,
1187        #[serde(
1188            rename = "targetEventId",
1189            default,
1190            skip_serializing_if = "Option::is_none"
1191        )]
1192        target_event_id: Option<String>,
1193        #[serde(rename = "notBefore")]
1194        not_before: u64,
1195        #[serde(default, skip_serializing_if = "Option::is_none")]
1196        expiration: Option<u64>,
1197    },
1198    ReminderChange {
1199        #[serde(rename = "actionRef")]
1200        action_ref: String,
1201        #[serde(rename = "reminderId")]
1202        reminder_id: String,
1203        #[serde(default, skip_serializing_if = "Option::is_none")]
1204        note: Option<String>,
1205        #[serde(
1206            rename = "targetEventId",
1207            default,
1208            skip_serializing_if = "Option::is_none"
1209        )]
1210        target_event_id: Option<String>,
1211        #[serde(rename = "notBefore")]
1212        not_before: u64,
1213        #[serde(default, skip_serializing_if = "Option::is_none")]
1214        expiration: Option<u64>,
1215    },
1216    ReminderComplete {
1217        #[serde(rename = "actionRef")]
1218        action_ref: String,
1219        #[serde(rename = "reminderId")]
1220        reminder_id: String,
1221    },
1222    ReminderCancel {
1223        #[serde(rename = "actionRef")]
1224        action_ref: String,
1225        #[serde(rename = "reminderId")]
1226        reminder_id: String,
1227    },
1228}
1229
1230impl Issue31CommandArguments {
1231    pub fn action_ref(&self) -> &str {
1232        match self {
1233            Self::SendMessage { action_ref, .. }
1234            | Self::InterruptTurn { action_ref, .. }
1235            | Self::ReadStatePatch { action_ref, .. }
1236            | Self::ReminderCreate { action_ref, .. }
1237            | Self::ReminderChange { action_ref, .. }
1238            | Self::ReminderComplete { action_ref, .. }
1239            | Self::ReminderCancel { action_ref, .. } => action_ref,
1240        }
1241    }
1242
1243    pub fn validate(&self) -> Result<(), Issue31NostrError> {
1244        let valid_short_identifier = |value: &str| {
1245            !value.is_empty()
1246                && value.len() <= 64
1247                && value
1248                    .bytes()
1249                    .all(|byte| byte.is_ascii_alphanumeric() || b"._:-".contains(&byte))
1250        };
1251        match self {
1252            Self::SendMessage {
1253                action_ref,
1254                conversation,
1255                text,
1256            } if action_ref == ISSUE31_ACTION_SEND_MESSAGE
1257                && valid_conversation_tag(conversation)
1258                && !text.is_empty()
1259                && text.len() <= 12_000 =>
1260            {
1261                Ok(())
1262            }
1263            Self::InterruptTurn {
1264                action_ref,
1265                conversation,
1266                turn_ref,
1267            } if action_ref == ISSUE31_ACTION_INTERRUPT_TURN
1268                && valid_conversation_tag(conversation)
1269                && valid_ref(turn_ref) =>
1270            {
1271                Ok(())
1272            }
1273            Self::ReadStatePatch {
1274                action_ref,
1275                slot_id,
1276                client_id,
1277                context_ref,
1278                ..
1279            } if action_ref == ISSUE31_ACTION_ADVANCE_READ_STATE
1280                && valid_short_identifier(slot_id)
1281                && valid_short_identifier(client_id)
1282                && !context_ref.is_empty()
1283                && context_ref.len() <= 256
1284                && !context_ref.chars().any(char::is_control) =>
1285            {
1286                Ok(())
1287            }
1288            Self::ReminderCreate {
1289                action_ref,
1290                reminder_id,
1291                note,
1292                target_event_id,
1293                not_before,
1294                expiration,
1295            } if action_ref == ISSUE31_ACTION_CREATE_REMINDER => validate_reminder_arguments(
1296                reminder_id,
1297                note.as_deref(),
1298                target_event_id.as_deref(),
1299                *not_before,
1300                *expiration,
1301            ),
1302            Self::ReminderChange {
1303                action_ref,
1304                reminder_id,
1305                note,
1306                target_event_id,
1307                not_before,
1308                expiration,
1309            } if action_ref == ISSUE31_ACTION_CHANGE_REMINDER => validate_reminder_arguments(
1310                reminder_id,
1311                note.as_deref(),
1312                target_event_id.as_deref(),
1313                *not_before,
1314                *expiration,
1315            ),
1316            Self::ReminderComplete {
1317                action_ref,
1318                reminder_id,
1319            } if action_ref == ISSUE31_ACTION_COMPLETE_REMINDER && valid_hex32(reminder_id) => {
1320                Ok(())
1321            }
1322            Self::ReminderCancel {
1323                action_ref,
1324                reminder_id,
1325            } if action_ref == ISSUE31_ACTION_CANCEL_REMINDER && valid_hex32(reminder_id) => Ok(()),
1326            _ => Err(Issue31NostrError::Invalid(
1327                "Issue 31 command arguments violate their action contract".into(),
1328            )),
1329        }
1330    }
1331}
1332
1333#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1334#[serde(rename_all = "snake_case")]
1335pub enum Issue31CommandHandlingStatus {
1336    Accepted,
1337    Failed,
1338    Refused,
1339    Unavailable,
1340}
1341
1342#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1343#[serde(tag = "recordType", rename_all = "snake_case", deny_unknown_fields)]
1344pub enum Issue31CommandRecordV2 {
1345    CommandIntent {
1346        schema: String,
1347        #[serde(rename = "hostRef")]
1348        host_ref: String,
1349        #[serde(rename = "hostPublicKeyHex")]
1350        host_public_key_hex: String,
1351        #[serde(rename = "devicePublicKeyHex")]
1352        device_public_key_hex: String,
1353        #[serde(rename = "grantRef")]
1354        grant_ref: String,
1355        #[serde(rename = "idempotencyRef")]
1356        idempotency_ref: String,
1357        #[serde(rename = "expectedGeneration")]
1358        expected_generation: u64,
1359        arguments: Issue31CommandArguments,
1360        #[serde(rename = "issuedAt")]
1361        issued_at: u64,
1362        #[serde(rename = "expiresAt")]
1363        expires_at: u64,
1364    },
1365    CommandResult {
1366        schema: String,
1367        #[serde(rename = "hostRef")]
1368        host_ref: String,
1369        #[serde(rename = "hostPublicKeyHex")]
1370        host_public_key_hex: String,
1371        #[serde(rename = "devicePublicKeyHex")]
1372        device_public_key_hex: String,
1373        #[serde(rename = "grantRef")]
1374        grant_ref: String,
1375        #[serde(rename = "intentEventId")]
1376        intent_event_id: String,
1377        #[serde(rename = "actionRef")]
1378        action_ref: String,
1379        #[serde(rename = "idempotencyRef")]
1380        idempotency_ref: String,
1381        #[serde(rename = "expectedGeneration")]
1382        expected_generation: u64,
1383        status: Issue31CommandHandlingStatus,
1384        #[serde(rename = "handlingRef")]
1385        handling_ref: String,
1386        #[serde(rename = "reasonRef", default, skip_serializing_if = "Option::is_none")]
1387        reason_ref: Option<String>,
1388        #[serde(
1389            rename = "sourceEventId",
1390            default,
1391            skip_serializing_if = "Option::is_none"
1392        )]
1393        source_event_id: Option<String>,
1394        #[serde(rename = "handledAt")]
1395        handled_at: u64,
1396    },
1397}
1398
1399impl Issue31CommandRecordV2 {
1400    pub fn decode(bytes: &[u8]) -> Result<Self, Issue31NostrError> {
1401        if bytes.len() > 64 * 1024 {
1402            return Err(Issue31NostrError::Invalid(
1403                "command v2 record exceeds the record budget".into(),
1404            ));
1405        }
1406        let record: Self = serde_json::from_slice(bytes)
1407            .map_err(|error| Issue31NostrError::Decode(error.to_string()))?;
1408        record.validate()?;
1409        Ok(record)
1410    }
1411
1412    pub fn validate(&self) -> Result<(), Issue31NostrError> {
1413        let (schema, host_ref, host_key, device_key, grant_ref, idempotency_ref, generation) =
1414            self.binding();
1415        if schema != ISSUE31_COMMAND_SCHEMA_V2
1416            || !valid_ref(host_ref)
1417            || !valid_hex64(host_key)
1418            || !valid_hex64(device_key)
1419            || !valid_ref(grant_ref)
1420            || !valid_ref(idempotency_ref)
1421            || generation == 0
1422        {
1423            return Err(Issue31NostrError::Invalid(
1424                "invalid command v2 binding".into(),
1425            ));
1426        }
1427        match self {
1428            Self::CommandIntent {
1429                arguments,
1430                issued_at,
1431                expires_at,
1432                ..
1433            } => {
1434                arguments.validate()?;
1435                validate_lifetime(*issued_at, *expires_at)
1436            }
1437            Self::CommandResult {
1438                intent_event_id,
1439                action_ref,
1440                status,
1441                handling_ref,
1442                reason_ref,
1443                source_event_id,
1444                ..
1445            } => {
1446                validate_hex_value(intent_event_id, "intent event")?;
1447                validate_ref_value(action_ref, "action")?;
1448                validate_ref_value(handling_ref, "handling")?;
1449                if let Some(reason_ref) = reason_ref {
1450                    validate_ref_value(reason_ref, "reason")?;
1451                }
1452                if let Some(source_event_id) = source_event_id {
1453                    validate_hex_value(source_event_id, "source event")?;
1454                }
1455                if *status == Issue31CommandHandlingStatus::Accepted && reason_ref.is_some() {
1456                    return Err(Issue31NostrError::Invalid(
1457                        "accepted command handling cannot carry a failure reason".into(),
1458                    ));
1459                }
1460                Ok(())
1461            }
1462        }
1463    }
1464
1465    fn binding(&self) -> (&str, &str, &str, &str, &str, &str, u64) {
1466        match self {
1467            Self::CommandIntent {
1468                schema,
1469                host_ref,
1470                host_public_key_hex,
1471                device_public_key_hex,
1472                grant_ref,
1473                idempotency_ref,
1474                expected_generation,
1475                ..
1476            }
1477            | Self::CommandResult {
1478                schema,
1479                host_ref,
1480                host_public_key_hex,
1481                device_public_key_hex,
1482                grant_ref,
1483                idempotency_ref,
1484                expected_generation,
1485                ..
1486            } => (
1487                schema,
1488                host_ref,
1489                host_public_key_hex,
1490                device_public_key_hex,
1491                grant_ref,
1492                idempotency_ref,
1493                *expected_generation,
1494            ),
1495        }
1496    }
1497
1498    pub fn validate_private_binding(
1499        &self,
1500        sender_public_key_hex: &str,
1501        recipient_public_key_hex: &str,
1502    ) -> Result<(), Issue31NostrError> {
1503        self.validate()?;
1504        let (_, _, host_key, device_key, _, _, _) = self.binding();
1505        let device_authored = matches!(self, Self::CommandIntent { .. });
1506        let (expected_sender, expected_recipient) = if device_authored {
1507            (device_key, host_key)
1508        } else {
1509            (host_key, device_key)
1510        };
1511        if sender_public_key_hex != expected_sender
1512            || recipient_public_key_hex != expected_recipient
1513        {
1514            return Err(Issue31NostrError::Invalid(
1515                "command v2 signer or recipient does not match its binding".into(),
1516            ));
1517        }
1518        Ok(())
1519    }
1520
1521    pub fn device_public_key_hex(&self) -> &str {
1522        self.binding().3
1523    }
1524}
1525
1526#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
1527#[serde(rename_all = "snake_case")]
1528pub enum Issue31SourceRole {
1529    Owner,
1530    Sarah,
1531}
1532
1533#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1534#[serde(rename_all = "camelCase", deny_unknown_fields)]
1535pub struct Issue31AuthorityDecisionProjection {
1536    pub state: String,
1537    pub decision_ref: String,
1538    #[serde(default, skip_serializing_if = "Option::is_none")]
1539    pub reason_ref: Option<String>,
1540}
1541
1542#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1543#[serde(rename_all = "camelCase", deny_unknown_fields)]
1544pub struct Issue31TargetOutcomeProjection {
1545    pub state: String,
1546    #[serde(default, skip_serializing_if = "Option::is_none")]
1547    pub outcome_ref: Option<String>,
1548    #[serde(default, skip_serializing_if = "Option::is_none")]
1549    pub reason_ref: Option<String>,
1550}
1551
1552#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1553#[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)]
1554pub enum Issue31OwnerProjectionBody {
1555    Message {
1556        role: Issue31SourceRole,
1557        conversation: String,
1558        text: String,
1559        #[serde(
1560            rename = "replyToEventId",
1561            default,
1562            skip_serializing_if = "Option::is_none"
1563        )]
1564        reply_to_event_id: Option<String>,
1565    },
1566    Turn {
1567        payload: serde_json::Value,
1568    },
1569    AuthorityReceipt {
1570        #[serde(rename = "receiptRef")]
1571        receipt_ref: String,
1572        #[serde(rename = "turnRef")]
1573        turn_ref: String,
1574        #[serde(rename = "authorityDecision")]
1575        authority_decision: Issue31AuthorityDecisionProjection,
1576        #[serde(rename = "targetOutcome")]
1577        target_outcome: Issue31TargetOutcomeProjection,
1578    },
1579    Engram {
1580        #[serde(rename = "dTag")]
1581        d_tag: String,
1582        plaintext: String,
1583    },
1584    ReadState {
1585        #[serde(rename = "dTag")]
1586        d_tag: String,
1587        plaintext: String,
1588    },
1589    Reminder {
1590        #[serde(rename = "reminderId")]
1591        reminder_id: String,
1592        plaintext: String,
1593        #[serde(rename = "notBefore", default, skip_serializing_if = "Option::is_none")]
1594        not_before: Option<u64>,
1595        #[serde(default, skip_serializing_if = "Option::is_none")]
1596        expiration: Option<u64>,
1597    },
1598}
1599
1600#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1601#[serde(rename_all = "camelCase", deny_unknown_fields)]
1602pub struct Issue31OwnerProjectionRecord {
1603    pub schema: String,
1604    pub record_type: String,
1605    pub host_ref: String,
1606    pub host_public_key_hex: String,
1607    pub device_public_key_hex: String,
1608    pub grant_ref: String,
1609    pub expected_generation: u64,
1610    pub source_event_id: String,
1611    pub source_author_public_key_hex: String,
1612    pub source_role: Issue31SourceRole,
1613    pub source_kind: u16,
1614    pub source_created_at: u64,
1615    pub projected_at: u64,
1616    pub projection: Issue31OwnerProjectionBody,
1617}
1618
1619impl Issue31OwnerProjectionRecord {
1620    pub fn decode(bytes: &[u8]) -> Result<Self, Issue31NostrError> {
1621        if bytes.len() > 640 * 1024 {
1622            return Err(Issue31NostrError::Invalid(
1623                "owner projection exceeds the record budget".into(),
1624            ));
1625        }
1626        let record: Self = serde_json::from_slice(bytes)
1627            .map_err(|error| Issue31NostrError::Decode(error.to_string()))?;
1628        record.validate()?;
1629        Ok(record)
1630    }
1631
1632    pub fn validate(&self) -> Result<(), Issue31NostrError> {
1633        if self.schema != ISSUE31_OWNER_PROJECTION_SCHEMA
1634            || self.record_type != "owner_projection"
1635            || !valid_ref(&self.host_ref)
1636            || !valid_hex64(&self.host_public_key_hex)
1637            || !valid_hex64(&self.device_public_key_hex)
1638            || !valid_ref(&self.grant_ref)
1639            || self.expected_generation == 0
1640            || !valid_hex64(&self.source_event_id)
1641            || !valid_hex64(&self.source_author_public_key_hex)
1642            || self.projected_at < self.source_created_at
1643        {
1644            return Err(Issue31NostrError::Invalid(
1645                "invalid owner projection binding".into(),
1646            ));
1647        }
1648        let (expected_kind, expected_role) = self.projection.validate()?;
1649        if self.source_kind != expected_kind || self.source_role != expected_role {
1650            return Err(Issue31NostrError::Invalid(
1651                "owner projection source kind or role does not match its body".into(),
1652            ));
1653        }
1654        Ok(())
1655    }
1656
1657    pub fn validate_private_binding(
1658        &self,
1659        sender_public_key_hex: &str,
1660        recipient_public_key_hex: &str,
1661        sarah_public_key_hex: &str,
1662    ) -> Result<(), Issue31NostrError> {
1663        self.validate()?;
1664        let expected_source_author = match self.source_role {
1665            Issue31SourceRole::Owner => &self.host_public_key_hex,
1666            Issue31SourceRole::Sarah => sarah_public_key_hex,
1667        };
1668        if sender_public_key_hex != self.host_public_key_hex
1669            || recipient_public_key_hex != self.device_public_key_hex
1670            || self.source_author_public_key_hex != expected_source_author
1671        {
1672            return Err(Issue31NostrError::Invalid(
1673                "owner projection signer, recipient, or source author is invalid".into(),
1674            ));
1675        }
1676        Ok(())
1677    }
1678}
1679
1680impl Issue31OwnerProjectionBody {
1681    fn validate(&self) -> Result<(u16, Issue31SourceRole), Issue31NostrError> {
1682        match self {
1683            Self::Message {
1684                role,
1685                conversation,
1686                text,
1687                reply_to_event_id,
1688            } if valid_conversation_tag(conversation)
1689                && !text.is_empty()
1690                && text.len() <= 12_000
1691                && reply_to_event_id
1692                    .as_ref()
1693                    .is_none_or(|value| valid_hex64(value)) =>
1694            {
1695                Ok((ISSUE31_PRIVATE_RUMOR_KIND, *role))
1696            }
1697            Self::Turn { payload } if valid_turn_payload(payload) => {
1698                Ok((SARAH_TURN_RECORD_KIND, Issue31SourceRole::Sarah))
1699            }
1700            Self::AuthorityReceipt {
1701                receipt_ref,
1702                turn_ref,
1703                authority_decision,
1704                target_outcome,
1705            } => {
1706                validate_ref_value(receipt_ref, "receipt")?;
1707                validate_ref_value(turn_ref, "turn")?;
1708                validate_authority_projection(authority_decision, target_outcome)?;
1709                Ok((SARAH_AUTHORITY_RECEIPT_KIND, Issue31SourceRole::Sarah))
1710            }
1711            Self::Engram { d_tag, plaintext }
1712                if valid_hex64(d_tag)
1713                    && !plaintext.is_empty()
1714                    && plaintext.len() <= 65_535
1715                    && valid_engram_plaintext(plaintext) =>
1716            {
1717                Ok((SARAH_ENGRAM_KIND, Issue31SourceRole::Sarah))
1718            }
1719            Self::ReadState { d_tag, plaintext }
1720                if !d_tag.is_empty()
1721                    && d_tag.len() <= 256
1722                    && !plaintext.is_empty()
1723                    && plaintext.len() <= 524_288
1724                    && valid_read_state_plaintext(plaintext) =>
1725            {
1726                Ok((SARAH_READ_STATE_KIND, Issue31SourceRole::Owner))
1727            }
1728            Self::Reminder {
1729                reminder_id,
1730                plaintext,
1731                not_before,
1732                expiration,
1733            } if valid_hex32(reminder_id)
1734                && !plaintext.is_empty()
1735                && plaintext.len() <= 524_288
1736                && valid_reminder_plaintext(plaintext, *not_before)
1737                && expiration
1738                    .zip(*not_before)
1739                    .is_none_or(|(expiration, not_before)| expiration > not_before) =>
1740            {
1741                Ok((SARAH_REMINDER_KIND, Issue31SourceRole::Owner))
1742            }
1743            _ => Err(Issue31NostrError::Invalid(
1744                "owner projection body violates its source contract".into(),
1745            )),
1746        }
1747    }
1748}
1749
1750#[derive(Debug, Clone, PartialEq, Eq)]
1751pub struct Issue31OwnerProjectionInput<'a> {
1752    pub host_ref: &'a str,
1753    pub host_public_key_hex: &'a str,
1754    pub device_public_key_hex: &'a str,
1755    pub sarah_public_key_hex: &'a str,
1756    pub grant_ref: &'a str,
1757    pub expected_generation: u64,
1758    pub source_event_id: &'a str,
1759    pub source_author_public_key_hex: &'a str,
1760    pub source_kind: u16,
1761    pub source_created_at: u64,
1762    pub projected_at: u64,
1763    pub projection: Issue31OwnerProjectionBody,
1764}
1765
1766#[derive(Debug, Clone, PartialEq, Eq)]
1767pub struct Issue31OwnerProjectionEmission {
1768    pub record: Issue31OwnerProjectionRecord,
1769    pub content: String,
1770}
1771
1772/// Emit an owner projection for one admitted device.
1773///
1774/// The emitted bytes are routed back through `Issue31OwnerProjectionRecord::decode`
1775/// and the private-binding check before they are returned, so the host cannot
1776/// publish a projection its own reader would refuse. `decode` also applies the
1777/// record budget, which a direct `validate` call does not: a body inside its
1778/// per-field bounds can still serialize past the budget once JSON escaping is
1779/// applied, and that record would be readable on the host and unreadable on the
1780/// device. `source_role` is derived from the body rather than supplied, because
1781/// a caller-chosen role is a second source of truth for something the body
1782/// already decides.
1783pub fn emit_issue31_owner_projection(
1784    input: Issue31OwnerProjectionInput<'_>,
1785) -> Result<Issue31OwnerProjectionEmission, Issue31NostrError> {
1786    let (_, source_role) = input.projection.validate()?;
1787    let record = Issue31OwnerProjectionRecord {
1788        schema: ISSUE31_OWNER_PROJECTION_SCHEMA.into(),
1789        record_type: "owner_projection".into(),
1790        host_ref: input.host_ref.into(),
1791        host_public_key_hex: input.host_public_key_hex.into(),
1792        device_public_key_hex: input.device_public_key_hex.into(),
1793        grant_ref: input.grant_ref.into(),
1794        expected_generation: input.expected_generation,
1795        source_event_id: input.source_event_id.into(),
1796        source_author_public_key_hex: input.source_author_public_key_hex.into(),
1797        source_role,
1798        source_kind: input.source_kind,
1799        source_created_at: input.source_created_at,
1800        projected_at: input.projected_at,
1801        projection: input.projection,
1802    };
1803    let content = serde_json::to_string(&record)
1804        .map_err(|error| Issue31NostrError::Invalid(error.to_string()))?;
1805    let decoded = Issue31OwnerProjectionRecord::decode(content.as_bytes())?;
1806    decoded.validate_private_binding(
1807        input.host_public_key_hex,
1808        input.device_public_key_hex,
1809        input.sarah_public_key_hex,
1810    )?;
1811    if decoded != record {
1812        return Err(Issue31NostrError::Invalid(
1813            "owner projection did not survive its own decoder".into(),
1814        ));
1815    }
1816    Ok(Issue31OwnerProjectionEmission {
1817        record: decoded,
1818        content,
1819    })
1820}
1821
1822/// Why a source the owner is entitled to see never became a projection.
1823///
1824/// Only causes the host can actually observe are on the wire. A device-side
1825/// read failure is real and is counted, but it is counted on the device, so a
1826/// host cannot assert something only the device can know.
1827#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
1828#[serde(rename_all = "snake_case")]
1829pub enum Issue31WithheldCause {
1830    /// The source was quarantined: its plaintext, its `d` tag, or its
1831    /// projection body was refused, so it was removed from the pass.
1832    Quarantined,
1833    /// The bounded projection scan stopped before the end of the conversation,
1834    /// so an unknown number of later sources were never examined at all.
1835    ScanBound,
1836}
1837
1838impl Issue31WithheldCause {
1839    /// Whether the host can state an exact number for this cause.
1840    ///
1841    /// A quarantine is counted one event at a time, so it is exact. The scan
1842    /// bound is the opposite: the host stopped reading, so it knows only that
1843    /// at least one source is unexamined. Reporting "1 withheld" as exact when
1844    /// nine hundred are unread would be a worse lie than silence.
1845    fn is_exact(self) -> bool {
1846        match self {
1847            Self::Quarantined => true,
1848            Self::ScanBound => false,
1849        }
1850    }
1851}
1852
1853#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1854#[serde(rename_all = "camelCase", deny_unknown_fields)]
1855pub struct Issue31WithheldSourceCount {
1856    pub cause: Issue31WithheldCause,
1857    pub count: u32,
1858    /// `true` when `count` is the number withheld, `false` when it is a lower
1859    /// bound and the true number is unknown.
1860    pub exact: bool,
1861    pub reason_ref: String,
1862}
1863
1864/// A host statement, per admitted device, about how complete that device's
1865/// owner projection is.
1866///
1867/// Exit 4 of omega#46 requires that every engram reaches the device or that the
1868/// gap is exact. Before this record there was no mechanism the device could
1869/// see: a phone rendered a confident, complete-looking list and nothing said it
1870/// was short. Silence is not completeness, so a device with no such record must
1871/// read "unknown", never "complete" — that is why a complete pass emits a
1872/// record too, rather than emitting nothing.
1873#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
1874#[serde(rename_all = "camelCase", deny_unknown_fields)]
1875pub struct Issue31WithheldSourcesRecord {
1876    pub schema: String,
1877    pub record_type: String,
1878    pub host_ref: String,
1879    pub host_public_key_hex: String,
1880    pub device_public_key_hex: String,
1881    pub grant_ref: String,
1882    pub expected_generation: u64,
1883    pub observed_at: u64,
1884    /// `complete` when nothing was withheld, `partial` when something was.
1885    /// The two states are structurally different so a reader cannot render
1886    /// them the same way by accident.
1887    pub coverage: String,
1888    pub withheld: Vec<Issue31WithheldSourceCount>,
1889}
1890
1891pub const ISSUE31_WITHHELD_COVERAGE_COMPLETE: &str = "complete";
1892pub const ISSUE31_WITHHELD_COVERAGE_PARTIAL: &str = "partial";
1893const MAX_ISSUE31_WITHHELD_ENTRIES: usize = 8;
1894
1895impl Issue31WithheldSourcesRecord {
1896    pub fn decode(bytes: &[u8]) -> Result<Self, Issue31NostrError> {
1897        if bytes.len() > 8 * 1024 {
1898            return Err(Issue31NostrError::Invalid(
1899                "withheld sources record exceeds the record budget".into(),
1900            ));
1901        }
1902        let record: Self = serde_json::from_slice(bytes)
1903            .map_err(|error| Issue31NostrError::Decode(error.to_string()))?;
1904        record.validate()?;
1905        Ok(record)
1906    }
1907
1908    pub fn validate(&self) -> Result<(), Issue31NostrError> {
1909        if self.schema != ISSUE31_WITHHELD_SOURCES_SCHEMA
1910            || self.record_type != "withheld_sources"
1911            || !valid_ref(&self.host_ref)
1912            || !valid_hex64(&self.host_public_key_hex)
1913            || !valid_hex64(&self.device_public_key_hex)
1914            || !valid_ref(&self.grant_ref)
1915            || self.expected_generation == 0
1916        {
1917            return Err(Issue31NostrError::Invalid(
1918                "invalid withheld sources binding".into(),
1919            ));
1920        }
1921        if self.withheld.len() > MAX_ISSUE31_WITHHELD_ENTRIES {
1922            return Err(Issue31NostrError::Invalid(
1923                "withheld sources record exceeds its entry bound".into(),
1924            ));
1925        }
1926        let mut seen: BTreeSet<(Issue31WithheldCause, &str)> = BTreeSet::new();
1927        for entry in &self.withheld {
1928            if entry.count == 0 {
1929                return Err(Issue31NostrError::Invalid(
1930                    "a withheld source count of zero is not a withheld source".into(),
1931                ));
1932            }
1933            if entry.exact != entry.cause.is_exact() {
1934                return Err(Issue31NostrError::Invalid(
1935                    "withheld source exactness does not match what its cause can know".into(),
1936                ));
1937            }
1938            if !valid_ref(&entry.reason_ref) {
1939                return Err(Issue31NostrError::Invalid(
1940                    "a withheld source count needs an exact reason".into(),
1941                ));
1942            }
1943            if !seen.insert((entry.cause, entry.reason_ref.as_str())) {
1944                return Err(Issue31NostrError::Invalid(
1945                    "withheld source counts repeat a cause and reason".into(),
1946                ));
1947            }
1948        }
1949        let expected_coverage = if self.withheld.is_empty() {
1950            ISSUE31_WITHHELD_COVERAGE_COMPLETE
1951        } else {
1952            ISSUE31_WITHHELD_COVERAGE_PARTIAL
1953        };
1954        if self.coverage != expected_coverage {
1955            return Err(Issue31NostrError::Invalid(
1956                "withheld sources coverage does not match its counts".into(),
1957            ));
1958        }
1959        Ok(())
1960    }
1961
1962    pub fn validate_private_binding(
1963        &self,
1964        sender_public_key_hex: &str,
1965        recipient_public_key_hex: &str,
1966    ) -> Result<(), Issue31NostrError> {
1967        self.validate()?;
1968        if sender_public_key_hex != self.host_public_key_hex
1969            || recipient_public_key_hex != self.device_public_key_hex
1970        {
1971            return Err(Issue31NostrError::Invalid(
1972                "withheld sources signer or recipient is invalid".into(),
1973            ));
1974        }
1975        Ok(())
1976    }
1977
1978    /// The part of the record that says something about the world, with the
1979    /// observation time removed.
1980    ///
1981    /// The host re-runs its projection pass continuously. Re-publishing an
1982    /// identical statement with only a new timestamp would fill the relay with
1983    /// noise, so the host compares this instead.
1984    pub fn substance(&self) -> (String, Vec<Issue31WithheldSourceCount>) {
1985        (self.coverage.clone(), self.withheld.clone())
1986    }
1987}
1988
1989#[derive(Debug, Clone, PartialEq, Eq)]
1990pub struct Issue31WithheldSourcesInput<'a> {
1991    pub host_ref: &'a str,
1992    pub host_public_key_hex: &'a str,
1993    pub device_public_key_hex: &'a str,
1994    pub grant_ref: &'a str,
1995    pub expected_generation: u64,
1996    pub observed_at: u64,
1997    pub withheld: Vec<Issue31WithheldSourceCount>,
1998}
1999
2000#[derive(Debug, Clone, PartialEq, Eq)]
2001pub struct Issue31WithheldSourcesEmission {
2002    pub record: Issue31WithheldSourcesRecord,
2003    pub content: String,
2004}
2005
2006/// Emit one device's withheld-source statement.
2007///
2008/// This follows `emit_issue31_owner_projection`: the bytes are routed back
2009/// through this record's own decoder and private-binding check before they are
2010/// returned, so the host cannot publish a coverage statement its own reader
2011/// would refuse. `coverage` is derived from the counts rather than supplied,
2012/// because a caller-chosen coverage value is a second source of truth for
2013/// something the counts already decide — and it is exactly the field a bug
2014/// would set to "complete" over a non-empty list.
2015pub fn emit_issue31_withheld_sources(
2016    input: Issue31WithheldSourcesInput<'_>,
2017) -> Result<Issue31WithheldSourcesEmission, Issue31NostrError> {
2018    let mut withheld = input.withheld;
2019    withheld.sort_by(|left, right| {
2020        left.cause
2021            .cmp(&right.cause)
2022            .then_with(|| left.reason_ref.cmp(&right.reason_ref))
2023    });
2024    let coverage = if withheld.is_empty() {
2025        ISSUE31_WITHHELD_COVERAGE_COMPLETE
2026    } else {
2027        ISSUE31_WITHHELD_COVERAGE_PARTIAL
2028    };
2029    let record = Issue31WithheldSourcesRecord {
2030        schema: ISSUE31_WITHHELD_SOURCES_SCHEMA.into(),
2031        record_type: "withheld_sources".into(),
2032        host_ref: input.host_ref.into(),
2033        host_public_key_hex: input.host_public_key_hex.into(),
2034        device_public_key_hex: input.device_public_key_hex.into(),
2035        grant_ref: input.grant_ref.into(),
2036        expected_generation: input.expected_generation,
2037        observed_at: input.observed_at,
2038        coverage: coverage.into(),
2039        withheld,
2040    };
2041    let content = serde_json::to_string(&record)
2042        .map_err(|error| Issue31NostrError::Invalid(error.to_string()))?;
2043    let decoded = Issue31WithheldSourcesRecord::decode(content.as_bytes())?;
2044    decoded.validate_private_binding(input.host_public_key_hex, input.device_public_key_hex)?;
2045    if decoded != record {
2046        return Err(Issue31NostrError::Invalid(
2047            "withheld sources record did not survive its own decoder".into(),
2048        ));
2049    }
2050    Ok(Issue31WithheldSourcesEmission {
2051        record: decoded,
2052        content,
2053    })
2054}
2055
2056#[derive(Debug, Clone, PartialEq, Eq)]
2057pub struct Issue31CommandEvent {
2058    pub event_id: String,
2059    pub record: Issue31CommandRecord,
2060}
2061
2062#[derive(Debug, Clone, PartialEq, Eq)]
2063pub struct Issue31CommandState {
2064    pub intent_event_id: String,
2065    pub intent: Issue31CommandRecord,
2066    pub result_event_id: Option<String>,
2067    pub result: Option<Issue31CommandRecord>,
2068}
2069
2070pub fn reconcile_issue31_commands(
2071    events: &[Issue31CommandEvent],
2072) -> Result<Vec<Issue31CommandState>, Issue31NostrError> {
2073    let mut intents: BTreeMap<String, (&String, &Issue31CommandRecord)> = BTreeMap::new();
2074    let mut results: BTreeMap<String, (&String, &Issue31CommandRecord)> = BTreeMap::new();
2075    for event in events {
2076        event.record.validate()?;
2077        if !valid_hex64(&event.event_id) {
2078            return Err(Issue31NostrError::Invalid(
2079                "invalid command event id".into(),
2080            ));
2081        }
2082        let (_, _, _, _, _, _, idempotency_ref, _) = event.record.binding();
2083        let target = match &event.record {
2084            Issue31CommandRecord::CommandIntent { .. } => &mut intents,
2085            Issue31CommandRecord::CommandResult { .. } => &mut results,
2086        };
2087        if let Some((prior_event_id, prior)) = target.get_mut(idempotency_ref) {
2088            if *prior != &event.record {
2089                return Err(Issue31NostrError::Invalid(format!(
2090                    "conflicting records for {idempotency_ref}"
2091                )));
2092            }
2093            if event.event_id.as_str() < prior_event_id.as_str() {
2094                *prior_event_id = &event.event_id;
2095            }
2096        } else {
2097            target.insert(
2098                idempotency_ref.to_string(),
2099                (&event.event_id, &event.record),
2100            );
2101        }
2102    }
2103
2104    let mut states = Vec::new();
2105    for (idempotency_ref, (intent_event_id, intent)) in intents {
2106        let result = results.get(&idempotency_ref).copied();
2107        if let Some((_, result_record)) = result {
2108            let Issue31CommandRecord::CommandResult {
2109                intent_event_id: result_intent_id,
2110                ..
2111            } = result_record
2112            else {
2113                return Err(Issue31NostrError::Invalid(
2114                    "result map contained intent".into(),
2115                ));
2116            };
2117            if result_intent_id != intent_event_id || result_record.binding() != intent.binding() {
2118                return Err(Issue31NostrError::Invalid(format!(
2119                    "command result does not match {idempotency_ref}"
2120                )));
2121            }
2122        }
2123        states.push(Issue31CommandState {
2124            intent_event_id: intent_event_id.clone(),
2125            intent: intent.clone(),
2126            result_event_id: result.map(|(event_id, _)| event_id.clone()),
2127            result: result.map(|(_, record)| record.clone()),
2128        });
2129    }
2130    Ok(states)
2131}
2132
2133#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
2134pub struct Issue31HostConfiguration {
2135    pub host_ref: String,
2136    pub host_public_key_hex: String,
2137    pub sarah_public_key_hex: String,
2138    #[serde(default)]
2139    pub conversation: String,
2140    pub display_name: String,
2141    pub relay_urls: Vec<String>,
2142    pub generation: u64,
2143}
2144
2145#[derive(Debug, Clone, PartialEq, Eq)]
2146pub struct Issue31CommandExecution {
2147    pub status: Issue31CommandStatus,
2148    pub outcome_ref: String,
2149    pub reason_ref: Option<String>,
2150}
2151
2152#[derive(Debug, Clone, PartialEq, Eq)]
2153pub struct Issue31CommandExecutionV2 {
2154    pub status: Issue31CommandHandlingStatus,
2155    pub handling_ref: String,
2156    pub reason_ref: Option<String>,
2157    pub source_event_id: Option<String>,
2158}
2159
2160#[derive(Debug, Clone, Serialize, Deserialize)]
2161#[serde(rename_all = "camelCase", deny_unknown_fields)]
2162pub struct Issue31HostController {
2163    configuration: Issue31HostConfiguration,
2164    #[serde(skip)]
2165    admitted_device_scopes: BTreeMap<String, BTreeSet<Issue31PairingScope>>,
2166    pairing_events: Vec<Issue31PairingEvent>,
2167    processed_pairing_event_ids: BTreeSet<String>,
2168    processed_command_event_ids: BTreeSet<String>,
2169    command_results: BTreeMap<String, (Issue31CommandRecord, Issue31CommandRecord)>,
2170    #[serde(default)]
2171    command_results_v2: BTreeMap<String, (Issue31CommandRecordV2, Issue31CommandRecordV2)>,
2172    #[serde(default)]
2173    projected_source_event_ids: BTreeMap<String, BTreeSet<String>>,
2174    /// Revocation event ids the owner has explicitly cleared by re-admitting the
2175    /// device they name. A revocation that is not listed here permanently blocks
2176    /// its device from being challenged or granted again, whatever the runtime
2177    /// admission allowlist says.
2178    #[serde(default)]
2179    cleared_device_revocation_event_ids: BTreeSet<String>,
2180}
2181
2182const MAX_ISSUE31_PAIRING_EVENTS: usize = 4_096;
2183const MAX_ISSUE31_PROCESSED_EVENTS: usize = 4_096;
2184const MAX_ISSUE31_COMMAND_RESULTS: usize = 1_024;
2185const MAX_ISSUE31_PROJECTED_SOURCE_EVENTS: usize = 16_384;
2186
2187impl Issue31HostController {
2188    pub fn new(configuration: Issue31HostConfiguration) -> Result<Self, Issue31NostrError> {
2189        Issue31HostDiscovery {
2190            schema: ISSUE31_HOST_DISCOVERY_SCHEMA.into(),
2191            host_ref: configuration.host_ref.clone(),
2192            host_public_key_hex: configuration.host_public_key_hex.clone(),
2193            sarah_public_key_hex: configuration.sarah_public_key_hex.clone(),
2194            display_name: configuration.display_name.clone(),
2195            protocols: vec![ISSUE31_PAIRING_SCHEMA.into(), ISSUE31_COMMAND_SCHEMA.into()],
2196            relay_urls: configuration.relay_urls.clone(),
2197            generation: configuration.generation,
2198            issued_at: 1,
2199            expires_at: 2,
2200        }
2201        .validate()?;
2202        Issue31HostDiscoveryV2 {
2203            schema: ISSUE31_HOST_DISCOVERY_SCHEMA_V2.into(),
2204            host_ref: configuration.host_ref.clone(),
2205            host_public_key_hex: configuration.host_public_key_hex.clone(),
2206            sarah_public_key_hex: configuration.sarah_public_key_hex.clone(),
2207            conversation: configuration.conversation.clone(),
2208            display_name: configuration.display_name.clone(),
2209            protocols: vec![
2210                ISSUE31_PAIRING_SCHEMA.into(),
2211                ISSUE31_COMMAND_SCHEMA.into(),
2212                ISSUE31_COMMAND_SCHEMA_V2.into(),
2213            ],
2214            relay_urls: configuration.relay_urls.clone(),
2215            generation: configuration.generation,
2216            issued_at: 1,
2217            expires_at: 2,
2218        }
2219        .validate()?;
2220        Ok(Self {
2221            configuration,
2222            admitted_device_scopes: BTreeMap::new(),
2223            pairing_events: Vec::new(),
2224            processed_pairing_event_ids: BTreeSet::new(),
2225            processed_command_event_ids: BTreeSet::new(),
2226            command_results: BTreeMap::new(),
2227            command_results_v2: BTreeMap::new(),
2228            projected_source_event_ids: BTreeMap::new(),
2229            cleared_device_revocation_event_ids: BTreeSet::new(),
2230        })
2231    }
2232
2233    pub fn discovery(
2234        &self,
2235        issued_at: u64,
2236        expires_at: u64,
2237    ) -> Result<Issue31HostDiscovery, Issue31NostrError> {
2238        let discovery = Issue31HostDiscovery {
2239            schema: ISSUE31_HOST_DISCOVERY_SCHEMA.into(),
2240            host_ref: self.configuration.host_ref.clone(),
2241            host_public_key_hex: self.configuration.host_public_key_hex.clone(),
2242            sarah_public_key_hex: self.configuration.sarah_public_key_hex.clone(),
2243            display_name: self.configuration.display_name.clone(),
2244            protocols: vec![ISSUE31_PAIRING_SCHEMA.into(), ISSUE31_COMMAND_SCHEMA.into()],
2245            relay_urls: self.configuration.relay_urls.clone(),
2246            generation: self.configuration.generation,
2247            issued_at,
2248            expires_at,
2249        };
2250        discovery.validate()?;
2251        Ok(discovery)
2252    }
2253
2254    pub fn discovery_v2(
2255        &self,
2256        issued_at: u64,
2257        expires_at: u64,
2258    ) -> Result<Issue31HostDiscoveryV2, Issue31NostrError> {
2259        let discovery = Issue31HostDiscoveryV2 {
2260            schema: ISSUE31_HOST_DISCOVERY_SCHEMA_V2.into(),
2261            host_ref: self.configuration.host_ref.clone(),
2262            host_public_key_hex: self.configuration.host_public_key_hex.clone(),
2263            sarah_public_key_hex: self.configuration.sarah_public_key_hex.clone(),
2264            conversation: self.configuration.conversation.clone(),
2265            display_name: self.configuration.display_name.clone(),
2266            protocols: vec![
2267                ISSUE31_PAIRING_SCHEMA.into(),
2268                ISSUE31_COMMAND_SCHEMA.into(),
2269                ISSUE31_COMMAND_SCHEMA_V2.into(),
2270            ],
2271            relay_urls: self.configuration.relay_urls.clone(),
2272            generation: self.configuration.generation,
2273            issued_at,
2274            expires_at,
2275        };
2276        discovery.validate()?;
2277        Ok(discovery)
2278    }
2279
2280    pub fn matches_configuration(&self, configuration: &Issue31HostConfiguration) -> bool {
2281        &self.configuration == configuration
2282    }
2283
2284    pub fn adopt_conversation_if_missing(
2285        &mut self,
2286        conversation: &str,
2287    ) -> Result<(), Issue31NostrError> {
2288        if self.configuration.conversation.is_empty() {
2289            if !valid_conversation_tag(conversation) {
2290                return Err(Issue31NostrError::Invalid(
2291                    "invalid Issue 31 migration conversation".into(),
2292                ));
2293            }
2294            self.configuration.conversation = conversation.into();
2295        }
2296        Ok(())
2297    }
2298
2299    pub fn set_admitted_device_policy(
2300        &mut self,
2301        public_keys: Vec<String>,
2302        approved_scopes: Vec<Issue31PairingScope>,
2303    ) -> Result<(), Issue31NostrError> {
2304        if public_keys.is_empty()
2305            || public_keys.len() > 32
2306            || public_keys
2307                .iter()
2308                .any(|public_key| !valid_hex64(public_key))
2309        {
2310            return Err(Issue31NostrError::Invalid(
2311                "Issue 31 device allowlist must contain one to 32 lowercase public keys".into(),
2312            ));
2313        }
2314        let public_key_count = public_keys.len();
2315        let admitted = public_keys.into_iter().collect::<BTreeSet<_>>();
2316        if admitted.len() != public_key_count {
2317            return Err(Issue31NostrError::Invalid(
2318                "Issue 31 device allowlist contains duplicates".into(),
2319            ));
2320        }
2321        validate_scopes(&approved_scopes, "owner-approved scopes")?;
2322        let approved_scopes = approved_scopes.into_iter().collect::<BTreeSet<_>>();
2323        self.admitted_device_scopes = admitted
2324            .into_iter()
2325            .map(|public_key| (public_key, approved_scopes.clone()))
2326            .collect();
2327        Ok(())
2328    }
2329
2330    pub fn admitted_device_fingerprints(&self) -> Vec<String> {
2331        self.admitted_device_scopes
2332            .keys()
2333            .map(|public_key| {
2334                let digest = format!("{:x}", Sha256::digest(public_key.as_bytes()));
2335                digest[..16].to_ascii_uppercase()
2336            })
2337            .collect()
2338    }
2339
2340    pub fn grant_projections(
2341        &self,
2342        now: u64,
2343    ) -> Result<Vec<Issue31GrantProjection>, Issue31NostrError> {
2344        let grant_refs = self
2345            .pairing_events
2346            .iter()
2347            .filter_map(|event| match &event.record {
2348                Issue31PairingRecord::ScopedGrant { grant_ref, .. }
2349                | Issue31PairingRecord::GrantRenewal { grant_ref, .. }
2350                | Issue31PairingRecord::GrantRevocation { grant_ref, .. } => {
2351                    Some(grant_ref.clone())
2352                }
2353                _ => None,
2354            })
2355            .collect::<BTreeSet<_>>();
2356        grant_refs
2357            .into_iter()
2358            .map(|grant_ref| {
2359                let state =
2360                    fold_issue31_grant(&self.pairing_events, &grant_ref)?.ok_or_else(|| {
2361                        Issue31NostrError::Invalid(
2362                            "durable grant projection lost its lifecycle state".into(),
2363                        )
2364                    })?;
2365                let device_digest = format!(
2366                    "{:x}",
2367                    Sha256::digest(state.device_public_key_hex.as_bytes())
2368                );
2369                let status = match state.status {
2370                    Issue31GrantStatus::Revoked => "revoked",
2371                    Issue31GrantStatus::Active
2372                        if state.expires_at.is_some_and(|expires_at| now >= expires_at) =>
2373                    {
2374                        "expired"
2375                    }
2376                    Issue31GrantStatus::Active => "active",
2377                };
2378                Ok(Issue31GrantProjection {
2379                    grant_ref: state.grant_ref,
2380                    device_fingerprint: device_digest[..16].to_ascii_uppercase(),
2381                    generation: state.generation,
2382                    status: status.into(),
2383                    scopes: state.scopes,
2384                    expires_at: state.expires_at,
2385                    source_event_id: state.source_event_id,
2386                })
2387            })
2388            .collect()
2389    }
2390
2391    pub fn pairing_event_was_processed(&self, event_id: &str) -> bool {
2392        self.processed_pairing_event_ids.contains(event_id)
2393    }
2394
2395    /// Every revocation in the durable pairing log that names this device and
2396    /// that the owner has not cleared by re-admitting it.
2397    ///
2398    /// The grant fold is keyed by `grant_ref`, so it can only ever say that one
2399    /// grant died. Revocation is a statement about the *device*: without this
2400    /// scan a revoked device simply pairs again under a fresh `grant_ref` and
2401    /// restores the exact authority the owner just took away. The scan looks at
2402    /// every pairing event, not just the grant being folded.
2403    pub fn outstanding_device_revocations(&self, device_public_key_hex: &str) -> Vec<String> {
2404        self.pairing_events
2405            .iter()
2406            .filter(|event| {
2407                matches!(&event.record, Issue31PairingRecord::GrantRevocation { .. })
2408                    && record_device_public_key(&event.record) == device_public_key_hex
2409                    && !self
2410                        .cleared_device_revocation_event_ids
2411                        .contains(&event.event_id)
2412            })
2413            .map(|event| event.event_id.clone())
2414            .collect()
2415    }
2416
2417    pub fn device_admission_is_revoked(&self, device_public_key_hex: &str) -> bool {
2418        !self
2419            .outstanding_device_revocations(device_public_key_hex)
2420            .is_empty()
2421    }
2422
2423    /// Owner-side re-admission: clear the revocations that currently block this
2424    /// device so it may pair again.
2425    ///
2426    /// Only the revocations that exist *now* are cleared, by event id. A later
2427    /// revocation is a new event id and is therefore not cleared, so it fails
2428    /// closed; and replaying an already-cleared revocation cannot re-block the
2429    /// device, because clearance is keyed to the event rather than to a counter.
2430    pub fn readmit_device(
2431        &mut self,
2432        device_public_key_hex: &str,
2433    ) -> Result<Vec<String>, Issue31NostrError> {
2434        if !valid_hex64(device_public_key_hex) {
2435            return Err(Issue31NostrError::Invalid(
2436                "Issue 31 device public key is invalid".into(),
2437            ));
2438        }
2439        let outstanding = self.outstanding_device_revocations(device_public_key_hex);
2440        if self.cleared_device_revocation_event_ids.len() + outstanding.len()
2441            > MAX_ISSUE31_PROCESSED_EVENTS
2442        {
2443            return Err(Issue31NostrError::Invalid(
2444                "Issue 31 cleared revocation bound is exhausted".into(),
2445            ));
2446        }
2447        for event_id in &outstanding {
2448            self.cleared_device_revocation_event_ids
2449                .insert(event_id.clone());
2450        }
2451        Ok(outstanding)
2452    }
2453
2454    /// Re-admit the device behind a grant without ever naming its public key.
2455    ///
2456    /// The owner-facing grant projection deliberately shows only a fingerprint,
2457    /// so the owner acts on a `grant_ref` and the host resolves the device.
2458    pub fn readmit_device_for_grant(
2459        &mut self,
2460        grant_ref: &str,
2461    ) -> Result<Vec<String>, Issue31NostrError> {
2462        let grant = fold_issue31_grant(&self.pairing_events, grant_ref)?.ok_or_else(|| {
2463            Issue31NostrError::Invalid("cannot re-admit the device of an unknown grant".into())
2464        })?;
2465        if grant.status != Issue31GrantStatus::Revoked {
2466            return Err(Issue31NostrError::Invalid(
2467                "re-admission applies only to a revoked grant".into(),
2468            ));
2469        }
2470        let device_public_key_hex = grant.device_public_key_hex;
2471        self.readmit_device(&device_public_key_hex)
2472    }
2473
2474    pub fn validate_persisted_state(&self) -> Result<(), Issue31NostrError> {
2475        Self::new(self.configuration.clone())?;
2476        if self.pairing_events.len() > MAX_ISSUE31_PAIRING_EVENTS
2477            || self.processed_pairing_event_ids.len() > MAX_ISSUE31_PROCESSED_EVENTS
2478            || self.processed_command_event_ids.len() > MAX_ISSUE31_PROCESSED_EVENTS
2479            || self.command_results.len() > MAX_ISSUE31_COMMAND_RESULTS
2480            || self.command_results_v2.len() > MAX_ISSUE31_COMMAND_RESULTS
2481            || self.cleared_device_revocation_event_ids.len() > MAX_ISSUE31_PROCESSED_EVENTS
2482            || self
2483                .projected_source_event_ids
2484                .values()
2485                .map(BTreeSet::len)
2486                .sum::<usize>()
2487                > MAX_ISSUE31_PROJECTED_SOURCE_EVENTS
2488        {
2489            return Err(Issue31NostrError::Invalid(
2490                "persisted Issue 31 host state exceeds its bounds".into(),
2491            ));
2492        }
2493        for event in &self.pairing_events {
2494            if !valid_hex64(&event.event_id) {
2495                return Err(Issue31NostrError::Invalid(
2496                    "persisted pairing event id is invalid".into(),
2497                ));
2498            }
2499            event.record.validate()?;
2500            ensure_pairing_targets_host(&event.record, &self.configuration)?;
2501        }
2502        if self
2503            .processed_pairing_event_ids
2504            .iter()
2505            .chain(&self.processed_command_event_ids)
2506            .chain(&self.cleared_device_revocation_event_ids)
2507            .any(|event_id| !valid_hex64(event_id))
2508        {
2509            return Err(Issue31NostrError::Invalid(
2510                "persisted processed event id is invalid".into(),
2511            ));
2512        }
2513        // A cleared revocation must name a revocation that actually exists in the
2514        // durable log. Otherwise persisted state could pre-clear a revocation the
2515        // owner has not yet issued and silently unblock a device on arrival.
2516        for event_id in &self.cleared_device_revocation_event_ids {
2517            if !self.pairing_events.iter().any(|event| {
2518                event.event_id == *event_id
2519                    && matches!(&event.record, Issue31PairingRecord::GrantRevocation { .. })
2520            }) {
2521                return Err(Issue31NostrError::Invalid(
2522                    "persisted device re-admission clears an unknown revocation".into(),
2523                ));
2524            }
2525        }
2526        for (idempotency_ref, (intent, result)) in &self.command_results {
2527            validate_ref_value(idempotency_ref, "persisted idempotency")?;
2528            intent.validate()?;
2529            result.validate()?;
2530            let Issue31CommandRecord::CommandIntent {
2531                idempotency_ref: intent_idempotency_ref,
2532                issued_at,
2533                ..
2534            } = intent
2535            else {
2536                return Err(Issue31NostrError::Invalid(
2537                    "persisted command result omitted its intent".into(),
2538                ));
2539            };
2540            let Issue31CommandRecord::CommandResult {
2541                intent_event_id,
2542                idempotency_ref: result_idempotency_ref,
2543                completed_at,
2544                ..
2545            } = result
2546            else {
2547                return Err(Issue31NostrError::Invalid(
2548                    "persisted command result stored a second intent".into(),
2549                ));
2550            };
2551            if intent.binding() != result.binding()
2552                || intent_idempotency_ref != idempotency_ref
2553                || result_idempotency_ref != idempotency_ref
2554                || completed_at < issued_at
2555                || !self.processed_command_event_ids.contains(intent_event_id)
2556            {
2557                return Err(Issue31NostrError::Invalid(
2558                    "persisted command result changes its intent binding".into(),
2559                ));
2560            }
2561        }
2562        for (idempotency_ref, (intent, result)) in &self.command_results_v2 {
2563            validate_ref_value(idempotency_ref, "persisted idempotency")?;
2564            intent.validate()?;
2565            result.validate()?;
2566            let Issue31CommandRecordV2::CommandIntent {
2567                idempotency_ref: intent_idempotency_ref,
2568                issued_at,
2569                arguments,
2570                ..
2571            } = intent
2572            else {
2573                return Err(Issue31NostrError::Invalid(
2574                    "persisted command v2 result omitted its intent".into(),
2575                ));
2576            };
2577            let Issue31CommandRecordV2::CommandResult {
2578                intent_event_id,
2579                action_ref,
2580                idempotency_ref: result_idempotency_ref,
2581                handled_at,
2582                ..
2583            } = result
2584            else {
2585                return Err(Issue31NostrError::Invalid(
2586                    "persisted command v2 result stored a second intent".into(),
2587                ));
2588            };
2589            if intent.binding() != result.binding()
2590                || action_ref != arguments.action_ref()
2591                || intent_idempotency_ref != idempotency_ref
2592                || result_idempotency_ref != idempotency_ref
2593                || handled_at < issued_at
2594                || !self.processed_command_event_ids.contains(intent_event_id)
2595            {
2596                return Err(Issue31NostrError::Invalid(
2597                    "persisted command v2 result changes its intent binding".into(),
2598                ));
2599            }
2600        }
2601        for (grant_ref, source_event_ids) in &self.projected_source_event_ids {
2602            validate_ref_value(grant_ref, "projected grant")?;
2603            if source_event_ids
2604                .iter()
2605                .any(|event_id| !valid_hex64(event_id))
2606            {
2607                return Err(Issue31NostrError::Invalid(
2608                    "persisted projected source event id is invalid".into(),
2609                ));
2610            }
2611        }
2612        Ok(())
2613    }
2614
2615    pub fn record_emitted_pairing(
2616        &mut self,
2617        event_id: String,
2618        record: Issue31PairingRecord,
2619    ) -> Result<(), Issue31NostrError> {
2620        ensure_pairing_targets_host(&record, &self.configuration)?;
2621        record.validate_private_binding(
2622            &self.configuration.host_public_key_hex,
2623            record_device_public_key(&record),
2624        )?;
2625        self.insert_pairing_event(Issue31PairingEvent { event_id, record })
2626    }
2627
2628    pub fn handle_pairing_event(
2629        &mut self,
2630        event: Issue31PairingEvent,
2631        now: u64,
2632    ) -> Result<Option<Issue31PairingRecord>, Issue31NostrError> {
2633        if self.processed_pairing_event_ids.contains(&event.event_id) {
2634            return Ok(None);
2635        }
2636        event.record.validate()?;
2637        ensure_pairing_targets_host(&event.record, &self.configuration)?;
2638        if matches!(
2639            &event.record,
2640            Issue31PairingRecord::PairingRequest {
2641                device_public_key_hex,
2642                requested_scopes,
2643                ..
2644            } if self
2645                .admitted_device_scopes
2646                .get(device_public_key_hex)
2647                .is_none_or(|approved| requested_scopes.iter().all(|scope| !approved.contains(scope)))
2648        ) {
2649            return Ok(None);
2650        }
2651        if matches!(
2652            &event.record,
2653            Issue31PairingRecord::PairingRequest { device_public_key_hex, .. }
2654                | Issue31PairingRecord::PairingResponse { device_public_key_hex, .. }
2655                if self.device_admission_is_revoked(device_public_key_hex)
2656        ) {
2657            return Err(Issue31NostrError::Invalid(
2658                "device admission was revoked; owner re-admission is required".into(),
2659            ));
2660        }
2661        let outbound = match &event.record {
2662            Issue31PairingRecord::PairingRequest {
2663                device_public_key_hex,
2664                issued_at,
2665                expires_at,
2666                ..
2667            } => {
2668                require_live_record(*issued_at, *expires_at, now, "pairing request")?;
2669                let challenge_seed = rand::random::<[u8; 32]>();
2670                let challenge = format!("{:x}", Sha256::digest(challenge_seed));
2671                let event_digest = digest_ref_suffix(event.event_id.as_bytes());
2672                Some(Issue31PairingRecord::PairingChallenge {
2673                    schema: ISSUE31_PAIRING_SCHEMA.into(),
2674                    host_ref: self.configuration.host_ref.clone(),
2675                    host_public_key_hex: self.configuration.host_public_key_hex.clone(),
2676                    device_public_key_hex: device_public_key_hex.clone(),
2677                    issued_at: now,
2678                    pairing_challenge_ref: format!("pairing_challenge.omega.{event_digest}"),
2679                    pairing_request_event_id: event.event_id.clone(),
2680                    challenge,
2681                    expires_at: (*expires_at).min(now.saturating_add(600)),
2682                })
2683            }
2684            Issue31PairingRecord::PairingResponse {
2685                device_public_key_hex,
2686                issued_at,
2687                pairing_challenge_event_id,
2688                challenge: response_challenge,
2689                expires_at,
2690                ..
2691            } => {
2692                require_live_record(*issued_at, *expires_at, now, "pairing response")?;
2693                let challenge = self
2694                    .pairing_events
2695                    .iter()
2696                    .find(|candidate| candidate.event_id == *pairing_challenge_event_id);
2697                let Some(Issue31PairingEvent {
2698                    record:
2699                        Issue31PairingRecord::PairingChallenge {
2700                            pairing_request_event_id,
2701                            challenge,
2702                            expires_at: challenge_expires_at,
2703                            ..
2704                        },
2705                    ..
2706                }) = challenge
2707                else {
2708                    return Err(Issue31NostrError::Invalid(
2709                        "pairing response references an unknown challenge".into(),
2710                    ));
2711                };
2712                if response_challenge != challenge || now >= *challenge_expires_at {
2713                    return Err(Issue31NostrError::Invalid(
2714                        "pairing response challenge is wrong or expired".into(),
2715                    ));
2716                }
2717                let request = self
2718                    .pairing_events
2719                    .iter()
2720                    .find(|candidate| candidate.event_id == *pairing_request_event_id);
2721                let Some(Issue31PairingEvent {
2722                    record:
2723                        Issue31PairingRecord::PairingRequest {
2724                            requested_scopes,
2725                            device_public_key_hex: request_device_key,
2726                            ..
2727                        },
2728                    ..
2729                }) = request
2730                else {
2731                    return Err(Issue31NostrError::Invalid(
2732                        "pairing challenge references an unknown request".into(),
2733                    ));
2734                };
2735                if request_device_key != device_public_key_hex {
2736                    return Err(Issue31NostrError::Invalid(
2737                        "pairing response changes device identity".into(),
2738                    ));
2739                }
2740                let approved_scopes = self
2741                    .admitted_device_scopes
2742                    .get(device_public_key_hex)
2743                    .ok_or_else(|| {
2744                        Issue31NostrError::Invalid("device no longer has owner admission".into())
2745                    })?;
2746                let scopes = requested_scopes
2747                    .iter()
2748                    .copied()
2749                    .filter(|scope| approved_scopes.contains(scope))
2750                    .collect::<Vec<_>>();
2751                validate_scopes(&scopes, "owner-approved grant scopes")?;
2752                let event_digest = digest_ref_suffix(event.event_id.as_bytes());
2753                Some(Issue31PairingRecord::ScopedGrant {
2754                    schema: ISSUE31_PAIRING_SCHEMA.into(),
2755                    host_ref: self.configuration.host_ref.clone(),
2756                    host_public_key_hex: self.configuration.host_public_key_hex.clone(),
2757                    sarah_public_key_hex: self.configuration.sarah_public_key_hex.clone(),
2758                    device_public_key_hex: device_public_key_hex.clone(),
2759                    issued_at: now,
2760                    pairing_response_event_id: event.event_id.clone(),
2761                    grant_ref: format!("grant.omega.{event_digest}"),
2762                    generation: 1,
2763                    scopes,
2764                    expires_at: now.saturating_add(24 * 60 * 60),
2765                })
2766            }
2767            Issue31PairingRecord::PairingChallenge { .. }
2768            | Issue31PairingRecord::ScopedGrant { .. }
2769            | Issue31PairingRecord::GrantRenewal { .. }
2770            | Issue31PairingRecord::GrantRevocation { .. } => None,
2771        };
2772        self.insert_pairing_event(event)?;
2773        Ok(outbound)
2774    }
2775
2776    pub fn renew_grant(
2777        &self,
2778        grant_ref: &str,
2779        scopes: Vec<Issue31PairingScope>,
2780        now: u64,
2781        expires_at: u64,
2782    ) -> Result<Issue31PairingRecord, Issue31NostrError> {
2783        validate_scopes(&scopes, "renewal scopes")?;
2784        validate_lifetime(now, expires_at)?;
2785        let prior = fold_issue31_grant(&self.pairing_events, grant_ref)?
2786            .ok_or_else(|| Issue31NostrError::Invalid("cannot renew an unknown grant".into()))?;
2787        if prior.status != Issue31GrantStatus::Active
2788            || scopes.iter().any(|scope| !prior.scopes.contains(scope))
2789        {
2790            return Err(Issue31NostrError::Invalid(
2791                "renewal requires an active grant and cannot widen scope".into(),
2792            ));
2793        }
2794        Ok(Issue31PairingRecord::GrantRenewal {
2795            schema: ISSUE31_PAIRING_SCHEMA.into(),
2796            host_ref: prior.host_ref,
2797            host_public_key_hex: prior.host_public_key_hex,
2798            sarah_public_key_hex: prior.sarah_public_key_hex,
2799            device_public_key_hex: prior.device_public_key_hex,
2800            issued_at: now,
2801            grant_ref: grant_ref.to_string(),
2802            previous_grant_event_id: prior.source_event_id,
2803            prior_generation: prior.generation,
2804            generation: prior.generation.saturating_add(1),
2805            scopes,
2806            expires_at,
2807        })
2808    }
2809
2810    pub fn revoke_grant(
2811        &self,
2812        grant_ref: &str,
2813        now: u64,
2814        reason_ref: Option<String>,
2815    ) -> Result<Issue31PairingRecord, Issue31NostrError> {
2816        let prior = fold_issue31_grant(&self.pairing_events, grant_ref)?
2817            .ok_or_else(|| Issue31NostrError::Invalid("cannot revoke an unknown grant".into()))?;
2818        if prior.status == Issue31GrantStatus::Revoked {
2819            return Err(Issue31NostrError::Invalid(
2820                "grant is already revoked".into(),
2821            ));
2822        }
2823        let record = Issue31PairingRecord::GrantRevocation {
2824            schema: ISSUE31_PAIRING_SCHEMA.into(),
2825            host_ref: prior.host_ref,
2826            host_public_key_hex: prior.host_public_key_hex,
2827            sarah_public_key_hex: prior.sarah_public_key_hex,
2828            device_public_key_hex: prior.device_public_key_hex,
2829            issued_at: now,
2830            grant_ref: grant_ref.to_string(),
2831            generation: prior.generation,
2832            reason_ref,
2833        };
2834        record.validate()?;
2835        Ok(record)
2836    }
2837
2838    pub fn handle_command_event<F>(
2839        &mut self,
2840        event: Issue31CommandEvent,
2841        now: u64,
2842        execute: F,
2843    ) -> Result<Option<Issue31CommandRecord>, Issue31NostrError>
2844    where
2845        F: FnOnce(&str, &str, &str) -> Issue31CommandExecution,
2846    {
2847        if !valid_hex64(&event.event_id) {
2848            return Err(Issue31NostrError::Invalid(
2849                "Issue 31 command event id is invalid".into(),
2850            ));
2851        }
2852        if self.processed_command_event_ids.contains(&event.event_id) {
2853            return Ok(None);
2854        }
2855        event.record.validate()?;
2856        if self.processed_command_event_ids.len() >= MAX_ISSUE31_PROCESSED_EVENTS {
2857            return Err(Issue31NostrError::Invalid(
2858                "Issue 31 processed command event bound is exhausted".into(),
2859            ));
2860        }
2861        let Issue31CommandRecord::CommandIntent {
2862            host_ref,
2863            host_public_key_hex,
2864            device_public_key_hex,
2865            grant_ref,
2866            action_ref,
2867            idempotency_ref,
2868            expected_generation,
2869            arguments_ref,
2870            issued_at,
2871            expires_at,
2872            ..
2873        } = &event.record
2874        else {
2875            self.processed_command_event_ids.insert(event.event_id);
2876            return Ok(None);
2877        };
2878        if let Some((prior_intent, prior_result)) = self.command_results.get(idempotency_ref) {
2879            if prior_intent == &event.record {
2880                self.processed_command_event_ids.insert(event.event_id);
2881                return Ok(Some(prior_result.clone()));
2882            }
2883            return Err(Issue31NostrError::Invalid(format!(
2884                "idempotency ref {idempotency_ref} conflicts with an earlier command"
2885            )));
2886        }
2887        if self.command_results.len() >= MAX_ISSUE31_COMMAND_RESULTS {
2888            return Err(Issue31NostrError::Invalid(
2889                "Issue 31 command result bound is exhausted".into(),
2890            ));
2891        }
2892
2893        let refusal = |reason: &str| Issue31CommandExecution {
2894            status: Issue31CommandStatus::Refused,
2895            outcome_ref: "outcome.omega.refused".into(),
2896            reason_ref: Some(reason.into()),
2897        };
2898        let execution = if host_ref != &self.configuration.host_ref
2899            || host_public_key_hex != &self.configuration.host_public_key_hex
2900        {
2901            refusal("reason.omega.host_binding_mismatch")
2902        } else if require_live_record(*issued_at, *expires_at, now, "command intent").is_err() {
2903            refusal("reason.omega.command_expired")
2904        } else {
2905            let grant = fold_issue31_grant(&self.pairing_events, grant_ref)?;
2906            match grant {
2907                Some(grant)
2908                    if grant.status == Issue31GrantStatus::Active
2909                        && grant.device_public_key_hex == *device_public_key_hex
2910                        && grant.generation == *expected_generation
2911                        && grant.expires_at.is_some_and(|expires_at| now < expires_at) =>
2912                {
2913                    match required_scope(action_ref) {
2914                        Some(scope) if grant.scopes.contains(&scope) => {
2915                            // The idempotency reference travels with the action
2916                            // so the executor can make one command map to one
2917                            // host record however many times it is replayed
2918                            // (omega#91).
2919                            execute(action_ref, arguments_ref, idempotency_ref)
2920                        }
2921                        Some(_) => refusal("reason.omega.scope_denied"),
2922                        None => Issue31CommandExecution {
2923                            status: Issue31CommandStatus::Unavailable,
2924                            outcome_ref: "outcome.omega.unavailable".into(),
2925                            reason_ref: Some("reason.omega.action_unsupported".into()),
2926                        },
2927                    }
2928                }
2929                _ => refusal("reason.omega.grant_invalid"),
2930            }
2931        };
2932        let result = Issue31CommandRecord::CommandResult {
2933            schema: ISSUE31_COMMAND_SCHEMA.into(),
2934            host_ref: host_ref.clone(),
2935            host_public_key_hex: host_public_key_hex.clone(),
2936            device_public_key_hex: device_public_key_hex.clone(),
2937            grant_ref: grant_ref.clone(),
2938            intent_event_id: event.event_id.clone(),
2939            action_ref: action_ref.clone(),
2940            idempotency_ref: idempotency_ref.clone(),
2941            expected_generation: *expected_generation,
2942            status: execution.status,
2943            outcome_ref: execution.outcome_ref,
2944            reason_ref: execution.reason_ref,
2945            completed_at: now,
2946        };
2947        result.validate()?;
2948        self.processed_command_event_ids.insert(event.event_id);
2949        self.command_results
2950            .insert(idempotency_ref.clone(), (event.record, result.clone()));
2951        Ok(Some(result))
2952    }
2953
2954    pub fn handle_command_event_v2<F>(
2955        &mut self,
2956        event_id: String,
2957        record: Issue31CommandRecordV2,
2958        now: u64,
2959        execute: F,
2960    ) -> Result<Option<Issue31CommandRecordV2>, Issue31NostrError>
2961    where
2962        F: FnOnce(&Issue31CommandArguments, &str, &str, &str, u64) -> Issue31CommandExecutionV2,
2963    {
2964        if !valid_hex64(&event_id) {
2965            return Err(Issue31NostrError::Invalid(
2966                "Issue 31 command v2 event id is invalid".into(),
2967            ));
2968        }
2969        if self.processed_command_event_ids.contains(&event_id) {
2970            return Ok(None);
2971        }
2972        record.validate()?;
2973        if self.processed_command_event_ids.len() >= MAX_ISSUE31_PROCESSED_EVENTS {
2974            return Err(Issue31NostrError::Invalid(
2975                "Issue 31 processed command event bound is exhausted".into(),
2976            ));
2977        }
2978        let Issue31CommandRecordV2::CommandIntent {
2979            host_ref,
2980            host_public_key_hex,
2981            device_public_key_hex,
2982            grant_ref,
2983            idempotency_ref,
2984            expected_generation,
2985            arguments,
2986            issued_at,
2987            expires_at,
2988            ..
2989        } = &record
2990        else {
2991            self.processed_command_event_ids.insert(event_id);
2992            return Ok(None);
2993        };
2994        if let Some((prior_intent, prior_result)) = self.command_results_v2.get(idempotency_ref) {
2995            if prior_intent == &record {
2996                self.processed_command_event_ids.insert(event_id);
2997                return Ok(Some(prior_result.clone()));
2998            }
2999            return Err(Issue31NostrError::Invalid(format!(
3000                "idempotency ref {idempotency_ref} conflicts with an earlier command v2"
3001            )));
3002        }
3003        if self.command_results_v2.len() >= MAX_ISSUE31_COMMAND_RESULTS {
3004            return Err(Issue31NostrError::Invalid(
3005                "Issue 31 command v2 result bound is exhausted".into(),
3006            ));
3007        }
3008
3009        let refusal = |reason: &str| Issue31CommandExecutionV2 {
3010            status: Issue31CommandHandlingStatus::Refused,
3011            handling_ref: "handling.omega.refused".into(),
3012            reason_ref: Some(reason.into()),
3013            source_event_id: None,
3014        };
3015        let execution = if host_ref != &self.configuration.host_ref
3016            || host_public_key_hex != &self.configuration.host_public_key_hex
3017        {
3018            refusal("reason.omega.host_binding_mismatch")
3019        } else if require_live_record(*issued_at, *expires_at, now, "command v2 intent").is_err() {
3020            refusal("reason.omega.command_expired")
3021        } else {
3022            let grant = fold_issue31_grant(&self.pairing_events, grant_ref)?;
3023            match grant {
3024                Some(grant)
3025                    if grant.status == Issue31GrantStatus::Active
3026                        && grant.device_public_key_hex == *device_public_key_hex
3027                        && grant.generation == *expected_generation
3028                        && grant.expires_at.is_some_and(|expires_at| now < expires_at) =>
3029                {
3030                    let required_scope = required_scope(arguments.action_ref());
3031                    match required_scope {
3032                        Some(scope) if grant.scopes.contains(&scope) => execute(
3033                            arguments,
3034                            idempotency_ref,
3035                            grant_ref,
3036                            device_public_key_hex,
3037                            *expected_generation,
3038                        ),
3039                        Some(_) => refusal("reason.omega.scope_denied"),
3040                        None => Issue31CommandExecutionV2 {
3041                            status: Issue31CommandHandlingStatus::Unavailable,
3042                            handling_ref: "handling.omega.unavailable".into(),
3043                            reason_ref: Some("reason.omega.action_unsupported".into()),
3044                            source_event_id: None,
3045                        },
3046                    }
3047                }
3048                _ => refusal("reason.omega.grant_invalid"),
3049            }
3050        };
3051        let result = Issue31CommandRecordV2::CommandResult {
3052            schema: ISSUE31_COMMAND_SCHEMA_V2.into(),
3053            host_ref: host_ref.clone(),
3054            host_public_key_hex: host_public_key_hex.clone(),
3055            device_public_key_hex: device_public_key_hex.clone(),
3056            grant_ref: grant_ref.clone(),
3057            intent_event_id: event_id.clone(),
3058            action_ref: arguments.action_ref().into(),
3059            idempotency_ref: idempotency_ref.clone(),
3060            expected_generation: *expected_generation,
3061            status: execution.status,
3062            handling_ref: execution.handling_ref,
3063            reason_ref: execution.reason_ref,
3064            source_event_id: execution.source_event_id,
3065            handled_at: now,
3066        };
3067        result.validate()?;
3068        self.processed_command_event_ids.insert(event_id);
3069        self.command_results_v2
3070            .insert(idempotency_ref.clone(), (record, result.clone()));
3071        Ok(Some(result))
3072    }
3073
3074    pub fn active_grants(&self, now: u64) -> Result<Vec<Issue31GrantState>, Issue31NostrError> {
3075        let grant_refs = self
3076            .pairing_events
3077            .iter()
3078            .filter_map(|event| {
3079                event
3080                    .record
3081                    .lifecycle_binding()
3082                    .map(|binding| binding.4.to_string())
3083            })
3084            .collect::<BTreeSet<_>>();
3085        let mut grants = Vec::new();
3086        for grant_ref in grant_refs {
3087            let Some(grant) = fold_issue31_grant(&self.pairing_events, &grant_ref)? else {
3088                continue;
3089            };
3090            if grant.status == Issue31GrantStatus::Active
3091                && grant.expires_at.is_some_and(|expires_at| now < expires_at)
3092                && grant.scopes.contains(&Issue31PairingScope::ObserveIssue31)
3093            {
3094                grants.push(grant);
3095            }
3096        }
3097        Ok(grants)
3098    }
3099
3100    pub fn source_was_projected(
3101        &self,
3102        grant_ref: &str,
3103        generation: u64,
3104        source_event_id: &str,
3105    ) -> bool {
3106        let projection_ref = format!("{grant_ref}:{generation}");
3107        self.projected_source_event_ids
3108            .get(&projection_ref)
3109            .is_some_and(|event_ids| event_ids.contains(source_event_id))
3110    }
3111
3112    pub fn record_source_projection(
3113        &mut self,
3114        grant_ref: String,
3115        generation: u64,
3116        source_event_id: String,
3117    ) -> Result<(), Issue31NostrError> {
3118        validate_ref_value(&grant_ref, "projected grant")?;
3119        validate_generation(generation)?;
3120        validate_hex_value(&source_event_id, "projected source event")?;
3121        let projection_ref = format!("{grant_ref}:{generation}");
3122        let projected_count = self
3123            .projected_source_event_ids
3124            .values()
3125            .map(BTreeSet::len)
3126            .sum::<usize>();
3127        if projected_count >= MAX_ISSUE31_PROJECTED_SOURCE_EVENTS
3128            && !self.source_was_projected(&grant_ref, generation, &source_event_id)
3129        {
3130            return Err(Issue31NostrError::Invalid(
3131                "Issue 31 projected source event bound is exhausted".into(),
3132            ));
3133        }
3134        self.projected_source_event_ids
3135            .entry(projection_ref)
3136            .or_default()
3137            .insert(source_event_id);
3138        Ok(())
3139    }
3140
3141    fn insert_pairing_event(
3142        &mut self,
3143        event: Issue31PairingEvent,
3144    ) -> Result<(), Issue31NostrError> {
3145        if !valid_hex64(&event.event_id) {
3146            return Err(Issue31NostrError::Invalid(
3147                "Issue 31 pairing event id is invalid".into(),
3148            ));
3149        }
3150        if let Some(prior) = self
3151            .pairing_events
3152            .iter()
3153            .find(|prior| prior.event_id == event.event_id)
3154        {
3155            if prior.record != event.record {
3156                return Err(Issue31NostrError::Invalid(format!(
3157                    "Issue 31 event {} has conflicting records",
3158                    event.event_id
3159                )));
3160            }
3161            return Ok(());
3162        }
3163        if self.pairing_events.len() >= MAX_ISSUE31_PAIRING_EVENTS
3164            || self.processed_pairing_event_ids.len() >= MAX_ISSUE31_PROCESSED_EVENTS
3165        {
3166            return Err(Issue31NostrError::Invalid(
3167                "Issue 31 pairing event bound is exhausted".into(),
3168            ));
3169        }
3170        self.processed_pairing_event_ids
3171            .insert(event.event_id.clone());
3172        self.pairing_events.push(event);
3173        Ok(())
3174    }
3175}
3176
3177fn ensure_pairing_targets_host(
3178    record: &Issue31PairingRecord,
3179    configuration: &Issue31HostConfiguration,
3180) -> Result<(), Issue31NostrError> {
3181    let (_, host_ref, host_public_key_hex, _, _) = record.base();
3182    if host_ref != configuration.host_ref
3183        || host_public_key_hex != configuration.host_public_key_hex
3184    {
3185        return Err(Issue31NostrError::Invalid(
3186            "pairing record targets another host".into(),
3187        ));
3188    }
3189    let sarah_public_key_hex = match record {
3190        Issue31PairingRecord::ScopedGrant {
3191            sarah_public_key_hex,
3192            ..
3193        }
3194        | Issue31PairingRecord::GrantRenewal {
3195            sarah_public_key_hex,
3196            ..
3197        }
3198        | Issue31PairingRecord::GrantRevocation {
3199            sarah_public_key_hex,
3200            ..
3201        } => Some(sarah_public_key_hex),
3202        Issue31PairingRecord::PairingRequest { .. }
3203        | Issue31PairingRecord::PairingChallenge { .. }
3204        | Issue31PairingRecord::PairingResponse { .. } => None,
3205    };
3206    if sarah_public_key_hex.is_some_and(|key| key != &configuration.sarah_public_key_hex) {
3207        return Err(Issue31NostrError::Invalid(
3208            "pairing grant binds another Sarah identity".into(),
3209        ));
3210    }
3211    Ok(())
3212}
3213
3214fn record_device_public_key(record: &Issue31PairingRecord) -> &str {
3215    let (_, _, _, device_public_key_hex, _) = record.base();
3216    device_public_key_hex
3217}
3218
3219fn require_live_record(
3220    issued_at: u64,
3221    expires_at: u64,
3222    now: u64,
3223    label: &str,
3224) -> Result<(), Issue31NostrError> {
3225    validate_lifetime(issued_at, expires_at)?;
3226    if issued_at > now.saturating_add(300) || now >= expires_at {
3227        return Err(Issue31NostrError::Invalid(format!(
3228            "{label} is future-dated or expired"
3229        )));
3230    }
3231    Ok(())
3232}
3233
3234fn required_scope(action_ref: &str) -> Option<Issue31PairingScope> {
3235    match action_ref {
3236        "action.omega.send_message" | ISSUE31_ACTION_SEND_MESSAGE => {
3237            Some(Issue31PairingScope::SendMessage)
3238        }
3239        "action.omega.interrupt_turn" | ISSUE31_ACTION_INTERRUPT_TURN => {
3240            Some(Issue31PairingScope::InterruptTurn)
3241        }
3242        ISSUE31_ACTION_ADVANCE_READ_STATE
3243        | ISSUE31_ACTION_CREATE_REMINDER
3244        | ISSUE31_ACTION_CHANGE_REMINDER
3245        | ISSUE31_ACTION_COMPLETE_REMINDER
3246        | ISSUE31_ACTION_CANCEL_REMINDER => Some(Issue31PairingScope::ObserveIssue31),
3247        "action.omega.full_auto.stop"
3248        | "action.omega.full_auto.pause"
3249        | "action.omega.full_auto.resume" => Some(Issue31PairingScope::ControlFullAuto),
3250        "action.omega.provider_handoff" => Some(Issue31PairingScope::RequestProviderHandoff),
3251        "action.omega.community.act" => Some(Issue31PairingScope::ActInCommunity),
3252        _ => None,
3253    }
3254}
3255
3256fn digest_ref_suffix(bytes: &[u8]) -> String {
3257    let digest = format!("{:x}", Sha256::digest(bytes));
3258    digest[..24].to_string()
3259}
3260
3261/// One device paired all the way to a signed scoped grant, through the real
3262/// pairing state machine.
3263///
3264/// Shared so a test that needs a differently-scoped grant does not grow a
3265/// second, subtly different copy of the chain.
3266#[cfg(test)]
3267pub(crate) fn paired_fixture(
3268    scopes: Vec<Issue31PairingScope>,
3269) -> (
3270    Issue31HostConfiguration,
3271    Issue31HostController,
3272    String,
3273    String,
3274) {
3275    let host_public_key_hex = "1".repeat(64);
3276    let device_public_key_hex = "2".repeat(64);
3277    let configuration = Issue31HostConfiguration {
3278        host_ref: "omega.host.local".into(),
3279        host_public_key_hex: host_public_key_hex.clone(),
3280        sarah_public_key_hex: "3".repeat(64),
3281        conversation: "sarah.0123456789abcdef01234567".into(),
3282        display_name: "Local Omega".into(),
3283        relay_urls: vec!["wss://relay.example.com".into()],
3284        generation: 1,
3285    };
3286    let mut controller = Issue31HostController::new(configuration.clone()).expect("controller");
3287    controller
3288        .set_admitted_device_policy(vec![device_public_key_hex.clone()], scopes.clone())
3289        .expect("admit device");
3290    let challenge = controller
3291        .handle_pairing_event(
3292            Issue31PairingEvent {
3293                event_id: "a".repeat(64),
3294                record: Issue31PairingRecord::PairingRequest {
3295                    schema: ISSUE31_PAIRING_SCHEMA.into(),
3296                    host_ref: configuration.host_ref.clone(),
3297                    host_public_key_hex: host_public_key_hex.clone(),
3298                    device_public_key_hex: device_public_key_hex.clone(),
3299                    issued_at: 100,
3300                    pairing_request_ref: "pairing_request.restart.device".into(),
3301                    requested_scopes: scopes,
3302                    expires_at: 1_000,
3303                },
3304            },
3305            101,
3306        )
3307        .expect("request")
3308        .expect("challenge");
3309    let challenge_value = match &challenge {
3310        Issue31PairingRecord::PairingChallenge { challenge, .. } => challenge.clone(),
3311        _ => panic!("challenge"),
3312    };
3313    controller
3314        .record_emitted_pairing("b".repeat(64), challenge)
3315        .expect("record challenge");
3316    let grant = controller
3317        .handle_pairing_event(
3318            Issue31PairingEvent {
3319                event_id: "c".repeat(64),
3320                record: Issue31PairingRecord::PairingResponse {
3321                    schema: ISSUE31_PAIRING_SCHEMA.into(),
3322                    host_ref: configuration.host_ref.clone(),
3323                    host_public_key_hex,
3324                    device_public_key_hex: device_public_key_hex.clone(),
3325                    issued_at: 102,
3326                    pairing_response_ref: "pairing_response.restart.device".into(),
3327                    pairing_challenge_event_id: "b".repeat(64),
3328                    challenge: challenge_value,
3329                    expires_at: 1_000,
3330                },
3331            },
3332            102,
3333        )
3334        .expect("response")
3335        .expect("grant");
3336    let grant_ref = match &grant {
3337        Issue31PairingRecord::ScopedGrant { grant_ref, .. } => grant_ref.clone(),
3338        _ => panic!("grant"),
3339    };
3340    controller
3341        .record_emitted_pairing("d".repeat(64), grant)
3342        .expect("record grant");
3343    (configuration, controller, device_public_key_hex, grant_ref)
3344}
3345
3346#[cfg(test)]
3347pub(crate) fn restart_fixture() -> (
3348    Issue31HostConfiguration,
3349    Issue31HostController,
3350    String,
3351    String,
3352) {
3353    let (configuration, mut controller, device_public_key_hex, grant_ref) =
3354        paired_fixture(vec![Issue31PairingScope::ControlFullAuto]);
3355    let host_public_key_hex = configuration.host_public_key_hex.clone();
3356    controller
3357        .handle_command_event(
3358            Issue31CommandEvent {
3359                event_id: "e".repeat(64),
3360                record: Issue31CommandRecord::CommandIntent {
3361                    schema: ISSUE31_COMMAND_SCHEMA.into(),
3362                    host_ref: configuration.host_ref.clone(),
3363                    host_public_key_hex,
3364                    device_public_key_hex: device_public_key_hex.clone(),
3365                    grant_ref: grant_ref.clone(),
3366                    action_ref: "action.omega.full_auto.stop".into(),
3367                    idempotency_ref: "idempotency.restart.first_stop".into(),
3368                    expected_generation: 1,
3369                    arguments_ref: "arguments.omega.none".into(),
3370                    issued_at: 103,
3371                    expires_at: 200,
3372                },
3373            },
3374            104,
3375            |_, _, _| Issue31CommandExecution {
3376                status: Issue31CommandStatus::Stopped,
3377                outcome_ref: "outcome.omega.stopped".into(),
3378                reason_ref: None,
3379            },
3380        )
3381        .expect("command")
3382        .expect("result");
3383    let revocation = controller
3384        .revoke_grant(&grant_ref, 105, Some("reason.omega.owner_revoked".into()))
3385        .expect("revoke");
3386    controller
3387        .record_emitted_pairing("f".repeat(64), revocation)
3388        .expect("record revoke");
3389    (configuration, controller, device_public_key_hex, grant_ref)
3390}
3391
3392fn validate_sarah_binding(
3393    host_public_key_hex: &str,
3394    sarah_public_key_hex: &str,
3395) -> Result<(), Issue31NostrError> {
3396    if !valid_hex64(sarah_public_key_hex) || sarah_public_key_hex == host_public_key_hex {
3397        return Err(Issue31NostrError::Invalid(
3398            "grant Sarah identity is invalid or aliases the host".into(),
3399        ));
3400    }
3401    Ok(())
3402}
3403
3404fn validate_generation(generation: u64) -> Result<(), Issue31NostrError> {
3405    if generation == 0 {
3406        Err(Issue31NostrError::Invalid(
3407            "generation must be positive".into(),
3408        ))
3409    } else {
3410        Ok(())
3411    }
3412}
3413
3414fn validate_lifetime(issued_at: u64, expires_at: u64) -> Result<(), Issue31NostrError> {
3415    if expires_at <= issued_at {
3416        Err(Issue31NostrError::Invalid(
3417            "expiration must follow issue time".into(),
3418        ))
3419    } else {
3420        Ok(())
3421    }
3422}
3423
3424fn validate_ref_value(value: &str, field: &str) -> Result<(), Issue31NostrError> {
3425    if valid_ref(value) {
3426        Ok(())
3427    } else {
3428        Err(Issue31NostrError::Invalid(format!("invalid {field} ref")))
3429    }
3430}
3431
3432fn validate_hex_value(value: &str, field: &str) -> Result<(), Issue31NostrError> {
3433    if valid_hex64(value) {
3434        Ok(())
3435    } else {
3436        Err(Issue31NostrError::Invalid(format!("invalid {field} id")))
3437    }
3438}
3439
3440fn validate_reminder_arguments(
3441    reminder_id: &str,
3442    note: Option<&str>,
3443    target_event_id: Option<&str>,
3444    not_before: u64,
3445    expiration: Option<u64>,
3446) -> Result<(), Issue31NostrError> {
3447    if !valid_hex32(reminder_id)
3448        || note.is_some_and(|note| note.len() > 4_096)
3449        || target_event_id.is_some_and(|event_id| !valid_hex64(event_id))
3450        || expiration.is_some_and(|expiration| expiration <= not_before)
3451    {
3452        return Err(Issue31NostrError::Invalid(
3453            "invalid Issue 31 reminder arguments".into(),
3454        ));
3455    }
3456    Ok(())
3457}
3458
3459fn validate_authority_projection(
3460    decision: &Issue31AuthorityDecisionProjection,
3461    outcome: &Issue31TargetOutcomeProjection,
3462) -> Result<(), Issue31NostrError> {
3463    if !matches!(decision.state.as_str(), "allowed" | "refused")
3464        || !valid_ref(&decision.decision_ref)
3465        || decision
3466            .reason_ref
3467            .as_ref()
3468            .is_some_and(|reason| !valid_ref(reason))
3469        || !matches!(
3470            outcome.state.as_str(),
3471            "pending" | "succeeded" | "failed" | "stopped" | "unavailable"
3472        )
3473        || outcome
3474            .outcome_ref
3475            .as_ref()
3476            .is_some_and(|reference| !valid_ref(reference))
3477        || outcome
3478            .reason_ref
3479            .as_ref()
3480            .is_some_and(|reason| !valid_ref(reason))
3481        || (outcome.state != "pending" && outcome.outcome_ref.is_none())
3482    {
3483        return Err(Issue31NostrError::Invalid(
3484            "invalid Issue 31 authority receipt projection".into(),
3485        ));
3486    }
3487    Ok(())
3488}
3489
3490fn valid_turn_payload(payload: &serde_json::Value) -> bool {
3491    let Some(object) = payload.as_object() else {
3492        return false;
3493    };
3494    let allowed_keys = [
3495        "schema",
3496        "entry",
3497        "conversation",
3498        "turnRef",
3499        "seq",
3500        "timestamp",
3501        "parents",
3502        "payload",
3503    ];
3504    if object.len() != allowed_keys.len()
3505        || object
3506            .keys()
3507            .any(|key| !allowed_keys.contains(&key.as_str()))
3508        || object.get("schema").and_then(serde_json::Value::as_str)
3509            != Some("openagents.sarah.turn_record.v1")
3510        || !object
3511            .get("entry")
3512            .and_then(serde_json::Value::as_str)
3513            .is_some_and(|entry| {
3514                matches!(
3515                    entry,
3516                    "turn.started"
3517                        | "tool.call"
3518                        | "tool.result"
3519                        | "tool.error"
3520                        | "turn.finished"
3521                        | "turn.interrupted"
3522                )
3523            })
3524        || !object
3525            .get("conversation")
3526            .and_then(serde_json::Value::as_str)
3527            .is_some_and(valid_conversation_tag)
3528        || !object
3529            .get("turnRef")
3530            .and_then(serde_json::Value::as_str)
3531            .is_some_and(|value| !value.is_empty() && value.len() <= 256)
3532        || !object
3533            .get("seq")
3534            .and_then(serde_json::Value::as_u64)
3535            .is_some_and(|sequence| sequence >= 1)
3536        || !object
3537            .get("timestamp")
3538            .and_then(serde_json::Value::as_str)
3539            .is_some_and(|timestamp| !timestamp.is_empty())
3540        || !object
3541            .get("payload")
3542            .is_some_and(serde_json::Value::is_object)
3543    {
3544        return false;
3545    }
3546    object
3547        .get("parents")
3548        .and_then(serde_json::Value::as_array)
3549        .is_some_and(|parents| {
3550            parents.iter().all(|parent| {
3551                let Some(parent) = parent.as_object() else {
3552                    return false;
3553                };
3554                parent.len() == 2
3555                    && parent
3556                        .get("eventId")
3557                        .and_then(serde_json::Value::as_str)
3558                        .is_some_and(valid_hex64)
3559                    && parent
3560                        .get("marker")
3561                        .and_then(serde_json::Value::as_str)
3562                        .is_some_and(|marker| {
3563                            matches!(
3564                                marker,
3565                                "prompt" | "reply" | "root" | "mention" | "tool" | "prior"
3566                            )
3567                        })
3568            })
3569        })
3570}
3571
3572fn valid_engram_plaintext(plaintext: &str) -> bool {
3573    let Ok(value) = serde_json::from_str::<serde_json::Value>(plaintext) else {
3574        return false;
3575    };
3576    let Some(object) = value.as_object() else {
3577        return false;
3578    };
3579    let Some(slug) = object.get("slug").and_then(serde_json::Value::as_str) else {
3580        return false;
3581    };
3582    if slug == "core" {
3583        return object
3584            .get("profile")
3585            .and_then(serde_json::Value::as_str)
3586            .is_some();
3587    }
3588    let valid_slug = slug.starts_with("mem/")
3589        && slug.len() <= 255
3590        && slug.split('/').skip(1).all(|segment| {
3591            !segment.is_empty()
3592                && segment.len() <= 64
3593                && segment.bytes().enumerate().all(|(index, byte)| {
3594                    byte.is_ascii_lowercase()
3595                        || byte.is_ascii_digit()
3596                        || (index > 0 && matches!(byte, b'_' | b'-'))
3597                })
3598        });
3599    valid_slug
3600        && object
3601            .get("value")
3602            .is_some_and(|value| value.is_null() || value.is_string())
3603}
3604
3605fn valid_read_state_plaintext(plaintext: &str) -> bool {
3606    let Ok(value) = serde_json::from_str::<serde_json::Value>(plaintext) else {
3607        return false;
3608    };
3609    let Some(object) = value.as_object() else {
3610        return false;
3611    };
3612    object.get("v").and_then(serde_json::Value::as_u64) == Some(1)
3613        && object
3614            .get("client_id")
3615            .and_then(serde_json::Value::as_str)
3616            .is_some_and(|client_id| !client_id.is_empty() && client_id.len() <= 64)
3617        && object
3618            .get("contexts")
3619            .and_then(serde_json::Value::as_object)
3620            .is_some_and(|contexts| contexts.len() <= 10_000)
3621}
3622
3623fn valid_reminder_plaintext(plaintext: &str, not_before: Option<u64>) -> bool {
3624    let Ok(value) = serde_json::from_str::<serde_json::Value>(plaintext) else {
3625        return false;
3626    };
3627    let Some(status) = value
3628        .as_object()
3629        .and_then(|object| object.get("status"))
3630        .and_then(serde_json::Value::as_str)
3631    else {
3632        return false;
3633    };
3634    matches!(status, "pending" | "done" | "cancelled")
3635        && (status != "pending" || not_before.is_some())
3636}
3637
3638fn validate_scopes(scopes: &[Issue31PairingScope], field: &str) -> Result<(), Issue31NostrError> {
3639    let unique: BTreeSet<Issue31PairingScope> = scopes.iter().copied().collect();
3640    if scopes.is_empty() || scopes.len() > 6 || unique.len() != scopes.len() {
3641        Err(Issue31NostrError::Invalid(format!("invalid {field}")))
3642    } else {
3643        Ok(())
3644    }
3645}
3646
3647fn valid_hex64(value: &str) -> bool {
3648    value.len() == 64
3649        && value
3650            .bytes()
3651            .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
3652}
3653
3654fn valid_hex32(value: &str) -> bool {
3655    value.len() == 32
3656        && value
3657            .bytes()
3658            .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
3659}
3660
3661fn valid_ref(value: &str) -> bool {
3662    if value.len() < 3 || value.len() > 256 {
3663        return false;
3664    }
3665    let mut colon_parts = value.split(':');
3666    let Some(path) = colon_parts.next() else {
3667        return false;
3668    };
3669    if let Some(suffix) = colon_parts.next()
3670        && (suffix.is_empty()
3671            || !suffix
3672                .bytes()
3673                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'_' | b'-')))
3674    {
3675        return false;
3676    }
3677    if colon_parts.next().is_some() {
3678        return false;
3679    }
3680    let segments: Vec<&str> = path.split('.').collect();
3681    if segments.len() < 2 {
3682        return false;
3683    }
3684    let Some(first) = segments.first() else {
3685        return false;
3686    };
3687    if !first
3688        .bytes()
3689        .next()
3690        .is_some_and(|byte| byte.is_ascii_lowercase())
3691        || !first.bytes().all(|byte| {
3692            byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'_' | b'-')
3693        })
3694    {
3695        return false;
3696    }
3697    segments.iter().skip(1).all(|segment| {
3698        segment
3699            .bytes()
3700            .next()
3701            .is_some_and(|byte| byte.is_ascii_alphanumeric())
3702            && segment
3703                .bytes()
3704                .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-'))
3705    })
3706}
3707
3708pub fn is_issue31_public_ref(value: &str) -> bool {
3709    valid_ref(value)
3710}
3711
3712fn valid_relay_url(value: &str) -> bool {
3713    if value.len() < 6 || value.len() > 512 {
3714        return false;
3715    }
3716    let Ok(url) = url::Url::parse(value) else {
3717        return false;
3718    };
3719    matches!(url.scheme(), "ws" | "wss")
3720        && url.host_str().is_some()
3721        && url.username().is_empty()
3722        && url.password().is_none()
3723        && url.query().is_none()
3724        && url.fragment().is_none()
3725}
3726
3727fn valid_conversation_tag(value: &str) -> bool {
3728    let Some(hex_part) = value.strip_prefix("sarah.") else {
3729        return false;
3730    };
3731    hex_part.len() == 24
3732        && hex_part
3733            .bytes()
3734            .all(|byte| byte.is_ascii_digit() || matches!(byte, b'a'..=b'f'))
3735}
3736
3737fn all_unique(values: &[String]) -> bool {
3738    values.iter().collect::<BTreeSet<_>>().len() == values.len()
3739}
3740
3741#[cfg(test)]
3742mod tests {
3743    use std::cell::Cell;
3744
3745    use super::*;
3746
3747    #[test]
3748    fn shared_fixtures_decode_and_reconcile() {
3749        let discovery = Issue31HostDiscovery::decode(include_bytes!(
3750            "../fixtures/openagents.omega.issue31.host_discovery.v1.canonical.json"
3751        ))
3752        .expect("host fixture");
3753        let pairing = Issue31PairingRecord::decode(include_bytes!(
3754            "../fixtures/openagents.omega.issue31.pairing.v1.canonical.json"
3755        ))
3756        .expect("pairing fixture");
3757        let intent = Issue31CommandRecord::decode(include_bytes!(
3758            "../fixtures/openagents.omega.issue31.command.v1.canonical-intent.json"
3759        ))
3760        .expect("intent fixture");
3761        let result = Issue31CommandRecord::decode(include_bytes!(
3762            "../fixtures/openagents.omega.issue31.command.v1.canonical-result.json"
3763        ))
3764        .expect("result fixture");
3765        assert_eq!(discovery.generation, 4);
3766        assert_eq!(discovery.sarah_public_key_hex, "3".repeat(64));
3767        assert!(matches!(
3768            pairing,
3769            Issue31PairingRecord::ScopedGrant {
3770                sarah_public_key_hex,
3771                ..
3772            } if sarah_public_key_hex == "3".repeat(64)
3773        ));
3774        let states = reconcile_issue31_commands(&[
3775            Issue31CommandEvent {
3776                event_id: "4".repeat(64),
3777                record: intent,
3778            },
3779            Issue31CommandEvent {
3780                event_id: "5".repeat(64),
3781                record: result,
3782            },
3783        ])
3784        .expect("reconcile");
3785        assert_eq!(states.len(), 1);
3786        assert!(states[0].result.is_some());
3787    }
3788
3789    #[test]
3790    fn shared_v2_fixtures_decode_with_canonical_hashes() {
3791        let discovery_bytes =
3792            include_bytes!("../fixtures/openagents.omega.issue31.host_discovery.v2.canonical.json");
3793        let intent_bytes =
3794            include_bytes!("../fixtures/openagents.omega.issue31.command.v2.canonical-intent.json");
3795        let result_bytes =
3796            include_bytes!("../fixtures/openagents.omega.issue31.command.v2.canonical-result.json");
3797        let projection_bytes = include_bytes!(
3798            "../fixtures/openagents.omega.issue31.owner_projection.v1.canonical.json"
3799        );
3800        let discovery = Issue31HostDiscoveryV2::decode(discovery_bytes).expect("v2 discovery");
3801        let intent = Issue31CommandRecordV2::decode(intent_bytes).expect("v2 intent");
3802        let result = Issue31CommandRecordV2::decode(result_bytes).expect("v2 result");
3803        let projection =
3804            Issue31OwnerProjectionRecord::decode(projection_bytes).expect("owner projection");
3805        projection
3806            .validate_private_binding(&"1".repeat(64), &"2".repeat(64), &"3".repeat(64))
3807            .expect("projection source binding");
3808
3809        assert_eq!(discovery.conversation, "sarah.0123456789abcdef01234567");
3810        assert!(matches!(
3811            intent,
3812            Issue31CommandRecordV2::CommandIntent { .. }
3813        ));
3814        assert!(matches!(
3815            result,
3816            Issue31CommandRecordV2::CommandResult {
3817                status: Issue31CommandHandlingStatus::Accepted,
3818                ..
3819            }
3820        ));
3821        assert!(matches!(
3822            projection.projection,
3823            Issue31OwnerProjectionBody::Message {
3824                role: Issue31SourceRole::Owner,
3825                ..
3826            }
3827        ));
3828        assert_eq!(
3829            format!("{:x}", Sha256::digest(discovery_bytes)),
3830            "a5604d4c792a5ed556f023e150f01b371c5cf702b95b72786e0c7a9adbbdcb1c"
3831        );
3832        assert_eq!(
3833            format!("{:x}", Sha256::digest(intent_bytes)),
3834            "7bb7b23680be10756184668ae7722c09c634a1941b086f66d0425da4e8371bbe"
3835        );
3836        assert_eq!(
3837            format!("{:x}", Sha256::digest(result_bytes)),
3838            "51bca57e14c3d45518c342c2d1f848972281de848f809c34566ed183c7e4e387"
3839        );
3840        assert_eq!(
3841            format!("{:x}", Sha256::digest(projection_bytes)),
3842            "2a8bec5fa23f27d20db35f3d76bd59817672431328f191bc4302dfa37e7f804d"
3843        );
3844    }
3845
3846    const OWNER_PROJECTION_BODY_FIXTURES: &[(&str, &str, &[u8])] = &[
3847        (
3848            "read-state",
3849            "efd96dbe997e021c8e77300a802ab929b8c05981a1029c509b94d47410afc264",
3850            include_bytes!(
3851                "../fixtures/openagents.omega.issue31.owner_projection.v1.canonical-read-state.json"
3852            ),
3853        ),
3854        (
3855            "reminder",
3856            "d21b2168d32d3c8e76294502b9a30a75a6f5f4f15c5195ac0c160fad15539fe3",
3857            include_bytes!(
3858                "../fixtures/openagents.omega.issue31.owner_projection.v1.canonical-reminder.json"
3859            ),
3860        ),
3861        (
3862            "authority-receipt",
3863            "50d97118aec8931e624856246ae5e187bb6944f750052383f89472c4e9e27733",
3864            include_bytes!(
3865                "../fixtures/openagents.omega.issue31.owner_projection.v1.canonical-authority-receipt.json"
3866            ),
3867        ),
3868        (
3869            "engram",
3870            "d499644feb77cb7d61b35fda9a4fbafe0b06fcc86e33a61985fbe36c1a819dae",
3871            include_bytes!(
3872                "../fixtures/openagents.omega.issue31.owner_projection.v1.canonical-engram.json"
3873            ),
3874        ),
3875    ];
3876
3877    const OWNER_PROJECTION_NEGATIVE_FIXTURES: &[(&str, &str, &[u8])] = &[
3878        (
3879            "read-state-role",
3880            "1073644580d2c8d8768866c81a26cb8b044b13da4297383d54962ff949982baa",
3881            include_bytes!(
3882                "../fixtures/openagents.omega.issue31.owner_projection.v1.negative-read-state-role.json"
3883            ),
3884        ),
3885        (
3886            "read-state-version",
3887            "9b134615eb992b2395c59dfc72abe8be3d472dd69c8dff73b7ec670757ebcfba",
3888            include_bytes!(
3889                "../fixtures/openagents.omega.issue31.owner_projection.v1.negative-read-state-version.json"
3890            ),
3891        ),
3892        (
3893            "reminder-pending-without-not-before",
3894            "e0f8a8c6a6f22c4b53326dbeb193eeda0832b8b8cbe0c2aae02ca7e1b70a1cec",
3895            include_bytes!(
3896                "../fixtures/openagents.omega.issue31.owner_projection.v1.negative-reminder-pending-without-not-before.json"
3897            ),
3898        ),
3899        (
3900            "authority-receipt-terminal-without-outcome",
3901            "737bbaa8fece20bf9b898f4f76f5bf6d4369a1b0a487984302f0cf9a8bd9cc3b",
3902            include_bytes!(
3903                "../fixtures/openagents.omega.issue31.owner_projection.v1.negative-authority-receipt-terminal-without-outcome.json"
3904            ),
3905        ),
3906        (
3907            "engram-slug",
3908            "3997fb6d470f20e4796f4765d2406e6e3f4a629f090b3c4ef26e77274cf7b6ed",
3909            include_bytes!(
3910                "../fixtures/openagents.omega.issue31.owner_projection.v1.negative-engram-slug.json"
3911            ),
3912        ),
3913    ];
3914
3915    /// The read-state, reminder, authority-receipt, and engram projection bodies
3916    /// are the three omega#46 exits that had no shared fixture and therefore no
3917    /// agreement between this host and the device reader. The pinned digests are
3918    /// the byte-sharing mechanism: the same digests are asserted by the
3919    /// TypeScript peer, so a one-sided edit fails on both sides.
3920    #[test]
3921    fn owner_projection_body_fixtures_decode_and_bind() {
3922        for (label, digest, bytes) in OWNER_PROJECTION_BODY_FIXTURES {
3923            assert_eq!(
3924                &format!("{:x}", Sha256::digest(bytes)),
3925                digest,
3926                "{label} fixture bytes changed"
3927            );
3928            let record = Issue31OwnerProjectionRecord::decode(bytes)
3929                .unwrap_or_else(|error| panic!("{label} fixture decodes: {error}"));
3930            record
3931                .validate_private_binding(&"1".repeat(64), &"2".repeat(64), &"3".repeat(64))
3932                .unwrap_or_else(|error| panic!("{label} fixture binds: {error}"));
3933        }
3934    }
3935
3936    #[test]
3937    fn owner_projection_negative_fixtures_are_refused() {
3938        for (label, digest, bytes) in OWNER_PROJECTION_NEGATIVE_FIXTURES {
3939            assert_eq!(
3940                &format!("{:x}", Sha256::digest(bytes)),
3941                digest,
3942                "{label} fixture bytes changed"
3943            );
3944            assert!(
3945                Issue31OwnerProjectionRecord::decode(bytes).is_err(),
3946                "{label} negative fixture must be refused"
3947            );
3948        }
3949    }
3950
3951    /// The fixtures are only shared truth if the host actually emits them. This
3952    /// drives the real emitter with each fixture's own inputs and requires the
3953    /// emitted bytes back.
3954    #[test]
3955    fn the_emitter_reproduces_every_canonical_body_fixture() {
3956        for (label, _, bytes) in OWNER_PROJECTION_BODY_FIXTURES {
3957            let expected =
3958                Issue31OwnerProjectionRecord::decode(bytes).expect("canonical fixture decodes");
3959            let emission = emit_issue31_owner_projection(Issue31OwnerProjectionInput {
3960                host_ref: &expected.host_ref,
3961                host_public_key_hex: &expected.host_public_key_hex,
3962                device_public_key_hex: &expected.device_public_key_hex,
3963                sarah_public_key_hex: &"3".repeat(64),
3964                grant_ref: &expected.grant_ref,
3965                expected_generation: expected.expected_generation,
3966                source_event_id: &expected.source_event_id,
3967                source_author_public_key_hex: &expected.source_author_public_key_hex,
3968                source_kind: expected.source_kind,
3969                source_created_at: expected.source_created_at,
3970                projected_at: expected.projected_at,
3971                projection: expected.projection.clone(),
3972            })
3973            .unwrap_or_else(|error| panic!("{label} emits: {error}"));
3974            assert_eq!(emission.record, expected, "{label} emitted record differs");
3975            let emitted: serde_json::Value =
3976                serde_json::from_str(&emission.content).expect("emitted content is JSON");
3977            let fixture: serde_json::Value =
3978                serde_json::from_slice(bytes).expect("fixture is JSON");
3979            assert_eq!(emitted, fixture, "{label} emitted bytes differ from fixture");
3980        }
3981    }
3982
3983    const WITHHELD_SOURCES_FIXTURES: &[(&str, &str, &[u8])] = &[
3984        (
3985            "complete",
3986            "c1339d6da3b99ca83c099cf87d3cf93a81fa1c90aac25ab54af8f886ce36c28a",
3987            include_bytes!(
3988                "../fixtures/openagents.omega.issue31.withheld_sources.v1.canonical-complete.json"
3989            ),
3990        ),
3991        (
3992            "partial",
3993            "acb28484bf4d8722d774837abda0cc36edce0c74dc599edff820e4e70476bb01",
3994            include_bytes!(
3995                "../fixtures/openagents.omega.issue31.withheld_sources.v1.canonical-partial.json"
3996            ),
3997        ),
3998    ];
3999
4000    /// Each negative fixture is one specific way a coverage statement could lie
4001    /// about how much of the owner's view reached the device.
4002    const WITHHELD_SOURCES_NEGATIVE_FIXTURES: &[(&str, &str, &[u8])] = &[
4003        (
4004            "complete-with-counts",
4005            "8d5e2b9a718a65b2c808343e0723707acb423928652d3b399c5ef3b396a506fa",
4006            include_bytes!(
4007                "../fixtures/openagents.omega.issue31.withheld_sources.v1.negative-complete-with-counts.json"
4008            ),
4009        ),
4010        (
4011            "scan-bound-exact",
4012            "8ece8427b2d133bf13ace98a8eee7e1755a58a4dceccf45c507f15f3351a5b54",
4013            include_bytes!(
4014                "../fixtures/openagents.omega.issue31.withheld_sources.v1.negative-scan-bound-exact.json"
4015            ),
4016        ),
4017        (
4018            "zero-count",
4019            "02babd0fb35243371e977d3db394ed71b91de7df2ea60270bf5f19d08ed40c81",
4020            include_bytes!(
4021                "../fixtures/openagents.omega.issue31.withheld_sources.v1.negative-zero-count.json"
4022            ),
4023        ),
4024        (
4025            "unreadable-cause",
4026            "c8eca6e40d9555c329a537eb14e667e6191f71267ca09db5885f9820630e7033",
4027            include_bytes!(
4028                "../fixtures/openagents.omega.issue31.withheld_sources.v1.negative-unreadable-cause.json"
4029            ),
4030        ),
4031    ];
4032
4033    /// The coverage statement is the only thing that lets a device tell "this is
4034    /// everything" from "this is what arrived". The digests are pinned
4035    /// identically in the TypeScript peer, so a one-sided edit fails on both
4036    /// sides rather than drifting into disagreement about what the phone is
4037    /// being told.
4038    #[test]
4039    fn withheld_sources_fixtures_decode_and_bind() {
4040        for (label, digest, bytes) in WITHHELD_SOURCES_FIXTURES {
4041            assert_eq!(
4042                &format!("{:x}", Sha256::digest(bytes)),
4043                digest,
4044                "{label} fixture bytes changed"
4045            );
4046            let record = Issue31WithheldSourcesRecord::decode(bytes)
4047                .unwrap_or_else(|error| panic!("{label} fixture decodes: {error}"));
4048            record
4049                .validate_private_binding(&"1".repeat(64), &"2".repeat(64))
4050                .unwrap_or_else(|error| panic!("{label} fixture binds: {error}"));
4051        }
4052    }
4053
4054    #[test]
4055    fn a_complete_statement_and_a_partial_statement_are_not_the_same_record() {
4056        let complete = Issue31WithheldSourcesRecord::decode(WITHHELD_SOURCES_FIXTURES[0].2)
4057            .expect("the complete fixture decodes");
4058        let partial = Issue31WithheldSourcesRecord::decode(WITHHELD_SOURCES_FIXTURES[1].2)
4059            .expect("the partial fixture decodes");
4060        assert_eq!(complete.coverage, ISSUE31_WITHHELD_COVERAGE_COMPLETE);
4061        assert!(complete.withheld.is_empty());
4062        assert_eq!(partial.coverage, ISSUE31_WITHHELD_COVERAGE_PARTIAL);
4063        assert_ne!(complete.coverage, partial.coverage);
4064        // Every count names why, because "3 missing" without a reason is
4065        // nearly as unhelpful as silence.
4066        for entry in &partial.withheld {
4067            assert!(is_issue31_public_ref(&entry.reason_ref));
4068            assert!(entry.count > 0);
4069        }
4070        assert_eq!(
4071            partial
4072                .withheld
4073                .iter()
4074                .map(|entry| entry.cause)
4075                .collect::<Vec<_>>(),
4076            vec![
4077                Issue31WithheldCause::Quarantined,
4078                Issue31WithheldCause::ScanBound
4079            ]
4080        );
4081    }
4082
4083    /// Each named refusal gets its own assertion, so no case can be carried by
4084    /// a neighbour's evidence.
4085    #[test]
4086    fn a_complete_coverage_over_a_non_empty_count_list_is_refused() {
4087        let (label, digest, bytes) = WITHHELD_SOURCES_NEGATIVE_FIXTURES[0];
4088        assert_eq!(
4089            &format!("{:x}", Sha256::digest(bytes)),
4090            digest,
4091            "{label} fixture bytes changed"
4092        );
4093        assert!(
4094            Issue31WithheldSourcesRecord::decode(bytes).is_err(),
4095            "a record that says complete while withholding sources must be refused"
4096        );
4097    }
4098
4099    #[test]
4100    fn an_exact_scan_bound_count_is_refused() {
4101        let (label, digest, bytes) = WITHHELD_SOURCES_NEGATIVE_FIXTURES[1];
4102        assert_eq!(
4103            &format!("{:x}", Sha256::digest(bytes)),
4104            digest,
4105            "{label} fixture bytes changed"
4106        );
4107        assert!(
4108            Issue31WithheldSourcesRecord::decode(bytes).is_err(),
4109            "a host that stopped reading cannot state an exact number"
4110        );
4111    }
4112
4113    #[test]
4114    fn a_withheld_count_of_zero_is_refused() {
4115        let (label, digest, bytes) = WITHHELD_SOURCES_NEGATIVE_FIXTURES[2];
4116        assert_eq!(
4117            &format!("{:x}", Sha256::digest(bytes)),
4118            digest,
4119            "{label} fixture bytes changed"
4120        );
4121        assert!(
4122            Issue31WithheldSourcesRecord::decode(bytes).is_err(),
4123            "a zero count is not a withheld source"
4124        );
4125    }
4126
4127    /// A device-side read failure is real, but only the device can observe it.
4128    /// The wire vocabulary has no such cause, so a host cannot assert one.
4129    #[test]
4130    fn a_host_cannot_claim_a_cause_only_the_device_can_observe() {
4131        let (label, digest, bytes) = WITHHELD_SOURCES_NEGATIVE_FIXTURES[3];
4132        assert_eq!(
4133            &format!("{:x}", Sha256::digest(bytes)),
4134            digest,
4135            "{label} fixture bytes changed"
4136        );
4137        assert!(
4138            Issue31WithheldSourcesRecord::decode(bytes).is_err(),
4139            "a device-observed cause must not be assertable by the host"
4140        );
4141    }
4142
4143    #[test]
4144    fn the_withheld_emitter_reproduces_every_canonical_fixture() {
4145        for (label, _, bytes) in WITHHELD_SOURCES_FIXTURES {
4146            let expected =
4147                Issue31WithheldSourcesRecord::decode(bytes).expect("canonical fixture decodes");
4148            let emission = emit_issue31_withheld_sources(Issue31WithheldSourcesInput {
4149                host_ref: &expected.host_ref,
4150                host_public_key_hex: &expected.host_public_key_hex,
4151                device_public_key_hex: &expected.device_public_key_hex,
4152                grant_ref: &expected.grant_ref,
4153                expected_generation: expected.expected_generation,
4154                observed_at: expected.observed_at,
4155                withheld: expected.withheld.clone(),
4156            })
4157            .unwrap_or_else(|error| panic!("{label} emits: {error}"));
4158            assert_eq!(emission.record, expected, "{label} emitted record differs");
4159            let emitted: serde_json::Value =
4160                serde_json::from_str(&emission.content).expect("emitted content is JSON");
4161            let fixture: serde_json::Value =
4162                serde_json::from_slice(bytes).expect("fixture is JSON");
4163            assert_eq!(emitted, fixture, "{label} emitted bytes differ from fixture");
4164        }
4165    }
4166
4167    /// Falsification. `coverage` is derived from the counts rather than
4168    /// supplied, so the one field a bug would set to "complete" over a
4169    /// non-empty list cannot be set at all. Feeding the emitter a count list it
4170    /// must refuse proves the routing back through its own decoder is load
4171    /// bearing rather than decorative.
4172    #[test]
4173    fn the_withheld_emitter_cannot_produce_a_record_its_own_decoder_refuses() {
4174        let repeated = vec![
4175            Issue31WithheldSourceCount {
4176                cause: Issue31WithheldCause::Quarantined,
4177                count: 1,
4178                exact: true,
4179                reason_ref: "reason.omega.invalid_projection_source".into(),
4180            },
4181            Issue31WithheldSourceCount {
4182                cause: Issue31WithheldCause::Quarantined,
4183                count: 4,
4184                exact: true,
4185                reason_ref: "reason.omega.invalid_projection_source".into(),
4186            },
4187        ];
4188        let error = emit_issue31_withheld_sources(Issue31WithheldSourcesInput {
4189            host_ref: "omega.host.local",
4190            host_public_key_hex: &"1".repeat(64),
4191            device_public_key_hex: &"2".repeat(64),
4192            grant_ref: "grant.omega.device_1",
4193            expected_generation: 3,
4194            observed_at: 1_784_937_651,
4195            withheld: repeated,
4196        })
4197        .expect_err("two counts for one cause and reason are ambiguous");
4198        assert!(matches!(error, Issue31NostrError::Invalid(_)));
4199    }
4200
4201    /// Falsification, on the other half. A cause that cannot state an exact
4202    /// number must not be emitted as though it could, even when the caller asks
4203    /// for it.
4204    #[test]
4205    fn the_withheld_emitter_refuses_a_precision_the_cause_cannot_have() {
4206        let error = emit_issue31_withheld_sources(Issue31WithheldSourcesInput {
4207            host_ref: "omega.host.local",
4208            host_public_key_hex: &"1".repeat(64),
4209            device_public_key_hex: &"2".repeat(64),
4210            grant_ref: "grant.omega.device_1",
4211            expected_generation: 3,
4212            observed_at: 1_784_937_651,
4213            withheld: vec![Issue31WithheldSourceCount {
4214                cause: Issue31WithheldCause::ScanBound,
4215                count: 900,
4216                exact: true,
4217                reason_ref: "reason.omega.projection_scan_bound".into(),
4218            }],
4219        })
4220        .expect_err("the scan bound cannot be exact");
4221        assert!(matches!(error, Issue31NostrError::Invalid(_)));
4222
4223        emit_issue31_withheld_sources(Issue31WithheldSourcesInput {
4224            host_ref: "omega.host.local",
4225            host_public_key_hex: &"1".repeat(64),
4226            device_public_key_hex: &"2".repeat(64),
4227            grant_ref: "grant.omega.device_1",
4228            expected_generation: 3,
4229            observed_at: 1_784_937_651,
4230            withheld: vec![Issue31WithheldSourceCount {
4231                cause: Issue31WithheldCause::ScanBound,
4232                count: 900,
4233                exact: false,
4234                reason_ref: "reason.omega.projection_scan_bound".into(),
4235            }],
4236        })
4237        .expect("the same count as a lower bound is exactly what the host knows");
4238    }
4239
4240    /// Falsification. A read-state body can sit inside every per-field bound the
4241    /// projection contract states and still serialize past the record budget once
4242    /// JSON escaping is applied, because the escaping happens outside the body.
4243    /// `validate` accepts that body. The emitter must not, because the device
4244    /// reader applies the budget and would refuse the published record.
4245    #[test]
4246    fn the_emitter_cannot_produce_a_record_its_own_decoder_refuses() {
4247        let mut contexts = String::new();
4248        for index in 0..1_800 {
4249            if index > 0 {
4250                contexts.push(',');
4251            }
4252            // Every escaped quote in a context id costs one byte in the body and
4253            // three in the record, so a legal body crosses the record budget.
4254            contexts.push_str(&format!("\"{}\":1784937608", "\\\"".repeat(120)));
4255            let _ = index;
4256        }
4257        let plaintext = format!("{{\"v\":1,\"client_id\":\"omega-host\",\"contexts\":{{{contexts}}}}}");
4258        assert!(
4259            plaintext.len() <= 524_288,
4260            "the oversized body must stay inside its own plaintext bound"
4261        );
4262        let projection = Issue31OwnerProjectionBody::ReadState {
4263            d_tag: "read-state:owner-private".into(),
4264            plaintext,
4265        };
4266        projection
4267            .validate()
4268            .expect("the body is legal on its own terms");
4269
4270        let error = emit_issue31_owner_projection(Issue31OwnerProjectionInput {
4271            host_ref: "omega.host.local",
4272            host_public_key_hex: &"1".repeat(64),
4273            device_public_key_hex: &"2".repeat(64),
4274            sarah_public_key_hex: &"3".repeat(64),
4275            grant_ref: "grant.omega.device_1",
4276            expected_generation: 3,
4277            source_event_id: &"c".repeat(64),
4278            source_author_public_key_hex: &"1".repeat(64),
4279            source_kind: SARAH_READ_STATE_KIND,
4280            source_created_at: 1_784_937_620,
4281            projected_at: 1_784_937_621,
4282            projection,
4283        })
4284        .expect_err("the emitter must refuse a record its own decoder refuses");
4285        assert!(matches!(error, Issue31NostrError::Invalid(_)));
4286    }
4287
4288    /// A reference the host builds from a Sarah-authored tag is still untrusted
4289    /// input. The emitter refuses it rather than publishing a record the device
4290    /// reader would reject.
4291    #[test]
4292    fn the_emitter_refuses_an_unsafe_reference_taken_from_a_source_event() {
4293        let projection = Issue31OwnerProjectionBody::AuthorityReceipt {
4294            receipt_ref: format!("receipt.issue31.{}", "a".repeat(24)),
4295            turn_ref: "turn.issue31.release_evidence".into(),
4296            authority_decision: Issue31AuthorityDecisionProjection {
4297                state: "refused".into(),
4298                decision_ref: format!("decision.issue31.{}", "a".repeat(24)),
4299                reason_ref: Some("reason.openagents._reserved".into()),
4300            },
4301            target_outcome: Issue31TargetOutcomeProjection {
4302                state: "pending".into(),
4303                outcome_ref: None,
4304                reason_ref: None,
4305            },
4306        };
4307        emit_issue31_owner_projection(Issue31OwnerProjectionInput {
4308            host_ref: "omega.host.local",
4309            host_public_key_hex: &"1".repeat(64),
4310            device_public_key_hex: &"2".repeat(64),
4311            sarah_public_key_hex: &"3".repeat(64),
4312            grant_ref: "grant.omega.device_1",
4313            expected_generation: 3,
4314            source_event_id: &("a".repeat(24) + &"b".repeat(40)),
4315            source_author_public_key_hex: &"3".repeat(64),
4316            source_kind: SARAH_AUTHORITY_RECEIPT_KIND,
4317            source_created_at: 1_784_937_640,
4318            projected_at: 1_784_937_641,
4319            projection,
4320        })
4321        .expect_err("a leading underscore segment is not a public reference");
4322    }
4323
4324    #[test]
4325    fn v2_host_discovery_validation() {
4326        let v2 = Issue31HostDiscoveryV2 {
4327            schema: ISSUE31_HOST_DISCOVERY_SCHEMA_V2.into(),
4328            host_ref: "omega.host.local".into(),
4329            host_public_key_hex: "1".repeat(64),
4330            sarah_public_key_hex: "3".repeat(64),
4331            conversation: format!("sarah.{}", "a".repeat(24)),
4332            display_name: "Omega Primary Host".into(),
4333            protocols: vec![
4334                ISSUE31_PAIRING_SCHEMA.into(),
4335                ISSUE31_COMMAND_SCHEMA.into(),
4336                ISSUE31_COMMAND_SCHEMA_V2.into(),
4337            ],
4338            relay_urls: vec!["wss://relay.openagents.com".into()],
4339            generation: 1,
4340            issued_at: 100,
4341            expires_at: 200,
4342        };
4343        v2.validate().expect("v2 discovery valid");
4344        let encoded = serde_json::to_vec(&v2).expect("serialize");
4345        let decoded = Issue31HostDiscoveryV2::decode(&encoded).expect("decode v2");
4346        assert_eq!(decoded.conversation, format!("sarah.{}", "a".repeat(24)));
4347    }
4348
4349    #[test]
4350    fn persisted_v1_controller_adopts_the_configured_conversation() {
4351        let configuration = Issue31HostConfiguration {
4352            host_ref: "omega.host.local".into(),
4353            host_public_key_hex: "1".repeat(64),
4354            sarah_public_key_hex: "3".repeat(64),
4355            conversation: "sarah.0123456789abcdef01234567".into(),
4356            display_name: "Local Omega".into(),
4357            relay_urls: vec!["wss://relay.example.com".into()],
4358            generation: 1,
4359        };
4360        let controller = Issue31HostController::new(configuration.clone()).expect("controller");
4361        let mut value = serde_json::to_value(controller).expect("controller json");
4362        value["configuration"]
4363            .as_object_mut()
4364            .expect("configuration object")
4365            .remove("conversation");
4366        let mut restored: Issue31HostController =
4367            serde_json::from_value(value).expect("deserialize v1 state");
4368        restored
4369            .adopt_conversation_if_missing(&configuration.conversation)
4370            .expect("adopt conversation");
4371        assert!(restored.matches_configuration(&configuration));
4372        restored
4373            .validate_persisted_state()
4374            .expect("validate migrated state");
4375    }
4376
4377    #[test]
4378    fn excess_fields_and_unsafe_relays_fail_closed() {
4379        let bytes =
4380            include_bytes!("../fixtures/openagents.omega.issue31.host_discovery.v1.canonical.json");
4381        let mut value: serde_json::Value = serde_json::from_slice(bytes).expect("fixture json");
4382        value["secret"] = serde_json::Value::String("nsec1forbidden".into());
4383        assert!(Issue31HostDiscovery::decode(value.to_string().as_bytes()).is_err());
4384        value.as_object_mut().expect("object").remove("secret");
4385        value["relayUrls"] = serde_json::json!(["wss://owner:secret@relay.example.com"]);
4386        assert!(Issue31HostDiscovery::decode(value.to_string().as_bytes()).is_err());
4387        value["relayUrls"] = serde_json::json!(["wss://relay.example.com/?token=forbidden"]);
4388        assert!(Issue31HostDiscovery::decode(value.to_string().as_bytes()).is_err());
4389
4390        let pairing_bytes =
4391            include_bytes!("../fixtures/openagents.omega.issue31.pairing.v1.canonical.json");
4392        let mut pairing: serde_json::Value =
4393            serde_json::from_slice(pairing_bytes).expect("pairing fixture json");
4394        pairing["sarahPublicKeyHex"] = pairing["hostPublicKeyHex"].clone();
4395        assert!(Issue31PairingRecord::decode(pairing.to_string().as_bytes()).is_err());
4396        pairing
4397            .as_object_mut()
4398            .expect("pairing object")
4399            .remove("sarahPublicKeyHex");
4400        assert!(Issue31PairingRecord::decode(pairing.to_string().as_bytes()).is_err());
4401    }
4402
4403    /// Drive a full pairing handshake for `device_public_key_hex` and return the
4404    /// grant the host issued, or `Err` if the host refused at any step.
4405    fn attempt_pairing(
4406        controller: &mut Issue31HostController,
4407        configuration: &Issue31HostConfiguration,
4408        device_public_key_hex: &str,
4409        seed: &str,
4410        now: u64,
4411    ) -> Result<Option<Issue31PairingRecord>, Issue31NostrError> {
4412        let request_event_id = format!("{:x}", Sha256::digest(format!("{seed}.request")));
4413        let challenge_event_id = format!("{:x}", Sha256::digest(format!("{seed}.challenge")));
4414        let response_event_id = format!("{:x}", Sha256::digest(format!("{seed}.response")));
4415        let challenge = controller.handle_pairing_event(
4416            Issue31PairingEvent {
4417                event_id: request_event_id,
4418                record: Issue31PairingRecord::PairingRequest {
4419                    schema: ISSUE31_PAIRING_SCHEMA.into(),
4420                    host_ref: configuration.host_ref.clone(),
4421                    host_public_key_hex: configuration.host_public_key_hex.clone(),
4422                    device_public_key_hex: device_public_key_hex.to_string(),
4423                    issued_at: now,
4424                    pairing_request_ref: format!("pairing_request.{seed}"),
4425                    requested_scopes: vec![Issue31PairingScope::ControlFullAuto],
4426                    expires_at: now.saturating_add(600),
4427                },
4428            },
4429            now,
4430        )?;
4431        let Some(challenge) = challenge else {
4432            return Ok(None);
4433        };
4434        let challenge_value = match &challenge {
4435            Issue31PairingRecord::PairingChallenge { challenge, .. } => challenge.clone(),
4436            _ => panic!("expected a pairing challenge"),
4437        };
4438        controller.record_emitted_pairing(challenge_event_id.clone(), challenge)?;
4439        controller.handle_pairing_event(
4440            Issue31PairingEvent {
4441                event_id: response_event_id,
4442                record: Issue31PairingRecord::PairingResponse {
4443                    schema: ISSUE31_PAIRING_SCHEMA.into(),
4444                    host_ref: configuration.host_ref.clone(),
4445                    host_public_key_hex: configuration.host_public_key_hex.clone(),
4446                    device_public_key_hex: device_public_key_hex.to_string(),
4447                    issued_at: now,
4448                    pairing_response_ref: format!("pairing_response.{seed}"),
4449                    pairing_challenge_event_id: challenge_event_id,
4450                    challenge: challenge_value,
4451                    expires_at: now.saturating_add(600),
4452                },
4453            },
4454            now,
4455        )
4456    }
4457
4458    /// A revoked device must not be able to restore its authority by pairing
4459    /// again. The grant fold is keyed by `grant_ref`, so a fresh handshake would
4460    /// otherwise mint a brand-new `grant_ref` carrying the same scopes the owner
4461    /// just took away, with no owner action in between.
4462    #[test]
4463    fn revoked_device_cannot_repair_without_owner_readmission() {
4464        let (configuration, mut controller, device_public_key_hex, revoked_grant_ref) =
4465            restart_fixture();
4466        assert!(controller.device_admission_is_revoked(&device_public_key_hex));
4467        let refusal = attempt_pairing(
4468            &mut controller,
4469            &configuration,
4470            &device_public_key_hex,
4471            "revoked.repair",
4472            200,
4473        )
4474        .expect_err("a revoked device must be refused a pairing challenge");
4475        assert!(matches!(refusal, Issue31NostrError::Invalid(message) if message
4476            .contains("device admission was revoked")));
4477        // The only grant on record is still the revoked one.
4478        let projections = controller.grant_projections(200).expect("grant list");
4479        assert_eq!(projections.len(), 1);
4480        assert_eq!(projections[0].grant_ref, revoked_grant_ref);
4481        assert_eq!(projections[0].status, "revoked");
4482    }
4483
4484    /// The runtime admission allowlist is not persisted — it is re-applied from
4485    /// configuration on every start. So an in-memory block would evaporate on
4486    /// restart. The block has to live in the durable pairing log.
4487    #[test]
4488    fn revocation_block_survives_restart_and_allowlist_rebind() {
4489        let (configuration, controller, device_public_key_hex, _) = restart_fixture();
4490        let encoded = serde_json::to_vec(&controller).expect("serialize controller");
4491        let mut reloaded: Issue31HostController =
4492            serde_json::from_slice(&encoded).expect("deserialize controller");
4493        reloaded.validate_persisted_state().expect("persisted state");
4494        // Exactly what start-up does: re-apply the owner's configured allowlist.
4495        reloaded
4496            .set_admitted_device_policy(
4497                vec![device_public_key_hex.clone()],
4498                vec![Issue31PairingScope::ControlFullAuto],
4499            )
4500            .expect("rebind runtime policy");
4501        assert!(reloaded.device_admission_is_revoked(&device_public_key_hex));
4502        attempt_pairing(
4503            &mut reloaded,
4504            &configuration,
4505            &device_public_key_hex,
4506            "restart.repair",
4507            200,
4508        )
4509        .expect_err("the durable revocation must outlive a restart");
4510    }
4511
4512    /// Re-admission is an explicit owner act, and it clears the revocations that
4513    /// exist at that moment by event id. Replaying a cleared revocation must not
4514    /// re-block the device, and a later revocation must block it again.
4515    #[test]
4516    fn owner_readmission_clears_only_the_revocations_it_saw() {
4517        let (configuration, mut controller, device_public_key_hex, revoked_grant_ref) =
4518            restart_fixture();
4519        let cleared = controller
4520            .readmit_device(&device_public_key_hex)
4521            .expect("readmit");
4522        assert_eq!(cleared.len(), 1);
4523        assert!(!controller.device_admission_is_revoked(&device_public_key_hex));
4524        let grant = attempt_pairing(
4525            &mut controller,
4526            &configuration,
4527            &device_public_key_hex,
4528            "readmitted.repair",
4529            200,
4530        )
4531        .expect("re-admitted device pairs")
4532        .expect("re-admitted device receives a grant");
4533        let Issue31PairingRecord::ScopedGrant {
4534            grant_ref: new_grant_ref,
4535            ..
4536        } = &grant
4537        else {
4538            panic!("expected a scoped grant");
4539        };
4540        assert_ne!(new_grant_ref, &revoked_grant_ref);
4541        let new_grant_ref = new_grant_ref.clone();
4542        controller
4543            .record_emitted_pairing(format!("{:x}", Sha256::digest("readmitted.grant")), grant)
4544            .expect("record grant");
4545        // Replaying the already-cleared revocation cannot re-block the device.
4546        assert!(!controller.device_admission_is_revoked(&device_public_key_hex));
4547        // A *new* revocation is a new event id, so it is not cleared and blocks again.
4548        let revocation = controller
4549            .revoke_grant(&new_grant_ref, 300, Some("reason.omega.owner_revoked".into()))
4550            .expect("revoke the new grant");
4551        controller
4552            .record_emitted_pairing(
4553                format!("{:x}", Sha256::digest("readmitted.revocation")),
4554                revocation,
4555            )
4556            .expect("record revocation");
4557        assert!(controller.device_admission_is_revoked(&device_public_key_hex));
4558        attempt_pairing(
4559            &mut controller,
4560            &configuration,
4561            &device_public_key_hex,
4562            "second.repair",
4563            400,
4564        )
4565        .expect_err("a second revocation must fail closed again");
4566    }
4567
4568    /// Persisted state must not be able to pre-clear a revocation that does not
4569    /// exist, which would silently unblock a device the moment its revocation
4570    /// arrived from the relay.
4571    #[test]
4572    fn persisted_readmission_must_name_a_real_revocation() {
4573        let (_, controller, _, _) = restart_fixture();
4574        let mut encoded: serde_json::Value =
4575            serde_json::from_slice(&serde_json::to_vec(&controller).expect("serialize"))
4576                .expect("controller json");
4577        encoded["clearedDeviceRevocationEventIds"] =
4578            serde_json::json!([serde_json::Value::String("9".repeat(64))]);
4579        let forged: Issue31HostController =
4580            serde_json::from_value(encoded).expect("deserialize forged controller");
4581        assert!(forged.validate_persisted_state().is_err());
4582    }
4583
4584    #[test]
4585    fn production_host_controller_pairs_executes_renews_and_revokes() {
4586        let host_public_key_hex = "1".repeat(64);
4587        let device_public_key_hex = "2".repeat(64);
4588        let mut controller = Issue31HostController::new(Issue31HostConfiguration {
4589            host_ref: "omega.host.local".into(),
4590            host_public_key_hex: host_public_key_hex.clone(),
4591            sarah_public_key_hex: "3".repeat(64),
4592            conversation: "sarah.0123456789abcdef01234567".into(),
4593            display_name: "Local Omega".into(),
4594            relay_urls: vec!["wss://relay.example.com".into()],
4595            generation: 1,
4596        })
4597        .expect("controller");
4598        assert!(
4599            controller
4600                .set_admitted_device_policy(Vec::new(), vec![Issue31PairingScope::ObserveIssue31],)
4601                .is_err()
4602        );
4603        assert!(
4604            controller
4605                .set_admitted_device_policy(
4606                    vec!["A".repeat(64)],
4607                    vec![Issue31PairingScope::ObserveIssue31],
4608                )
4609                .is_err()
4610        );
4611        controller
4612            .set_admitted_device_policy(
4613                vec![device_public_key_hex.clone()],
4614                vec![
4615                    Issue31PairingScope::ObserveIssue31,
4616                    Issue31PairingScope::ControlFullAuto,
4617                ],
4618            )
4619            .expect("admit device");
4620        let expected_fingerprint =
4621            format!("{:x}", Sha256::digest(device_public_key_hex.as_bytes()))[..16]
4622                .to_ascii_uppercase();
4623        assert_eq!(
4624            controller.admitted_device_fingerprints(),
4625            vec![expected_fingerprint]
4626        );
4627        assert!(
4628            !serde_json::to_string(&controller)
4629                .expect("serialize runtime policy boundary")
4630                .contains(&device_public_key_hex)
4631        );
4632        let attacker_request = Issue31PairingRecord::PairingRequest {
4633            schema: ISSUE31_PAIRING_SCHEMA.into(),
4634            host_ref: "omega.host.local".into(),
4635            host_public_key_hex: host_public_key_hex.clone(),
4636            device_public_key_hex: "3".repeat(64),
4637            issued_at: 100,
4638            pairing_request_ref: "pairing_request.attacker.one".into(),
4639            requested_scopes: vec![
4640                Issue31PairingScope::ObserveIssue31,
4641                Issue31PairingScope::SendMessage,
4642                Issue31PairingScope::InterruptTurn,
4643                Issue31PairingScope::ControlFullAuto,
4644                Issue31PairingScope::RequestProviderHandoff,
4645                Issue31PairingScope::ActInCommunity,
4646            ],
4647            expires_at: 1_000,
4648        };
4649        assert!(
4650            controller
4651                .handle_pairing_event(
4652                    Issue31PairingEvent {
4653                        event_id: "9".repeat(64),
4654                        record: attacker_request,
4655                    },
4656                    101,
4657                )
4658                .expect("ignore attacker")
4659                .is_none()
4660        );
4661        assert!(controller.pairing_events.is_empty());
4662        let request_event_id = "a".repeat(64);
4663        let request = Issue31PairingRecord::PairingRequest {
4664            schema: ISSUE31_PAIRING_SCHEMA.into(),
4665            host_ref: "omega.host.local".into(),
4666            host_public_key_hex: host_public_key_hex.clone(),
4667            device_public_key_hex: device_public_key_hex.clone(),
4668            issued_at: 100,
4669            pairing_request_ref: "pairing_request.device.one".into(),
4670            requested_scopes: vec![
4671                Issue31PairingScope::ObserveIssue31,
4672                Issue31PairingScope::ControlFullAuto,
4673            ],
4674            expires_at: 1_000,
4675        };
4676        let challenge = controller
4677            .handle_pairing_event(
4678                Issue31PairingEvent {
4679                    event_id: request_event_id,
4680                    record: request,
4681                },
4682                101,
4683            )
4684            .expect("request")
4685            .expect("challenge");
4686        let challenge_value = match &challenge {
4687            Issue31PairingRecord::PairingChallenge { challenge, .. } => challenge.clone(),
4688            _ => panic!("expected challenge"),
4689        };
4690        let challenge_event_id = "b".repeat(64);
4691        controller
4692            .record_emitted_pairing(challenge_event_id.clone(), challenge)
4693            .expect("record challenge");
4694        let response_event_id = "c".repeat(64);
4695        let grant = controller
4696            .handle_pairing_event(
4697                Issue31PairingEvent {
4698                    event_id: response_event_id,
4699                    record: Issue31PairingRecord::PairingResponse {
4700                        schema: ISSUE31_PAIRING_SCHEMA.into(),
4701                        host_ref: "omega.host.local".into(),
4702                        host_public_key_hex: host_public_key_hex.clone(),
4703                        device_public_key_hex: device_public_key_hex.clone(),
4704                        issued_at: 102,
4705                        pairing_response_ref: "pairing_response.device.one".into(),
4706                        pairing_challenge_event_id: challenge_event_id,
4707                        challenge: challenge_value,
4708                        expires_at: 1_000,
4709                    },
4710                },
4711                102,
4712            )
4713            .expect("response")
4714            .expect("grant");
4715        let grant_ref = match &grant {
4716            Issue31PairingRecord::ScopedGrant {
4717                grant_ref,
4718                sarah_public_key_hex,
4719                scopes,
4720                ..
4721            } => {
4722                assert_eq!(sarah_public_key_hex, &"3".repeat(64));
4723                assert_eq!(
4724                    scopes,
4725                    &vec![
4726                        Issue31PairingScope::ObserveIssue31,
4727                        Issue31PairingScope::ControlFullAuto,
4728                    ]
4729                );
4730                grant_ref.clone()
4731            }
4732            _ => panic!("expected scoped grant"),
4733        };
4734        controller
4735            .record_emitted_pairing("d".repeat(64), grant)
4736            .expect("record grant");
4737
4738        let executions = Cell::new(0_u32);
4739        let result = controller
4740            .handle_command_event(
4741                Issue31CommandEvent {
4742                    event_id: "e".repeat(64),
4743                    record: Issue31CommandRecord::CommandIntent {
4744                        schema: ISSUE31_COMMAND_SCHEMA.into(),
4745                        host_ref: "omega.host.local".into(),
4746                        host_public_key_hex: host_public_key_hex.clone(),
4747                        device_public_key_hex: device_public_key_hex.clone(),
4748                        grant_ref: grant_ref.clone(),
4749                        action_ref: "action.omega.full_auto.stop".into(),
4750                        idempotency_ref: "idempotency.device.stop_one".into(),
4751                        expected_generation: 1,
4752                        arguments_ref: "arguments.omega.none".into(),
4753                        issued_at: 103,
4754                        expires_at: 200,
4755                    },
4756                },
4757                104,
4758                |_, _, _| {
4759                    executions.set(executions.get().saturating_add(1));
4760                    Issue31CommandExecution {
4761                        status: Issue31CommandStatus::Stopped,
4762                        outcome_ref: "outcome.omega.stopped".into(),
4763                        reason_ref: None,
4764                    }
4765                },
4766            )
4767            .expect("command")
4768            .expect("terminal result");
4769        assert_eq!(executions.get(), 1);
4770        assert!(matches!(
4771            result,
4772            Issue31CommandRecord::CommandResult {
4773                status: Issue31CommandStatus::Stopped,
4774                ..
4775            }
4776        ));
4777
4778        let v2_executions = Cell::new(0_u32);
4779        let v2_result = controller
4780            .handle_command_event_v2(
4781                "7".repeat(64),
4782                Issue31CommandRecordV2::CommandIntent {
4783                    schema: ISSUE31_COMMAND_SCHEMA_V2.into(),
4784                    host_ref: "omega.host.local".into(),
4785                    host_public_key_hex,
4786                    device_public_key_hex,
4787                    grant_ref: grant_ref.clone(),
4788                    idempotency_ref: "idempotency.device.read_one".into(),
4789                    expected_generation: 1,
4790                    arguments: Issue31CommandArguments::ReadStatePatch {
4791                        action_ref: ISSUE31_ACTION_ADVANCE_READ_STATE.into(),
4792                        slot_id: "mobile".into(),
4793                        client_id: "iphone".into(),
4794                        context_ref: "sarah-conversation:sarah.0123456789abcdef01234567".into(),
4795                        read_at: 104,
4796                    },
4797                    issued_at: 103,
4798                    expires_at: 200,
4799                },
4800                104,
4801                |arguments, _, _, _, _| {
4802                    assert!(matches!(
4803                        arguments,
4804                        Issue31CommandArguments::ReadStatePatch { .. }
4805                    ));
4806                    v2_executions.set(v2_executions.get().saturating_add(1));
4807                    Issue31CommandExecutionV2 {
4808                        status: Issue31CommandHandlingStatus::Accepted,
4809                        handling_ref: "handling.omega.read_one".into(),
4810                        reason_ref: None,
4811                        source_event_id: Some("8".repeat(64)),
4812                    }
4813                },
4814            )
4815            .expect("command v2")
4816            .expect("handling result");
4817        assert_eq!(v2_executions.get(), 1);
4818        assert!(matches!(
4819            v2_result,
4820            Issue31CommandRecordV2::CommandResult {
4821                status: Issue31CommandHandlingStatus::Accepted,
4822                source_event_id: Some(_),
4823                ..
4824            }
4825        ));
4826        assert_eq!(
4827            controller.active_grants(104).expect("active grants").len(),
4828            1
4829        );
4830        controller
4831            .record_source_projection(grant_ref.clone(), 1, "8".repeat(64))
4832            .expect("record projection");
4833        assert!(controller.source_was_projected(&grant_ref, 1, &"8".repeat(64)));
4834
4835        let renewal = controller
4836            .renew_grant(
4837                &grant_ref,
4838                vec![Issue31PairingScope::ControlFullAuto],
4839                105,
4840                2_000,
4841            )
4842            .expect("renewal");
4843        controller
4844            .record_emitted_pairing("f".repeat(64), renewal)
4845            .expect("record renewal");
4846        assert!(!controller.source_was_projected(&grant_ref, 2, &"8".repeat(64)));
4847        controller
4848            .record_source_projection(grant_ref.clone(), 2, "8".repeat(64))
4849            .expect("record renewed projection");
4850        assert!(controller.source_was_projected(&grant_ref, 2, &"8".repeat(64)));
4851        let revocation = controller
4852            .revoke_grant(&grant_ref, 106, Some("reason.omega.owner_revoked".into()))
4853            .expect("revocation");
4854        controller
4855            .record_emitted_pairing("0".repeat(64), revocation)
4856            .expect("record revocation");
4857        let state = fold_issue31_grant(&controller.pairing_events, &grant_ref)
4858            .expect("fold")
4859            .expect("state");
4860        assert_eq!(state.status, Issue31GrantStatus::Revoked);
4861    }
4862
4863    #[test]
4864    fn scoped_grant_requires_the_complete_pairing_chain() {
4865        let grant = Issue31PairingRecord::decode(include_bytes!(
4866            "../fixtures/openagents.omega.issue31.pairing.v1.canonical.json"
4867        ))
4868        .expect("grant fixture");
4869        let error = fold_issue31_grant(
4870            &[Issue31PairingEvent {
4871                event_id: "1".repeat(64),
4872                record: grant,
4873            }],
4874            "grant.omega.device_1",
4875        )
4876        .expect_err("unsolicited grant must fail");
4877        assert!(error.to_string().contains("pairing response"));
4878    }
4879
4880    #[test]
4881    fn runtime_admission_policy_rebind_does_not_change_durable_grants() {
4882        let (_, controller, device_public_key_hex, grant_ref) = restart_fixture();
4883        let projections = controller.grant_projections(107).expect("grant list");
4884        assert_eq!(projections.len(), 1);
4885        assert_eq!(projections[0].grant_ref, grant_ref);
4886        assert_eq!(projections[0].status, "revoked");
4887        let public_projection = serde_json::to_string(&projections).expect("projection json");
4888        assert!(!public_projection.contains(&device_public_key_hex));
4889        let encoded = serde_json::to_vec(&controller).expect("serialize controller");
4890        let mut reloaded: Issue31HostController =
4891            serde_json::from_slice(&encoded).expect("deserialize controller");
4892        assert!(reloaded.admitted_device_fingerprints().is_empty());
4893        let before = fold_issue31_grant(&reloaded.pairing_events, &grant_ref)
4894            .expect("fold before policy rebind")
4895            .expect("durable grant");
4896        reloaded
4897            .set_admitted_device_policy(
4898                vec!["4".repeat(64)],
4899                vec![Issue31PairingScope::ObserveIssue31],
4900            )
4901            .expect("rebind runtime policy");
4902        let after = fold_issue31_grant(&reloaded.pairing_events, &grant_ref)
4903            .expect("fold after policy rebind")
4904            .expect("durable grant");
4905        assert_eq!(before, after);
4906    }
4907
4908    #[test]
4909    fn controller_bounds_refuse_before_execution_or_mutation() {
4910        let host_public_key_hex = "1".repeat(64);
4911        let mut controller = Issue31HostController::new(Issue31HostConfiguration {
4912            host_ref: "omega.host.local".into(),
4913            host_public_key_hex: host_public_key_hex.clone(),
4914            sarah_public_key_hex: "3".repeat(64),
4915            conversation: "sarah.0123456789abcdef01234567".into(),
4916            display_name: "Local Omega".into(),
4917            relay_urls: vec!["wss://relay.example.com".into()],
4918            generation: 1,
4919        })
4920        .expect("controller");
4921        controller.processed_command_event_ids = (0..MAX_ISSUE31_PROCESSED_EVENTS)
4922            .map(|index| format!("{index:064x}"))
4923            .collect();
4924        let executed = Cell::new(false);
4925        let error = controller
4926            .handle_command_event(
4927                Issue31CommandEvent {
4928                    event_id: "f".repeat(64),
4929                    record: Issue31CommandRecord::CommandIntent {
4930                        schema: ISSUE31_COMMAND_SCHEMA.into(),
4931                        host_ref: "omega.host.local".into(),
4932                        host_public_key_hex,
4933                        device_public_key_hex: "2".repeat(64),
4934                        grant_ref: "grant.omega.bound_test".into(),
4935                        action_ref: "action.omega.full_auto.stop".into(),
4936                        idempotency_ref: "idempotency.omega.bound_test".into(),
4937                        expected_generation: 1,
4938                        arguments_ref: "arguments.omega.none".into(),
4939                        issued_at: 100,
4940                        expires_at: 200,
4941                    },
4942                },
4943                101,
4944                |_, _, _| {
4945                    executed.set(true);
4946                    Issue31CommandExecution {
4947                        status: Issue31CommandStatus::Stopped,
4948                        outcome_ref: "outcome.omega.stopped".into(),
4949                        reason_ref: None,
4950                    }
4951                },
4952            )
4953            .expect_err("processed event bound");
4954        assert!(error.to_string().contains("bound"));
4955        assert!(!executed.get());
4956        assert!(controller.command_results.is_empty());
4957
4958        controller.processed_command_event_ids.clear();
4959        controller.command_results = (0..MAX_ISSUE31_COMMAND_RESULTS)
4960            .map(|index| {
4961                let idempotency_ref = format!("idempotency.omega.bound_{index}");
4962                let intent = Issue31CommandRecord::CommandIntent {
4963                    schema: ISSUE31_COMMAND_SCHEMA.into(),
4964                    host_ref: "omega.host.local".into(),
4965                    host_public_key_hex: "1".repeat(64),
4966                    device_public_key_hex: "2".repeat(64),
4967                    grant_ref: "grant.omega.bound_test".into(),
4968                    action_ref: "action.omega.full_auto.stop".into(),
4969                    idempotency_ref: idempotency_ref.clone(),
4970                    expected_generation: 1,
4971                    arguments_ref: "arguments.omega.none".into(),
4972                    issued_at: 100,
4973                    expires_at: 200,
4974                };
4975                let result = Issue31CommandRecord::CommandResult {
4976                    schema: ISSUE31_COMMAND_SCHEMA.into(),
4977                    host_ref: "omega.host.local".into(),
4978                    host_public_key_hex: "1".repeat(64),
4979                    device_public_key_hex: "2".repeat(64),
4980                    grant_ref: "grant.omega.bound_test".into(),
4981                    intent_event_id: format!("{index:064x}"),
4982                    action_ref: "action.omega.full_auto.stop".into(),
4983                    idempotency_ref: idempotency_ref.clone(),
4984                    expected_generation: 1,
4985                    status: Issue31CommandStatus::Unavailable,
4986                    outcome_ref: "outcome.omega.unavailable".into(),
4987                    reason_ref: Some("reason.omega.controller_not_bound".into()),
4988                    completed_at: 101,
4989                };
4990                (idempotency_ref, (intent, result))
4991            })
4992            .collect();
4993        controller.processed_command_event_ids = (0..MAX_ISSUE31_COMMAND_RESULTS)
4994            .map(|index| format!("{index:064x}"))
4995            .collect();
4996        let command_result_count = controller.command_results.len();
4997        let executed = Cell::new(false);
4998        let error = controller
4999            .handle_command_event(
5000                Issue31CommandEvent {
5001                    event_id: "f".repeat(64),
5002                    record: Issue31CommandRecord::CommandIntent {
5003                        schema: ISSUE31_COMMAND_SCHEMA.into(),
5004                        host_ref: "omega.host.local".into(),
5005                        host_public_key_hex: "1".repeat(64),
5006                        device_public_key_hex: "2".repeat(64),
5007                        grant_ref: "grant.omega.bound_test".into(),
5008                        action_ref: "action.omega.full_auto.stop".into(),
5009                        idempotency_ref: "idempotency.omega.bound_overflow".into(),
5010                        expected_generation: 1,
5011                        arguments_ref: "arguments.omega.none".into(),
5012                        issued_at: 100,
5013                        expires_at: 200,
5014                    },
5015                },
5016                101,
5017                |_, _, _| {
5018                    executed.set(true);
5019                    Issue31CommandExecution {
5020                        status: Issue31CommandStatus::Stopped,
5021                        outcome_ref: "outcome.omega.stopped".into(),
5022                        reason_ref: None,
5023                    }
5024                },
5025            )
5026            .expect_err("command result bound");
5027        assert!(error.to_string().contains("bound"));
5028        assert!(!executed.get());
5029        assert_eq!(controller.command_results.len(), command_result_count);
5030        assert!(
5031            !controller
5032                .processed_command_event_ids
5033                .contains(&"f".repeat(64))
5034        );
5035        let encoded = serde_json::to_vec(&controller).expect("serialize bounded controller");
5036        let reloaded: Issue31HostController =
5037            serde_json::from_slice(&encoded).expect("reload bounded controller");
5038        reloaded
5039            .validate_persisted_state()
5040            .expect("bounded controller remains persistable");
5041
5042        controller.processed_pairing_event_ids = (0..MAX_ISSUE31_PROCESSED_EVENTS)
5043            .map(|index| format!("{index:064x}"))
5044            .collect();
5045        let error = controller
5046            .record_emitted_pairing(
5047                "e".repeat(64),
5048                Issue31PairingRecord::PairingChallenge {
5049                    schema: ISSUE31_PAIRING_SCHEMA.into(),
5050                    host_ref: "omega.host.local".into(),
5051                    host_public_key_hex: "1".repeat(64),
5052                    device_public_key_hex: "2".repeat(64),
5053                    issued_at: 100,
5054                    pairing_challenge_ref: "pairing_challenge.omega.bound_test".into(),
5055                    pairing_request_event_id: "d".repeat(64),
5056                    challenge: "3".repeat(64),
5057                    expires_at: 200,
5058                },
5059            )
5060            .expect_err("pairing event bound");
5061        assert!(error.to_string().contains("bound"));
5062        assert!(controller.pairing_events.is_empty());
5063    }
5064}
5065
Served at tenant.openagents/omega Member data and write actions are omitted.