backbone_integrations/lib.rs
1//! Integrations Module
2//!
3//! Generated by metaphor-schema. Enhanced with runtime implementations.
4//!
5//! This module provides:
6//! - Domain entities and repositories
7//! - Application services
8//! - HTTP and gRPC handlers
9//! - Route configuration
10//! - State machine enforcement
11//! - Validation rules runtime
12//! - RBAC middleware
13//! - Trigger execution system
14//! - Computed fields
15//! - Workflow orchestrator
16
17#![recursion_limit = "1024"]
18#![allow(unused_imports)]
19
20// Generated modules
21pub mod domain;
22pub mod infrastructure;
23pub mod application;
24pub mod presentation;
25pub mod seeders;
26pub mod exports;
27// <<< CUSTOM MODULES
28// The hand-authored write path: receive an inbound provider event idempotently and map it via TargetPort,
29// plus the port + receiver types a composing service needs to wire the seam from one place.
30pub use application::service::IntegrationsWriteService;
31pub use application::service::{TargetPort, MapRequest, MapOutcome, MapRejected, MappedRef};
32pub use application::service::{InboundEvent, ReceiveOutcome, NewConnector, FailedEvent, IntegrationError};
33pub use application::service::{IntegrationEvent, IntegrationEventMapped, IntegrationEventSink, LoggingSink};
34// The OAuth credential port (ADR-0024 amendment: the store is reached through a port, never a
35// Cargo edge) — the types a composing host needs to bind the credential store for the OAuth flow.
36pub use application::service::{OAuthCredentialFailure, OAuthCredentialStore, PURPOSE_OAUTH_TOKEN, TokenBundle, TokenMetadata};
37// The one OAuth generation: the service + config a composing host binds (with the two ports it
38// runs on), and the outbound endpoint-guard surface its transport belongs to.
39pub use application::service::{
40 AccountStatus, AuthorizeRequest, AuthorizeResponse, CompleteOutcome, CompleteRequest,
41 IntegrationsOauthConfig, IntegrationsOauthService, OauthError, RefreshSummary,
42 STATE_TTL_SECONDS,
43};
44pub use infrastructure::http::{OAuthTransport, ReqwestOAuthTransport};
45// END CUSTOM
46
47// Re-exports - Infrastructure
48pub use infrastructure::persistence::*;
49
50// Re-exports - Application services
51pub use application::service::IntegrationConnectorService;
52pub use application::service::IntegrationAccountService;
53pub use application::service::IntegrationEventService;
54
55// Re-exports - Workflows
56pub use application::workflows::*;
57
58use std::sync::Arc;
59use axum::Router;
60use sqlx::PgPool;
61
62/// Integrations module configuration
63///
64/// Use the builder pattern to configure and register this module:
65///
66/// ```text
67/// let integrations = IntegrationsModule::builder()
68/// .with_database(pool.clone())
69/// .build()?;
70///
71/// // Unguarded full CRUD (trusted/admin); compose a guarded router for production.
72/// let router = integrations.all_crud_routes();
73/// ```
74pub struct IntegrationsModule {
75 pub(crate) integration_connector_service: Arc<IntegrationConnectorService>,
76 #[allow(dead_code)]
77 pub(crate) integration_account_service: Arc<IntegrationAccountService>,
78 pub(crate) integration_event_service: Arc<IntegrationEventService>,
79 // <<< CUSTOM FIELDS
80 /// The hand-authored receive/retry/map path. The module's reason for existing — without this field
81 /// it's unreachable through the public API (CLAUDE.md: "MUST register every service in the {Domain}Module builder").
82 pub integrations_write_service: Arc<IntegrationsWriteService>,
83 /// The one OAuth generation service. `Option` because composition opts in
84 /// (`with_oauth` + the two ports); `oauth_routes()` is empty without it.
85 pub integrations_oauth: Option<Arc<IntegrationsOauthService>>,
86 // END CUSTOM
87}
88
89impl IntegrationsModule {
90 /// Create a new module builder
91 pub fn builder() -> IntegrationsModuleBuilder {
92 IntegrationsModuleBuilder::new()
93 }
94
95 /// Mount ALL generated CRUD endpoints (12 per entity) with NO domain
96 /// validation — the fully **unguarded** surface. A well-formed request can
97 /// create invalid rows or soft-delete a referenced master out from under its
98 /// dependents. Prefer a guarded composition (read + validated writes) for any
99 /// real deployment; use this only in trusted/admin/seeding contexts.
100 pub fn all_crud_routes(&self) -> Router {
101 use presentation::http::{
102 create_integration_connector_routes,
103 create_integration_event_routes,
104 };
105
106 Router::new()
107 .merge(create_integration_connector_routes(self.integration_connector_service.clone()))
108 .merge(create_integration_event_routes(self.integration_event_service.clone()))
109 }
110
111 /// Deprecated alias for [`Self::all_crud_routes`]. `routes()` reads like
112 /// "the routes" but mounts UNVALIDATED generic CRUD on every entity — a naive
113 /// mount exposes unguarded writes. Compose a guarded router (read + validated
114 /// writes) for production, or call `all_crud_routes()` to opt into the full
115 /// unguarded surface explicitly.
116 #[deprecated(note = "mounts unvalidated generic CRUD; prefer readonly_routes() + validated writes, or all_crud_routes() for the full/unguarded surface")]
117 pub fn routes(&self) -> Router {
118 self.all_crud_routes()
119 }
120
121 /// Read-only routes for every entity (GET endpoints only) — the safe base.
122 ///
123 /// Generic mutation can't reach here, so this surface cannot bypass a
124 /// validated write service's invariants. Use this as the production base and
125 /// merge validated write routes (or a write service's HTTP layer) onto it.
126 pub fn readonly_routes(&self) -> Router {
127 use presentation::http::{
128 create_integration_connector_read_routes,
129 create_integration_event_read_routes,
130 };
131
132 Router::new()
133 .merge(create_integration_connector_read_routes(self.integration_connector_service.clone()))
134 .merge(create_integration_event_read_routes(self.integration_event_service.clone()))
135 }
136
137 // <<< CUSTOM METHODS
138 /// Production-safe router: connector admin CRUD + event READ-only.
139 ///
140 /// Integration events are produced by the idempotent `receive_event` path, not
141 /// hand-mutated, so the generic event write/bulk endpoints are deliberately
142 /// excluded here — a caller cannot bypass the dedup/map state machine through
143 /// this surface. Mount this (or `readonly_routes()`) for production; reserve
144 /// `all_crud_routes()` for trusted/admin/seeding only.
145 pub fn guarded_routes(&self) -> Router {
146 use presentation::http::{
147 create_integration_connector_routes,
148 create_integration_event_read_routes,
149 };
150
151 Router::new()
152 .merge(create_integration_connector_routes(self.integration_connector_service.clone()))
153 .merge(create_integration_event_read_routes(self.integration_event_service.clone()))
154 }
155
156 /// The verb-shaped OAuth surface (authorize / callback / complete /
157 /// disconnect / status). Empty when the module was built without
158 /// `with_oauth` — mounting it anyway is safe, it contributes no routes.
159 pub fn oauth_routes(&self) -> Router {
160 use presentation::http::create_oauth_routes;
161
162 match &self.integrations_oauth {
163 Some(service) => create_oauth_routes(service.clone()),
164 None => Router::new(),
165 }
166 }
167 // END CUSTOM
168}
169
170/// Builder for IntegrationsModule
171pub struct IntegrationsModuleBuilder {
172 db_pool: Option<PgPool>,
173 // <<< CUSTOM BUILDER FIELDS
174 // OAuth generation fields (the one flow's construction inputs)
175 oauth_config: Option<IntegrationsOauthConfig>,
176 oauth_transport: Option<Arc<dyn OAuthTransport>>,
177 oauth_store: Option<Arc<dyn OAuthCredentialStore>>,
178 // END CUSTOM
179}
180
181impl IntegrationsModuleBuilder {
182 /// Create a new builder
183 pub fn new() -> Self {
184 Self {
185 db_pool: None,
186 // <<< CUSTOM BUILDER DEFAULTS
187 oauth_config: None,
188 oauth_transport: None,
189 oauth_store: None,
190 // END CUSTOM
191 }
192 }
193
194 /// Set the database connection pool
195 pub fn with_database(mut self, pool: PgPool) -> Self {
196 self.db_pool = Some(pool);
197 self
198 }
199
200 // <<< CUSTOM - custom builder methods
201 /// Opt the module into the one OAuth generation. The config carries the
202 /// deployment's public base, per-provider clients, and endpoint overrides
203 /// — every override passes the endpoint guard at build time (a bad one
204 /// fails the build), and the state-signing key is read from the
205 /// environment variable the config names (never a file value).
206 pub fn with_oauth(mut self, config: IntegrationsOauthConfig) -> Self {
207 self.oauth_config = Some(config);
208 self
209 }
210
211 /// The outbound OAuth transport (token exchange + server-side identity
212 /// read). Defaults to [`ReqwestOAuthTransport`] when omitted.
213 pub fn with_oauth_transport(mut self, transport: Arc<dyn OAuthTransport>) -> Self {
214 self.oauth_transport = Some(transport);
215 self
216 }
217
218 /// Bind the credential store through the port (ADR-0024 amendment: no
219 /// Cargo edge — the host adapts its store onto these verbs).
220 pub fn with_oauth_store(mut self, store: Arc<dyn OAuthCredentialStore>) -> Self {
221 self.oauth_store = Some(store);
222 self
223 }
224 // END CUSTOM
225
226 /// Build the module with configured dependencies
227 pub fn build(self) -> anyhow::Result<IntegrationsModule> {
228 let db_pool = self.db_pool
229 .ok_or_else(|| anyhow::anyhow!("Database pool not configured"))?;
230
231 // IntegrationConnector service
232 let integration_connector_repository = Arc::new(IntegrationConnectorRepository::new(db_pool.clone()));
233 let integration_connector_service = Arc::new(IntegrationConnectorService::with_repository(integration_connector_repository.clone()));
234
235 // IntegrationAccount service
236 let integration_account_repository = Arc::new(IntegrationAccountRepository::new(db_pool.clone()));
237 let integration_account_service = Arc::new(IntegrationAccountService::with_repository(integration_account_repository.clone()));
238
239 // IntegrationEvent service
240 let integration_event_repository = Arc::new(IntegrationEventRepository::new(db_pool.clone()));
241 let integration_event_service = Arc::new(IntegrationEventService::with_repository(integration_event_repository.clone()));
242
243 // <<< CUSTOM
244 // IntegrationsWrite service — the hand-authored receive/retry/map path; constructed alongside the
245 // generated CRUD services so the module ships its whole public surface from the builder.
246 let integrations_write_service = Arc::new(IntegrationsWriteService::new(db_pool.clone()));
247 // The one OAuth generation. Constructed only when composition opted in; a partial opt-in
248 // (config without both ports) is a build error, and IntegrationsOauthService::build
249 // re-validates everything fail-closed: the named env var must hold the state-signing key,
250 // public_base must be present, and every endpoint override must pass the guard.
251 let integrations_oauth = match (self.oauth_config, self.oauth_transport, self.oauth_store) {
252 (Some(config), transport, Some(store)) => {
253 let transport = match transport {
254 Some(t) => t,
255 None => Arc::new(ReqwestOAuthTransport::new()
256 .map_err(|e| anyhow::anyhow!("building the default OAuth transport: {e}"))?),
257 };
258 Some(Arc::new(IntegrationsOauthService::build(
259 db_pool.clone(),
260 config,
261 transport,
262 store,
263 )?))
264 }
265 (Some(_), _, None) => {
266 return Err(anyhow::anyhow!(
267 "with_oauth needs the credential store bound through the port: with_oauth_store(..)"
268 ))
269 }
270 (None, _, _) => None,
271 };
272 // END CUSTOM
273
274 Ok(IntegrationsModule {
275 integration_connector_service,
276 integration_account_service,
277 integration_event_service,
278 // <<< CUSTOM
279 integrations_write_service,
280 integrations_oauth,
281 // END CUSTOM
282 })
283 }
284}
285
286impl Default for IntegrationsModuleBuilder {
287 fn default() -> Self {
288 Self::new()
289 }
290}