//! Generated by `trust-tasks-codegen` — do not edit by hand.
//!
//! Spec slug: `rooms/keys/read`. Version: `0.1`.
#[allow(unused_imports)]
use serde::{Deserialize, Serialize};
/// Error types.
pub mod error {
/// Error from a `TryFrom` or `FromStr` implementation.
pub struct ConversionError(::std::borrow::Cow<'static, str>);
impl ::std::error::Error for ConversionError {}
impl ::std::fmt::Display for ConversionError {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
::std::fmt::Display::fmt(&self.0, f)
}
}
impl ::std::fmt::Debug for ConversionError {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> {
::std::fmt::Debug::fmt(&self.0, f)
}
}
impl From<&'static str> for ConversionError {
fn from(value: &'static str) -> Self {
Self(value.into())
}
}
impl From<String> for ConversionError {
fn from(value: String) -> Self {
Self(value.into())
}
}
}
/**
The root of the room's record tree — a host's commitment to *which records the room holds*, as distinct from what any one of them says.
A room's records are already signed and room-bound, so a host cannot forge, alter or relocate one. What it can do for free is stay silent: a listing that omits a record is indistinguishable from a room that never held it. This value is what makes that omission detectable, so it is only worth anything when the reader can compare it against a copy the host did not choose for them — one it gave another member, one it gave the same member earlier, or the witnessed anchor. A commitment read once, in isolation, proves nothing.
**A root on its own is not comparable, and an earlier revision of this description said it was.** It claimed a host showing two members two different roots had been caught, which is false while a room can move between two reads: the host answers *there was a write*, and nothing contradicts it. Comparison needs the state each root describes, which is what `HeadVersion` names — and `RecordCount` is the other half of the same omission, since Certificate Transparency's signed tree head is a root *and a size* and this family shipped only the root. A reader that receives a `dataCommitment` **without** them can use it against a witnessed anchor, where the epoch pins the state, and **MUST NOT** compare it against another root.
**The construction is normative**, because two hosts that compute different roots over the same room make every comparison meaningless:
1. Take every record the room holds — including tombstones, which are records — and order them by `key` using unsigned byte order.
2. Leaf: `SHA-256(0x00 || JCS(record))`, where the record is a `CommittedRecord` — that definition fixes the members exactly, and this step used to name a *projection* instead, which two implementations could read two ways — JCS is its RFC 8785 canonicalization, and `0x00` is RFC 6962's leaf-domain prefix.
3. Internal node: `SHA-256(0x01 || left || right)`.
4. A level with an odd number of nodes promotes the last one unchanged. It MUST NOT be duplicated: duplicating makes a tree of n leaves collide with one of n+1 whose last is repeated, so two different rooms commit to the same root.
5. A room holding no records commits to `SHA-256("")`, a distinguished value rather than zeroes — a root of zeroes is what an uninitialised buffer looks like, and an empty room is a real state a host must be able to commit to honestly.
The leaf covers the whole record rather than its body, and that is deliberate: a host that could flip `status` from active to retracted, move `pinned`, or rewrite `author` on an `attributed` room would rewrite what the room means without touching a byte of ciphertext. The **plaintext is never involved** — on the sealed tiers the host holds ciphertext and commits to exactly what it stores.
The commitment is over the **whole room**, never over the page being returned. A page-scoped root is one a host satisfies by construction and could never fail.
Proving that a *particular* record sits under this root is a separate question, answered by `RecordTrace` on a single-record read. A commitment catches a host that equivocates; a trace binds one record to what the host committed to. Neither is the other, and a reader wanting completeness needs both plus a root it did not get from the host it is checking.*/
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "DataCommitment",
/// "description": "\nThe root of the room's record tree — a host's commitment to *which records the room holds*, as distinct from what any one of them says.\n\nA room's records are already signed and room-bound, so a host cannot forge, alter or relocate one. What it can do for free is stay silent: a listing that omits a record is indistinguishable from a room that never held it. This value is what makes that omission detectable, so it is only worth anything when the reader can compare it against a copy the host did not choose for them — one it gave another member, one it gave the same member earlier, or the witnessed anchor. A commitment read once, in isolation, proves nothing.\n\n**A root on its own is not comparable, and an earlier revision of this description said it was.** It claimed a host showing two members two different roots had been caught, which is false while a room can move between two reads: the host answers *there was a write*, and nothing contradicts it. Comparison needs the state each root describes, which is what `HeadVersion` names — and `RecordCount` is the other half of the same omission, since Certificate Transparency's signed tree head is a root *and a size* and this family shipped only the root. A reader that receives a `dataCommitment` **without** them can use it against a witnessed anchor, where the epoch pins the state, and **MUST NOT** compare it against another root.\n\n**The construction is normative**, because two hosts that compute different roots over the same room make every comparison meaningless:\n 1. Take every record the room holds — including tombstones, which are records — and order them by `key` using unsigned byte order.\n 2. Leaf: `SHA-256(0x00 || JCS(record))`, where the record is a `CommittedRecord` — that definition fixes the members exactly, and this step used to name a *projection* instead, which two implementations could read two ways — JCS is its RFC 8785 canonicalization, and `0x00` is RFC 6962's leaf-domain prefix.\n 3. Internal node: `SHA-256(0x01 || left || right)`.\n 4. A level with an odd number of nodes promotes the last one unchanged. It MUST NOT be duplicated: duplicating makes a tree of n leaves collide with one of n+1 whose last is repeated, so two different rooms commit to the same root.\n 5. A room holding no records commits to `SHA-256(\"\")`, a distinguished value rather than zeroes — a root of zeroes is what an uninitialised buffer looks like, and an empty room is a real state a host must be able to commit to honestly.\n\nThe leaf covers the whole record rather than its body, and that is deliberate: a host that could flip `status` from active to retracted, move `pinned`, or rewrite `author` on an `attributed` room would rewrite what the room means without touching a byte of ciphertext. The **plaintext is never involved** — on the sealed tiers the host holds ciphertext and commits to exactly what it stores.\n\nThe commitment is over the **whole room**, never over the page being returned. A page-scoped root is one a host satisfies by construction and could never fail.\n\nProving that a *particular* record sits under this root is a separate question, answered by `RecordTrace` on a single-record read. A commitment catches a host that equivocates; a trace binds one record to what the host committed to. Neither is the other, and a reader wanting completeness needs both plus a root it did not get from the host it is checking.",
/// "$ref": "#/definitions/DigestMultibase"
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(transparent)]
pub struct DataCommitment(pub DigestMultibase);
impl ::std::ops::Deref for DataCommitment {
type Target = DigestMultibase;
fn deref(&self) -> &DigestMultibase {
&self.0
}
}
impl ::std::convert::From<DataCommitment> for DigestMultibase {
fn from(value: DataCommitment) -> Self {
value.0
}
}
impl ::std::convert::From<DigestMultibase> for DataCommitment {
fn from(value: DigestMultibase) -> Self {
Self(value)
}
}
impl ::std::str::FromStr for DataCommitment {
type Err = <DigestMultibase as ::std::str::FromStr>::Err;
fn from_str(value: &str) -> ::std::result::Result<Self, Self::Err> {
Ok(Self(value.parse()?))
}
}
impl ::std::convert::TryFrom<&str> for DataCommitment {
type Error = <DigestMultibase as ::std::str::FromStr>::Err;
fn try_from(value: &str) -> ::std::result::Result<Self, Self::Error> {
value.parse()
}
}
impl ::std::convert::TryFrom<String> for DataCommitment {
type Error = <DigestMultibase as ::std::str::FromStr>::Err;
fn try_from(value: String) -> ::std::result::Result<Self, Self::Error> {
value.parse()
}
}
impl ::std::fmt::Display for DataCommitment {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
self.0.fmt(f)
}
}
/**
A cryptographic digest as a multibase-encoded multihash — the encoding the W3C Verifiable Credentials Data Model 2.0 defines for `digestMultibase`, and the one `did:webvh` uses for its SCID and entry hashes.
Multihash carries the hash algorithm in-band, so the value is self-describing and the wire format survives an algorithm change without a schema revision; multibase does the same for the base encoding, so a verifier never infers base58 from base64url by context. A bare hex string or a `sha-256:`-style prefix hard-codes one algorithm into the wire contract and is non-conforming here.
This definition constrains the *encoding only*. What the digest is computed over is stated by each referencing field, because it differs legitimately: a digest over a JSON document is taken over its RFC 8785 (JCS) canonicalization, while a digest over an opaque artifact is taken over its bytes. A field whose input is a JSON document and which does not name a canonicalization is not reproducible.
Restricted to the two multibase headers W3C Controlled Identifiers 1.0 §2.4 normatively requires — `z` (base58btc) and `u` (base64url-no-pad). CID permits others but states that "interoperability is not guaranteed between implementations using such values", and a registry whose purpose is interoperability should not mint digests a conforming verifier may be unable to read. The alphabets are enforced rather than assumed: base58btc excludes 0, O, I and l, and an earlier permissive pattern let three published examples carry digests that were not valid base58 at all. base58btc is RECOMMENDED, for consistency with `did:key` and `did:webvh`.*/
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "DigestMultibase",
/// "description": "\nA cryptographic digest as a multibase-encoded multihash — the encoding the W3C Verifiable Credentials Data Model 2.0 defines for `digestMultibase`, and the one `did:webvh` uses for its SCID and entry hashes.\n\nMultihash carries the hash algorithm in-band, so the value is self-describing and the wire format survives an algorithm change without a schema revision; multibase does the same for the base encoding, so a verifier never infers base58 from base64url by context. A bare hex string or a `sha-256:`-style prefix hard-codes one algorithm into the wire contract and is non-conforming here.\n\nThis definition constrains the *encoding only*. What the digest is computed over is stated by each referencing field, because it differs legitimately: a digest over a JSON document is taken over its RFC 8785 (JCS) canonicalization, while a digest over an opaque artifact is taken over its bytes. A field whose input is a JSON document and which does not name a canonicalization is not reproducible.\n\nRestricted to the two multibase headers W3C Controlled Identifiers 1.0 §2.4 normatively requires — `z` (base58btc) and `u` (base64url-no-pad). CID permits others but states that \"interoperability is not guaranteed between implementations using such values\", and a registry whose purpose is interoperability should not mint digests a conforming verifier may be unable to read. The alphabets are enforced rather than assumed: base58btc excludes 0, O, I and l, and an earlier permissive pattern let three published examples carry digests that were not valid base58 at all. base58btc is RECOMMENDED, for consistency with `did:key` and `did:webvh`.",
/// "examples": [
/// "zQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR"
/// ],
/// "type": "string",
/// "minLength": 16,
/// "pattern": "^(z[1-9A-HJ-NP-Za-km-z]+|u[A-Za-z0-9_-]+)$"
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct DigestMultibase(::std::string::String);
impl ::std::ops::Deref for DigestMultibase {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<DigestMultibase> for ::std::string::String {
fn from(value: DigestMultibase) -> Self {
value.0
}
}
impl ::std::str::FromStr for DigestMultibase {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
if value.chars().count() < 16usize {
return Err("shorter than 16 characters".into());
}
static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
::std::sync::LazyLock::new(|| {
::regress::Regex::new("^(z[1-9A-HJ-NP-Za-km-z]+|u[A-Za-z0-9_-]+)$").unwrap()
});
if PATTERN.find(value).is_none() {
return Err(
"doesn't match pattern \"^(z[1-9A-HJ-NP-Za-km-z]+|u[A-Za-z0-9_-]+)$\"".into(),
);
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for DigestMultibase {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for DigestMultibase {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for DigestMultibase {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> ::serde::Deserialize<'de> for DigestMultibase {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
::std::string::String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as ::serde::de::Error>::custom(e.to_string())
})
}
}
///Vendor-namespaced extension object per SPEC.md §4.5.1. Each immediate key MUST be a reverse-DNS namespace; structure under each namespace is opaque to the framework.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "Ext",
/// "description": "Vendor-namespaced extension object per SPEC.md §4.5.1. Each immediate key MUST be a reverse-DNS namespace; structure under each namespace is opaque to the framework.",
/// "type": "object",
/// "minProperties": 1,
/// "additionalProperties": true,
/// "propertyNames": {
/// "pattern": "^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$"
/// }
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(transparent)]
pub struct Ext(pub ::std::collections::HashMap<ExtKey, ::serde_json::Value>);
impl ::std::ops::Deref for Ext {
type Target = ::std::collections::HashMap<ExtKey, ::serde_json::Value>;
fn deref(&self) -> &::std::collections::HashMap<ExtKey, ::serde_json::Value> {
&self.0
}
}
impl ::std::convert::From<Ext> for ::std::collections::HashMap<ExtKey, ::serde_json::Value> {
fn from(value: Ext) -> Self {
value.0
}
}
impl ::std::convert::From<::std::collections::HashMap<ExtKey, ::serde_json::Value>> for Ext {
fn from(value: ::std::collections::HashMap<ExtKey, ::serde_json::Value>) -> Self {
Self(value)
}
}
///`ExtKey`
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "type": "string",
/// "pattern": "^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$"
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct ExtKey(::std::string::String);
impl ::std::ops::Deref for ExtKey {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<ExtKey> for ::std::string::String {
fn from(value: ExtKey) -> Self {
value.0
}
}
impl ::std::str::FromStr for ExtKey {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
static PATTERN: ::std::sync::LazyLock<::regress::Regex> =
::std::sync::LazyLock::new(|| {
::regress::Regex::new("^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$").unwrap()
});
if PATTERN.find(value).is_none() {
return Err("doesn't match pattern \"^[a-z][a-z0-9-]*(\\.[a-z0-9-]+)+$\"".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for ExtKey {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ExtKey {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ExtKey {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> ::serde::Deserialize<'de> for ExtKey {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
::std::string::String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as ::serde::de::Error>::custom(e.to_string())
})
}
}
/**
The highest version among the records `DataCommitment` covers. `0` for a room that holds none.
**Derived from the same set as the root, and not read from the room's own counter.** The two agree for any host that has never erased a record — versions are assigned strictly increasing and a retraction keeps its tombstone — but they are not interchangeable, because a root and a counter are *two reads*, and two reads are not a snapshot. A write landing between them yields a pair that is individually correct and jointly false: two members holding roots taken over different trees, labelled with one version. That reads as equivocation and is not, and a **false accusation discredits the mechanism rather than the host** — the worst outcome available here. Taken from the committed set, the version cannot disagree with the root it labels, whatever else is happening to the room.
The corollary is worth stating: a host that **erases** a record — as distinct from retracting it, which leaves a tombstone in the tree — moves the root without necessarily moving this value, and two members straddling that erasure would see one version over two roots. Erasure is a retention act with its own answer, and a family that exposes one owes this definition another look.
**This is what makes two roots comparable at all**, and without it the comparison this family is built on cannot be performed. A room moves: every put, curate and retraction assigns a new version, so two roots taken at two moments differ legitimately and a reader learns nothing from the difference. Shown two different roots, a host that equivocated and a host that was merely written to are indistinguishable — the first can always answer *the room moved between your reads*, and nothing contradicts it.
A version is assigned by exactly the mutations that change the tree — every put, curate and retraction takes the next one — so the highest of them names the **state** the root describes. Two roots carrying the same `headVersion` and differing is a host caught: there is no write to attribute the difference to. Two roots carrying different ones are simply two moments, and a reader should draw nothing from them.
A host can lie about this number too, and it is then lying about the counter it also uses for optimistic concurrency (`expectedVersion`) and for incremental sync (`sinceVersion`) — so a member holding a signed acknowledgement of a write at version `V` contradicts any head below `V` directly.
**Not a timestamp.** A time is host-asserted, unverifiable and useless for this: two roots a second apart are not evidence of anything, while two roots at one version are.*/
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "HeadVersion",
/// "description": "\nThe highest version among the records `DataCommitment` covers. `0` for a room that holds none.\n\n**Derived from the same set as the root, and not read from the room's own counter.** The two agree for any host that has never erased a record — versions are assigned strictly increasing and a retraction keeps its tombstone — but they are not interchangeable, because a root and a counter are *two reads*, and two reads are not a snapshot. A write landing between them yields a pair that is individually correct and jointly false: two members holding roots taken over different trees, labelled with one version. That reads as equivocation and is not, and a **false accusation discredits the mechanism rather than the host** — the worst outcome available here. Taken from the committed set, the version cannot disagree with the root it labels, whatever else is happening to the room.\n\nThe corollary is worth stating: a host that **erases** a record — as distinct from retracting it, which leaves a tombstone in the tree — moves the root without necessarily moving this value, and two members straddling that erasure would see one version over two roots. Erasure is a retention act with its own answer, and a family that exposes one owes this definition another look.\n\n**This is what makes two roots comparable at all**, and without it the comparison this family is built on cannot be performed. A room moves: every put, curate and retraction assigns a new version, so two roots taken at two moments differ legitimately and a reader learns nothing from the difference. Shown two different roots, a host that equivocated and a host that was merely written to are indistinguishable — the first can always answer *the room moved between your reads*, and nothing contradicts it.\n\nA version is assigned by exactly the mutations that change the tree — every put, curate and retraction takes the next one — so the highest of them names the **state** the root describes. Two roots carrying the same `headVersion` and differing is a host caught: there is no write to attribute the difference to. Two roots carrying different ones are simply two moments, and a reader should draw nothing from them.\n\nA host can lie about this number too, and it is then lying about the counter it also uses for optimistic concurrency (`expectedVersion`) and for incremental sync (`sinceVersion`) — so a member holding a signed acknowledgement of a write at version `V` contradicts any head below `V` directly.\n\n**Not a timestamp.** A time is host-asserted, unverifiable and useless for this: two roots a second apart are not evidence of anything, while two roots at one version are.",
/// "type": "integer",
/// "minimum": 0.0
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(transparent)]
pub struct HeadVersion(pub u64);
impl ::std::ops::Deref for HeadVersion {
type Target = u64;
fn deref(&self) -> &u64 {
&self.0
}
}
impl ::std::convert::From<HeadVersion> for u64 {
fn from(value: HeadVersion) -> Self {
value.0
}
}
impl ::std::convert::From<u64> for HeadVersion {
fn from(value: u64) -> Self {
Self(value)
}
}
impl ::std::str::FromStr for HeadVersion {
type Err = <u64 as ::std::str::FromStr>::Err;
fn from_str(value: &str) -> ::std::result::Result<Self, Self::Err> {
Ok(Self(value.parse()?))
}
}
impl ::std::convert::TryFrom<&str> for HeadVersion {
type Error = <u64 as ::std::str::FromStr>::Err;
fn try_from(value: &str) -> ::std::result::Result<Self, Self::Error> {
value.parse()
}
}
impl ::std::convert::TryFrom<String> for HeadVersion {
type Error = <u64 as ::std::str::FromStr>::Err;
fn try_from(value: String) -> ::std::result::Result<Self, Self::Error> {
value.parse()
}
}
impl ::std::fmt::Display for HeadVersion {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
self.0.fmt(f)
}
}
///TODO: what the request payload of rooms/keys/read carries. The outer document members (id, type, issuer, recipient, issuedAt, expiresAt, proof) are owned by the framework — SPEC §6.3.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "$id": "https://trusttasks.org/spec/rooms/keys/read/0.1",
/// "title": "Payload",
/// "description": "TODO: what the request payload of rooms/keys/read carries. The outer document members (id, type, issuer, recipient, issuedAt, expiresAt, proof) are owned by the framework — SPEC §6.3.",
/// "type": "object",
/// "required": [
/// "host",
/// "key",
/// "roomId"
/// ],
/// "properties": {
/// "ext": {
/// "description": "Ecosystem-defined extension members per SPEC.md §4.5.1.",
/// "$ref": "#/definitions/Ext"
/// },
/// "host": {
/// "description": "The host to read from, as a DID. Named by the caller because nothing maps a room to its host — a room is portable, so a remembered host goes stale silently, and a room may be registered with more than one. A caller who names the wrong host learns so as a refusal from a party that does not serve this room, which is loud and immediate.",
/// "type": "string"
/// },
/// "key": {
/// "description": "The record to read.",
/// "type": "string",
/// "maxLength": 512
/// },
/// "roomId": {
/// "description": "The room to read from. The recipient MUST already hold group state for it.",
/// "type": "string"
/// }
/// },
/// "additionalProperties": false
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct Payload {
///Ecosystem-defined extension members per SPEC.md §4.5.1.
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub ext: ::std::option::Option<Ext>,
///The host to read from, as a DID. Named by the caller because nothing maps a room to its host — a room is portable, so a remembered host goes stale silently, and a room may be registered with more than one. A caller who names the wrong host learns so as a refusal from a party that does not serve this room, which is loud and immediate.
pub host: ::std::string::String,
///The record to read.
pub key: PayloadKey,
///The room to read from. The recipient MUST already hold group state for it.
#[serde(rename = "roomId")]
pub room_id: ::std::string::String,
}
impl Payload {
pub fn builder() -> builder::Payload {
Default::default()
}
}
///The record to read.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "The record to read.",
/// "type": "string",
/// "maxLength": 512
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct PayloadKey(::std::string::String);
impl ::std::ops::Deref for PayloadKey {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<PayloadKey> for ::std::string::String {
fn from(value: PayloadKey) -> Self {
value.0
}
}
impl ::std::str::FromStr for PayloadKey {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
if value.chars().count() > 512usize {
return Err("longer than 512 characters".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for PayloadKey {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for PayloadKey {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for PayloadKey {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl<'de> ::serde::Deserialize<'de> for PayloadKey {
fn deserialize<D>(deserializer: D) -> ::std::result::Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
::std::string::String::deserialize(deserializer)?
.parse()
.map_err(|e: self::error::ConversionError| {
<D::Error as ::serde::de::Error>::custom(e.to_string())
})
}
}
/**
What the agent checked on the member's behalf, and what it found.
**A verdict, never an error.** None of these values fails the task, including the ones that report a host caught out. A member's own agent refusing to hand over a record because the *host* misbehaved punishes the member for somebody else's act — and locks them out of the room holding the records that would show what happened, at the moment they most need them. The consequence belongs on the **write** path, where continuing to hand material to a party you have caught is what compounds the damage.
So a consumer **MUST NOT** treat any value here as a failed read, and **MUST** surface an adverse one rather than logging it. What that costs is words: a member shown a bare warning icon dismisses it, and a member later refused a write with no explanation blames their own agent. A detection the member attributes to the wrong party is worse than no detection.
Members are **absent where the task cannot produce them** rather than carrying a not-applicable value — a listing has no `trace` because there is no single record to trace, and a single read has no `count` because a count is only checkable against a listing read to its end.*/
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "ReadVerification",
/// "description": "\nWhat the agent checked on the member's behalf, and what it found.\n\n**A verdict, never an error.** None of these values fails the task, including the ones that report a host caught out. A member's own agent refusing to hand over a record because the *host* misbehaved punishes the member for somebody else's act — and locks them out of the room holding the records that would show what happened, at the moment they most need them. The consequence belongs on the **write** path, where continuing to hand material to a party you have caught is what compounds the damage.\n\nSo a consumer **MUST NOT** treat any value here as a failed read, and **MUST** surface an adverse one rather than logging it. What that costs is words: a member shown a bare warning icon dismisses it, and a member later refused a write with no explanation blames their own agent. A detection the member attributes to the wrong party is worse than no detection.\n\nMembers are **absent where the task cannot produce them** rather than carrying a not-applicable value — a listing has no `trace` because there is no single record to trace, and a single read has no `count` because a count is only checkable against a listing read to its end.",
/// "type": "object",
/// "required": [
/// "priorRoots"
/// ],
/// "properties": {
/// "anchor": {
/// "description": "\nHow what the host served compares with the room's own **witnessed anchor** (`EpochAnchor`).\n\nThis is the comparison that needs neither a gossip channel rooms deliberately lack nor durable state in an agent: every member resolves the same room DID and reads the same entry, co-signed by witnesses. It is the only one of the three a first-time reader can make.\n\n - `agrees` — the host served the anchored state, and its root matches.\n - `ahead` — the room has moved past the anchor. The ordinary case; an anchor describes a moment, not the present, and says nothing about records written since.\n - `behind` — **the host is serving a state older than the room's own witnessed statement.** A rollback, and a detection nothing else in this family can make: a member with no history, no peer and no prior read still catches it.\n - `conflict` — same `headVersion` as the anchor, different root. The host has contradicted a value its own room published and witnesses co-signed.\n - `none` — the room has published no anchor. Not a fault; anchoring costs a witnessed update and a key rotation, and a room may reasonably decline.\n - `notChecked` — the consumer did not resolve the room. An honest answer, and **not** a synonym for `none`: one says the room published nothing, the other says nobody looked.",
/// "type": "string",
/// "enum": [
/// "agrees",
/// "ahead",
/// "behind",
/// "conflict",
/// "none",
/// "notChecked"
/// ]
/// },
/// "count": {
/// "description": "\nWhether the number of records returned matches the `recordCount` the host committed to.\n\nOnly ever comparable against a listing with **no** `prefix`, **no** `sinceVersion` and read to its end — anything else legitimately holds fewer, and comparing it is a discrepancy the reader manufactured. `notComparable` is that case, and it is the common one.\n\n`short` is a host contradicting itself inside one exchange: it committed to a tree of N records and served fewer, with no filter to explain the difference. That is the omission the commitment exists to make detectable, caught without a second party and without an anchor.",
/// "type": "string",
/// "enum": [
/// "agrees",
/// "short",
/// "notComparable",
/// "notOffered"
/// ]
/// },
/// "head": {
/// "description": "What the host asserted about the room, passed through unaltered so the member can compare it somewhere this agent cannot reach — with another member, or against a witnessed anchor. All three or none: a root without the state it describes is not comparable to another root, which is the whole of `HeadVersion`.",
/// "type": "object",
/// "required": [
/// "dataCommitment",
/// "headVersion",
/// "recordCount"
/// ],
/// "properties": {
/// "dataCommitment": {
/// "$ref": "#/definitions/DataCommitment"
/// },
/// "headVersion": {
/// "$ref": "#/definitions/HeadVersion"
/// },
/// "recordCount": {
/// "$ref": "#/definitions/RecordCount"
/// }
/// },
/// "additionalProperties": false
/// },
/// "priorRoots": {
/// "description": "\nWhether this root matches what the agent has seen from this host for this room **at this `headVersion`**.\n\nThis is the comparison a member cannot make for themselves. A tab does not outlive itself and a CLI holds nothing; the agent is the only party on the member's side of the boundary that saw both reads.\n\n - `agree` — seen at this head before, same root.\n - `conflict` — seen at this head before, **different root**. A host caught: there is no write to attribute the difference to, because a write would have moved the head.\n - `noneHeld` — first read at this head. Not evidence of anything; a memory of one is not a comparison.\n - `notChecked` — this agent keeps no root history. An honest answer for an agent that cannot make the comparison, and **not** a synonym for `noneHeld`: one says nothing was found, the other says nothing was looked for.\n\nREQUIRED, so that an agent which does not check has to say so rather than omit the question.",
/// "type": "string",
/// "enum": [
/// "agree",
/// "conflict",
/// "noneHeld",
/// "notChecked"
/// ]
/// },
/// "trace": {
/// "description": "\nWhether the record's `RecordTrace` reached the `dataCommitment` served beside it.\n\n`verified` says the record is under the root the host asserted — and **nothing about whether that root is the room's**, which is what `priorRoots` and an anchor are for. `notOffered` is a host that maintains no tree, which is legal and informative. `failed` is arithmetic that does not close: the host served a path that does not reach its own root, which is either a defect or a fabrication and is not distinguishable from here.",
/// "type": "string",
/// "enum": [
/// "verified",
/// "failed",
/// "notOffered"
/// ]
/// }
/// },
/// "additionalProperties": false
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct ReadVerification {
/**
How what the host served compares with the room's own **witnessed anchor** (`EpochAnchor`).
This is the comparison that needs neither a gossip channel rooms deliberately lack nor durable state in an agent: every member resolves the same room DID and reads the same entry, co-signed by witnesses. It is the only one of the three a first-time reader can make.
- `agrees` — the host served the anchored state, and its root matches.
- `ahead` — the room has moved past the anchor. The ordinary case; an anchor describes a moment, not the present, and says nothing about records written since.
- `behind` — **the host is serving a state older than the room's own witnessed statement.** A rollback, and a detection nothing else in this family can make: a member with no history, no peer and no prior read still catches it.
- `conflict` — same `headVersion` as the anchor, different root. The host has contradicted a value its own room published and witnesses co-signed.
- `none` — the room has published no anchor. Not a fault; anchoring costs a witnessed update and a key rotation, and a room may reasonably decline.
- `notChecked` — the consumer did not resolve the room. An honest answer, and **not** a synonym for `none`: one says the room published nothing, the other says nobody looked.*/
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub anchor: ::std::option::Option<ReadVerificationAnchor>,
/**
Whether the number of records returned matches the `recordCount` the host committed to.
Only ever comparable against a listing with **no** `prefix`, **no** `sinceVersion` and read to its end — anything else legitimately holds fewer, and comparing it is a discrepancy the reader manufactured. `notComparable` is that case, and it is the common one.
`short` is a host contradicting itself inside one exchange: it committed to a tree of N records and served fewer, with no filter to explain the difference. That is the omission the commitment exists to make detectable, caught without a second party and without an anchor.*/
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub count: ::std::option::Option<ReadVerificationCount>,
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub head: ::std::option::Option<ReadVerificationHead>,
/**
Whether this root matches what the agent has seen from this host for this room **at this `headVersion`**.
This is the comparison a member cannot make for themselves. A tab does not outlive itself and a CLI holds nothing; the agent is the only party on the member's side of the boundary that saw both reads.
- `agree` — seen at this head before, same root.
- `conflict` — seen at this head before, **different root**. A host caught: there is no write to attribute the difference to, because a write would have moved the head.
- `noneHeld` — first read at this head. Not evidence of anything; a memory of one is not a comparison.
- `notChecked` — this agent keeps no root history. An honest answer for an agent that cannot make the comparison, and **not** a synonym for `noneHeld`: one says nothing was found, the other says nothing was looked for.
REQUIRED, so that an agent which does not check has to say so rather than omit the question.*/
#[serde(rename = "priorRoots")]
pub prior_roots: ReadVerificationPriorRoots,
/**
Whether the record's `RecordTrace` reached the `dataCommitment` served beside it.
`verified` says the record is under the root the host asserted — and **nothing about whether that root is the room's**, which is what `priorRoots` and an anchor are for. `notOffered` is a host that maintains no tree, which is legal and informative. `failed` is arithmetic that does not close: the host served a path that does not reach its own root, which is either a defect or a fabrication and is not distinguishable from here.*/
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub trace: ::std::option::Option<ReadVerificationTrace>,
}
impl ReadVerification {
pub fn builder() -> builder::ReadVerification {
Default::default()
}
}
/**
How what the host served compares with the room's own **witnessed anchor** (`EpochAnchor`).
This is the comparison that needs neither a gossip channel rooms deliberately lack nor durable state in an agent: every member resolves the same room DID and reads the same entry, co-signed by witnesses. It is the only one of the three a first-time reader can make.
- `agrees` — the host served the anchored state, and its root matches.
- `ahead` — the room has moved past the anchor. The ordinary case; an anchor describes a moment, not the present, and says nothing about records written since.
- `behind` — **the host is serving a state older than the room's own witnessed statement.** A rollback, and a detection nothing else in this family can make: a member with no history, no peer and no prior read still catches it.
- `conflict` — same `headVersion` as the anchor, different root. The host has contradicted a value its own room published and witnesses co-signed.
- `none` — the room has published no anchor. Not a fault; anchoring costs a witnessed update and a key rotation, and a room may reasonably decline.
- `notChecked` — the consumer did not resolve the room. An honest answer, and **not** a synonym for `none`: one says the room published nothing, the other says nobody looked.*/
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "\nHow what the host served compares with the room's own **witnessed anchor** (`EpochAnchor`).\n\nThis is the comparison that needs neither a gossip channel rooms deliberately lack nor durable state in an agent: every member resolves the same room DID and reads the same entry, co-signed by witnesses. It is the only one of the three a first-time reader can make.\n\n - `agrees` — the host served the anchored state, and its root matches.\n - `ahead` — the room has moved past the anchor. The ordinary case; an anchor describes a moment, not the present, and says nothing about records written since.\n - `behind` — **the host is serving a state older than the room's own witnessed statement.** A rollback, and a detection nothing else in this family can make: a member with no history, no peer and no prior read still catches it.\n - `conflict` — same `headVersion` as the anchor, different root. The host has contradicted a value its own room published and witnesses co-signed.\n - `none` — the room has published no anchor. Not a fault; anchoring costs a witnessed update and a key rotation, and a room may reasonably decline.\n - `notChecked` — the consumer did not resolve the room. An honest answer, and **not** a synonym for `none`: one says the room published nothing, the other says nobody looked.",
/// "type": "string",
/// "enum": [
/// "agrees",
/// "ahead",
/// "behind",
/// "conflict",
/// "none",
/// "notChecked"
/// ]
///}
/// ```
/// </details>
#[derive(
::serde::Deserialize,
::serde::Serialize,
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
)]
#[non_exhaustive]
pub enum ReadVerificationAnchor {
#[serde(rename = "agrees")]
Agrees,
#[serde(rename = "ahead")]
Ahead,
#[serde(rename = "behind")]
Behind,
#[serde(rename = "conflict")]
Conflict,
#[serde(rename = "none")]
None,
#[serde(rename = "notChecked")]
NotChecked,
}
impl ::std::fmt::Display for ReadVerificationAnchor {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match *self {
Self::Agrees => f.write_str("agrees"),
Self::Ahead => f.write_str("ahead"),
Self::Behind => f.write_str("behind"),
Self::Conflict => f.write_str("conflict"),
Self::None => f.write_str("none"),
Self::NotChecked => f.write_str("notChecked"),
}
}
}
impl ::std::str::FromStr for ReadVerificationAnchor {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
match value {
"agrees" => Ok(Self::Agrees),
"ahead" => Ok(Self::Ahead),
"behind" => Ok(Self::Behind),
"conflict" => Ok(Self::Conflict),
"none" => Ok(Self::None),
"notChecked" => Ok(Self::NotChecked),
_ => Err("invalid value".into()),
}
}
}
impl ::std::convert::TryFrom<&str> for ReadVerificationAnchor {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ReadVerificationAnchor {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ReadVerificationAnchor {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
/**
Whether the number of records returned matches the `recordCount` the host committed to.
Only ever comparable against a listing with **no** `prefix`, **no** `sinceVersion` and read to its end — anything else legitimately holds fewer, and comparing it is a discrepancy the reader manufactured. `notComparable` is that case, and it is the common one.
`short` is a host contradicting itself inside one exchange: it committed to a tree of N records and served fewer, with no filter to explain the difference. That is the omission the commitment exists to make detectable, caught without a second party and without an anchor.*/
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "\nWhether the number of records returned matches the `recordCount` the host committed to.\n\nOnly ever comparable against a listing with **no** `prefix`, **no** `sinceVersion` and read to its end — anything else legitimately holds fewer, and comparing it is a discrepancy the reader manufactured. `notComparable` is that case, and it is the common one.\n\n`short` is a host contradicting itself inside one exchange: it committed to a tree of N records and served fewer, with no filter to explain the difference. That is the omission the commitment exists to make detectable, caught without a second party and without an anchor.",
/// "type": "string",
/// "enum": [
/// "agrees",
/// "short",
/// "notComparable",
/// "notOffered"
/// ]
///}
/// ```
/// </details>
#[derive(
::serde::Deserialize,
::serde::Serialize,
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
)]
#[non_exhaustive]
pub enum ReadVerificationCount {
#[serde(rename = "agrees")]
Agrees,
#[serde(rename = "short")]
Short,
#[serde(rename = "notComparable")]
NotComparable,
#[serde(rename = "notOffered")]
NotOffered,
}
impl ::std::fmt::Display for ReadVerificationCount {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match *self {
Self::Agrees => f.write_str("agrees"),
Self::Short => f.write_str("short"),
Self::NotComparable => f.write_str("notComparable"),
Self::NotOffered => f.write_str("notOffered"),
}
}
}
impl ::std::str::FromStr for ReadVerificationCount {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
match value {
"agrees" => Ok(Self::Agrees),
"short" => Ok(Self::Short),
"notComparable" => Ok(Self::NotComparable),
"notOffered" => Ok(Self::NotOffered),
_ => Err("invalid value".into()),
}
}
}
impl ::std::convert::TryFrom<&str> for ReadVerificationCount {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ReadVerificationCount {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ReadVerificationCount {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
///What the host asserted about the room, passed through unaltered so the member can compare it somewhere this agent cannot reach — with another member, or against a witnessed anchor. All three or none: a root without the state it describes is not comparable to another root, which is the whole of `HeadVersion`.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "What the host asserted about the room, passed through unaltered so the member can compare it somewhere this agent cannot reach — with another member, or against a witnessed anchor. All three or none: a root without the state it describes is not comparable to another root, which is the whole of `HeadVersion`.",
/// "type": "object",
/// "required": [
/// "dataCommitment",
/// "headVersion",
/// "recordCount"
/// ],
/// "properties": {
/// "dataCommitment": {
/// "$ref": "#/definitions/DataCommitment"
/// },
/// "headVersion": {
/// "$ref": "#/definitions/HeadVersion"
/// },
/// "recordCount": {
/// "$ref": "#/definitions/RecordCount"
/// }
/// },
/// "additionalProperties": false
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct ReadVerificationHead {
#[serde(rename = "dataCommitment")]
pub data_commitment: DataCommitment,
#[serde(rename = "headVersion")]
pub head_version: HeadVersion,
#[serde(rename = "recordCount")]
pub record_count: RecordCount,
}
impl ReadVerificationHead {
pub fn builder() -> builder::ReadVerificationHead {
Default::default()
}
}
/**
Whether this root matches what the agent has seen from this host for this room **at this `headVersion`**.
This is the comparison a member cannot make for themselves. A tab does not outlive itself and a CLI holds nothing; the agent is the only party on the member's side of the boundary that saw both reads.
- `agree` — seen at this head before, same root.
- `conflict` — seen at this head before, **different root**. A host caught: there is no write to attribute the difference to, because a write would have moved the head.
- `noneHeld` — first read at this head. Not evidence of anything; a memory of one is not a comparison.
- `notChecked` — this agent keeps no root history. An honest answer for an agent that cannot make the comparison, and **not** a synonym for `noneHeld`: one says nothing was found, the other says nothing was looked for.
REQUIRED, so that an agent which does not check has to say so rather than omit the question.*/
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "\nWhether this root matches what the agent has seen from this host for this room **at this `headVersion`**.\n\nThis is the comparison a member cannot make for themselves. A tab does not outlive itself and a CLI holds nothing; the agent is the only party on the member's side of the boundary that saw both reads.\n\n - `agree` — seen at this head before, same root.\n - `conflict` — seen at this head before, **different root**. A host caught: there is no write to attribute the difference to, because a write would have moved the head.\n - `noneHeld` — first read at this head. Not evidence of anything; a memory of one is not a comparison.\n - `notChecked` — this agent keeps no root history. An honest answer for an agent that cannot make the comparison, and **not** a synonym for `noneHeld`: one says nothing was found, the other says nothing was looked for.\n\nREQUIRED, so that an agent which does not check has to say so rather than omit the question.",
/// "type": "string",
/// "enum": [
/// "agree",
/// "conflict",
/// "noneHeld",
/// "notChecked"
/// ]
///}
/// ```
/// </details>
#[derive(
::serde::Deserialize,
::serde::Serialize,
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
)]
#[non_exhaustive]
pub enum ReadVerificationPriorRoots {
#[serde(rename = "agree")]
Agree,
#[serde(rename = "conflict")]
Conflict,
#[serde(rename = "noneHeld")]
NoneHeld,
#[serde(rename = "notChecked")]
NotChecked,
}
impl ::std::fmt::Display for ReadVerificationPriorRoots {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match *self {
Self::Agree => f.write_str("agree"),
Self::Conflict => f.write_str("conflict"),
Self::NoneHeld => f.write_str("noneHeld"),
Self::NotChecked => f.write_str("notChecked"),
}
}
}
impl ::std::str::FromStr for ReadVerificationPriorRoots {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
match value {
"agree" => Ok(Self::Agree),
"conflict" => Ok(Self::Conflict),
"noneHeld" => Ok(Self::NoneHeld),
"notChecked" => Ok(Self::NotChecked),
_ => Err("invalid value".into()),
}
}
}
impl ::std::convert::TryFrom<&str> for ReadVerificationPriorRoots {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ReadVerificationPriorRoots {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ReadVerificationPriorRoots {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
/**
Whether the record's `RecordTrace` reached the `dataCommitment` served beside it.
`verified` says the record is under the root the host asserted — and **nothing about whether that root is the room's**, which is what `priorRoots` and an anchor are for. `notOffered` is a host that maintains no tree, which is legal and informative. `failed` is arithmetic that does not close: the host served a path that does not reach its own root, which is either a defect or a fabrication and is not distinguishable from here.*/
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "\nWhether the record's `RecordTrace` reached the `dataCommitment` served beside it.\n\n`verified` says the record is under the root the host asserted — and **nothing about whether that root is the room's**, which is what `priorRoots` and an anchor are for. `notOffered` is a host that maintains no tree, which is legal and informative. `failed` is arithmetic that does not close: the host served a path that does not reach its own root, which is either a defect or a fabrication and is not distinguishable from here.",
/// "type": "string",
/// "enum": [
/// "verified",
/// "failed",
/// "notOffered"
/// ]
///}
/// ```
/// </details>
#[derive(
::serde::Deserialize,
::serde::Serialize,
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
)]
#[non_exhaustive]
pub enum ReadVerificationTrace {
#[serde(rename = "verified")]
Verified,
#[serde(rename = "failed")]
Failed,
#[serde(rename = "notOffered")]
NotOffered,
}
impl ::std::fmt::Display for ReadVerificationTrace {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match *self {
Self::Verified => f.write_str("verified"),
Self::Failed => f.write_str("failed"),
Self::NotOffered => f.write_str("notOffered"),
}
}
}
impl ::std::str::FromStr for ReadVerificationTrace {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
match value {
"verified" => Ok(Self::Verified),
"failed" => Ok(Self::Failed),
"notOffered" => Ok(Self::NotOffered),
_ => Err("invalid value".into()),
}
}
}
impl ::std::convert::TryFrom<&str> for ReadVerificationTrace {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ReadVerificationTrace {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ReadVerificationTrace {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
/**
How many records the room held when `DataCommitment` was computed — the number of leaves in that tree, tombstones included, since a tombstone is a record.
**Why a root needs this.** Certificate Transparency's signed tree head is a root *and a tree size*; this family shipped the root alone, and the half that was dropped is the half that makes a listing checkable. A reader holding a **complete, unfiltered** listing cannot recompute the root — a leaf commits to a whole record and a listing returns a projection without the body — but it can count. A host that omits a record from a listing while committing to a tree that holds it now contradicts itself in the same response, with no second party and no anchor involved.
A host can of course understate both together. That is the point rather than a hole: the omission stops being silence and becomes a **specific claim about how many records the room holds**, which any other member's view, or any writer's signed put acknowledgement, contradicts. Making an omission attributable is the whole of what this machinery buys; it never claimed to make one impossible.
**It counts the room, never the page.** The same rule `DataCommitment` states, and the same trap: a count scoped to what was returned is one a host satisfies by construction. So this is only comparable against a listing read to the end with **no** `prefix` and **no** `sinceVersion` — a filtered listing legitimately holds fewer, and a reader that compares one against this has found a discrepancy it created itself.*/
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "RecordCount",
/// "description": "\nHow many records the room held when `DataCommitment` was computed — the number of leaves in that tree, tombstones included, since a tombstone is a record.\n\n**Why a root needs this.** Certificate Transparency's signed tree head is a root *and a tree size*; this family shipped the root alone, and the half that was dropped is the half that makes a listing checkable. A reader holding a **complete, unfiltered** listing cannot recompute the root — a leaf commits to a whole record and a listing returns a projection without the body — but it can count. A host that omits a record from a listing while committing to a tree that holds it now contradicts itself in the same response, with no second party and no anchor involved.\n\nA host can of course understate both together. That is the point rather than a hole: the omission stops being silence and becomes a **specific claim about how many records the room holds**, which any other member's view, or any writer's signed put acknowledgement, contradicts. Making an omission attributable is the whole of what this machinery buys; it never claimed to make one impossible.\n\n**It counts the room, never the page.** The same rule `DataCommitment` states, and the same trap: a count scoped to what was returned is one a host satisfies by construction. So this is only comparable against a listing read to the end with **no** `prefix` and **no** `sinceVersion` — a filtered listing legitimately holds fewer, and a reader that compares one against this has found a discrepancy it created itself.",
/// "type": "integer",
/// "minimum": 0.0
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(transparent)]
pub struct RecordCount(pub u64);
impl ::std::ops::Deref for RecordCount {
type Target = u64;
fn deref(&self) -> &u64 {
&self.0
}
}
impl ::std::convert::From<RecordCount> for u64 {
fn from(value: RecordCount) -> Self {
value.0
}
}
impl ::std::convert::From<u64> for RecordCount {
fn from(value: u64) -> Self {
Self(value)
}
}
impl ::std::str::FromStr for RecordCount {
type Err = <u64 as ::std::str::FromStr>::Err;
fn from_str(value: &str) -> ::std::result::Result<Self, Self::Err> {
Ok(Self(value.parse()?))
}
}
impl ::std::convert::TryFrom<&str> for RecordCount {
type Error = <u64 as ::std::str::FromStr>::Err;
fn try_from(value: &str) -> ::std::result::Result<Self, Self::Error> {
value.parse()
}
}
impl ::std::convert::TryFrom<String> for RecordCount {
type Error = <u64 as ::std::str::FromStr>::Err;
fn try_from(value: String) -> ::std::result::Result<Self, Self::Error> {
value.parse()
}
}
impl ::std::fmt::Display for RecordCount {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
self.0.fmt(f)
}
}
///Success response to rooms/keys/read. Type https://trusttasks.org/spec/rooms/keys/read/0.1#response.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "Response",
/// "description": "Success response to rooms/keys/read. Type https://trusttasks.org/spec/rooms/keys/read/0.1#response.",
/// "type": "object",
/// "required": [
/// "key",
/// "roomId",
/// "verification",
/// "version"
/// ],
/// "properties": {
/// "author": {
/// "description": "The member who wrote it, where the tier discloses one. Absent on `private`, where authorship is inside the body the recipient just opened.",
/// "type": "string"
/// },
/// "cleartext": {
/// "description": "The record body on an `open` room, where there is nothing to open. Carried as itself rather than base64url so that the shape says which tier the member is on — a member ought to be able to tell that this room's host can read what they just read.",
/// "type": "object",
/// "additionalProperties": true
/// },
/// "ext": {
/// "$ref": "#/definitions/Ext"
/// },
/// "key": {
/// "type": "string"
/// },
/// "plaintext": {
/// "description": "The opened record, base64url — the sealed tiers. **Never the key**, which is the whole reason this task exists rather than the record being handed to the member to open. Spelled as `rooms/keys/open` spells it, because it is the same bytes by the same route.",
/// "type": "string"
/// },
/// "roomId": {
/// "type": "string"
/// },
/// "status": {
/// "description": "Curation state, passed through. A `retracted` record has no body: the tombstone is the answer, not a failure.",
/// "type": "string",
/// "enum": [
/// "active",
/// "deprecated",
/// "retracted"
/// ]
/// },
/// "updatedAt": {
/// "type": "string",
/// "format": "date-time"
/// },
/// "verification": {
/// "description": "What the recipient checked and what it found. REQUIRED: an agent that returns a record without saying what it checked has made the member's decision for them.",
/// "$ref": "#/definitions/ReadVerification"
/// },
/// "version": {
/// "description": "The record's version at the host.",
/// "type": "integer",
/// "minimum": 1.0
/// }
/// },
/// "additionalProperties": false,
/// "$anchor": "response"
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct Response {
///The member who wrote it, where the tier discloses one. Absent on `private`, where authorship is inside the body the recipient just opened.
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub author: ::std::option::Option<::std::string::String>,
///The record body on an `open` room, where there is nothing to open. Carried as itself rather than base64url so that the shape says which tier the member is on — a member ought to be able to tell that this room's host can read what they just read.
#[serde(default, skip_serializing_if = "::serde_json::Map::is_empty")]
pub cleartext: ::serde_json::Map<::std::string::String, ::serde_json::Value>,
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub ext: ::std::option::Option<Ext>,
pub key: ::std::string::String,
///The opened record, base64url — the sealed tiers. **Never the key**, which is the whole reason this task exists rather than the record being handed to the member to open. Spelled as `rooms/keys/open` spells it, because it is the same bytes by the same route.
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub plaintext: ::std::option::Option<::std::string::String>,
#[serde(rename = "roomId")]
pub room_id: ::std::string::String,
///Curation state, passed through. A `retracted` record has no body: the tombstone is the answer, not a failure.
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub status: ::std::option::Option<ResponseStatus>,
#[serde(
rename = "updatedAt",
default,
skip_serializing_if = "::std::option::Option::is_none"
)]
pub updated_at: ::std::option::Option<::chrono::DateTime<::chrono::offset::Utc>>,
///What the recipient checked and what it found. REQUIRED: an agent that returns a record without saying what it checked has made the member's decision for them.
pub verification: ReadVerification,
///The record's version at the host.
pub version: ::std::num::NonZeroU64,
}
impl Response {
pub fn builder() -> builder::Response {
Default::default()
}
}
///Curation state, passed through. A `retracted` record has no body: the tombstone is the answer, not a failure.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "Curation state, passed through. A `retracted` record has no body: the tombstone is the answer, not a failure.",
/// "type": "string",
/// "enum": [
/// "active",
/// "deprecated",
/// "retracted"
/// ]
///}
/// ```
/// </details>
#[derive(
::serde::Deserialize,
::serde::Serialize,
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
)]
#[non_exhaustive]
pub enum ResponseStatus {
#[serde(rename = "active")]
Active,
#[serde(rename = "deprecated")]
Deprecated,
#[serde(rename = "retracted")]
Retracted,
}
impl ::std::fmt::Display for ResponseStatus {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match *self {
Self::Active => f.write_str("active"),
Self::Deprecated => f.write_str("deprecated"),
Self::Retracted => f.write_str("retracted"),
}
}
}
impl ::std::str::FromStr for ResponseStatus {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
match value {
"active" => Ok(Self::Active),
"deprecated" => Ok(Self::Deprecated),
"retracted" => Ok(Self::Retracted),
_ => Err("invalid value".into()),
}
}
}
impl ::std::convert::TryFrom<&str> for ResponseStatus {
type Error = self::error::ConversionError;
fn try_from(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<&::std::string::String> for ResponseStatus {
type Error = self::error::ConversionError;
fn try_from(
value: &::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
impl ::std::convert::TryFrom<::std::string::String> for ResponseStatus {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
/// Types for composing complex structures.
pub mod builder {
#[derive(Clone, Debug)]
pub struct Payload {
ext: ::std::result::Result<::std::option::Option<super::Ext>, ::std::string::String>,
host: ::std::result::Result<::std::string::String, ::std::string::String>,
key: ::std::result::Result<super::PayloadKey, ::std::string::String>,
room_id: ::std::result::Result<::std::string::String, ::std::string::String>,
}
impl ::std::default::Default for Payload {
fn default() -> Self {
Self {
ext: Ok(Default::default()),
host: Err("no value supplied for host".to_string()),
key: Err("no value supplied for key".to_string()),
room_id: Err("no value supplied for room_id".to_string()),
}
}
}
impl Payload {
pub fn ext<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::Ext>>,
T::Error: ::std::fmt::Display,
{
self.ext = value
.try_into()
.map_err(|e| format!("error converting supplied value for ext: {e}"));
self
}
pub fn host<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::string::String>,
T::Error: ::std::fmt::Display,
{
self.host = value
.try_into()
.map_err(|e| format!("error converting supplied value for host: {e}"));
self
}
pub fn key<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::PayloadKey>,
T::Error: ::std::fmt::Display,
{
self.key = value
.try_into()
.map_err(|e| format!("error converting supplied value for key: {e}"));
self
}
pub fn room_id<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::string::String>,
T::Error: ::std::fmt::Display,
{
self.room_id = value
.try_into()
.map_err(|e| format!("error converting supplied value for room_id: {e}"));
self
}
}
impl ::std::convert::TryFrom<Payload> for super::Payload {
type Error = super::error::ConversionError;
fn try_from(value: Payload) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self {
ext: value.ext?,
host: value.host?,
key: value.key?,
room_id: value.room_id?,
})
}
}
impl ::std::convert::From<super::Payload> for Payload {
fn from(value: super::Payload) -> Self {
Self {
ext: Ok(value.ext),
host: Ok(value.host),
key: Ok(value.key),
room_id: Ok(value.room_id),
}
}
}
#[derive(Clone, Debug)]
pub struct ReadVerification {
anchor: ::std::result::Result<
::std::option::Option<super::ReadVerificationAnchor>,
::std::string::String,
>,
count: ::std::result::Result<
::std::option::Option<super::ReadVerificationCount>,
::std::string::String,
>,
head: ::std::result::Result<
::std::option::Option<super::ReadVerificationHead>,
::std::string::String,
>,
prior_roots:
::std::result::Result<super::ReadVerificationPriorRoots, ::std::string::String>,
trace: ::std::result::Result<
::std::option::Option<super::ReadVerificationTrace>,
::std::string::String,
>,
}
impl ::std::default::Default for ReadVerification {
fn default() -> Self {
Self {
anchor: Ok(Default::default()),
count: Ok(Default::default()),
head: Ok(Default::default()),
prior_roots: Err("no value supplied for prior_roots".to_string()),
trace: Ok(Default::default()),
}
}
}
impl ReadVerification {
pub fn anchor<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::ReadVerificationAnchor>>,
T::Error: ::std::fmt::Display,
{
self.anchor = value
.try_into()
.map_err(|e| format!("error converting supplied value for anchor: {e}"));
self
}
pub fn count<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::ReadVerificationCount>>,
T::Error: ::std::fmt::Display,
{
self.count = value
.try_into()
.map_err(|e| format!("error converting supplied value for count: {e}"));
self
}
pub fn head<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::ReadVerificationHead>>,
T::Error: ::std::fmt::Display,
{
self.head = value
.try_into()
.map_err(|e| format!("error converting supplied value for head: {e}"));
self
}
pub fn prior_roots<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::ReadVerificationPriorRoots>,
T::Error: ::std::fmt::Display,
{
self.prior_roots = value
.try_into()
.map_err(|e| format!("error converting supplied value for prior_roots: {e}"));
self
}
pub fn trace<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::ReadVerificationTrace>>,
T::Error: ::std::fmt::Display,
{
self.trace = value
.try_into()
.map_err(|e| format!("error converting supplied value for trace: {e}"));
self
}
}
impl ::std::convert::TryFrom<ReadVerification> for super::ReadVerification {
type Error = super::error::ConversionError;
fn try_from(
value: ReadVerification,
) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self {
anchor: value.anchor?,
count: value.count?,
head: value.head?,
prior_roots: value.prior_roots?,
trace: value.trace?,
})
}
}
impl ::std::convert::From<super::ReadVerification> for ReadVerification {
fn from(value: super::ReadVerification) -> Self {
Self {
anchor: Ok(value.anchor),
count: Ok(value.count),
head: Ok(value.head),
prior_roots: Ok(value.prior_roots),
trace: Ok(value.trace),
}
}
}
#[derive(Clone, Debug)]
pub struct ReadVerificationHead {
data_commitment: ::std::result::Result<super::DataCommitment, ::std::string::String>,
head_version: ::std::result::Result<super::HeadVersion, ::std::string::String>,
record_count: ::std::result::Result<super::RecordCount, ::std::string::String>,
}
impl ::std::default::Default for ReadVerificationHead {
fn default() -> Self {
Self {
data_commitment: Err("no value supplied for data_commitment".to_string()),
head_version: Err("no value supplied for head_version".to_string()),
record_count: Err("no value supplied for record_count".to_string()),
}
}
}
impl ReadVerificationHead {
pub fn data_commitment<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::DataCommitment>,
T::Error: ::std::fmt::Display,
{
self.data_commitment = value
.try_into()
.map_err(|e| format!("error converting supplied value for data_commitment: {e}"));
self
}
pub fn head_version<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::HeadVersion>,
T::Error: ::std::fmt::Display,
{
self.head_version = value
.try_into()
.map_err(|e| format!("error converting supplied value for head_version: {e}"));
self
}
pub fn record_count<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::RecordCount>,
T::Error: ::std::fmt::Display,
{
self.record_count = value
.try_into()
.map_err(|e| format!("error converting supplied value for record_count: {e}"));
self
}
}
impl ::std::convert::TryFrom<ReadVerificationHead> for super::ReadVerificationHead {
type Error = super::error::ConversionError;
fn try_from(
value: ReadVerificationHead,
) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self {
data_commitment: value.data_commitment?,
head_version: value.head_version?,
record_count: value.record_count?,
})
}
}
impl ::std::convert::From<super::ReadVerificationHead> for ReadVerificationHead {
fn from(value: super::ReadVerificationHead) -> Self {
Self {
data_commitment: Ok(value.data_commitment),
head_version: Ok(value.head_version),
record_count: Ok(value.record_count),
}
}
}
#[derive(Clone, Debug)]
pub struct Response {
author: ::std::result::Result<
::std::option::Option<::std::string::String>,
::std::string::String,
>,
cleartext: ::std::result::Result<
::serde_json::Map<::std::string::String, ::serde_json::Value>,
::std::string::String,
>,
ext: ::std::result::Result<::std::option::Option<super::Ext>, ::std::string::String>,
key: ::std::result::Result<::std::string::String, ::std::string::String>,
plaintext: ::std::result::Result<
::std::option::Option<::std::string::String>,
::std::string::String,
>,
room_id: ::std::result::Result<::std::string::String, ::std::string::String>,
status: ::std::result::Result<
::std::option::Option<super::ResponseStatus>,
::std::string::String,
>,
updated_at: ::std::result::Result<
::std::option::Option<::chrono::DateTime<::chrono::offset::Utc>>,
::std::string::String,
>,
verification: ::std::result::Result<super::ReadVerification, ::std::string::String>,
version: ::std::result::Result<::std::num::NonZeroU64, ::std::string::String>,
}
impl ::std::default::Default for Response {
fn default() -> Self {
Self {
author: Ok(Default::default()),
cleartext: Ok(Default::default()),
ext: Ok(Default::default()),
key: Err("no value supplied for key".to_string()),
plaintext: Ok(Default::default()),
room_id: Err("no value supplied for room_id".to_string()),
status: Ok(Default::default()),
updated_at: Ok(Default::default()),
verification: Err("no value supplied for verification".to_string()),
version: Err("no value supplied for version".to_string()),
}
}
}
impl Response {
pub fn author<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>,
T::Error: ::std::fmt::Display,
{
self.author = value
.try_into()
.map_err(|e| format!("error converting supplied value for author: {e}"));
self
}
pub fn cleartext<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<
::serde_json::Map<::std::string::String, ::serde_json::Value>,
>,
T::Error: ::std::fmt::Display,
{
self.cleartext = value
.try_into()
.map_err(|e| format!("error converting supplied value for cleartext: {e}"));
self
}
pub fn ext<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::Ext>>,
T::Error: ::std::fmt::Display,
{
self.ext = value
.try_into()
.map_err(|e| format!("error converting supplied value for ext: {e}"));
self
}
pub fn key<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::string::String>,
T::Error: ::std::fmt::Display,
{
self.key = value
.try_into()
.map_err(|e| format!("error converting supplied value for key: {e}"));
self
}
pub fn plaintext<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>,
T::Error: ::std::fmt::Display,
{
self.plaintext = value
.try_into()
.map_err(|e| format!("error converting supplied value for plaintext: {e}"));
self
}
pub fn room_id<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::string::String>,
T::Error: ::std::fmt::Display,
{
self.room_id = value
.try_into()
.map_err(|e| format!("error converting supplied value for room_id: {e}"));
self
}
pub fn status<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::ResponseStatus>>,
T::Error: ::std::fmt::Display,
{
self.status = value
.try_into()
.map_err(|e| format!("error converting supplied value for status: {e}"));
self
}
pub fn updated_at<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<
::std::option::Option<::chrono::DateTime<::chrono::offset::Utc>>,
>,
T::Error: ::std::fmt::Display,
{
self.updated_at = value
.try_into()
.map_err(|e| format!("error converting supplied value for updated_at: {e}"));
self
}
pub fn verification<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::ReadVerification>,
T::Error: ::std::fmt::Display,
{
self.verification = value
.try_into()
.map_err(|e| format!("error converting supplied value for verification: {e}"));
self
}
pub fn version<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::num::NonZeroU64>,
T::Error: ::std::fmt::Display,
{
self.version = value
.try_into()
.map_err(|e| format!("error converting supplied value for version: {e}"));
self
}
}
impl ::std::convert::TryFrom<Response> for super::Response {
type Error = super::error::ConversionError;
fn try_from(value: Response) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self {
author: value.author?,
cleartext: value.cleartext?,
ext: value.ext?,
key: value.key?,
plaintext: value.plaintext?,
room_id: value.room_id?,
status: value.status?,
updated_at: value.updated_at?,
verification: value.verification?,
version: value.version?,
})
}
}
impl ::std::convert::From<super::Response> for Response {
fn from(value: super::Response) -> Self {
Self {
author: Ok(value.author),
cleartext: Ok(value.cleartext),
ext: Ok(value.ext),
key: Ok(value.key),
plaintext: Ok(value.plaintext),
room_id: Ok(value.room_id),
status: Ok(value.status),
updated_at: Ok(value.updated_at),
verification: Ok(value.verification),
version: Ok(value.version),
}
}
}
}
impl crate::Payload for Payload {
const TYPE_URI: &'static str = "https://trusttasks.org/spec/rooms/keys/read/0.1";
const IS_PROOF_REQUIRED: bool = true;
const IS_ISSUED_AT_REQUIRED: bool = true;
const IS_RECIPIENT_REQUIRED: bool = true;
const PAYLOAD_SCHEMA: Option<&'static str> = Some(
"{\n \"$defs\": {\n \"DataCommitment\": {\n \"$ref\": \"#/$defs/DigestMultibase\",\n \"description\": \"The root of the room's record tree — a host's commitment to *which records the room holds*, as distinct from what any one of them says.\\n\\nA room's records are already signed and room-bound, so a host cannot forge, alter or relocate one. What it can do for free is stay silent: a listing that omits a record is indistinguishable from a room that never held it. This value is what makes that omission detectable, so it is only worth anything when the reader can compare it against a copy the host did not choose for them — one it gave another member, one it gave the same member earlier, or the witnessed anchor. A commitment read once, in isolation, proves nothing.\\n\\n**A root on its own is not comparable, and an earlier revision of this description said it was.** It claimed a host showing two members two different roots had been caught, which is false while a room can move between two reads: the host answers *there was a write*, and nothing contradicts it. Comparison needs the state each root describes, which is what `HeadVersion` names — and `RecordCount` is the other half of the same omission, since Certificate Transparency's signed tree head is a root *and a size* and this family shipped only the root. A reader that receives a `dataCommitment` **without** them can use it against a witnessed anchor, where the epoch pins the state, and **MUST NOT** compare it against another root.\\n\\n**The construction is normative**, because two hosts that compute different roots over the same room make every comparison meaningless:\\n 1. Take every record the room holds — including tombstones, which are records — and order them by `key` using unsigned byte order.\\n 2. Leaf: `SHA-256(0x00 || JCS(record))`, where the record is a `CommittedRecord` — that definition fixes the members exactly, and this step used to name a *projection* instead, which two implementations could read two ways — JCS is its RFC 8785 canonicalization, and `0x00` is RFC 6962's leaf-domain prefix.\\n 3. Internal node: `SHA-256(0x01 || left || right)`.\\n 4. A level with an odd number of nodes promotes the last one unchanged. It MUST NOT be duplicated: duplicating makes a tree of n leaves collide with one of n+1 whose last is repeated, so two different rooms commit to the same root.\\n 5. A room holding no records commits to `SHA-256(\\\"\\\")`, a distinguished value rather than zeroes — a root of zeroes is what an uninitialised buffer looks like, and an empty room is a real state a host must be able to commit to honestly.\\n\\nThe leaf covers the whole record rather than its body, and that is deliberate: a host that could flip `status` from active to retracted, move `pinned`, or rewrite `author` on an `attributed` room would rewrite what the room means without touching a byte of ciphertext. The **plaintext is never involved** — on the sealed tiers the host holds ciphertext and commits to exactly what it stores.\\n\\nThe commitment is over the **whole room**, never over the page being returned. A page-scoped root is one a host satisfies by construction and could never fail.\\n\\nProving that a *particular* record sits under this root is a separate question, answered by `RecordTrace` on a single-record read. A commitment catches a host that equivocates; a trace binds one record to what the host committed to. Neither is the other, and a reader wanting completeness needs both plus a root it did not get from the host it is checking.\",\n \"title\": \"DataCommitment\"\n },\n \"DigestMultibase\": {\n \"description\": \"A cryptographic digest as a multibase-encoded multihash — the encoding the W3C Verifiable Credentials Data Model 2.0 defines for `digestMultibase`, and the one `did:webvh` uses for its SCID and entry hashes.\\n\\nMultihash carries the hash algorithm in-band, so the value is self-describing and the wire format survives an algorithm change without a schema revision; multibase does the same for the base encoding, so a verifier never infers base58 from base64url by context. A bare hex string or a `sha-256:`-style prefix hard-codes one algorithm into the wire contract and is non-conforming here.\\n\\nThis definition constrains the *encoding only*. What the digest is computed over is stated by each referencing field, because it differs legitimately: a digest over a JSON document is taken over its RFC 8785 (JCS) canonicalization, while a digest over an opaque artifact is taken over its bytes. A field whose input is a JSON document and which does not name a canonicalization is not reproducible.\\n\\nRestricted to the two multibase headers W3C Controlled Identifiers 1.0 §2.4 normatively requires — `z` (base58btc) and `u` (base64url-no-pad). CID permits others but states that \\\"interoperability is not guaranteed between implementations using such values\\\", and a registry whose purpose is interoperability should not mint digests a conforming verifier may be unable to read. The alphabets are enforced rather than assumed: base58btc excludes 0, O, I and l, and an earlier permissive pattern let three published examples carry digests that were not valid base58 at all. base58btc is RECOMMENDED, for consistency with `did:key` and `did:webvh`.\",\n \"examples\": [\n \"zQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR\"\n ],\n \"minLength\": 16,\n \"pattern\": \"^(z[1-9A-HJ-NP-Za-km-z]+|u[A-Za-z0-9_-]+)$\",\n \"title\": \"DigestMultibase\",\n \"type\": \"string\"\n },\n \"Ext\": {\n \"additionalProperties\": true,\n \"description\": \"Vendor-namespaced extension object per SPEC.md §4.5.1. Each immediate key MUST be a reverse-DNS namespace; structure under each namespace is opaque to the framework.\",\n \"minProperties\": 1,\n \"propertyNames\": {\n \"pattern\": \"^[a-z][a-z0-9-]*(\\\\.[a-z0-9-]+)+$\"\n },\n \"title\": \"Ext\",\n \"type\": \"object\"\n },\n \"HeadVersion\": {\n \"description\": \"The highest version among the records `DataCommitment` covers. `0` for a room that holds none.\\n\\n**Derived from the same set as the root, and not read from the room's own counter.** The two agree for any host that has never erased a record — versions are assigned strictly increasing and a retraction keeps its tombstone — but they are not interchangeable, because a root and a counter are *two reads*, and two reads are not a snapshot. A write landing between them yields a pair that is individually correct and jointly false: two members holding roots taken over different trees, labelled with one version. That reads as equivocation and is not, and a **false accusation discredits the mechanism rather than the host** — the worst outcome available here. Taken from the committed set, the version cannot disagree with the root it labels, whatever else is happening to the room.\\n\\nThe corollary is worth stating: a host that **erases** a record — as distinct from retracting it, which leaves a tombstone in the tree — moves the root without necessarily moving this value, and two members straddling that erasure would see one version over two roots. Erasure is a retention act with its own answer, and a family that exposes one owes this definition another look.\\n\\n**This is what makes two roots comparable at all**, and without it the comparison this family is built on cannot be performed. A room moves: every put, curate and retraction assigns a new version, so two roots taken at two moments differ legitimately and a reader learns nothing from the difference. Shown two different roots, a host that equivocated and a host that was merely written to are indistinguishable — the first can always answer *the room moved between your reads*, and nothing contradicts it.\\n\\nA version is assigned by exactly the mutations that change the tree — every put, curate and retraction takes the next one — so the highest of them names the **state** the root describes. Two roots carrying the same `headVersion` and differing is a host caught: there is no write to attribute the difference to. Two roots carrying different ones are simply two moments, and a reader should draw nothing from them.\\n\\nA host can lie about this number too, and it is then lying about the counter it also uses for optimistic concurrency (`expectedVersion`) and for incremental sync (`sinceVersion`) — so a member holding a signed acknowledgement of a write at version `V` contradicts any head below `V` directly.\\n\\n**Not a timestamp.** A time is host-asserted, unverifiable and useless for this: two roots a second apart are not evidence of anything, while two roots at one version are.\",\n \"minimum\": 0,\n \"title\": \"HeadVersion\",\n \"type\": \"integer\"\n },\n \"ReadVerification\": {\n \"additionalProperties\": false,\n \"description\": \"What the agent checked on the member's behalf, and what it found.\\n\\n**A verdict, never an error.** None of these values fails the task, including the ones that report a host caught out. A member's own agent refusing to hand over a record because the *host* misbehaved punishes the member for somebody else's act — and locks them out of the room holding the records that would show what happened, at the moment they most need them. The consequence belongs on the **write** path, where continuing to hand material to a party you have caught is what compounds the damage.\\n\\nSo a consumer **MUST NOT** treat any value here as a failed read, and **MUST** surface an adverse one rather than logging it. What that costs is words: a member shown a bare warning icon dismisses it, and a member later refused a write with no explanation blames their own agent. A detection the member attributes to the wrong party is worse than no detection.\\n\\nMembers are **absent where the task cannot produce them** rather than carrying a not-applicable value — a listing has no `trace` because there is no single record to trace, and a single read has no `count` because a count is only checkable against a listing read to its end.\",\n \"properties\": {\n \"anchor\": {\n \"description\": \"How what the host served compares with the room's own **witnessed anchor** (`EpochAnchor`).\\n\\nThis is the comparison that needs neither a gossip channel rooms deliberately lack nor durable state in an agent: every member resolves the same room DID and reads the same entry, co-signed by witnesses. It is the only one of the three a first-time reader can make.\\n\\n - `agrees` — the host served the anchored state, and its root matches.\\n - `ahead` — the room has moved past the anchor. The ordinary case; an anchor describes a moment, not the present, and says nothing about records written since.\\n - `behind` — **the host is serving a state older than the room's own witnessed statement.** A rollback, and a detection nothing else in this family can make: a member with no history, no peer and no prior read still catches it.\\n - `conflict` — same `headVersion` as the anchor, different root. The host has contradicted a value its own room published and witnesses co-signed.\\n - `none` — the room has published no anchor. Not a fault; anchoring costs a witnessed update and a key rotation, and a room may reasonably decline.\\n - `notChecked` — the consumer did not resolve the room. An honest answer, and **not** a synonym for `none`: one says the room published nothing, the other says nobody looked.\",\n \"enum\": [\n \"agrees\",\n \"ahead\",\n \"behind\",\n \"conflict\",\n \"none\",\n \"notChecked\"\n ],\n \"type\": \"string\"\n },\n \"count\": {\n \"description\": \"Whether the number of records returned matches the `recordCount` the host committed to.\\n\\nOnly ever comparable against a listing with **no** `prefix`, **no** `sinceVersion` and read to its end — anything else legitimately holds fewer, and comparing it is a discrepancy the reader manufactured. `notComparable` is that case, and it is the common one.\\n\\n`short` is a host contradicting itself inside one exchange: it committed to a tree of N records and served fewer, with no filter to explain the difference. That is the omission the commitment exists to make detectable, caught without a second party and without an anchor.\",\n \"enum\": [\n \"agrees\",\n \"short\",\n \"notComparable\",\n \"notOffered\"\n ],\n \"type\": \"string\"\n },\n \"head\": {\n \"additionalProperties\": false,\n \"description\": \"What the host asserted about the room, passed through unaltered so the member can compare it somewhere this agent cannot reach — with another member, or against a witnessed anchor. All three or none: a root without the state it describes is not comparable to another root, which is the whole of `HeadVersion`.\",\n \"properties\": {\n \"dataCommitment\": {\n \"$ref\": \"#/$defs/DataCommitment\"\n },\n \"headVersion\": {\n \"$ref\": \"#/$defs/HeadVersion\"\n },\n \"recordCount\": {\n \"$ref\": \"#/$defs/RecordCount\"\n }\n },\n \"required\": [\n \"dataCommitment\",\n \"recordCount\",\n \"headVersion\"\n ],\n \"type\": \"object\"\n },\n \"priorRoots\": {\n \"description\": \"Whether this root matches what the agent has seen from this host for this room **at this `headVersion`**.\\n\\nThis is the comparison a member cannot make for themselves. A tab does not outlive itself and a CLI holds nothing; the agent is the only party on the member's side of the boundary that saw both reads.\\n\\n - `agree` — seen at this head before, same root.\\n - `conflict` — seen at this head before, **different root**. A host caught: there is no write to attribute the difference to, because a write would have moved the head.\\n - `noneHeld` — first read at this head. Not evidence of anything; a memory of one is not a comparison.\\n - `notChecked` — this agent keeps no root history. An honest answer for an agent that cannot make the comparison, and **not** a synonym for `noneHeld`: one says nothing was found, the other says nothing was looked for.\\n\\nREQUIRED, so that an agent which does not check has to say so rather than omit the question.\",\n \"enum\": [\n \"agree\",\n \"conflict\",\n \"noneHeld\",\n \"notChecked\"\n ],\n \"type\": \"string\"\n },\n \"trace\": {\n \"description\": \"Whether the record's `RecordTrace` reached the `dataCommitment` served beside it.\\n\\n`verified` says the record is under the root the host asserted — and **nothing about whether that root is the room's**, which is what `priorRoots` and an anchor are for. `notOffered` is a host that maintains no tree, which is legal and informative. `failed` is arithmetic that does not close: the host served a path that does not reach its own root, which is either a defect or a fabrication and is not distinguishable from here.\",\n \"enum\": [\n \"verified\",\n \"failed\",\n \"notOffered\"\n ],\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"priorRoots\"\n ],\n \"title\": \"ReadVerification\",\n \"type\": \"object\"\n },\n \"RecordCount\": {\n \"description\": \"How many records the room held when `DataCommitment` was computed — the number of leaves in that tree, tombstones included, since a tombstone is a record.\\n\\n**Why a root needs this.** Certificate Transparency's signed tree head is a root *and a tree size*; this family shipped the root alone, and the half that was dropped is the half that makes a listing checkable. A reader holding a **complete, unfiltered** listing cannot recompute the root — a leaf commits to a whole record and a listing returns a projection without the body — but it can count. A host that omits a record from a listing while committing to a tree that holds it now contradicts itself in the same response, with no second party and no anchor involved.\\n\\nA host can of course understate both together. That is the point rather than a hole: the omission stops being silence and becomes a **specific claim about how many records the room holds**, which any other member's view, or any writer's signed put acknowledgement, contradicts. Making an omission attributable is the whole of what this machinery buys; it never claimed to make one impossible.\\n\\n**It counts the room, never the page.** The same rule `DataCommitment` states, and the same trap: a count scoped to what was returned is one a host satisfies by construction. So this is only comparable against a listing read to the end with **no** `prefix` and **no** `sinceVersion` — a filtered listing legitimately holds fewer, and a reader that compares one against this has found a discrepancy it created itself.\",\n \"minimum\": 0,\n \"title\": \"RecordCount\",\n \"type\": \"integer\"\n },\n \"Response\": {\n \"$anchor\": \"response\",\n \"additionalProperties\": false,\n \"description\": \"Success response to rooms/keys/read. Type https://trusttasks.org/spec/rooms/keys/read/0.1#response.\",\n \"properties\": {\n \"author\": {\n \"description\": \"The member who wrote it, where the tier discloses one. Absent on `private`, where authorship is inside the body the recipient just opened.\",\n \"type\": \"string\"\n },\n \"cleartext\": {\n \"additionalProperties\": true,\n \"description\": \"The record body on an `open` room, where there is nothing to open. Carried as itself rather than base64url so that the shape says which tier the member is on — a member ought to be able to tell that this room's host can read what they just read.\",\n \"type\": \"object\"\n },\n \"ext\": {\n \"$ref\": \"#/$defs/Ext\"\n },\n \"key\": {\n \"type\": \"string\"\n },\n \"plaintext\": {\n \"description\": \"The opened record, base64url — the sealed tiers. **Never the key**, which is the whole reason this task exists rather than the record being handed to the member to open. Spelled as `rooms/keys/open` spells it, because it is the same bytes by the same route.\",\n \"type\": \"string\"\n },\n \"roomId\": {\n \"type\": \"string\"\n },\n \"status\": {\n \"description\": \"Curation state, passed through. A `retracted` record has no body: the tombstone is the answer, not a failure.\",\n \"enum\": [\n \"active\",\n \"deprecated\",\n \"retracted\"\n ],\n \"type\": \"string\"\n },\n \"updatedAt\": {\n \"format\": \"date-time\",\n \"type\": \"string\"\n },\n \"verification\": {\n \"$ref\": \"#/$defs/ReadVerification\",\n \"description\": \"What the recipient checked and what it found. REQUIRED: an agent that returns a record without saying what it checked has made the member's decision for them.\"\n },\n \"version\": {\n \"description\": \"The record's version at the host.\",\n \"minimum\": 1,\n \"type\": \"integer\"\n }\n },\n \"required\": [\n \"roomId\",\n \"key\",\n \"version\",\n \"verification\"\n ],\n \"title\": \"Rooms Keys Read — response payload\",\n \"type\": \"object\"\n }\n },\n \"$id\": \"https://trusttasks.org/spec/rooms/keys/read/0.1\",\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"additionalProperties\": false,\n \"description\": \"TODO: what the request payload of rooms/keys/read carries. The outer document members (id, type, issuer, recipient, issuedAt, expiresAt, proof) are owned by the framework — SPEC §6.3.\",\n \"properties\": {\n \"ext\": {\n \"$ref\": \"#/$defs/Ext\",\n \"description\": \"Ecosystem-defined extension members per SPEC.md §4.5.1.\"\n },\n \"host\": {\n \"description\": \"The host to read from, as a DID. Named by the caller because nothing maps a room to its host — a room is portable, so a remembered host goes stale silently, and a room may be registered with more than one. A caller who names the wrong host learns so as a refusal from a party that does not serve this room, which is loud and immediate.\",\n \"type\": \"string\"\n },\n \"key\": {\n \"description\": \"The record to read.\",\n \"maxLength\": 512,\n \"type\": \"string\"\n },\n \"roomId\": {\n \"description\": \"The room to read from. The recipient MUST already hold group state for it.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"roomId\",\n \"host\",\n \"key\"\n ],\n \"title\": \"Rooms Keys Read — payload\",\n \"type\": \"object\"\n}\n",
);
}
impl crate::Payload for Response {
const TYPE_URI: &'static str = "https://trusttasks.org/spec/rooms/keys/read/0.1#response";
const IS_PROOF_REQUIRED: bool = true;
const IS_ISSUED_AT_REQUIRED: bool = true;
const IS_RECIPIENT_REQUIRED: bool = true;
const PAYLOAD_SCHEMA: Option<&'static str> = Some(
"{\n \"$defs\": {\n \"DataCommitment\": {\n \"$ref\": \"#/$defs/DigestMultibase\",\n \"description\": \"The root of the room's record tree — a host's commitment to *which records the room holds*, as distinct from what any one of them says.\\n\\nA room's records are already signed and room-bound, so a host cannot forge, alter or relocate one. What it can do for free is stay silent: a listing that omits a record is indistinguishable from a room that never held it. This value is what makes that omission detectable, so it is only worth anything when the reader can compare it against a copy the host did not choose for them — one it gave another member, one it gave the same member earlier, or the witnessed anchor. A commitment read once, in isolation, proves nothing.\\n\\n**A root on its own is not comparable, and an earlier revision of this description said it was.** It claimed a host showing two members two different roots had been caught, which is false while a room can move between two reads: the host answers *there was a write*, and nothing contradicts it. Comparison needs the state each root describes, which is what `HeadVersion` names — and `RecordCount` is the other half of the same omission, since Certificate Transparency's signed tree head is a root *and a size* and this family shipped only the root. A reader that receives a `dataCommitment` **without** them can use it against a witnessed anchor, where the epoch pins the state, and **MUST NOT** compare it against another root.\\n\\n**The construction is normative**, because two hosts that compute different roots over the same room make every comparison meaningless:\\n 1. Take every record the room holds — including tombstones, which are records — and order them by `key` using unsigned byte order.\\n 2. Leaf: `SHA-256(0x00 || JCS(record))`, where the record is a `CommittedRecord` — that definition fixes the members exactly, and this step used to name a *projection* instead, which two implementations could read two ways — JCS is its RFC 8785 canonicalization, and `0x00` is RFC 6962's leaf-domain prefix.\\n 3. Internal node: `SHA-256(0x01 || left || right)`.\\n 4. A level with an odd number of nodes promotes the last one unchanged. It MUST NOT be duplicated: duplicating makes a tree of n leaves collide with one of n+1 whose last is repeated, so two different rooms commit to the same root.\\n 5. A room holding no records commits to `SHA-256(\\\"\\\")`, a distinguished value rather than zeroes — a root of zeroes is what an uninitialised buffer looks like, and an empty room is a real state a host must be able to commit to honestly.\\n\\nThe leaf covers the whole record rather than its body, and that is deliberate: a host that could flip `status` from active to retracted, move `pinned`, or rewrite `author` on an `attributed` room would rewrite what the room means without touching a byte of ciphertext. The **plaintext is never involved** — on the sealed tiers the host holds ciphertext and commits to exactly what it stores.\\n\\nThe commitment is over the **whole room**, never over the page being returned. A page-scoped root is one a host satisfies by construction and could never fail.\\n\\nProving that a *particular* record sits under this root is a separate question, answered by `RecordTrace` on a single-record read. A commitment catches a host that equivocates; a trace binds one record to what the host committed to. Neither is the other, and a reader wanting completeness needs both plus a root it did not get from the host it is checking.\",\n \"title\": \"DataCommitment\"\n },\n \"DigestMultibase\": {\n \"description\": \"A cryptographic digest as a multibase-encoded multihash — the encoding the W3C Verifiable Credentials Data Model 2.0 defines for `digestMultibase`, and the one `did:webvh` uses for its SCID and entry hashes.\\n\\nMultihash carries the hash algorithm in-band, so the value is self-describing and the wire format survives an algorithm change without a schema revision; multibase does the same for the base encoding, so a verifier never infers base58 from base64url by context. A bare hex string or a `sha-256:`-style prefix hard-codes one algorithm into the wire contract and is non-conforming here.\\n\\nThis definition constrains the *encoding only*. What the digest is computed over is stated by each referencing field, because it differs legitimately: a digest over a JSON document is taken over its RFC 8785 (JCS) canonicalization, while a digest over an opaque artifact is taken over its bytes. A field whose input is a JSON document and which does not name a canonicalization is not reproducible.\\n\\nRestricted to the two multibase headers W3C Controlled Identifiers 1.0 §2.4 normatively requires — `z` (base58btc) and `u` (base64url-no-pad). CID permits others but states that \\\"interoperability is not guaranteed between implementations using such values\\\", and a registry whose purpose is interoperability should not mint digests a conforming verifier may be unable to read. The alphabets are enforced rather than assumed: base58btc excludes 0, O, I and l, and an earlier permissive pattern let three published examples carry digests that were not valid base58 at all. base58btc is RECOMMENDED, for consistency with `did:key` and `did:webvh`.\",\n \"examples\": [\n \"zQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR\"\n ],\n \"minLength\": 16,\n \"pattern\": \"^(z[1-9A-HJ-NP-Za-km-z]+|u[A-Za-z0-9_-]+)$\",\n \"title\": \"DigestMultibase\",\n \"type\": \"string\"\n },\n \"Ext\": {\n \"additionalProperties\": true,\n \"description\": \"Vendor-namespaced extension object per SPEC.md §4.5.1. Each immediate key MUST be a reverse-DNS namespace; structure under each namespace is opaque to the framework.\",\n \"minProperties\": 1,\n \"propertyNames\": {\n \"pattern\": \"^[a-z][a-z0-9-]*(\\\\.[a-z0-9-]+)+$\"\n },\n \"title\": \"Ext\",\n \"type\": \"object\"\n },\n \"HeadVersion\": {\n \"description\": \"The highest version among the records `DataCommitment` covers. `0` for a room that holds none.\\n\\n**Derived from the same set as the root, and not read from the room's own counter.** The two agree for any host that has never erased a record — versions are assigned strictly increasing and a retraction keeps its tombstone — but they are not interchangeable, because a root and a counter are *two reads*, and two reads are not a snapshot. A write landing between them yields a pair that is individually correct and jointly false: two members holding roots taken over different trees, labelled with one version. That reads as equivocation and is not, and a **false accusation discredits the mechanism rather than the host** — the worst outcome available here. Taken from the committed set, the version cannot disagree with the root it labels, whatever else is happening to the room.\\n\\nThe corollary is worth stating: a host that **erases** a record — as distinct from retracting it, which leaves a tombstone in the tree — moves the root without necessarily moving this value, and two members straddling that erasure would see one version over two roots. Erasure is a retention act with its own answer, and a family that exposes one owes this definition another look.\\n\\n**This is what makes two roots comparable at all**, and without it the comparison this family is built on cannot be performed. A room moves: every put, curate and retraction assigns a new version, so two roots taken at two moments differ legitimately and a reader learns nothing from the difference. Shown two different roots, a host that equivocated and a host that was merely written to are indistinguishable — the first can always answer *the room moved between your reads*, and nothing contradicts it.\\n\\nA version is assigned by exactly the mutations that change the tree — every put, curate and retraction takes the next one — so the highest of them names the **state** the root describes. Two roots carrying the same `headVersion` and differing is a host caught: there is no write to attribute the difference to. Two roots carrying different ones are simply two moments, and a reader should draw nothing from them.\\n\\nA host can lie about this number too, and it is then lying about the counter it also uses for optimistic concurrency (`expectedVersion`) and for incremental sync (`sinceVersion`) — so a member holding a signed acknowledgement of a write at version `V` contradicts any head below `V` directly.\\n\\n**Not a timestamp.** A time is host-asserted, unverifiable and useless for this: two roots a second apart are not evidence of anything, while two roots at one version are.\",\n \"minimum\": 0,\n \"title\": \"HeadVersion\",\n \"type\": \"integer\"\n },\n \"ReadVerification\": {\n \"additionalProperties\": false,\n \"description\": \"What the agent checked on the member's behalf, and what it found.\\n\\n**A verdict, never an error.** None of these values fails the task, including the ones that report a host caught out. A member's own agent refusing to hand over a record because the *host* misbehaved punishes the member for somebody else's act — and locks them out of the room holding the records that would show what happened, at the moment they most need them. The consequence belongs on the **write** path, where continuing to hand material to a party you have caught is what compounds the damage.\\n\\nSo a consumer **MUST NOT** treat any value here as a failed read, and **MUST** surface an adverse one rather than logging it. What that costs is words: a member shown a bare warning icon dismisses it, and a member later refused a write with no explanation blames their own agent. A detection the member attributes to the wrong party is worse than no detection.\\n\\nMembers are **absent where the task cannot produce them** rather than carrying a not-applicable value — a listing has no `trace` because there is no single record to trace, and a single read has no `count` because a count is only checkable against a listing read to its end.\",\n \"properties\": {\n \"anchor\": {\n \"description\": \"How what the host served compares with the room's own **witnessed anchor** (`EpochAnchor`).\\n\\nThis is the comparison that needs neither a gossip channel rooms deliberately lack nor durable state in an agent: every member resolves the same room DID and reads the same entry, co-signed by witnesses. It is the only one of the three a first-time reader can make.\\n\\n - `agrees` — the host served the anchored state, and its root matches.\\n - `ahead` — the room has moved past the anchor. The ordinary case; an anchor describes a moment, not the present, and says nothing about records written since.\\n - `behind` — **the host is serving a state older than the room's own witnessed statement.** A rollback, and a detection nothing else in this family can make: a member with no history, no peer and no prior read still catches it.\\n - `conflict` — same `headVersion` as the anchor, different root. The host has contradicted a value its own room published and witnesses co-signed.\\n - `none` — the room has published no anchor. Not a fault; anchoring costs a witnessed update and a key rotation, and a room may reasonably decline.\\n - `notChecked` — the consumer did not resolve the room. An honest answer, and **not** a synonym for `none`: one says the room published nothing, the other says nobody looked.\",\n \"enum\": [\n \"agrees\",\n \"ahead\",\n \"behind\",\n \"conflict\",\n \"none\",\n \"notChecked\"\n ],\n \"type\": \"string\"\n },\n \"count\": {\n \"description\": \"Whether the number of records returned matches the `recordCount` the host committed to.\\n\\nOnly ever comparable against a listing with **no** `prefix`, **no** `sinceVersion` and read to its end — anything else legitimately holds fewer, and comparing it is a discrepancy the reader manufactured. `notComparable` is that case, and it is the common one.\\n\\n`short` is a host contradicting itself inside one exchange: it committed to a tree of N records and served fewer, with no filter to explain the difference. That is the omission the commitment exists to make detectable, caught without a second party and without an anchor.\",\n \"enum\": [\n \"agrees\",\n \"short\",\n \"notComparable\",\n \"notOffered\"\n ],\n \"type\": \"string\"\n },\n \"head\": {\n \"additionalProperties\": false,\n \"description\": \"What the host asserted about the room, passed through unaltered so the member can compare it somewhere this agent cannot reach — with another member, or against a witnessed anchor. All three or none: a root without the state it describes is not comparable to another root, which is the whole of `HeadVersion`.\",\n \"properties\": {\n \"dataCommitment\": {\n \"$ref\": \"#/$defs/DataCommitment\"\n },\n \"headVersion\": {\n \"$ref\": \"#/$defs/HeadVersion\"\n },\n \"recordCount\": {\n \"$ref\": \"#/$defs/RecordCount\"\n }\n },\n \"required\": [\n \"dataCommitment\",\n \"recordCount\",\n \"headVersion\"\n ],\n \"type\": \"object\"\n },\n \"priorRoots\": {\n \"description\": \"Whether this root matches what the agent has seen from this host for this room **at this `headVersion`**.\\n\\nThis is the comparison a member cannot make for themselves. A tab does not outlive itself and a CLI holds nothing; the agent is the only party on the member's side of the boundary that saw both reads.\\n\\n - `agree` — seen at this head before, same root.\\n - `conflict` — seen at this head before, **different root**. A host caught: there is no write to attribute the difference to, because a write would have moved the head.\\n - `noneHeld` — first read at this head. Not evidence of anything; a memory of one is not a comparison.\\n - `notChecked` — this agent keeps no root history. An honest answer for an agent that cannot make the comparison, and **not** a synonym for `noneHeld`: one says nothing was found, the other says nothing was looked for.\\n\\nREQUIRED, so that an agent which does not check has to say so rather than omit the question.\",\n \"enum\": [\n \"agree\",\n \"conflict\",\n \"noneHeld\",\n \"notChecked\"\n ],\n \"type\": \"string\"\n },\n \"trace\": {\n \"description\": \"Whether the record's `RecordTrace` reached the `dataCommitment` served beside it.\\n\\n`verified` says the record is under the root the host asserted — and **nothing about whether that root is the room's**, which is what `priorRoots` and an anchor are for. `notOffered` is a host that maintains no tree, which is legal and informative. `failed` is arithmetic that does not close: the host served a path that does not reach its own root, which is either a defect or a fabrication and is not distinguishable from here.\",\n \"enum\": [\n \"verified\",\n \"failed\",\n \"notOffered\"\n ],\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"priorRoots\"\n ],\n \"title\": \"ReadVerification\",\n \"type\": \"object\"\n },\n \"RecordCount\": {\n \"description\": \"How many records the room held when `DataCommitment` was computed — the number of leaves in that tree, tombstones included, since a tombstone is a record.\\n\\n**Why a root needs this.** Certificate Transparency's signed tree head is a root *and a tree size*; this family shipped the root alone, and the half that was dropped is the half that makes a listing checkable. A reader holding a **complete, unfiltered** listing cannot recompute the root — a leaf commits to a whole record and a listing returns a projection without the body — but it can count. A host that omits a record from a listing while committing to a tree that holds it now contradicts itself in the same response, with no second party and no anchor involved.\\n\\nA host can of course understate both together. That is the point rather than a hole: the omission stops being silence and becomes a **specific claim about how many records the room holds**, which any other member's view, or any writer's signed put acknowledgement, contradicts. Making an omission attributable is the whole of what this machinery buys; it never claimed to make one impossible.\\n\\n**It counts the room, never the page.** The same rule `DataCommitment` states, and the same trap: a count scoped to what was returned is one a host satisfies by construction. So this is only comparable against a listing read to the end with **no** `prefix` and **no** `sinceVersion` — a filtered listing legitimately holds fewer, and a reader that compares one against this has found a discrepancy it created itself.\",\n \"minimum\": 0,\n \"title\": \"RecordCount\",\n \"type\": \"integer\"\n },\n \"Response\": {\n \"$anchor\": \"response\",\n \"additionalProperties\": false,\n \"description\": \"Success response to rooms/keys/read. Type https://trusttasks.org/spec/rooms/keys/read/0.1#response.\",\n \"properties\": {\n \"author\": {\n \"description\": \"The member who wrote it, where the tier discloses one. Absent on `private`, where authorship is inside the body the recipient just opened.\",\n \"type\": \"string\"\n },\n \"cleartext\": {\n \"additionalProperties\": true,\n \"description\": \"The record body on an `open` room, where there is nothing to open. Carried as itself rather than base64url so that the shape says which tier the member is on — a member ought to be able to tell that this room's host can read what they just read.\",\n \"type\": \"object\"\n },\n \"ext\": {\n \"$ref\": \"#/$defs/Ext\"\n },\n \"key\": {\n \"type\": \"string\"\n },\n \"plaintext\": {\n \"description\": \"The opened record, base64url — the sealed tiers. **Never the key**, which is the whole reason this task exists rather than the record being handed to the member to open. Spelled as `rooms/keys/open` spells it, because it is the same bytes by the same route.\",\n \"type\": \"string\"\n },\n \"roomId\": {\n \"type\": \"string\"\n },\n \"status\": {\n \"description\": \"Curation state, passed through. A `retracted` record has no body: the tombstone is the answer, not a failure.\",\n \"enum\": [\n \"active\",\n \"deprecated\",\n \"retracted\"\n ],\n \"type\": \"string\"\n },\n \"updatedAt\": {\n \"format\": \"date-time\",\n \"type\": \"string\"\n },\n \"verification\": {\n \"$ref\": \"#/$defs/ReadVerification\",\n \"description\": \"What the recipient checked and what it found. REQUIRED: an agent that returns a record without saying what it checked has made the member's decision for them.\"\n },\n \"version\": {\n \"description\": \"The record's version at the host.\",\n \"minimum\": 1,\n \"type\": \"integer\"\n }\n },\n \"required\": [\n \"roomId\",\n \"key\",\n \"version\",\n \"verification\"\n ],\n \"title\": \"Rooms Keys Read — response payload\",\n \"type\": \"object\"\n }\n },\n \"$ref\": \"#/$defs/Response\",\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\"\n}\n",
);
}
impl crate::RequestPayload for Payload {
type Response = Response;
}
/// The extended error codes this specification declares (SPEC §7.3 item 9,
/// §8.5), in declaration order. Empty when it declares none.
pub const ERROR_CODES: &[crate::DeclaredErrorCode] = &[
error_codes::NOT_A_MEMBER,
error_codes::HOST_UNREACHABLE,
error_codes::HOST_REFUSED,
error_codes::CANNOT_OPEN,
];
/// One constant per extended error code this specification declares
/// (SPEC §7.3 item 9), named for its local part.
///
/// Emit these rather than a string literal: the code is read from the
/// specification, so it cannot name a code the specification never
/// declared.
pub mod error_codes {
/// `rooms/keys/read:notAMember`
///
/// The recipient holds no group state for this room, so it has nothing to present and nothing to open with.
///
/// Declared `retryable: false`.
pub const NOT_A_MEMBER: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "rooms/keys/read:notAMember",
retryable: false,
};
/// `rooms/keys/read:hostUnreachable`
///
/// The named host could not be resolved, advertises no transport this recipient speaks, or did not answer.
///
/// Declared `retryable: true`.
pub const HOST_UNREACHABLE: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "rooms/keys/read:hostUnreachable",
retryable: true,
};
/// `rooms/keys/read:hostRefused`
///
/// The host answered and declined. Its own code and reason are carried in `details` — commonly that it does not serve this room, or that no record has this key.
///
/// Declared `retryable: false`.
pub const HOST_REFUSED: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "rooms/keys/read:hostRefused",
retryable: false,
};
/// `rooms/keys/read:cannotOpen`
///
/// The record was fetched and its epoch key is not held. `details.epoch` names the epoch; `rooms/keys/backfill` is the repair.
///
/// Declared `retryable: false`.
pub const CANNOT_OPEN: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "rooms/keys/read:cannotOpen",
retryable: false,
};
}
#[cfg(test)]
mod conformance {
//! Round-trip tests harvested from the spec's `spec.md`,
//! plus a `rejects_invalid_examples` test for any fixtures
//! in `payload.invalid-examples.json` (validate feature).
#[test]
fn request_example_1() {
const JSON: &str = "{\n \"id\": \"urn:uuid:00000000-0000-4000-8000-000000000001\",\n \"type\": \"https://trusttasks.org/spec/rooms/keys/read/0.1#request\",\n \"issuer\": \"did:example:member\",\n \"recipient\": \"did:example:keyholder\",\n \"issuedAt\": \"2026-01-01T00:00:00Z\",\n \"threadId\": \"urn:uuid:00000000-0000-4000-8000-0000000000ff\",\n \"payload\": {\n \"roomId\": \"did:webvh:example.com:rooms:northwind\",\n \"host\": \"did:webvh:example.com:northwind-community\",\n \"key\": \"giXFLTGBdnnQJRoIsktuIg\"\n }\n}\n";
let doc: crate::TrustTask<super::Payload> =
serde_json::from_str(JSON).expect("deserialize request example");
let rendered = serde_json::to_value(&doc).expect("re-serialize");
let expected: serde_json::Value = serde_json::from_str(JSON).expect("re-parse expected");
assert_eq!(rendered, expected, "request example failed round-trip");
}
#[test]
fn response_example_1() {
const JSON: &str = "{\n \"id\": \"urn:uuid:00000000-0000-4000-8000-000000000002\",\n \"type\": \"https://trusttasks.org/spec/rooms/keys/read/0.1#response\",\n \"issuer\": \"did:example:keyholder\",\n \"recipient\": \"did:example:member\",\n \"issuedAt\": \"2026-01-01T00:00:02Z\",\n \"threadId\": \"urn:uuid:00000000-0000-4000-8000-0000000000ff\",\n \"payload\": {\n \"roomId\": \"did:webvh:example.com:rooms:northwind\",\n \"key\": \"giXFLTGBdnnQJRoIsktuIg\",\n \"version\": 412,\n \"status\": \"active\",\n \"updatedAt\": \"2026-01-01T00:00:00Z\",\n \"plaintext\": \"eyJ0aXRsZSI6IlByaWNpbmcgaG9sZHMifQ\",\n \"verification\": {\n \"trace\": \"verified\",\n \"priorRoots\": \"agree\",\n \"head\": {\n \"dataCommitment\": \"zQmbWqxBEKC3P8tqsKc98xmWNzrzDtRLMiMPL8wBuTGsMnR\",\n \"recordCount\": 118,\n \"headVersion\": 412\n }\n }\n }\n}\n";
let doc: crate::TrustTask<super::Response> =
serde_json::from_str(JSON).expect("deserialize response example");
let rendered = serde_json::to_value(&doc).expect("re-serialize");
let expected: serde_json::Value = serde_json::from_str(JSON).expect("re-parse expected");
assert_eq!(rendered, expected, "response example failed round-trip");
}
#[test]
fn response_example_2() {
const JSON: &str = "{\n \"id\": \"urn:uuid:00000000-0000-4000-8000-000000000003\",\n \"type\": \"https://trusttasks.org/spec/rooms/keys/read/0.1#response\",\n \"issuer\": \"did:example:keyholder\",\n \"recipient\": \"did:example:member\",\n \"issuedAt\": \"2026-01-01T00:00:02Z\",\n \"threadId\": \"urn:uuid:00000000-0000-4000-8000-0000000000ff\",\n \"payload\": {\n \"roomId\": \"did:webvh:example.com:rooms:northwind\",\n \"key\": \"giXFLTGBdnnQJRoIsktuIg\",\n \"version\": 412,\n \"status\": \"active\",\n \"updatedAt\": \"2026-01-01T00:00:00Z\",\n \"plaintext\": \"eyJ0aXRsZSI6IlByaWNpbmcgaG9sZHMifQ\",\n \"verification\": {\n \"trace\": \"verified\",\n \"priorRoots\": \"conflict\",\n \"head\": {\n \"dataCommitment\": \"zQmXo1sV5aJ7bT2kQdF9wRnPzYcH4uMgLtEjV6NrBqWsDpK\",\n \"recordCount\": 117,\n \"headVersion\": 412\n }\n }\n }\n}\n";
let doc: crate::TrustTask<super::Response> =
serde_json::from_str(JSON).expect("deserialize response example");
let rendered = serde_json::to_value(&doc).expect("re-serialize");
let expected: serde_json::Value = serde_json::from_str(JSON).expect("re-parse expected");
assert_eq!(rendered, expected, "response example failed round-trip");
}
}