@qwickapps/server 1.4.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (273) hide show
  1. package/CHANGELOG.md +507 -0
  2. package/README.md +9 -0
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.d.ts.map +1 -1
  5. package/dist/index.js +12 -2
  6. package/dist/index.js.map +1 -1
  7. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -1
  8. package/dist/plugins/bans/bans-plugin.js +12 -3
  9. package/dist/plugins/bans/bans-plugin.js.map +1 -1
  10. package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts +11 -0
  11. package/dist/plugins/devices/__tests__/devices-plugin.test.d.ts.map +1 -0
  12. package/dist/plugins/devices/__tests__/devices-plugin.test.js +410 -0
  13. package/dist/plugins/devices/__tests__/devices-plugin.test.js.map +1 -0
  14. package/dist/plugins/devices/__tests__/token-utils.test.d.ts +7 -0
  15. package/dist/plugins/devices/__tests__/token-utils.test.d.ts.map +1 -0
  16. package/dist/plugins/devices/__tests__/token-utils.test.js +197 -0
  17. package/dist/plugins/devices/__tests__/token-utils.test.js.map +1 -0
  18. package/dist/plugins/devices/adapters/compute-adapter.d.ts +36 -0
  19. package/dist/plugins/devices/adapters/compute-adapter.d.ts.map +1 -0
  20. package/dist/plugins/devices/adapters/compute-adapter.js +100 -0
  21. package/dist/plugins/devices/adapters/compute-adapter.js.map +1 -0
  22. package/dist/plugins/devices/adapters/index.d.ts +12 -0
  23. package/dist/plugins/devices/adapters/index.d.ts.map +1 -0
  24. package/dist/plugins/devices/adapters/index.js +10 -0
  25. package/dist/plugins/devices/adapters/index.js.map +1 -0
  26. package/dist/plugins/devices/adapters/mobile-adapter.d.ts +41 -0
  27. package/dist/plugins/devices/adapters/mobile-adapter.d.ts.map +1 -0
  28. package/dist/plugins/devices/adapters/mobile-adapter.js +131 -0
  29. package/dist/plugins/devices/adapters/mobile-adapter.js.map +1 -0
  30. package/dist/plugins/devices/devices-plugin.d.ts +70 -0
  31. package/dist/plugins/devices/devices-plugin.d.ts.map +1 -0
  32. package/dist/plugins/devices/devices-plugin.js +453 -0
  33. package/dist/plugins/devices/devices-plugin.js.map +1 -0
  34. package/dist/plugins/devices/index.d.ts +18 -0
  35. package/dist/plugins/devices/index.d.ts.map +1 -0
  36. package/dist/plugins/devices/index.js +18 -0
  37. package/dist/plugins/devices/index.js.map +1 -0
  38. package/dist/plugins/devices/stores/index.d.ts +9 -0
  39. package/dist/plugins/devices/stores/index.d.ts.map +1 -0
  40. package/dist/plugins/devices/stores/index.js +9 -0
  41. package/dist/plugins/devices/stores/index.js.map +1 -0
  42. package/dist/plugins/devices/stores/postgres-store.d.ts +26 -0
  43. package/dist/plugins/devices/stores/postgres-store.d.ts.map +1 -0
  44. package/dist/plugins/devices/stores/postgres-store.js +199 -0
  45. package/dist/plugins/devices/stores/postgres-store.js.map +1 -0
  46. package/dist/plugins/devices/token-utils.d.ts +100 -0
  47. package/dist/plugins/devices/token-utils.d.ts.map +1 -0
  48. package/dist/plugins/devices/token-utils.js +162 -0
  49. package/dist/plugins/devices/token-utils.js.map +1 -0
  50. package/dist/plugins/devices/types.d.ts +307 -0
  51. package/dist/plugins/devices/types.d.ts.map +1 -0
  52. package/dist/plugins/devices/types.js +10 -0
  53. package/dist/plugins/devices/types.js.map +1 -0
  54. package/dist/plugins/index.d.ts +14 -2
  55. package/dist/plugins/index.d.ts.map +1 -1
  56. package/dist/plugins/index.js +13 -1
  57. package/dist/plugins/index.js.map +1 -1
  58. package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts +5 -0
  59. package/dist/plugins/notifications/__tests__/notifications-manager.test.d.ts.map +1 -0
  60. package/dist/plugins/notifications/__tests__/notifications-manager.test.js +470 -0
  61. package/dist/plugins/notifications/__tests__/notifications-manager.test.js.map +1 -0
  62. package/dist/plugins/notifications/index.d.ts +71 -0
  63. package/dist/plugins/notifications/index.d.ts.map +1 -0
  64. package/dist/plugins/notifications/index.js +72 -0
  65. package/dist/plugins/notifications/index.js.map +1 -0
  66. package/dist/plugins/notifications/notifications-manager.d.ts +182 -0
  67. package/dist/plugins/notifications/notifications-manager.d.ts.map +1 -0
  68. package/dist/plugins/notifications/notifications-manager.js +610 -0
  69. package/dist/plugins/notifications/notifications-manager.js.map +1 -0
  70. package/dist/plugins/notifications/notifications-plugin.d.ts +83 -0
  71. package/dist/plugins/notifications/notifications-plugin.d.ts.map +1 -0
  72. package/dist/plugins/notifications/notifications-plugin.js +337 -0
  73. package/dist/plugins/notifications/notifications-plugin.js.map +1 -0
  74. package/dist/plugins/notifications/types.d.ts +164 -0
  75. package/dist/plugins/notifications/types.d.ts.map +1 -0
  76. package/dist/plugins/notifications/types.js +9 -0
  77. package/dist/plugins/notifications/types.js.map +1 -0
  78. package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts +12 -0
  79. package/dist/plugins/parental/__tests__/parental-plugin.test.d.ts.map +1 -0
  80. package/dist/plugins/parental/__tests__/parental-plugin.test.js +349 -0
  81. package/dist/plugins/parental/__tests__/parental-plugin.test.js.map +1 -0
  82. package/dist/plugins/parental/adapters/index.d.ts +8 -0
  83. package/dist/plugins/parental/adapters/index.d.ts.map +1 -0
  84. package/dist/plugins/parental/adapters/index.js +7 -0
  85. package/dist/plugins/parental/adapters/index.js.map +1 -0
  86. package/dist/plugins/parental/adapters/kids-adapter.d.ts +24 -0
  87. package/dist/plugins/parental/adapters/kids-adapter.d.ts.map +1 -0
  88. package/dist/plugins/parental/adapters/kids-adapter.js +174 -0
  89. package/dist/plugins/parental/adapters/kids-adapter.js.map +1 -0
  90. package/dist/plugins/parental/index.d.ts +14 -0
  91. package/dist/plugins/parental/index.d.ts.map +1 -0
  92. package/dist/plugins/parental/index.js +15 -0
  93. package/dist/plugins/parental/index.js.map +1 -0
  94. package/dist/plugins/parental/parental-plugin.d.ts +88 -0
  95. package/dist/plugins/parental/parental-plugin.d.ts.map +1 -0
  96. package/dist/plugins/parental/parental-plugin.js +666 -0
  97. package/dist/plugins/parental/parental-plugin.js.map +1 -0
  98. package/dist/plugins/parental/stores/index.d.ts +7 -0
  99. package/dist/plugins/parental/stores/index.d.ts.map +1 -0
  100. package/dist/plugins/parental/stores/index.js +7 -0
  101. package/dist/plugins/parental/stores/index.js.map +1 -0
  102. package/dist/plugins/parental/stores/postgres-store.d.ts +10 -0
  103. package/dist/plugins/parental/stores/postgres-store.d.ts.map +1 -0
  104. package/dist/plugins/parental/stores/postgres-store.js +209 -0
  105. package/dist/plugins/parental/stores/postgres-store.js.map +1 -0
  106. package/dist/plugins/parental/types.d.ts +154 -0
  107. package/dist/plugins/parental/types.d.ts.map +1 -0
  108. package/dist/plugins/parental/types.js +10 -0
  109. package/dist/plugins/parental/types.js.map +1 -0
  110. package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts +11 -0
  111. package/dist/plugins/profiles/__tests__/profiles-plugin.test.d.ts.map +1 -0
  112. package/dist/plugins/profiles/__tests__/profiles-plugin.test.js +243 -0
  113. package/dist/plugins/profiles/__tests__/profiles-plugin.test.js.map +1 -0
  114. package/dist/plugins/profiles/index.d.ts +12 -0
  115. package/dist/plugins/profiles/index.d.ts.map +1 -0
  116. package/dist/plugins/profiles/index.js +13 -0
  117. package/dist/plugins/profiles/index.js.map +1 -0
  118. package/dist/plugins/profiles/profiles-plugin.d.ts +71 -0
  119. package/dist/plugins/profiles/profiles-plugin.d.ts.map +1 -0
  120. package/dist/plugins/profiles/profiles-plugin.js +481 -0
  121. package/dist/plugins/profiles/profiles-plugin.js.map +1 -0
  122. package/dist/plugins/profiles/stores/index.d.ts +9 -0
  123. package/dist/plugins/profiles/stores/index.d.ts.map +1 -0
  124. package/dist/plugins/profiles/stores/index.js +9 -0
  125. package/dist/plugins/profiles/stores/index.js.map +1 -0
  126. package/dist/plugins/profiles/stores/postgres-store.d.ts +18 -0
  127. package/dist/plugins/profiles/stores/postgres-store.d.ts.map +1 -0
  128. package/dist/plugins/profiles/stores/postgres-store.js +310 -0
  129. package/dist/plugins/profiles/stores/postgres-store.js.map +1 -0
  130. package/dist/plugins/profiles/types.d.ts +289 -0
  131. package/dist/plugins/profiles/types.d.ts.map +1 -0
  132. package/dist/plugins/profiles/types.js +10 -0
  133. package/dist/plugins/profiles/types.js.map +1 -0
  134. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts +11 -0
  135. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.d.ts.map +1 -0
  136. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js +305 -0
  137. package/dist/plugins/subscriptions/__tests__/subscriptions-plugin.test.js.map +1 -0
  138. package/dist/plugins/subscriptions/index.d.ts +12 -0
  139. package/dist/plugins/subscriptions/index.d.ts.map +1 -0
  140. package/dist/plugins/subscriptions/index.js +13 -0
  141. package/dist/plugins/subscriptions/index.js.map +1 -0
  142. package/dist/plugins/subscriptions/stores/index.d.ts +9 -0
  143. package/dist/plugins/subscriptions/stores/index.d.ts.map +1 -0
  144. package/dist/plugins/subscriptions/stores/index.js +9 -0
  145. package/dist/plugins/subscriptions/stores/index.js.map +1 -0
  146. package/dist/plugins/subscriptions/stores/postgres-store.d.ts +14 -0
  147. package/dist/plugins/subscriptions/stores/postgres-store.d.ts.map +1 -0
  148. package/dist/plugins/subscriptions/stores/postgres-store.js +359 -0
  149. package/dist/plugins/subscriptions/stores/postgres-store.js.map +1 -0
  150. package/dist/plugins/subscriptions/subscriptions-plugin.d.ts +82 -0
  151. package/dist/plugins/subscriptions/subscriptions-plugin.d.ts.map +1 -0
  152. package/dist/plugins/subscriptions/subscriptions-plugin.js +449 -0
  153. package/dist/plugins/subscriptions/subscriptions-plugin.js.map +1 -0
  154. package/dist/plugins/subscriptions/types.d.ts +308 -0
  155. package/dist/plugins/subscriptions/types.d.ts.map +1 -0
  156. package/dist/plugins/subscriptions/types.js +10 -0
  157. package/dist/plugins/subscriptions/types.js.map +1 -0
  158. package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts +11 -0
  159. package/dist/plugins/usage/__tests__/usage-plugin.test.d.ts.map +1 -0
  160. package/dist/plugins/usage/__tests__/usage-plugin.test.js +218 -0
  161. package/dist/plugins/usage/__tests__/usage-plugin.test.js.map +1 -0
  162. package/dist/plugins/usage/index.d.ts +12 -0
  163. package/dist/plugins/usage/index.d.ts.map +1 -0
  164. package/dist/plugins/usage/index.js +13 -0
  165. package/dist/plugins/usage/index.js.map +1 -0
  166. package/dist/plugins/usage/stores/index.d.ts +9 -0
  167. package/dist/plugins/usage/stores/index.d.ts.map +1 -0
  168. package/dist/plugins/usage/stores/index.js +9 -0
  169. package/dist/plugins/usage/stores/index.js.map +1 -0
  170. package/dist/plugins/usage/stores/postgres-store.d.ts +14 -0
  171. package/dist/plugins/usage/stores/postgres-store.d.ts.map +1 -0
  172. package/dist/plugins/usage/stores/postgres-store.js +146 -0
  173. package/dist/plugins/usage/stores/postgres-store.js.map +1 -0
  174. package/dist/plugins/usage/types.d.ts +195 -0
  175. package/dist/plugins/usage/types.d.ts.map +1 -0
  176. package/dist/plugins/usage/types.js +10 -0
  177. package/dist/plugins/usage/types.js.map +1 -0
  178. package/dist/plugins/usage/usage-plugin.d.ts +51 -0
  179. package/dist/plugins/usage/usage-plugin.d.ts.map +1 -0
  180. package/dist/plugins/usage/usage-plugin.js +412 -0
  181. package/dist/plugins/usage/usage-plugin.js.map +1 -0
  182. package/dist/plugins/users/__tests__/postgres-store.test.d.ts +10 -0
  183. package/dist/plugins/users/__tests__/postgres-store.test.d.ts.map +1 -0
  184. package/dist/plugins/users/__tests__/postgres-store.test.js +229 -0
  185. package/dist/plugins/users/__tests__/postgres-store.test.js.map +1 -0
  186. package/dist/plugins/users/__tests__/users-plugin.test.js +3 -0
  187. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -1
  188. package/dist/plugins/users/index.d.ts +2 -2
  189. package/dist/plugins/users/index.d.ts.map +1 -1
  190. package/dist/plugins/users/index.js +1 -1
  191. package/dist/plugins/users/index.js.map +1 -1
  192. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -1
  193. package/dist/plugins/users/stores/postgres-store.js +76 -0
  194. package/dist/plugins/users/stores/postgres-store.js.map +1 -1
  195. package/dist/plugins/users/types.d.ts +74 -6
  196. package/dist/plugins/users/types.d.ts.map +1 -1
  197. package/dist/plugins/users/users-plugin.d.ts +15 -1
  198. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  199. package/dist/plugins/users/users-plugin.js +29 -0
  200. package/dist/plugins/users/users-plugin.js.map +1 -1
  201. package/dist-ui/assets/index-CynOqPkb.js +469 -0
  202. package/dist-ui/assets/index-CynOqPkb.js.map +1 -0
  203. package/dist-ui/index.html +1 -1
  204. package/dist-ui-lib/api/controlPanelApi.d.ts +46 -0
  205. package/dist-ui-lib/components/StatCard.d.ts +16 -0
  206. package/dist-ui-lib/dashboard/widgets/NotificationsStatsWidget.d.ts +12 -0
  207. package/dist-ui-lib/dashboard/widgets/index.d.ts +1 -0
  208. package/dist-ui-lib/index.js +1822 -1611
  209. package/dist-ui-lib/index.js.map +1 -1
  210. package/dist-ui-lib/pages/NotificationsPage.d.ts +9 -0
  211. package/dist-ui-lib/utils/formatters.d.ts +19 -0
  212. package/package.json +3 -2
  213. package/src/index.ts +178 -0
  214. package/src/plugins/bans/bans-plugin.ts +15 -3
  215. package/src/plugins/devices/__tests__/devices-plugin.test.ts +551 -0
  216. package/src/plugins/devices/__tests__/token-utils.test.ts +264 -0
  217. package/src/plugins/devices/adapters/compute-adapter.ts +139 -0
  218. package/src/plugins/devices/adapters/index.ts +13 -0
  219. package/src/plugins/devices/adapters/mobile-adapter.ts +179 -0
  220. package/src/plugins/devices/devices-plugin.ts +538 -0
  221. package/src/plugins/devices/index.ts +69 -0
  222. package/src/plugins/devices/stores/index.ts +9 -0
  223. package/src/plugins/devices/stores/postgres-store.ts +304 -0
  224. package/src/plugins/devices/token-utils.ts +213 -0
  225. package/src/plugins/devices/types.ts +351 -0
  226. package/src/plugins/index.ts +218 -0
  227. package/src/plugins/notifications/__tests__/notifications-manager.test.ts +637 -0
  228. package/src/plugins/notifications/index.ts +91 -0
  229. package/src/plugins/notifications/notifications-manager.ts +773 -0
  230. package/src/plugins/notifications/notifications-plugin.ts +398 -0
  231. package/src/plugins/notifications/types.ts +207 -0
  232. package/src/plugins/parental/__tests__/parental-plugin.test.ts +465 -0
  233. package/src/plugins/parental/adapters/index.ts +8 -0
  234. package/src/plugins/parental/adapters/kids-adapter.ts +206 -0
  235. package/src/plugins/parental/index.ts +55 -0
  236. package/src/plugins/parental/parental-plugin.ts +759 -0
  237. package/src/plugins/parental/stores/index.ts +7 -0
  238. package/src/plugins/parental/stores/postgres-store.ts +304 -0
  239. package/src/plugins/parental/types.ts +180 -0
  240. package/src/plugins/profiles/__tests__/profiles-plugin.test.ts +321 -0
  241. package/src/plugins/profiles/index.ts +49 -0
  242. package/src/plugins/profiles/profiles-plugin.ts +546 -0
  243. package/src/plugins/profiles/stores/index.ts +9 -0
  244. package/src/plugins/profiles/stores/postgres-store.ts +439 -0
  245. package/src/plugins/profiles/types.ts +338 -0
  246. package/src/plugins/subscriptions/__tests__/subscriptions-plugin.test.ts +404 -0
  247. package/src/plugins/subscriptions/index.ts +51 -0
  248. package/src/plugins/subscriptions/stores/index.ts +9 -0
  249. package/src/plugins/subscriptions/stores/postgres-store.ts +482 -0
  250. package/src/plugins/subscriptions/subscriptions-plugin.ts +530 -0
  251. package/src/plugins/subscriptions/types.ts +355 -0
  252. package/src/plugins/usage/__tests__/usage-plugin.test.ts +288 -0
  253. package/src/plugins/usage/index.ts +39 -0
  254. package/src/plugins/usage/stores/index.ts +9 -0
  255. package/src/plugins/usage/stores/postgres-store.ts +213 -0
  256. package/src/plugins/usage/types.ts +222 -0
  257. package/src/plugins/usage/usage-plugin.ts +484 -0
  258. package/src/plugins/users/__tests__/postgres-store.test.ts +326 -0
  259. package/src/plugins/users/__tests__/users-plugin.test.ts +3 -0
  260. package/src/plugins/users/index.ts +6 -0
  261. package/src/plugins/users/stores/postgres-store.ts +104 -0
  262. package/src/plugins/users/types.ts +82 -6
  263. package/src/plugins/users/users-plugin.ts +37 -0
  264. package/ui/src/App.tsx +5 -1
  265. package/ui/src/api/controlPanelApi.ts +103 -6
  266. package/ui/src/components/StatCard.tsx +58 -0
  267. package/ui/src/dashboard/builtInWidgets.tsx +3 -1
  268. package/ui/src/dashboard/widgets/NotificationsStatsWidget.tsx +167 -0
  269. package/ui/src/dashboard/widgets/index.ts +1 -0
  270. package/ui/src/pages/NotificationsPage.tsx +417 -0
  271. package/ui/src/utils/formatters.ts +33 -0
  272. package/dist-ui/assets/index-D7DoZ9rL.js +0 -478
  273. package/dist-ui/assets/index-D7DoZ9rL.js.map +0 -1
@@ -0,0 +1,398 @@
1
+ /**
2
+ * Notifications Plugin
3
+ *
4
+ * Provides realtime SSE-based notifications for @qwickapps/server applications.
5
+ * Uses PostgreSQL LISTEN/NOTIFY for event distribution.
6
+ *
7
+ * ## Features
8
+ * - PostgreSQL LISTEN/NOTIFY integration
9
+ * - SSE endpoint for client connections
10
+ * - Device/user-based event filtering
11
+ * - Automatic reconnection with exponential backoff
12
+ * - Heartbeat system for connection health
13
+ * - Statistics and monitoring
14
+ *
15
+ * ## Usage
16
+ *
17
+ * ```typescript
18
+ * import { createGateway, createNotificationsPlugin } from '@qwickapps/server';
19
+ *
20
+ * const gateway = createGateway({
21
+ * productName: 'MyApp',
22
+ * controlPanel: {
23
+ * plugins: [
24
+ * { plugin: createPostgresPlugin({ url: DATABASE_URL }) },
25
+ * { plugin: createNotificationsPlugin({
26
+ * channels: ['events', 'messages'],
27
+ * heartbeat: { interval: 60000 },
28
+ * }) },
29
+ * ],
30
+ * },
31
+ * });
32
+ * ```
33
+ *
34
+ * ## SSE Endpoint
35
+ *
36
+ * ```
37
+ * GET /notifications/stream?device_id=xxx&user_id=yyy
38
+ *
39
+ * Events:
40
+ * - connected: Initial connection confirmation
41
+ * - heartbeat: Periodic health check
42
+ * - {channel}: Events from subscribed channels
43
+ * ```
44
+ *
45
+ * ## Security Note
46
+ *
47
+ * This plugin does NOT handle authentication. Authentication should be
48
+ * configured at the gateway level using the `guard` option or an auth
49
+ * middleware. The plugin trusts that requests reaching it are authorized.
50
+ *
51
+ * Example with gateway guard:
52
+ * ```typescript
53
+ * const gateway = createGateway({
54
+ * controlPanel: {
55
+ * guard: { type: 'basic', username: 'admin', password: 'secret' },
56
+ * plugins: [createNotificationsPlugin({ ... })],
57
+ * },
58
+ * });
59
+ * ```
60
+ *
61
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
62
+ */
63
+
64
+ import { randomUUID } from 'crypto';
65
+ import type { Request, Response } from 'express';
66
+ import type { Plugin, PluginConfig, PluginRegistry } from '../../core/plugin-registry.js';
67
+ import type { NotificationsPluginConfig } from './types.js';
68
+ import {
69
+ NotificationsManager,
70
+ setNotificationsManager,
71
+ } from './notifications-manager.js';
72
+ import { getPostgres, hasPostgres } from '../postgres-plugin.js';
73
+ import { getAuthenticatedUser } from '../auth/auth-plugin.js';
74
+
75
+ // Validation constants
76
+ const MAX_ID_LENGTH = 128;
77
+ const ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
78
+
79
+ /**
80
+ * Validate a device_id or user_id parameter
81
+ */
82
+ function validateId(id: string | undefined, paramName: string): { valid: boolean; error?: string } {
83
+ if (!id) {
84
+ return { valid: true }; // undefined is allowed
85
+ }
86
+
87
+ if (id.length > MAX_ID_LENGTH) {
88
+ return { valid: false, error: `${paramName} exceeds maximum length of ${MAX_ID_LENGTH} characters` };
89
+ }
90
+
91
+ if (!ID_PATTERN.test(id)) {
92
+ return { valid: false, error: `${paramName} contains invalid characters (allowed: alphanumeric, underscore, hyphen)` };
93
+ }
94
+
95
+ return { valid: true };
96
+ }
97
+
98
+ /**
99
+ * Create the Notifications plugin
100
+ *
101
+ * @param config Plugin configuration
102
+ * @returns Plugin instance
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * import { createNotificationsPlugin } from '@qwickapps/server';
107
+ *
108
+ * const plugin = createNotificationsPlugin({
109
+ * channels: ['bot_events', 'chat_messages'],
110
+ * heartbeat: { interval: 60000 },
111
+ * api: { prefix: '/notifications' },
112
+ * });
113
+ * ```
114
+ */
115
+ export function createNotificationsPlugin(config: NotificationsPluginConfig): Plugin {
116
+ const apiPrefix = config.api?.prefix || '/notifications';
117
+ const streamEnabled = config.api?.stream !== false;
118
+ const statsEnabled = config.api?.stats !== false;
119
+
120
+ let manager: NotificationsManager | null = null;
121
+
122
+ return {
123
+ id: 'notifications',
124
+ name: 'Notifications',
125
+ version: '1.0.0',
126
+
127
+ async onStart(_pluginConfig: PluginConfig, registry: PluginRegistry): Promise<void> {
128
+ const logger = registry.getLogger('notifications');
129
+
130
+ // Check for postgres plugin dependency
131
+ if (!hasPostgres()) {
132
+ throw new Error(
133
+ 'Notifications plugin requires postgres plugin. ' +
134
+ 'Please add createPostgresPlugin() before createNotificationsPlugin().'
135
+ );
136
+ }
137
+
138
+ // Get database connection string from postgres plugin
139
+ const postgres = getPostgres();
140
+ const pool = postgres.getPool();
141
+
142
+ // Extract connection string from pool config
143
+ // Note: pg.Pool stores config internally, we need to reconstruct it
144
+ const poolConfig = (pool as unknown as { options: { connectionString?: string } }).options;
145
+ let connectionString = poolConfig?.connectionString;
146
+
147
+ if (!connectionString) {
148
+ // If no connection string, try to get from environment
149
+ connectionString = process.env.DATABASE_URL;
150
+ }
151
+
152
+ if (!connectionString) {
153
+ throw new Error(
154
+ 'Could not determine PostgreSQL connection string. ' +
155
+ 'Ensure DATABASE_URL is set or postgres plugin was configured with a URL.'
156
+ );
157
+ }
158
+
159
+ logger.debug('Initializing notifications manager', {
160
+ channels: config.channels,
161
+ heartbeatInterval: config.heartbeat?.interval,
162
+ });
163
+
164
+ // Create and initialize manager
165
+ manager = new NotificationsManager(
166
+ connectionString,
167
+ config.channels,
168
+ config,
169
+ logger
170
+ );
171
+
172
+ await manager.initialize();
173
+ setNotificationsManager(manager);
174
+
175
+ // Register health check
176
+ registry.registerHealthCheck({
177
+ name: 'notifications',
178
+ type: 'custom',
179
+ check: async () => {
180
+ const health = manager?.getConnectionHealth();
181
+ return {
182
+ healthy: health?.isHealthy ?? false,
183
+ details: {
184
+ connected: health?.isConnected,
185
+ channels: config.channels,
186
+ activeClients: manager?.getStats().currentConnections ?? 0,
187
+ lastEventAt: health?.lastEventAt?.toISOString(),
188
+ isReconnecting: health?.isReconnecting,
189
+ },
190
+ };
191
+ },
192
+ });
193
+
194
+ // Register SSE stream endpoint
195
+ if (streamEnabled) {
196
+ registry.addRoute({
197
+ method: 'get',
198
+ path: `${apiPrefix}/stream`,
199
+ pluginId: 'notifications',
200
+ handler: (req: Request, res: Response) => {
201
+ const deviceId = req.query.device_id as string | undefined;
202
+ const userId = req.query.user_id as string | undefined;
203
+
204
+ // Require at least one filter
205
+ if (!deviceId && !userId) {
206
+ res.status(400).json({
207
+ error: 'Bad Request',
208
+ message: 'At least one of device_id or user_id query parameter is required',
209
+ });
210
+ return;
211
+ }
212
+
213
+ // Validate device_id
214
+ const deviceValidation = validateId(deviceId, 'device_id');
215
+ if (!deviceValidation.valid) {
216
+ res.status(400).json({
217
+ error: 'Bad Request',
218
+ message: deviceValidation.error,
219
+ });
220
+ return;
221
+ }
222
+
223
+ // Validate user_id
224
+ const userValidation = validateId(userId, 'user_id');
225
+ if (!userValidation.valid) {
226
+ res.status(400).json({
227
+ error: 'Bad Request',
228
+ message: userValidation.error,
229
+ });
230
+ return;
231
+ }
232
+
233
+ // Set SSE headers
234
+ res.setHeader('Content-Type', 'text/event-stream');
235
+ res.setHeader('Cache-Control', 'no-cache');
236
+ res.setHeader('Connection', 'keep-alive');
237
+ res.setHeader('X-Accel-Buffering', 'no'); // Disable nginx buffering
238
+
239
+ // Disable compression for SSE
240
+ res.setHeader('Content-Encoding', 'identity');
241
+
242
+ // Flush headers
243
+ res.flushHeaders();
244
+
245
+ // Generate client ID and register
246
+ const clientId = randomUUID();
247
+ const registered = manager?.registerClient(clientId, deviceId, userId, res);
248
+
249
+ // Handle capacity limit
250
+ if (!registered) {
251
+ res.write('event: error\n');
252
+ res.write('data: {"error": "Server at capacity, please try again later"}\n\n');
253
+ res.end();
254
+ }
255
+ },
256
+ });
257
+
258
+ logger.debug(`SSE endpoint registered: GET ${apiPrefix}/stream`);
259
+ }
260
+
261
+ // Register stats endpoint
262
+ if (statsEnabled) {
263
+ registry.addRoute({
264
+ method: 'get',
265
+ path: `${apiPrefix}/stats`,
266
+ pluginId: 'notifications',
267
+ handler: (_req: Request, res: Response) => {
268
+ if (!manager) {
269
+ res.status(503).json({ error: 'Service unavailable' });
270
+ return;
271
+ }
272
+
273
+ const stats = manager.getStats();
274
+ res.json({
275
+ ...stats,
276
+ channels: config.channels,
277
+ lastEventAt: stats.connectionHealth.lastEventAt?.toISOString(),
278
+ lastReconnectionAt: stats.lastReconnectionAt?.toISOString(),
279
+ });
280
+ },
281
+ });
282
+
283
+ logger.debug(`Stats endpoint registered: GET ${apiPrefix}/stats`);
284
+
285
+ // Register clients endpoint
286
+ registry.addRoute({
287
+ method: 'get',
288
+ path: `${apiPrefix}/clients`,
289
+ pluginId: 'notifications',
290
+ handler: (_req: Request, res: Response) => {
291
+ if (!manager) {
292
+ res.status(503).json({ error: 'Service unavailable' });
293
+ return;
294
+ }
295
+
296
+ const clients = manager.getClients();
297
+ res.json({
298
+ clients,
299
+ total: clients.length,
300
+ });
301
+ },
302
+ });
303
+
304
+ logger.debug(`Clients endpoint registered: GET ${apiPrefix}/clients`);
305
+
306
+ // Register disconnect client endpoint
307
+ registry.addRoute({
308
+ method: 'delete',
309
+ path: `${apiPrefix}/clients/:id`,
310
+ pluginId: 'notifications',
311
+ handler: (req: Request, res: Response) => {
312
+ if (!manager) {
313
+ res.status(503).json({ error: 'Service unavailable' });
314
+ return;
315
+ }
316
+
317
+ const clientId = req.params.id;
318
+ if (!clientId) {
319
+ res.status(400).json({ error: 'Bad Request', message: 'Client ID is required' });
320
+ return;
321
+ }
322
+
323
+ // Get admin user info for audit logging
324
+ const adminUser = getAuthenticatedUser(req);
325
+ const disconnectedBy = {
326
+ userId: adminUser?.id,
327
+ email: adminUser?.email,
328
+ ip: req.ip || req.socket.remoteAddress,
329
+ };
330
+
331
+ const disconnected = manager.disconnectClient(clientId, disconnectedBy);
332
+ if (!disconnected) {
333
+ res.status(404).json({ error: 'Not Found', message: 'Client not found' });
334
+ return;
335
+ }
336
+
337
+ res.json({ success: true });
338
+ },
339
+ });
340
+
341
+ logger.debug(`Disconnect endpoint registered: DELETE ${apiPrefix}/clients/:id`);
342
+
343
+ // Register force reconnect endpoint
344
+ registry.addRoute({
345
+ method: 'post',
346
+ path: `${apiPrefix}/reconnect`,
347
+ pluginId: 'notifications',
348
+ handler: async (_req: Request, res: Response) => {
349
+ if (!manager) {
350
+ res.status(503).json({ error: 'Service unavailable' });
351
+ return;
352
+ }
353
+
354
+ try {
355
+ await manager.forceReconnect();
356
+ res.json({ success: true, message: 'Reconnection initiated' });
357
+ } catch (error) {
358
+ const errorMsg = error instanceof Error ? error.message : 'Unknown error';
359
+ res.status(500).json({ error: 'Reconnection failed', message: errorMsg });
360
+ }
361
+ },
362
+ });
363
+
364
+ logger.debug(`Reconnect endpoint registered: POST ${apiPrefix}/reconnect`);
365
+
366
+ // Register UI menu item for management page
367
+ registry.addMenuItem({
368
+ pluginId: 'notifications',
369
+ id: 'notifications:sidebar',
370
+ label: 'Notifications',
371
+ icon: 'notifications',
372
+ route: '/notifications',
373
+ order: 45, // After Rate Limits (40)
374
+ });
375
+
376
+ // Register dashboard widget
377
+ registry.addWidget({
378
+ id: 'notifications-stats',
379
+ title: 'Notifications',
380
+ component: 'NotificationsStatsWidget',
381
+ priority: 25, // After ServiceHealthWidget (10) and AuthStatusWidget (20)
382
+ showByDefault: true,
383
+ pluginId: 'notifications',
384
+ });
385
+ }
386
+
387
+ logger.info('Notifications plugin started');
388
+ },
389
+
390
+ async onStop(): Promise<void> {
391
+ if (manager) {
392
+ await manager.shutdown();
393
+ setNotificationsManager(null);
394
+ manager = null;
395
+ }
396
+ },
397
+ };
398
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Notifications Plugin Types
3
+ *
4
+ * Type definitions for the realtime notifications plugin.
5
+ *
6
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
7
+ */
8
+
9
+ import type { Response } from 'express';
10
+
11
+ // =============================================================================
12
+ // Configuration Types
13
+ // =============================================================================
14
+
15
+ /**
16
+ * Configuration for the notifications plugin
17
+ */
18
+ export interface NotificationsPluginConfig {
19
+ /**
20
+ * PostgreSQL channels to LISTEN on.
21
+ * Each channel maps to a NOTIFY channel in PostgreSQL.
22
+ * Example: ['bot_events', 'chat_messages']
23
+ */
24
+ channels: string[];
25
+
26
+ /**
27
+ * Heartbeat configuration
28
+ */
29
+ heartbeat?: {
30
+ /** Interval in milliseconds (default: 60000 = 1 minute) */
31
+ interval?: number;
32
+ /** Include server status in heartbeat (default: true) */
33
+ includeStatus?: boolean;
34
+ };
35
+
36
+ /**
37
+ * Reconnection configuration for PostgreSQL LISTEN connection
38
+ */
39
+ reconnect?: {
40
+ /** Maximum reconnection attempts before giving up (default: 10) */
41
+ maxAttempts?: number;
42
+ /** Base delay in milliseconds for exponential backoff (default: 1000) */
43
+ baseDelay?: number;
44
+ /** Maximum delay in milliseconds (default: 60000) */
45
+ maxDelay?: number;
46
+ };
47
+
48
+ /**
49
+ * API endpoint configuration
50
+ */
51
+ api?: {
52
+ /** Route prefix (default: '/notifications') */
53
+ prefix?: string;
54
+ /** Enable /stream SSE endpoint (default: true) */
55
+ stream?: boolean;
56
+ /** Enable /stats endpoint (default: true) */
57
+ stats?: boolean;
58
+ };
59
+
60
+ /**
61
+ * Enable debug logging (default: false)
62
+ */
63
+ debug?: boolean;
64
+ }
65
+
66
+ // =============================================================================
67
+ // SSE Client Types
68
+ // =============================================================================
69
+
70
+ /**
71
+ * Represents a connected SSE client
72
+ */
73
+ export interface SSEClient {
74
+ /** Unique client identifier */
75
+ id: string;
76
+ /** Device ID for filtering (optional) */
77
+ deviceId?: string;
78
+ /** User ID for filtering (optional) */
79
+ userId?: string;
80
+ /** Express response object for SSE */
81
+ response: Response;
82
+ /** Connection timestamp */
83
+ connectedAt: Date;
84
+ }
85
+
86
+ // =============================================================================
87
+ // Event Types
88
+ // =============================================================================
89
+
90
+ /**
91
+ * PostgreSQL NOTIFY payload structure
92
+ */
93
+ export interface NotifyPayload {
94
+ /** Event type (e.g., 'command', 'status') */
95
+ eventType?: string;
96
+ /** Target device ID (for routing) */
97
+ deviceId?: string;
98
+ /** Target user ID (for routing) */
99
+ userId?: string;
100
+ /** Event payload data */
101
+ payload?: unknown;
102
+ /** Additional fields */
103
+ [key: string]: unknown;
104
+ }
105
+
106
+ /**
107
+ * SSE event to send to clients
108
+ */
109
+ export interface SSEEvent {
110
+ /** Event type name */
111
+ eventType: string;
112
+ /** Event data */
113
+ payload: unknown;
114
+ }
115
+
116
+ // =============================================================================
117
+ // Statistics Types
118
+ // =============================================================================
119
+
120
+ /**
121
+ * Connection health information
122
+ */
123
+ export interface ConnectionHealth {
124
+ /** Whether LISTEN connection is established */
125
+ isConnected: boolean;
126
+ /** Whether connection is considered healthy */
127
+ isHealthy: boolean;
128
+ /** Last time an event was received */
129
+ lastEventAt: Date | null;
130
+ /** Time since last event in milliseconds */
131
+ timeSinceLastEvent: number;
132
+ /** Number of channels being listened to */
133
+ channelCount: number;
134
+ /** Whether reconnection is in progress */
135
+ isReconnecting: boolean;
136
+ /** Current reconnection attempt number */
137
+ reconnectAttempts: number;
138
+ }
139
+
140
+ /**
141
+ * Notifications manager statistics
142
+ */
143
+ export interface NotificationsStats {
144
+ /** Total connections since startup */
145
+ totalConnections: number;
146
+ /** Currently active connections */
147
+ currentConnections: number;
148
+ /** Total events received from PostgreSQL */
149
+ eventsProcessed: number;
150
+ /** Total events routed to clients */
151
+ eventsRouted: number;
152
+ /** Events that failed JSON parsing */
153
+ eventsParseFailed: number;
154
+ /** Events dropped because no clients matched */
155
+ eventsDroppedNoClients: number;
156
+ /** Total reconnection attempts */
157
+ reconnectionAttempts: number;
158
+ /** Last reconnection timestamp */
159
+ lastReconnectionAt?: Date;
160
+ /** Client breakdown by type */
161
+ clientsByType: {
162
+ /** Clients with device_id filter */
163
+ device: number;
164
+ /** Clients with user_id filter */
165
+ user: number;
166
+ };
167
+ /** Connection health status */
168
+ connectionHealth: ConnectionHealth;
169
+ }
170
+
171
+ // =============================================================================
172
+ // Manager Interface
173
+ // =============================================================================
174
+
175
+ /**
176
+ * NotificationsManager interface for external access
177
+ */
178
+ export interface NotificationsManagerInterface {
179
+ /** Register a new SSE client */
180
+ registerClient(
181
+ id: string,
182
+ deviceId: string | undefined,
183
+ userId: string | undefined,
184
+ response: Response
185
+ ): void;
186
+
187
+ /** Broadcast event to a specific device */
188
+ broadcastToDevice(deviceId: string, eventType: string, payload: unknown): number;
189
+
190
+ /** Broadcast event to all devices for a user */
191
+ broadcastToUser(userId: string, eventType: string, payload: unknown): number;
192
+
193
+ /** Broadcast to all clients (channel-level broadcast) */
194
+ broadcastToAll(eventType: string, payload: unknown): number;
195
+
196
+ /** Get current statistics */
197
+ getStats(): NotificationsStats;
198
+
199
+ /** Get connection health */
200
+ getConnectionHealth(): ConnectionHealth;
201
+
202
+ /** Force reconnection to PostgreSQL */
203
+ forceReconnect(): Promise<void>;
204
+
205
+ /** Shutdown the manager */
206
+ shutdown(): Promise<void>;
207
+ }