backbone-integrations 0.6.0

Integration registry: connectors, integration accounts and an idempotent inbound event lane, with one OAuth flow (HMAC-bound state, PKCE)
Documentation
//! Integrations Module
//!
//! Generated by metaphor-schema. Enhanced with runtime implementations.
//!
//! This module provides:
//! - Domain entities and repositories
//! - Application services
//! - HTTP and gRPC handlers
//! - Route configuration
//! - State machine enforcement
//! - Validation rules runtime
//! - RBAC middleware
//! - Trigger execution system
//! - Computed fields
//! - Workflow orchestrator

#![recursion_limit = "1024"]
#![allow(unused_imports)]

// Generated modules
pub mod domain;
pub mod infrastructure;
pub mod application;
pub mod presentation;
pub mod seeders;
pub mod exports;
// <<< CUSTOM MODULES
// The hand-authored write path: receive an inbound provider event idempotently and map it via TargetPort,
// plus the port + receiver types a composing service needs to wire the seam from one place.
pub use application::service::IntegrationsWriteService;
pub use application::service::{TargetPort, MapRequest, MapOutcome, MapRejected, MappedRef};
pub use application::service::{InboundEvent, ReceiveOutcome, NewConnector, FailedEvent, IntegrationError};
pub use application::service::{IntegrationEvent, IntegrationEventMapped, IntegrationEventSink, LoggingSink};
// The OAuth credential port (ADR-0024 amendment: the store is reached through a port, never a
// Cargo edge) — the types a composing host needs to bind the credential store for the OAuth flow.
pub use application::service::{OAuthCredentialFailure, OAuthCredentialStore, PURPOSE_OAUTH_TOKEN, TokenBundle, TokenMetadata};
// The one OAuth generation: the service + config a composing host binds (with the two ports it
// runs on), and the outbound endpoint-guard surface its transport belongs to.
pub use application::service::{
    AccountStatus, AuthorizeRequest, AuthorizeResponse, CompleteOutcome, CompleteRequest,
    IntegrationsOauthConfig, IntegrationsOauthService, OauthError, RefreshSummary,
    STATE_TTL_SECONDS,
};
pub use infrastructure::http::{OAuthTransport, ReqwestOAuthTransport};
// END CUSTOM

// Re-exports - Infrastructure
pub use infrastructure::persistence::*;

// Re-exports - Application services
pub use application::service::IntegrationConnectorService;
pub use application::service::IntegrationAccountService;
pub use application::service::IntegrationEventService;

// Re-exports - Workflows
pub use application::workflows::*;

use std::sync::Arc;
use axum::Router;
use sqlx::PgPool;

/// Integrations module configuration
///
/// Use the builder pattern to configure and register this module:
///
/// ```text
/// let integrations = IntegrationsModule::builder()
///     .with_database(pool.clone())
///     .build()?;
///
/// // Unguarded full CRUD (trusted/admin); compose a guarded router for production.
/// let router = integrations.all_crud_routes();
/// ```
pub struct IntegrationsModule {
    pub(crate) integration_connector_service: Arc<IntegrationConnectorService>,
    pub(crate) integration_account_service: Arc<IntegrationAccountService>,
    pub(crate) integration_event_service: Arc<IntegrationEventService>,
    // <<< CUSTOM FIELDS
    /// The hand-authored receive/retry/map path. The module's reason for existing — without this field
    /// it's unreachable through the public API (CLAUDE.md: "MUST register every service in the {Domain}Module builder").
    pub integrations_write_service: Arc<IntegrationsWriteService>,
    /// The one OAuth generation service. `Option` because composition opts in
    /// (`with_oauth` + the two ports); `oauth_routes()` is empty without it.
    pub integrations_oauth: Option<Arc<IntegrationsOauthService>>,
    // END CUSTOM
}

impl IntegrationsModule {
    /// Create a new module builder
    pub fn builder() -> IntegrationsModuleBuilder {
        IntegrationsModuleBuilder::new()
    }

    /// Mount ALL generated CRUD endpoints (12 per entity) with NO domain
    /// validation — the fully **unguarded** surface. A well-formed request can
    /// create invalid rows or soft-delete a referenced master out from under its
    /// dependents. Prefer a guarded composition (read + validated writes) for any
    /// real deployment; use this only in trusted/admin/seeding contexts.
    pub fn all_crud_routes(&self) -> Router {
        use presentation::http::{
            create_integration_connector_routes,
            create_integration_event_routes,
        };

        Router::new()
            .merge(create_integration_connector_routes(self.integration_connector_service.clone()))
            .merge(create_integration_event_routes(self.integration_event_service.clone()))
    }

    /// Deprecated alias for [`Self::all_crud_routes`]. `routes()` reads like
    /// "the routes" but mounts UNVALIDATED generic CRUD on every entity — a naive
    /// mount exposes unguarded writes. Compose a guarded router (read + validated
    /// writes) for production, or call `all_crud_routes()` to opt into the full
    /// unguarded surface explicitly.
    #[deprecated(note = "mounts unvalidated generic CRUD; prefer readonly_routes() + validated writes, or all_crud_routes() for the full/unguarded surface")]
    pub fn routes(&self) -> Router {
        self.all_crud_routes()
    }

    /// Read-only routes for every entity (GET endpoints only) — the safe base.
    ///
    /// Generic mutation can't reach here, so this surface cannot bypass a
    /// validated write service's invariants. Use this as the production base and
    /// merge validated write routes (or a write service's HTTP layer) onto it.
    pub fn readonly_routes(&self) -> Router {
        use presentation::http::{
            create_integration_connector_read_routes,
            create_integration_event_read_routes,
        };

        Router::new()
            .merge(create_integration_connector_read_routes(self.integration_connector_service.clone()))
            .merge(create_integration_event_read_routes(self.integration_event_service.clone()))
    }

    // <<< CUSTOM METHODS
    /// Production-safe router: connector admin CRUD + event READ-only.
    ///
    /// Integration events are produced by the idempotent `receive_event` path, not
    /// hand-mutated, so the generic event write/bulk endpoints are deliberately
    /// excluded here — a caller cannot bypass the dedup/map state machine through
    /// this surface. Mount this (or `readonly_routes()`) for production; reserve
    /// `all_crud_routes()` for trusted/admin/seeding only.
    pub fn guarded_routes(&self) -> Router {
        use presentation::http::{
            create_integration_connector_routes,
            create_integration_event_read_routes,
        };

        Router::new()
            .merge(create_integration_connector_routes(self.integration_connector_service.clone()))
            .merge(create_integration_event_read_routes(self.integration_event_service.clone()))
    }

    /// The verb-shaped OAuth surface (authorize / callback / complete /
    /// disconnect / status). Empty when the module was built without
    /// `with_oauth` — mounting it anyway is safe, it contributes no routes.
    pub fn oauth_routes(&self) -> Router {
        use presentation::http::create_oauth_routes;

        match &self.integrations_oauth {
            Some(service) => create_oauth_routes(service.clone()),
            None => Router::new(),
        }
    }
    // END CUSTOM
}

/// Builder for IntegrationsModule
pub struct IntegrationsModuleBuilder {
    db_pool: Option<PgPool>,
    // <<< CUSTOM BUILDER FIELDS
    // OAuth generation fields (the one flow's construction inputs)
    oauth_config: Option<IntegrationsOauthConfig>,
    oauth_transport: Option<Arc<dyn OAuthTransport>>,
    oauth_store: Option<Arc<dyn OAuthCredentialStore>>,
    // END CUSTOM
}

impl IntegrationsModuleBuilder {
    /// Create a new builder
    pub fn new() -> Self {
        Self {
            db_pool: None,
            // <<< CUSTOM BUILDER DEFAULTS
            oauth_config: None,
            oauth_transport: None,
            oauth_store: None,
            // END CUSTOM
        }
    }

    /// Set the database connection pool
    pub fn with_database(mut self, pool: PgPool) -> Self {
        self.db_pool = Some(pool);
        self
    }

    // <<< CUSTOM - custom builder methods
    /// Opt the module into the one OAuth generation. The config carries the
    /// deployment's public base, per-provider clients, and endpoint overrides
    /// — every override passes the endpoint guard at build time (a bad one
    /// fails the build), and the state-signing key is read from the
    /// environment variable the config names (never a file value).
    pub fn with_oauth(mut self, config: IntegrationsOauthConfig) -> Self {
        self.oauth_config = Some(config);
        self
    }

    /// The outbound OAuth transport (token exchange + server-side identity
    /// read). Defaults to [`ReqwestOAuthTransport`] when omitted.
    pub fn with_oauth_transport(mut self, transport: Arc<dyn OAuthTransport>) -> Self {
        self.oauth_transport = Some(transport);
        self
    }

    /// Bind the credential store through the port (ADR-0024 amendment: no
    /// Cargo edge — the host adapts its store onto these verbs).
    pub fn with_oauth_store(mut self, store: Arc<dyn OAuthCredentialStore>) -> Self {
        self.oauth_store = Some(store);
        self
    }
    // END CUSTOM

    /// Build the module with configured dependencies
    pub fn build(self) -> anyhow::Result<IntegrationsModule> {
        let db_pool = self.db_pool
            .ok_or_else(|| anyhow::anyhow!("Database pool not configured"))?;

        // IntegrationConnector service
        let integration_connector_repository = Arc::new(IntegrationConnectorRepository::new(db_pool.clone()));
        let integration_connector_service = Arc::new(IntegrationConnectorService::with_repository(integration_connector_repository.clone()));

        // IntegrationAccount service
        let integration_account_repository = Arc::new(IntegrationAccountRepository::new(db_pool.clone()));
        let integration_account_service = Arc::new(IntegrationAccountService::with_repository(integration_account_repository.clone()));

        // IntegrationEvent service
        let integration_event_repository = Arc::new(IntegrationEventRepository::new(db_pool.clone()));
        let integration_event_service = Arc::new(IntegrationEventService::with_repository(integration_event_repository.clone()));

        // <<< CUSTOM
        // IntegrationsWrite service — the hand-authored receive/retry/map path; constructed alongside the
        // generated CRUD services so the module ships its whole public surface from the builder.
        let integrations_write_service = Arc::new(IntegrationsWriteService::new(db_pool.clone()));
        // The one OAuth generation. Constructed only when composition opted in; a partial opt-in
        // (config without both ports) is a build error, and IntegrationsOauthService::build
        // re-validates everything fail-closed: the named env var must hold the state-signing key,
        // public_base must be present, and every endpoint override must pass the guard.
        let integrations_oauth = match (self.oauth_config, self.oauth_transport, self.oauth_store) {
            (Some(config), transport, Some(store)) => {
                let transport = match transport {
                    Some(t) => t,
                    None => Arc::new(ReqwestOAuthTransport::new()
                        .map_err(|e| anyhow::anyhow!("building the default OAuth transport: {e}"))?),
                };
                Some(Arc::new(IntegrationsOauthService::build(
                    db_pool.clone(),
                    config,
                    transport,
                    store,
                )?))
            }
            (Some(_), _, None) => {
                return Err(anyhow::anyhow!(
                    "with_oauth needs the credential store bound through the port: with_oauth_store(..)"
                ))
            }
            (None, _, _) => None,
        };
        // END CUSTOM

        Ok(IntegrationsModule {
            integration_connector_service,
            integration_account_service,
            integration_event_service,
            // <<< CUSTOM
            integrations_write_service,
            integrations_oauth,
            // END CUSTOM
        })
    }
}

impl Default for IntegrationsModuleBuilder {
    fn default() -> Self {
        Self::new()
    }
}