1pub mod config;
40pub mod host_functions;
41pub mod host_imports;
42pub mod hot_reload;
43pub mod loader;
44pub mod metrics;
45pub mod runtime;
46pub mod sandbox;
47
48pub use config::{PluginConfig, PluginRuntimeConfig, PluginRuntimeConfigBuilder};
49pub use host_functions::HostFunctionRegistry;
50pub use hot_reload::{HotReloader, ReloadError, ReloadEvent};
51pub use loader::{PluginLoadError, PluginLoader, PluginManifest, SignatureVerifier};
52pub use metrics::{HookLatency, PluginMetrics, PluginStats};
53pub use runtime::{LoadedPlugin, PluginError, PluginState, WasmPluginRuntime};
54pub use sandbox::{Permission, PluginSandbox, ResourceLimits, SecurityPolicy};
55
56use dashmap::DashMap;
57use parking_lot::RwLock;
58use std::collections::HashMap;
59use std::sync::Arc;
60use std::time::Duration;
61
62#[derive(Debug, Clone)]
64pub struct PluginMetadata {
65 pub name: String,
67
68 pub version: String,
70
71 pub description: String,
73
74 pub author: String,
76
77 pub hooks: Vec<HookType>,
79
80 pub permissions: Vec<Permission>,
82
83 pub min_memory: usize,
85
86 pub max_memory: usize,
88}
89
90impl Default for PluginMetadata {
91 fn default() -> Self {
92 Self {
93 name: String::new(),
94 version: "0.0.0".to_string(),
95 description: String::new(),
96 author: String::new(),
97 hooks: Vec::new(),
98 permissions: Vec::new(),
99 min_memory: 1024 * 1024, max_memory: 64 * 1024 * 1024, }
102 }
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
107pub enum HookType {
108 PreQuery,
110
111 PostQuery,
113
114 Authenticate,
116
117 Authorize,
119
120 CacheGet,
122
123 CacheSet,
125
126 Route,
128
129 Rewrite,
131
132 Metrics,
134
135 OnConnect,
137
138 OnDisconnect,
140
141 Custom,
143}
144
145impl HookType {
146 pub fn export_name(&self) -> &'static str {
148 match self {
149 HookType::PreQuery => "pre_query",
150 HookType::PostQuery => "post_query",
151 HookType::Authenticate => "authenticate",
152 HookType::Authorize => "authorize",
153 HookType::CacheGet => "cache_get",
154 HookType::CacheSet => "cache_set",
155 HookType::Route => "route",
156 HookType::Rewrite => "rewrite",
157 HookType::Metrics => "metrics",
158 HookType::OnConnect => "on_connect",
159 HookType::OnDisconnect => "on_disconnect",
160 HookType::Custom => "custom_hook",
161 }
162 }
163
164 #[allow(clippy::should_implement_trait)]
166 pub fn from_str(s: &str) -> Option<Self> {
167 match s.to_lowercase().as_str() {
168 "pre_query" | "prequery" => Some(HookType::PreQuery),
169 "post_query" | "postquery" => Some(HookType::PostQuery),
170 "authenticate" | "auth" => Some(HookType::Authenticate),
171 "authorize" => Some(HookType::Authorize),
172 "cache_get" | "cacheget" => Some(HookType::CacheGet),
173 "cache_set" | "cacheset" => Some(HookType::CacheSet),
174 "route" | "routing" => Some(HookType::Route),
175 "rewrite" => Some(HookType::Rewrite),
176 "metrics" => Some(HookType::Metrics),
177 "on_connect" | "connect" => Some(HookType::OnConnect),
178 "on_disconnect" | "disconnect" => Some(HookType::OnDisconnect),
179 "custom" => Some(HookType::Custom),
180 _ => None,
181 }
182 }
183}
184
185#[derive(Debug, Clone, serde::Serialize)]
187pub struct HookContext {
188 pub request_id: String,
190
191 pub client_id: Option<String>,
193
194 pub identity: Option<String>,
196
197 pub database: Option<String>,
199
200 pub branch: Option<String>,
202
203 pub attributes: HashMap<String, String>,
205}
206
207impl Default for HookContext {
208 fn default() -> Self {
209 Self {
210 request_id: uuid::Uuid::new_v4().to_string(),
211 client_id: None,
212 identity: None,
213 database: None,
214 branch: None,
215 attributes: HashMap::new(),
216 }
217 }
218}
219
220#[derive(Debug, Clone)]
222pub struct QueryContext {
223 pub query: String,
225
226 pub normalized: String,
228
229 pub tables: Vec<String>,
231
232 pub is_read_only: bool,
234
235 pub hook_context: HookContext,
237}
238
239#[derive(Debug, Clone)]
241pub enum PreQueryResult {
242 Continue,
244
245 Rewrite(String),
247
248 Block(String),
250
251 Cached(Vec<u8>),
253}
254
255#[derive(Debug, Clone, serde::Serialize)]
261pub struct PostQueryOutcome {
262 pub success: bool,
264
265 pub target_node: Option<String>,
267
268 pub elapsed_us: u64,
270
271 pub response_bytes: u64,
273
274 pub error: Option<String>,
276}
277
278#[derive(Debug, Clone)]
280pub enum AuthResult {
281 Success(Identity),
283
284 Denied(String),
286
287 Defer,
289}
290
291#[derive(Debug, Clone, Default)]
293pub struct Identity {
294 pub user_id: String,
296
297 pub username: String,
299
300 pub roles: Vec<String>,
302
303 pub tenant_id: Option<String>,
305
306 pub claims: HashMap<String, String>,
308}
309
310#[derive(Debug, Clone)]
312pub enum RouteResult {
313 Default,
315
316 Node(String),
318
319 Primary,
321
322 Standby,
324
325 Branch(String),
327
328 Block(String),
334}
335
336pub struct PluginManager {
338 runtime: Arc<WasmPluginRuntime>,
340
341 plugins: DashMap<String, Arc<LoadedPlugin>>,
343
344 hooks: RwLock<HashMap<HookType, Vec<String>>>,
346
347 #[allow(dead_code)]
349 config: PluginRuntimeConfig,
350
351 hot_reloader: Option<HotReloader>,
353
354 metrics: Arc<PluginMetrics>,
356}
357
358impl PluginManager {
359 pub fn new(config: PluginRuntimeConfig) -> Result<Self, PluginError> {
361 let runtime = Arc::new(WasmPluginRuntime::new(&config)?);
362 let metrics = Arc::new(PluginMetrics::new());
363
364 let hot_reloader = if config.hot_reload {
365 Some(HotReloader::new(&config.plugin_dir)?)
366 } else {
367 None
368 };
369
370 Ok(Self {
371 runtime,
372 plugins: DashMap::new(),
373 hooks: RwLock::new(HashMap::new()),
374 config,
375 hot_reloader,
376 metrics,
377 })
378 }
379
380 pub fn load_plugin(&self, path: &std::path::Path) -> Result<(), PluginError> {
384 let mut loader = PluginLoader::new();
385 if let Some(ref dir) = self.runtime.config().trust_root {
386 let verifier = SignatureVerifier::from_trust_root(dir)
387 .map_err(|e| PluginError::LoadError(e.to_string()))?;
388 loader = loader.with_signature_verifier(verifier);
389 }
390 let (manifest, wasm_bytes) = loader.load(path)?;
391
392 let plugin = self.runtime.instantiate(&manifest, &wasm_bytes)?;
393 let plugin = Arc::new(plugin);
394
395 {
397 let mut hooks = self.hooks.write();
398 for hook in &manifest.hooks {
399 hooks.entry(*hook).or_default().push(manifest.name.clone());
400 }
401 }
402
403 self.plugins.insert(manifest.name.clone(), plugin);
404
405 tracing::info!(
406 plugin = %manifest.name,
407 version = %manifest.version,
408 hooks = ?manifest.hooks,
409 "Plugin loaded"
410 );
411
412 Ok(())
413 }
414
415 pub fn unload_plugin(&self, name: &str) -> Result<(), PluginError> {
417 if let Some((_, plugin)) = self.plugins.remove(name) {
418 let mut hooks = self.hooks.write();
420 for hook_plugins in hooks.values_mut() {
421 hook_plugins.retain(|p| p != name);
422 }
423
424 if let Err(e) = self.runtime.call_hook(&plugin, HookType::OnDisconnect, &[]) {
426 tracing::warn!(plugin = %name, error = %e, "Error calling on_unload");
427 }
428
429 tracing::info!(plugin = %name, "Plugin unloaded");
430 }
431
432 Ok(())
433 }
434
435 pub fn reload_plugin(&self, name: &str) -> Result<(), PluginError> {
437 if let Some(plugin) = self.plugins.get(name) {
438 let path = plugin.path.clone();
439 drop(plugin);
440
441 self.unload_plugin(name)?;
442 self.load_plugin(&path)?;
443 }
444
445 Ok(())
446 }
447
448 pub fn kv(&self) -> &host_imports::KvBackend {
452 self.runtime.kv()
453 }
454
455 pub fn has_hook(&self, hook: HookType) -> bool {
459 self.hooks
460 .read()
461 .get(&hook)
462 .is_some_and(|names| !names.is_empty())
463 }
464
465 pub fn execute_pre_query(&self, ctx: &QueryContext) -> PreQueryResult {
467 let hooks = self.hooks.read();
468 let plugin_names = hooks.get(&HookType::PreQuery).cloned().unwrap_or_default();
469 drop(hooks);
470
471 for plugin_name in plugin_names {
472 if let Some(plugin) = self.plugins.get(&plugin_name) {
473 let start = std::time::Instant::now();
474
475 match self.runtime.call_pre_query(&plugin, ctx) {
476 Ok(result) => {
477 self.metrics.record_hook_call(
478 &plugin_name,
479 HookType::PreQuery,
480 start.elapsed(),
481 true,
482 );
483
484 match result {
485 PreQueryResult::Continue => continue,
486 other => return other,
487 }
488 }
489 Err(e) => {
490 self.metrics.record_hook_call(
491 &plugin_name,
492 HookType::PreQuery,
493 start.elapsed(),
494 false,
495 );
496 tracing::warn!(
497 plugin = %plugin_name,
498 error = %e,
499 "Pre-query hook failed"
500 );
501 }
502 }
503 }
504 }
505
506 PreQueryResult::Continue
507 }
508
509 pub fn execute_post_query(&self, ctx: &QueryContext, outcome: &PostQueryOutcome) {
516 let hooks = self.hooks.read();
517 let plugin_names = hooks.get(&HookType::PostQuery).cloned().unwrap_or_default();
518 drop(hooks);
519
520 if plugin_names.is_empty() {
521 return;
522 }
523
524 let payload = match serde_json::to_vec(&(ctx, outcome)) {
527 Ok(v) => v,
528 Err(e) => {
529 tracing::warn!(error = %e, "Post-query serialisation failed");
530 return;
531 }
532 };
533
534 for plugin_name in plugin_names {
535 if let Some(plugin) = self.plugins.get(&plugin_name) {
536 let start = std::time::Instant::now();
537
538 match self
539 .runtime
540 .call_hook(&plugin, HookType::PostQuery, &payload)
541 {
542 Ok(_) => {
543 self.metrics.record_hook_call(
544 &plugin_name,
545 HookType::PostQuery,
546 start.elapsed(),
547 true,
548 );
549 }
550 Err(e) => {
551 self.metrics.record_hook_call(
552 &plugin_name,
553 HookType::PostQuery,
554 start.elapsed(),
555 false,
556 );
557 tracing::warn!(
558 plugin = %plugin_name,
559 error = %e,
560 "Post-query hook failed"
561 );
562 }
563 }
564 }
565 }
566 }
567
568 pub fn execute_authenticate(&self, request: &AuthRequest) -> AuthResult {
570 let hooks = self.hooks.read();
571 let plugin_names = hooks
572 .get(&HookType::Authenticate)
573 .cloned()
574 .unwrap_or_default();
575 drop(hooks);
576
577 for plugin_name in plugin_names {
578 if let Some(plugin) = self.plugins.get(&plugin_name) {
579 let start = std::time::Instant::now();
580
581 match self.runtime.call_authenticate(&plugin, request) {
582 Ok(result) => {
583 self.metrics.record_hook_call(
584 &plugin_name,
585 HookType::Authenticate,
586 start.elapsed(),
587 true,
588 );
589
590 match result {
591 AuthResult::Defer => continue,
592 other => return other,
593 }
594 }
595 Err(e) => {
596 self.metrics.record_hook_call(
597 &plugin_name,
598 HookType::Authenticate,
599 start.elapsed(),
600 false,
601 );
602 tracing::warn!(
603 plugin = %plugin_name,
604 error = %e,
605 "Authenticate hook failed"
606 );
607 }
608 }
609 }
610 }
611
612 AuthResult::Defer
613 }
614
615 pub fn execute_route(&self, ctx: &QueryContext) -> RouteResult {
617 let hooks = self.hooks.read();
618 let plugin_names = hooks.get(&HookType::Route).cloned().unwrap_or_default();
619 drop(hooks);
620
621 for plugin_name in plugin_names {
622 if let Some(plugin) = self.plugins.get(&plugin_name) {
623 let start = std::time::Instant::now();
624
625 match self.runtime.call_route(&plugin, ctx) {
626 Ok(result) => {
627 self.metrics.record_hook_call(
628 &plugin_name,
629 HookType::Route,
630 start.elapsed(),
631 true,
632 );
633
634 match result {
635 RouteResult::Default => continue,
636 other => return other,
637 }
638 }
639 Err(e) => {
640 self.metrics.record_hook_call(
641 &plugin_name,
642 HookType::Route,
643 start.elapsed(),
644 false,
645 );
646 tracing::warn!(
647 plugin = %plugin_name,
648 error = %e,
649 "Route hook failed"
650 );
651 }
652 }
653 }
654 }
655
656 RouteResult::Default
657 }
658
659 pub fn list_plugins(&self) -> Vec<PluginInfo> {
661 self.plugins
662 .iter()
663 .map(|entry| {
664 let plugin = entry.value();
665 let stats = self.metrics.get_plugin_stats(&plugin.metadata.name);
666
667 PluginInfo {
668 name: plugin.metadata.name.clone(),
669 version: plugin.metadata.version.clone(),
670 description: plugin.metadata.description.clone(),
671 hooks: plugin.metadata.hooks.clone(),
672 state: plugin.state.clone(),
673 stats,
674 }
675 })
676 .collect()
677 }
678
679 pub fn get_metrics(&self) -> PluginManagerMetrics {
681 PluginManagerMetrics {
682 plugins_loaded: self.plugins.len(),
683 total_hook_calls: self.metrics.total_calls(),
684 total_errors: self.metrics.total_errors(),
685 avg_latency: self.metrics.avg_latency(),
686 plugins: self.list_plugins(),
687 }
688 }
689
690 pub fn check_updates(&self) -> Result<Vec<ReloadEvent>, PluginError> {
692 if let Some(ref reloader) = self.hot_reloader {
693 let events = reloader.check()?;
694
695 for event in &events {
696 match event {
697 ReloadEvent::Modified(name) => {
698 tracing::info!(plugin = %name, "Hot reloading plugin");
699 if let Err(e) = self.reload_plugin(name) {
700 tracing::error!(plugin = %name, error = %e, "Hot reload failed");
701 }
702 }
703 ReloadEvent::Removed(name) => {
704 tracing::info!(plugin = %name, "Plugin file removed, unloading");
705 if let Err(e) = self.unload_plugin(name) {
706 tracing::error!(plugin = %name, error = %e, "Unload failed");
707 }
708 }
709 ReloadEvent::Added(path) => {
710 tracing::info!(path = %path.display(), "New plugin detected, loading");
711 if let Err(e) = self.load_plugin(path) {
712 tracing::error!(path = %path.display(), error = %e, "Load failed");
713 }
714 }
715 }
716 }
717
718 Ok(events)
719 } else {
720 Ok(Vec::new())
721 }
722 }
723}
724
725#[derive(Debug, Clone)]
727pub struct AuthRequest {
728 pub headers: HashMap<String, String>,
730
731 pub username: Option<String>,
733
734 pub password: Option<String>,
736
737 pub client_ip: String,
739
740 pub database: Option<String>,
742}
743
744#[derive(Debug, Clone)]
746pub struct PluginInfo {
747 pub name: String,
749
750 pub version: String,
752
753 pub description: String,
755
756 pub hooks: Vec<HookType>,
758
759 pub state: PluginState,
761
762 pub stats: PluginStats,
764}
765
766#[derive(Debug, Clone)]
768pub struct PluginManagerMetrics {
769 pub plugins_loaded: usize,
771
772 pub total_hook_calls: u64,
774
775 pub total_errors: u64,
777
778 pub avg_latency: Duration,
780
781 pub plugins: Vec<PluginInfo>,
783}
784
785#[cfg(test)]
786mod tests {
787 use super::*;
788
789 #[test]
790 fn test_hook_type_export_name() {
791 assert_eq!(HookType::PreQuery.export_name(), "pre_query");
792 assert_eq!(HookType::Authenticate.export_name(), "authenticate");
793 assert_eq!(HookType::Route.export_name(), "route");
794 }
795
796 #[test]
797 fn test_hook_type_from_str() {
798 assert_eq!(HookType::from_str("pre_query"), Some(HookType::PreQuery));
799 assert_eq!(
800 HookType::from_str("authenticate"),
801 Some(HookType::Authenticate)
802 );
803 assert_eq!(HookType::from_str("unknown"), None);
804 }
805
806 #[test]
807 fn test_plugin_metadata_default() {
808 let meta = PluginMetadata::default();
809 assert!(meta.name.is_empty());
810 assert_eq!(meta.version, "0.0.0");
811 assert!(meta.hooks.is_empty());
812 }
813
814 #[test]
815 fn test_hook_context_default() {
816 let ctx = HookContext::default();
817 assert!(!ctx.request_id.is_empty());
818 assert!(ctx.client_id.is_none());
819 }
820
821 #[test]
822 fn test_pre_query_result() {
823 let result = PreQueryResult::Continue;
824 assert!(matches!(result, PreQueryResult::Continue));
825
826 let result = PreQueryResult::Block("blocked".to_string());
827 assert!(matches!(result, PreQueryResult::Block(_)));
828 }
829
830 #[test]
831 fn test_auth_result() {
832 let result = AuthResult::Denied("invalid".to_string());
833 assert!(matches!(result, AuthResult::Denied(_)));
834
835 let result = AuthResult::Defer;
836 assert!(matches!(result, AuthResult::Defer));
837 }
838
839 #[test]
840 fn test_route_result() {
841 let result = RouteResult::Default;
842 assert!(matches!(result, RouteResult::Default));
843
844 let result = RouteResult::Branch("test".to_string());
845 assert!(matches!(result, RouteResult::Branch(_)));
846 }
847
848 #[test]
849 fn test_identity_default() {
850 let identity = Identity::default();
851 assert!(identity.user_id.is_empty());
852 assert!(identity.roles.is_empty());
853 assert!(identity.tenant_id.is_none());
854 }
855
856 #[test]
861 fn test_execute_post_query_no_plugins_is_noop() {
862 let config = PluginRuntimeConfig::default();
863 let pm = PluginManager::new(config).expect("construct PluginManager");
864
865 let ctx = QueryContext {
866 query: "SELECT 1".to_string(),
867 normalized: "SELECT 1".to_string(),
868 tables: Vec::new(),
869 is_read_only: true,
870 hook_context: HookContext::default(),
871 };
872 let outcome = PostQueryOutcome {
873 success: true,
874 target_node: Some("primary".to_string()),
875 elapsed_us: 42,
876 response_bytes: 128,
877 error: None,
878 };
879
880 pm.execute_post_query(&ctx, &outcome);
882
883 let metrics = pm.get_metrics();
885 assert_eq!(metrics.plugins_loaded, 0);
886 assert_eq!(metrics.total_hook_calls, 0);
887 }
888
889 #[test]
892 fn test_execute_pre_query_no_plugins_returns_continue() {
893 let pm =
894 PluginManager::new(PluginRuntimeConfig::default()).expect("construct PluginManager");
895 let ctx = QueryContext {
896 query: "SELECT 1".to_string(),
897 normalized: "SELECT 1".to_string(),
898 tables: Vec::new(),
899 is_read_only: true,
900 hook_context: HookContext::default(),
901 };
902 assert!(matches!(
903 pm.execute_pre_query(&ctx),
904 PreQueryResult::Continue
905 ));
906 }
907
908 #[test]
911 fn test_post_query_outcome_serialisation() {
912 let outcome = PostQueryOutcome {
913 success: false,
914 target_node: None,
915 elapsed_us: 1234,
916 response_bytes: 0,
917 error: Some("backend timeout".to_string()),
918 };
919 let json = serde_json::to_string(&outcome).expect("serialise");
920 assert!(json.contains("\"success\":false"));
921 assert!(json.contains("\"elapsed_us\":1234"));
922 assert!(json.contains("backend timeout"));
923 }
924}