node-app-api 5.21.0

Shared types and C ABI definitions for the Node-App host API v1
Documentation
//! Shared types used by both native and scripted app loaders
//!
//! These types are serialized to JSON for IPC (Bun loader) and
//! converted to/from C representations for FFI (native loader).

use bitflags::bitflags;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// App metadata returned during initialization
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeAppInfo {
    /// Human-readable app name (must be unique across installed apps)
    pub name: String,
    /// Semantic version string (e.g., "0.1.0")
    pub version: String,
    /// Author name or organization
    pub author: String,
    /// Brief description of the app's functionality
    pub description: String,
    /// Declared capabilities as string list (e.g., ["http_handler", "event_listener"])
    pub capabilities: Vec<String>,
}

impl NodeAppInfo {
    /// Parse capabilities strings into the Capabilities bitflag
    pub fn capability_flags(&self) -> Capabilities {
        let mut flags = Capabilities::empty();
        for cap in &self.capabilities {
            match cap.as_str() {
                "http_handler" => flags |= Capabilities::HTTP_HANDLER,
                "event_listener" => flags |= Capabilities::EVENT_LISTENER,
                "filesystem_read" => flags |= Capabilities::FILESYSTEM_READ,
                "filesystem_write" => flags |= Capabilities::FILESYSTEM_WRITE,
                "network_outbound" => flags |= Capabilities::NETWORK_OUTBOUND,
                "system_info" => flags |= Capabilities::SYSTEM_INFO,
                "capability_provider" => flags |= Capabilities::CAPABILITY_PROVIDER,
                _ => {
                    eprintln!("[node-app-api] Unknown capability '{}', ignoring", cap);
                }
            }
        }
        flags
    }
}

bitflags! {
    /// Capability flags declaring what an app can do
    #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
    pub struct Capabilities: u32 {
        /// App can handle proxied HTTP requests
        const HTTP_HANDLER     = 0b0000_0001;  // Bit 0
        /// App can receive domain events
        const EVENT_LISTENER   = 0b0000_0010;  // Bit 1
        /// App can read files from allowed paths
        const FILESYSTEM_READ  = 0b0000_0100;  // Bit 2
        /// App can write files to allowed paths
        const FILESYSTEM_WRITE = 0b0000_1000;  // Bit 3
        /// App can make outbound network requests
        const NETWORK_OUTBOUND = 0b0001_0000;  // Bit 4
        /// App can read system information (hostname, etc.)
        const SYSTEM_INFO      = 0b0010_0000;  // Bit 5
        /// App provides service capabilities to other apps
        const CAPABILITY_PROVIDER = 0b0100_0000;  // Bit 6
    }
}

impl Capabilities {
    /// Convert to a list of capability strings for API responses
    pub fn to_string_list(&self) -> Vec<String> {
        let mut list = Vec::new();
        if self.contains(Capabilities::HTTP_HANDLER) {
            list.push("http_handler".to_string());
        }
        if self.contains(Capabilities::EVENT_LISTENER) {
            list.push("event_listener".to_string());
        }
        if self.contains(Capabilities::FILESYSTEM_READ) {
            list.push("filesystem_read".to_string());
        }
        if self.contains(Capabilities::FILESYSTEM_WRITE) {
            list.push("filesystem_write".to_string());
        }
        if self.contains(Capabilities::NETWORK_OUTBOUND) {
            list.push("network_outbound".to_string());
        }
        if self.contains(Capabilities::SYSTEM_INFO) {
            list.push("system_info".to_string());
        }
        if self.contains(Capabilities::CAPABILITY_PROVIDER) {
            list.push("capability_provider".to_string());
        }
        list
    }

    /// Parse from integer (used for database storage)
    pub fn from_bits_safe(bits: u32) -> Self {
        Capabilities::from_bits_truncate(bits)
    }

    /// Convert capability string to Capabilities flag
    pub fn from_string(s: &str) -> Option<Self> {
        match s.to_lowercase().as_str() {
            "http_handler" => Some(Capabilities::HTTP_HANDLER),
            "event_listener" => Some(Capabilities::EVENT_LISTENER),
            "filesystem_read" => Some(Capabilities::FILESYSTEM_READ),
            "filesystem_write" => Some(Capabilities::FILESYSTEM_WRITE),
            "network_outbound" => Some(Capabilities::NETWORK_OUTBOUND),
            "system_info" => Some(Capabilities::SYSTEM_INFO),
            "capability_provider" => Some(Capabilities::CAPABILITY_PROVIDER),
            _ => None,
        }
    }

    /// Get all capability names as static strings
    pub fn all_capability_names() -> &'static [&'static str] {
        &[
            "http_handler",
            "event_listener",
            "filesystem_read",
            "filesystem_write",
            "network_outbound",
            "system_info",
            "capability_provider",
        ]
    }
}

/// Caller identity injected by the host proxy after JWT validation.
/// Only present on requests that came through the scoped-JWT proxy route.
/// Apps should trust this over any HTTP headers (headers are stripped by the proxy).
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CallerContext {
    /// The app name whose scoped JWT was used (always equals the receiving app's name)
    pub app_name: String,
    /// Permissions granted by the user at consent time and embedded in the scoped JWT
    pub granted_permissions: Vec<String>,
}

/// HTTP request forwarded to an app
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppRequest {
    /// Unique request identifier for correlation
    pub id: String,
    /// HTTP method (GET, POST, PUT, DELETE, PATCH)
    pub method: String,
    /// Sub-path within the app (e.g., "/hello", "/stats")
    pub path: String,
    /// HTTP headers as key-value pairs (x-node-* headers are stripped by the proxy)
    pub headers: HashMap<String, String>,
    /// Request body as JSON value
    pub body: serde_json::Value,
    /// Caller identity set by the host proxy after scoped JWT validation.
    /// None for direct (non-proxy) requests such as internal capability calls.
    #[serde(default)]
    pub caller: Option<CallerContext>,
}

/// HTTP response from an app
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppResponse {
    /// HTTP status code
    pub status: u16,
    /// Response headers
    pub headers: HashMap<String, String>,
    /// Response body as JSON value
    pub body: serde_json::Value,
}

/// Domain event forwarded to apps
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppEvent {
    /// Event name (e.g., "payment_received", "channel_opened")
    pub name: String,
    /// Event data as JSON value
    pub data: serde_json::Value,
}

// ── Capability System Types (337-core-capability-registry) ──────────

/// A service capability request dispatched to a provider app.
/// Used for both FFI (serialized as JSON bytes) and IPC (serialized as JSON).
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct CapabilityRequest {
    /// Request correlation ID (UUID)
    pub id: String,
    /// Target capability name (dot-notation, e.g., "terminal.create_session")
    pub capability: String,
    /// Request payload as JSON value
    pub payload: serde_json::Value,
    /// Optional per-capability timeout in milliseconds (falls back to 30s default)
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub timeout_ms: Option<u64>,
    // Distributed tracing fields (426-capability-tracing-visualization)
    /// Root trace ID grouping all spans in one logical invocation.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trace_id: Option<String>,
    /// Span ID of the caller (used as parent_span_id in sub-invocations).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub span_id: Option<String>,
    /// Parent span ID for this invocation (None if root).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub parent_span_id: Option<String>,
    /// Current nesting depth (0 for root, max 16).
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub trace_depth: Option<u8>,
}

/// Response from a provider app after handling a capability request.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityResponse {
    /// Correlation ID matching the request
    pub id: String,
    /// Whether the invocation succeeded
    pub success: bool,
    /// Response data (on success) or error details (on failure)
    pub payload: serde_json::Value,
}

/// Declaration of a service capability that an app provides.
/// Used in app manifests and registration APIs.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProvidedCapability {
    /// Dot-notation capability name (e.g., "terminal.create_session")
    pub name: String,
    /// Human-readable description of what this capability does
    pub description: String,
    /// JSON Schema for request payload validation (None = no validation)
    #[serde(default)]
    pub request_schema: Option<serde_json::Value>,
    /// JSON Schema for response payload validation (None = no validation)
    #[serde(default)]
    pub response_schema: Option<serde_json::Value>,
    /// Provider priority (lower = preferred, default 100)
    #[serde(default = "default_capability_priority")]
    pub priority: i32,
    /// Example request payloads for developer documentation and testing
    #[serde(default)]
    pub examples: Vec<CapabilityExample>,
}

/// An example request payload for a capability.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityExample {
    /// Short label describing the example (e.g., "Insert a document")
    pub label: String,
    /// Example request payload
    pub request: serde_json::Value,
}

fn default_capability_priority() -> i32 {
    100
}