Skip to main content

rustev_contract/
remote_sink.rs

1//! Transport-neutral evidence delivery documents (spec 042, 3.11).
2//! A receipt is a delivery observation, never authorization.
3
4use crate::DocumentError;
5use crate::ids::ContentDigest;
6use crate::remote::RemoteCode;
7use crate::remote_context::{checked_document, invalid};
8use crate::run::RunRecord;
9use crate::scope::Handle;
10use serde::{Deserialize, Serialize};
11
12pub const PROTOCOL: &str = "rustev.remote-sink/1";
13pub const DESCRIBE: &str = "rustev.remote-sink-describe/1";
14pub const DELIVER: &str = "rustev.remote-sink-deliver/1";
15
16#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
17#[serde(deny_unknown_fields)]
18pub struct DescribeRequest {
19    pub schema: String,
20    pub versions: Vec<u32>,
21}
22crate::document::document!(DescribeRequest, DESCRIBE, crate::limits::REMOTE_V1);
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
25#[serde(deny_unknown_fields)]
26pub struct DescribeResponse {
27    pub schema: String,
28    pub version: u32,
29    pub record_schemas: Vec<RecordSchema>,
30    /// Sink-defined states; no durability meaning is inferred from a name.
31    pub durability_states: Vec<String>,
32    pub deduplication_window_ms: u64,
33    pub max_record_bytes: u64,
34}
35crate::document::document!(DescribeResponse, DESCRIBE, crate::limits::REMOTE_V1);
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, schemars::JsonSchema)]
38pub enum RecordSchema {
39    #[serde(rename = "rustev.run/1")]
40    V1,
41    #[serde(rename = "rustev.run/2")]
42    V2,
43    #[serde(rename = "rustev.run/3")]
44    V3,
45}
46impl RecordSchema {
47    pub const fn as_str(self) -> &'static str {
48        match self {
49            Self::V1 => crate::schema::RUN,
50            Self::V2 => crate::schema::RUN_V2,
51            Self::V3 => crate::schema::RUN_V3,
52        }
53    }
54}
55
56/// Registration's tagged content digest, using the existing digest validator.
57pub type RegistrationId = ContentDigest;
58
59/// The registration's signer observation (spec 042, 3.3.3 and 3.3.7).
60#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
61#[serde(rename_all = "snake_case", deny_unknown_fields)]
62pub enum Signer {
63    Unsigned,
64    Ed25519 { key_id: String },
65    Sigstore { identity: String, issuer: String },
66}
67
68#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
69#[serde(deny_unknown_fields)]
70pub struct DeliverRequest {
71    pub schema: String,
72    pub version: u32,
73    /// A non-empty delivery key, at most 256 bytes (spec 003, 3.9.2).
74    pub decision_id: Handle,
75    pub record_schema: RecordSchema,
76    /// Exact UTF-8 canonical run-record bytes, not a re-rendered object.
77    pub record: String,
78    pub record_digest: ContentDigest,
79    pub registration_id: RegistrationId,
80    pub signer: Signer,
81    pub service_build: String,
82    #[serde(default, skip_serializing_if = "Option::is_none")]
83    pub traceparent: Option<String>,
84}
85// The envelope can contain an escaped record at its full existing byte bound.
86// It does not enlarge the nested run parser's limits.
87pub const DELIVER_LIMITS: crate::bounded::ParseLimits = crate::bounded::ParseLimits {
88    max_bytes: 32 * 1024 * 1024,
89    max_string_bytes: 24 * 1024 * 1024,
90    ..crate::limits::REMOTE_V1
91};
92checked_document!(
93    DeliverRequest,
94    DELIVER,
95    DELIVER_LIMITS,
96    |d: &DeliverRequest| {
97        crate::remote_context::validate_traceparent(d.traceparent.as_deref())?;
98        let run = RunRecord::parse(d.record.as_bytes())?;
99        if run.schema != d.record_schema.as_str() || run.decision_id != d.decision_id.as_str() {
100            return Err(invalid(
101                "record schema or decision id differs from envelope",
102            ));
103        }
104        let canonical = run.record_canonical().map_err(|e| invalid(e.to_string()))?;
105        if canonical != d.record.as_bytes() {
106            return Err(invalid("run record is not canonical"));
107        }
108        if run.record_digest().map_err(|e| invalid(e.to_string()))? != d.record_digest {
109            return Err(invalid("run record digest differs from envelope"));
110        }
111        Ok(())
112    }
113);
114
115#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
116#[serde(deny_unknown_fields)]
117pub struct DeliverResponse {
118    pub schema: String,
119    pub result: DeliveryResult,
120}
121crate::document::document!(DeliverResponse, DELIVER, crate::limits::REMOTE_V1);
122
123#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, schemars::JsonSchema)]
124#[serde(rename_all = "snake_case", deny_unknown_fields)]
125pub enum DeliveryResult {
126    Accepted {
127        durability: String,
128        external_id: String,
129        accepted_digest: ContentDigest,
130    },
131    Duplicate {
132        accepted_digest: ContentDigest,
133    },
134    Conflict,
135    Rejected {
136        code: RemoteCode,
137    },
138}
139impl DeliverResponse {
140    /// The caller compares the receipt with the record it actually sent.
141    pub fn validate_digest(&self, sent: &ContentDigest) -> Result<(), DocumentError> {
142        match &self.result {
143            DeliveryResult::Accepted {
144                accepted_digest, ..
145            }
146            | DeliveryResult::Duplicate { accepted_digest }
147                if accepted_digest != sent =>
148            {
149                Err(invalid("malformed_response: accepted digest differs"))
150            }
151            _ => Ok(()),
152        }
153    }
154}