@qwickapps/server 1.5.1 → 1.6.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 (135) hide show
  1. package/CHANGELOG.md +43 -0
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +41 -0
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/guards.d.ts.map +1 -1
  6. package/dist/core/guards.js +77 -0
  7. package/dist/core/guards.js.map +1 -1
  8. package/dist/core/health-manager.d.ts +4 -0
  9. package/dist/core/health-manager.d.ts.map +1 -1
  10. package/dist/core/health-manager.js +6 -1
  11. package/dist/core/health-manager.js.map +1 -1
  12. package/dist/core/plugin-registry.d.ts +55 -5
  13. package/dist/core/plugin-registry.d.ts.map +1 -1
  14. package/dist/core/plugin-registry.js +57 -19
  15. package/dist/core/plugin-registry.js.map +1 -1
  16. package/dist/core/types.d.ts +2 -0
  17. package/dist/core/types.d.ts.map +1 -1
  18. package/dist/index.d.ts +2 -2
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +3 -1
  21. package/dist/index.js.map +1 -1
  22. package/dist/plugins/api-keys/api-keys-plugin.d.ts +46 -0
  23. package/dist/plugins/api-keys/api-keys-plugin.d.ts.map +1 -0
  24. package/dist/plugins/api-keys/api-keys-plugin.js +329 -0
  25. package/dist/plugins/api-keys/api-keys-plugin.js.map +1 -0
  26. package/dist/plugins/api-keys/index.d.ts +14 -0
  27. package/dist/plugins/api-keys/index.d.ts.map +1 -0
  28. package/dist/plugins/api-keys/index.js +17 -0
  29. package/dist/plugins/api-keys/index.js.map +1 -0
  30. package/dist/plugins/api-keys/middleware/bearer-token-auth.d.ts +74 -0
  31. package/dist/plugins/api-keys/middleware/bearer-token-auth.d.ts.map +1 -0
  32. package/dist/plugins/api-keys/middleware/bearer-token-auth.js +201 -0
  33. package/dist/plugins/api-keys/middleware/bearer-token-auth.js.map +1 -0
  34. package/dist/plugins/api-keys/middleware/index.d.ts +7 -0
  35. package/dist/plugins/api-keys/middleware/index.d.ts.map +1 -0
  36. package/dist/plugins/api-keys/middleware/index.js +7 -0
  37. package/dist/plugins/api-keys/middleware/index.js.map +1 -0
  38. package/dist/plugins/api-keys/stores/index.d.ts +7 -0
  39. package/dist/plugins/api-keys/stores/index.d.ts.map +1 -0
  40. package/dist/plugins/api-keys/stores/index.js +7 -0
  41. package/dist/plugins/api-keys/stores/index.js.map +1 -0
  42. package/dist/plugins/api-keys/stores/postgres-store.d.ts +34 -0
  43. package/dist/plugins/api-keys/stores/postgres-store.d.ts.map +1 -0
  44. package/dist/plugins/api-keys/stores/postgres-store.js +360 -0
  45. package/dist/plugins/api-keys/stores/postgres-store.js.map +1 -0
  46. package/dist/plugins/api-keys/types.d.ts +268 -0
  47. package/dist/plugins/api-keys/types.d.ts.map +1 -0
  48. package/dist/plugins/api-keys/types.js +56 -0
  49. package/dist/plugins/api-keys/types.js.map +1 -0
  50. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -1
  51. package/dist/plugins/auth/auth-plugin.js +17 -1
  52. package/dist/plugins/auth/auth-plugin.js.map +1 -1
  53. package/dist/plugins/auth/auth-plugin.test.js +133 -0
  54. package/dist/plugins/auth/auth-plugin.test.js.map +1 -1
  55. package/dist/plugins/auth/env-config.d.ts.map +1 -1
  56. package/dist/plugins/auth/env-config.js +6 -2
  57. package/dist/plugins/auth/env-config.js.map +1 -1
  58. package/dist/plugins/auth/types.d.ts +10 -0
  59. package/dist/plugins/auth/types.d.ts.map +1 -1
  60. package/dist/plugins/auth/types.js.map +1 -1
  61. package/dist/plugins/devices/__tests__/token-utils.test.js +4 -2
  62. package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -1
  63. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  64. package/dist/plugins/frontend-app-plugin.js +21 -4
  65. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  66. package/dist/plugins/index.d.ts +2 -0
  67. package/dist/plugins/index.d.ts.map +1 -1
  68. package/dist/plugins/index.js +2 -0
  69. package/dist/plugins/index.js.map +1 -1
  70. package/dist/plugins/qwickbrain/index.d.ts +25 -0
  71. package/dist/plugins/qwickbrain/index.d.ts.map +1 -0
  72. package/dist/plugins/qwickbrain/index.js +24 -0
  73. package/dist/plugins/qwickbrain/index.js.map +1 -0
  74. package/dist/plugins/qwickbrain/qwickbrain-plugin.d.ts +23 -0
  75. package/dist/plugins/qwickbrain/qwickbrain-plugin.d.ts.map +1 -0
  76. package/dist/plugins/qwickbrain/qwickbrain-plugin.js +528 -0
  77. package/dist/plugins/qwickbrain/qwickbrain-plugin.js.map +1 -0
  78. package/dist/plugins/qwickbrain/types.d.ts +131 -0
  79. package/dist/plugins/qwickbrain/types.d.ts.map +1 -0
  80. package/dist/plugins/qwickbrain/types.js +9 -0
  81. package/dist/plugins/qwickbrain/types.js.map +1 -0
  82. package/dist/plugins/users/__tests__/postgres-store.test.js +1 -0
  83. package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -1
  84. package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
  85. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
  86. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
  87. package/dist/plugins/users/stores/postgres-store.js +59 -1
  88. package/dist/plugins/users/stores/postgres-store.js.map +1 -1
  89. package/dist/plugins/users/types.d.ts +22 -0
  90. package/dist/plugins/users/types.d.ts.map +1 -1
  91. package/dist-ui/assets/index-5nX8fM1a.js +469 -0
  92. package/dist-ui/assets/index-5nX8fM1a.js.map +1 -0
  93. package/dist-ui/index.html +1 -1
  94. package/dist-ui-lib/api/controlPanelApi.d.ts +68 -0
  95. package/dist-ui-lib/components/index.d.ts +2 -1
  96. package/dist-ui-lib/index.js +2642 -2281
  97. package/dist-ui-lib/index.js.map +1 -1
  98. package/dist-ui-lib/pages/APIKeysPage.d.ts +13 -0
  99. package/dist-ui-lib/pages/AcceptInvitationPage.d.ts +28 -0
  100. package/package.json +3 -2
  101. package/src/core/control-panel.ts +47 -0
  102. package/src/core/guards.ts +89 -0
  103. package/src/core/health-manager.ts +6 -1
  104. package/src/core/plugin-registry.ts +123 -25
  105. package/src/core/types.ts +2 -0
  106. package/src/index.ts +11 -0
  107. package/src/plugins/api-keys/api-keys-plugin.ts +397 -0
  108. package/src/plugins/api-keys/index.ts +49 -0
  109. package/src/plugins/api-keys/middleware/bearer-token-auth.ts +250 -0
  110. package/src/plugins/api-keys/middleware/index.ts +12 -0
  111. package/src/plugins/api-keys/stores/index.ts +7 -0
  112. package/src/plugins/api-keys/stores/postgres-store.ts +487 -0
  113. package/src/plugins/api-keys/types.ts +243 -0
  114. package/src/plugins/auth/auth-plugin.test.ts +167 -0
  115. package/src/plugins/auth/auth-plugin.ts +17 -1
  116. package/src/plugins/auth/env-config.ts +6 -2
  117. package/src/plugins/auth/types.ts +10 -0
  118. package/src/plugins/devices/__tests__/token-utils.test.ts +4 -2
  119. package/src/plugins/frontend-app-plugin.ts +24 -4
  120. package/src/plugins/index.ts +15 -0
  121. package/src/plugins/qwickbrain/index.ts +33 -0
  122. package/src/plugins/qwickbrain/qwickbrain-plugin.ts +642 -0
  123. package/src/plugins/qwickbrain/types.ts +146 -0
  124. package/src/plugins/users/__tests__/postgres-store.test.ts +1 -0
  125. package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
  126. package/src/plugins/users/stores/postgres-store.ts +69 -0
  127. package/src/plugins/users/types.ts +25 -0
  128. package/ui/src/App.tsx +6 -1
  129. package/ui/src/api/controlPanelApi.ts +206 -37
  130. package/ui/src/components/index.ts +6 -0
  131. package/ui/src/pages/APIKeysPage.tsx +661 -0
  132. package/ui/src/pages/AcceptInvitationPage.tsx +169 -0
  133. package/ui/src/pages/UsersPage.tsx +225 -2
  134. package/dist-ui/assets/index-CynOqPkb.js +0 -469
  135. package/dist-ui/assets/index-CynOqPkb.js.map +0 -1
@@ -0,0 +1,13 @@
1
+ /**
2
+ * APIKeysPage Component
3
+ *
4
+ * API key management page for authentication and authorization.
5
+ * Allows users to create, view, and manage API keys with scopes.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+ export interface APIKeysPageProps {
10
+ title?: string;
11
+ subtitle?: string;
12
+ }
13
+ export declare function APIKeysPage({ title, subtitle, }: APIKeysPageProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * AcceptInvitationPage Component
3
+ *
4
+ * Standalone page for users to accept invitations and activate their accounts.
5
+ * Can be used in control panel or frontend applications.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+ import { type User } from '../api/controlPanelApi';
10
+ export interface AcceptInvitationPageProps {
11
+ /** Invitation token (if not provided, will extract from URL) */
12
+ token?: string;
13
+ /** Title text */
14
+ title?: string;
15
+ /** Subtitle text */
16
+ subtitle?: string;
17
+ /** Success message */
18
+ successMessage?: string;
19
+ /** URL to redirect to after successful activation (optional) */
20
+ redirectUrl?: string;
21
+ /** Label for the redirect button */
22
+ redirectLabel?: string;
23
+ /** Callback when invitation is accepted successfully */
24
+ onSuccess?: (user: User) => void;
25
+ /** Callback when invitation acceptance fails */
26
+ onError?: (error: string) => void;
27
+ }
28
+ export declare function AcceptInvitationPage({ token: tokenProp, title, subtitle, successMessage, redirectUrl, redirectLabel, onSuccess, onError, }: AcceptInvitationPageProps): import("react/jsx-runtime").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qwickapps/server",
3
- "version": "1.5.1",
3
+ "version": "1.6.0",
4
4
  "description": "Plugin-based application server framework for building websites, APIs, admin dashboards, and full-stack products",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -54,7 +54,8 @@
54
54
  "cors": "^2.8.5",
55
55
  "express": "^4.18.2",
56
56
  "helmet": "^7.1.0",
57
- "http-proxy-middleware": "^3.0.3"
57
+ "http-proxy-middleware": "^3.0.3",
58
+ "zod": "^3.22.4"
58
59
  },
59
60
  "devDependencies": {
60
61
  "@emotion/react": "^11.14.0",
@@ -378,6 +378,53 @@ export function createControlPanel(options: CreateControlPanelOptions): ControlP
378
378
  }
379
379
  }
380
380
 
381
+ // Register all routes with automatic ordering by path specificity
382
+ const routes = pluginRegistry.getRoutes();
383
+
384
+ // Sort routes by path specificity (longest first, wildcards last)
385
+ routes.sort((a, b) => {
386
+ // Wildcards last
387
+ const aWild = a.path.includes('*') ? 1 : 0;
388
+ const bWild = b.path.includes('*') ? 1 : 0;
389
+ if (aWild !== bWild) return aWild - bWild;
390
+
391
+ // Longer paths first (more specific)
392
+ return b.path.length - a.path.length;
393
+ });
394
+
395
+ // Register routes with Express router (mounted at apiBasePath)
396
+ logger.debug(`Registering ${routes.length} routes in priority order:`);
397
+ for (const route of routes) {
398
+ // TODO: Add auth middleware if route.auth?.required
399
+ // For now, just register the handler
400
+ // Auth will be handled by auth plugin middleware globally
401
+
402
+ const handlers = [route.handler];
403
+
404
+ switch (route.method) {
405
+ case 'get':
406
+ router.get(route.path, ...handlers);
407
+ break;
408
+ case 'post':
409
+ router.post(route.path, ...handlers);
410
+ break;
411
+ case 'put':
412
+ router.put(route.path, ...handlers);
413
+ break;
414
+ case 'delete':
415
+ router.delete(route.path, ...handlers);
416
+ break;
417
+ case 'patch':
418
+ router.patch(route.path, ...handlers);
419
+ break;
420
+ case 'use':
421
+ router.use(route.path, ...handlers);
422
+ break;
423
+ }
424
+
425
+ logger.debug(` ${route.method.toUpperCase()} ${apiBasePath}${route.path}`);
426
+ }
427
+
381
428
  return new Promise((resolve) => {
382
429
  server = app.listen(config.port, () => {
383
430
  logger.debug(`Control panel listening on port ${config.port}`);
@@ -6,6 +6,7 @@
6
6
  * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
7
  */
8
8
 
9
+ import { createHmac, timingSafeEqual } from 'node:crypto';
9
10
  import type { Request, Response, NextFunction, RequestHandler } from 'express';
10
11
  import type {
11
12
  RouteGuardConfig,
@@ -14,6 +15,68 @@ import type {
14
15
  Auth0GuardConfig,
15
16
  } from './types.js';
16
17
 
18
+ // Session cookie configuration
19
+ const SESSION_COOKIE_NAME = 'cpanel_session';
20
+ const DEFAULT_SESSION_DURATION_HOURS = 8;
21
+
22
+ /**
23
+ * Create a signed session token with expiration
24
+ */
25
+ function createSessionToken(secret: string, durationHours: number): string {
26
+ const expiresAt = Date.now() + durationHours * 60 * 60 * 1000;
27
+ const payload = `cpanel:${expiresAt}`;
28
+ const signature = createHmac('sha256', secret).update(payload).digest('hex');
29
+ return `${payload}:${signature}`;
30
+ }
31
+
32
+ /**
33
+ * Verify a session token and check expiration
34
+ */
35
+ function verifySessionToken(token: string, secret: string): boolean {
36
+ const parts = token.split(':');
37
+ if (parts.length !== 3 || parts[0] !== 'cpanel') {
38
+ return false;
39
+ }
40
+
41
+ const [prefix, expiresAt, signature] = parts;
42
+ const payload = `${prefix}:${expiresAt}`;
43
+
44
+ // Verify signature
45
+ const expectedSignature = createHmac('sha256', secret).update(payload).digest('hex');
46
+ try {
47
+ if (!timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
48
+ return false;
49
+ }
50
+ } catch {
51
+ return false;
52
+ }
53
+
54
+ // Check expiration
55
+ const expires = parseInt(expiresAt, 10);
56
+ if (isNaN(expires) || Date.now() > expires) {
57
+ return false;
58
+ }
59
+
60
+ return true;
61
+ }
62
+
63
+ /**
64
+ * Parse cookies from Cookie header
65
+ */
66
+ function parseCookies(cookieHeader: string | undefined): Record<string, string> {
67
+ const cookies: Record<string, string> = {};
68
+ if (!cookieHeader) return cookies;
69
+
70
+ cookieHeader.split(';').forEach((cookie) => {
71
+ const [name, ...rest] = cookie.trim().split('=');
72
+ if (name && rest.length > 0) {
73
+ cookies[name] = rest.join('=');
74
+ }
75
+ });
76
+
77
+ return cookies;
78
+ }
79
+
17
80
  /**
18
81
  * Create a route guard middleware from configuration
19
82
  */
@@ -34,11 +97,18 @@ export function createRouteGuard(config: RouteGuardConfig): RequestHandler {
34
97
 
35
98
  /**
36
99
  * Create basic auth guard middleware
100
+ *
101
+ * This guard supports session cookies to prevent repeated login prompts.
102
+ * After successful Basic Auth, a signed session cookie is set that allows
103
+ * subsequent requests to proceed without re-prompting for credentials.
37
104
  */
38
105
  function createBasicAuthGuard(config: BasicAuthGuardConfig): RequestHandler {
39
106
  const expectedAuth = `Basic ${Buffer.from(`${config.username}:${config.password}`).toString('base64')}`;
40
107
  const realm = config.realm || 'Protected';
41
108
  const excludePaths = config.excludePaths || [];
109
+ const sessionDurationHours = config.sessionDurationHours ?? DEFAULT_SESSION_DURATION_HOURS;
110
+ // Use password as HMAC secret for session tokens
111
+ const sessionSecret = config.password;
42
112
 
43
113
  return (req: Request, res: Response, next: NextFunction) => {
44
114
  // Check if path is excluded
@@ -46,11 +116,30 @@ function createBasicAuthGuard(config: BasicAuthGuardConfig): RequestHandler {
46
116
  return next();
47
117
  }
48
118
 
119
+ // Check for valid session cookie first
120
+ const cookies = parseCookies(req.headers.cookie);
121
+ const sessionToken = cookies[SESSION_COOKIE_NAME];
122
+ if (sessionToken && verifySessionToken(sessionToken, sessionSecret)) {
123
+ return next();
124
+ }
125
+
126
+ // Check Authorization header
49
127
  const authHeader = req.headers.authorization;
50
128
  if (authHeader === expectedAuth) {
129
+ // Set session cookie on successful auth
130
+ const token = createSessionToken(sessionSecret, sessionDurationHours);
131
+ const maxAge = sessionDurationHours * 60 * 60; // seconds
132
+ // Add Secure flag when running over HTTPS
133
+ const isSecure = req.secure || req.headers['x-forwarded-proto'] === 'https';
134
+ const secureFlag = isSecure ? ' Secure;' : '';
135
+ res.setHeader(
136
+ 'Set-Cookie',
137
+ `${SESSION_COOKIE_NAME}=${token}; Path=/; HttpOnly; SameSite=Strict;${secureFlag} Max-Age=${maxAge}`
138
+ );
51
139
  return next();
52
140
  }
53
141
 
142
+ // No valid session or auth header - prompt for credentials
54
143
  res.setHeader('WWW-Authenticate', `Basic realm="${realm}"`);
55
144
  res.status(401).json({
56
145
  error: 'Unauthorized',
@@ -182,6 +182,10 @@ export class HealthManager {
182
182
 
183
183
  /**
184
184
  * Get aggregated status
185
+ *
186
+ * Returns 'degraded' instead of 'unhealthy' when subsystems fail,
187
+ * allowing the service to remain available even when dependencies are down.
188
+ * This ensures the control panel and other features remain accessible.
185
189
  */
186
190
  getAggregatedStatus(): HealthStatus {
187
191
  const results = Array.from(this.results.values());
@@ -191,7 +195,8 @@ export class HealthManager {
191
195
  const unhealthyCount = results.filter((r) => r.status === 'unhealthy').length;
192
196
  const degradedCount = results.filter((r) => r.status === 'degraded').length;
193
197
 
194
- if (unhealthyCount > 0) return 'unhealthy';
198
+ // Return 'degraded' instead of 'unhealthy' to keep service available (HTTP 200)
199
+ if (unhealthyCount > 0) return 'degraded';
195
200
  if (degradedCount > 0) return 'degraded';
196
201
 
197
202
  const hasUnknown = results.some((r) => r.status === 'unknown');
@@ -51,10 +51,19 @@ export interface PluginInfo {
51
51
  id: string;
52
52
  name: string;
53
53
  version?: string;
54
+ type: PluginType;
55
+ slug?: string; // Current slug (may be customized)
54
56
  status: 'starting' | 'active' | 'stopped' | 'error';
55
57
  error?: string;
56
58
  }
57
59
 
60
+ /**
61
+ * Plugin type determines routing capabilities
62
+ * - regular: Can only handle routes under their slug prefix (e.g., /mcp/*)
63
+ * - system: Can handle any path (e.g., /*, /auth/*)
64
+ */
65
+ export type PluginType = 'regular' | 'system';
66
+
58
67
  /**
59
68
  * The Plugin interface - simple lifecycle with event handling
60
69
  */
@@ -68,6 +77,30 @@ export interface Plugin {
68
77
  /** Plugin version (semver) */
69
78
  version?: string;
70
79
 
80
+ /**
81
+ * Plugin type determines routing capabilities
82
+ * - regular: Must use slug prefix for all routes
83
+ * - system: Can register routes at any path
84
+ * Default: 'regular'
85
+ */
86
+ type?: PluginType;
87
+
88
+ /**
89
+ * Default slug for regular plugins (path prefix for routes)
90
+ * Example: 'mcp' makes routes available under /mcp/*
91
+ * Can be overridden via plugin config
92
+ * Ignored for system plugins
93
+ */
94
+ slug?: string;
95
+
96
+ /**
97
+ * Configuration options for this plugin
98
+ */
99
+ configurable?: {
100
+ /** Allow users to customize the slug via UI */
101
+ slug?: boolean;
102
+ };
103
+
71
104
  /**
72
105
  * Called when the plugin starts.
73
106
  * Initialize resources, register routes/UI contributions here.
@@ -150,18 +183,34 @@ export interface WidgetContribution {
150
183
  pluginId: string;
151
184
  }
152
185
 
186
+ /**
187
+ * Auth configuration for routes
188
+ */
189
+ export interface RouteAuthConfig {
190
+ /** Whether authentication is required */
191
+ required: boolean;
192
+ /** Allowed roles (if auth required) */
193
+ roles?: string[];
194
+ /** Paths to exclude from auth (for middleware routes using 'use' method) */
195
+ excludePaths?: string[];
196
+ }
197
+
153
198
  /**
154
199
  * Route definition for API routes
155
200
  */
156
201
  export interface RouteDefinition {
157
- /** HTTP method */
158
- method: 'get' | 'post' | 'put' | 'delete' | 'patch';
159
- /** Route path */
202
+ /** HTTP method (including 'use' for middleware) */
203
+ method: 'get' | 'post' | 'put' | 'delete' | 'patch' | 'use';
204
+ /** Route path (will be auto-prefixed with slug for regular plugins) */
160
205
  path: string;
161
206
  /** Request handler */
162
207
  handler: RequestHandler;
163
- /** Plugin ID that contributed this */
164
- pluginId: string;
208
+ /** Authentication configuration */
209
+ auth?: RouteAuthConfig;
210
+ /** Plugin ID that contributed this (set automatically) */
211
+ pluginId?: string;
212
+ /** Original path before slug prefixing (set automatically) */
213
+ originalPath?: string;
165
214
  }
166
215
 
167
216
  /**
@@ -321,6 +370,8 @@ export class PluginRegistryImpl implements PluginRegistry {
321
370
  private pluginStatus = new Map<string, PluginInfo['status']>();
322
371
  private pluginErrors = new Map<string, string>();
323
372
  private pluginConfigs = new Map<string, PluginConfig>();
373
+ private pluginSlugs = new Map<string, string>(); // pluginId -> slug
374
+ private currentPlugin: string | null = null; // Track plugin during onStart
324
375
 
325
376
  private routes: RouteDefinition[] = [];
326
377
  private menuItems: MenuContribution[] = [];
@@ -371,6 +422,8 @@ export class PluginRegistryImpl implements PluginRegistry {
371
422
  id: plugin.id,
372
423
  name: plugin.name,
373
424
  version: plugin.version,
425
+ type: plugin.type || 'regular',
426
+ slug: this.pluginSlugs.get(plugin.id),
374
427
  status: this.pluginStatus.get(plugin.id) || 'stopped',
375
428
  error: this.pluginErrors.get(plugin.id),
376
429
  }));
@@ -381,28 +434,34 @@ export class PluginRegistryImpl implements PluginRegistry {
381
434
  // ---------------------------------------------------------------------------
382
435
 
383
436
  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;
437
+ if (!this.currentPlugin) {
438
+ throw new Error('addRoute can only be called during plugin.onStart()');
403
439
  }
404
440
 
405
- this.logger.debug(`Route registered: ${route.method.toUpperCase()} ${route.path} by ${route.pluginId}`);
441
+ const plugin = this.plugins.get(this.currentPlugin)!;
442
+ const pluginType = plugin.type || 'regular';
443
+ const originalPath = route.path;
444
+ let fullPath = route.path;
445
+
446
+ // Auto-prefix for regular plugins
447
+ if (pluginType === 'regular') {
448
+ const slug = this.pluginSlugs.get(this.currentPlugin)!;
449
+ fullPath = `/${slug}${route.path}`;
450
+ }
451
+
452
+ const routeWithMetadata: RouteDefinition = {
453
+ ...route,
454
+ path: fullPath,
455
+ pluginId: this.currentPlugin,
456
+ originalPath,
457
+ };
458
+
459
+ this.routes.push(routeWithMetadata);
460
+
461
+ this.logger.debug(
462
+ `Route registered: ${route.method.toUpperCase()} ${fullPath} by ${this.currentPlugin}` +
463
+ (pluginType === 'regular' ? ` (original: ${originalPath})` : '')
464
+ );
406
465
  }
407
466
 
408
467
  addMenuItem(menu: MenuContribution): void {
@@ -572,6 +631,22 @@ export class PluginRegistryImpl implements PluginRegistry {
572
631
  return this.healthManager;
573
632
  }
574
633
 
634
+ // ---------------------------------------------------------------------------
635
+ // Slug management (internal)
636
+ // ---------------------------------------------------------------------------
637
+
638
+ /**
639
+ * Check if a slug is available
640
+ */
641
+ private isSlugAvailable(slug: string, excludePluginId?: string): boolean {
642
+ for (const [pluginId, existingSlug] of this.pluginSlugs.entries()) {
643
+ if (pluginId !== excludePluginId && existingSlug === slug) {
644
+ return false;
645
+ }
646
+ }
647
+ return true;
648
+ }
649
+
575
650
  // ---------------------------------------------------------------------------
576
651
  // Plugin lifecycle management (internal)
577
652
  // ---------------------------------------------------------------------------
@@ -583,6 +658,27 @@ export class PluginRegistryImpl implements PluginRegistry {
583
658
  this.plugins.set(plugin.id, plugin);
584
659
  this.pluginConfigs.set(plugin.id, config);
585
660
  this.pluginStatus.set(plugin.id, 'starting');
661
+ this.currentPlugin = plugin.id;
662
+
663
+ const pluginType = plugin.type || 'regular';
664
+
665
+ // Handle slug for regular plugins
666
+ if (pluginType === 'regular') {
667
+ const slug = (config.slug as string | undefined) || plugin.slug || plugin.id;
668
+
669
+ // Validate slug uniqueness
670
+ if (!this.isSlugAvailable(slug, plugin.id)) {
671
+ this.currentPlugin = null;
672
+ const errorMessage = `Slug conflict: "${slug}" already in use`;
673
+ this.pluginStatus.set(plugin.id, 'error');
674
+ this.pluginErrors.set(plugin.id, errorMessage);
675
+ this.logger.error(`Plugin ${plugin.id} failed to start: ${errorMessage}`);
676
+ return false;
677
+ }
678
+
679
+ this.pluginSlugs.set(plugin.id, slug);
680
+ this.logger.debug(`Plugin ${plugin.id} registered with slug: ${slug}`);
681
+ }
586
682
 
587
683
  try {
588
684
  await Promise.race([
@@ -592,6 +688,7 @@ export class PluginRegistryImpl implements PluginRegistry {
592
688
 
593
689
  this.pluginStatus.set(plugin.id, 'active');
594
690
  this.pluginErrors.delete(plugin.id);
691
+ this.currentPlugin = null;
595
692
 
596
693
  this.emit({
597
694
  type: 'plugin:started',
@@ -605,6 +702,7 @@ export class PluginRegistryImpl implements PluginRegistry {
605
702
  const errorMessage = error instanceof Error ? error.message : String(error);
606
703
  this.pluginStatus.set(plugin.id, 'error');
607
704
  this.pluginErrors.set(plugin.id, errorMessage);
705
+ this.currentPlugin = null;
608
706
 
609
707
  this.emit({
610
708
  type: 'plugin:error',
package/src/core/types.ts CHANGED
@@ -24,6 +24,8 @@ export interface BasicAuthGuardConfig {
24
24
  realm?: string;
25
25
  /** Paths to exclude from authentication (e.g., ['/health']) */
26
26
  excludePaths?: string[];
27
+ /** Session cookie duration in hours (default: 8) */
28
+ sessionDurationHours?: number;
27
29
  }
28
30
 
29
31
  /**
package/src/index.ts CHANGED
@@ -262,6 +262,10 @@ export {
262
262
  getActivityLog,
263
263
  postgresParentalStore,
264
264
  kidsAdapter,
265
+ // QwickBrain plugin
266
+ createQwickBrainPlugin,
267
+ getConnectionStatus,
268
+ isConnected,
265
269
  } from './plugins/index.js';
266
270
  export type {
267
271
  HealthPluginConfig,
@@ -414,4 +418,11 @@ export type {
414
418
  ParentalApiConfig,
415
419
  PostgresParentalStoreConfig,
416
420
  KidsAdapterConfig,
421
+ // QwickBrain plugin types
422
+ QwickBrainPluginConfig,
423
+ MCPToolDefinition,
424
+ MCPToolCallRequest,
425
+ MCPToolCallResponse,
426
+ QwickBrainConnectionStatus,
427
+ MCPRateLimitConfig,
417
428
  } from './plugins/index.js';