openrtc 1.0.2

OpenRTC: a Rust-first P2P runtime for device discovery, signaling, and iroh/QUIC networking.
Documentation
//! Phase 7 of the OpenRTC auth + connection remediation plan
//! (`docs/plans/openrtc-auth-connection-remediation-plan.md`).
//!
//! Structured correlation log fields for scoped-session lifecycles. Every
//! event the Phase 0 parser (`tests/helpers/correlationLog.ts`) recognises
//! flows through `CorrelationContext` so a regression has one log shape
//! to grep for instead of N ad-hoc `println!` formats.
//!
//! ## Field schema (matches Phase 0 parser regexes)
//!
//!   - `session_kind=<value>` — `app-user-device` | `drive-grant-guest` | `share-anonymous` | …
//!   - `grant_id=<value>` — optional parsed identifier (legacy name; often extracted from scope)
//!   - `scope=<value>` — application scope string (e.g. `drive-grant:<id>` | `user-device` | …)
//!   - `peer_node_id=<value>` — iroh `EndpointId`
//!   - `connection_id=<value>` — deterministic local↔remote connection id
//!   - `transport_epoch=<value>` — counter bumped per transport replacement
//!   - `channel=<value>` — logical channel label (application-defined)
//!   - `token_fp=<value>` — short SHA-256 fingerprint of an admission token
//!
//! ## Usage
//!
//! ```ignore
//! use crate::client::correlation::CorrelationContext;
//! let ctx = CorrelationContext::new()
//!     .session_kind("scoped-guest")
//!     .scope("scoped:abc")
//!     .peer_node_id("node-A");
//! crate::clog!("[scoped-actor][dial]", &ctx, "attempt={} error={}", n, error);
//! ```
//!
//! Empty/None fields are omitted so log lines stay short for short-lived
//! contexts.

#![cfg(not(target_arch = "wasm32"))]

use std::fmt;

/// Correlation fields the Phase 0 parser recognises. Every scoped-session
/// lifecycle event should populate at least the fields it has cheap
/// access to; missing fields are simply omitted from the rendered line.
#[derive(Debug, Clone, Default)]
pub struct CorrelationContext {
    pub session_kind: Option<String>,
    pub grant_id: Option<String>,
    pub scope: Option<String>,
    pub peer_node_id: Option<String>,
    pub connection_id: Option<String>,
    pub transport_epoch: Option<String>,
    pub channel: Option<String>,
    pub token_fp: Option<String>,
}

impl CorrelationContext {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn session_kind(mut self, value: impl Into<String>) -> Self {
        self.session_kind = Some(value.into());
        self
    }

    pub fn grant_id(mut self, value: impl Into<String>) -> Self {
        self.grant_id = Some(value.into());
        self
    }

    pub fn scope(mut self, value: impl Into<String>) -> Self {
        self.scope = Some(value.into());
        self
    }

    pub fn peer_node_id(mut self, value: impl Into<String>) -> Self {
        self.peer_node_id = Some(value.into());
        self
    }

    pub fn connection_id(mut self, value: impl Into<String>) -> Self {
        self.connection_id = Some(value.into());
        self
    }

    pub fn transport_epoch(mut self, value: impl Into<String>) -> Self {
        self.transport_epoch = Some(value.into());
        self
    }

    pub fn channel(mut self, value: impl Into<String>) -> Self {
        self.channel = Some(value.into());
        self
    }

    pub fn token_fp(mut self, value: impl Into<String>) -> Self {
        self.token_fp = Some(value.into());
        self
    }

    /// Builder helper: extract scope/grant from a `drive-grant:<id>`-style
    /// scope string. Sets both `scope` and `grant_id` in one call.
    pub fn from_drive_grant_scope(scope: &str) -> Self {
        let mut ctx = Self::new().scope(scope);
        if let Some(grant_id) = scope.strip_prefix("drive-grant:") {
            ctx = ctx.grant_id(grant_id);
        }
        ctx
    }

    /// Returns `true` if the context contains at least one populated
    /// field. Useful for tests and for skip-the-prefix logic when
    /// callers want a tag-only line.
    pub fn is_empty(&self) -> bool {
        self.session_kind.is_none()
            && self.grant_id.is_none()
            && self.scope.is_none()
            && self.peer_node_id.is_none()
            && self.connection_id.is_none()
            && self.transport_epoch.is_none()
            && self.channel.is_none()
            && self.token_fp.is_none()
    }
}

impl fmt::Display for CorrelationContext {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let mut first = true;
        let mut emit = |k: &str, v: &str| -> fmt::Result {
            if !first {
                write!(f, " ")?;
            }
            first = false;
            write!(f, "{}={}", k, v)
        };
        if let Some(v) = &self.session_kind {
            emit("session_kind", v)?;
        }
        if let Some(v) = &self.grant_id {
            emit("grant_id", v)?;
        }
        if let Some(v) = &self.scope {
            emit("scope", v)?;
        }
        if let Some(v) = &self.peer_node_id {
            emit("peer_node_id", v)?;
        }
        if let Some(v) = &self.connection_id {
            emit("connection_id", v)?;
        }
        if let Some(v) = &self.transport_epoch {
            emit("transport_epoch", v)?;
        }
        if let Some(v) = &self.channel {
            emit("channel", v)?;
        }
        if let Some(v) = &self.token_fp {
            emit("token_fp", v)?;
        }
        Ok(())
    }
}

/// Phase 7 structured-log macro. Emits a single `println!` line of the
/// form `<tag> <ctx-fields> <message>` — exactly the shape the Phase 0
/// `correlationLog.ts` parser already understands.
///
/// Examples:
///
/// ```ignore
/// clog!("[drive-grant-actor]", &ctx, "ensure_ready start");
/// clog!(
///     "[PlutoRTC][session-token-presentation]",
///     &ctx,
///     "dialer write+finish done payload_len={}",
///     payload_len,
/// );
/// ```
#[macro_export]
macro_rules! clog {
    ($tag:expr, $ctx:expr $(,)?) => {
        {
            let ctx_ref: &$crate::client::correlation::CorrelationContext = $ctx;
            if ctx_ref.is_empty() {
                println!("{}", $tag);
            } else {
                println!("{} {}", $tag, ctx_ref);
            }
        }
    };
    ($tag:expr, $ctx:expr, $($rest:tt)+) => {
        {
            let ctx_ref: &$crate::client::correlation::CorrelationContext = $ctx;
            let body = format!($($rest)+);
            if ctx_ref.is_empty() {
                println!("{} {}", $tag, body);
            } else {
                println!("{} {} {}", $tag, ctx_ref, body);
            }
        }
    };
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn renders_only_populated_fields() {
        let ctx = CorrelationContext::new()
            .session_kind("drive-grant-guest")
            .scope("drive-grant:abc")
            .peer_node_id("node-A");
        let rendered = format!("{}", ctx);
        assert_eq!(
            rendered,
            "session_kind=drive-grant-guest scope=drive-grant:abc peer_node_id=node-A"
        );
    }

    #[test]
    fn empty_context_renders_empty_string() {
        assert_eq!(format!("{}", CorrelationContext::new()), "");
        assert!(CorrelationContext::new().is_empty());
    }

    #[test]
    fn from_drive_grant_scope_extracts_grant_id() {
        let ctx = CorrelationContext::from_drive_grant_scope("drive-grant:my-grant-uuid");
        assert_eq!(ctx.scope.as_deref(), Some("drive-grant:my-grant-uuid"));
        assert_eq!(ctx.grant_id.as_deref(), Some("my-grant-uuid"));
    }

    #[test]
    fn from_drive_grant_scope_leaves_grant_id_unset_for_non_grant_scopes() {
        let ctx = CorrelationContext::from_drive_grant_scope("user-device");
        assert_eq!(ctx.scope.as_deref(), Some("user-device"));
        assert!(ctx.grant_id.is_none());
    }

    #[test]
    fn full_field_set_orders_deterministically() {
        let ctx = CorrelationContext::new()
            .session_kind("drive-grant-guest")
            .grant_id("g1")
            .scope("drive-grant:g1")
            .peer_node_id("node-A")
            .connection_id("conn-1")
            .transport_epoch("3")
            .channel("drive-view.list")
            .token_fp("ab12cd34");
        let rendered = format!("{}", ctx);
        assert_eq!(
            rendered,
            "session_kind=drive-grant-guest grant_id=g1 scope=drive-grant:g1 \
             peer_node_id=node-A connection_id=conn-1 transport_epoch=3 \
             channel=drive-view.list token_fp=ab12cd34"
        );
    }
}