137 lines
5.0 KiB
Rust
137 lines
5.0 KiB
Rust
//! `BfldEvent` — privacy-gated output event. ADR-121 §2.1, ADR-122 §2.1.
|
|
//!
|
|
//! Field exposure per privacy_class (ADR-122 §2.1):
|
|
//!
|
|
//! | Field | Raw(0) | Derived(1) | Anonymous(2) | Restricted(3) |
|
|
//! |------------------------|--------|------------|--------------|---------------|
|
|
//! | presence | y | y | y | y |
|
|
//! | motion | y | y | y | y |
|
|
//! | person_count | y | y | y | y |
|
|
//! | confidence | y | y | y | y |
|
|
//! | zone_id | y | y | y | y |
|
|
//! | identity_risk_score | y | y | **y** | **n** |
|
|
//! | rf_signature_hash | y | y | **y** | **n** |
|
|
//!
|
|
//! Construction defers to [`BfldEvent::with_privacy_gating`] which applies
|
|
//! the policy by stripping disallowed fields to `None` based on the supplied
|
|
//! `privacy_class`. Direct field access remains possible (for unit tests),
|
|
//! but the JSON serializer always honors the gating because the dropped
|
|
//! fields are `None` and the `Serialize` derive uses `skip_serializing_if`.
|
|
|
|
#![cfg(feature = "std")]
|
|
|
|
use crate::PrivacyClass;
|
|
|
|
#[cfg(feature = "serde-json")]
|
|
use serde::Serialize;
|
|
|
|
/// Privacy-gated output event published by the BFLD pipeline.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
#[cfg_attr(feature = "serde-json", derive(Serialize))]
|
|
pub struct BfldEvent {
|
|
/// Always `"bfld_update"`. Tags the event type for downstream routers.
|
|
#[cfg_attr(feature = "serde-json", serde(rename = "type"))]
|
|
pub event_type: &'static str,
|
|
|
|
/// Originating BFLD node identifier.
|
|
pub node_id: String,
|
|
|
|
/// Monotonic capture-clock timestamp in nanoseconds.
|
|
pub timestamp_ns: u64,
|
|
|
|
/// Whether an occupant is present in the sensing zone.
|
|
pub presence: bool,
|
|
|
|
/// Normalized motion magnitude in `[0.0, 1.0]`.
|
|
pub motion: f32,
|
|
|
|
/// Estimated number of occupants.
|
|
pub person_count: u8,
|
|
|
|
/// Sensing confidence in `[0.0, 1.0]`.
|
|
pub confidence: f32,
|
|
|
|
/// Optional zone identifier; absent if the deployment is single-zone.
|
|
#[cfg_attr(feature = "serde-json", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub zone_id: Option<String>,
|
|
|
|
/// Privacy classification byte for this event.
|
|
#[cfg_attr(feature = "serde-json", serde(serialize_with = "ser_privacy_class"))]
|
|
pub privacy_class: PrivacyClass,
|
|
|
|
/// Identity-risk score, `[0.0, 1.0]`. Class 2 only; `None` at class 3.
|
|
#[cfg_attr(feature = "serde-json", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub identity_risk_score: Option<f32>,
|
|
|
|
/// 256-bit BLAKE3 keyed hash of the current cluster. Class 2 only; `None` at class 3.
|
|
#[cfg_attr(feature = "serde-json", serde(skip_serializing_if = "Option::is_none"))]
|
|
pub rf_signature_hash: Option<[u8; 32]>,
|
|
}
|
|
|
|
impl BfldEvent {
|
|
/// Build an event from sensing fields, applying the privacy_class policy
|
|
/// to mask identity-derived fields. `identity_risk_score` and
|
|
/// `rf_signature_hash` are nulled out at class `Restricted`.
|
|
#[must_use]
|
|
pub fn with_privacy_gating(
|
|
node_id: String,
|
|
timestamp_ns: u64,
|
|
presence: bool,
|
|
motion: f32,
|
|
person_count: u8,
|
|
confidence: f32,
|
|
zone_id: Option<String>,
|
|
privacy_class: PrivacyClass,
|
|
identity_risk_score: Option<f32>,
|
|
rf_signature_hash: Option<[u8; 32]>,
|
|
) -> Self {
|
|
let mut e = Self {
|
|
event_type: "bfld_update",
|
|
node_id,
|
|
timestamp_ns,
|
|
presence,
|
|
motion,
|
|
person_count,
|
|
confidence,
|
|
zone_id,
|
|
privacy_class,
|
|
identity_risk_score,
|
|
rf_signature_hash,
|
|
};
|
|
e.apply_privacy_gating();
|
|
e
|
|
}
|
|
|
|
/// Idempotently mask fields disallowed at the current `privacy_class`.
|
|
/// Called by [`Self::with_privacy_gating`]; exposed for callers that
|
|
/// mutate the event in place before publication.
|
|
pub fn apply_privacy_gating(&mut self) {
|
|
if self.privacy_class.as_u8() >= PrivacyClass::Restricted.as_u8() {
|
|
self.identity_risk_score = None;
|
|
self.rf_signature_hash = None;
|
|
}
|
|
}
|
|
|
|
/// Serialize to canonical JSON. Fields masked by privacy gating are omitted
|
|
/// entirely (not emitted as `null`), so a privacy-gated event is
|
|
/// observationally indistinguishable from one that never had the field set.
|
|
#[cfg(feature = "serde-json")]
|
|
pub fn to_json(&self) -> Result<String, serde_json::Error> {
|
|
serde_json::to_string(self)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "serde-json")]
|
|
fn ser_privacy_class<S: serde::Serializer>(
|
|
class: &PrivacyClass,
|
|
s: S,
|
|
) -> Result<S::Ok, S::Error> {
|
|
let name = match class {
|
|
PrivacyClass::Raw => "raw",
|
|
PrivacyClass::Derived => "derived",
|
|
PrivacyClass::Anonymous => "anonymous",
|
|
PrivacyClass::Restricted => "restricted",
|
|
};
|
|
s.serialize_str(name)
|
|
}
|