@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.
- package/README.md +238 -0
- package/dist/core/control-panel.d.ts +7 -2
- package/dist/core/control-panel.d.ts.map +1 -1
- package/dist/core/control-panel.js +92 -54
- package/dist/core/control-panel.js.map +1 -1
- package/dist/core/gateway.d.ts +159 -79
- package/dist/core/gateway.d.ts.map +1 -1
- package/dist/core/gateway.js +679 -319
- package/dist/core/gateway.js.map +1 -1
- package/dist/core/index.d.ts +3 -1
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +2 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/plugin-registry.d.ts +271 -0
- package/dist/core/plugin-registry.d.ts.map +1 -0
- package/dist/core/plugin-registry.js +326 -0
- package/dist/core/plugin-registry.js.map +1 -0
- package/dist/core/types.d.ts +16 -33
- package/dist/core/types.d.ts.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -7
- package/dist/index.js.map +1 -1
- package/dist/plugins/auth/adapters/auth0-adapter.d.ts +14 -0
- package/dist/plugins/auth/adapters/auth0-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/auth0-adapter.js +179 -0
- package/dist/plugins/auth/adapters/auth0-adapter.js.map +1 -0
- package/dist/plugins/auth/adapters/basic-adapter.d.ts +13 -0
- package/dist/plugins/auth/adapters/basic-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/basic-adapter.js +51 -0
- package/dist/plugins/auth/adapters/basic-adapter.js.map +1 -0
- package/dist/plugins/auth/adapters/index.d.ts +9 -0
- package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/index.js +9 -0
- package/dist/plugins/auth/adapters/index.js.map +1 -0
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts +13 -0
- package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -0
- package/dist/plugins/auth/adapters/supabase-adapter.js +109 -0
- package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.d.ts +40 -0
- package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
- package/dist/plugins/auth/auth-plugin.js +255 -0
- package/dist/plugins/auth/auth-plugin.js.map +1 -0
- package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
- package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
- package/dist/plugins/auth/auth-plugin.test.js +147 -0
- package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
- package/dist/plugins/auth/index.d.ts +12 -0
- package/dist/plugins/auth/index.d.ts.map +1 -0
- package/dist/plugins/auth/index.js +13 -0
- package/dist/plugins/auth/index.js.map +1 -0
- package/dist/plugins/auth/types.d.ts +148 -0
- package/dist/plugins/auth/types.d.ts.map +1 -0
- package/dist/plugins/auth/types.js +14 -0
- package/dist/plugins/auth/types.js.map +1 -0
- package/dist/plugins/bans/bans-plugin.d.ts +59 -0
- package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
- package/dist/plugins/bans/bans-plugin.js +428 -0
- package/dist/plugins/bans/bans-plugin.js.map +1 -0
- package/dist/plugins/bans/index.d.ts +9 -0
- package/dist/plugins/bans/index.d.ts.map +1 -0
- package/dist/plugins/bans/index.js +10 -0
- package/dist/plugins/bans/index.js.map +1 -0
- package/dist/plugins/bans/stores/index.d.ts +7 -0
- package/dist/plugins/bans/stores/index.d.ts.map +1 -0
- package/dist/plugins/bans/stores/index.js +7 -0
- package/dist/plugins/bans/stores/index.js.map +1 -0
- package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
- package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/bans/stores/postgres-store.js +132 -0
- package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
- package/dist/plugins/bans/types.d.ts +128 -0
- package/dist/plugins/bans/types.d.ts.map +1 -0
- package/dist/plugins/bans/types.js +11 -0
- package/dist/plugins/bans/types.js.map +1 -0
- package/dist/plugins/cache-plugin.d.ts +14 -3
- package/dist/plugins/cache-plugin.d.ts.map +1 -1
- package/dist/plugins/cache-plugin.js +27 -7
- package/dist/plugins/cache-plugin.js.map +1 -1
- package/dist/plugins/cache-plugin.test.js +96 -32
- package/dist/plugins/cache-plugin.test.js.map +1 -1
- package/dist/plugins/config-plugin.d.ts +3 -2
- package/dist/plugins/config-plugin.d.ts.map +1 -1
- package/dist/plugins/config-plugin.js +17 -10
- package/dist/plugins/config-plugin.js.map +1 -1
- package/dist/plugins/diagnostics-plugin.d.ts +2 -2
- package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
- package/dist/plugins/diagnostics-plugin.js +17 -10
- package/dist/plugins/diagnostics-plugin.js.map +1 -1
- package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
- package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
- package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
- package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
- package/dist/plugins/entitlements/index.d.ts +12 -0
- package/dist/plugins/entitlements/index.d.ts.map +1 -0
- package/dist/plugins/entitlements/index.js +16 -0
- package/dist/plugins/entitlements/index.js.map +1 -0
- package/dist/plugins/entitlements/sources/index.d.ts +9 -0
- package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
- package/dist/plugins/entitlements/sources/index.js +9 -0
- package/dist/plugins/entitlements/sources/index.js.map +1 -0
- package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
- package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
- package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
- package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
- package/dist/plugins/entitlements/types.d.ts +232 -0
- package/dist/plugins/entitlements/types.d.ts.map +1 -0
- package/dist/plugins/entitlements/types.js +11 -0
- package/dist/plugins/entitlements/types.js.map +1 -0
- package/dist/plugins/frontend-app-plugin.d.ts +9 -3
- package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
- package/dist/plugins/frontend-app-plugin.js +14 -9
- package/dist/plugins/frontend-app-plugin.js.map +1 -1
- package/dist/plugins/health-plugin.d.ts +5 -2
- package/dist/plugins/health-plugin.d.ts.map +1 -1
- package/dist/plugins/health-plugin.js +20 -5
- package/dist/plugins/health-plugin.js.map +1 -1
- package/dist/plugins/index.d.ts +8 -2
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +8 -2
- package/dist/plugins/index.js.map +1 -1
- package/dist/plugins/logs-plugin.d.ts +3 -2
- package/dist/plugins/logs-plugin.d.ts.map +1 -1
- package/dist/plugins/logs-plugin.js +21 -12
- package/dist/plugins/logs-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.d.ts +3 -3
- package/dist/plugins/postgres-plugin.d.ts.map +1 -1
- package/dist/plugins/postgres-plugin.js +9 -7
- package/dist/plugins/postgres-plugin.js.map +1 -1
- package/dist/plugins/postgres-plugin.test.js +47 -29
- package/dist/plugins/postgres-plugin.test.js.map +1 -1
- package/dist/plugins/users/index.d.ts +12 -0
- package/dist/plugins/users/index.d.ts.map +1 -0
- package/dist/plugins/users/index.js +13 -0
- package/dist/plugins/users/index.js.map +1 -0
- package/dist/plugins/users/stores/index.d.ts +7 -0
- package/dist/plugins/users/stores/index.d.ts.map +1 -0
- package/dist/plugins/users/stores/index.js +7 -0
- package/dist/plugins/users/stores/index.js.map +1 -0
- package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
- package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
- package/dist/plugins/users/stores/postgres-store.js +157 -0
- package/dist/plugins/users/stores/postgres-store.js.map +1 -0
- package/dist/plugins/users/types.d.ts +189 -0
- package/dist/plugins/users/types.d.ts.map +1 -0
- package/dist/plugins/users/types.js +12 -0
- package/dist/plugins/users/types.js.map +1 -0
- package/dist/plugins/users/users-plugin.d.ts +39 -0
- package/dist/plugins/users/users-plugin.d.ts.map +1 -0
- package/dist/plugins/users/users-plugin.js +242 -0
- package/dist/plugins/users/users-plugin.js.map +1 -0
- package/dist-ui/assets/index-Bsp2ntcw.js +465 -0
- package/dist-ui/assets/index-Bsp2ntcw.js.map +1 -0
- package/dist-ui/index.html +1 -1
- package/dist-ui-lib/api/controlPanelApi.d.ts +232 -0
- package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
- package/dist-ui-lib/components/index.d.ts +18 -0
- package/dist-ui-lib/config/AppConfig.d.ts +7 -0
- package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
- package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
- package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
- package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +44 -0
- package/dist-ui-lib/dashboard/builtInWidgets.d.ts +19 -0
- package/dist-ui-lib/dashboard/index.d.ts +13 -0
- package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
- package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
- package/dist-ui-lib/index.js +6441 -0
- package/dist-ui-lib/index.js.map +1 -0
- package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
- package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
- package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
- package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
- package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
- package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
- package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
- package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
- package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
- package/package.json +18 -6
- package/src/core/control-panel.ts +114 -61
- package/src/core/gateway.ts +863 -403
- package/src/core/index.ts +21 -2
- package/src/core/plugin-registry.ts +653 -0
- package/src/core/types.ts +31 -37
- package/src/index.ts +118 -19
- package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
- package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
- package/src/plugins/auth/adapters/index.ts +9 -0
- package/src/plugins/auth/adapters/supabase-adapter.ts +141 -0
- package/src/plugins/auth/auth-plugin.test.ts +176 -0
- package/src/plugins/auth/auth-plugin.ts +303 -0
- package/src/plugins/auth/index.ts +33 -0
- package/src/plugins/auth/types.ts +165 -0
- package/src/plugins/bans/bans-plugin.ts +485 -0
- package/src/plugins/bans/index.ts +31 -0
- package/src/plugins/bans/stores/index.ts +7 -0
- package/src/plugins/bans/stores/postgres-store.ts +195 -0
- package/src/plugins/bans/types.ts +141 -0
- package/src/plugins/cache-plugin.test.ts +105 -32
- package/src/plugins/cache-plugin.ts +40 -9
- package/src/plugins/config-plugin.ts +23 -12
- package/src/plugins/diagnostics-plugin.ts +22 -12
- package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
- package/src/plugins/entitlements/index.ts +51 -0
- package/src/plugins/entitlements/sources/index.ts +9 -0
- package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
- package/src/plugins/entitlements/types.ts +256 -0
- package/src/plugins/frontend-app-plugin.ts +24 -12
- package/src/plugins/health-plugin.ts +27 -7
- package/src/plugins/index.ts +106 -4
- package/src/plugins/logs-plugin.ts +28 -14
- package/src/plugins/postgres-plugin.test.ts +49 -29
- package/src/plugins/postgres-plugin.ts +11 -9
- package/src/plugins/users/index.ts +35 -0
- package/src/plugins/users/stores/index.ts +7 -0
- package/src/plugins/users/stores/postgres-store.ts +225 -0
- package/src/plugins/users/types.ts +209 -0
- package/src/plugins/users/users-plugin.ts +281 -0
- package/ui/src/App.tsx +185 -31
- package/ui/src/api/controlPanelApi.ts +354 -1
- package/ui/src/components/ControlPanelApp.tsx +209 -0
- package/ui/src/components/index.ts +62 -0
- package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
- package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
- package/ui/src/dashboard/PluginWidgetRenderer.tsx +115 -0
- package/ui/src/dashboard/WidgetComponentRegistry.tsx +116 -0
- package/ui/src/dashboard/builtInWidgets.tsx +29 -0
- package/ui/src/dashboard/index.ts +35 -0
- package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
- package/ui/src/dashboard/widgets/index.ts +7 -0
- package/ui/src/pages/DashboardPage.tsx +28 -149
- package/ui/src/pages/EntitlementsPage.tsx +557 -0
- package/ui/src/pages/LogsPage.tsx +174 -8
- package/ui/src/pages/PluginPage.tsx +148 -0
- package/ui/src/pages/SystemPage.tsx +445 -0
- package/ui/src/pages/UsersPage.tsx +837 -0
- package/ui/tsconfig.lib.json +11 -0
- package/ui/vite.lib.config.ts +51 -0
- package/dist-ui/assets/index-CW1BviRn.js +0 -465
- package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
- 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
|
+
}
|