1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
//! Construction, wiring, and route/subscription accessors for CertmeshCore.
//!
//! Part of the inherent impl CertmeshCore, split from lib.rs (certmesh M2).
//! As a child module of the crate root, 'use super::*' inherits lib.rs's
//! imports, sibling modules, and crate-private state/helpers as in the original.
use super::*;
impl CertmeshCore {
/// Construct a facade from an existing shared state.
pub(crate) fn from_state(state: Arc<CertmeshState>) -> Self {
Self { state }
}
/// The resolved filesystem paths this core operates on.
///
/// The data root is resolved once at the composition root and injected
/// via the `*_with_paths` constructors; every operation reads it from
/// here. There is no ambient fallback.
pub fn paths(&self) -> &CertmeshPaths {
&self.state.paths
}
/// Create a new CertmeshCore with an unlocked CA and explicit paths.
pub fn new_with_paths(
ca: ca::CaState,
roster: Roster,
auth_state: Option<AuthState>,
paths: CertmeshPaths,
) -> Self {
let rate_limiter = load_rate_limiter(&paths);
let posture_tx = initial_posture_tx(&paths);
Self {
state: Arc::new(CertmeshState {
paths,
ca: tokio::sync::Mutex::new(Some(ca)),
roster: tokio::sync::Mutex::new(roster),
auth: tokio::sync::Mutex::new(auth_state),
pending_challenge: tokio::sync::Mutex::new(None),
rate_limiter: tokio::sync::Mutex::new(rate_limiter),
approval_tx: tokio::sync::Mutex::new(None),
event_tx: koi_common::events::event_channel().0,
posture_tx,
}),
}
}
/// Create a CertmeshCore in locked state with explicit paths.
pub fn locked_with_paths(roster: Roster, paths: CertmeshPaths) -> Self {
let rate_limiter = load_rate_limiter(&paths);
let posture_tx = initial_posture_tx(&paths);
Self {
state: Arc::new(CertmeshState {
paths,
ca: tokio::sync::Mutex::new(None),
roster: tokio::sync::Mutex::new(roster),
auth: tokio::sync::Mutex::new(None),
pending_challenge: tokio::sync::Mutex::new(None),
rate_limiter: tokio::sync::Mutex::new(rate_limiter),
approval_tx: tokio::sync::Mutex::new(None),
event_tx: koi_common::events::event_channel().0,
posture_tx,
}),
}
}
/// Create a CertmeshCore in uninitialized state with explicit paths.
///
/// HTTP routes are still mounted so `/create` is reachable on a fresh install.
/// All operations that require an initialized CA will return `CaNotInitialized`.
pub fn uninitialized_with_paths(paths: CertmeshPaths) -> Self {
let rate_limiter = load_rate_limiter(&paths);
let posture_tx = initial_posture_tx(&paths);
Self {
state: Arc::new(CertmeshState {
paths,
ca: tokio::sync::Mutex::new(None),
roster: tokio::sync::Mutex::new(Roster::empty()),
auth: tokio::sync::Mutex::new(None),
pending_challenge: tokio::sync::Mutex::new(None),
rate_limiter: tokio::sync::Mutex::new(rate_limiter),
approval_tx: tokio::sync::Mutex::new(None),
event_tx: koi_common::events::event_channel().0,
posture_tx,
}),
}
}
/// Build the HTTP router for this domain.
///
/// The binary crate mounts this at `/v1/certmesh/`.
pub fn routes(&self) -> Router {
http::routes(Arc::clone(&self.state))
}
/// Build the HTTP router for external embedding.
///
/// This mirrors `routes()` but avoids exposing CertmeshState.
pub fn http_routes(&self) -> Router {
http::routes(Arc::clone(&self.state))
}
/// Build the inter-node router for the mTLS listener.
///
/// Contains only routes that require mutual TLS between mesh members:
/// promote, health, renew, roster, set-hook.
pub fn inter_node_routes(&self) -> Router {
http::inter_node_routes(Arc::clone(&self.state))
}
/// Set the approval channel used for enrollment approvals.
pub async fn set_approval_channel(&self, tx: mpsc::Sender<ApprovalRequest>) {
*self.state.approval_tx.lock().await = Some(tx);
}
/// Subscribe to certmesh events.
pub fn subscribe(&self) -> broadcast::Receiver<CertmeshEvent> {
self.state.event_tx.subscribe()
}
/// Watch this node's posture (ADR-020 §5). The receiver always holds the
/// current [`Posture`] (so a new subscriber reads it immediately) and is
/// notified on every Open↔Authenticated transition — the signal a listener
/// supervisor uses to flip plain↔mTLS without polling. Transitions are also
/// surfaced as `KoiEvent::PostureChanged` by the embedded facade.
pub fn watch_posture(&self) -> watch::Receiver<Posture> {
self.state.posture_tx.subscribe()
}
/// Build the RFC 8555 ACME server state over this CA.
///
/// The binary calls this when starting the dedicated server-auth TLS
/// listener, passing the ACME base URL, the Koi DNS zone, and the
/// `AcmeDnsSolver` bridge. The returned `AcmeState` shares this core's CA and
/// roster (so ACME issuance lands in the same roster as TOTP enrollment), and
/// is mounted via [`acme::routes`].
pub fn acme_state(&self, config: acme::AcmeStateConfig) -> std::sync::Arc<acme::AcmeState> {
acme::AcmeState::new(Arc::clone(&self.state), config)
}
}