@qwickapps/server 1.2.0 → 1.3.0

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