Skip to main content

kernel/event/record/
remote.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4// Copyright (c) 2026 2youg1 and the sprawling contributors
5
6//! The remote door's five lines: it opened, it closed, a device paired,
7//! a device was revoked, a device started a remote session
8//! (`crates/kernel/spec/Event/Record.lean` ยง8-81).
9//!
10//! **No line here carries a key, a pairing code or a session id.** The
11//! history says who was let in and when; what a device proves itself
12//! with stays in the device table, and a ledger anybody can replay
13//! would otherwise hand a reader the means to be that device.
14//!
15//! A device is spelled by the text of its id and a person's name for
16//! it, and its authority by the word the remote door gives it: the
17//! types that decide those live in `remote_access`, which the kernel
18//! does not depend on.
19
20use serde::{Deserialize, Serialize};
21
22use crate::event::TimeMs;
23
24/// `remote_opened`: the door is open until `closes_at`, reached at `url`.
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
27pub struct RemoteOpened {
28    /// When the door closes by itself.
29    pub closes_at: TimeMs,
30    /// The `https://` address the route answered, which a device opens.
31    pub url: String,
32}
33
34/// `remote_closed`: the door closed, and why.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
36#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
37pub struct RemoteClosed {
38    pub why: RemoteClosing,
39}
40
41/// Who closed the remote door.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
43#[serde(rename_all = "snake_case")]
44#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45pub enum RemoteClosing {
46    /// The person, at the city's console.
47    Console,
48    /// A device holding a session, which may lock the door behind it.
49    Locked,
50    /// The time the door was opened for ran out.
51    Expired,
52}
53
54/// `device_paired`: a device presented a good code and the city keeps it.
55#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
56#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
57pub struct DevicePaired {
58    /// The device id's text, as the device table spells it.
59    pub device: String,
60    /// What the person called the device.
61    pub name: String,
62    /// `watch` or `act`.
63    pub authority: String,
64}
65
66/// `device_revoked`: the person took a device's pairing away.
67#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
68#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
69pub struct DeviceRevoked {
70    pub device: String,
71    pub name: String,
72}
73
74/// `remote_session_started`: a paired device proved its key and holds a
75/// session until `expires`, which is never later than the door closes.
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
77#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
78pub struct RemoteSessionStarted {
79    pub device: String,
80    pub expires: TimeMs,
81}
82
83#[cfg(test)]
84#[allow(clippy::unwrap_used, clippy::expect_used, reason = "test code")]
85mod tests {
86    use super::*;
87    use crate::event::Payload;
88
89    /// Each line's bytes, so a ledger written by this build reads back in
90    /// the next one.
91    #[test]
92    fn each_remote_door_line_spells_its_keys_once() {
93        let device = "aaaqeayeaudaocajbifqydiob4".to_owned();
94        let cases: [(Payload, &str); 5] = [
95            (
96                Payload::of(&RemoteOpened {
97                    closes_at: TimeMs::new(9),
98                    url: "https://city.example".to_owned(),
99                })
100                .unwrap(),
101                "{\"closes_at\":9,\"url\":\"https://city.example\"}",
102            ),
103            (
104                Payload::of(&RemoteClosed {
105                    why: RemoteClosing::Locked,
106                })
107                .unwrap(),
108                "{\"why\":\"locked\"}",
109            ),
110            (
111                Payload::of(&DevicePaired {
112                    device: device.clone(),
113                    name: "phone".to_owned(),
114                    authority: "watch".to_owned(),
115                })
116                .unwrap(),
117                "{\"authority\":\"watch\",\"device\":\"aaaqeayeaudaocajbifqydiob4\",\"name\":\"phone\"}",
118            ),
119            (
120                Payload::of(&DeviceRevoked {
121                    device: device.clone(),
122                    name: "phone".to_owned(),
123                })
124                .unwrap(),
125                "{\"device\":\"aaaqeayeaudaocajbifqydiob4\",\"name\":\"phone\"}",
126            ),
127            (
128                Payload::of(&RemoteSessionStarted {
129                    device,
130                    expires: TimeMs::new(7),
131                })
132                .unwrap(),
133                "{\"device\":\"aaaqeayeaudaocajbifqydiob4\",\"expires\":7}",
134            ),
135        ];
136        for (payload, wire) in cases {
137            assert_eq!(serde_json::to_string(&payload).unwrap(), wire);
138            let back: Payload = serde_json::from_str(wire).unwrap();
139            assert_eq!(back, payload);
140        }
141    }
142}