@qwickapps/server 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/README.md +392 -0
  2. package/dist/core/control-panel.d.ts +7 -2
  3. package/dist/core/control-panel.d.ts.map +1 -1
  4. package/dist/core/control-panel.js +120 -54
  5. package/dist/core/control-panel.js.map +1 -1
  6. package/dist/core/gateway.d.ts +159 -79
  7. package/dist/core/gateway.d.ts.map +1 -1
  8. package/dist/core/gateway.js +679 -319
  9. package/dist/core/gateway.js.map +1 -1
  10. package/dist/core/index.d.ts +3 -1
  11. package/dist/core/index.d.ts.map +1 -1
  12. package/dist/core/index.js +2 -0
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/core/plugin-registry.d.ts +307 -0
  15. package/dist/core/plugin-registry.d.ts.map +1 -0
  16. package/dist/core/plugin-registry.js +352 -0
  17. package/dist/core/plugin-registry.js.map +1 -0
  18. package/dist/core/types.d.ts +16 -33
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/index.d.ts +8 -5
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +15 -7
  23. package/dist/index.js.map +1 -1
  24. package/dist/plugins/auth/adapters/auth0-adapter.d.ts +14 -0
  25. package/dist/plugins/auth/adapters/auth0-adapter.d.ts.map +1 -0
  26. package/dist/plugins/auth/adapters/auth0-adapter.js +179 -0
  27. package/dist/plugins/auth/adapters/auth0-adapter.js.map +1 -0
  28. package/dist/plugins/auth/adapters/basic-adapter.d.ts +13 -0
  29. package/dist/plugins/auth/adapters/basic-adapter.d.ts.map +1 -0
  30. package/dist/plugins/auth/adapters/basic-adapter.js +51 -0
  31. package/dist/plugins/auth/adapters/basic-adapter.js.map +1 -0
  32. package/dist/plugins/auth/adapters/index.d.ts +10 -0
  33. package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
  34. package/dist/plugins/auth/adapters/index.js +10 -0
  35. package/dist/plugins/auth/adapters/index.js.map +1 -0
  36. package/dist/plugins/auth/adapters/supabase-adapter.d.ts +13 -0
  37. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -0
  38. package/dist/plugins/auth/adapters/supabase-adapter.js +109 -0
  39. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -0
  40. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
  41. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
  42. package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
  43. package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
  44. package/dist/plugins/auth/auth-plugin.d.ts +40 -0
  45. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
  46. package/dist/plugins/auth/auth-plugin.js +255 -0
  47. package/dist/plugins/auth/auth-plugin.js.map +1 -0
  48. package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
  49. package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
  50. package/dist/plugins/auth/auth-plugin.test.js +147 -0
  51. package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
  52. package/dist/plugins/auth/env-config.d.ts +88 -0
  53. package/dist/plugins/auth/env-config.d.ts.map +1 -0
  54. package/dist/plugins/auth/env-config.js +489 -0
  55. package/dist/plugins/auth/env-config.js.map +1 -0
  56. package/dist/plugins/auth/index.d.ts +14 -0
  57. package/dist/plugins/auth/index.d.ts.map +1 -0
  58. package/dist/plugins/auth/index.js +16 -0
  59. package/dist/plugins/auth/index.js.map +1 -0
  60. package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
  61. package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
  62. package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
  63. package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
  64. package/dist/plugins/auth/types.d.ts +218 -0
  65. package/dist/plugins/auth/types.d.ts.map +1 -0
  66. package/dist/plugins/auth/types.js +14 -0
  67. package/dist/plugins/auth/types.js.map +1 -0
  68. package/dist/plugins/bans/bans-plugin.d.ts +59 -0
  69. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
  70. package/dist/plugins/bans/bans-plugin.js +428 -0
  71. package/dist/plugins/bans/bans-plugin.js.map +1 -0
  72. package/dist/plugins/bans/index.d.ts +9 -0
  73. package/dist/plugins/bans/index.d.ts.map +1 -0
  74. package/dist/plugins/bans/index.js +10 -0
  75. package/dist/plugins/bans/index.js.map +1 -0
  76. package/dist/plugins/bans/stores/index.d.ts +7 -0
  77. package/dist/plugins/bans/stores/index.d.ts.map +1 -0
  78. package/dist/plugins/bans/stores/index.js +7 -0
  79. package/dist/plugins/bans/stores/index.js.map +1 -0
  80. package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
  81. package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
  82. package/dist/plugins/bans/stores/postgres-store.js +132 -0
  83. package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
  84. package/dist/plugins/bans/types.d.ts +128 -0
  85. package/dist/plugins/bans/types.d.ts.map +1 -0
  86. package/dist/plugins/bans/types.js +11 -0
  87. package/dist/plugins/bans/types.js.map +1 -0
  88. package/dist/plugins/cache-plugin.d.ts +14 -3
  89. package/dist/plugins/cache-plugin.d.ts.map +1 -1
  90. package/dist/plugins/cache-plugin.js +27 -7
  91. package/dist/plugins/cache-plugin.js.map +1 -1
  92. package/dist/plugins/cache-plugin.test.js +99 -32
  93. package/dist/plugins/cache-plugin.test.js.map +1 -1
  94. package/dist/plugins/config-plugin.d.ts +3 -2
  95. package/dist/plugins/config-plugin.d.ts.map +1 -1
  96. package/dist/plugins/config-plugin.js +17 -10
  97. package/dist/plugins/config-plugin.js.map +1 -1
  98. package/dist/plugins/diagnostics-plugin.d.ts +2 -2
  99. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
  100. package/dist/plugins/diagnostics-plugin.js +17 -10
  101. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  102. package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
  103. package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
  104. package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
  105. package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
  106. package/dist/plugins/entitlements/index.d.ts +12 -0
  107. package/dist/plugins/entitlements/index.d.ts.map +1 -0
  108. package/dist/plugins/entitlements/index.js +16 -0
  109. package/dist/plugins/entitlements/index.js.map +1 -0
  110. package/dist/plugins/entitlements/sources/index.d.ts +9 -0
  111. package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
  112. package/dist/plugins/entitlements/sources/index.js +9 -0
  113. package/dist/plugins/entitlements/sources/index.js.map +1 -0
  114. package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
  115. package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
  116. package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
  117. package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
  118. package/dist/plugins/entitlements/types.d.ts +232 -0
  119. package/dist/plugins/entitlements/types.d.ts.map +1 -0
  120. package/dist/plugins/entitlements/types.js +11 -0
  121. package/dist/plugins/entitlements/types.js.map +1 -0
  122. package/dist/plugins/frontend-app-plugin.d.ts +9 -3
  123. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  124. package/dist/plugins/frontend-app-plugin.js +14 -9
  125. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  126. package/dist/plugins/health-plugin.d.ts +5 -2
  127. package/dist/plugins/health-plugin.d.ts.map +1 -1
  128. package/dist/plugins/health-plugin.js +20 -5
  129. package/dist/plugins/health-plugin.js.map +1 -1
  130. package/dist/plugins/index.d.ts +10 -2
  131. package/dist/plugins/index.d.ts.map +1 -1
  132. package/dist/plugins/index.js +10 -2
  133. package/dist/plugins/index.js.map +1 -1
  134. package/dist/plugins/logs-plugin.d.ts +3 -2
  135. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  136. package/dist/plugins/logs-plugin.js +21 -12
  137. package/dist/plugins/logs-plugin.js.map +1 -1
  138. package/dist/plugins/postgres-plugin.d.ts +3 -3
  139. package/dist/plugins/postgres-plugin.d.ts.map +1 -1
  140. package/dist/plugins/postgres-plugin.js +9 -7
  141. package/dist/plugins/postgres-plugin.js.map +1 -1
  142. package/dist/plugins/postgres-plugin.test.js +50 -29
  143. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  144. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
  145. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
  146. package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
  147. package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
  148. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
  149. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
  150. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
  151. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
  152. package/dist/plugins/preferences/index.d.ts +12 -0
  153. package/dist/plugins/preferences/index.d.ts.map +1 -0
  154. package/dist/plugins/preferences/index.js +13 -0
  155. package/dist/plugins/preferences/index.js.map +1 -0
  156. package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
  157. package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
  158. package/dist/plugins/preferences/preferences-plugin.js +226 -0
  159. package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
  160. package/dist/plugins/preferences/stores/index.d.ts +9 -0
  161. package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
  162. package/dist/plugins/preferences/stores/index.js +9 -0
  163. package/dist/plugins/preferences/stores/index.js.map +1 -0
  164. package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
  165. package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
  166. package/dist/plugins/preferences/stores/postgres-store.js +181 -0
  167. package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
  168. package/dist/plugins/preferences/types.d.ts +91 -0
  169. package/dist/plugins/preferences/types.d.ts.map +1 -0
  170. package/dist/plugins/preferences/types.js +10 -0
  171. package/dist/plugins/preferences/types.js.map +1 -0
  172. package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
  173. package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
  174. package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
  175. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
  176. package/dist/plugins/users/index.d.ts +12 -0
  177. package/dist/plugins/users/index.d.ts.map +1 -0
  178. package/dist/plugins/users/index.js +13 -0
  179. package/dist/plugins/users/index.js.map +1 -0
  180. package/dist/plugins/users/stores/index.d.ts +7 -0
  181. package/dist/plugins/users/stores/index.d.ts.map +1 -0
  182. package/dist/plugins/users/stores/index.js +7 -0
  183. package/dist/plugins/users/stores/index.js.map +1 -0
  184. package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
  185. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
  186. package/dist/plugins/users/stores/postgres-store.js +157 -0
  187. package/dist/plugins/users/stores/postgres-store.js.map +1 -0
  188. package/dist/plugins/users/types.d.ts +225 -0
  189. package/dist/plugins/users/types.d.ts.map +1 -0
  190. package/dist/plugins/users/types.js +12 -0
  191. package/dist/plugins/users/types.js.map +1 -0
  192. package/dist/plugins/users/users-plugin.d.ts +45 -0
  193. package/dist/plugins/users/users-plugin.d.ts.map +1 -0
  194. package/dist/plugins/users/users-plugin.js +359 -0
  195. package/dist/plugins/users/users-plugin.js.map +1 -0
  196. package/dist-ui/assets/index-BY8OxNgO.js +465 -0
  197. package/dist-ui/assets/index-BY8OxNgO.js.map +1 -0
  198. package/dist-ui/index.html +1 -1
  199. package/dist-ui-lib/api/controlPanelApi.d.ts +278 -0
  200. package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
  201. package/dist-ui-lib/components/index.d.ts +18 -0
  202. package/dist-ui-lib/config/AppConfig.d.ts +7 -0
  203. package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
  204. package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
  205. package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
  206. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +48 -0
  207. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +25 -0
  208. package/dist-ui-lib/dashboard/index.d.ts +13 -0
  209. package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
  210. package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
  211. package/dist-ui-lib/index.js +5172 -0
  212. package/dist-ui-lib/index.js.map +1 -0
  213. package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
  214. package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
  215. package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
  216. package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
  217. package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
  218. package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
  219. package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
  220. package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
  221. package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
  222. package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
  223. package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
  224. package/package.json +24 -7
  225. package/src/core/control-panel.ts +145 -61
  226. package/src/core/gateway.ts +863 -403
  227. package/src/core/index.ts +21 -2
  228. package/src/core/plugin-registry.ts +716 -0
  229. package/src/core/types.ts +31 -37
  230. package/src/index.ts +125 -19
  231. package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
  232. package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
  233. package/src/plugins/auth/adapters/index.ts +10 -0
  234. package/src/plugins/auth/adapters/supabase-adapter.ts +149 -0
  235. package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
  236. package/src/plugins/auth/auth-plugin.test.ts +176 -0
  237. package/src/plugins/auth/auth-plugin.ts +303 -0
  238. package/src/plugins/auth/env-config.ts +572 -0
  239. package/src/plugins/auth/index.ts +42 -0
  240. package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
  241. package/src/plugins/auth/types.ts +245 -0
  242. package/src/plugins/bans/bans-plugin.ts +485 -0
  243. package/src/plugins/bans/index.ts +31 -0
  244. package/src/plugins/bans/stores/index.ts +7 -0
  245. package/src/plugins/bans/stores/postgres-store.ts +195 -0
  246. package/src/plugins/bans/types.ts +141 -0
  247. package/src/plugins/cache-plugin.test.ts +108 -32
  248. package/src/plugins/cache-plugin.ts +40 -9
  249. package/src/plugins/config-plugin.ts +23 -12
  250. package/src/plugins/diagnostics-plugin.ts +22 -12
  251. package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
  252. package/src/plugins/entitlements/index.ts +51 -0
  253. package/src/plugins/entitlements/sources/index.ts +9 -0
  254. package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
  255. package/src/plugins/entitlements/types.ts +256 -0
  256. package/src/plugins/frontend-app-plugin.ts +24 -12
  257. package/src/plugins/health-plugin.ts +27 -7
  258. package/src/plugins/index.ts +132 -4
  259. package/src/plugins/logs-plugin.ts +28 -14
  260. package/src/plugins/postgres-plugin.test.ts +52 -29
  261. package/src/plugins/postgres-plugin.ts +11 -9
  262. package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
  263. package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
  264. package/src/plugins/preferences/index.ts +30 -0
  265. package/src/plugins/preferences/preferences-plugin.ts +270 -0
  266. package/src/plugins/preferences/stores/index.ts +9 -0
  267. package/src/plugins/preferences/stores/postgres-store.ts +252 -0
  268. package/src/plugins/preferences/types.ts +100 -0
  269. package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
  270. package/src/plugins/users/index.ts +38 -0
  271. package/src/plugins/users/stores/index.ts +7 -0
  272. package/src/plugins/users/stores/postgres-store.ts +225 -0
  273. package/src/plugins/users/types.ts +247 -0
  274. package/src/plugins/users/users-plugin.ts +418 -0
  275. package/ui/src/App.tsx +188 -31
  276. package/ui/src/api/controlPanelApi.ts +453 -1
  277. package/ui/src/components/ControlPanelApp.tsx +212 -0
  278. package/ui/src/components/index.ts +62 -0
  279. package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
  280. package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
  281. package/ui/src/dashboard/PluginWidgetRenderer.tsx +118 -0
  282. package/ui/src/dashboard/WidgetComponentRegistry.tsx +120 -0
  283. package/ui/src/dashboard/builtInWidgets.tsx +35 -0
  284. package/ui/src/dashboard/index.ts +35 -0
  285. package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
  286. package/ui/src/dashboard/widgets/index.ts +7 -0
  287. package/ui/src/pages/AuthPage.tsx +259 -0
  288. package/ui/src/pages/DashboardPage.tsx +28 -149
  289. package/ui/src/pages/EntitlementsPage.tsx +557 -0
  290. package/ui/src/pages/LogsPage.tsx +174 -8
  291. package/ui/src/pages/PluginPage.tsx +148 -0
  292. package/ui/src/pages/PluginsPage.tsx +394 -0
  293. package/ui/src/pages/SystemPage.tsx +445 -0
  294. package/ui/src/pages/UsersPage.tsx +837 -0
  295. package/ui/tsconfig.lib.json +11 -0
  296. package/ui/vite.lib.config.ts +56 -0
  297. package/dist-ui/assets/index-CW1BviRn.js +0 -465
  298. package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
  299. package/ui/src/pages/HealthPage.tsx +0 -204
@@ -0,0 +1,716 @@
1
+ /**
2
+ * Plugin Registry - Event-Driven Plugin Architecture v2.0
3
+ *
4
+ * A simple, event-driven plugin system where:
5
+ * - Plugins register in `onStart`, cleanup in `onStop`
6
+ * - Changes are broadcast via events
7
+ * - Plugins react to events via `onPluginEvent`
8
+ *
9
+ * No frozen registries, no complex phases. Just start, events, stop.
10
+ *
11
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
12
+ */
13
+
14
+ import type { RequestHandler, Application, Router } from 'express';
15
+ import type { Logger, HealthCheck } from './types.js';
16
+ import { HealthManager } from './health-manager.js';
17
+
18
+ // =============================================================================
19
+ // Plugin Event Types
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Events that plugins can react to
24
+ */
25
+ export type PluginEvent =
26
+ | { type: 'plugin:started'; pluginId: string; config: unknown }
27
+ | { type: 'plugin:stopped'; pluginId: string }
28
+ | { type: 'plugin:config-changed'; pluginId: string; key: string; oldValue: unknown; newValue: unknown }
29
+ | { type: 'plugin:error'; pluginId: string; error: Error };
30
+
31
+ /**
32
+ * Event handler type
33
+ */
34
+ export type PluginEventHandler = (event: PluginEvent) => void | Promise<void>;
35
+
36
+ // =============================================================================
37
+ // Plugin Interface
38
+ // =============================================================================
39
+
40
+ /**
41
+ * Plugin configuration passed to onStart
42
+ */
43
+ export interface PluginConfig {
44
+ [key: string]: unknown;
45
+ }
46
+
47
+ /**
48
+ * Plugin info for listing
49
+ */
50
+ export interface PluginInfo {
51
+ id: string;
52
+ name: string;
53
+ version?: string;
54
+ status: 'starting' | 'active' | 'stopped' | 'error';
55
+ error?: string;
56
+ }
57
+
58
+ /**
59
+ * The Plugin interface - simple lifecycle with event handling
60
+ */
61
+ export interface Plugin {
62
+ /** Unique plugin identifier */
63
+ id: string;
64
+
65
+ /** Human-readable plugin name */
66
+ name: string;
67
+
68
+ /** Plugin version (semver) */
69
+ version?: string;
70
+
71
+ /**
72
+ * Called when the plugin starts.
73
+ * Initialize resources, register routes/UI contributions here.
74
+ * Check dependencies with registry.hasPlugin().
75
+ */
76
+ onStart(config: PluginConfig, registry: PluginRegistry): Promise<void>;
77
+
78
+ /**
79
+ * Called when the plugin stops.
80
+ * Clean up resources here.
81
+ */
82
+ onStop(): Promise<void>;
83
+
84
+ /**
85
+ * React to system events (optional).
86
+ * Called when other plugins start/stop, configs change, or errors occur.
87
+ */
88
+ onPluginEvent?(event: PluginEvent): Promise<void>;
89
+ }
90
+
91
+ // =============================================================================
92
+ // UI Contribution Types
93
+ // =============================================================================
94
+
95
+ /**
96
+ * Menu item contribution for sidebar/navigation
97
+ */
98
+ export interface MenuContribution {
99
+ /** Unique ID for this menu item */
100
+ id: string;
101
+ /** Display label */
102
+ label: string;
103
+ /** Icon name (e.g., 'users', 'settings', 'ban') */
104
+ icon?: string;
105
+ /** Route path this menu item links to */
106
+ route: string;
107
+ /** Display order (lower = higher) */
108
+ order?: number;
109
+ /** Badge to display (static string or API endpoint) */
110
+ badge?: string | { api: string };
111
+ /** Parent menu ID for submenus */
112
+ parent?: string;
113
+ /** Plugin ID that contributed this */
114
+ pluginId: string;
115
+ }
116
+
117
+ /**
118
+ * Page contribution for control panel
119
+ */
120
+ export interface PageContribution {
121
+ /** Unique ID for this page */
122
+ id: string;
123
+ /** Route path (e.g., '/users', '/bans/:id') */
124
+ route: string;
125
+ /** Component name to render (matched by frontend) */
126
+ component: string;
127
+ /** Page title */
128
+ title?: string;
129
+ /** Plugin ID that contributed this */
130
+ pluginId: string;
131
+ }
132
+
133
+ /**
134
+ * Widget contribution for dashboards
135
+ */
136
+ export interface WidgetContribution {
137
+ /** Unique ID for this widget */
138
+ id: string;
139
+ /** Widget title */
140
+ title: string;
141
+ /** Component name to render (matched by frontend widget registry) */
142
+ component: string;
143
+ /** Priority for ordering (lower = first, default: 100) */
144
+ priority?: number;
145
+ /** Whether this widget is shown by default (default: false) */
146
+ showByDefault?: boolean;
147
+ /** Default size */
148
+ defaultSize?: { width: number; height: number };
149
+ /** Plugin ID that contributed this */
150
+ pluginId: string;
151
+ }
152
+
153
+ /**
154
+ * Route definition for API routes
155
+ */
156
+ export interface RouteDefinition {
157
+ /** HTTP method */
158
+ method: 'get' | 'post' | 'put' | 'delete' | 'patch';
159
+ /** Route path */
160
+ path: string;
161
+ /** Request handler */
162
+ handler: RequestHandler;
163
+ /** Plugin ID that contributed this */
164
+ pluginId: string;
165
+ }
166
+
167
+ /**
168
+ * Configuration UI contribution for plugin settings
169
+ */
170
+ export interface ConfigContribution {
171
+ /** Unique ID for this config contribution */
172
+ id: string;
173
+ /** React component name to render (matched by frontend registry) */
174
+ component: string;
175
+ /** Display title for the config section */
176
+ title?: string;
177
+ /** Plugin ID that contributed this */
178
+ pluginId: string;
179
+ }
180
+
181
+ /**
182
+ * Aggregated contributions for a specific plugin
183
+ */
184
+ export interface PluginContributions {
185
+ routes: Array<{ method: string; path: string }>;
186
+ menuItems: MenuContribution[];
187
+ pages: PageContribution[];
188
+ widgets: WidgetContribution[];
189
+ config?: ConfigContribution;
190
+ }
191
+
192
+ // =============================================================================
193
+ // Plugin Registry Interface
194
+ // =============================================================================
195
+
196
+ /**
197
+ * The Plugin Registry - a directory for plugins and their contributions
198
+ *
199
+ * Not frozen, mutable anytime. Query plugins, register contributions,
200
+ * subscribe to events.
201
+ */
202
+ export interface PluginRegistry {
203
+ // ---------------------------------------------------------------------------
204
+ // Plugin queries
205
+ // ---------------------------------------------------------------------------
206
+
207
+ /** Check if a plugin is registered and active */
208
+ hasPlugin(id: string): boolean;
209
+
210
+ /** Get a plugin by ID (cast to your expected type) */
211
+ getPlugin<T extends Plugin = Plugin>(id: string): T | null;
212
+
213
+ /** List all registered plugins */
214
+ listPlugins(): PluginInfo[];
215
+
216
+ // ---------------------------------------------------------------------------
217
+ // Contribution registration
218
+ // ---------------------------------------------------------------------------
219
+
220
+ /** Register an API route */
221
+ addRoute(route: RouteDefinition): void;
222
+
223
+ /** Register a menu item */
224
+ addMenuItem(menu: MenuContribution): void;
225
+
226
+ /** Register a page */
227
+ addPage(page: PageContribution): void;
228
+
229
+ /** Register a widget */
230
+ addWidget(widget: WidgetContribution): void;
231
+
232
+ /** Register a config component for plugin settings UI */
233
+ addConfigComponent(config: ConfigContribution): void;
234
+
235
+ // ---------------------------------------------------------------------------
236
+ // Contribution queries
237
+ // ---------------------------------------------------------------------------
238
+
239
+ /** Get all registered routes */
240
+ getRoutes(): RouteDefinition[];
241
+
242
+ /** Get all menu items */
243
+ getMenuItems(): MenuContribution[];
244
+
245
+ /** Get all pages */
246
+ getPages(): PageContribution[];
247
+
248
+ /** Get all widgets */
249
+ getWidgets(): WidgetContribution[];
250
+
251
+ /** Get all config components */
252
+ getConfigComponents(): ConfigContribution[];
253
+
254
+ /** Get all contributions for a specific plugin */
255
+ getPluginContributions(pluginId: string): PluginContributions;
256
+
257
+ // ---------------------------------------------------------------------------
258
+ // Configuration
259
+ // ---------------------------------------------------------------------------
260
+
261
+ /** Get plugin configuration */
262
+ getConfig<T = PluginConfig>(pluginId: string): T;
263
+
264
+ /** Update plugin configuration (emits plugin:config-changed event) */
265
+ setConfig<T = PluginConfig>(pluginId: string, config: Partial<T>): Promise<void>;
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Events
269
+ // ---------------------------------------------------------------------------
270
+
271
+ /** Subscribe to plugin events, returns unsubscribe function */
272
+ subscribe(handler: PluginEventHandler): () => void;
273
+
274
+ /** Emit an event to all subscribers and plugins */
275
+ emit(event: PluginEvent): void;
276
+
277
+ // ---------------------------------------------------------------------------
278
+ // Health checks
279
+ // ---------------------------------------------------------------------------
280
+
281
+ /** Register a health check */
282
+ registerHealthCheck(check: HealthCheck): void;
283
+
284
+ // ---------------------------------------------------------------------------
285
+ // Express integration
286
+ // ---------------------------------------------------------------------------
287
+
288
+ /** Get the Express app (for advanced use cases) */
289
+ getApp(): Application;
290
+
291
+ /** Get the Express router (for advanced use cases) */
292
+ getRouter(): Router;
293
+
294
+ /** Get the logger for a plugin */
295
+ getLogger(pluginId: string): Logger;
296
+ }
297
+
298
+ // =============================================================================
299
+ // Plugin Registry Implementation
300
+ // =============================================================================
301
+
302
+ /**
303
+ * Default timeout for plugin operations (30 seconds)
304
+ */
305
+ const DEFAULT_TIMEOUT = 30000;
306
+
307
+ /**
308
+ * Create a timeout promise
309
+ */
310
+ function timeout(ms: number): Promise<never> {
311
+ return new Promise((_, reject) =>
312
+ setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms)
313
+ );
314
+ }
315
+
316
+ /**
317
+ * Plugin Registry Implementation
318
+ */
319
+ export class PluginRegistryImpl implements PluginRegistry {
320
+ private plugins = new Map<string, Plugin>();
321
+ private pluginStatus = new Map<string, PluginInfo['status']>();
322
+ private pluginErrors = new Map<string, string>();
323
+ private pluginConfigs = new Map<string, PluginConfig>();
324
+
325
+ private routes: RouteDefinition[] = [];
326
+ private menuItems: MenuContribution[] = [];
327
+ private pages: PageContribution[] = [];
328
+ private widgets: WidgetContribution[] = [];
329
+ private configComponents: ConfigContribution[] = [];
330
+
331
+ private eventHandlers = new Set<PluginEventHandler>();
332
+
333
+ private app: Application;
334
+ private router: Router;
335
+ private logger: Logger;
336
+ private healthManager: HealthManager;
337
+ private loggerFactory: (name: string) => Logger;
338
+
339
+ constructor(
340
+ app: Application,
341
+ router: Router,
342
+ logger: Logger,
343
+ healthManager: HealthManager,
344
+ loggerFactory: (name: string) => Logger
345
+ ) {
346
+ this.app = app;
347
+ this.router = router;
348
+ this.logger = logger;
349
+ this.healthManager = healthManager;
350
+ this.loggerFactory = loggerFactory;
351
+ }
352
+
353
+ // ---------------------------------------------------------------------------
354
+ // Plugin queries
355
+ // ---------------------------------------------------------------------------
356
+
357
+ hasPlugin(id: string): boolean {
358
+ return this.plugins.has(id) && this.pluginStatus.get(id) === 'active';
359
+ }
360
+
361
+ getPlugin<T extends Plugin = Plugin>(id: string): T | null {
362
+ const plugin = this.plugins.get(id);
363
+ if (!plugin || this.pluginStatus.get(id) !== 'active') {
364
+ return null;
365
+ }
366
+ return plugin as T;
367
+ }
368
+
369
+ listPlugins(): PluginInfo[] {
370
+ return Array.from(this.plugins.values()).map((plugin) => ({
371
+ id: plugin.id,
372
+ name: plugin.name,
373
+ version: plugin.version,
374
+ status: this.pluginStatus.get(plugin.id) || 'stopped',
375
+ error: this.pluginErrors.get(plugin.id),
376
+ }));
377
+ }
378
+
379
+ // ---------------------------------------------------------------------------
380
+ // Contribution registration
381
+ // ---------------------------------------------------------------------------
382
+
383
+ addRoute(route: RouteDefinition): void {
384
+ this.routes.push(route);
385
+
386
+ // Register with Express router
387
+ switch (route.method) {
388
+ case 'get':
389
+ this.router.get(route.path, route.handler);
390
+ break;
391
+ case 'post':
392
+ this.router.post(route.path, route.handler);
393
+ break;
394
+ case 'put':
395
+ this.router.put(route.path, route.handler);
396
+ break;
397
+ case 'delete':
398
+ this.router.delete(route.path, route.handler);
399
+ break;
400
+ case 'patch':
401
+ this.router.patch(route.path, route.handler);
402
+ break;
403
+ }
404
+
405
+ this.logger.debug(`Route registered: ${route.method.toUpperCase()} ${route.path} by ${route.pluginId}`);
406
+ }
407
+
408
+ addMenuItem(menu: MenuContribution): void {
409
+ this.menuItems.push(menu);
410
+ this.logger.debug(`Menu item registered: ${menu.label} by ${menu.pluginId}`);
411
+ }
412
+
413
+ addPage(page: PageContribution): void {
414
+ this.pages.push(page);
415
+ this.logger.debug(`Page registered: ${page.route} by ${page.pluginId}`);
416
+ }
417
+
418
+ addWidget(widget: WidgetContribution): void {
419
+ this.widgets.push(widget);
420
+ this.logger.debug(`Widget registered: ${widget.title} by ${widget.pluginId}`);
421
+ }
422
+
423
+ addConfigComponent(config: ConfigContribution): void {
424
+ // Only one config component per plugin - warn if replacing
425
+ const existing = this.configComponents.find((c) => c.pluginId === config.pluginId);
426
+ if (existing) {
427
+ this.logger.warn(`Replacing config component for plugin ${config.pluginId}: ${existing.component} → ${config.component}`);
428
+ }
429
+ this.configComponents = this.configComponents.filter((c) => c.pluginId !== config.pluginId);
430
+ this.configComponents.push(config);
431
+ this.logger.debug(`Config component registered: ${config.component} by ${config.pluginId}`);
432
+ }
433
+
434
+ // ---------------------------------------------------------------------------
435
+ // Contribution queries
436
+ // ---------------------------------------------------------------------------
437
+
438
+ getRoutes(): RouteDefinition[] {
439
+ return [...this.routes];
440
+ }
441
+
442
+ getMenuItems(): MenuContribution[] {
443
+ return [...this.menuItems].sort((a, b) => (a.order ?? 100) - (b.order ?? 100));
444
+ }
445
+
446
+ getPages(): PageContribution[] {
447
+ return [...this.pages];
448
+ }
449
+
450
+ getWidgets(): WidgetContribution[] {
451
+ return [...this.widgets];
452
+ }
453
+
454
+ getConfigComponents(): ConfigContribution[] {
455
+ return [...this.configComponents];
456
+ }
457
+
458
+ getPluginContributions(pluginId: string): PluginContributions {
459
+ return {
460
+ routes: this.routes
461
+ .filter((r) => r.pluginId === pluginId)
462
+ .map((r) => ({ method: r.method, path: r.path })),
463
+ menuItems: this.menuItems.filter((m) => m.pluginId === pluginId),
464
+ pages: this.pages.filter((p) => p.pluginId === pluginId),
465
+ widgets: this.widgets.filter((w) => w.pluginId === pluginId),
466
+ config: this.configComponents.find((c) => c.pluginId === pluginId),
467
+ };
468
+ }
469
+
470
+ // ---------------------------------------------------------------------------
471
+ // Configuration
472
+ // ---------------------------------------------------------------------------
473
+
474
+ getConfig<T = PluginConfig>(pluginId: string): T {
475
+ return (this.pluginConfigs.get(pluginId) || {}) as T;
476
+ }
477
+
478
+ async setConfig<T = PluginConfig>(pluginId: string, config: Partial<T>): Promise<void> {
479
+ const oldConfig = this.pluginConfigs.get(pluginId) || {};
480
+ const newConfig = { ...oldConfig, ...config };
481
+ this.pluginConfigs.set(pluginId, newConfig);
482
+
483
+ // Emit config-changed events for each changed key
484
+ for (const key of Object.keys(config as Record<string, unknown>)) {
485
+ const oldValue = (oldConfig as Record<string, unknown>)[key];
486
+ const newValue = (config as Record<string, unknown>)[key];
487
+ if (oldValue !== newValue) {
488
+ this.emit({
489
+ type: 'plugin:config-changed',
490
+ pluginId,
491
+ key,
492
+ oldValue,
493
+ newValue,
494
+ });
495
+ }
496
+ }
497
+ }
498
+
499
+ // ---------------------------------------------------------------------------
500
+ // Events
501
+ // ---------------------------------------------------------------------------
502
+
503
+ subscribe(handler: PluginEventHandler): () => void {
504
+ this.eventHandlers.add(handler);
505
+ return () => {
506
+ this.eventHandlers.delete(handler);
507
+ };
508
+ }
509
+
510
+ emit(event: PluginEvent): void {
511
+ // Notify all subscribers
512
+ for (const handler of this.eventHandlers) {
513
+ try {
514
+ const result = handler(event);
515
+ if (result instanceof Promise) {
516
+ result.catch((err) => {
517
+ this.logger.error('Event handler error', { error: err.message, event: event.type });
518
+ });
519
+ }
520
+ } catch (err) {
521
+ this.logger.error('Event handler error', { error: (err as Error).message, event: event.type });
522
+ }
523
+ }
524
+
525
+ // Notify all plugins that implement onPluginEvent
526
+ for (const plugin of this.plugins.values()) {
527
+ if (plugin.onPluginEvent) {
528
+ try {
529
+ const result = plugin.onPluginEvent(event);
530
+ if (result instanceof Promise) {
531
+ result.catch((err) => {
532
+ this.logger.error(`Plugin ${plugin.id} event handler error`, { error: err.message, event: event.type });
533
+ });
534
+ }
535
+ } catch (err) {
536
+ this.logger.error(`Plugin ${plugin.id} event handler error`, { error: (err as Error).message, event: event.type });
537
+ }
538
+ }
539
+ }
540
+ }
541
+
542
+ // ---------------------------------------------------------------------------
543
+ // Health checks
544
+ // ---------------------------------------------------------------------------
545
+
546
+ registerHealthCheck(check: HealthCheck): void {
547
+ this.healthManager.register(check);
548
+ this.logger.debug(`Health check registered: ${check.name}`);
549
+ }
550
+
551
+ // ---------------------------------------------------------------------------
552
+ // Express integration
553
+ // ---------------------------------------------------------------------------
554
+
555
+ getApp(): Application {
556
+ return this.app;
557
+ }
558
+
559
+ getRouter(): Router {
560
+ return this.router;
561
+ }
562
+
563
+ getLogger(pluginId: string): Logger {
564
+ return this.loggerFactory(pluginId);
565
+ }
566
+
567
+ // ---------------------------------------------------------------------------
568
+ // Internal: Health Manager access
569
+ // ---------------------------------------------------------------------------
570
+
571
+ getHealthManager(): HealthManager {
572
+ return this.healthManager;
573
+ }
574
+
575
+ // ---------------------------------------------------------------------------
576
+ // Plugin lifecycle management (internal)
577
+ // ---------------------------------------------------------------------------
578
+
579
+ /**
580
+ * Start a plugin with error isolation
581
+ */
582
+ async startPlugin(plugin: Plugin, config: PluginConfig): Promise<boolean> {
583
+ this.plugins.set(plugin.id, plugin);
584
+ this.pluginConfigs.set(plugin.id, config);
585
+ this.pluginStatus.set(plugin.id, 'starting');
586
+
587
+ try {
588
+ await Promise.race([
589
+ plugin.onStart(config, this),
590
+ timeout(DEFAULT_TIMEOUT),
591
+ ]);
592
+
593
+ this.pluginStatus.set(plugin.id, 'active');
594
+ this.pluginErrors.delete(plugin.id);
595
+
596
+ this.emit({
597
+ type: 'plugin:started',
598
+ pluginId: plugin.id,
599
+ config,
600
+ });
601
+
602
+ this.logger.debug(`Plugin started: ${plugin.id}`);
603
+ return true;
604
+ } catch (error) {
605
+ const errorMessage = error instanceof Error ? error.message : String(error);
606
+ this.pluginStatus.set(plugin.id, 'error');
607
+ this.pluginErrors.set(plugin.id, errorMessage);
608
+
609
+ this.emit({
610
+ type: 'plugin:error',
611
+ pluginId: plugin.id,
612
+ error: error instanceof Error ? error : new Error(errorMessage),
613
+ });
614
+
615
+ this.logger.error(`Plugin ${plugin.id} failed to start`, { error: errorMessage });
616
+ return false;
617
+ }
618
+ }
619
+
620
+ /**
621
+ * Stop a plugin with error isolation
622
+ */
623
+ async stopPlugin(pluginId: string): Promise<boolean> {
624
+ const plugin = this.plugins.get(pluginId);
625
+ if (!plugin) {
626
+ return false;
627
+ }
628
+
629
+ try {
630
+ await Promise.race([
631
+ plugin.onStop(),
632
+ timeout(DEFAULT_TIMEOUT),
633
+ ]);
634
+
635
+ this.pluginStatus.set(pluginId, 'stopped');
636
+
637
+ // Remove contributions from this plugin
638
+ this.routes = this.routes.filter((r) => r.pluginId !== pluginId);
639
+ this.menuItems = this.menuItems.filter((m) => m.pluginId !== pluginId);
640
+ this.pages = this.pages.filter((p) => p.pluginId !== pluginId);
641
+ this.widgets = this.widgets.filter((w) => w.pluginId !== pluginId);
642
+ this.configComponents = this.configComponents.filter((c) => c.pluginId !== pluginId);
643
+
644
+ this.emit({
645
+ type: 'plugin:stopped',
646
+ pluginId,
647
+ });
648
+
649
+ this.logger.debug(`Plugin stopped: ${pluginId}`);
650
+ return true;
651
+ } catch (error) {
652
+ const errorMessage = error instanceof Error ? error.message : String(error);
653
+ this.logger.error(`Plugin ${pluginId} failed to stop cleanly`, { error: errorMessage });
654
+
655
+ // Still mark as stopped
656
+ this.pluginStatus.set(pluginId, 'stopped');
657
+ this.emit({ type: 'plugin:stopped', pluginId });
658
+
659
+ return false;
660
+ }
661
+ }
662
+
663
+ /**
664
+ * Stop all plugins (in reverse order they were started)
665
+ */
666
+ async stopAllPlugins(): Promise<void> {
667
+ const pluginIds = Array.from(this.plugins.keys()).reverse();
668
+ for (const pluginId of pluginIds) {
669
+ await this.stopPlugin(pluginId);
670
+ }
671
+ }
672
+ }
673
+
674
+ // =============================================================================
675
+ // Singleton and Factory
676
+ // =============================================================================
677
+
678
+ let registryInstance: PluginRegistryImpl | null = null;
679
+
680
+ /**
681
+ * Create and initialize the plugin registry
682
+ */
683
+ export function createPluginRegistry(
684
+ app: Application,
685
+ router: Router,
686
+ logger: Logger,
687
+ healthManager: HealthManager,
688
+ loggerFactory: (name: string) => Logger
689
+ ): PluginRegistryImpl {
690
+ registryInstance = new PluginRegistryImpl(app, router, logger, healthManager, loggerFactory);
691
+ return registryInstance;
692
+ }
693
+
694
+ /**
695
+ * Get the plugin registry singleton
696
+ */
697
+ export function getPluginRegistry(): PluginRegistry {
698
+ if (!registryInstance) {
699
+ throw new Error('Plugin registry not initialized. Call createPluginRegistry first.');
700
+ }
701
+ return registryInstance;
702
+ }
703
+
704
+ /**
705
+ * Check if plugin registry is initialized
706
+ */
707
+ export function hasPluginRegistry(): boolean {
708
+ return registryInstance !== null;
709
+ }
710
+
711
+ /**
712
+ * Reset the plugin registry (for testing)
713
+ */
714
+ export function resetPluginRegistry(): void {
715
+ registryInstance = null;
716
+ }