Skip to main content

heldar_kernel/
modules.rs

1//! Module manifests — the compile-time half of the plugin platform.
2//!
3//! A [`ModuleManifest`] describes one loaded module (an app crate today; a runtime-registered sidecar
4//! plugin in a later phase). The composing binary collects every module's manifest into
5//! [`crate::state::AppState::modules`], and `GET /api/v1/modules` serves the set so the dashboard
6//! renders its nav + routes from live truth instead of a hardcoded list. The kernel itself ships no
7//! manifest and names no module — it only carries and serves whatever the binary composes.
8
9use serde::{Deserialize, Serialize};
10
11/// Where a module comes from. Drives how the (future) plugin store shelves it and how the dashboard
12/// badges it. Runtime-imported plugins use [`ModuleKind::Imported`].
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
14#[serde(rename_all = "snake_case")]
15pub enum ModuleKind {
16    /// First-party, open (Apache-2.0) module compiled into the build.
17    Core,
18    /// First-party proprietary vertical compiled into the build.
19    Proprietary,
20    /// Third-party / user module loaded at runtime (later phase).
21    Imported,
22}
23
24/// A nav destination a module contributes to the dashboard. `icon` is a key the dashboard resolves to
25/// a glyph, falling back to a generic module glyph for unknown keys (so imported plugins still render).
26#[derive(Clone, Debug, Serialize, Deserialize)]
27pub struct NavEntry {
28    /// Client route path, e.g. `/entry`.
29    pub path: String,
30    /// Human label shown in the nav rail.
31    pub label: String,
32    /// Icon key the dashboard maps to a glyph.
33    pub icon: String,
34}
35
36/// Describes one loaded module. Serialized as-is at `GET /api/v1/modules`.
37#[derive(Clone, Debug, Serialize)]
38pub struct ModuleManifest {
39    /// Stable id, e.g. `entry`. The dashboard keys its page registry on this.
40    pub id: String,
41    /// Display name, e.g. `Access Control`.
42    pub name: String,
43    /// Module version (the crate version for compiled modules).
44    pub version: String,
45    /// Who publishes the module.
46    pub publisher: String,
47    /// Provenance (core / proprietary / imported).
48    pub kind: ModuleKind,
49    /// One-line description for the module list / store.
50    pub description: String,
51    /// Nav entries this module contributes (usually one).
52    pub nav: Vec<NavEntry>,
53    /// How the dashboard renders the module's content. Compiled modules use a `bundled` page; runtime
54    /// sidecar plugins are `iframe`-mounted at `/m/{id}/` (the kernel reverse-proxies to the sidecar).
55    pub mount: MountKind,
56    /// Reachability of a sidecar's base URL (`unknown`/`healthy`/`unreachable`); `None` for compiled.
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub health: Option<String>,
59}
60
61/// How the dashboard renders a module's content area.
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
63#[serde(rename_all = "snake_case")]
64pub enum MountKind {
65    /// A page component shipped in the dashboard bundle, keyed by module id (compiled modules).
66    Bundled,
67    /// An iframe to `/m/{id}/`, which the kernel reverse-proxies to the sidecar (imported modules).
68    Iframe,
69}
70
71impl ModuleManifest {
72    /// Convenience builder for a single-nav-entry compiled (bundled) module.
73    pub fn new(
74        id: &str,
75        name: &str,
76        version: &str,
77        publisher: &str,
78        kind: ModuleKind,
79        description: &str,
80        nav: Vec<NavEntry>,
81    ) -> Self {
82        Self {
83            id: id.to_string(),
84            name: name.to_string(),
85            version: version.to_string(),
86            publisher: publisher.to_string(),
87            kind,
88            description: description.to_string(),
89            nav,
90            mount: MountKind::Bundled,
91            health: None,
92        }
93    }
94}
95
96impl NavEntry {
97    pub fn new(path: &str, label: &str, icon: &str) -> Self {
98        Self {
99            path: path.to_string(),
100            label: label.to_string(),
101            icon: icon.to_string(),
102        }
103    }
104}
105
106/* ------------------------------------------------------------------ */
107/* Runtime sidecar registrations (Phase B)                            */
108/* ------------------------------------------------------------------ */
109
110/// The manifest a sidecar plugin presents to register itself (POST /api/v1/modules). The kernel mints
111/// a scoped API key + a webhook subscription from it and reverse-proxies `/m/{id}/*` to `base_url`.
112#[derive(Clone, Debug, Deserialize)]
113pub struct ModuleRegisterRequest {
114    /// Stable id (slug): the `/m/{id}/` mount + nav key. Must not collide with a compiled module.
115    pub id: String,
116    pub name: String,
117    #[serde(default)]
118    pub version: String,
119    #[serde(default)]
120    pub publisher: String,
121    #[serde(default)]
122    pub description: String,
123    /// The sidecar's origin the kernel reverse-proxies to (http/https), e.g. `http://127.0.0.1:9123`.
124    pub base_url: String,
125    /// Nav entries to surface (defaults to one entry at `/{id}` if omitted).
126    #[serde(default)]
127    pub nav: Vec<NavEntry>,
128    /// Event types to deliver to the sidecar's webhook (`["*"]` = all). Defaults to all.
129    #[serde(default)]
130    pub subscribes: Option<Vec<String>>,
131    /// Role of the minted API key. Restricted to least-privilege (`viewer` | `integration`).
132    #[serde(default)]
133    pub role: Option<String>,
134}
135
136/// A stored sidecar registration row.
137#[derive(Clone, Debug, sqlx::FromRow)]
138pub struct ModuleRegistration {
139    pub id: String,
140    pub name: String,
141    pub version: String,
142    pub publisher: String,
143    pub description: String,
144    pub base_url: String,
145    /// JSON array of [`NavEntry`].
146    pub nav: sqlx::types::Json<Vec<NavEntry>>,
147    /// JSON array of event-type tokens.
148    pub subscribes: sqlx::types::Json<Vec<String>>,
149    pub role: String,
150    pub api_key_id: Option<String>,
151    pub webhook_id: Option<String>,
152    pub health: String,
153    pub health_checked_at: Option<chrono::DateTime<chrono::Utc>>,
154    pub created_at: chrono::DateTime<chrono::Utc>,
155    pub updated_at: chrono::DateTime<chrono::Utc>,
156}
157
158impl ModuleRegistration {
159    /// Project the stored row into the uniform manifest the dashboard consumes (kind = imported,
160    /// iframe-mounted, with live health).
161    pub fn to_manifest(&self) -> ModuleManifest {
162        ModuleManifest {
163            id: self.id.clone(),
164            name: self.name.clone(),
165            version: self.version.clone(),
166            publisher: self.publisher.clone(),
167            kind: ModuleKind::Imported,
168            description: self.description.clone(),
169            nav: self.nav.0.clone(),
170            mount: MountKind::Iframe,
171            health: Some(self.health.clone()),
172        }
173    }
174}
175
176/// Admin-only detail for a single registration (includes the sidecar URL + minted resource ids).
177#[derive(Clone, Debug, Serialize)]
178pub struct ModuleDetail {
179    pub id: String,
180    pub name: String,
181    pub version: String,
182    pub publisher: String,
183    pub description: String,
184    pub base_url: String,
185    pub nav: Vec<NavEntry>,
186    pub subscribes: Vec<String>,
187    pub role: String,
188    pub api_key_id: Option<String>,
189    pub webhook_id: Option<String>,
190    pub health: String,
191    pub health_checked_at: Option<chrono::DateTime<chrono::Utc>>,
192    pub created_at: chrono::DateTime<chrono::Utc>,
193}
194
195impl From<&ModuleRegistration> for ModuleDetail {
196    fn from(r: &ModuleRegistration) -> Self {
197        ModuleDetail {
198            id: r.id.clone(),
199            name: r.name.clone(),
200            version: r.version.clone(),
201            publisher: r.publisher.clone(),
202            description: r.description.clone(),
203            base_url: r.base_url.clone(),
204            nav: r.nav.0.clone(),
205            subscribes: r.subscribes.0.clone(),
206            role: r.role.clone(),
207            api_key_id: r.api_key_id.clone(),
208            webhook_id: r.webhook_id.clone(),
209            health: r.health.clone(),
210            health_checked_at: r.health_checked_at,
211            created_at: r.created_at,
212        }
213    }
214}
215
216/// The once-returned credentials a freshly registered sidecar needs to configure itself.
217#[derive(Clone, Debug, Serialize)]
218pub struct ModuleRegistered {
219    pub module: ModuleDetail,
220    /// The minted API key (plaintext, returned ONCE) the sidecar uses to call kernel APIs.
221    pub api_key: String,
222    /// The HMAC-SHA256 secret (returned ONCE) the kernel signs the sidecar's webhook deliveries with.
223    pub webhook_secret: String,
224}