//! Generated by `trust-tasks-codegen` — do not edit by hand.
//!
//! Spec slug: `rooms/epoch/mint`. 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())
}
}
}
///What a party presents to act on a room. Carries the whole authority chain: a host MUST NOT dereference an authority credential's `parent` to fetch a link it was not given. Resolving over the network would make verification depend on availability, turn every identifier into a request the host can be induced to make against an address the holder chooses, and signal credential use to whoever hosts the identifier. A host MUST bind the presenter to the chain's leaf. A chain that verifies is evidence that authority was conferred on somebody; it is not evidence that the party presenting it is that somebody. The leaf's subject MUST equal the party the host authenticated for this request — an identity the transport established or a document `proof` proved, never one named in a payload. A host that omits this check authorizes every captured presentation, and the omission is silent, because the chain still verifies.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "AuthorityPresentation",
/// "description": "What a party presents to act on a room. Carries the whole authority chain: a host MUST NOT dereference an authority credential's `parent` to fetch a link it was not given. Resolving over the network would make verification depend on availability, turn every identifier into a request the host can be induced to make against an address the holder chooses, and signal credential use to whoever hosts the identifier. A host MUST bind the presenter to the chain's leaf. A chain that verifies is evidence that authority was conferred on somebody; it is not evidence that the party presenting it is that somebody. The leaf's subject MUST equal the party the host authenticated for this request — an identity the transport established or a document `proof` proved, never one named in a payload. A host that omits this check authorizes every captured presentation, and the omission is silent, because the chain still verifies.",
/// "type": "object",
/// "required": [
/// "authority",
/// "membership"
/// ],
/// "properties": {
/// "authority": {
/// "description": "The authority chain, LEAF FIRST: the first element is the credential being relied on and the last MUST be one issued by the room itself. Every link the presenter relies on is present, because the host will not fetch one. Capped at 8: verification is linear in chain length and runs on every operation, so an unbounded chain is a denial-of-service surface against the host. The known uses need 2 to 3 — a person attenuating to an agent, and that agent to a sub-agent.",
/// "type": "array",
/// "items": {
/// "type": "string"
/// },
/// "maxItems": 8,
/// "minItems": 1
/// },
/// "membership": {
/// "description": "The presenter's membership credential for this room, or — on a `private` room — a zero-knowledge presentation of it. Serialized per the governing profile.",
/// "type": "string"
/// },
/// "subjectBinding": {
/// "description": "REQUIRED on a `private` room, where the subject identifier is withheld: a proof that the membership credential and the authority chain's leaf describe the SAME subject. Without it two parties pool credentials — one contributes membership, the other authority — and the combination verifies as a single party holding both. A host MUST refuse a private-room presentation that omits this.",
/// "type": "string"
/// }
/// },
/// "additionalProperties": false
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct AuthorityPresentation {
///The authority chain, LEAF FIRST: the first element is the credential being relied on and the last MUST be one issued by the room itself. Every link the presenter relies on is present, because the host will not fetch one. Capped at 8: verification is linear in chain length and runs on every operation, so an unbounded chain is a denial-of-service surface against the host. The known uses need 2 to 3 — a person attenuating to an agent, and that agent to a sub-agent.
pub authority: ::std::vec::Vec<::std::string::String>,
///The presenter's membership credential for this room, or — on a `private` room — a zero-knowledge presentation of it. Serialized per the governing profile.
pub membership: ::std::string::String,
///REQUIRED on a `private` room, where the subject identifier is withheld: a proof that the membership credential and the authority chain's leaf describe the SAME subject. Without it two parties pool credentials — one contributes membership, the other authority — and the combination verifies as a single party holding both. A host MUST refuse a private-room presentation that omits this.
#[serde(
rename = "subjectBinding",
default,
skip_serializing_if = "::std::option::Option::is_none"
)]
pub subject_binding: ::std::option::Option<::std::string::String>,
}
impl AuthorityPresentation {
pub fn builder() -> builder::AuthorityPresentation {
Default::default()
}
}
///One rung of a room's epoch key chain: the storage key of epoch `epoch - 1`, sealed under the storage key of `epoch`. A group key schedule offers no way to derive an earlier epoch's key from a later one — that property is what makes removing a member mean something — so without a chain the first membership change makes every record already in the room unopenable by everyone, including whoever wrote it. The chain is the one-way street run deliberately the other way: a member holding the current key walks it backwards to any retained epoch, and a member holding an earlier key still derives nothing later. Removal stays forward-only; reading stays possible. What a chain costs is stated where it is chosen, in the room's retention policy.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "EpochLink",
/// "description": "One rung of a room's epoch key chain: the storage key of epoch `epoch - 1`, sealed under the storage key of `epoch`. A group key schedule offers no way to derive an earlier epoch's key from a later one — that property is what makes removing a member mean something — so without a chain the first membership change makes every record already in the room unopenable by everyone, including whoever wrote it. The chain is the one-way street run deliberately the other way: a member holding the current key walks it backwards to any retained epoch, and a member holding an earlier key still derives nothing later. Removal stays forward-only; reading stays possible. What a chain costs is stated where it is chosen, in the room's retention policy.",
/// "type": "object",
/// "required": [
/// "epoch",
/// "nonce",
/// "wrapped"
/// ],
/// "properties": {
/// "epoch": {
/// "description": "The epoch whose storage key opens this link; it wraps the storage key of `epoch - 1`. Never 1: a room's first epoch has no predecessor, so a link claiming one wraps something that is not an earlier epoch's key.",
/// "type": "integer",
/// "minimum": 2.0
/// },
/// "nonce": {
/// "description": "AEAD nonce, base64url.",
/// "type": "string"
/// },
/// "wrapped": {
/// "description": "The wrapped predecessor key, base64url. Bound by AEAD associated data to `roomId` and to this link's own position in the chain, so a link lifted to another rung, or served under another room, fails to open rather than yielding a key that is wrong. The binding is load-bearing rather than decorative: every rung is a fixed-length key sealed under a fixed-length key, so nothing about the ciphertext itself says where it belongs.",
/// "type": "string"
/// }
/// },
/// "additionalProperties": false
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct EpochLink {
///The epoch whose storage key opens this link; it wraps the storage key of `epoch - 1`. Never 1: a room's first epoch has no predecessor, so a link claiming one wraps something that is not an earlier epoch's key.
pub epoch: i64,
///AEAD nonce, base64url.
pub nonce: ::std::string::String,
///The wrapped predecessor key, base64url. Bound by AEAD associated data to `roomId` and to this link's own position in the chain, so a link lifted to another rung, or served under another room, fails to open rather than yielding a key that is wrong. The binding is load-bearing rather than decorative: every rung is a fixed-length key sealed under a fixed-length key, so nothing about the ciphertext itself says where it belongs.
pub wrapped: ::std::string::String,
}
impl EpochLink {
pub fn builder() -> builder::EpochLink {
Default::default()
}
}
///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())
})
}
}
///`Payload`
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "$id": "https://trusttasks.org/spec/rooms/epoch/mint/0.1",
/// "title": "Payload",
/// "type": "object",
/// "required": [
/// "epoch",
/// "presentation",
/// "roomId"
/// ],
/// "properties": {
/// "commit": {
/// "description": "\nThe MLS commit that produced this epoch, base64url, for the host to relay to members who were not online to receive it.\n\nCarried **here** for the same reason `link` is: minting is the moment the committer holds it, and a separate publish task would be a second chance to forget. A room that advances without leaving the commit somewhere fetchable **forks** — every member who missed the delivery is left at an epoch the room has moved past, holding keys that open nothing new.\n\n**Opaque to the host, and that is why this is safe on every tier.** A commit is ciphertext plus a leaf index; it names nobody. That is the difference from a Welcome, which the host is deliberately kept off the path of because it names the party joining. A host relaying commits learns that the room moved, which it already knew from `epoch`.\n\nOPTIONAL, because a room whose members are all online when it commits needs no relay and a host that stores one is storing it for nobody. A host **MUST NOT** replace a commit it already holds for an epoch: the first one published is the one members may already have applied, and a second would fork the very group it was meant to keep together.",
/// "type": "string",
/// "maxLength": 262144
/// },
/// "epoch": {
/// "description": "The new epoch number, which MUST be exactly one greater than the current one. A host records the number and never learns the key.",
/// "type": "integer",
/// "minimum": 2.0
/// },
/// "ext": {
/// "description": "Ecosystem-defined extension members per SPEC.md §4.5.1.",
/// "$ref": "#/definitions/Ext"
/// },
/// "link": {
/// "description": "The rung of the epoch key chain that this advance produces: the outgoing epoch's storage key sealed under the incoming one. Carried here because minting is the only moment at which one party holds both keys, and a room that advances without producing it silently loses the ability to read everything written before — for every member, including whoever wrote it. Its `epoch` MUST equal `epoch`, and a host MUST reject the request otherwise; a host MUST NOT replace a link it already holds for an epoch, because a second one is either a replay or a re-pointing of the room's history at key material of somebody else's choosing, and the members who already walked the original would never see the difference. Absent where the room does not keep its history readable, and necessarily absent for a room's first epoch, which has no predecessor.",
/// "$ref": "#/definitions/EpochLink",
/// "$comment": "Optional rather than required: this member was added to an already-published version, and requiring it would break every conforming producer. A room that omits it is making the choice the description names, not failing to make one."
/// },
/// "presentation": {
/// "description": "Must confer the `admin` action at this room's scope. Restricting epoch minting matters: if any key-holder could mint one, any member could evict any other by declining to seal the new key to them — silently, and with no server-side check possible on a room whose membership the host cannot see.",
/// "$ref": "#/definitions/AuthorityPresentation"
/// },
/// "reason": {
/// "description": "Optional operator-facing rationale, recorded in the room's audit.",
/// "type": "string",
/// "maxLength": 1024
/// },
/// "roomId": {
/// "description": "The room whose epoch advances.",
/// "type": "string"
/// }
/// },
/// "additionalProperties": false
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct Payload {
/**
The MLS commit that produced this epoch, base64url, for the host to relay to members who were not online to receive it.
Carried **here** for the same reason `link` is: minting is the moment the committer holds it, and a separate publish task would be a second chance to forget. A room that advances without leaving the commit somewhere fetchable **forks** — every member who missed the delivery is left at an epoch the room has moved past, holding keys that open nothing new.
**Opaque to the host, and that is why this is safe on every tier.** A commit is ciphertext plus a leaf index; it names nobody. That is the difference from a Welcome, which the host is deliberately kept off the path of because it names the party joining. A host relaying commits learns that the room moved, which it already knew from `epoch`.
OPTIONAL, because a room whose members are all online when it commits needs no relay and a host that stores one is storing it for nobody. A host **MUST NOT** replace a commit it already holds for an epoch: the first one published is the one members may already have applied, and a second would fork the very group it was meant to keep together.*/
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub commit: ::std::option::Option<PayloadCommit>,
///The new epoch number, which MUST be exactly one greater than the current one. A host records the number and never learns the key.
pub epoch: i64,
///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 rung of the epoch key chain that this advance produces: the outgoing epoch's storage key sealed under the incoming one. Carried here because minting is the only moment at which one party holds both keys, and a room that advances without producing it silently loses the ability to read everything written before — for every member, including whoever wrote it. Its `epoch` MUST equal `epoch`, and a host MUST reject the request otherwise; a host MUST NOT replace a link it already holds for an epoch, because a second one is either a replay or a re-pointing of the room's history at key material of somebody else's choosing, and the members who already walked the original would never see the difference. Absent where the room does not keep its history readable, and necessarily absent for a room's first epoch, which has no predecessor.
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub link: ::std::option::Option<EpochLink>,
///Must confer the `admin` action at this room's scope. Restricting epoch minting matters: if any key-holder could mint one, any member could evict any other by declining to seal the new key to them — silently, and with no server-side check possible on a room whose membership the host cannot see.
pub presentation: AuthorityPresentation,
///Optional operator-facing rationale, recorded in the room's audit.
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub reason: ::std::option::Option<PayloadReason>,
///The room whose epoch advances.
#[serde(rename = "roomId")]
pub room_id: ::std::string::String,
}
impl Payload {
pub fn builder() -> builder::Payload {
Default::default()
}
}
/**
The MLS commit that produced this epoch, base64url, for the host to relay to members who were not online to receive it.
Carried **here** for the same reason `link` is: minting is the moment the committer holds it, and a separate publish task would be a second chance to forget. A room that advances without leaving the commit somewhere fetchable **forks** — every member who missed the delivery is left at an epoch the room has moved past, holding keys that open nothing new.
**Opaque to the host, and that is why this is safe on every tier.** A commit is ciphertext plus a leaf index; it names nobody. That is the difference from a Welcome, which the host is deliberately kept off the path of because it names the party joining. A host relaying commits learns that the room moved, which it already knew from `epoch`.
OPTIONAL, because a room whose members are all online when it commits needs no relay and a host that stores one is storing it for nobody. A host **MUST NOT** replace a commit it already holds for an epoch: the first one published is the one members may already have applied, and a second would fork the very group it was meant to keep together.*/
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "\nThe MLS commit that produced this epoch, base64url, for the host to relay to members who were not online to receive it.\n\nCarried **here** for the same reason `link` is: minting is the moment the committer holds it, and a separate publish task would be a second chance to forget. A room that advances without leaving the commit somewhere fetchable **forks** — every member who missed the delivery is left at an epoch the room has moved past, holding keys that open nothing new.\n\n**Opaque to the host, and that is why this is safe on every tier.** A commit is ciphertext plus a leaf index; it names nobody. That is the difference from a Welcome, which the host is deliberately kept off the path of because it names the party joining. A host relaying commits learns that the room moved, which it already knew from `epoch`.\n\nOPTIONAL, because a room whose members are all online when it commits needs no relay and a host that stores one is storing it for nobody. A host **MUST NOT** replace a commit it already holds for an epoch: the first one published is the one members may already have applied, and a second would fork the very group it was meant to keep together.",
/// "type": "string",
/// "maxLength": 262144
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct PayloadCommit(::std::string::String);
impl ::std::ops::Deref for PayloadCommit {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<PayloadCommit> for ::std::string::String {
fn from(value: PayloadCommit) -> Self {
value.0
}
}
impl ::std::str::FromStr for PayloadCommit {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
if value.chars().count() > 262144usize {
return Err("longer than 262144 characters".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for PayloadCommit {
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 PayloadCommit {
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 PayloadCommit {
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 PayloadCommit {
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())
})
}
}
///Optional operator-facing rationale, recorded in the room's audit.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "Optional operator-facing rationale, recorded in the room's audit.",
/// "type": "string",
/// "maxLength": 1024
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct PayloadReason(::std::string::String);
impl ::std::ops::Deref for PayloadReason {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<PayloadReason> for ::std::string::String {
fn from(value: PayloadReason) -> Self {
value.0
}
}
impl ::std::str::FromStr for PayloadReason {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
if value.chars().count() > 1024usize {
return Err("longer than 1024 characters".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for PayloadReason {
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 PayloadReason {
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 PayloadReason {
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 PayloadReason {
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())
})
}
}
///Success response to rooms/epoch/mint. Type https://trusttasks.org/spec/rooms/epoch/mint/0.1#response.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "Response",
/// "description": "Success response to rooms/epoch/mint. Type https://trusttasks.org/spec/rooms/epoch/mint/0.1#response.",
/// "type": "object",
/// "required": [
/// "epoch",
/// "roomId"
/// ],
/// "properties": {
/// "epoch": {
/// "type": "integer",
/// "minimum": 2.0
/// },
/// "ext": {
/// "$ref": "#/definitions/Ext"
/// },
/// "roomId": {
/// "type": "string"
/// }
/// },
/// "additionalProperties": false,
/// "$anchor": "response"
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
#[non_exhaustive]
pub struct Response {
pub epoch: i64,
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub ext: ::std::option::Option<Ext>,
#[serde(rename = "roomId")]
pub room_id: ::std::string::String,
}
impl Response {
pub fn builder() -> builder::Response {
Default::default()
}
}
/// Types for composing complex structures.
pub mod builder {
#[derive(Clone, Debug)]
pub struct AuthorityPresentation {
authority:
::std::result::Result<::std::vec::Vec<::std::string::String>, ::std::string::String>,
membership: ::std::result::Result<::std::string::String, ::std::string::String>,
subject_binding: ::std::result::Result<
::std::option::Option<::std::string::String>,
::std::string::String,
>,
}
impl ::std::default::Default for AuthorityPresentation {
fn default() -> Self {
Self {
authority: Err("no value supplied for authority".to_string()),
membership: Err("no value supplied for membership".to_string()),
subject_binding: Ok(Default::default()),
}
}
}
impl AuthorityPresentation {
pub fn authority<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::vec::Vec<::std::string::String>>,
T::Error: ::std::fmt::Display,
{
self.authority = value
.try_into()
.map_err(|e| format!("error converting supplied value for authority: {e}"));
self
}
pub fn membership<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::string::String>,
T::Error: ::std::fmt::Display,
{
self.membership = value
.try_into()
.map_err(|e| format!("error converting supplied value for membership: {e}"));
self
}
pub fn subject_binding<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>,
T::Error: ::std::fmt::Display,
{
self.subject_binding = value
.try_into()
.map_err(|e| format!("error converting supplied value for subject_binding: {e}"));
self
}
}
impl ::std::convert::TryFrom<AuthorityPresentation> for super::AuthorityPresentation {
type Error = super::error::ConversionError;
fn try_from(
value: AuthorityPresentation,
) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self {
authority: value.authority?,
membership: value.membership?,
subject_binding: value.subject_binding?,
})
}
}
impl ::std::convert::From<super::AuthorityPresentation> for AuthorityPresentation {
fn from(value: super::AuthorityPresentation) -> Self {
Self {
authority: Ok(value.authority),
membership: Ok(value.membership),
subject_binding: Ok(value.subject_binding),
}
}
}
#[derive(Clone, Debug)]
pub struct EpochLink {
epoch: ::std::result::Result<i64, ::std::string::String>,
nonce: ::std::result::Result<::std::string::String, ::std::string::String>,
wrapped: ::std::result::Result<::std::string::String, ::std::string::String>,
}
impl ::std::default::Default for EpochLink {
fn default() -> Self {
Self {
epoch: Err("no value supplied for epoch".to_string()),
nonce: Err("no value supplied for nonce".to_string()),
wrapped: Err("no value supplied for wrapped".to_string()),
}
}
}
impl EpochLink {
pub fn epoch<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<i64>,
T::Error: ::std::fmt::Display,
{
self.epoch = value
.try_into()
.map_err(|e| format!("error converting supplied value for epoch: {e}"));
self
}
pub fn nonce<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::string::String>,
T::Error: ::std::fmt::Display,
{
self.nonce = value
.try_into()
.map_err(|e| format!("error converting supplied value for nonce: {e}"));
self
}
pub fn wrapped<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::string::String>,
T::Error: ::std::fmt::Display,
{
self.wrapped = value
.try_into()
.map_err(|e| format!("error converting supplied value for wrapped: {e}"));
self
}
}
impl ::std::convert::TryFrom<EpochLink> for super::EpochLink {
type Error = super::error::ConversionError;
fn try_from(
value: EpochLink,
) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self {
epoch: value.epoch?,
nonce: value.nonce?,
wrapped: value.wrapped?,
})
}
}
impl ::std::convert::From<super::EpochLink> for EpochLink {
fn from(value: super::EpochLink) -> Self {
Self {
epoch: Ok(value.epoch),
nonce: Ok(value.nonce),
wrapped: Ok(value.wrapped),
}
}
}
#[derive(Clone, Debug)]
pub struct Payload {
commit: ::std::result::Result<
::std::option::Option<super::PayloadCommit>,
::std::string::String,
>,
epoch: ::std::result::Result<i64, ::std::string::String>,
ext: ::std::result::Result<::std::option::Option<super::Ext>, ::std::string::String>,
link: ::std::result::Result<::std::option::Option<super::EpochLink>, ::std::string::String>,
presentation: ::std::result::Result<super::AuthorityPresentation, ::std::string::String>,
reason: ::std::result::Result<
::std::option::Option<super::PayloadReason>,
::std::string::String,
>,
room_id: ::std::result::Result<::std::string::String, ::std::string::String>,
}
impl ::std::default::Default for Payload {
fn default() -> Self {
Self {
commit: Ok(Default::default()),
epoch: Err("no value supplied for epoch".to_string()),
ext: Ok(Default::default()),
link: Ok(Default::default()),
presentation: Err("no value supplied for presentation".to_string()),
reason: Ok(Default::default()),
room_id: Err("no value supplied for room_id".to_string()),
}
}
}
impl Payload {
pub fn commit<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::PayloadCommit>>,
T::Error: ::std::fmt::Display,
{
self.commit = value
.try_into()
.map_err(|e| format!("error converting supplied value for commit: {e}"));
self
}
pub fn epoch<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<i64>,
T::Error: ::std::fmt::Display,
{
self.epoch = value
.try_into()
.map_err(|e| format!("error converting supplied value for epoch: {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 link<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::EpochLink>>,
T::Error: ::std::fmt::Display,
{
self.link = value
.try_into()
.map_err(|e| format!("error converting supplied value for link: {e}"));
self
}
pub fn presentation<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<super::AuthorityPresentation>,
T::Error: ::std::fmt::Display,
{
self.presentation = value
.try_into()
.map_err(|e| format!("error converting supplied value for presentation: {e}"));
self
}
pub fn reason<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<::std::option::Option<super::PayloadReason>>,
T::Error: ::std::fmt::Display,
{
self.reason = value
.try_into()
.map_err(|e| format!("error converting supplied value for reason: {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 {
commit: value.commit?,
epoch: value.epoch?,
ext: value.ext?,
link: value.link?,
presentation: value.presentation?,
reason: value.reason?,
room_id: value.room_id?,
})
}
}
impl ::std::convert::From<super::Payload> for Payload {
fn from(value: super::Payload) -> Self {
Self {
commit: Ok(value.commit),
epoch: Ok(value.epoch),
ext: Ok(value.ext),
link: Ok(value.link),
presentation: Ok(value.presentation),
reason: Ok(value.reason),
room_id: Ok(value.room_id),
}
}
}
#[derive(Clone, Debug)]
pub struct Response {
epoch: ::std::result::Result<i64, ::std::string::String>,
ext: ::std::result::Result<::std::option::Option<super::Ext>, ::std::string::String>,
room_id: ::std::result::Result<::std::string::String, ::std::string::String>,
}
impl ::std::default::Default for Response {
fn default() -> Self {
Self {
epoch: Err("no value supplied for epoch".to_string()),
ext: Ok(Default::default()),
room_id: Err("no value supplied for room_id".to_string()),
}
}
}
impl Response {
pub fn epoch<T>(mut self, value: T) -> Self
where
T: ::std::convert::TryInto<i64>,
T::Error: ::std::fmt::Display,
{
self.epoch = value
.try_into()
.map_err(|e| format!("error converting supplied value for epoch: {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 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<Response> for super::Response {
type Error = super::error::ConversionError;
fn try_from(value: Response) -> ::std::result::Result<Self, super::error::ConversionError> {
Ok(Self {
epoch: value.epoch?,
ext: value.ext?,
room_id: value.room_id?,
})
}
}
impl ::std::convert::From<super::Response> for Response {
fn from(value: super::Response) -> Self {
Self {
epoch: Ok(value.epoch),
ext: Ok(value.ext),
room_id: Ok(value.room_id),
}
}
}
}
impl crate::Payload for Payload {
const TYPE_URI: &'static str = "https://trusttasks.org/spec/rooms/epoch/mint/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 \"AuthorityPresentation\": {\n \"additionalProperties\": false,\n \"description\": \"What a party presents to act on a room. Carries the whole authority chain: a host MUST NOT dereference an authority credential's `parent` to fetch a link it was not given. Resolving over the network would make verification depend on availability, turn every identifier into a request the host can be induced to make against an address the holder chooses, and signal credential use to whoever hosts the identifier. A host MUST bind the presenter to the chain's leaf. A chain that verifies is evidence that authority was conferred on somebody; it is not evidence that the party presenting it is that somebody. The leaf's subject MUST equal the party the host authenticated for this request — an identity the transport established or a document `proof` proved, never one named in a payload. A host that omits this check authorizes every captured presentation, and the omission is silent, because the chain still verifies.\",\n \"properties\": {\n \"authority\": {\n \"description\": \"The authority chain, LEAF FIRST: the first element is the credential being relied on and the last MUST be one issued by the room itself. Every link the presenter relies on is present, because the host will not fetch one. Capped at 8: verification is linear in chain length and runs on every operation, so an unbounded chain is a denial-of-service surface against the host. The known uses need 2 to 3 — a person attenuating to an agent, and that agent to a sub-agent.\",\n \"items\": {\n \"type\": \"string\"\n },\n \"maxItems\": 8,\n \"minItems\": 1,\n \"type\": \"array\"\n },\n \"membership\": {\n \"description\": \"The presenter's membership credential for this room, or — on a `private` room — a zero-knowledge presentation of it. Serialized per the governing profile.\",\n \"type\": \"string\"\n },\n \"subjectBinding\": {\n \"description\": \"REQUIRED on a `private` room, where the subject identifier is withheld: a proof that the membership credential and the authority chain's leaf describe the SAME subject. Without it two parties pool credentials — one contributes membership, the other authority — and the combination verifies as a single party holding both. A host MUST refuse a private-room presentation that omits this.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"membership\",\n \"authority\"\n ],\n \"title\": \"AuthorityPresentation\",\n \"type\": \"object\"\n },\n \"EpochLink\": {\n \"additionalProperties\": false,\n \"description\": \"One rung of a room's epoch key chain: the storage key of epoch `epoch - 1`, sealed under the storage key of `epoch`. A group key schedule offers no way to derive an earlier epoch's key from a later one — that property is what makes removing a member mean something — so without a chain the first membership change makes every record already in the room unopenable by everyone, including whoever wrote it. The chain is the one-way street run deliberately the other way: a member holding the current key walks it backwards to any retained epoch, and a member holding an earlier key still derives nothing later. Removal stays forward-only; reading stays possible. What a chain costs is stated where it is chosen, in the room's retention policy.\",\n \"properties\": {\n \"epoch\": {\n \"description\": \"The epoch whose storage key opens this link; it wraps the storage key of `epoch - 1`. Never 1: a room's first epoch has no predecessor, so a link claiming one wraps something that is not an earlier epoch's key.\",\n \"minimum\": 2,\n \"type\": \"integer\"\n },\n \"nonce\": {\n \"description\": \"AEAD nonce, base64url.\",\n \"type\": \"string\"\n },\n \"wrapped\": {\n \"description\": \"The wrapped predecessor key, base64url. Bound by AEAD associated data to `roomId` and to this link's own position in the chain, so a link lifted to another rung, or served under another room, fails to open rather than yielding a key that is wrong. The binding is load-bearing rather than decorative: every rung is a fixed-length key sealed under a fixed-length key, so nothing about the ciphertext itself says where it belongs.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"epoch\",\n \"wrapped\",\n \"nonce\"\n ],\n \"title\": \"EpochLink\",\n \"type\": \"object\"\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 \"Response\": {\n \"$anchor\": \"response\",\n \"additionalProperties\": false,\n \"description\": \"Success response to rooms/epoch/mint. Type https://trusttasks.org/spec/rooms/epoch/mint/0.1#response.\",\n \"properties\": {\n \"epoch\": {\n \"minimum\": 2,\n \"type\": \"integer\"\n },\n \"ext\": {\n \"$ref\": \"#/$defs/Ext\"\n },\n \"roomId\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"roomId\",\n \"epoch\"\n ],\n \"title\": \"Rooms Epoch Mint — response payload\",\n \"type\": \"object\"\n }\n },\n \"$id\": \"https://trusttasks.org/spec/rooms/epoch/mint/0.1\",\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"additionalProperties\": false,\n \"properties\": {\n \"commit\": {\n \"description\": \"The MLS commit that produced this epoch, base64url, for the host to relay to members who were not online to receive it.\\n\\nCarried **here** for the same reason `link` is: minting is the moment the committer holds it, and a separate publish task would be a second chance to forget. A room that advances without leaving the commit somewhere fetchable **forks** — every member who missed the delivery is left at an epoch the room has moved past, holding keys that open nothing new.\\n\\n**Opaque to the host, and that is why this is safe on every tier.** A commit is ciphertext plus a leaf index; it names nobody. That is the difference from a Welcome, which the host is deliberately kept off the path of because it names the party joining. A host relaying commits learns that the room moved, which it already knew from `epoch`.\\n\\nOPTIONAL, because a room whose members are all online when it commits needs no relay and a host that stores one is storing it for nobody. A host **MUST NOT** replace a commit it already holds for an epoch: the first one published is the one members may already have applied, and a second would fork the very group it was meant to keep together.\",\n \"maxLength\": 262144,\n \"type\": \"string\"\n },\n \"epoch\": {\n \"description\": \"The new epoch number, which MUST be exactly one greater than the current one. A host records the number and never learns the key.\",\n \"minimum\": 2,\n \"type\": \"integer\"\n },\n \"ext\": {\n \"$ref\": \"#/$defs/Ext\",\n \"description\": \"Ecosystem-defined extension members per SPEC.md §4.5.1.\"\n },\n \"link\": {\n \"$comment\": \"Optional rather than required: this member was added to an already-published version, and requiring it would break every conforming producer. A room that omits it is making the choice the description names, not failing to make one.\",\n \"$ref\": \"#/$defs/EpochLink\",\n \"description\": \"The rung of the epoch key chain that this advance produces: the outgoing epoch's storage key sealed under the incoming one. Carried here because minting is the only moment at which one party holds both keys, and a room that advances without producing it silently loses the ability to read everything written before — for every member, including whoever wrote it. Its `epoch` MUST equal `epoch`, and a host MUST reject the request otherwise; a host MUST NOT replace a link it already holds for an epoch, because a second one is either a replay or a re-pointing of the room's history at key material of somebody else's choosing, and the members who already walked the original would never see the difference. Absent where the room does not keep its history readable, and necessarily absent for a room's first epoch, which has no predecessor.\"\n },\n \"presentation\": {\n \"$ref\": \"#/$defs/AuthorityPresentation\",\n \"description\": \"Must confer the `admin` action at this room's scope. Restricting epoch minting matters: if any key-holder could mint one, any member could evict any other by declining to seal the new key to them — silently, and with no server-side check possible on a room whose membership the host cannot see.\"\n },\n \"reason\": {\n \"description\": \"Optional operator-facing rationale, recorded in the room's audit.\",\n \"maxLength\": 1024,\n \"type\": \"string\"\n },\n \"roomId\": {\n \"description\": \"The room whose epoch advances.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"roomId\",\n \"epoch\",\n \"presentation\"\n ],\n \"title\": \"Rooms Epoch Mint — payload\",\n \"type\": \"object\"\n}\n",
);
}
impl crate::Payload for Response {
const TYPE_URI: &'static str = "https://trusttasks.org/spec/rooms/epoch/mint/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 \"AuthorityPresentation\": {\n \"additionalProperties\": false,\n \"description\": \"What a party presents to act on a room. Carries the whole authority chain: a host MUST NOT dereference an authority credential's `parent` to fetch a link it was not given. Resolving over the network would make verification depend on availability, turn every identifier into a request the host can be induced to make against an address the holder chooses, and signal credential use to whoever hosts the identifier. A host MUST bind the presenter to the chain's leaf. A chain that verifies is evidence that authority was conferred on somebody; it is not evidence that the party presenting it is that somebody. The leaf's subject MUST equal the party the host authenticated for this request — an identity the transport established or a document `proof` proved, never one named in a payload. A host that omits this check authorizes every captured presentation, and the omission is silent, because the chain still verifies.\",\n \"properties\": {\n \"authority\": {\n \"description\": \"The authority chain, LEAF FIRST: the first element is the credential being relied on and the last MUST be one issued by the room itself. Every link the presenter relies on is present, because the host will not fetch one. Capped at 8: verification is linear in chain length and runs on every operation, so an unbounded chain is a denial-of-service surface against the host. The known uses need 2 to 3 — a person attenuating to an agent, and that agent to a sub-agent.\",\n \"items\": {\n \"type\": \"string\"\n },\n \"maxItems\": 8,\n \"minItems\": 1,\n \"type\": \"array\"\n },\n \"membership\": {\n \"description\": \"The presenter's membership credential for this room, or — on a `private` room — a zero-knowledge presentation of it. Serialized per the governing profile.\",\n \"type\": \"string\"\n },\n \"subjectBinding\": {\n \"description\": \"REQUIRED on a `private` room, where the subject identifier is withheld: a proof that the membership credential and the authority chain's leaf describe the SAME subject. Without it two parties pool credentials — one contributes membership, the other authority — and the combination verifies as a single party holding both. A host MUST refuse a private-room presentation that omits this.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"membership\",\n \"authority\"\n ],\n \"title\": \"AuthorityPresentation\",\n \"type\": \"object\"\n },\n \"EpochLink\": {\n \"additionalProperties\": false,\n \"description\": \"One rung of a room's epoch key chain: the storage key of epoch `epoch - 1`, sealed under the storage key of `epoch`. A group key schedule offers no way to derive an earlier epoch's key from a later one — that property is what makes removing a member mean something — so without a chain the first membership change makes every record already in the room unopenable by everyone, including whoever wrote it. The chain is the one-way street run deliberately the other way: a member holding the current key walks it backwards to any retained epoch, and a member holding an earlier key still derives nothing later. Removal stays forward-only; reading stays possible. What a chain costs is stated where it is chosen, in the room's retention policy.\",\n \"properties\": {\n \"epoch\": {\n \"description\": \"The epoch whose storage key opens this link; it wraps the storage key of `epoch - 1`. Never 1: a room's first epoch has no predecessor, so a link claiming one wraps something that is not an earlier epoch's key.\",\n \"minimum\": 2,\n \"type\": \"integer\"\n },\n \"nonce\": {\n \"description\": \"AEAD nonce, base64url.\",\n \"type\": \"string\"\n },\n \"wrapped\": {\n \"description\": \"The wrapped predecessor key, base64url. Bound by AEAD associated data to `roomId` and to this link's own position in the chain, so a link lifted to another rung, or served under another room, fails to open rather than yielding a key that is wrong. The binding is load-bearing rather than decorative: every rung is a fixed-length key sealed under a fixed-length key, so nothing about the ciphertext itself says where it belongs.\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"epoch\",\n \"wrapped\",\n \"nonce\"\n ],\n \"title\": \"EpochLink\",\n \"type\": \"object\"\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 \"Response\": {\n \"$anchor\": \"response\",\n \"additionalProperties\": false,\n \"description\": \"Success response to rooms/epoch/mint. Type https://trusttasks.org/spec/rooms/epoch/mint/0.1#response.\",\n \"properties\": {\n \"epoch\": {\n \"minimum\": 2,\n \"type\": \"integer\"\n },\n \"ext\": {\n \"$ref\": \"#/$defs/Ext\"\n },\n \"roomId\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"roomId\",\n \"epoch\"\n ],\n \"title\": \"Rooms Epoch Mint — 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_AUTHORIZED,
error_codes::NON_SEQUENTIAL,
error_codes::CHAIN_TOO_DEEP,
];
/// 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/epoch/mint:notAuthorized`
///
/// The presentation does not confer `admin` at this room's scope.
///
/// Declared `retryable: false`.
pub const NOT_AUTHORIZED: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "rooms/epoch/mint:notAuthorized",
retryable: false,
};
/// `rooms/epoch/mint:nonSequential`
///
/// The epoch is not exactly one greater than the current one.
///
/// Declared `retryable: false`.
pub const NON_SEQUENTIAL: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "rooms/epoch/mint:nonSequential",
retryable: false,
};
/// `rooms/epoch/mint:chainTooDeep`
///
/// The authority chain exceeds the maximum of 8 links.
///
/// Declared `retryable: false`.
pub const CHAIN_TOO_DEEP: crate::DeclaredErrorCode = crate::DeclaredErrorCode {
code: "rooms/epoch/mint:chainTooDeep",
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).
/// Each fixture in `payload.invalid-examples.json` MUST be
/// rejected by at least one of: serde deserialization, or
/// JSON-Schema validation under the `validate` feature. The
/// fixture file documents the producer-side bug class that
/// each payload exemplifies; this generated test pins it.
#[cfg(feature = "validate")]
#[test]
fn rejects_invalid_examples() {
use crate::validate::ValidatedPayload;
let fixtures: &[(&str, &str)] = &[
(
"An epoch link claiming epoch 1. A room's first epoch has no predecessor, so a rung 1 wraps something that is not an earlier epoch's key; the floor of 2 on EpochLink.epoch is what refuses it. (The stronger rule — that a link's epoch must equal the epoch being minted — is a MUST in prose, because a schema cannot compare two of its own members.)",
"{\n \"epoch\": 2,\n \"link\": {\n \"epoch\": 1,\n \"nonce\": \"b0Zt8Qm2Yq1sVvR0\",\n \"wrapped\": \"9jK2_QhV1sVvR0m5xAqZ7A\"\n },\n \"presentation\": {\n \"authority\": [\n \"eyJhbGciOiJFZERTQSJ9.admin-vac\"\n ],\n \"membership\": \"eyJhbGciOiJFZERTQSJ9.owner-vmc\"\n },\n \"roomId\": \"did:webvh:example.com:rooms:northwind\"\n}",
),
(
"An epoch link with no nonce. Every rung is AEAD ciphertext and a nonce is not optional for one; a link missing it cannot be opened by anybody, and would be stored as a rung that silently severs the chain at that point.",
"{\n \"epoch\": 3,\n \"link\": {\n \"epoch\": 3,\n \"wrapped\": \"9jK2_QhV1sVvR0m5xAqZ7A\"\n },\n \"presentation\": {\n \"authority\": [\n \"eyJhbGciOiJFZERTQSJ9.admin-vac\"\n ],\n \"membership\": \"eyJhbGciOiJFZERTQSJ9.owner-vmc\"\n },\n \"roomId\": \"did:webvh:example.com:rooms:northwind\"\n}",
),
(
"Unknown member inside the link — additionalProperties: false on EpochLink. A rung carries wrapped key material and nothing descriptive; an extra member is the obvious place to leak what the sealing exists to hide.",
"{\n \"epoch\": 3,\n \"link\": {\n \"epoch\": 3,\n \"nonce\": \"b0Zt8Qm2Yq1sVvR0\",\n \"removedMember\": \"did:example:bob\",\n \"wrapped\": \"9jK2_QhV1sVvR0m5xAqZ7A\"\n },\n \"presentation\": {\n \"authority\": [\n \"eyJhbGciOiJFZERTQSJ9.admin-vac\"\n ],\n \"membership\": \"eyJhbGciOiJFZERTQSJ9.owner-vmc\"\n },\n \"roomId\": \"did:webvh:example.com:rooms:northwind\"\n}",
),
(
"Minting epoch 1. An epoch advances by exactly one from an existing room, whose first epoch is 1 already; a request to mint it is either a re-registration or an off-by-one, and both would reset a room rather than advance it.",
"{\n \"epoch\": 1,\n \"presentation\": {\n \"authority\": [\n \"eyJhbGciOiJFZERTQSJ9.admin-vac\"\n ],\n \"membership\": \"eyJhbGciOiJFZERTQSJ9.owner-vmc\"\n },\n \"roomId\": \"did:webvh:example.com:rooms:northwind\"\n}",
),
(
"Bare/unnamespaced ext key — SPEC §4.5.1 requires every immediate child of ext to be reverse-DNS namespaced.",
"{\n \"epoch\": 2,\n \"ext\": {\n \"bare-key\": {\n \"anything\": \"here\"\n }\n },\n \"presentation\": {\n \"authority\": [\n \"eyJhbGciOiJFZERTQSJ9.admin-vac\"\n ],\n \"membership\": \"eyJhbGciOiJFZERTQSJ9.owner-vmc\"\n },\n \"roomId\": \"did:webvh:example.com:rooms:northwind\"\n}",
),
];
for (i, (note, raw)) in fixtures.iter().enumerate() {
let value: serde_json::Value = match serde_json::from_str(raw) {
Ok(v) => v,
Err(_) => continue,
};
let serde_ok = serde_json::from_value::<super::Payload>(value.clone()).is_ok();
let schema_ok = super::Payload::validate_value(&value).is_ok();
assert!(
!(serde_ok && schema_ok),
"invalid-example #{} ({:?}) was accepted by both serde and JSON Schema; \
the fixture's stated failure class is no longer caught:\n{}",
i + 1,
note,
raw
);
}
}
}