Skip to main content

riff_core/
wire.rs

1//! Requests, replies and events between `riff` and `riff-server`.
2//!
3//! # Protocol
4//!
5//! Each call is a `POST` with a JSON body. A reply is JSON with status
6//! 200. An error is plain text with status 400, 403, 404 or 409.
7//!
8//! Each call is one type that implements [`Call`]: the trait gives the
9//! path and the type of the reply (01M3WRD8TBDPA4JNEZY6J4N2EX). The
10//! client and the server use the same ones. A command asks for a
11//! change of the state that the log gives. A signal changes only what
12//! the server keeps in memory. A query reads.
13//!
14//! | Path | Request | Reply | Sort |
15//! |---|---|---|---|
16//! | `/v1/register` | [`Register`] | `null` | command |
17//! | `/v1/start` | [`Start`] | [`Started`] | command |
18//! | `/v1/end` | [`End`] | `null` | command |
19//! | `/v1/join` | [`Join`] | `null` | command |
20//! | `/v1/leave` | [`Leave`] | `null` | command |
21//! | `/v1/post` | [`Post`] | [`Posted`] | command |
22//! | `/v1/claim` | [`Claim`] | [`ClaimReply`] | command |
23//! | `/v1/release` | [`Release`] | `null` | command |
24//! | `/v1/release/for` | [`ReleaseFor`] | `null` | command |
25//! | `/v1/lead` | [`Lead`] | [`LeadReply`] | command |
26//! | `/v1/pause` | [`Pause`] | [`RiffReply`] | command |
27//! | `/v1/resume` | [`Resume`] | [`RiffReply`] | command |
28//! | `/v1/idle/set` | [`SetIdle`] | [`Idle`] | command |
29//! | `/v1/plan/hold` | [`Hold`] | [`HoldReply`] | command |
30//! | `/v1/plan/free` | [`Free`] | [`FreeReply`] | command |
31//! | `/v1/plan` | [`SetPlan`] | [`PlanReply`] | command |
32//! | `/v1/plan/off` | [`PlanOff`] | [`PlanOffReply`] | command |
33//! | `/v1/notes/set` | [`SetNotes`] | [`SetNotesReply`] | command |
34//! | `/v1/status` | [`SetStatus`] | `null` | signal |
35//! | `/v1/blocked` | [`SetBlocked`] | `null` | signal |
36//! | `/v1/step` | [`SetStep`] | `null` | signal |
37//! | `/v1/blocked/look` | [`BlockedLook`] | [`BlockedLookReply`] | signal |
38//! | `/v1/items` | [`ItemFacts`] | `null` | signal |
39//! | `/v1/alive` | [`Alive`] | [`AliveReply`] | signal |
40//! | `/v1/plan/seen` | [`PlanSeen`] | [`PlanReply`] | signal |
41//! | `/v1/who` | [`WhoRequest`] | [`WhoReply`] | query |
42//! | `/v1/threads` | [`Threads`] | [`ThreadsReply`] | query |
43//! | `/v1/read` | [`Read`] | [`ReadReply`] | query |
44//! | `/v1/riff` | [`RiffQuery`] | [`RiffReply`] | query |
45//! | `/v1/idle` | [`IdleQuery`] | [`Idle`] | query |
46//! | `/v1/plan/show` | [`PlanShow`] | [`PlanReply`] | query |
47//! | `/v1/notes` | [`NotesShow`] | [`NotesReply`] | query |
48//!
49//! `GET /v1/me` gives [`MeReply`]. `GET /v1/server` gives
50//! [`ServerFacts`], also while the server replies 503 to each other
51//! call.
52//!
53//! `POST /v1/token` is an OAuth 2.1 token endpoint. Its request is a
54//! form, [`TokenRequest`]. Its reply is [`TokenReply`], or
55//! [`TokenError`] with status 400. `GET /v1/sign-in` gives
56//! [`SignInConfig`], or status 404 when the server has no sign-in
57//! provider.
58//!
59//! `POST /v1/revoke` ends each sign-in of one person (R20). It needs
60//! an access token and a DPoP proof. Its request is [`Revoke`] and
61//! its reply is [`Revoked`]. A missing or bad token gets status 401. A
62//! person who is not an admin and names another user gets status 403.
63//!
64//! Five calls change or show who may join the riff
65//! (01M3JN3AHMK532XMRDASD4XD5D). Each needs an access token and a DPoP
66//! proof, like `revoke`:
67//!
68//! | Path | Request | Reply | Who |
69//! |---|---|---|---|
70//! | `POST /v1/invite` | [`Invite`] | [`Invited`] | an admin |
71//! | `POST /v1/remove` | [`Remove`] | [`Removed`] | an admin |
72//! | `POST /v1/members` | [`Members`] | [`MembersReply`] | each person |
73//! | `POST /v1/log` | [`LogQuery`] | [`LogReply`] | an admin |
74//! | `POST /v1/admin` | [`SetAdmin`] | [`AdminSet`] | the owner |
75//! | `POST /v1/owner` | [`PassOwner`] | [`OwnerPassed`] | the owner |
76//!
77//! A person who is not an admin gets status 403 from `invite`,
78//! `remove` and `log`. A person who is not the owner gets status 403 from
79//! `admin` and `owner`. `owner` to a person who is not a member or an
80//! admin gets status 400. `remove` of the owner or of an admin gets status 400.
81//!
82//! Two metadata documents follow the MCP authorization spec (R22):
83//!
84//! | Path | Reply |
85//! |---|---|
86//! | `GET /.well-known/oauth-protected-resource` | [`ResourceMetadata`] (RFC 9728) |
87//! | `GET /.well-known/oauth-authorization-server` | [`ServerMetadata`] (RFC 8414) |
88//!
89//! Two streams use server-sent events. Each event is one `data:` line
90//! that holds JSON:
91//!
92//! | Stream | Query | Event |
93//! |---|---|---|
94//! | `GET /v1/watch` | `uri=<session URI>` | [`Wake`] |
95//! | `GET /v1/tail` | `thread=<thread name>` | [`Tailed`] |
96//!
97//! `GET /v1/me` with the query `uri=<session URI>` gives [`MeReply`]:
98//! only the session of the caller (01M3T5GFVS8NMA992KHZN4VE17). The
99//! status line calls it, not `who`.
100//!
101//! Each stream starts with the comment line `: ready`, so that it sends
102//! its first bytes when it opens. A reader skips each line that is not
103//! a `data:` line.
104//!
105//! A session is live while its watch stream is open. Each request
106//! carries the session URI of its sender as `me`. The server finds the
107//! session by the *who* part of that URI. When the server needs
108//! sign-in, the *who* part must match the token, or the reply is 403
109//! (R104). A session keeps the user of its first call: a known session
110//! ID with another user gets 409 (R159).
111//! Only `register` sets the place of a known session; each other call
112//! uses the URI only to make a session that the server does not know.
113//!
114//! # Example
115//!
116//! A post to the holder of a claim:
117//!
118//! ```
119//! use riff_core::wire::Post;
120//!
121//! let post: Post = serde_json::from_str(r#"{
122//!     "me": "riff://mike@pangolin/como-technologies/riff?session=a6cf",
123//!     "thread": "como-technologies/riff",
124//!     "to": [{ "claim": "issue-6" }],
125//!     "body": "The API is ready."
126//! }"#).unwrap();
127//! assert_eq!(post.to[0].claim.as_deref(), Some("issue-6"));
128//! ```
129
130use std::collections::BTreeMap;
131
132use schemars::JsonSchema;
133use serde::de::DeserializeOwned;
134use serde::{Deserialize, Serialize};
135
136use crate::dpop::Key;
137use crate::name::{SessionUri, ThreadName};
138use crate::record::{By, Line, Plan, PlanSet, Record};
139use crate::selector::Selector;
140use crate::signed::Content;
141
142/// The HTTP header of a refused command: the code of the refusal, for
143/// example `held` (01M3WRD9JBQMNN96TXJH8EAJ3W). The text of the reply
144/// is the reason, for a person.
145pub const REFUSED_HEADER: &str = "riff-refused";
146
147/// The HTTP header of the call ID of a command
148/// (01M48VFX22S4811DYBBD7QDW24). Each try of one call sends the same ID.
149/// `riff-server` runs a command with a call ID one time only, and gives
150/// a second try the reply of the first.
151pub const CALL_HEADER: &str = "riff-call";
152
153/// The HTTP header of the reply to a repeated call: `1`. The reply is
154/// the reply of the kept call, on the state of now
155/// (01M48VFX22S4811DYBBD7QDW24).
156pub const REPEAT_HEADER: &str = "riff-repeat";
157
158// ANCHOR: call
159/// A call: one wire type with its path and the type of its reply
160/// (01M3WRD8TBDPA4JNEZY6J4N2EX). The client sends each call with one
161/// generic function, and the server makes one route for each.
162///
163/// ```
164/// use riff_core::wire::{Call, Claim};
165///
166/// /// The path of a call and the JSON of its body.
167/// fn request<C: Call>(call: &C) -> (&'static str, String) {
168///     (C::PATH, serde_json::to_string(call).unwrap())
169/// }
170///
171/// let claim = Claim {
172///     me: "riff://mike@pangolin/como-technologies/riff?session=a6cf".parse()?,
173///     thread: "como-technologies/riff".parse()?,
174///     item: "issue-12".into(),
175/// };
176/// let (path, body) = request(&claim);
177/// assert_eq!(path, "/v1/claim");
178/// assert!(body.contains("issue-12"));
179/// let reply: <Claim as Call>::Reply = serde_json::from_str(
180///     r#"{"holder":"riff://mike@pangolin/como-technologies/riff?session=a6cf&claim=issue-12"}"#,
181/// ).unwrap();
182/// assert_eq!(reply.holder.claims(), ["issue-12"]);
183/// # Ok::<(), riff_core::name::NameError>(())
184/// ```
185pub trait Call: Serialize + DeserializeOwned {
186    /// The HTTP path of the call, for example `/v1/claim`.
187    const PATH: &'static str;
188    /// The reply to the call.
189    type Reply: Serialize + DeserializeOwned;
190}
191// ANCHOR_END: call
192
193/// Gives each call its path and its reply.
194macro_rules! calls {
195    ($($call:ty => $path:literal, $reply:ty;)*) => {
196        $(impl Call for $call {
197            const PATH: &'static str = $path;
198            type Reply = $reply;
199        })*
200    };
201}
202
203calls! {
204    Register => "/v1/register", ();
205    Start => "/v1/start", Started;
206    End => "/v1/end", ();
207    Join => "/v1/join", ();
208    Leave => "/v1/leave", ();
209    Post => "/v1/post", Posted;
210    Claim => "/v1/claim", ClaimReply;
211    Release => "/v1/release", ReleaseReply;
212    ReleaseFor => "/v1/release/for", ();
213    Lead => "/v1/lead", LeadReply;
214    Pause => "/v1/pause", RiffReply;
215    Resume => "/v1/resume", RiffReply;
216    SetIdle => "/v1/idle/set", Idle;
217    Hold => "/v1/plan/hold", HoldReply;
218    Free => "/v1/plan/free", FreeReply;
219    SetPlan => "/v1/plan", PlanReply;
220    PlanOff => "/v1/plan/off", PlanOffReply;
221    PlanSeen => "/v1/plan/seen", PlanReply;
222    PlanShow => "/v1/plan/show", PlanReply;
223    SetNotes => "/v1/notes/set", SetNotesReply;
224    NotesShow => "/v1/notes", NotesReply;
225    SetStatus => "/v1/status", ();
226    SetBlocked => "/v1/blocked", ();
227    SetStep => "/v1/step", ();
228    SetMachine => "/v1/machine", ();
229    BlockedLook => "/v1/blocked/look", BlockedLookReply;
230    ItemFacts => "/v1/items", ();
231    Alive => "/v1/alive", AliveReply;
232    WhoRequest => "/v1/who", WhoReply;
233    Threads => "/v1/threads", ThreadsReply;
234    Read => "/v1/read", ReadReply;
235    RiffQuery => "/v1/riff", RiffReply;
236    IdleQuery => "/v1/idle", Idle;
237    Revoke => "/v1/revoke", Revoked;
238    Invite => "/v1/invite", Invited;
239    Remove => "/v1/remove", Removed;
240    Members => "/v1/members", MembersReply;
241    LogQuery => "/v1/log", LogReply;
242    SetAdmin => "/v1/admin", AdminSet;
243    PassOwner => "/v1/owner", OwnerPassed;
244    TakeOwner => "/v1/owner/take", OwnerAsked;
245    DenyOwner => "/v1/owner/deny", OwnerDenied;
246}
247
248/// `POST /v1/register`: a session says that it exists and where it
249/// works. A session registers when it starts and when it moves. It
250/// joins the thread of its repository. A worker says that it is a
251/// worker, so `who` shows it the same on each machine
252/// (01M3NT4M159EHN5W8JRTQ417N4).
253///
254/// ```
255/// use riff_core::wire::Register;
256///
257/// let old: Register = serde_json::from_str(r#"{"me":"riff://mike@pangolin/o/r?session=a1"}"#).unwrap();
258/// assert!(!old.worker);
259/// ```
260#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
261pub struct Register {
262    pub me: SessionUri,
263    /// True when the session is a worker: `riff workers run` started it.
264    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
265    pub worker: bool,
266}
267
268/// `riff mcp` sends a keep-alive this often, also while no turn runs
269/// (R204). A session with no sign of life for 3 minutes is gone.
270pub const ALIVE_EVERY: std::time::Duration = std::time::Duration::from_secs(60);
271
272/// `POST /v1/alive`: a keep-alive. It shows that the session still runs,
273/// but it is not a call: the idle time in `who` stays (R204).
274#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
275pub struct Alive {
276    pub me: SessionUri,
277    /// The newest fact of the hooks of the session on its machine, when
278    /// it has one (01M41FZNTPXQNCZ1S99HE42PYQ).
279    #[serde(default, skip_serializing_if = "Option::is_none")]
280    pub activity: Option<Activity>,
281    /// The seconds since the last prompt of the person in the session,
282    /// when the prompt hook saw one (01M48VDWPDYRPEAXHR1MYDN1M7).
283    #[serde(default, skip_serializing_if = "Option::is_none")]
284    pub prompt_secs: Option<u64>,
285    /// True when the riff daemon sends the keep-alive for a session in
286    /// a container. The session is live while it is not gone
287    /// (01M510MG0CKDXAKF6YJ768K7E8).
288    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
289    pub container: bool,
290    /// The riff of the process that lives as long as the session:
291    /// `riff mcp`, the riff daemon, `riff run` or `riff workers host`.
292    /// Each other keep-alive sends none, and the server keeps the last
293    /// one (01M514C11BPE93PJT4221PYVHS).
294    #[serde(default, skip_serializing_if = "Option::is_none")]
295    pub binary: Option<RiffBinary>,
296}
297
298/// The riff that a session runs: its build, and whether its file was
299/// replaced after the start (01M514C11BPE93PJT4221PYVHS).
300///
301/// ```
302/// use riff_core::build::Build;
303/// use riff_core::wire::RiffBinary;
304///
305/// let server: Build = "1.4.0 122daab96637 2026-10-15T23:08:34Z".parse().unwrap();
306/// let binary = |build: &str, replaced| RiffBinary { build: build.into(), replaced };
307/// let old = binary("1.3.1 aaaaaaaaaaaa 2026-10-14T10:00:00Z", false);
308/// assert!(old.out_of_date(Some(&server)));
309/// // The same version from an older commit is older too.
310/// assert!(binary("1.4.0 bbbbbbbbbbbb 2026-10-15T20:00:00Z", false).out_of_date(Some(&server)));
311/// // The build of the server, or a newer one, is current.
312/// assert!(!binary(&server.to_string(), false).out_of_date(Some(&server)));
313/// assert!(!binary("1.4.1 cccccccccccc 2026-10-16T10:00:00Z", false).out_of_date(Some(&server)));
314/// // A replaced file is out of date with each build.
315/// assert!(binary(&server.to_string(), true).out_of_date(Some(&server)));
316/// assert!(binary(&server.to_string(), true).out_of_date(None));
317/// // With no build of the server, or no build that riff can read,
318/// // only a replaced file counts.
319/// assert!(!old.out_of_date(None));
320/// assert!(!binary("next", false).out_of_date(Some(&server)));
321/// ```
322#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
323pub struct RiffBinary {
324    /// The build of the riff ([`crate::build::VERSION`]).
325    pub build: String,
326    /// True when the file of the riff was replaced after the start of
327    /// the process: Linux names it `PATH (deleted)`.
328    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
329    pub replaced: bool,
330}
331
332impl RiffBinary {
333    /// True when the session must update: its file was replaced, or its
334    /// build is older than the build of the server, `server`
335    /// (01M514C142K5BM2746F1D9Y448, 01M514C19H3WQP1FSX9RBVSC69).
336    pub fn out_of_date(&self, server: Option<&crate::build::Build>) -> bool {
337        self.replaced
338            || server.is_some_and(|server| {
339                self.build
340                    .parse::<crate::build::Build>()
341                    .is_ok_and(|build| build.older_than(server))
342            })
343    }
344}
345
346/// The most characters in the text of a tool of an [`Activity`].
347pub const ACTIVITY_CHARS: usize = 80;
348
349/// What the hooks of a session saw last (01M41FZNTPXQNCZ1S99HE42PYQ):
350/// a tool that runs, a turn that runs between two tools, or a turn
351/// that ended. A hook writes it on the machine at each tool call and at
352/// the end of each turn, and makes no call. `riff mcp` sends the newest
353/// one in its keep-alive. A riff tool and `riff watch` give no fact:
354/// they are no work.
355///
356/// ```
357/// use riff_core::wire::Activity;
358///
359/// let tool = Activity { tool: Some("Bash: run just ci".into()), turn: true, secs: 30 };
360/// assert!(tool.works());
361/// let between = Activity { tool: None, turn: true, secs: 2 };
362/// assert!(between.works());
363/// let ended = Activity { tool: None, turn: false, secs: 5 };
364/// assert!(!ended.works());
365/// let json = serde_json::to_string(&ended).unwrap();
366/// assert_eq!(json, r#"{"secs":5}"#);
367/// ```
368#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
369pub struct Activity {
370    /// The tool that runs, with its short text: `Bash: run just ci`.
371    #[serde(default, skip_serializing_if = "Option::is_none")]
372    pub tool: Option<String>,
373    /// True while a turn runs. False when the turn ended.
374    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
375    pub turn: bool,
376    /// The seconds since the fact.
377    pub secs: u64,
378}
379
380impl Activity {
381    /// True while a turn runs: a sign of work.
382    pub fn works(&self) -> bool {
383        self.turn
384    }
385}
386
387/// The reply to [`Alive`].
388#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
389pub struct AliveReply {
390    /// True when the session is a worker that must clear its context
391    /// before its next claim (01M3X9XB37TQCXWPNFZRMRGJB4). The reply to its last release
392    /// said so too.
393    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
394    pub clear: bool,
395}
396
397/// A worker session sends a keep-alive this often, so that it stops soon
398/// after the server asks (01M3Q5A0NKY1FCS0YH6N6YD3GN).
399pub const WORKER_ALIVE_EVERY: std::time::Duration = std::time::Duration::from_secs(10);
400
401/// `POST /v1/end`: the session ended (R205). It leaves `who`, and its
402/// claims are free at once.
403#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
404pub struct End {
405    pub me: SessionUri,
406}
407
408/// `POST /v1/who`: lists the known sessions. The call counts as a call
409/// of `me`.
410#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
411pub struct WhoRequest {
412    pub me: SessionUri,
413    /// True lists gone sessions too.
414    #[serde(default)]
415    pub all: bool,
416}
417
418#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
419pub struct WhoReply {
420    pub sessions: Vec<SessionInfo>,
421    /// The owner of the riff (01M3Q63NK0AHM25MB258B0K8XP).
422    #[serde(default)]
423    pub owner: RiffOwner,
424    /// Each member of the riff with a USER, also when away
425    /// (01M3NT4M3A4E3K5S2NM7MS6PQD). A riff with no sign-in has none.
426    #[serde(default, skip_serializing_if = "Vec::is_empty")]
427    pub people: Vec<Person>,
428}
429
430/// The reply to `GET /v1/me`: only the session of the caller, and the
431/// build of the server (01M3T5GFVS8NMA992KHZN4VE17). `session` is None
432/// when the server does not know the session.
433///
434/// ```
435/// use riff_core::wire::MeReply;
436///
437/// let reply: MeReply =
438///     serde_json::from_str(r#"{"session":null,"build":"0.8.0 e3cfe5a2919c 2026-09-29T21:12:48Z"}"#)?;
439/// assert!(reply.session.is_none());
440/// assert_eq!(reply.build, "0.8.0 e3cfe5a2919c 2026-09-29T21:12:48Z");
441/// # Ok::<(), serde_json::Error>(())
442/// ```
443#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
444pub struct MeReply {
445    /// The session of the caller, as `who` shows it.
446    pub session: Option<SessionInfo>,
447    /// The build of the server, as the `riff-build` header gives it.
448    pub build: String,
449}
450
451/// The reply to `GET /v1/server`: the facts of one `riff-server`, for
452/// `riff server` (01M3TJWJ12WEDCXW3W0529KRP2). Each field has a default,
453/// so a `riff` of another build reads the reply. Each time is in
454/// milliseconds since the Unix epoch, on the clock of the server.
455///
456/// ```
457/// use riff_core::wire::ServerFacts;
458///
459/// let facts: ServerFacts = serde_json::from_str(r#"{"position":7,"later":true}"#)?;
460/// assert_eq!(facts.position, 7);
461/// assert!(facts.not_serving.is_none() && facts.checkpoint.is_none());
462/// # Ok::<(), serde_json::Error>(())
463/// ```
464#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
465#[serde(default)]
466pub struct ServerFacts {
467    /// Why the server replies 503 to each other call. `None`: it serves.
468    pub not_serving: Option<String>,
469    /// The last error of the server since its start.
470    pub last_error: Option<FactError>,
471    /// The position of the last written record of the log.
472    pub position: u64,
473    /// The time of the last chunk write.
474    pub chunk_written_at_ms: Option<u64>,
475    /// How long the last chunk write took, in milliseconds.
476    pub chunk_write_ms: Option<u64>,
477    /// The number of failed tries of a chunk write since the start.
478    pub write_errors: u64,
479    /// The number of records that this build skipped since the start.
480    pub skipped_records: u64,
481    /// The newest checkpoint.
482    pub checkpoint: Option<CheckpointFacts>,
483    /// Why this build writes no checkpoint. `None`: it writes them.
484    pub no_checkpoint: Option<String>,
485    /// The number of chunks in the store.
486    pub chunks: u64,
487    pub sessions: u64,
488    /// The number of read cursors.
489    pub cursors: u64,
490    pub threads: u64,
491    /// The number of live token chains: one for each sign-in.
492    pub sign_ins: u64,
493    /// The memory that the server uses, in bytes. `None` when the
494    /// system does not tell.
495    pub memory_bytes: Option<u64>,
496    /// The start time of the instance.
497    pub started_at_ms: u64,
498    /// How long the load and the replay took, in milliseconds.
499    pub replay_ms: u64,
500    /// The time of this reply.
501    pub now_ms: u64,
502}
503
504/// An error of the server in [`ServerFacts`].
505#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
506#[serde(default)]
507pub struct FactError {
508    pub message: String,
509    pub at_ms: u64,
510}
511
512/// The newest checkpoint in [`ServerFacts`].
513#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
514#[serde(default)]
515pub struct CheckpointFacts {
516    pub position: u64,
517    pub written_at_ms: u64,
518    /// The version of the build that wrote it.
519    pub build: String,
520}
521
522/// A member of the riff in the reply to `who`: a person, not a session
523/// (01M3NT4M3A4E3K5S2NM7MS6PQD).
524///
525/// ```
526/// use riff_core::wire::{Person, PersonRole};
527///
528/// let ada: Person =
529///     serde_json::from_str(r#"{"user":"ada","role":"owner","live":false,"seen_secs":3600}"#).unwrap();
530/// assert_eq!(ada.role, PersonRole::Owner);
531/// assert_eq!(ada.role.tag(), Some("owner"));
532/// assert_eq!(PersonRole::Member.tag(), None);
533/// assert_eq!(ada.seen_secs, Some(3600));
534/// ```
535#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
536pub struct Person {
537    /// The USER of the person.
538    pub user: String,
539    pub role: PersonRole,
540    /// True while a session of the person is live.
541    pub live: bool,
542    /// The seconds since the last call of a session of the person.
543    /// `None` when the server knows no session of the person.
544    #[serde(default, skip_serializing_if = "Option::is_none")]
545    pub seen_secs: Option<u64>,
546}
547
548/// The role of a person in the riff.
549#[derive(
550    Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,
551)]
552#[serde(rename_all = "snake_case")]
553pub enum PersonRole {
554    Owner,
555    Admin,
556    Member,
557}
558
559impl PersonRole {
560    /// The tag on the row of the person: `owner`, `admin`, or none for
561    /// a member.
562    pub fn tag(self) -> Option<&'static str> {
563        match self {
564            PersonRole::Owner => Some("owner"),
565            PersonRole::Admin => Some("admin"),
566            PersonRole::Member => None,
567        }
568    }
569}
570
571/// The owner of the riff, in the reply to `who`
572/// (01M3Q63NK0AHM25MB258B0K8XP). A reply with no owner field reads as
573/// [`RiffOwner::NoSignIn`].
574///
575/// ```
576/// use riff_core::wire::{RiffOwner, WhoReply};
577///
578/// let reply: WhoReply = serde_json::from_str(
579///     r#"{"sessions":[],"owner":{"kind":"owner","user":"ada","email":"ada@gmail.com"}}"#,
580/// ).unwrap();
581/// assert_eq!(reply.owner, RiffOwner::Owner { user: "ada".into(), email: "ada@gmail.com".into() });
582/// assert!(reply.owner.is("ada") && !reply.owner.is("bob"));
583///
584/// let old: WhoReply = serde_json::from_str(r#"{"sessions":[]}"#).unwrap();
585/// assert_eq!(old.owner, RiffOwner::NoSignIn);
586/// assert!(!RiffOwner::Nobody.is("ada"));
587/// ```
588#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
589#[serde(tag = "kind", rename_all = "snake_case")]
590pub enum RiffOwner {
591    /// A riff with no sign-in: it shows no owner.
592    #[default]
593    NoSignIn,
594    /// A riff with sign-in and no owner yet.
595    Nobody,
596    /// The owner: the USER and the email, in lower case.
597    Owner { user: String, email: String },
598}
599
600impl RiffOwner {
601    /// True when `user` is the owner.
602    pub fn is(&self, user: &str) -> bool {
603        matches!(self, RiffOwner::Owner { user: owner, .. } if owner == user)
604    }
605}
606
607#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
608pub struct SessionInfo {
609    /// The URI now: the place and the claims are current.
610    pub uri: SessionUri,
611    /// True while the session has an open watch stream. A lead and a
612    /// session in a container are live while they are not gone, also
613    /// with no watch (01M48VDGQ5KETKPM4G6TKTC2MB,
614    /// 01M510MG0CKDXAKF6YJ768K7E8).
615    pub live: bool,
616    /// The seconds since the last call of the session. 0 while it is
617    /// live.
618    #[serde(default)]
619    pub idle_secs: u64,
620    /// The last status that the session set, if it set one.
621    #[serde(default, skip_serializing_if = "Option::is_none")]
622    pub status: Option<StatusInfo>,
623    /// True when the session registered as a worker
624    /// (01M3NT4M159EHN5W8JRTQ417N4).
625    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
626    pub worker: bool,
627    /// True when the riff daemon keeps the session alive: it runs in a
628    /// container. It is live while it is not gone
629    /// (01M510MG0CKDXAKF6YJ768K7E8).
630    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
631    pub container: bool,
632    /// The riff that the session runs, from its last keep-alive that
633    /// named one. `None` when no keep-alive named one, as from an older
634    /// riff (01M514C11BPE93PJT4221PYVHS).
635    #[serde(default, skip_serializing_if = "Option::is_none")]
636    pub binary: Option<RiffBinary>,
637    /// The seconds since the claims of the session last changed: a
638    /// claim, a release, a new start, or the start of `riff-server`. A
639    /// worker with no claim is idle for this time
640    /// (01M3Q551WCMPQRCNJ8FXQEBFY4).
641    #[serde(default)]
642    pub claims_secs: u64,
643    /// True when the session is a worker that must clear its context
644    /// before its next claim (01M3X9XAK1KPZZVM1AJR2H8DSS).
645    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
646    pub must_clear: bool,
647    /// The seconds since the last fresh start of the session: a new
648    /// agent process or a clear of its context. `None` when the log has
649    /// no such start (01M3X9XC99KY4RQY36A7CYWY11).
650    #[serde(default, skip_serializing_if = "Option::is_none")]
651    pub fresh_secs: Option<u64>,
652    /// The state of the session, that the server derives
653    /// (01M3QB6CJ1XCQG5B1BVR8AF3B4). An older server sends none: see
654    /// [`SessionInfo::fill_state`].
655    #[serde(default, skip_serializing_if = "Option::is_none")]
656    pub state: Option<SessionState>,
657    /// The newest fact of the hooks of the session, with its age now
658    /// (01M41FZNTPXQNCZ1S99HE42PYQ).
659    #[serde(default, skip_serializing_if = "Option::is_none")]
660    pub work: Option<Activity>,
661    /// What each claim of the session waits for, when each one waits
662    /// (01M41FZP9A50CH4A2VX344DW49).
663    #[serde(default, skip_serializing_if = "Option::is_none")]
664    pub waits: Option<Waits>,
665    /// The block of the session, while it holds
666    /// (01M41FZPGEK4TNPSM2051W4VMS).
667    #[serde(default, skip_serializing_if = "Option::is_none")]
668    pub blocked: Option<BlockedInfo>,
669    /// The long step of the session, until it is done
670    /// (01M48VDGTD40P8RBZMS0XB5M9N).
671    #[serde(default, skip_serializing_if = "Option::is_none")]
672    pub step: Option<StepInfo>,
673    /// The last numbers of the machine of the session, with their age,
674    /// when it sent some (01M515KSWVGCBS76AWZV74TJQ3).
675    #[serde(default, skip_serializing_if = "Option::is_none")]
676    pub machine: Option<MachineInfo>,
677}
678
679impl SessionInfo {
680    /// Sets the state from the other facts of the session when the
681    /// server sent none, as an older server does. `riff` is the state of
682    /// the riff. A state from the server stays.
683    ///
684    /// ```
685    /// use riff_core::wire::{RiffState, SessionInfo, SessionState};
686    ///
687    /// // A who reply of an older server: no state.
688    /// let json = r#"{"uri":"riff://mike@thelio/o/r?session=w1&claim=issue-12","live":true}"#;
689    /// let mut s: SessionInfo = serde_json::from_str(json).unwrap();
690    /// assert_eq!(s.state, None);
691    /// s.fill_state(RiffState::Running);
692    /// assert_eq!(s.state, Some(SessionState::Busy));
693    /// s.fill_state(RiffState::Paused);
694    /// assert_eq!(s.state, Some(SessionState::Busy), "a state stays");
695    /// ```
696    pub fn fill_state(&mut self, riff: RiffState) {
697        if self.state.is_some() {
698            return;
699        }
700        self.state = Some(SessionState::of(&Facts {
701            live: self.live,
702            paused: riff == RiffState::Paused,
703            blocked: self.blocked.is_some(),
704            must_clear: self.must_clear,
705            waiting: self.waits.is_some(),
706            claims: !self.uri.claims().is_empty(),
707            lead: self.uri.lead(),
708            turn: self.work.as_ref().is_some_and(Activity::works),
709        }));
710    }
711}
712
713/// The state of a session. The server derives it from facts; no
714/// session reports it (01M3QB6CJ1XCQG5B1BVR8AF3B4,
715/// 01M41FZQVEF8S2W9RCM4V87C3D). The first state that matches wins, in
716/// this order:
717///
718/// 1. `offline`: the session has no open watch stream. A lead is not
719///    offline while it is not gone, also with no watch
720///    (01M48VDGQ5KETKPM4G6TKTC2MB). A session in a container is not
721///    offline while the keep-alive of the riff daemon comes
722///    (01M510MG0CKDXAKF6YJ768K7E8).
723/// 2. `paused`: the riff is paused, or the repository of the session is
724///    paused (01M3XAHZBGSSJB3YX23K88W01K).
725/// 3. `blocked`: the session said that it cannot go on with no
726///    decision, and the block holds (01M41FZPGEK4TNPSM2051W4VMS). A
727///    lead with a block is `waiting` for its person
728///    (01M48VDS8RKJS9HG3KSEYGBFGV).
729/// 4. `must_clear`: it is a worker that must clear its context before
730///    its next claim (01M3X9XAK1KPZZVM1AJR2H8DSS).
731/// 5. `waiting`: each claim of the session waits for the machinery: a
732///    verify, a merge, or an item of its `Needs:` line
733///    (01M41FZP9A50CH4A2VX344DW49). Or a lead waits for its person.
734/// 6. `busy`: it holds a claim. Or it is the lead, and a turn runs.
735/// 7. `idle`: each other session.
736///
737/// ```mermaid
738/// stateDiagram-v2
739///     [*] --> idle
740///     idle --> busy: claim
741///     busy --> waiting: a verify is asked, a merge waits, a need is open
742///     waiting --> busy: the fact ends
743///     busy --> blocked: blocked REASON (wakes the lead)
744///     blocked --> busy: an answer, then a sign of work
745///     busy --> must_clear: the last release of a worker
746///     must_clear --> idle: a clear
747/// ```
748///
749/// ```
750/// use riff_core::wire::{Facts, SessionState};
751///
752/// let all = Facts {
753///     live: true, paused: true, blocked: true, must_clear: true,
754///     waiting: true, claims: true, lead: false, turn: false,
755/// };
756/// let of = |f: Facts| SessionState::of(&f);
757/// assert_eq!(of(Facts { live: false, ..all }), SessionState::Offline);
758/// assert_eq!(of(all), SessionState::Paused);
759/// let running = Facts { paused: false, ..all };
760/// assert_eq!(of(running), SessionState::Blocked);
761/// let free = Facts { blocked: false, ..running };
762/// assert_eq!(of(free), SessionState::MustClear);
763/// let cleared = Facts { must_clear: false, ..free };
764/// assert_eq!(of(cleared), SessionState::Waiting);
765/// assert_eq!(of(Facts { waiting: false, ..cleared }), SessionState::Busy);
766/// let none = Facts { waiting: false, claims: false, ..cleared };
767/// assert_eq!(of(none), SessionState::Idle);
768///
769/// // A lead: busy in a turn, waiting for its person, idle else.
770/// let lead = Facts { lead: true, ..none };
771/// assert_eq!(of(lead), SessionState::Idle);
772/// assert_eq!(of(Facts { turn: true, ..lead }), SessionState::Busy);
773/// assert_eq!(of(Facts { blocked: true, turn: true, ..lead }), SessionState::Waiting);
774///
775/// assert_eq!(serde_json::to_string(&SessionState::MustClear).unwrap(), r#""must_clear""#);
776/// assert_eq!(SessionState::MustClear.word(), "must clear");
777/// assert_eq!(serde_json::to_string(&SessionState::Waiting).unwrap(), r#""waiting""#);
778/// assert_eq!(SessionState::Blocked.word(), "blocked");
779/// ```
780#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
781#[serde(rename_all = "snake_case")]
782pub enum SessionState {
783    #[default]
784    Offline,
785    Paused,
786    Blocked,
787    MustClear,
788    Waiting,
789    Busy,
790    Idle,
791}
792
793/// The facts that make the state of a session ([`SessionState::of`]).
794#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
795pub struct Facts {
796    /// An open watch stream, or a lead or a session in a container
797    /// that is not gone.
798    pub live: bool,
799    /// The riff or the repository of the session is paused.
800    pub paused: bool,
801    /// A block that holds.
802    pub blocked: bool,
803    /// A worker that must clear its context.
804    pub must_clear: bool,
805    /// Each claim waits.
806    pub waiting: bool,
807    /// The session holds a claim.
808    pub claims: bool,
809    /// The session is the lead of its user.
810    pub lead: bool,
811    /// The hooks saw a turn that runs.
812    pub turn: bool,
813}
814
815impl SessionState {
816    /// The state of a session from its facts. A lead has no claims, so
817    /// its state comes from its turn and its block
818    /// (01M48VDS8RKJS9HG3KSEYGBFGV).
819    pub fn of(f: &Facts) -> Self {
820        if !f.live {
821            SessionState::Offline
822        } else if f.paused {
823            SessionState::Paused
824        } else if f.blocked && !f.lead {
825            SessionState::Blocked
826        } else if f.must_clear {
827            SessionState::MustClear
828        } else if (f.waiting && f.claims) || (f.blocked && f.lead) {
829            SessionState::Waiting
830        } else if f.claims || (f.lead && f.turn) {
831            SessionState::Busy
832        } else {
833            SessionState::Idle
834        }
835    }
836
837    /// The word of the state, the same in each command.
838    pub fn word(self) -> &'static str {
839        match self {
840            SessionState::Offline => "offline",
841            SessionState::Paused => "paused",
842            SessionState::Blocked => "blocked",
843            SessionState::MustClear => "must clear",
844            SessionState::Waiting => "waiting",
845            SessionState::Busy => "busy",
846            SessionState::Idle => "idle",
847        }
848    }
849}
850
851/// What the claims of a session wait for (01M41FZP9A50CH4A2VX344DW49).
852/// The server makes it from the facts of the items ([`ItemFact`]).
853/// Nobody must decide, so it wakes nobody.
854///
855/// ```
856/// use riff_core::wire::Waits;
857///
858/// assert_eq!(Waits::Verify { pull: 418 }.to_string(), "waits for a verify of PR #418");
859/// assert_eq!(Waits::Merge { pull: 418 }.to_string(), "waits for the merge of PR #418");
860/// assert_eq!(Waits::Needs { issues: vec![12, 15] }.to_string(), "waits for #12, #15");
861/// let json = serde_json::to_string(&Waits::Merge { pull: 418 }).unwrap();
862/// assert_eq!(json, r#"{"for":"merge","pull":418}"#);
863/// ```
864#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
865#[serde(tag = "for", rename_all = "snake_case")]
866pub enum Waits {
867    /// The pull request of the item waits for a verify.
868    Verify { pull: u64 },
869    /// The verify passed: the pull request waits for the merge.
870    Merge { pull: u64 },
871    /// Items of the `Needs:` line of the item are open.
872    Needs { issues: Vec<u64> },
873}
874
875impl std::fmt::Display for Waits {
876    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
877        match self {
878            Waits::Verify { pull } => write!(f, "waits for a verify of PR #{pull}"),
879            Waits::Merge { pull } => write!(f, "waits for the merge of PR #{pull}"),
880            Waits::Needs { issues } => {
881                let issues: Vec<String> = issues.iter().map(|n| format!("#{n}")).collect();
882                write!(f, "waits for {}", issues.join(", "))
883            }
884        }
885    }
886}
887
888/// A block in the reply to `who` (01M41FZPGEK4TNPSM2051W4VMS).
889#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
890pub struct BlockedInfo {
891    /// Why the session cannot go on.
892    pub reason: String,
893    /// The seconds since the session said it.
894    pub secs: u64,
895    /// True when a message woke the session after the block. The block
896    /// ends at the next sign of work (01M41FZPT31ATXP75QW965P3JB).
897    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
898    pub answered: bool,
899    /// True when riff woke the lead a second time
900    /// (01M41FZQ545HQ9Q75CSKX8HF8H).
901    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
902    pub woken_again: bool,
903    /// True when the lead gave no answer after the second wake
904    /// (01M41FZQCHWY1YVGAZ60ZHJK21).
905    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
906    pub unanswered: bool,
907}
908
909/// The most characters in the step of a [`Status`], and in the reason
910/// of a [`SetBlocked`].
911pub const STATUS_CHARS: usize = 400;
912
913/// The most characters in the numbers of a [`SetMachine`]: the facts of
914/// the daemon of a host are longer than a status
915/// (01M57D3F50347RD8ED79KEA9V7).
916pub const MACHINE_CHARS: usize = 4000;
917
918/// What a session does now, in its own words: its current step. The
919/// words help a person. They make no state: a block is [`SetBlocked`]
920/// (01M41FZPGEK4TNPSM2051W4VMS).
921#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
922pub struct Status {
923    pub step: String,
924}
925
926impl Status {
927    /// Refuses a status that `who` cannot show on one line: an empty
928    /// step, a line break, or more than [`STATUS_CHARS`] characters.
929    ///
930    /// ```
931    /// use riff_core::wire::Status;
932    ///
933    /// let status = |step: &str| Status { step: step.into() };
934    /// assert!(status("write the tests").check().is_ok());
935    /// assert!(status(" ").check().is_err());
936    /// assert!(status("two\nlines").check().is_err());
937    /// assert!(status(&"x".repeat(401)).check().is_err());
938    /// ```
939    pub fn check(&self) -> Result<(), String> {
940        one_line("step", &self.step)
941    }
942}
943
944/// Refuses a text that `who` cannot show on one line: empty, with a
945/// line break, or with more than [`STATUS_CHARS`] characters. `what`
946/// names the text in the error.
947fn one_line(what: &str, text: &str) -> Result<(), String> {
948    line_of(what, text, STATUS_CHARS)
949}
950
951/// Refuses a text that is empty, has a line break, or has more than
952/// `most` characters.
953fn line_of(what: &str, text: &str, most: usize) -> Result<(), String> {
954    if text.trim().is_empty() {
955        return Err(format!("the {what} is empty"));
956    }
957    if text.chars().any(char::is_control) {
958        return Err(format!("the {what} must be one line"));
959    }
960    if text.chars().count() > most {
961        return Err(format!("the {what} has more than {most} characters"));
962    }
963    Ok(())
964}
965
966/// A [`Status`] in the reply to `who`, with its age.
967#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
968pub struct StatusInfo {
969    #[serde(flatten)]
970    pub status: Status,
971    /// The seconds since the session set the status.
972    pub age_secs: u64,
973    /// True when the session set the status before the last change of
974    /// its state: a claim or a release, a pause or a resume of the
975    /// riff, or a start of `riff-server` (01M3Q551YHYZBFV2NDS1QCYXCD).
976    /// A stale status is not the current state of the session.
977    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
978    pub stale: bool,
979}
980
981/// `POST /v1/status`: sets the status of `me`. It replaces the old
982/// status.
983#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
984pub struct SetStatus {
985    pub me: SessionUri,
986    pub status: Status,
987}
988
989/// `POST /v1/blocked`: `me` cannot go on with no decision
990/// (01M41FZPGEK4TNPSM2051W4VMS). It is a signal. The `blocked` tool and
991/// `riff blocked` send it with the message that wakes the lead, in one
992/// command: a session does neither alone.
993///
994/// ```
995/// use riff_core::wire::SetBlocked;
996///
997/// let me: riff_core::name::SessionUri = "riff://mike@pangolin/o/r?session=a1".parse()?;
998/// let set = |reason: &str| SetBlocked { me: me.clone(), reason: reason.into() };
999/// assert!(set("which of the two designs?").check().is_ok());
1000/// assert_eq!(set(" ").check().unwrap_err(), "the reason is empty");
1001/// # Ok::<(), riff_core::name::NameError>(())
1002/// ```
1003#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1004pub struct SetBlocked {
1005    pub me: SessionUri,
1006    pub reason: String,
1007}
1008
1009impl SetBlocked {
1010    /// Refuses a reason that is not one line of at most
1011    /// [`STATUS_CHARS`] characters.
1012    pub fn check(&self) -> Result<(), String> {
1013        one_line("reason", &self.reason)
1014    }
1015}
1016
1017/// A change of the long step of a session (01M48VDGTD40P8RBZMS0XB5M9N):
1018/// a step that runs longer than a tool call, for example a live window
1019/// of two hours.
1020///
1021/// ```
1022/// use riff_core::wire::StepChange;
1023///
1024/// let start = StepChange::Start { name: "live window".into() };
1025/// assert_eq!(serde_json::to_string(&start).unwrap(), r#"{"start":{"name":"live window"}}"#);
1026/// assert_eq!(serde_json::to_string(&StepChange::Done).unwrap(), r#""done""#);
1027/// ```
1028#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1029#[serde(rename_all = "snake_case")]
1030pub enum StepChange {
1031    /// A new step starts. It replaces the old step.
1032    Start { name: String },
1033    /// The step ended well. No step shows.
1034    Done,
1035    /// The step failed, for `reason`. It shows until the next change.
1036    Fail { reason: String },
1037}
1038
1039/// `POST /v1/step`: a change of the long step of `me`
1040/// (01M48VDGTD40P8RBZMS0XB5M9N). It is a signal, and a sign of life.
1041/// `riff step fail` also wakes the lead (01M48VDS663X064YS5ZGCCZSTB).
1042///
1043/// ```
1044/// use riff_core::wire::{SetStep, StepChange};
1045///
1046/// let me: riff_core::name::SessionUri = "riff://mike@pangolin/o/r?session=a1".parse()?;
1047/// let set = |change| SetStep { me: me.clone(), change };
1048/// assert!(set(StepChange::Start { name: "live window".into() }).check().is_ok());
1049/// assert!(set(StepChange::Done).check().is_ok());
1050/// assert_eq!(
1051///     set(StepChange::Fail { reason: " ".into() }).check().unwrap_err(),
1052///     "the reason is empty"
1053/// );
1054/// assert_eq!(
1055///     set(StepChange::Start { name: "a\nb".into() }).check().unwrap_err(),
1056///     "the name must be one line"
1057/// );
1058/// # Ok::<(), riff_core::name::NameError>(())
1059/// ```
1060#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1061pub struct SetStep {
1062    pub me: SessionUri,
1063    pub change: StepChange,
1064}
1065
1066impl SetStep {
1067    /// Refuses a name or a reason that is not one line of at most
1068    /// [`STATUS_CHARS`] characters.
1069    pub fn check(&self) -> Result<(), String> {
1070        match &self.change {
1071            StepChange::Start { name } => one_line("name", name),
1072            StepChange::Done => Ok(()),
1073            StepChange::Fail { reason } => one_line("reason", reason),
1074        }
1075    }
1076}
1077
1078/// `POST /v1/machine`: the numbers of the machine of `me`
1079/// (01M515KSWVGCBS76AWZV74TJQ3). The `riff mcp` of the lead sends them
1080/// for its machine, which has no `riff workers host`. The numbers are
1081/// the text of the status of a workers host. The daemon of a host sends
1082/// its facts in the same way (01M57D3F50347RD8ED79KEA9V7). riff-server
1083/// keeps the text and does not read it. It is a signal, like a status.
1084///
1085/// ```
1086/// use riff_core::wire::SetMachine;
1087///
1088/// let me: riff_core::name::SessionUri = "riff://mike@pangolin/o/r?session=a1".parse()?;
1089/// let set = |numbers: &str| SetMachine { me: me.clone(), numbers: numbers.into() };
1090/// assert!(set("workers host: limit 2, floor 4GB, no workers").check().is_ok());
1091/// assert_eq!(set(" ").check().unwrap_err(), "the text of the numbers is empty");
1092/// assert!(set(&"x".repeat(4000)).check().is_ok());
1093/// assert!(set(&"x".repeat(4001)).check().is_err());
1094/// assert!(set("two\nlines").check().is_err());
1095/// # Ok::<(), riff_core::name::NameError>(())
1096/// ```
1097#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1098pub struct SetMachine {
1099    pub me: SessionUri,
1100    pub numbers: String,
1101}
1102
1103impl SetMachine {
1104    /// Refuses numbers that are not one line of at most
1105    /// [`MACHINE_CHARS`] characters.
1106    pub fn check(&self) -> Result<(), String> {
1107        line_of("text of the numbers", &self.numbers, MACHINE_CHARS)
1108    }
1109}
1110
1111/// The numbers of the machine of a session in the reply to `who`
1112/// (01M515KSWVGCBS76AWZV74TJQ3).
1113#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1114pub struct MachineInfo {
1115    /// The text of [`SetMachine`].
1116    pub numbers: String,
1117    /// The seconds since the session sent them.
1118    pub age_secs: u64,
1119}
1120
1121/// A long step in the reply to `who` (01M48VDGTD40P8RBZMS0XB5M9N).
1122#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1123pub struct StepInfo {
1124    /// The name of the step.
1125    pub name: String,
1126    /// The seconds since the step started, or since it failed.
1127    pub secs: u64,
1128    /// The reason, when the step failed.
1129    #[serde(default, skip_serializing_if = "Option::is_none")]
1130    pub failed: Option<String>,
1131}
1132
1133/// `POST /v1/blocked/look`: the lead `me` looks at the blocks of the
1134/// sessions of its user in its repository. A block with no answer for
1135/// `after_secs` wakes the lead again (01M41FZQ545HQ9Q75CSKX8HF8H). A
1136/// block with no answer for `after_secs` after that is unanswered
1137/// (01M41FZQCHWY1YVGAZ60ZHJK21). The server refuses a caller that is not
1138/// the lead.
1139#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1140pub struct BlockedLook {
1141    pub me: SessionUri,
1142    pub after_secs: u64,
1143}
1144
1145/// The reply to [`BlockedLook`]: each block that this look made
1146/// unanswered. The `riff mcp` of the lead shows each one in a desktop
1147/// notification (01M41FZQKZKW131Z8822G31T5G).
1148#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1149pub struct BlockedLookReply {
1150    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1151    pub unanswered: Vec<Unanswered>,
1152}
1153
1154/// A block with no answer of the lead: the session with its claims, and
1155/// the reason. It holds no text of a message.
1156#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1157pub struct Unanswered {
1158    pub session: SessionUri,
1159    pub reason: String,
1160}
1161
1162/// `POST /v1/items`: what a client saw of the items of its repository
1163/// on the forge (01M41FZP2C4Z4J6WKRXZ5B31EH). The server has no
1164/// credential of the forge: `riff pr open`, `riff verify`, `riff pr
1165/// wait` and the look of the lead send the facts. It is a signal. With
1166/// `all`, the facts replace each fact of the repository: an item with
1167/// no fact in the list has none now. Else each fact replaces only the
1168/// fact of its item.
1169#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1170pub struct ItemFacts {
1171    pub me: SessionUri,
1172    pub items: Vec<ItemFact>,
1173    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1174    pub all: bool,
1175}
1176
1177/// The facts of one item, for example `issue-12`.
1178///
1179/// ```
1180/// use riff_core::wire::{ItemFact, PullFact, PullState, Waits};
1181///
1182/// let fact = |state| ItemFact {
1183///     item: "issue-12".into(),
1184///     pull: Some(PullFact { number: 40, state }),
1185///     needs: vec![],
1186/// };
1187/// assert_eq!(fact(PullState::Asked).waits("issue-12"), Some(Waits::Verify { pull: 40 }));
1188/// assert_eq!(fact(PullState::Passed).waits("issue-12"), Some(Waits::Merge { pull: 40 }));
1189/// assert_eq!(fact(PullState::Passed).waits("verify-issue-12"), Some(Waits::Merge { pull: 40 }));
1190/// assert_eq!(fact(PullState::Asked).waits("verify-issue-12"), None, "the verifier works");
1191/// assert_eq!(fact(PullState::Failed).waits("issue-12"), None);
1192/// assert_eq!(fact(PullState::Merged).waits("issue-12"), None);
1193/// let needs = ItemFact { item: "issue-12".into(), pull: None, needs: vec![9] };
1194/// assert_eq!(needs.waits("issue-12"), Some(Waits::Needs { issues: vec![9] }));
1195/// assert_eq!(needs.waits("verify-issue-12"), None);
1196/// ```
1197#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1198pub struct ItemFact {
1199    pub item: String,
1200    /// The newest pull request of the item.
1201    #[serde(default, skip_serializing_if = "Option::is_none")]
1202    pub pull: Option<PullFact>,
1203    /// The open items of its `Needs:` line.
1204    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1205    pub needs: Vec<u64>,
1206}
1207
1208impl ItemFact {
1209    /// What a session that holds `claim` waits for, by this fact. The
1210    /// author waits for the verify and for the merge. The session that
1211    /// verifies works until its result, then waits for the merge.
1212    pub fn waits(&self, claim: &str) -> Option<Waits> {
1213        let verify = claim.starts_with("verify-");
1214        match self.pull.as_ref().map(|p| (p.number, p.state)) {
1215            Some((pull, PullState::Asked)) if !verify => Some(Waits::Verify { pull }),
1216            Some((pull, PullState::Passed)) => Some(Waits::Merge { pull }),
1217            _ if !verify && !self.needs.is_empty() => Some(Waits::Needs {
1218                issues: self.needs.clone(),
1219            }),
1220            _ => None,
1221        }
1222    }
1223}
1224
1225/// A pull request of an item, and its state.
1226#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1227pub struct PullFact {
1228    pub number: u64,
1229    pub state: PullState,
1230}
1231
1232/// The state of a pull request: from the status `riff/verify` of its
1233/// head, and from its merge.
1234#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1235#[serde(rename_all = "snake_case")]
1236pub enum PullState {
1237    /// Open, with no verify result: it waits for a verify.
1238    Asked,
1239    /// The verify passed: it waits for the merge.
1240    Passed,
1241    /// The verify failed: the item is free with its work.
1242    Failed,
1243    /// Merged.
1244    Merged,
1245}
1246
1247/// `POST /v1/threads`: lists the threads of `me`, with unread counts.
1248#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1249pub struct Threads {
1250    pub me: SessionUri,
1251}
1252
1253#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1254pub struct ThreadsReply {
1255    pub threads: Vec<ThreadInfo>,
1256}
1257
1258#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1259pub struct ThreadInfo {
1260    pub thread: ThreadName,
1261    pub members: Vec<SessionUri>,
1262    pub unread: usize,
1263}
1264
1265/// `POST /v1/join`: adds `me` to a thread. It makes the thread if it
1266/// is new.
1267#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1268pub struct Join {
1269    pub me: SessionUri,
1270    pub thread: ThreadName,
1271}
1272
1273/// `POST /v1/leave`: removes `me` from a thread. It is no longer the
1274/// lead there.
1275#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1276pub struct Leave {
1277    pub me: SessionUri,
1278    pub thread: ThreadName,
1279}
1280
1281/// `POST /v1/post`: adds a message to a thread and wakes each session
1282/// that `to` selects. The sender and each woken session join the
1283/// thread.
1284///
1285/// With no thread, the post is a direct message: `to` must be one
1286/// selector with a `session`, and the thread is the direct thread of
1287/// the two sessions.
1288///
1289/// A signed-in sender signs the post with [`Post::sign`] (R195).
1290///
1291/// ```
1292/// use riff_core::dpop::Key;
1293/// use riff_core::wire::Post;
1294///
1295/// let me = "riff://mike@pangolin/como-technologies/riff?session=a6cf".parse()?;
1296/// let mut post = Post::new(&me, Some("design".parse()?), vec![], "look");
1297/// let key = Key::generate();
1298/// post.sign(&key, 1_000);
1299/// let content = post.content().unwrap();
1300/// assert_eq!(content.verify(post.sig.as_ref().unwrap()).unwrap(), key.thumbprint());
1301/// # Ok::<(), riff_core::name::NameError>(())
1302/// ```
1303#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1304pub struct Post {
1305    pub me: SessionUri,
1306    #[serde(default)]
1307    pub thread: Option<ThreadName>,
1308    #[serde(default)]
1309    pub to: Vec<Selector>,
1310    pub body: String,
1311    #[serde(default, skip_serializing_if = "Kind::is_message")]
1312    pub kind: Kind,
1313    /// The signed time, in milliseconds since the Unix epoch.
1314    #[serde(default, skip_serializing_if = "Option::is_none")]
1315    pub at_ms: Option<u64>,
1316    /// The signature of the sender (see [`crate::signed`]).
1317    #[serde(default, skip_serializing_if = "Option::is_none")]
1318    pub sig: Option<String>,
1319    /// The bytes that `sig` signs: the base64url of the JSON of
1320    /// [`Content`]. The server and each reader keep them unchanged.
1321    #[serde(default, skip_serializing_if = "Option::is_none")]
1322    pub payload: Option<String>,
1323    /// The key of the news that the post tells. The signature does not
1324    /// cover it. The server drops a post whose key is the key of a kept
1325    /// message of the same user in the thread (01M5TKG2K1AYK1KE7C2AA2W9DS).
1326    #[serde(default, skip_serializing_if = "Option::is_none")]
1327    pub key: Option<String>,
1328}
1329
1330impl Post {
1331    /// A post of kind [`Kind::Message`], with no signature.
1332    pub fn new(me: &SessionUri, thread: Option<ThreadName>, to: Vec<Selector>, body: &str) -> Post {
1333        Post {
1334            me: me.clone(),
1335            thread,
1336            to,
1337            body: body.to_owned(),
1338            kind: Kind::Message,
1339            at_ms: None,
1340            sig: None,
1341            payload: None,
1342            key: None,
1343        }
1344    }
1345
1346    /// Signs the post with the device key `key`, at the time `at_ms`
1347    /// (R195, R196). The post carries the payload and the signature.
1348    pub fn sign(&mut self, key: &Key, at_ms: u64) {
1349        self.at_ms = Some(at_ms);
1350        self.payload = self.content().map(|content| content.payload());
1351        self.sig = self
1352            .payload
1353            .as_deref()
1354            .map(|payload| crate::signed::sign_payload(payload, key));
1355    }
1356
1357    /// What the signature covers. `None` when the post has no signed
1358    /// time.
1359    pub fn content(&self) -> Option<Content<'_>> {
1360        // A new field of a post must say here whether the signature
1361        // covers it.
1362        let Post {
1363            me,
1364            thread,
1365            to,
1366            body,
1367            kind,
1368            at_ms,
1369            sig: _,
1370            payload: _,
1371            key: _,
1372        } = self;
1373        Some(Content {
1374            from: me.who(),
1375            lead: me.lead(),
1376            thread: thread.as_ref(),
1377            to,
1378            body,
1379            kind: *kind,
1380            at_ms: (*at_ms)?,
1381        })
1382    }
1383}
1384
1385/// The kind of a post. A `post` call sends `message`, `status` or
1386/// `note`.
1387///
1388/// ```
1389/// use riff_core::wire::Kind;
1390///
1391/// assert_eq!(serde_json::to_string(&Kind::Status).unwrap(), r#""status""#);
1392/// assert_eq!("status".parse::<Kind>(), Ok(Kind::Status));
1393/// assert_eq!(serde_json::to_string(&Kind::Note).unwrap(), r#""note""#);
1394/// assert_eq!("note".parse::<Kind>(), Ok(Kind::Note));
1395/// assert!("other".parse::<Kind>().is_err());
1396/// assert!(Kind::Note.needs_body() && !Kind::Status.needs_body());
1397/// ```
1398#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, JsonSchema)]
1399#[serde(rename_all = "lowercase")]
1400pub enum Kind {
1401    /// A message to read.
1402    #[default]
1403    Message,
1404    /// A status request. Each session that it wakes sets its status
1405    /// with `status`. It does not post a reply.
1406    Status,
1407    /// A note: it informs and wakes no session. A session sees it at its
1408    /// next `read` (01M3JPMQE6S7YM4HPEVGXWK7ET).
1409    Note,
1410    /// A message that riff itself writes, as the person of the riff: the
1411    /// watch of a deploy, the daemon, the lines of `riff release`. It
1412    /// reads and wakes as a message. It never counts as the word of a
1413    /// person ([`Kind::by_riff`]). A session has no use for it, so a
1414    /// schema of a call does not list it.
1415    #[schemars(skip)]
1416    Riff,
1417    /// A kind that this build does not know. A reader shows the post as
1418    /// a message.
1419    #[schemars(skip)]
1420    Other,
1421}
1422
1423/// The set of kinds can grow. A build reads each value that it does not
1424/// know as [`Kind::Other`]: a text, and each other form of JSON
1425/// (01M3XSF90E9JYYTC13D9THY4WE). The kind [`Kind::Riff`] has a name in
1426/// JSON but not in [`FromStr`](std::str::FromStr): a person types no
1427/// such kind.
1428impl<'de> Deserialize<'de> for Kind {
1429    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
1430        let value = serde_json::Value::deserialize(deserializer)?;
1431        Ok(match value.as_str() {
1432            Some("riff") => Kind::Riff,
1433            text => text
1434                .and_then(|text| text.parse().ok())
1435                .unwrap_or(Kind::Other),
1436        })
1437    }
1438}
1439
1440impl Kind {
1441    pub fn is_message(&self) -> bool {
1442        *self == Kind::Message
1443    }
1444
1445    /// True for the kind of a line that riff writes, not a person:
1446    /// [`Kind::Riff`].
1447    ///
1448    /// ```
1449    /// use riff_core::wire::Kind;
1450    ///
1451    /// assert!(Kind::Riff.by_riff());
1452    /// assert!(![Kind::Message, Kind::Status, Kind::Note, Kind::Other].iter().any(Kind::by_riff));
1453    /// ```
1454    pub fn by_riff(&self) -> bool {
1455        *self == Kind::Riff
1456    }
1457
1458    /// True when a post of this kind needs a body. Only a status request
1459    /// needs none.
1460    pub fn needs_body(&self) -> bool {
1461        *self != Kind::Status
1462    }
1463
1464    /// True for a kind that a `post` call can have. A kind of a later
1465    /// build reads as [`Kind::Other`], and no `post` call can have it
1466    /// (01M3XSF90E9JYYTC13D9THY4WE).
1467    ///
1468    /// ```
1469    /// use riff_core::wire::Kind;
1470    ///
1471    /// let read = |json: &str| serde_json::from_str::<Kind>(json).unwrap();
1472    /// assert_eq!(read(r#""note""#), Kind::Note);
1473    /// // The kind of a line of riff has a name in JSON, and none for a person.
1474    /// assert_eq!(read(r#""riff""#), Kind::Riff);
1475    /// assert_eq!(serde_json::to_string(&Kind::Riff).unwrap(), r#""riff""#);
1476    /// assert!("riff".parse::<Kind>().is_err());
1477    /// // A kind of a later build can have each form of JSON.
1478    /// for later in [r#""poll""#, r#"{"poll":"wave"}"#, "7", "null"] {
1479    ///     assert_eq!(read(later), Kind::Other, "{later}");
1480    /// }
1481    /// assert!(Kind::Note.is_post() && Kind::Riff.is_post() && !Kind::Other.is_post());
1482    /// assert_eq!(serde_json::to_string(&Kind::Other).unwrap(), r#""other""#);
1483    ///
1484    /// // The schema of a call has only the kinds of a `post` call.
1485    /// let schema = serde_json::to_value(schemars::schema_for!(Kind)).unwrap();
1486    /// let kinds: Vec<&str> = schema["oneOf"]
1487    ///     .as_array()
1488    ///     .unwrap()
1489    ///     .iter()
1490    ///     .map(|kind| kind["const"].as_str().unwrap())
1491    ///     .collect();
1492    /// assert_eq!(kinds, ["message", "status", "note"]);
1493    /// ```
1494    pub fn is_post(&self) -> bool {
1495        *self != Kind::Other
1496    }
1497}
1498
1499impl std::str::FromStr for Kind {
1500    type Err = String;
1501
1502    fn from_str(text: &str) -> Result<Self, Self::Err> {
1503        match text {
1504            "message" => Ok(Kind::Message),
1505            "status" => Ok(Kind::Status),
1506            "note" => Ok(Kind::Note),
1507            _ => Err(format!("no kind {text}: use message, status or note")),
1508        }
1509    }
1510}
1511
1512/// The reply to `post`. It tells the sender who woke.
1513#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1514pub struct Posted {
1515    pub thread: ThreadName,
1516    pub seq: u64,
1517    /// Each session that the message woke.
1518    #[serde(default)]
1519    pub woken: Vec<SessionUri>,
1520    /// Each selector that matched no session.
1521    #[serde(default)]
1522    pub unmatched: Vec<Selector>,
1523}
1524
1525/// `POST /v1/read`: returns the messages that `me` has not read, or
1526/// all of them when `all` is true. It marks them as read.
1527#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1528pub struct Read {
1529    pub me: SessionUri,
1530    pub thread: ThreadName,
1531    #[serde(default)]
1532    pub all: bool,
1533    /// With `all`: the page after this seq, from [`ReadReply::next`].
1534    #[serde(default, skip_serializing_if = "Option::is_none")]
1535    pub after: Option<u64>,
1536}
1537
1538#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1539pub struct ReadReply {
1540    /// At most one page of messages (01M3TBZBX140GJWCV5GZ73Q5Z5).
1541    pub messages: Vec<Message>,
1542    /// The seq of the last message of the page, when more messages
1543    /// follow. Read again for the next page: with `all`, set
1544    /// [`Read::after`] to it.
1545    #[serde(default, skip_serializing_if = "Option::is_none")]
1546    pub next: Option<u64>,
1547    /// In an unread read: each message after the page that woke `me`
1548    /// and that no read showed yet. The read marks it as seen, so the
1549    /// next watch does not wake for it (01M588J4XNV1TXEPMGTKRVY9VK).
1550    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1551    pub woken: Vec<Message>,
1552    /// The keys of the user of each sender, to verify the messages.
1553    #[serde(default, skip_serializing_if = "Keys::is_empty")]
1554    pub keys: Keys,
1555    /// True from a riff with no sign-in: it trusts each caller (R211).
1556    #[serde(default)]
1557    pub trusted: bool,
1558}
1559
1560/// The thumbprints of the device keys of each user, by user: the keys
1561/// of the live sign-ins of that user. A server without sign-in gives
1562/// none (R201).
1563pub type Keys = BTreeMap<String, Vec<String>>;
1564
1565#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1566pub struct Message {
1567    pub seq: u64,
1568    /// The URI of the sender when it posted.
1569    pub from: SessionUri,
1570    /// The address of the post.
1571    #[serde(default)]
1572    pub to: Vec<Selector>,
1573    pub body: String,
1574    /// Milliseconds since the Unix epoch. For a signed message, the
1575    /// signed time (R198).
1576    pub at_ms: u64,
1577    #[serde(default, skip_serializing_if = "Kind::is_message")]
1578    pub kind: Kind,
1579    /// The signature of the sender (see [`crate::signed`]).
1580    #[serde(default, skip_serializing_if = "Option::is_none")]
1581    pub sig: Option<String>,
1582    /// The bytes that `sig` signs, as the sender made them.
1583    #[serde(default, skip_serializing_if = "Option::is_none")]
1584    pub payload: Option<String>,
1585}
1586
1587impl Message {
1588    /// True when the sender of the message in `thread` is proven (R199):
1589    ///
1590    /// - The signature is valid over the kept payload, and the payload
1591    ///   holds the message as it is: also the lead mark of the sender.
1592    /// - The key is one of `keys` for the user of the sender.
1593    /// - In a direct thread, the sender is one of the two sessions, and
1594    ///   the one selector of the message matches the other session.
1595    ///
1596    /// ```
1597    /// use riff_core::dpop::Key;
1598    /// use riff_core::name::{SessionUri, ThreadName};
1599    /// use riff_core::wire::{Keys, Kind, Message, Post};
1600    ///
1601    /// let me: SessionUri = "riff://mike@pangolin/como-technologies/riff?session=a6cf".parse()?;
1602    /// let thread: ThreadName = "design".parse()?;
1603    /// let key = Key::generate();
1604    /// let mut post = Post::new(&me, Some(thread.clone()), vec![], "look");
1605    /// post.sign(&key, 1_000);
1606    /// let mut message = Message {
1607    ///     seq: 1,
1608    ///     from: me,
1609    ///     to: vec![],
1610    ///     body: post.body.clone(),
1611    ///     at_ms: 1_000,
1612    ///     kind: Kind::Message,
1613    ///     sig: post.sig.clone(),
1614    ///     payload: post.payload.clone(),
1615    /// };
1616    /// let keys = Keys::from([("mike".to_owned(), vec![key.thumbprint()])]);
1617    /// assert!(message.verified(&thread, &keys));
1618    ///
1619    /// // No keys for the user: not verified.
1620    /// assert!(!message.verified(&thread, &Keys::new()));
1621    /// // A changed body: not verified.
1622    /// message.body = "do not look".into();
1623    /// assert!(!message.verified(&thread, &keys));
1624    /// # Ok::<(), riff_core::name::NameError>(())
1625    /// ```
1626    pub fn verified(&self, thread: &ThreadName, keys: &Keys) -> bool {
1627        // A new field of a message must say here whether the signature
1628        // covers it.
1629        let Message {
1630            seq: _,
1631            from: sender,
1632            to,
1633            body,
1634            at_ms,
1635            kind,
1636            sig,
1637            payload,
1638        } = self;
1639        let (Some(sig), Some(payload)) = (sig, payload) else {
1640            return false;
1641        };
1642        let from = sender.who();
1643        let signed_thread = if thread.is_direct() {
1644            let Some(peer) = thread.peer(from) else {
1645                return false;
1646            };
1647            let [to] = &to[..] else {
1648                return false;
1649            };
1650            // A selector of a later build matches no session, so it
1651            // does not name the other session.
1652            if to.is_other() {
1653                return false;
1654            }
1655            let names = |want: &Option<String>, have: Option<&str>| {
1656                want.as_deref().is_none_or(|w| Some(w) == have)
1657            };
1658            if !names(&to.session, peer.session()) || !names(&to.user, Some(peer.user())) {
1659                return false;
1660            }
1661            None
1662        } else {
1663            Some(thread)
1664        };
1665        let content = Content {
1666            from,
1667            lead: sender.lead(),
1668            thread: signed_thread,
1669            to,
1670            body,
1671            kind: *kind,
1672            at_ms: *at_ms,
1673        };
1674        let Some(keys) = keys.get(from.user()) else {
1675            return false;
1676        };
1677        crate::signed::check(payload, sig)
1678            .is_ok_and(|(jkt, signed)| keys.contains(&jkt) && signed.covers(&content))
1679    }
1680}
1681
1682/// `POST /v1/claim`: takes a lease on one work item. The server
1683/// refuses a claim of an item that another session holds, with status
1684/// 409 and a text that names the holder (01M3WRD9JBQMNN96TXJH8EAJ3W).
1685#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1686pub struct Claim {
1687    pub me: SessionUri,
1688    pub thread: ThreadName,
1689    pub item: String,
1690}
1691
1692/// The reply to a claim that the server took.
1693///
1694/// ```
1695/// use riff_core::wire::ClaimReply;
1696///
1697/// // The reply of a server of 1.0.0 has no warning.
1698/// let old: ClaimReply = serde_json::from_str(r#"{"holder":"riff://ann@heron/acme/app?session=a1"}"#).unwrap();
1699/// assert_eq!(old.warning, None);
1700/// ```
1701#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1702pub struct ClaimReply {
1703    /// The URI of `me` now, with the item in its claims.
1704    pub holder: SessionUri,
1705    /// Why a worker would not get this claim, for example a hold of the
1706    /// item (01M43GSGPJ69TPWPA4935WR8RW). The server grants it, because
1707    /// the caller is not a worker.
1708    #[serde(default, skip_serializing_if = "Option::is_none")]
1709    pub warning: Option<String>,
1710}
1711
1712/// `POST /v1/plan/hold`: a lead holds an item of its repository thread
1713/// with a reason, so that no worker can claim it
1714/// (01M43GSGB9ZFHSG0Q83Y50FEGW). A hold of a held item replaces its
1715/// reason. Only a lead of the thread, the owner or an admin can
1716/// (01M43GSGGY0QMB5D5EH92M6ZFP).
1717///
1718/// ```
1719/// use riff_core::wire::{Call, Hold};
1720///
1721/// let hold = Hold {
1722///     me: "riff://mike@pangolin/como-technologies/riff?session=a1".parse()?,
1723///     thread: "como-technologies/riff".parse()?,
1724///     item: "issue-366".into(),
1725///     reason: "waits for the word of Mike".into(),
1726/// };
1727/// assert_eq!(Hold::PATH, "/v1/plan/hold");
1728/// assert!(serde_json::to_string(&hold).unwrap().contains(r#""reason":"waits for the word of Mike""#));
1729/// # Ok::<(), riff_core::name::NameError>(())
1730/// ```
1731#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1732pub struct Hold {
1733    pub me: SessionUri,
1734    /// The repository thread of the item.
1735    pub thread: ThreadName,
1736    pub item: String,
1737    /// Why the item is held: 1 to 200 characters.
1738    pub reason: String,
1739}
1740
1741/// The reply to a hold.
1742#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1743pub struct HoldReply {
1744    /// True when the call made the hold or changed its reason.
1745    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1746    pub changed: bool,
1747    /// The session that holds a claim of the item now. A hold does not
1748    /// end a claim: it stops only the next claim of a worker.
1749    #[serde(default, skip_serializing_if = "Option::is_none")]
1750    pub holder: Option<SessionUri>,
1751}
1752
1753/// `POST /v1/plan/free`: a lead ends the hold of an item
1754/// (01M43GSGB9ZFHSG0Q83Y50FEGW). A free of an item with no hold changes
1755/// nothing.
1756///
1757/// ```
1758/// use riff_core::wire::{Call, Free, FreeReply};
1759///
1760/// assert_eq!(Free::PATH, "/v1/plan/free");
1761/// assert_eq!(serde_json::to_string(&FreeReply::default()).unwrap(), "{}");
1762/// ```
1763#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1764pub struct Free {
1765    pub me: SessionUri,
1766    /// The repository thread of the item.
1767    pub thread: ThreadName,
1768    pub item: String,
1769}
1770
1771/// The reply to a free.
1772#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1773pub struct FreeReply {
1774    /// True when the item was held, and the call ended the hold.
1775    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1776    pub freed: bool,
1777}
1778
1779/// The hold of one item: its reason, and who held it and when: the `by`
1780/// and the time of its `item_held` record (01M43GSGB9ZFHSG0Q83Y50FEGW).
1781#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1782pub struct HoldInfo {
1783    pub reason: String,
1784    /// The caller of the hold. `None` when the record has no cause.
1785    #[serde(default, skip_serializing_if = "Option::is_none")]
1786    pub by: Option<By>,
1787    /// The time of the record, in milliseconds since the Unix epoch.
1788    pub at_ms: u64,
1789}
1790
1791/// `POST /v1/plan`: a session sends the full plan of its repository
1792/// thread (01M4A4YTNSJR0R1T9JNXPBSKHC). `base` is the position of the
1793/// `plan_set` record of the plan that the client compared with, or
1794/// `None` when the server had no plan. A `base` that is not the
1795/// position on the server gets the code `stale_base`
1796/// (01M4A4YTR2NKVBPE6BT9EC3X75).
1797///
1798/// ```
1799/// use riff_core::record::{Plan, PlanItem, PlanSet};
1800/// use riff_core::wire::{Call, SetPlan};
1801///
1802/// let plan = SetPlan {
1803///     me: "riff://mike@pangolin/como-technologies/riff?session=a1".parse()?,
1804///     base: Some(3001),
1805///     plan: PlanSet {
1806///         thread: "como-technologies/riff".parse()?,
1807///         plan: Plan {
1808///             wave: None,
1809///             items: vec![PlanItem { item: "issue-366".into(), needs: vec![] }],
1810///             done: vec![],
1811///         },
1812///     },
1813/// };
1814/// assert_eq!(SetPlan::PATH, "/v1/plan");
1815/// assert_eq!(
1816///     serde_json::to_string(&plan).unwrap(),
1817///     r#"{"me":"riff://mike@pangolin/como-technologies/riff?session=a1","base":3001,"plan":{"thread":"como-technologies/riff","items":[{"item":"issue-366","needs":[]}],"done":[]}}"#
1818/// );
1819/// # Ok::<(), riff_core::name::NameError>(())
1820/// ```
1821#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1822pub struct SetPlan {
1823    pub me: SessionUri,
1824    #[serde(default, skip_serializing_if = "Option::is_none")]
1825    pub base: Option<u64>,
1826    pub plan: PlanSet,
1827}
1828
1829/// `POST /v1/plan/off`: the server forgets the plan of the repository
1830/// thread. The holds stay (01M4A4YTNSJR0R1T9JNXPBSKHC).
1831#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1832pub struct PlanOff {
1833    pub me: SessionUri,
1834    pub thread: ThreadName,
1835}
1836
1837/// The reply to a `plan_off`.
1838#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1839pub struct PlanOffReply {
1840    /// True when the thread had a plan, and the call ended it.
1841    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1842    pub ended: bool,
1843}
1844
1845/// `POST /v1/plan/seen`: a look saw that the plan of the forge is the
1846/// plan of the server at `position` (01M4A4YTYVHFGK0CJVACJQ8DQ3). It is
1847/// a signal: it makes no record.
1848#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1849pub struct PlanSeen {
1850    pub me: SessionUri,
1851    pub thread: ThreadName,
1852    pub position: u64,
1853}
1854
1855/// `POST /v1/plan/show`: the plan of a repository thread, and its holds
1856/// (01M4A4Z1NKPDBXV2PRZCG86G6A).
1857#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1858pub struct PlanShow {
1859    pub me: SessionUri,
1860    pub thread: ThreadName,
1861}
1862
1863/// `POST /v1/notes/set`: the lead of a user writes the notes of its
1864/// repository thread (01M59258K3TYR09HC4AWHWZPEX). The text replaces
1865/// the old text, and the old text stays in the history. Only a session
1866/// of the user that is not a worker, and the person of the user, can
1867/// write them (01M59258NTXPMWV18TKRKWNTYN). The text has at most
1868/// [`NOTES_MAX`](crate::notes::NOTES_MAX) bytes, and it looks like no
1869/// token (01M59258RJ3CG498Z8AVQ5TV5Q).
1870///
1871/// ```
1872/// use riff_core::wire::{Call, SetNotes};
1873///
1874/// let set = SetNotes {
1875///     me: "riff://mike@pangolin/como-technologies/riff?session=a1".parse()?,
1876///     thread: "como-technologies/riff".parse()?,
1877///     text: "Prefer thelio for builds.".into(),
1878/// };
1879/// assert_eq!(SetNotes::PATH, "/v1/notes/set");
1880/// assert!(serde_json::to_string(&set).unwrap().contains(r#""text":"Prefer thelio for builds.""#));
1881/// # Ok::<(), riff_core::name::NameError>(())
1882/// ```
1883#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1884pub struct SetNotes {
1885    pub me: SessionUri,
1886    /// The repository thread of the notes.
1887    pub thread: ThreadName,
1888    pub text: String,
1889}
1890
1891/// The reply to [`SetNotes`].
1892#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1893pub struct SetNotesReply {
1894    /// True when the call made a new text. The same text makes no
1895    /// record.
1896    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1897    pub changed: bool,
1898}
1899
1900/// `POST /v1/notes`: the query for the notes of a lead
1901/// (01M59258NTXPMWV18TKRKWNTYN). The user of the lead can read them,
1902/// and the owner of the riff. Another member cannot.
1903///
1904/// ```
1905/// use riff_core::wire::{Call, NotesShow};
1906///
1907/// let show: NotesShow = serde_json::from_str(
1908///     r#"{"me":"riff://mike@pangolin/como-technologies/riff?session=a1","thread":"como-technologies/riff"}"#,
1909/// ).unwrap();
1910/// assert_eq!(show.user, None);
1911/// assert!(!show.history);
1912/// assert_eq!(NotesShow::PATH, "/v1/notes");
1913/// ```
1914#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
1915pub struct NotesShow {
1916    pub me: SessionUri,
1917    pub thread: ThreadName,
1918    /// The user of the lead. Leave it out to read the notes of the
1919    /// caller. Only the owner can name another user.
1920    #[serde(default, skip_serializing_if = "Option::is_none")]
1921    pub user: Option<String>,
1922    /// True asks also for the old texts.
1923    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1924    pub history: bool,
1925}
1926
1927/// One text of the notes, with who wrote it and when.
1928#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1929pub struct NotesVersion {
1930    pub text: String,
1931    /// The caller of the write. `None` when the record has no cause.
1932    #[serde(default, skip_serializing_if = "Option::is_none")]
1933    pub by: Option<By>,
1934    /// The time of the record, in milliseconds since the Unix epoch.
1935    pub at_ms: u64,
1936}
1937
1938/// The reply to [`NotesShow`]: the notes now, and with `history` the old
1939/// texts, the oldest first. A lead with no notes has an empty text.
1940///
1941/// ```
1942/// use riff_core::wire::NotesReply;
1943///
1944/// assert_eq!(serde_json::to_string(&NotesReply::default()).unwrap(), r#"{"user":"","text":""}"#);
1945/// ```
1946#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1947pub struct NotesReply {
1948    /// The user of the lead.
1949    pub user: String,
1950    pub text: String,
1951    /// Who wrote the text, and when. `None` when there are no notes.
1952    #[serde(default, skip_serializing_if = "Option::is_none")]
1953    pub by: Option<By>,
1954    #[serde(default, skip_serializing_if = "Option::is_none")]
1955    pub at_ms: Option<u64>,
1956    /// Each text before this one, the oldest first. Empty with no
1957    /// `history`.
1958    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1959    pub history: Vec<NotesVersion>,
1960}
1961
1962/// The plan of the server for one repository thread
1963/// (01M4A4Z1NKPDBXV2PRZCG86G6A): the reply of `plan`, `plan_seen` and
1964/// the query `plan`.
1965///
1966/// ```
1967/// use riff_core::wire::PlanReply;
1968///
1969/// // A thread with no plan and no hold.
1970/// assert_eq!(serde_json::to_string(&PlanReply::default()).unwrap(), "{}");
1971/// ```
1972#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1973pub struct PlanReply {
1974    /// The plan. `None` when the plan of the thread is off.
1975    #[serde(default, skip_serializing_if = "Option::is_none")]
1976    pub plan: Option<PlanShown>,
1977    /// Each hold of the thread, by its item: also of an item that is not
1978    /// in the plan.
1979    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1980    pub holds: BTreeMap<String, HoldInfo>,
1981}
1982
1983/// The plan of one repository thread on the server, with its age.
1984#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1985pub struct PlanShown {
1986    #[serde(flatten)]
1987    pub plan: Plan,
1988    /// The position of the `plan_set` record of the plan: the `base` of
1989    /// the next `plan`.
1990    pub position: u64,
1991    /// The time of the `plan_set` record, in milliseconds since the Unix
1992    /// epoch.
1993    pub set_ms: u64,
1994    /// The time of the last `plan` or `plan_seen`, in milliseconds since
1995    /// the Unix epoch. `None` when no look came since the start of the
1996    /// server.
1997    #[serde(default, skip_serializing_if = "Option::is_none")]
1998    pub seen_ms: Option<u64>,
1999    /// True when no look came for `PLAN_TTL` (01M4A4Z1QTHYXZDMCP9DZ39WVT).
2000    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2001    pub stale: bool,
2002    /// The session that holds a claim of each item of the plan with a
2003    /// claim.
2004    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
2005    pub holders: BTreeMap<String, SessionUri>,
2006}
2007
2008/// `POST /v1/release`: frees a claim. Only its holder can.
2009#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
2010pub struct Release {
2011    pub me: SessionUri,
2012    pub thread: ThreadName,
2013    pub item: String,
2014}
2015
2016/// The reply to a release.
2017///
2018/// ```
2019/// use riff_core::wire::ReleaseReply;
2020///
2021/// assert_eq!(serde_json::to_string(&ReleaseReply::default()).unwrap(), "{}");
2022/// let last: ReleaseReply = serde_json::from_str(r#"{"must_clear":true}"#).unwrap();
2023/// assert!(last.must_clear);
2024/// ```
2025#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2026pub struct ReleaseReply {
2027    /// True when the release was the last claim of a worker: the worker
2028    /// must clear its context before its next claim (01M3X9XB37TQCXWPNFZRMRGJB4).
2029    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2030    pub must_clear: bool,
2031}
2032
2033/// `POST /v1/release/for`: the lead of a user frees the claim of
2034/// another session of that user (01M3WG243BW7P6E1ME0DFNQF8C). The
2035/// server posts a note to the thread of the claim.
2036#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
2037pub struct ReleaseFor {
2038    pub me: SessionUri,
2039    pub thread: ThreadName,
2040    pub item: String,
2041    /// The session that holds the item: its session ID, or the start of
2042    /// it.
2043    pub session: String,
2044}
2045
2046/// `POST /v1/lead`: the person `me` makes the session `session` the
2047/// lead of the user in the repository of `me`. It replaces the old
2048/// lead. A session cannot send it.
2049#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
2050pub struct Lead {
2051    pub me: SessionUri,
2052    /// The session ID of the new lead, or its start: at least 4
2053    /// characters.
2054    pub session: String,
2055}
2056
2057#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
2058pub struct LeadReply {
2059    /// The URI of the new lead now, with `lead=true`.
2060    pub lead: SessionUri,
2061    /// The old lead, when another session was the lead.
2062    #[serde(default)]
2063    pub replaced: Option<SessionUri>,
2064}
2065
2066/// `POST /v1/start`: a new start of the session: a new agent process,
2067/// a resume or a `/clear` (01M3JEE1QQCFS5TMZW5N2DAD2D). Its claims are free
2068/// at once. It keeps its ID, its threads and its lead. The call says
2069/// why the session starts, and if it is a worker (01M3X9X9M079WGFPJZHNXH9VEP).
2070///
2071/// ```
2072/// use riff_core::wire::{Start, StartReason};
2073///
2074/// let start: Start = serde_json::from_str(
2075///     r#"{"me":"riff://mike@pangolin/o/r?session=a1","reason":"clear","worker":true}"#,
2076/// ).unwrap();
2077/// assert_eq!(start.reason, StartReason::Clear);
2078/// assert!(start.worker);
2079/// ```
2080#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
2081pub struct Start {
2082    pub me: SessionUri,
2083    /// `process`, `resume` or `clear`.
2084    pub reason: StartReason,
2085    /// True when the session is a worker: `riff workers run` started it.
2086    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2087    pub worker: bool,
2088}
2089
2090/// Why a `session_started` record is there (01M3X9X9M079WGFPJZHNXH9VEP). A `start` call
2091/// sends `process`, `resume` or `clear`.
2092///
2093/// | Reason | Meaning | A fresh context |
2094/// |---|---|---|
2095/// | `process` | A new agent process. | Yes |
2096/// | `resume` | The agent process resumed an old conversation. | No |
2097/// | `clear` | The agent cleared its context. | Yes |
2098/// | `join` | The session came with no new start: a `register`. | No |
2099/// | `other` | A reason that this build does not know. | No |
2100///
2101/// ```
2102/// use riff_core::wire::StartReason;
2103///
2104/// let read = |json: &str| serde_json::from_str::<StartReason>(json).unwrap();
2105/// assert_eq!(read(r#""process""#), StartReason::Process);
2106/// assert_eq!(read(r#""wake""#), StartReason::Other);
2107/// // A reason of a later build can have each form of JSON.
2108/// for later in [r#"{"wake":"timer"}"#, "7", "null", r#"["clear"]"#] {
2109///     assert_eq!(read(later), StartReason::Other, "{later}");
2110/// }
2111/// assert!(StartReason::Clear.is_fresh() && StartReason::Process.is_fresh());
2112/// assert!(!StartReason::Resume.is_fresh() && !StartReason::Join.is_fresh());
2113/// assert!(StartReason::Resume.is_start() && !StartReason::Join.is_start());
2114/// assert_eq!(StartReason::Clear.word(), "clear");
2115/// ```
2116#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, JsonSchema)]
2117#[serde(rename_all = "snake_case")]
2118pub enum StartReason {
2119    Process,
2120    Resume,
2121    Clear,
2122    Join,
2123    /// A reason that this build does not know.
2124    Other,
2125}
2126
2127/// The set of reasons can grow. A build reads each value that it does
2128/// not know as [`StartReason::Other`]: a text, and each other form of
2129/// JSON (01M3XM2C18TT8VSKGD77YPZG53).
2130impl<'de> Deserialize<'de> for StartReason {
2131    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
2132        let value = serde_json::Value::deserialize(deserializer)?;
2133        let known = [
2134            StartReason::Process,
2135            StartReason::Resume,
2136            StartReason::Clear,
2137            StartReason::Join,
2138        ];
2139        Ok(known
2140            .into_iter()
2141            .find(|reason| value.as_str() == Some(reason.word()))
2142            .unwrap_or(StartReason::Other))
2143    }
2144}
2145
2146impl StartReason {
2147    /// True for a start with a fresh context: it ends MustClear.
2148    pub fn is_fresh(self) -> bool {
2149        matches!(self, StartReason::Process | StartReason::Clear)
2150    }
2151
2152    /// True for a reason that a `start` call can have.
2153    pub fn is_start(self) -> bool {
2154        matches!(
2155            self,
2156            StartReason::Process | StartReason::Resume | StartReason::Clear
2157        )
2158    }
2159
2160    /// The word of the reason, as the JSON has it.
2161    pub fn word(self) -> &'static str {
2162        match self {
2163            StartReason::Process => "process",
2164            StartReason::Resume => "resume",
2165            StartReason::Clear => "clear",
2166            StartReason::Join => "join",
2167            StartReason::Other => "other",
2168        }
2169    }
2170}
2171
2172#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
2173pub struct Started {
2174    /// Each claim that the start freed.
2175    #[serde(default)]
2176    pub freed: Vec<Freed>,
2177}
2178
2179/// A claim that a new start freed.
2180#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2181pub struct Freed {
2182    pub thread: ThreadName,
2183    pub item: String,
2184}
2185
2186/// `POST /v1/riff`: reads the pauses of the riff, for the place of
2187/// `me`. It changes nothing (01M3WRD9BSBKS9TN66H29TGTBV).
2188#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
2189pub struct RiffQuery {
2190    pub me: SessionUri,
2191}
2192
2193/// `POST /v1/pause`: pauses the repository of `me`, or with `riff` the
2194/// whole riff (01M3XAHZBGSSJB3YX23K88W01K). See
2195/// 01M3XAHZDSQR263QZVB41CK0MX for who can.
2196///
2197/// ```
2198/// use riff_core::wire::Pause;
2199///
2200/// let me = "riff://mike@pangolin/como-technologies/riff".parse()?;
2201/// let pause = Pause::here(me);
2202/// assert_eq!(
2203///     serde_json::to_string(&pause).unwrap(),
2204///     r#"{"me":"riff://mike@pangolin/como-technologies/riff"}"#
2205/// );
2206/// let json = serde_json::to_string(&Pause::whole("riff://mike@pangolin".parse()?)).unwrap();
2207/// assert_eq!(json, r#"{"me":"riff://mike@pangolin","riff":true}"#);
2208/// let whole: Pause = serde_json::from_str(r#"{"me":"riff://mike@pangolin","riff":true}"#).unwrap();
2209/// assert!(whole.riff);
2210/// # Ok::<(), riff_core::name::NameError>(())
2211/// ```
2212#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
2213pub struct Pause {
2214    pub me: SessionUri,
2215    /// True pauses the whole riff, not the repository of `me`.
2216    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2217    pub riff: bool,
2218    /// Pauses this repository, not the repository of `me`.
2219    #[serde(default, skip_serializing_if = "Option::is_none")]
2220    pub repository: Option<ThreadName>,
2221}
2222
2223impl Pause {
2224    /// The pause of the repository of `me`.
2225    pub fn here(me: SessionUri) -> Pause {
2226        Pause {
2227            me,
2228            riff: false,
2229            repository: None,
2230        }
2231    }
2232
2233    /// The pause of the whole riff.
2234    pub fn whole(me: SessionUri) -> Pause {
2235        Pause {
2236            riff: true,
2237            ..Pause::here(me)
2238        }
2239    }
2240}
2241
2242/// `POST /v1/resume`: resumes the repository of `me`, or with `riff`
2243/// the whole riff (01M3XAHZBGSSJB3YX23K88W01K). See
2244/// 01M3XAHZDSQR263QZVB41CK0MX for who can.
2245#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
2246pub struct Resume {
2247    pub me: SessionUri,
2248    /// True resumes the whole riff, not the repository of `me`.
2249    #[serde(default, skip_serializing_if = "std::ops::Not::not")]
2250    pub riff: bool,
2251    /// Resumes this repository, not the repository of `me`.
2252    #[serde(default, skip_serializing_if = "Option::is_none")]
2253    pub repository: Option<ThreadName>,
2254}
2255
2256impl Resume {
2257    /// The resume of the repository of `me`.
2258    pub fn here(me: SessionUri) -> Resume {
2259        Resume {
2260            me,
2261            riff: false,
2262            repository: None,
2263        }
2264    }
2265
2266    /// The resume of the whole riff.
2267    pub fn whole(me: SessionUri) -> Resume {
2268        Resume {
2269            riff: true,
2270            ..Resume::here(me)
2271        }
2272    }
2273}
2274
2275/// The pauses of the riff, as the caller sees them
2276/// (01M3XAHZJAF6YVDJ7WX74X8RBX): the reply to `/v1/riff`, `/v1/pause`
2277/// and `/v1/resume`.
2278///
2279/// ```
2280/// use riff_core::wire::{RiffReply, RiffState};
2281///
2282/// // The reply of a server from before the pause of a repository.
2283/// let old: RiffReply = serde_json::from_str(r#"{"state":"paused"}"#).unwrap();
2284/// assert_eq!(old.state, RiffState::Paused);
2285/// assert!(old.riff.is_none() && old.repositories.is_empty());
2286/// ```
2287#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2288pub struct RiffReply {
2289    /// The state for the place of the caller: paused when the whole
2290    /// riff is paused, or when the repository of the caller is paused.
2291    pub state: RiffState,
2292    /// True when the call changed a pause.
2293    #[serde(default)]
2294    pub changed: bool,
2295    /// The pause of the whole riff. `None` when the riff runs.
2296    #[serde(default, skip_serializing_if = "Option::is_none")]
2297    pub riff: Option<PauseInfo>,
2298    /// Each repository that is paused, in the order of the names.
2299    #[serde(default, skip_serializing_if = "Vec::is_empty")]
2300    pub repositories: Vec<RepositoryPause>,
2301}
2302
2303/// The pauses of a riff with no pause of a repository: `paused` is a
2304/// pause of the whole riff that no caller set.
2305///
2306/// ```
2307/// use riff_core::wire::{RiffReply, RiffState};
2308///
2309/// let paused = RiffReply::from(RiffState::Paused);
2310/// assert!(paused.riff.is_some());
2311/// assert!(RiffReply::from(RiffState::Running).riff.is_none());
2312/// ```
2313impl From<RiffState> for RiffReply {
2314    fn from(state: RiffState) -> RiffReply {
2315        RiffReply {
2316            state,
2317            changed: false,
2318            riff: (state == RiffState::Paused).then(PauseInfo::default),
2319            repositories: Vec::new(),
2320        }
2321    }
2322}
2323
2324impl RiffReply {
2325    /// The pause of the repository `thread`, when it is paused.
2326    pub fn repository(&self, thread: &ThreadName) -> Option<&PauseInfo> {
2327        self.repositories
2328            .iter()
2329            .find(|r| &r.repository == thread)
2330            .map(|r| &r.pause)
2331    }
2332}
2333
2334/// A pause that holds: who set it, and when
2335/// (01M3XAHZBGSSJB3YX23K88W01K).
2336///
2337/// ```
2338/// use riff_core::record::By;
2339/// use riff_core::wire::PauseInfo;
2340///
2341/// let pause = PauseInfo { by: Some(By::Person("mike".into())), at_ms: 7 };
2342/// let json = r#"{"by":{"person":"mike"},"at_ms":7}"#;
2343/// assert_eq!(serde_json::to_string(&pause).unwrap(), json);
2344/// assert_eq!(serde_json::from_str::<PauseInfo>(json).unwrap(), pause);
2345/// // The pause of a new riff: no caller set it.
2346/// assert_eq!(serde_json::to_string(&PauseInfo::default()).unwrap(), r#"{"at_ms":0}"#);
2347/// ```
2348#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2349pub struct PauseInfo {
2350    /// The caller that set the pause. `None` when it is not known: the
2351    /// pause of a new riff, or of a record with no cause.
2352    #[serde(default, skip_serializing_if = "Option::is_none")]
2353    pub by: Option<By>,
2354    /// The time of the pause, in milliseconds since the Unix epoch.
2355    #[serde(default)]
2356    pub at_ms: u64,
2357}
2358
2359/// A repository that is paused, with its pause.
2360#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2361pub struct RepositoryPause {
2362    pub repository: ThreadName,
2363    #[serde(flatten)]
2364    pub pause: PauseInfo,
2365}
2366
2367/// `POST /v1/idle`: reads the settings of idle workers
2368/// (01M3Q5A0TF9K49V8Z1ZY9NDF74). It changes nothing
2369/// (01M3WRD9BSBKS9TN66H29TGTBV).
2370#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
2371pub struct IdleQuery {
2372    pub me: SessionUri,
2373}
2374
2375/// `POST /v1/idle/set`: sets each given setting of idle workers, and
2376/// gives the settings. Only the owner or an admin can; in a riff with
2377/// no sign-in, each caller can (01M3Q5A0TF9K49V8Z1ZY9NDF74).
2378#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
2379pub struct SetIdle {
2380    pub me: SessionUri,
2381    #[serde(default, skip_serializing_if = "Option::is_none")]
2382    pub per_host: Option<u16>,
2383    #[serde(default, skip_serializing_if = "Option::is_none")]
2384    pub after_secs: Option<u64>,
2385}
2386
2387/// The settings of idle workers (01M3Q5A0TF9K49V8Z1ZY9NDF74). The server
2388/// stops no worker for them: the log and the checkpoint keep them so
2389/// that an old record still reads.
2390///
2391/// ```
2392/// use riff_core::wire::Idle;
2393///
2394/// let idle = Idle::default();
2395/// assert_eq!((idle.per_host, idle.after_secs), (1, 60));
2396/// ```
2397#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2398pub struct Idle {
2399    pub per_host: u16,
2400    pub after_secs: u64,
2401}
2402
2403impl Default for Idle {
2404    fn default() -> Self {
2405        Idle {
2406            per_host: 1,
2407            after_secs: 60,
2408        }
2409    }
2410}
2411
2412/// The state of a riff. A new riff is paused.
2413///
2414/// ```
2415/// use riff_core::wire::RiffState;
2416///
2417/// assert_eq!(RiffState::default(), RiffState::Paused);
2418/// assert_eq!(serde_json::to_string(&RiffState::Running).unwrap(), r#""running""#);
2419/// assert_eq!(RiffState::Running.to_string(), "running");
2420/// ```
2421#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2422#[serde(rename_all = "lowercase")]
2423pub enum RiffState {
2424    /// The sessions stop at their next step and wait. Nobody claims.
2425    #[default]
2426    Paused,
2427    /// The sessions pick and do work.
2428    Running,
2429}
2430
2431impl std::fmt::Display for RiffState {
2432    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2433        f.write_str(match self {
2434            RiffState::Paused => "paused",
2435            RiffState::Running => "running",
2436        })
2437    }
2438}
2439
2440/// An event on `GET /v1/watch?uri=…`: a message addressed to the
2441/// session.
2442#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2443pub struct Wake {
2444    pub thread: ThreadName,
2445    pub seq: u64,
2446    pub from: SessionUri,
2447    #[serde(default, skip_serializing_if = "Kind::is_message")]
2448    pub kind: Kind,
2449}
2450
2451/// An event on `GET /v1/tail?thread=…`.
2452#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2453pub struct Tailed {
2454    pub thread: ThreadName,
2455    pub message: Message,
2456    /// The keys of the user of the sender, to verify the message.
2457    #[serde(default, skip_serializing_if = "Keys::is_empty")]
2458    pub keys: Keys,
2459    /// True from a riff with no sign-in (R211). See
2460    /// [`ReadReply::trusted`].
2461    #[serde(default)]
2462    pub trusted: bool,
2463}
2464
2465/// The grant type that swaps an ID token of the sign-in provider for
2466/// a first pair of riff tokens (RFC 8693 token exchange).
2467pub const TOKEN_EXCHANGE: &str = "urn:ietf:params:oauth:grant-type:token-exchange";
2468
2469/// The subject token type of a [`TOKEN_EXCHANGE`] request.
2470pub const ID_TOKEN_TYPE: &str = "urn:ietf:params:oauth:token-type:id_token";
2471
2472/// The subject token type of a [`TOKEN_EXCHANGE`] request that swaps a
2473/// person access token for a session access token (R19).
2474pub const ACCESS_TOKEN_TYPE: &str = "urn:ietf:params:oauth:token-type:access_token";
2475
2476/// `POST /v1/token`, as `application/x-www-form-urlencoded`.
2477///
2478/// | `grant_type` | Fields |
2479/// |---|---|
2480/// | `refresh_token` | `refresh_token` |
2481/// | [`TOKEN_EXCHANGE`] | `subject_token` (an ID token), `subject_token_type` = [`ID_TOKEN_TYPE`] |
2482/// | [`TOKEN_EXCHANGE`] | `subject_token` (a person access token), `subject_token_type` = [`ACCESS_TOKEN_TYPE`], `session` |
2483#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
2484pub struct TokenRequest {
2485    pub grant_type: String,
2486    #[serde(default, skip_serializing_if = "Option::is_none")]
2487    pub refresh_token: Option<String>,
2488    #[serde(default, skip_serializing_if = "Option::is_none")]
2489    pub subject_token: Option<String>,
2490    #[serde(default, skip_serializing_if = "Option::is_none")]
2491    pub subject_token_type: Option<String>,
2492    /// The session ID of a session token.
2493    #[serde(default, skip_serializing_if = "Option::is_none")]
2494    pub session: Option<String>,
2495    /// The server that the token is for (RFC 8707). When it is set, it
2496    /// must be the public URL of the server.
2497    #[serde(default, skip_serializing_if = "Option::is_none")]
2498    pub resource: Option<String>,
2499}
2500
2501/// A new pair of riff tokens. Send `access_token` with the `DPoP`
2502/// scheme and a proof from the device key. Use `refresh_token` once, to
2503/// get the next pair.
2504///
2505/// The reply to a swap for a session token has an empty
2506/// `refresh_token`: a session token has none
2507/// (01M52JYGWD3ZJKNYE3HG5BJYTP).
2508///
2509/// ```
2510/// use riff_core::wire::TokenReply;
2511///
2512/// let json = r#"{"access_token":"a","token_type":"DPoP","expires_in":600,
2513///     "refresh_token":"","user":"mike"}"#;
2514/// let session: TokenReply = serde_json::from_str(json).unwrap();
2515/// assert!(session.refresh_token.is_empty());
2516/// ```
2517#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2518pub struct TokenReply {
2519    pub access_token: String,
2520    /// Always `DPoP`.
2521    pub token_type: String,
2522    /// Seconds until the access token expires.
2523    pub expires_in: u64,
2524    /// Empty for a session token.
2525    pub refresh_token: String,
2526    /// The user part of the session URI, from the sign-in (R36).
2527    pub user: String,
2528}
2529
2530/// `GET /v1/sign-in`: the OpenID Connect provider that `riff login`
2531/// signs in with.
2532#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2533pub struct SignInConfig {
2534    /// The issuer. Its discovery document is at
2535    /// `<issuer>/.well-known/openid-configuration`.
2536    pub issuer: String,
2537    pub client_id: String,
2538    /// Google asks for the secret of a desktop client too. It is not a
2539    /// secret: it ships to each person who signs in.
2540    #[serde(default, skip_serializing_if = "Option::is_none")]
2541    pub client_secret: Option<String>,
2542    /// The ID of this riff (01M3JNVBPMZ1K9WX7Q7DP6Y0DH). `riff` keeps it
2543    /// with its sign-in. Another ID means that the riff is new.
2544    pub riff_id: String,
2545}
2546
2547/// The fields that riff uses from the discovery document of an OpenID
2548/// Connect provider.
2549#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2550pub struct Discovery {
2551    pub issuer: String,
2552    pub authorization_endpoint: String,
2553    pub token_endpoint: String,
2554    pub jwks_uri: String,
2555}
2556
2557impl Discovery {
2558    /// The URL of the discovery document of `issuer`.
2559    ///
2560    /// ```
2561    /// use riff_core::wire::Discovery;
2562    ///
2563    /// assert_eq!(
2564    ///     Discovery::url("https://accounts.google.com/"),
2565    ///     "https://accounts.google.com/.well-known/openid-configuration"
2566    /// );
2567    /// ```
2568    pub fn url(issuer: &str) -> String {
2569        format!(
2570            "{}/.well-known/openid-configuration",
2571            issuer.trim_end_matches('/')
2572        )
2573    }
2574}
2575
2576/// `POST /v1/revoke`: ends each sign-in of a person.
2577#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2578pub struct Revoke {
2579    /// The person. Leave it out for the caller. Only an admin names
2580    /// another person.
2581    #[serde(default, skip_serializing_if = "Option::is_none")]
2582    pub user: Option<String>,
2583}
2584
2585/// The reply to [`Revoke`].
2586#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2587pub struct Revoked {
2588    pub user: String,
2589    /// The number of sign-ins that ended.
2590    pub sign_ins: usize,
2591}
2592
2593/// `POST /v1/invite`: adds a member, by verified email.
2594#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2595pub struct Invite {
2596    pub email: String,
2597}
2598
2599/// The reply to [`Invite`].
2600#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2601pub struct Invited {
2602    /// The email of the member, in lower case.
2603    pub email: String,
2604    /// The public address of the riff: the `--public-url` of the
2605    /// server. The member puts it in `RIFF_SERVER`.
2606    pub address: String,
2607}
2608
2609/// `POST /v1/remove`: removes a member and ends each sign-in of that
2610/// person.
2611#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2612pub struct Remove {
2613    pub email: String,
2614}
2615
2616/// The reply to [`Remove`].
2617#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2618pub struct Removed {
2619    /// The email, in lower case.
2620    pub email: String,
2621    /// The number of sign-ins that ended.
2622    pub sign_ins: usize,
2623}
2624
2625/// `POST /v1/admin`: the owner makes a person an admin, or an admin a
2626/// member again.
2627///
2628/// ```
2629/// use riff_core::wire::SetAdmin;
2630///
2631/// let add = SetAdmin { email: "bob@gmail.com".into(), admin: true };
2632/// assert_eq!(
2633///     serde_json::to_string(&add).unwrap(),
2634///     r#"{"email":"bob@gmail.com","admin":true}"#
2635/// );
2636/// ```
2637#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2638pub struct SetAdmin {
2639    pub email: String,
2640    /// True makes the person an admin. False makes an admin a member
2641    /// again.
2642    pub admin: bool,
2643}
2644
2645/// The reply to [`SetAdmin`]: the change of its record, with the email
2646/// in lower case.
2647pub use crate::record::AdminSet;
2648
2649/// `POST /v1/owner`: the owner passes the owner role to a member or an
2650/// admin. The old owner stays an admin.
2651///
2652/// ```
2653/// use riff_core::wire::PassOwner;
2654///
2655/// let pass = PassOwner { email: "bob@gmail.com".into() };
2656/// assert_eq!(serde_json::to_string(&pass).unwrap(), r#"{"email":"bob@gmail.com"}"#);
2657/// ```
2658#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2659pub struct PassOwner {
2660    pub email: String,
2661}
2662
2663/// The reply to [`PassOwner`].
2664#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2665pub struct OwnerPassed {
2666    /// The email of the new owner, in lower case.
2667    pub owner: String,
2668    /// The email of the old owner, now an admin.
2669    pub admin: String,
2670}
2671
2672/// `POST /v1/owner/take`: an admin asks for the owner role
2673/// (01M3N7K3ZAZFGABN7032AYJWEM).
2674#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2675pub struct TakeOwner {}
2676
2677/// The reply to [`TakeOwner`].
2678///
2679/// ```
2680/// use riff_core::wire::OwnerAsked;
2681///
2682/// let asked: OwnerAsked = serde_json::from_str(
2683///     r#"{"admin":"bob@gmail.com","owner":"ada@gmail.com","answer_secs":600}"#,
2684/// ).unwrap();
2685/// assert_eq!(asked.owner.as_deref(), Some("ada@gmail.com"));
2686/// assert!(!asked.already());
2687///
2688/// // The owner asked: nothing changed (01M3WRJAFS6W3J2ZRJ6XSW3SB5).
2689/// let same: OwnerAsked = serde_json::from_str(
2690///     r#"{"admin":"ada@gmail.com","owner":"ada@gmail.com","answer_secs":0}"#,
2691/// ).unwrap();
2692/// assert!(same.already());
2693/// ```
2694#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2695pub struct OwnerAsked {
2696    /// The email of the admin that asked, in lower case.
2697    pub admin: String,
2698    /// The email of the owner that answers. `None` when the riff had no
2699    /// owner: the admin is the owner now. The email of `admin` when the
2700    /// admin that asked is the owner already: nothing changed.
2701    pub owner: Option<String>,
2702    /// The time that the owner has to answer, in seconds. With no
2703    /// answer, the admin is the owner.
2704    pub answer_secs: u64,
2705}
2706
2707impl OwnerAsked {
2708    /// True when the admin that asked is the owner already
2709    /// (01M3WRJAFS6W3J2ZRJ6XSW3SB5).
2710    pub fn already(&self) -> bool {
2711        self.owner.as_deref() == Some(self.admin.as_str())
2712    }
2713}
2714
2715/// `POST /v1/owner/deny`: the owner keeps the owner role that an admin
2716/// asks for (01M3N7K41N03P26BEFFNX5617K).
2717#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2718pub struct DenyOwner {}
2719
2720/// The reply to [`DenyOwner`].
2721#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2722pub struct OwnerDenied {
2723    /// The email of the owner, who stays the owner.
2724    pub owner: String,
2725    /// The email of the admin that asked.
2726    pub admin: String,
2727}
2728
2729/// `POST /v1/log`: the records of the log of one repository, for an
2730/// audit (01M3ZWRC11R5M9V1KTF05P240W). Only the owner and the admins
2731/// can read them.
2732///
2733/// ```
2734/// use riff_core::wire::{Call, LogQuery};
2735///
2736/// let query = LogQuery { repo: "acme/app".parse()? };
2737/// assert_eq!(serde_json::to_string(&query).unwrap(), r#"{"repo":"acme/app"}"#);
2738/// assert_eq!(LogQuery::PATH, "/v1/log");
2739/// # Ok::<(), riff_core::name::NameError>(())
2740/// ```
2741#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2742pub struct LogQuery {
2743    /// The repository thread, for example `acme/app`.
2744    pub repo: ThreadName,
2745}
2746
2747/// The reply to [`LogQuery`]: each record of the repository
2748/// ([`Record::of_repository`]), in log order. A post has only its mark
2749/// ([`Record::for_audit`], 01M3ZWRC3XBFN8FJDGE8XWZ5EA).
2750///
2751/// A reader skips a record of a kind that its build does not know, as
2752/// [`Line::parse`] does (01M43GSMZKCMET3DG07K538EDD). So `riff audit`
2753/// of one build reads the log of a later server.
2754///
2755/// ```
2756/// use riff_core::wire::LogReply;
2757///
2758/// let reply: LogReply = serde_json::from_str(r#"{"records":[
2759///     {"position":1,"written_at_ms":1,"change":{"reacted":{"emoji":"+1"}}},
2760///     {"position":2,"written_at_ms":1,"change":{"member_invited":{"email":"ann@acme.io"}}}
2761/// ]}"#).unwrap();
2762/// assert_eq!(reply.records.len(), 1);
2763/// assert_eq!(reply.records[0].envelope.position, 2);
2764/// ```
2765#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
2766pub struct LogReply {
2767    #[serde(deserialize_with = "known_records")]
2768    pub records: Vec<Record>,
2769    /// The records that the log no longer keeps: `None` when it keeps
2770    /// each record from position 1 (01M588N3R2VP12A8X4EASX0BJ4).
2771    #[serde(default, skip_serializing_if = "Option::is_none")]
2772    pub pruned: Option<Pruned>,
2773}
2774
2775/// The start of the log after the checkpoint rule removed its oldest
2776/// chunks: the reply of `POST /v1/log` has the records from `first`
2777/// on (01M588N3R2VP12A8X4EASX0BJ4).
2778///
2779/// ```
2780/// use riff_core::wire::{LogReply, Pruned};
2781///
2782/// let reply: LogReply = serde_json::from_str(
2783///     r#"{"records":[],"pruned":{"first":1918,"first_ms":5000}}"#,
2784/// ).unwrap();
2785/// assert_eq!(reply.pruned, Some(Pruned { first: 1918, first_ms: 5000 }));
2786/// ```
2787#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2788pub struct Pruned {
2789    /// The position of the oldest record that the log keeps.
2790    pub first: u64,
2791    /// When the oldest record of a known kind that the log keeps was
2792    /// written, in milliseconds since the Unix epoch. A record before it
2793    /// can be gone.
2794    pub first_ms: u64,
2795}
2796
2797/// The records of a [`LogReply`] with a kind that this build knows.
2798/// A record that does not read is an error.
2799fn known_records<'de, D>(deserializer: D) -> Result<Vec<Record>, D::Error>
2800where
2801    D: serde::Deserializer<'de>,
2802{
2803    let values = Vec::<serde_json::Value>::deserialize(deserializer)?;
2804    let mut records = Vec::with_capacity(values.len());
2805    for value in values {
2806        match Line::parse(&value.to_string()).map_err(serde::de::Error::custom)? {
2807            Line::Record(record) => records.push(*record),
2808            Line::Unknown { .. } => {}
2809        }
2810    }
2811    Ok(records)
2812}
2813
2814/// `POST /v1/members`: shows who may join the riff.
2815#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2816pub struct Members {}
2817
2818/// The reply to [`Members`].
2819///
2820/// ```
2821/// use riff_core::wire::MembersReply;
2822///
2823/// let reply: MembersReply = serde_json::from_str(
2824///     r#"{"owner":"ada@gmail.com","admins":[],"members":["bob@gmail.com"],"allowed_domains":["x.io"]}"#,
2825/// ).unwrap();
2826/// assert_eq!(reply.owner.as_deref(), Some("ada@gmail.com"));
2827/// ```
2828#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2829pub struct MembersReply {
2830    /// The email of the owner. `None` when the riff has no owner: before
2831    /// the first sign-in, or after the owner was gone
2832    /// (01M3Q63NNC6SC03BFCG80M7B4D).
2833    pub owner: Option<String>,
2834    /// The email of each admin, sorted: the admins that the owner made
2835    /// and the admins of the settings (R210). The owner is not in it.
2836    pub admins: Vec<String>,
2837    /// The email of each member that is not the owner and not an admin,
2838    /// sorted (01M3MN157X8N9QKER1AJEPEJVX).
2839    pub members: Vec<String>,
2840    /// The allowed domains (R15).
2841    pub allowed_domains: Vec<String>,
2842}
2843
2844/// An OAuth error reply, for example `{"error":"invalid_grant"}`.
2845#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2846pub struct TokenError {
2847    pub error: String,
2848    /// What a person must read, for a refusal that only they can fix,
2849    /// for example a USER that another account holds (R209).
2850    #[serde(default, skip_serializing_if = "Option::is_none")]
2851    pub error_description: Option<String>,
2852}
2853
2854/// The protected resource metadata of `riff-server` (RFC 9728).
2855#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2856pub struct ResourceMetadata {
2857    /// The public URL of the server. Tokens are only for it.
2858    pub resource: String,
2859    /// The issuer of each token. It is the server itself.
2860    pub authorization_servers: Vec<String>,
2861    /// Always `["header"]`: a token goes only in the Authorization header.
2862    pub bearer_methods_supported: Vec<String>,
2863    /// Always `["ES256"]` (RFC 9449).
2864    pub dpop_signing_alg_values_supported: Vec<String>,
2865    /// Always true: each token is bound to a device key (R18).
2866    pub dpop_bound_access_tokens_required: bool,
2867}
2868
2869/// The authorization server metadata of `riff-server` (RFC 8414).
2870#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
2871pub struct ServerMetadata {
2872    pub issuer: String,
2873    pub token_endpoint: String,
2874    pub grant_types_supported: Vec<String>,
2875    /// Empty: the server has no authorization endpoint (R83).
2876    pub response_types_supported: Vec<String>,
2877    pub code_challenge_methods_supported: Vec<String>,
2878    /// Always `["none"]`: `riff` is a public client.
2879    pub token_endpoint_auth_methods_supported: Vec<String>,
2880    /// Always `["ES256"]` (RFC 9449).
2881    pub dpop_signing_alg_values_supported: Vec<String>,
2882}
2883
2884#[cfg(test)]
2885mod tests {
2886    use super::*;
2887    use crate::name::Who;
2888
2889    const MIKE: &str = "riff://mike@pangolin/como-technologies/riff?session=a6cf";
2890    const BRETT: &str = "riff://brett@heron/como-technologies/riff?session=77e0";
2891
2892    fn uri(text: &str) -> SessionUri {
2893        text.parse().unwrap()
2894    }
2895
2896    /// The message that the server stores for a signed post.
2897    fn stored(post: &Post, from: SessionUri) -> Message {
2898        Message {
2899            seq: 1,
2900            from,
2901            to: post.to.clone(),
2902            body: post.body.clone(),
2903            at_ms: post.at_ms.unwrap(),
2904            kind: post.kind,
2905            sig: post.sig.clone(),
2906            payload: post.payload.clone(),
2907        }
2908    }
2909
2910    fn keys(user: &str, key: &Key) -> Keys {
2911        Keys::from([(user.to_owned(), vec![key.thumbprint()])])
2912    }
2913
2914    #[test]
2915    fn a_direct_message_verifies_only_in_its_own_thread() {
2916        let key = Key::generate();
2917        let (mike, brett) = (uri(MIKE), uri(BRETT));
2918        let to = vec![Selector::session("77e0")];
2919        let mut post = Post::new(&mike, None, to, "hi");
2920        post.sign(&key, 5);
2921        let message = stored(&post, mike.clone());
2922        let keys = keys("mike", &key);
2923
2924        let theirs = ThreadName::direct(mike.who(), brett.who());
2925        assert!(message.verified(&theirs, &keys));
2926
2927        // The same message in the direct thread of mike and another session.
2928        let other = Who::new("ada", Some("c3")).unwrap();
2929        assert!(!message.verified(&ThreadName::direct(mike.who(), &other), &keys));
2930        // In a direct thread without mike.
2931        assert!(!message.verified(&ThreadName::direct(brett.who(), &other), &keys));
2932        // In a named thread.
2933        assert!(!message.verified(&"design".parse().unwrap(), &keys));
2934    }
2935
2936    #[test]
2937    fn a_direct_message_to_the_lead_must_reach_a_session_of_that_user() {
2938        let key = Key::generate();
2939        let mike = uri(MIKE);
2940        let lead = Who::new("mike", Some("1ead")).unwrap();
2941        let to = vec![Selector::lead("mike", "como-technologies/riff")];
2942        let mut post = Post::new(&mike, None, to, "may I?");
2943        post.sign(&key, 5);
2944        let message = stored(&post, mike.clone());
2945        let keys = keys("mike", &key);
2946        assert!(message.verified(&ThreadName::direct(mike.who(), &lead), &keys));
2947        let brett = Who::new("brett", Some("77e0")).unwrap();
2948        assert!(!message.verified(&ThreadName::direct(mike.who(), &brett), &keys));
2949    }
2950
2951    #[test]
2952    fn a_false_sender_is_not_verified() {
2953        let key = Key::generate();
2954        let thread: ThreadName = "design".parse().unwrap();
2955        let mut post = Post::new(&uri(MIKE), Some(thread.clone()), vec![], "go");
2956        post.sign(&key, 5);
2957        let keys = keys("mike", &key);
2958        assert!(stored(&post, uri(MIKE)).verified(&thread, &keys));
2959
2960        // The stored sender names the lead: another session of mike.
2961        let lead = uri("riff://mike@pangolin/como-technologies/riff?session=1ead&lead=true");
2962        assert!(!stored(&post, lead).verified(&thread, &keys));
2963        // The key of mike does not count for brett.
2964        assert!(!stored(&post, uri(BRETT)).verified(&thread, &keys));
2965    }
2966
2967    #[test]
2968    fn the_place_and_the_claims_are_not_signed() {
2969        let key = Key::generate();
2970        let thread: ThreadName = "design".parse().unwrap();
2971        let mut post = Post::new(&uri(MIKE), Some(thread.clone()), vec![], "go");
2972        post.sign(&key, 5);
2973        let now = "riff://mike@pangolin/other/repo?session=a6cf&claim=issue-6#api";
2974        assert!(stored(&post, uri(now)).verified(&thread, &keys("mike", &key)));
2975    }
2976
2977    #[test]
2978    fn the_lead_mark_is_signed() {
2979        let key = Key::generate();
2980        let keys = keys("mike", &key);
2981        let thread: ThreadName = "design".parse().unwrap();
2982        let lead = uri(&format!("{MIKE}&lead=true"));
2983
2984        // A lead mark added to the sender after the post.
2985        let mut post = Post::new(&uri(MIKE), Some(thread.clone()), vec![], "merge now");
2986        post.sign(&key, 5);
2987        assert!(stored(&post, uri(MIKE)).verified(&thread, &keys));
2988        assert!(!stored(&post, lead.clone()).verified(&thread, &keys));
2989
2990        // A post of the lead: its lead mark is signed.
2991        let mut post = Post::new(&lead, Some(thread.clone()), vec![], "merge now");
2992        post.sign(&key, 5);
2993        assert!(stored(&post, lead).verified(&thread, &keys));
2994        assert!(!stored(&post, uri(MIKE)).verified(&thread, &keys));
2995    }
2996
2997    #[test]
2998    fn a_message_without_a_signature_is_not_verified() {
2999        let key = Key::generate();
3000        let thread: ThreadName = "design".parse().unwrap();
3001        let message = Message {
3002            seq: 1,
3003            from: uri(MIKE),
3004            to: vec![],
3005            body: "go".into(),
3006            at_ms: 5,
3007            kind: Kind::Message,
3008            sig: None,
3009            payload: None,
3010        };
3011        assert!(!message.verified(&thread, &keys("mike", &key)));
3012    }
3013
3014    #[test]
3015    fn a_signed_post_carries_the_payload_that_it_signs() {
3016        let key = Key::generate();
3017        let thread: ThreadName = "design".parse().unwrap();
3018        let mut post = Post::new(&uri(MIKE), Some(thread.clone()), vec![], "go");
3019        post.sign(&key, 5);
3020        let payload = post.payload.clone().unwrap();
3021        assert_eq!(payload, post.content().unwrap().payload());
3022        let (jkt, signed) = crate::signed::check(&payload, post.sig.as_ref().unwrap()).unwrap();
3023        assert_eq!(jkt, key.thumbprint());
3024        assert!(signed.covers(&post.content().unwrap()));
3025    }
3026
3027    #[test]
3028    fn a_message_needs_its_kept_payload() {
3029        let key = Key::generate();
3030        let thread: ThreadName = "design".parse().unwrap();
3031        let keys = keys("mike", &key);
3032        let mut post = Post::new(&uri(MIKE), Some(thread.clone()), vec![], "go");
3033        post.sign(&key, 5);
3034
3035        let mut message = stored(&post, uri(MIKE));
3036        message.payload = None;
3037        assert!(!message.verified(&thread, &keys), "no payload");
3038
3039        // The payload of another message, with its own signature.
3040        let mut other = Post::new(&uri(MIKE), Some(thread.clone()), vec![], "stop");
3041        other.sign(&key, 5);
3042        let mut message = stored(&post, uri(MIKE));
3043        message.payload = other.payload.clone();
3044        message.sig = other.sig.clone();
3045        assert!(
3046            !message.verified(&thread, &keys),
3047            "the payload of another body"
3048        );
3049    }
3050
3051    #[test]
3052    fn a_new_field_in_the_payload_does_not_stop_the_check() {
3053        use base64::Engine;
3054        let key = Key::generate();
3055        let thread: ThreadName = "design".parse().unwrap();
3056        let mut post = Post::new(&uri(MIKE), Some(thread.clone()), vec![], "go");
3057        post.sign(&key, 5);
3058        // A later build adds `reply_to` to the payload, and signs it.
3059        let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD;
3060        let mut json: serde_json::Value =
3061            serde_json::from_slice(&b64.decode(post.payload.as_ref().unwrap()).unwrap()).unwrap();
3062        json["reply_to"] = 7.into();
3063        let payload = b64.encode(serde_json::to_vec(&json).unwrap());
3064        let mut message = stored(&post, uri(MIKE));
3065        message.sig = Some(crate::signed::sign_payload(&payload, &key));
3066        message.payload = Some(payload);
3067        assert!(message.verified(&thread, &keys("mike", &key)));
3068    }
3069}