//! Generated by `trust-tasks-codegen` — do not edit by hand.
//!
//! Spec slug: `task-consent/request`. 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())
}
}
}
///One consequence of executing the pending task, authored by the executor that is about to run it. An effect is produced by dry-running the real handler against the executor's own prior state — never by the requester, and never by re-implementing the handler's semantics elsewhere. A consent surface renders ONLY effects it received under the executor's signature.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "Effect",
/// "description": "One consequence of executing the pending task, authored by the executor that is about to run it. An effect is produced by dry-running the real handler against the executor's own prior state — never by the requester, and never by re-implementing the handler's semantics elsewhere. A consent surface renders ONLY effects it received under the executor's signature.",
/// "type": "object",
/// "required": [
/// "kind",
/// "summary"
/// ],
/// "properties": {
/// "after": {
/// "description": "OPTIONAL resulting value at `path` once the task executes. ABSENT means the value is removed (a deletion), under the same rule as `before`."
/// },
/// "before": {
/// "description": "OPTIONAL prior value at `path`, from the executor's authoritative state. ABSENT means there was no prior value (a creation); an explicit `null` is treated identically, so an executor MUST NOT rely on the two being distinguishable. Structured members are for rich rendering only — `summary` is what a surface is obliged to show, and it is where an executor states a distinction this shape cannot carry."
/// },
/// "detail": {
/// "description": "OPTIONAL kind-specific structured extras (e.g. `{ \"commitments\": 2 }` for `preRotationRefresh`).",
/// "type": "object",
/// "additionalProperties": true
/// },
/// "kind": {
/// "description": "Machine discriminator for rich rendering. Well-known kinds are registered in the prose (`documentChange`, `keyRotation`, `preRotationRefresh`, `resourceDelete`, `disclosure`), but the set is OPEN: handlers evolve faster than this schema, and an executor MUST be able to describe a consequence this version does not name. A surface that does not recognise a `kind` MUST still render `summary`.",
/// "type": "string",
/// "pattern": "^[a-z][a-zA-Z0-9]*$"
/// },
/// "path": {
/// "description": "OPTIONAL RFC 6901 JSON Pointer locating the change within the subject resource (e.g. `/service/0`).",
/// "type": "string"
/// },
/// "summary": {
/// "description": "REQUIRED human-facing sentence describing this consequence, authored by the executor. This is the ONLY member a consent surface is guaranteed to be able to render, and it is what makes an unrecognised `kind` degrade to something truthful rather than something invisible. A surface MUST render it verbatim; it MUST NOT substitute its own prose.",
/// "type": "string",
/// "minLength": 1
/// }
/// },
/// "additionalProperties": false
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct Effect {
///OPTIONAL resulting value at `path` once the task executes. ABSENT means the value is removed (a deletion), under the same rule as `before`.
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub after: ::std::option::Option<::serde_json::Value>,
///OPTIONAL prior value at `path`, from the executor's authoritative state. ABSENT means there was no prior value (a creation); an explicit `null` is treated identically, so an executor MUST NOT rely on the two being distinguishable. Structured members are for rich rendering only — `summary` is what a surface is obliged to show, and it is where an executor states a distinction this shape cannot carry.
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub before: ::std::option::Option<::serde_json::Value>,
///OPTIONAL kind-specific structured extras (e.g. `{ "commitments": 2 }` for `preRotationRefresh`).
#[serde(default, skip_serializing_if = "::serde_json::Map::is_empty")]
pub detail: ::serde_json::Map<::std::string::String, ::serde_json::Value>,
///Machine discriminator for rich rendering. Well-known kinds are registered in the prose (`documentChange`, `keyRotation`, `preRotationRefresh`, `resourceDelete`, `disclosure`), but the set is OPEN: handlers evolve faster than this schema, and an executor MUST be able to describe a consequence this version does not name. A surface that does not recognise a `kind` MUST still render `summary`.
pub kind: EffectKind,
///OPTIONAL RFC 6901 JSON Pointer locating the change within the subject resource (e.g. `/service/0`).
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub path: ::std::option::Option<::std::string::String>,
///REQUIRED human-facing sentence describing this consequence, authored by the executor. This is the ONLY member a consent surface is guaranteed to be able to render, and it is what makes an unrecognised `kind` degrade to something truthful rather than something invisible. A surface MUST render it verbatim; it MUST NOT substitute its own prose.
pub summary: EffectSummary,
}
///Machine discriminator for rich rendering. Well-known kinds are registered in the prose (`documentChange`, `keyRotation`, `preRotationRefresh`, `resourceDelete`, `disclosure`), but the set is OPEN: handlers evolve faster than this schema, and an executor MUST be able to describe a consequence this version does not name. A surface that does not recognise a `kind` MUST still render `summary`.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "Machine discriminator for rich rendering. Well-known kinds are registered in the prose (`documentChange`, `keyRotation`, `preRotationRefresh`, `resourceDelete`, `disclosure`), but the set is OPEN: handlers evolve faster than this schema, and an executor MUST be able to describe a consequence this version does not name. A surface that does not recognise a `kind` MUST still render `summary`.",
/// "type": "string",
/// "pattern": "^[a-z][a-zA-Z0-9]*$"
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct EffectKind(::std::string::String);
impl ::std::ops::Deref for EffectKind {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<EffectKind> for ::std::string::String {
fn from(value: EffectKind) -> Self {
value.0
}
}
impl ::std::str::FromStr for EffectKind {
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-zA-Z0-9]*$").unwrap());
if PATTERN.find(value).is_none() {
return Err("doesn't match pattern \"^[a-z][a-zA-Z0-9]*$\"".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for EffectKind {
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 EffectKind {
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 EffectKind {
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 EffectKind {
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())
})
}
}
///REQUIRED human-facing sentence describing this consequence, authored by the executor. This is the ONLY member a consent surface is guaranteed to be able to render, and it is what makes an unrecognised `kind` degrade to something truthful rather than something invisible. A surface MUST render it verbatim; it MUST NOT substitute its own prose.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "REQUIRED human-facing sentence describing this consequence, authored by the executor. This is the ONLY member a consent surface is guaranteed to be able to render, and it is what makes an unrecognised `kind` degrade to something truthful rather than something invisible. A surface MUST render it verbatim; it MUST NOT substitute its own prose.",
/// "type": "string",
/// "minLength": 1
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct EffectSummary(::std::string::String);
impl ::std::ops::Deref for EffectSummary {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<EffectSummary> for ::std::string::String {
fn from(value: EffectSummary) -> Self {
value.0
}
}
impl ::std::str::FromStr for EffectSummary {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
if value.chars().count() < 1usize {
return Err("shorter than 1 characters".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for EffectSummary {
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 EffectSummary {
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 EffectSummary {
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 EffectSummary {
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 authoritative SPEC §7.3 item 14 exposure class of the pending task, derived by the executor from the compiled handler it is about to invoke — never from the registry's declared value.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "Exposure",
/// "description": "The authoritative SPEC §7.3 item 14 exposure class of the pending task, derived by the executor from the compiled handler it is about to invoke — never from the registry's declared value.",
/// "type": "object",
/// "required": [
/// "actsAsSubject",
/// "discloses"
/// ],
/// "properties": {
/// "actsAsSubject": {
/// "description": "Whether execution exercises the subject's own authority to produce an attributable effect in the subject's name.",
/// "type": "boolean"
/// },
/// "discloses": {
/// "description": "Sensitivity of data the task returns to its caller.",
/// "type": "string",
/// "enum": [
/// "none",
/// "metadata",
/// "secret"
/// ]
/// }
/// },
/// "additionalProperties": false
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct Exposure {
///Whether execution exercises the subject's own authority to produce an attributable effect in the subject's name.
#[serde(rename = "actsAsSubject")]
pub acts_as_subject: bool,
///Sensitivity of data the task returns to its caller.
pub discloses: ExposureDiscloses,
}
///Sensitivity of data the task returns to its caller.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "Sensitivity of data the task returns to its caller.",
/// "type": "string",
/// "enum": [
/// "none",
/// "metadata",
/// "secret"
/// ]
///}
/// ```
/// </details>
#[derive(
::serde::Deserialize,
::serde::Serialize,
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
)]
pub enum ExposureDiscloses {
#[serde(rename = "none")]
None,
#[serde(rename = "metadata")]
Metadata,
#[serde(rename = "secret")]
Secret,
}
impl ::std::fmt::Display for ExposureDiscloses {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match *self {
Self::None => f.write_str("none"),
Self::Metadata => f.write_str("metadata"),
Self::Secret => f.write_str("secret"),
}
}
}
impl ::std::str::FromStr for ExposureDiscloses {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
match value {
"none" => Ok(Self::None),
"metadata" => Ok(Self::Metadata),
"secret" => Ok(Self::Secret),
_ => Err("invalid value".into()),
}
}
}
impl ::std::convert::TryFrom<&str> for ExposureDiscloses {
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 ExposureDiscloses {
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 ExposureDiscloses {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
///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 executor asks an enrolled approver device to authorize one pending privileged task, presenting the effects it computed by dry-running the real handler against its own prior state. The executor signs this document; the approver renders only what it verifies under that signature.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "$id": "https://trusttasks.org/spec/task-consent/request/0.1",
/// "title": "Payload",
/// "description": "The executor asks an enrolled approver device to authorize one pending privileged task, presenting the effects it computed by dry-running the real handler against its own prior state. The executor signs this document; the approver renders only what it verifies under that signature.",
/// "type": "object",
/// "required": [
/// "approverSet",
/// "challenge",
/// "effects",
/// "excludeRequester",
/// "expiresAt",
/// "exposure",
/// "minApprovals",
/// "payloadDigest",
/// "requester",
/// "sideEffects",
/// "taskType"
/// ],
/// "properties": {
/// "approverSet": {
/// "description": "Name of the approver set the policy's `requireConsent` named.",
/// "type": "string"
/// },
/// "challenge": {
/// "description": "Per-request nonce (≥128 bits entropy) echoed and signed by the matching task-consent/decision. It is also the salt in `payloadDigest`, so a party that holds this request can re-derive the digest and a party that does not cannot invert it.",
/// "type": "string",
/// "minLength": 16
/// },
/// "consequences": {
/// "description": "Static, human-facing second-order effects declared in the task's specification (SPEC §7.3 item 13). A fallback for handlers with no dry-run — per-task, not per-request, so it can say 'any document change rotates the update keys' but not which keys. Prefer `effects`.",
/// "type": "array",
/// "items": {
/// "type": "string",
/// "minLength": 1
/// }
/// },
/// "effects": {
/// "description": "What executing this task will actually do, computed by dry-running the real handler against the executor's prior state. MAY be empty when the executor has no dry-run for this handler — in which case `consequences` carries the specification's static fallback text. When BOTH are empty the surface MUST tell the approver the consequences could not be determined, and MUST NOT present the task as though it had none.",
/// "type": "array",
/// "items": {
/// "$ref": "#/definitions/Effect"
/// }
/// },
/// "excludeRequester": {
/// "description": "When true, `requester` may not count toward the threshold — so a single compromised device cannot both propose and approve. A surface MUST NOT offer to approve a request whose `requester` is its own subject when this is set.",
/// "type": "boolean"
/// },
/// "expiresAt": {
/// "description": "After this instant the pending request lapses and no decision is accepted for it.",
/// "type": "string",
/// "format": "date-time"
/// },
/// "exposure": {
/// "$ref": "#/definitions/Exposure"
/// },
/// "ext": {
/// "$ref": "#/definitions/Ext"
/// },
/// "minApprovals": {
/// "description": "Distinct approvals from the set required before the task may execute.",
/// "type": "integer",
/// "minimum": 1.0
/// },
/// "origin": {
/// "description": "OPTIONAL web origin that proposed the task, when it arrived via a relying party. This MUST be the origin the consumer's own runtime attested (e.g. the browser-supplied sender origin) — never a value the proposing page supplied, and never one the relying party could author.",
/// "type": "string"
/// },
/// "payloadDigest": {
/// "description": "The binding between what the approver sees and what executes. Digest of the canonical (RFC 8785 JCS) payload, the task type, and the `challenge` as salt. The decision echoes it; the executor re-derives it from the payload it is about to run and refuses on mismatch. Salted because an unsalted digest over a low-entropy payload is a confirmation oracle for anyone who observes it in transit.",
/// "type": "string"
/// },
/// "requester": {
/// "description": "DID that submitted the pending task. Shown to the approver, and compared against the approver when `excludeRequester` is set.",
/// "type": "string"
/// },
/// "requesterDeviceId": {
/// "description": "OPTIONAL enrolled device the task was submitted from.",
/// "type": "string"
/// },
/// "sideEffects": {
/// "description": "Authoritative SPEC §7.3 item 13 side-effect class of the pending task, derived by the executor from the compiled handler it is about to invoke. NEVER taken from the registry: a registry that decided this would be a consent kill-switch, downgradeable by publishing a new version with a weaker class.",
/// "type": "string",
/// "enum": [
/// "none",
/// "mutating",
/// "destructive"
/// ]
/// },
/// "statePin": {
/// "$ref": "#/definitions/StatePin"
/// },
/// "subject": {
/// "description": "OPTIONAL identifier the task acts on — the value at the specification's `subjectPath`, usually a DID. Absent for subjectless tasks.",
/// "type": "string"
/// },
/// "taskType": {
/// "description": "Type URI of the task awaiting approval. It is bound into `payloadDigest`: without that binding, two tasks whose payloads canonicalize identically would share a digest, and an approval for a benign task would authorize a destructive one.",
/// "type": "string",
/// "format": "uri"
/// }
/// },
/// "additionalProperties": false
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct Payload {
///Name of the approver set the policy's `requireConsent` named.
#[serde(rename = "approverSet")]
pub approver_set: ::std::string::String,
///Per-request nonce (≥128 bits entropy) echoed and signed by the matching task-consent/decision. It is also the salt in `payloadDigest`, so a party that holds this request can re-derive the digest and a party that does not cannot invert it.
pub challenge: PayloadChallenge,
///Static, human-facing second-order effects declared in the task's specification (SPEC §7.3 item 13). A fallback for handlers with no dry-run — per-task, not per-request, so it can say 'any document change rotates the update keys' but not which keys. Prefer `effects`.
#[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")]
pub consequences: ::std::vec::Vec<PayloadConsequencesItem>,
///What executing this task will actually do, computed by dry-running the real handler against the executor's prior state. MAY be empty when the executor has no dry-run for this handler — in which case `consequences` carries the specification's static fallback text. When BOTH are empty the surface MUST tell the approver the consequences could not be determined, and MUST NOT present the task as though it had none.
pub effects: ::std::vec::Vec<Effect>,
///When true, `requester` may not count toward the threshold — so a single compromised device cannot both propose and approve. A surface MUST NOT offer to approve a request whose `requester` is its own subject when this is set.
#[serde(rename = "excludeRequester")]
pub exclude_requester: bool,
///After this instant the pending request lapses and no decision is accepted for it.
#[serde(rename = "expiresAt")]
pub expires_at: ::chrono::DateTime<::chrono::offset::Utc>,
pub exposure: Exposure,
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub ext: ::std::option::Option<Ext>,
///Distinct approvals from the set required before the task may execute.
#[serde(rename = "minApprovals")]
pub min_approvals: ::std::num::NonZeroU64,
///OPTIONAL web origin that proposed the task, when it arrived via a relying party. This MUST be the origin the consumer's own runtime attested (e.g. the browser-supplied sender origin) — never a value the proposing page supplied, and never one the relying party could author.
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub origin: ::std::option::Option<::std::string::String>,
///The binding between what the approver sees and what executes. Digest of the canonical (RFC 8785 JCS) payload, the task type, and the `challenge` as salt. The decision echoes it; the executor re-derives it from the payload it is about to run and refuses on mismatch. Salted because an unsalted digest over a low-entropy payload is a confirmation oracle for anyone who observes it in transit.
#[serde(rename = "payloadDigest")]
pub payload_digest: ::std::string::String,
///DID that submitted the pending task. Shown to the approver, and compared against the approver when `excludeRequester` is set.
pub requester: ::std::string::String,
///OPTIONAL enrolled device the task was submitted from.
#[serde(
rename = "requesterDeviceId",
default,
skip_serializing_if = "::std::option::Option::is_none"
)]
pub requester_device_id: ::std::option::Option<::std::string::String>,
///Authoritative SPEC §7.3 item 13 side-effect class of the pending task, derived by the executor from the compiled handler it is about to invoke. NEVER taken from the registry: a registry that decided this would be a consent kill-switch, downgradeable by publishing a new version with a weaker class.
#[serde(rename = "sideEffects")]
pub side_effects: PayloadSideEffects,
#[serde(
rename = "statePin",
default,
skip_serializing_if = "::std::option::Option::is_none"
)]
pub state_pin: ::std::option::Option<StatePin>,
///OPTIONAL identifier the task acts on — the value at the specification's `subjectPath`, usually a DID. Absent for subjectless tasks.
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub subject: ::std::option::Option<::std::string::String>,
///Type URI of the task awaiting approval. It is bound into `payloadDigest`: without that binding, two tasks whose payloads canonicalize identically would share a digest, and an approval for a benign task would authorize a destructive one.
#[serde(rename = "taskType")]
pub task_type: ::std::string::String,
}
///Per-request nonce (≥128 bits entropy) echoed and signed by the matching task-consent/decision. It is also the salt in `payloadDigest`, so a party that holds this request can re-derive the digest and a party that does not cannot invert it.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "Per-request nonce (≥128 bits entropy) echoed and signed by the matching task-consent/decision. It is also the salt in `payloadDigest`, so a party that holds this request can re-derive the digest and a party that does not cannot invert it.",
/// "type": "string",
/// "minLength": 16
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct PayloadChallenge(::std::string::String);
impl ::std::ops::Deref for PayloadChallenge {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<PayloadChallenge> for ::std::string::String {
fn from(value: PayloadChallenge) -> Self {
value.0
}
}
impl ::std::str::FromStr for PayloadChallenge {
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());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for PayloadChallenge {
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 PayloadChallenge {
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 PayloadChallenge {
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 PayloadChallenge {
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())
})
}
}
///`PayloadConsequencesItem`
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "type": "string",
/// "minLength": 1
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct PayloadConsequencesItem(::std::string::String);
impl ::std::ops::Deref for PayloadConsequencesItem {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<PayloadConsequencesItem> for ::std::string::String {
fn from(value: PayloadConsequencesItem) -> Self {
value.0
}
}
impl ::std::str::FromStr for PayloadConsequencesItem {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
if value.chars().count() < 1usize {
return Err("shorter than 1 characters".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for PayloadConsequencesItem {
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 PayloadConsequencesItem {
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 PayloadConsequencesItem {
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 PayloadConsequencesItem {
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())
})
}
}
///Authoritative SPEC §7.3 item 13 side-effect class of the pending task, derived by the executor from the compiled handler it is about to invoke. NEVER taken from the registry: a registry that decided this would be a consent kill-switch, downgradeable by publishing a new version with a weaker class.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "Authoritative SPEC §7.3 item 13 side-effect class of the pending task, derived by the executor from the compiled handler it is about to invoke. NEVER taken from the registry: a registry that decided this would be a consent kill-switch, downgradeable by publishing a new version with a weaker class.",
/// "type": "string",
/// "enum": [
/// "none",
/// "mutating",
/// "destructive"
/// ]
///}
/// ```
/// </details>
#[derive(
::serde::Deserialize,
::serde::Serialize,
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
)]
pub enum PayloadSideEffects {
#[serde(rename = "none")]
None,
#[serde(rename = "mutating")]
Mutating,
#[serde(rename = "destructive")]
Destructive,
}
impl ::std::fmt::Display for PayloadSideEffects {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match *self {
Self::None => f.write_str("none"),
Self::Mutating => f.write_str("mutating"),
Self::Destructive => f.write_str("destructive"),
}
}
}
impl ::std::str::FromStr for PayloadSideEffects {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
match value {
"none" => Ok(Self::None),
"mutating" => Ok(Self::Mutating),
"destructive" => Ok(Self::Destructive),
_ => Err("invalid value".into()),
}
}
}
impl ::std::convert::TryFrom<&str> for PayloadSideEffects {
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 PayloadSideEffects {
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 PayloadSideEffects {
type Error = self::error::ConversionError;
fn try_from(
value: ::std::string::String,
) -> ::std::result::Result<Self, self::error::ConversionError> {
value.parse()
}
}
///Synchronous acknowledgement from the approver device that a prompt was raised. The decision itself arrives separately as a task-consent/decision — a human is in the loop, so it cannot be a synchronous reply.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "Response",
/// "description": "Synchronous acknowledgement from the approver device that a prompt was raised. The decision itself arrives separately as a task-consent/decision — a human is in the loop, so it cannot be a synchronous reply.",
/// "type": "object",
/// "required": [
/// "status"
/// ],
/// "properties": {
/// "ext": {
/// "$ref": "#/definitions/Ext"
/// },
/// "reason": {
/// "description": "REQUIRED when status is `refused`.",
/// "type": "string"
/// },
/// "status": {
/// "description": "`prompted` = the request was verified and an approval prompt raised. `refused` = the device will not prompt; `reason` MUST be set.",
/// "type": "string",
/// "enum": [
/// "prompted",
/// "refused"
/// ]
/// }
/// },
/// "additionalProperties": false,
/// "$anchor": "response"
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct Response {
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub ext: ::std::option::Option<Ext>,
///REQUIRED when status is `refused`.
#[serde(default, skip_serializing_if = "::std::option::Option::is_none")]
pub reason: ::std::option::Option<::std::string::String>,
///`prompted` = the request was verified and an approval prompt raised. `refused` = the device will not prompt; `reason` MUST be set.
pub status: ResponseStatus,
}
///`prompted` = the request was verified and an approval prompt raised. `refused` = the device will not prompt; `reason` MUST be set.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "`prompted` = the request was verified and an approval prompt raised. `refused` = the device will not prompt; `reason` MUST be set.",
/// "type": "string",
/// "enum": [
/// "prompted",
/// "refused"
/// ]
///}
/// ```
/// </details>
#[derive(
::serde::Deserialize,
::serde::Serialize,
Clone,
Copy,
Debug,
Eq,
Hash,
Ord,
PartialEq,
PartialOrd,
)]
pub enum ResponseStatus {
#[serde(rename = "prompted")]
Prompted,
#[serde(rename = "refused")]
Refused,
}
impl ::std::fmt::Display for ResponseStatus {
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
match *self {
Self::Prompted => f.write_str("prompted"),
Self::Refused => f.write_str("refused"),
}
}
}
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 {
"prompted" => Ok(Self::Prompted),
"refused" => Ok(Self::Refused),
_ => 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()
}
}
///The prior state the effects were computed against. A human in the loop makes the approval window minutes wide, so the state can move underneath a pending approval. The executor asserts this pin still holds at execution and refuses otherwise — without it, a lost update silently executes against state the approver never saw.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "title": "StatePin",
/// "description": "The prior state the effects were computed against. A human in the loop makes the approval window minutes wide, so the state can move underneath a pending approval. The executor asserts this pin still holds at execution and refuses otherwise — without it, a lost update silently executes against state the approver never saw.",
/// "type": "object",
/// "required": [
/// "resource",
/// "version"
/// ],
/// "properties": {
/// "resource": {
/// "description": "Identifier of the pinned resource (usually the subject DID).",
/// "type": "string",
/// "minLength": 1
/// },
/// "version": {
/// "description": "Opaque version identifier of the prior state (e.g. a did:webvh `versionId`). Compared for equality at execution; the executor MUST NOT attempt to order or interpret it.",
/// "type": "string",
/// "minLength": 1
/// }
/// },
/// "additionalProperties": false
///}
/// ```
/// </details>
#[derive(::serde::Deserialize, ::serde::Serialize, Clone, Debug)]
#[serde(deny_unknown_fields)]
pub struct StatePin {
///Identifier of the pinned resource (usually the subject DID).
pub resource: StatePinResource,
///Opaque version identifier of the prior state (e.g. a did:webvh `versionId`). Compared for equality at execution; the executor MUST NOT attempt to order or interpret it.
pub version: StatePinVersion,
}
///Identifier of the pinned resource (usually the subject DID).
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "Identifier of the pinned resource (usually the subject DID).",
/// "type": "string",
/// "minLength": 1
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct StatePinResource(::std::string::String);
impl ::std::ops::Deref for StatePinResource {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<StatePinResource> for ::std::string::String {
fn from(value: StatePinResource) -> Self {
value.0
}
}
impl ::std::str::FromStr for StatePinResource {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
if value.chars().count() < 1usize {
return Err("shorter than 1 characters".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for StatePinResource {
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 StatePinResource {
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 StatePinResource {
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 StatePinResource {
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())
})
}
}
///Opaque version identifier of the prior state (e.g. a did:webvh `versionId`). Compared for equality at execution; the executor MUST NOT attempt to order or interpret it.
///
/// <details><summary>JSON schema</summary>
///
/// ```json
///{
/// "description": "Opaque version identifier of the prior state (e.g. a did:webvh `versionId`). Compared for equality at execution; the executor MUST NOT attempt to order or interpret it.",
/// "type": "string",
/// "minLength": 1
///}
/// ```
/// </details>
#[derive(::serde::Serialize, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
#[serde(transparent)]
pub struct StatePinVersion(::std::string::String);
impl ::std::ops::Deref for StatePinVersion {
type Target = ::std::string::String;
fn deref(&self) -> &::std::string::String {
&self.0
}
}
impl ::std::convert::From<StatePinVersion> for ::std::string::String {
fn from(value: StatePinVersion) -> Self {
value.0
}
}
impl ::std::str::FromStr for StatePinVersion {
type Err = self::error::ConversionError;
fn from_str(value: &str) -> ::std::result::Result<Self, self::error::ConversionError> {
if value.chars().count() < 1usize {
return Err("shorter than 1 characters".into());
}
Ok(Self(value.to_string()))
}
}
impl ::std::convert::TryFrom<&str> for StatePinVersion {
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 StatePinVersion {
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 StatePinVersion {
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 StatePinVersion {
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())
})
}
}
impl crate::Payload for Payload {
const TYPE_URI: &'static str = "https://trusttasks.org/spec/task-consent/request/0.1";
const IS_PROOF_REQUIRED: bool = true;
const IS_RECIPIENT_REQUIRED: bool = true;
}
impl crate::Payload for Response {
const TYPE_URI: &'static str = "https://trusttasks.org/spec/task-consent/request/0.1#response";
const IS_PROOF_REQUIRED: bool = true;
const IS_RECIPIENT_REQUIRED: bool = true;
}
#[cfg(feature = "validate")]
impl crate::validate::ValidatedPayload for Payload {
const SCHEMA_JSON: &'static str = "{\n \"$defs\": {\n \"Effect\": {\n \"additionalProperties\": false,\n \"description\": \"One consequence of executing the pending task, authored by the executor that is about to run it. An effect is produced by dry-running the real handler against the executor's own prior state — never by the requester, and never by re-implementing the handler's semantics elsewhere. A consent surface renders ONLY effects it received under the executor's signature.\",\n \"properties\": {\n \"after\": {\n \"description\": \"OPTIONAL resulting value at `path` once the task executes. ABSENT means the value is removed (a deletion), under the same rule as `before`.\"\n },\n \"before\": {\n \"description\": \"OPTIONAL prior value at `path`, from the executor's authoritative state. ABSENT means there was no prior value (a creation); an explicit `null` is treated identically, so an executor MUST NOT rely on the two being distinguishable. Structured members are for rich rendering only — `summary` is what a surface is obliged to show, and it is where an executor states a distinction this shape cannot carry.\"\n },\n \"detail\": {\n \"additionalProperties\": true,\n \"description\": \"OPTIONAL kind-specific structured extras (e.g. `{ \\\"commitments\\\": 2 }` for `preRotationRefresh`).\",\n \"type\": \"object\"\n },\n \"kind\": {\n \"description\": \"Machine discriminator for rich rendering. Well-known kinds are registered in the prose (`documentChange`, `keyRotation`, `preRotationRefresh`, `resourceDelete`, `disclosure`), but the set is OPEN: handlers evolve faster than this schema, and an executor MUST be able to describe a consequence this version does not name. A surface that does not recognise a `kind` MUST still render `summary`.\",\n \"pattern\": \"^[a-z][a-zA-Z0-9]*$\",\n \"type\": \"string\"\n },\n \"path\": {\n \"description\": \"OPTIONAL RFC 6901 JSON Pointer locating the change within the subject resource (e.g. `/service/0`).\",\n \"type\": \"string\"\n },\n \"summary\": {\n \"description\": \"REQUIRED human-facing sentence describing this consequence, authored by the executor. This is the ONLY member a consent surface is guaranteed to be able to render, and it is what makes an unrecognised `kind` degrade to something truthful rather than something invisible. A surface MUST render it verbatim; it MUST NOT substitute its own prose.\",\n \"minLength\": 1,\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"kind\",\n \"summary\"\n ],\n \"title\": \"Effect\",\n \"type\": \"object\"\n },\n \"Exposure\": {\n \"additionalProperties\": false,\n \"description\": \"The authoritative SPEC §7.3 item 14 exposure class of the pending task, derived by the executor from the compiled handler it is about to invoke — never from the registry's declared value.\",\n \"properties\": {\n \"actsAsSubject\": {\n \"description\": \"Whether execution exercises the subject's own authority to produce an attributable effect in the subject's name.\",\n \"type\": \"boolean\"\n },\n \"discloses\": {\n \"description\": \"Sensitivity of data the task returns to its caller.\",\n \"enum\": [\n \"none\",\n \"metadata\",\n \"secret\"\n ],\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"discloses\",\n \"actsAsSubject\"\n ],\n \"title\": \"Exposure\",\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\": \"Synchronous acknowledgement from the approver device that a prompt was raised. The decision itself arrives separately as a task-consent/decision — a human is in the loop, so it cannot be a synchronous reply.\",\n \"properties\": {\n \"ext\": {\n \"$ref\": \"#/$defs/Ext\"\n },\n \"reason\": {\n \"description\": \"REQUIRED when status is `refused`.\",\n \"type\": \"string\"\n },\n \"status\": {\n \"description\": \"`prompted` = the request was verified and an approval prompt raised. `refused` = the device will not prompt; `reason` MUST be set.\",\n \"enum\": [\n \"prompted\",\n \"refused\"\n ],\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"status\"\n ],\n \"title\": \"Task Consent Request — response payload\",\n \"type\": \"object\"\n },\n \"StatePin\": {\n \"additionalProperties\": false,\n \"description\": \"The prior state the effects were computed against. A human in the loop makes the approval window minutes wide, so the state can move underneath a pending approval. The executor asserts this pin still holds at execution and refuses otherwise — without it, a lost update silently executes against state the approver never saw.\",\n \"properties\": {\n \"resource\": {\n \"description\": \"Identifier of the pinned resource (usually the subject DID).\",\n \"minLength\": 1,\n \"type\": \"string\"\n },\n \"version\": {\n \"description\": \"Opaque version identifier of the prior state (e.g. a did:webvh `versionId`). Compared for equality at execution; the executor MUST NOT attempt to order or interpret it.\",\n \"minLength\": 1,\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"resource\",\n \"version\"\n ],\n \"title\": \"StatePin\",\n \"type\": \"object\"\n }\n },\n \"$id\": \"https://trusttasks.org/spec/task-consent/request/0.1\",\n \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n \"additionalProperties\": false,\n \"description\": \"The executor asks an enrolled approver device to authorize one pending privileged task, presenting the effects it computed by dry-running the real handler against its own prior state. The executor signs this document; the approver renders only what it verifies under that signature.\",\n \"properties\": {\n \"approverSet\": {\n \"description\": \"Name of the approver set the policy's `requireConsent` named.\",\n \"type\": \"string\"\n },\n \"challenge\": {\n \"description\": \"Per-request nonce (≥128 bits entropy) echoed and signed by the matching task-consent/decision. It is also the salt in `payloadDigest`, so a party that holds this request can re-derive the digest and a party that does not cannot invert it.\",\n \"minLength\": 16,\n \"type\": \"string\"\n },\n \"consequences\": {\n \"description\": \"Static, human-facing second-order effects declared in the task's specification (SPEC §7.3 item 13). A fallback for handlers with no dry-run — per-task, not per-request, so it can say 'any document change rotates the update keys' but not which keys. Prefer `effects`.\",\n \"items\": {\n \"minLength\": 1,\n \"type\": \"string\"\n },\n \"type\": \"array\"\n },\n \"effects\": {\n \"description\": \"What executing this task will actually do, computed by dry-running the real handler against the executor's prior state. MAY be empty when the executor has no dry-run for this handler — in which case `consequences` carries the specification's static fallback text. When BOTH are empty the surface MUST tell the approver the consequences could not be determined, and MUST NOT present the task as though it had none.\",\n \"items\": {\n \"$ref\": \"#/$defs/Effect\"\n },\n \"type\": \"array\"\n },\n \"excludeRequester\": {\n \"description\": \"When true, `requester` may not count toward the threshold — so a single compromised device cannot both propose and approve. A surface MUST NOT offer to approve a request whose `requester` is its own subject when this is set.\",\n \"type\": \"boolean\"\n },\n \"expiresAt\": {\n \"description\": \"After this instant the pending request lapses and no decision is accepted for it.\",\n \"format\": \"date-time\",\n \"type\": \"string\"\n },\n \"exposure\": {\n \"$ref\": \"#/$defs/Exposure\"\n },\n \"ext\": {\n \"$ref\": \"#/$defs/Ext\"\n },\n \"minApprovals\": {\n \"description\": \"Distinct approvals from the set required before the task may execute.\",\n \"minimum\": 1,\n \"type\": \"integer\"\n },\n \"origin\": {\n \"description\": \"OPTIONAL web origin that proposed the task, when it arrived via a relying party. This MUST be the origin the consumer's own runtime attested (e.g. the browser-supplied sender origin) — never a value the proposing page supplied, and never one the relying party could author.\",\n \"type\": \"string\"\n },\n \"payloadDigest\": {\n \"description\": \"The binding between what the approver sees and what executes. Digest of the canonical (RFC 8785 JCS) payload, the task type, and the `challenge` as salt. The decision echoes it; the executor re-derives it from the payload it is about to run and refuses on mismatch. Salted because an unsalted digest over a low-entropy payload is a confirmation oracle for anyone who observes it in transit.\",\n \"type\": \"string\"\n },\n \"requester\": {\n \"description\": \"DID that submitted the pending task. Shown to the approver, and compared against the approver when `excludeRequester` is set.\",\n \"type\": \"string\"\n },\n \"requesterDeviceId\": {\n \"description\": \"OPTIONAL enrolled device the task was submitted from.\",\n \"type\": \"string\"\n },\n \"sideEffects\": {\n \"description\": \"Authoritative SPEC §7.3 item 13 side-effect class of the pending task, derived by the executor from the compiled handler it is about to invoke. NEVER taken from the registry: a registry that decided this would be a consent kill-switch, downgradeable by publishing a new version with a weaker class.\",\n \"enum\": [\n \"none\",\n \"mutating\",\n \"destructive\"\n ],\n \"type\": \"string\"\n },\n \"statePin\": {\n \"$ref\": \"#/$defs/StatePin\"\n },\n \"subject\": {\n \"description\": \"OPTIONAL identifier the task acts on — the value at the specification's `subjectPath`, usually a DID. Absent for subjectless tasks.\",\n \"type\": \"string\"\n },\n \"taskType\": {\n \"description\": \"Type URI of the task awaiting approval. It is bound into `payloadDigest`: without that binding, two tasks whose payloads canonicalize identically would share a digest, and an approval for a benign task would authorize a destructive one.\",\n \"format\": \"uri\",\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"challenge\",\n \"taskType\",\n \"payloadDigest\",\n \"sideEffects\",\n \"exposure\",\n \"effects\",\n \"requester\",\n \"approverSet\",\n \"minApprovals\",\n \"excludeRequester\",\n \"expiresAt\"\n ],\n \"title\": \"Task Consent Request — payload\",\n \"type\": \"object\"\n}\n";
}
#[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:9f2c1e70-6f0a-4a1b-9a3f-7c2b1d5e8a44\",\n \"type\": \"https://trusttasks.org/spec/task-consent/request/0.1\",\n \"issuer\": \"did:key:z6MkExecutorVtaExample\",\n \"recipient\": \"did:key:z6MkApproverPhoneExample\",\n \"issuedAt\": \"2026-07-13T09:41:00Z\",\n \"payload\": {\n \"challenge\": \"9c1f4b7a2e6d80f35a4c9b1e7d2f6083\",\n \"taskType\": \"https://trusttasks.org/spec/webvh/dids/update/1.0\",\n \"payloadDigest\": \"3b0c7f1d9e2a5648c1f30b7ae4d2986153ca0f7b8d41e6295af03c8bd71e4a62\",\n \"sideEffects\": \"mutating\",\n \"exposure\": {\n \"discloses\": \"none\",\n \"actsAsSubject\": false\n },\n \"subject\": \"did:webvh:QmSCIDExample:example.com:acme\",\n \"requester\": \"did:key:z6MkRequesterBrowserExample\",\n \"requesterDeviceId\": \"dev-8f21c0\",\n \"origin\": \"https://control.example.com\",\n \"statePin\": {\n \"resource\": \"did:webvh:QmSCIDExample:example.com:acme\",\n \"version\": \"3-QmPriorEntryHashExample\"\n },\n \"effects\": [\n {\n \"kind\": \"documentChange\",\n \"summary\": \"Adds a FileStore service endpoint at #files. There is no prior value at this path.\",\n \"path\": \"/service/0\",\n \"after\": {\n \"id\": \"#files\",\n \"type\": \"FileStore\",\n \"serviceEndpoint\": \"https://files.example.com/acme\"\n }\n },\n {\n \"kind\": \"keyRotation\",\n \"summary\": \"Rotates this DID's update key — any document change rotates it. The current update key stops being able to authorize changes.\",\n \"before\": [\"z6MkfrQCurrentUpdateKeyExample\"],\n \"after\": [\"z6MkpB2NextUpdateKeyExample\"]\n },\n {\n \"kind\": \"preRotationRefresh\",\n \"summary\": \"Refreshes 2 pre-rotation commitments for the next rotation.\",\n \"detail\": { \"commitments\": 2 }\n }\n ],\n \"approverSet\": \"operators\",\n \"minApprovals\": 1,\n \"excludeRequester\": true,\n \"expiresAt\": \"2026-07-13T09:56:00Z\"\n },\n \"proof\": {\n \"type\": \"DataIntegrityProof\",\n \"cryptosuite\": \"eddsa-jcs-2022\",\n \"created\": \"2026-07-13T09:41:00Z\",\n \"verificationMethod\": \"did:key:z6MkExecutorVtaExample#z6MkExecutorVtaExample\",\n \"proofPurpose\": \"assertionMethod\",\n \"proofValue\": \"z3FXQmV6ExampleProofValueForTaskConsentRequest\"\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:1d7e4c05-3b8f-49a2-8c61-0e5f2a9d3b17\",\n \"type\": \"https://trusttasks.org/spec/task-consent/request/0.1#response\",\n \"issuer\": \"did:key:z6MkApproverPhoneExample\",\n \"recipient\": \"did:key:z6MkExecutorVtaExample\",\n \"issuedAt\": \"2026-07-13T09:41:02Z\",\n \"payload\": {\n \"status\": \"prompted\"\n },\n \"proof\": {\n \"type\": \"DataIntegrityProof\",\n \"cryptosuite\": \"eddsa-jcs-2022\",\n \"created\": \"2026-07-13T09:41:02Z\",\n \"verificationMethod\": \"did:key:z6MkApproverPhoneExample#z6MkApproverPhoneExample\",\n \"proofPurpose\": \"assertionMethod\",\n \"proofValue\": \"z58aKqExampleProofValueForTaskConsentRequestResponse\"\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");
}
/// 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)] = &[
(
"Missing effects — an executor must send the member even when it is empty, so that 'no dry-run' is explicit rather than indistinguishable from 'no consequences'.",
"{\n \"approverSet\": \"operators\",\n \"challenge\": \"9c1f4b7a2e6d80f35a4c9b1e7d2f6083\",\n \"excludeRequester\": true,\n \"expiresAt\": \"2026-07-13T09:56:00Z\",\n \"exposure\": {\n \"actsAsSubject\": false,\n \"discloses\": \"none\"\n },\n \"minApprovals\": 1,\n \"payloadDigest\": \"3b0c7f1d9e2a5648c1f30b7ae4d2986153ca0f7b8d41e6295af03c8bd71e4a62\",\n \"requester\": \"did:key:z6MkRequesterBrowserExample\",\n \"sideEffects\": \"mutating\",\n \"taskType\": \"https://trusttasks.org/spec/webvh/dids/update/1.0\"\n}",
),
(
"Effect without a summary — a surface could not render an unrecognised kind truthfully.",
"{\n \"approverSet\": \"operators\",\n \"challenge\": \"9c1f4b7a2e6d80f35a4c9b1e7d2f6083\",\n \"effects\": [\n {\n \"after\": [\n \"z6MkB\"\n ],\n \"before\": [\n \"z6MkA\"\n ],\n \"kind\": \"keyRotation\"\n }\n ],\n \"excludeRequester\": true,\n \"expiresAt\": \"2026-07-13T09:56:00Z\",\n \"exposure\": {\n \"actsAsSubject\": false,\n \"discloses\": \"none\"\n },\n \"minApprovals\": 1,\n \"payloadDigest\": \"3b0c7f1d9e2a5648c1f30b7ae4d2986153ca0f7b8d41e6295af03c8bd71e4a62\",\n \"requester\": \"did:key:z6MkRequesterBrowserExample\",\n \"sideEffects\": \"mutating\",\n \"taskType\": \"https://trusttasks.org/spec/webvh/dids/update/1.0\"\n}",
),
(
"Unknown sideEffects level — the class is a closed enum; an unrecognised value must fail closed, not be treated as benign.",
"{\n \"approverSet\": \"operators\",\n \"challenge\": \"9c1f4b7a2e6d80f35a4c9b1e7d2f6083\",\n \"effects\": [],\n \"excludeRequester\": true,\n \"expiresAt\": \"2026-07-13T09:56:00Z\",\n \"exposure\": {\n \"actsAsSubject\": false,\n \"discloses\": \"none\"\n },\n \"minApprovals\": 1,\n \"payloadDigest\": \"3b0c7f1d9e2a5648c1f30b7ae4d2986153ca0f7b8d41e6295af03c8bd71e4a62\",\n \"requester\": \"did:key:z6MkRequesterBrowserExample\",\n \"sideEffects\": \"harmless\",\n \"taskType\": \"https://trusttasks.org/spec/webvh/dids/update/1.0\"\n}",
),
(
"Challenge too short — under 128 bits it is guessable, and it is also the digest salt.",
"{\n \"approverSet\": \"operators\",\n \"challenge\": \"short\",\n \"effects\": [],\n \"excludeRequester\": true,\n \"expiresAt\": \"2026-07-13T09:56:00Z\",\n \"exposure\": {\n \"actsAsSubject\": false,\n \"discloses\": \"none\"\n },\n \"minApprovals\": 1,\n \"payloadDigest\": \"3b0c7f1d9e2a5648c1f30b7ae4d2986153ca0f7b8d41e6295af03c8bd71e4a62\",\n \"requester\": \"did:key:z6MkRequesterBrowserExample\",\n \"sideEffects\": \"mutating\",\n \"taskType\": \"https://trusttasks.org/spec/webvh/dids/update/1.0\"\n}",
),
(
"minApprovals below 1 — a threshold of zero would authorize with no approver at all.",
"{\n \"approverSet\": \"operators\",\n \"challenge\": \"9c1f4b7a2e6d80f35a4c9b1e7d2f6083\",\n \"effects\": [],\n \"excludeRequester\": true,\n \"expiresAt\": \"2026-07-13T09:56:00Z\",\n \"exposure\": {\n \"actsAsSubject\": false,\n \"discloses\": \"none\"\n },\n \"minApprovals\": 0,\n \"payloadDigest\": \"3b0c7f1d9e2a5648c1f30b7ae4d2986153ca0f7b8d41e6295af03c8bd71e4a62\",\n \"requester\": \"did:key:z6MkRequesterBrowserExample\",\n \"sideEffects\": \"mutating\",\n \"taskType\": \"https://trusttasks.org/spec/webvh/dids/update/1.0\"\n}",
),
(
"Exposure missing actsAsSubject — both members are load-bearing for the policy decision.",
"{\n \"approverSet\": \"operators\",\n \"challenge\": \"9c1f4b7a2e6d80f35a4c9b1e7d2f6083\",\n \"effects\": [],\n \"excludeRequester\": true,\n \"expiresAt\": \"2026-07-13T09:56:00Z\",\n \"exposure\": {\n \"discloses\": \"none\"\n },\n \"minApprovals\": 1,\n \"payloadDigest\": \"3b0c7f1d9e2a5648c1f30b7ae4d2986153ca0f7b8d41e6295af03c8bd71e4a62\",\n \"requester\": \"did:key:z6MkRequesterBrowserExample\",\n \"sideEffects\": \"mutating\",\n \"taskType\": \"https://trusttasks.org/spec/webvh/dids/update/1.0\"\n}",
),
(
"Unknown property — the payload is closed, so nothing can ride along outside what the approver is shown.",
"{\n \"approverSet\": \"operators\",\n \"challenge\": \"9c1f4b7a2e6d80f35a4c9b1e7d2f6083\",\n \"effects\": [],\n \"excludeRequester\": true,\n \"expiresAt\": \"2026-07-13T09:56:00Z\",\n \"exposure\": {\n \"actsAsSubject\": false,\n \"discloses\": \"none\"\n },\n \"minApprovals\": 1,\n \"payloadDigest\": \"3b0c7f1d9e2a5648c1f30b7ae4d2986153ca0f7b8d41e6295af03c8bd71e4a62\",\n \"renderThisInstead\": \"Approve routine maintenance\",\n \"requester\": \"did:key:z6MkRequesterBrowserExample\",\n \"sideEffects\": \"mutating\",\n \"taskType\": \"https://trusttasks.org/spec/webvh/dids/update/1.0\"\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
);
}
}
}