riffdb_types/secret.rs
1//! The stable redaction marker for secret-classified fields (ADR-0118).
2//!
3//! The value-bearing wrapper (`SecretValue`) and the reveal authority live in
4//! `riffdb-policy`, because reveal authority is mintable only from a policy
5//! evaluation's sealed [`FieldMask`](https://docs.rs) obligation. This module
6//! carries only the value-FREE vocabulary shared by display surfaces: the
7//! marker text and the withheld-field descriptor.
8//!
9//! The classification is display-and-visibility metadata only: durable
10//! storage, backups, exports, and changelog frames carry secret fields at
11//! full fidelity, and no cryptographic property is implied.
12
13use std::fmt;
14use std::sync::Arc;
15
16use crate::ids::FieldId;
17
18/// Renders the one stable redaction marker for a secret-classified field.
19///
20/// The exact shape — `[redacted:field_name]` — is a public contract: every
21/// display surface emits this form and the redaction sweep asserts its
22/// presence byte-for-byte. Change it nowhere or everywhere.
23#[must_use]
24pub fn secret_redaction_marker(field_name: &str) -> String {
25 format!("[redacted:{field_name}]")
26}
27
28/// One secret-classified field withheld from a released record.
29///
30/// Carries no value — only the field identity and the marker every display
31/// surface renders in the value's place. Constructing one is harmless by
32/// design (there is nothing inside to protect), which is why it may live
33/// here while the value-bearing wrapper is sealed inside `riffdb-policy`.
34#[derive(Clone, Eq, PartialEq)]
35pub struct RedactedSecretField {
36 field: FieldId,
37 field_name: Arc<str>,
38}
39
40impl RedactedSecretField {
41 /// Describes one withheld secret field.
42 #[must_use]
43 pub fn new(field: FieldId, field_name: impl Into<Arc<str>>) -> Self {
44 Self {
45 field,
46 field_name: field_name.into(),
47 }
48 }
49
50 /// The withheld field's stable ID.
51 #[must_use]
52 pub const fn field(&self) -> FieldId {
53 self.field
54 }
55
56 /// The withheld field's source name.
57 #[must_use]
58 pub fn field_name(&self) -> &str {
59 &self.field_name
60 }
61
62 /// The stable redaction marker for this field.
63 #[must_use]
64 pub fn redaction_marker(&self) -> String {
65 secret_redaction_marker(&self.field_name)
66 }
67}
68
69impl fmt::Debug for RedactedSecretField {
70 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71 write!(formatter, "{}", secret_redaction_marker(&self.field_name))
72 }
73}
74
75impl fmt::Display for RedactedSecretField {
76 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
77 write!(formatter, "{}", secret_redaction_marker(&self.field_name))
78 }
79}
80
81#[cfg(test)]
82mod tests {
83 use super::*;
84
85 /// The exact marker text is a public contract, tested once here and
86 /// consumed everywhere.
87 #[test]
88 fn marker_has_the_exact_documented_shape() {
89 assert_eq!(
90 secret_redaction_marker("token_hash"),
91 "[redacted:token_hash]"
92 );
93 }
94
95 /// The withheld-field descriptor renders only the marker in every form.
96 #[test]
97 fn descriptor_renders_only_the_marker() {
98 let redacted = RedactedSecretField::new(FieldId::first(), "token_hash");
99 assert_eq!(format!("{redacted}"), "[redacted:token_hash]");
100 assert_eq!(format!("{redacted:?}"), "[redacted:token_hash]");
101 assert_eq!(redacted.redaction_marker(), "[redacted:token_hash]");
102 }
103}