@qwickapps/server 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (240) hide show
  1. package/README.md +238 -0
  2. package/dist/core/control-panel.d.ts +7 -2
  3. package/dist/core/control-panel.d.ts.map +1 -1
  4. package/dist/core/control-panel.js +92 -54
  5. package/dist/core/control-panel.js.map +1 -1
  6. package/dist/core/gateway.d.ts +159 -79
  7. package/dist/core/gateway.d.ts.map +1 -1
  8. package/dist/core/gateway.js +679 -319
  9. package/dist/core/gateway.js.map +1 -1
  10. package/dist/core/index.d.ts +3 -1
  11. package/dist/core/index.d.ts.map +1 -1
  12. package/dist/core/index.js +2 -0
  13. package/dist/core/index.js.map +1 -1
  14. package/dist/core/plugin-registry.d.ts +271 -0
  15. package/dist/core/plugin-registry.d.ts.map +1 -0
  16. package/dist/core/plugin-registry.js +326 -0
  17. package/dist/core/plugin-registry.js.map +1 -0
  18. package/dist/core/types.d.ts +16 -33
  19. package/dist/core/types.d.ts.map +1 -1
  20. package/dist/index.d.ts +8 -5
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +15 -7
  23. package/dist/index.js.map +1 -1
  24. package/dist/plugins/auth/adapters/auth0-adapter.d.ts +14 -0
  25. package/dist/plugins/auth/adapters/auth0-adapter.d.ts.map +1 -0
  26. package/dist/plugins/auth/adapters/auth0-adapter.js +179 -0
  27. package/dist/plugins/auth/adapters/auth0-adapter.js.map +1 -0
  28. package/dist/plugins/auth/adapters/basic-adapter.d.ts +13 -0
  29. package/dist/plugins/auth/adapters/basic-adapter.d.ts.map +1 -0
  30. package/dist/plugins/auth/adapters/basic-adapter.js +51 -0
  31. package/dist/plugins/auth/adapters/basic-adapter.js.map +1 -0
  32. package/dist/plugins/auth/adapters/index.d.ts +9 -0
  33. package/dist/plugins/auth/adapters/index.d.ts.map +1 -0
  34. package/dist/plugins/auth/adapters/index.js +9 -0
  35. package/dist/plugins/auth/adapters/index.js.map +1 -0
  36. package/dist/plugins/auth/adapters/supabase-adapter.d.ts +13 -0
  37. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -0
  38. package/dist/plugins/auth/adapters/supabase-adapter.js +109 -0
  39. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -0
  40. package/dist/plugins/auth/auth-plugin.d.ts +40 -0
  41. package/dist/plugins/auth/auth-plugin.d.ts.map +1 -0
  42. package/dist/plugins/auth/auth-plugin.js +255 -0
  43. package/dist/plugins/auth/auth-plugin.js.map +1 -0
  44. package/dist/plugins/auth/auth-plugin.test.d.ts +9 -0
  45. package/dist/plugins/auth/auth-plugin.test.d.ts.map +1 -0
  46. package/dist/plugins/auth/auth-plugin.test.js +147 -0
  47. package/dist/plugins/auth/auth-plugin.test.js.map +1 -0
  48. package/dist/plugins/auth/index.d.ts +12 -0
  49. package/dist/plugins/auth/index.d.ts.map +1 -0
  50. package/dist/plugins/auth/index.js +13 -0
  51. package/dist/plugins/auth/index.js.map +1 -0
  52. package/dist/plugins/auth/types.d.ts +148 -0
  53. package/dist/plugins/auth/types.d.ts.map +1 -0
  54. package/dist/plugins/auth/types.js +14 -0
  55. package/dist/plugins/auth/types.js.map +1 -0
  56. package/dist/plugins/bans/bans-plugin.d.ts +59 -0
  57. package/dist/plugins/bans/bans-plugin.d.ts.map +1 -0
  58. package/dist/plugins/bans/bans-plugin.js +428 -0
  59. package/dist/plugins/bans/bans-plugin.js.map +1 -0
  60. package/dist/plugins/bans/index.d.ts +9 -0
  61. package/dist/plugins/bans/index.d.ts.map +1 -0
  62. package/dist/plugins/bans/index.js +10 -0
  63. package/dist/plugins/bans/index.js.map +1 -0
  64. package/dist/plugins/bans/stores/index.d.ts +7 -0
  65. package/dist/plugins/bans/stores/index.d.ts.map +1 -0
  66. package/dist/plugins/bans/stores/index.js +7 -0
  67. package/dist/plugins/bans/stores/index.js.map +1 -0
  68. package/dist/plugins/bans/stores/postgres-store.d.ts +29 -0
  69. package/dist/plugins/bans/stores/postgres-store.d.ts.map +1 -0
  70. package/dist/plugins/bans/stores/postgres-store.js +132 -0
  71. package/dist/plugins/bans/stores/postgres-store.js.map +1 -0
  72. package/dist/plugins/bans/types.d.ts +128 -0
  73. package/dist/plugins/bans/types.d.ts.map +1 -0
  74. package/dist/plugins/bans/types.js +11 -0
  75. package/dist/plugins/bans/types.js.map +1 -0
  76. package/dist/plugins/cache-plugin.d.ts +14 -3
  77. package/dist/plugins/cache-plugin.d.ts.map +1 -1
  78. package/dist/plugins/cache-plugin.js +27 -7
  79. package/dist/plugins/cache-plugin.js.map +1 -1
  80. package/dist/plugins/cache-plugin.test.js +96 -32
  81. package/dist/plugins/cache-plugin.test.js.map +1 -1
  82. package/dist/plugins/config-plugin.d.ts +3 -2
  83. package/dist/plugins/config-plugin.d.ts.map +1 -1
  84. package/dist/plugins/config-plugin.js +17 -10
  85. package/dist/plugins/config-plugin.js.map +1 -1
  86. package/dist/plugins/diagnostics-plugin.d.ts +2 -2
  87. package/dist/plugins/diagnostics-plugin.d.ts.map +1 -1
  88. package/dist/plugins/diagnostics-plugin.js +17 -10
  89. package/dist/plugins/diagnostics-plugin.js.map +1 -1
  90. package/dist/plugins/entitlements/entitlements-plugin.d.ts +95 -0
  91. package/dist/plugins/entitlements/entitlements-plugin.d.ts.map +1 -0
  92. package/dist/plugins/entitlements/entitlements-plugin.js +707 -0
  93. package/dist/plugins/entitlements/entitlements-plugin.js.map +1 -0
  94. package/dist/plugins/entitlements/index.d.ts +12 -0
  95. package/dist/plugins/entitlements/index.d.ts.map +1 -0
  96. package/dist/plugins/entitlements/index.js +16 -0
  97. package/dist/plugins/entitlements/index.js.map +1 -0
  98. package/dist/plugins/entitlements/sources/index.d.ts +9 -0
  99. package/dist/plugins/entitlements/sources/index.d.ts.map +1 -0
  100. package/dist/plugins/entitlements/sources/index.js +9 -0
  101. package/dist/plugins/entitlements/sources/index.js.map +1 -0
  102. package/dist/plugins/entitlements/sources/postgres-source.d.ts +29 -0
  103. package/dist/plugins/entitlements/sources/postgres-source.d.ts.map +1 -0
  104. package/dist/plugins/entitlements/sources/postgres-source.js +169 -0
  105. package/dist/plugins/entitlements/sources/postgres-source.js.map +1 -0
  106. package/dist/plugins/entitlements/types.d.ts +232 -0
  107. package/dist/plugins/entitlements/types.d.ts.map +1 -0
  108. package/dist/plugins/entitlements/types.js +11 -0
  109. package/dist/plugins/entitlements/types.js.map +1 -0
  110. package/dist/plugins/frontend-app-plugin.d.ts +9 -3
  111. package/dist/plugins/frontend-app-plugin.d.ts.map +1 -1
  112. package/dist/plugins/frontend-app-plugin.js +14 -9
  113. package/dist/plugins/frontend-app-plugin.js.map +1 -1
  114. package/dist/plugins/health-plugin.d.ts +5 -2
  115. package/dist/plugins/health-plugin.d.ts.map +1 -1
  116. package/dist/plugins/health-plugin.js +20 -5
  117. package/dist/plugins/health-plugin.js.map +1 -1
  118. package/dist/plugins/index.d.ts +8 -2
  119. package/dist/plugins/index.d.ts.map +1 -1
  120. package/dist/plugins/index.js +8 -2
  121. package/dist/plugins/index.js.map +1 -1
  122. package/dist/plugins/logs-plugin.d.ts +3 -2
  123. package/dist/plugins/logs-plugin.d.ts.map +1 -1
  124. package/dist/plugins/logs-plugin.js +21 -12
  125. package/dist/plugins/logs-plugin.js.map +1 -1
  126. package/dist/plugins/postgres-plugin.d.ts +3 -3
  127. package/dist/plugins/postgres-plugin.d.ts.map +1 -1
  128. package/dist/plugins/postgres-plugin.js +9 -7
  129. package/dist/plugins/postgres-plugin.js.map +1 -1
  130. package/dist/plugins/postgres-plugin.test.js +47 -29
  131. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  132. package/dist/plugins/users/index.d.ts +12 -0
  133. package/dist/plugins/users/index.d.ts.map +1 -0
  134. package/dist/plugins/users/index.js +13 -0
  135. package/dist/plugins/users/index.js.map +1 -0
  136. package/dist/plugins/users/stores/index.d.ts +7 -0
  137. package/dist/plugins/users/stores/index.d.ts.map +1 -0
  138. package/dist/plugins/users/stores/index.js +7 -0
  139. package/dist/plugins/users/stores/index.js.map +1 -0
  140. package/dist/plugins/users/stores/postgres-store.d.ts +28 -0
  141. package/dist/plugins/users/stores/postgres-store.d.ts.map +1 -0
  142. package/dist/plugins/users/stores/postgres-store.js +157 -0
  143. package/dist/plugins/users/stores/postgres-store.js.map +1 -0
  144. package/dist/plugins/users/types.d.ts +189 -0
  145. package/dist/plugins/users/types.d.ts.map +1 -0
  146. package/dist/plugins/users/types.js +12 -0
  147. package/dist/plugins/users/types.js.map +1 -0
  148. package/dist/plugins/users/users-plugin.d.ts +39 -0
  149. package/dist/plugins/users/users-plugin.d.ts.map +1 -0
  150. package/dist/plugins/users/users-plugin.js +242 -0
  151. package/dist/plugins/users/users-plugin.js.map +1 -0
  152. package/dist-ui/assets/index-Bsp2ntcw.js +465 -0
  153. package/dist-ui/assets/index-Bsp2ntcw.js.map +1 -0
  154. package/dist-ui/index.html +1 -1
  155. package/dist-ui-lib/api/controlPanelApi.d.ts +232 -0
  156. package/dist-ui-lib/components/ControlPanelApp.d.ts +61 -0
  157. package/dist-ui-lib/components/index.d.ts +18 -0
  158. package/dist-ui-lib/config/AppConfig.d.ts +7 -0
  159. package/dist-ui-lib/dashboard/DashboardWidgetRegistry.d.ts +62 -0
  160. package/dist-ui-lib/dashboard/DashboardWidgetRenderer.d.ts +8 -0
  161. package/dist-ui-lib/dashboard/PluginWidgetRenderer.d.ts +19 -0
  162. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +44 -0
  163. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +19 -0
  164. package/dist-ui-lib/dashboard/index.d.ts +13 -0
  165. package/dist-ui-lib/dashboard/widgets/ServiceHealthWidget.d.ts +12 -0
  166. package/dist-ui-lib/dashboard/widgets/index.d.ts +6 -0
  167. package/dist-ui-lib/index.js +6441 -0
  168. package/dist-ui-lib/index.js.map +1 -0
  169. package/dist-ui-lib/pages/ConfigPage.d.ts +1 -0
  170. package/dist-ui-lib/pages/DashboardPage.d.ts +1 -0
  171. package/dist-ui-lib/pages/DiagnosticsPage.d.ts +1 -0
  172. package/dist-ui-lib/pages/EntitlementsPage.d.ts +17 -0
  173. package/dist-ui-lib/pages/LogsPage.d.ts +1 -0
  174. package/dist-ui-lib/pages/NotFoundPage.d.ts +1 -0
  175. package/dist-ui-lib/pages/PluginPage.d.ts +15 -0
  176. package/dist-ui-lib/pages/SystemPage.d.ts +1 -0
  177. package/dist-ui-lib/pages/UsersPage.d.ts +22 -0
  178. package/package.json +18 -6
  179. package/src/core/control-panel.ts +114 -61
  180. package/src/core/gateway.ts +863 -403
  181. package/src/core/index.ts +21 -2
  182. package/src/core/plugin-registry.ts +653 -0
  183. package/src/core/types.ts +31 -37
  184. package/src/index.ts +118 -19
  185. package/src/plugins/auth/adapters/auth0-adapter.ts +214 -0
  186. package/src/plugins/auth/adapters/basic-adapter.ts +61 -0
  187. package/src/plugins/auth/adapters/index.ts +9 -0
  188. package/src/plugins/auth/adapters/supabase-adapter.ts +141 -0
  189. package/src/plugins/auth/auth-plugin.test.ts +176 -0
  190. package/src/plugins/auth/auth-plugin.ts +303 -0
  191. package/src/plugins/auth/index.ts +33 -0
  192. package/src/plugins/auth/types.ts +165 -0
  193. package/src/plugins/bans/bans-plugin.ts +485 -0
  194. package/src/plugins/bans/index.ts +31 -0
  195. package/src/plugins/bans/stores/index.ts +7 -0
  196. package/src/plugins/bans/stores/postgres-store.ts +195 -0
  197. package/src/plugins/bans/types.ts +141 -0
  198. package/src/plugins/cache-plugin.test.ts +105 -32
  199. package/src/plugins/cache-plugin.ts +40 -9
  200. package/src/plugins/config-plugin.ts +23 -12
  201. package/src/plugins/diagnostics-plugin.ts +22 -12
  202. package/src/plugins/entitlements/entitlements-plugin.ts +820 -0
  203. package/src/plugins/entitlements/index.ts +51 -0
  204. package/src/plugins/entitlements/sources/index.ts +9 -0
  205. package/src/plugins/entitlements/sources/postgres-source.ts +253 -0
  206. package/src/plugins/entitlements/types.ts +256 -0
  207. package/src/plugins/frontend-app-plugin.ts +24 -12
  208. package/src/plugins/health-plugin.ts +27 -7
  209. package/src/plugins/index.ts +106 -4
  210. package/src/plugins/logs-plugin.ts +28 -14
  211. package/src/plugins/postgres-plugin.test.ts +49 -29
  212. package/src/plugins/postgres-plugin.ts +11 -9
  213. package/src/plugins/users/index.ts +35 -0
  214. package/src/plugins/users/stores/index.ts +7 -0
  215. package/src/plugins/users/stores/postgres-store.ts +225 -0
  216. package/src/plugins/users/types.ts +209 -0
  217. package/src/plugins/users/users-plugin.ts +281 -0
  218. package/ui/src/App.tsx +185 -31
  219. package/ui/src/api/controlPanelApi.ts +354 -1
  220. package/ui/src/components/ControlPanelApp.tsx +209 -0
  221. package/ui/src/components/index.ts +62 -0
  222. package/ui/src/dashboard/DashboardWidgetRegistry.tsx +129 -0
  223. package/ui/src/dashboard/DashboardWidgetRenderer.tsx +34 -0
  224. package/ui/src/dashboard/PluginWidgetRenderer.tsx +115 -0
  225. package/ui/src/dashboard/WidgetComponentRegistry.tsx +116 -0
  226. package/ui/src/dashboard/builtInWidgets.tsx +29 -0
  227. package/ui/src/dashboard/index.ts +35 -0
  228. package/ui/src/dashboard/widgets/ServiceHealthWidget.tsx +140 -0
  229. package/ui/src/dashboard/widgets/index.ts +7 -0
  230. package/ui/src/pages/DashboardPage.tsx +28 -149
  231. package/ui/src/pages/EntitlementsPage.tsx +557 -0
  232. package/ui/src/pages/LogsPage.tsx +174 -8
  233. package/ui/src/pages/PluginPage.tsx +148 -0
  234. package/ui/src/pages/SystemPage.tsx +445 -0
  235. package/ui/src/pages/UsersPage.tsx +837 -0
  236. package/ui/tsconfig.lib.json +11 -0
  237. package/ui/vite.lib.config.ts +51 -0
  238. package/dist-ui/assets/index-CW1BviRn.js +0 -465
  239. package/dist-ui/assets/index-CW1BviRn.js.map +0 -1
  240. package/ui/src/pages/HealthPage.tsx +0 -204
@@ -0,0 +1,707 @@
1
+ /**
2
+ * Entitlements Plugin
3
+ *
4
+ * User entitlement management plugin for @qwickapps/server.
5
+ * Supports pluggable sources (PostgreSQL, Keap, etc.) with Redis caching.
6
+ *
7
+ * Entitlements are string-based tags (e.g., 'pro', 'enterprise', 'feature:analytics').
8
+ * Multiple sources can be combined - entitlements are merged from all sources.
9
+ *
10
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
11
+ */
12
+ import { getCache } from '../cache-plugin.js';
13
+ // Plugin state
14
+ let primarySource = null;
15
+ let additionalSources = [];
16
+ let pluginConfig = null;
17
+ let cacheInstance = null;
18
+ let cacheKeyPrefix = 'entitlements:';
19
+ let cacheTtl = 300;
20
+ let cacheMappingTtl = 600;
21
+ let cacheEnabled = true;
22
+ let cacheVersion = 1;
23
+ /**
24
+ * Create the Entitlements plugin
25
+ */
26
+ export function createEntitlementsPlugin(config) {
27
+ const debug = config.debug || false;
28
+ // Routes are mounted under /api by the control panel, so don't include /api in prefix
29
+ const apiPrefix = config.api?.prefix || '/entitlements';
30
+ const apiEnabled = config.api?.enabled !== false;
31
+ const enableWriteApi = config.api?.enableWrite !== false;
32
+ function log(message, data) {
33
+ if (debug) {
34
+ console.log(`[EntitlementsPlugin] ${message}`, data || '');
35
+ }
36
+ }
37
+ // Cache key helpers
38
+ const keys = {
39
+ entitlements: (email) => `${cacheKeyPrefix}user:${email.toLowerCase()}`,
40
+ mapping: (source, id) => `${cacheKeyPrefix}mapping:${source}:${id}`,
41
+ };
42
+ return {
43
+ id: 'entitlements',
44
+ name: 'Entitlements',
45
+ version: '1.0.0',
46
+ async onStart(_pluginConfig, registry) {
47
+ log('Starting entitlements plugin');
48
+ // Initialize primary source
49
+ await config.source.initialize();
50
+ primarySource = config.source;
51
+ log('Primary source initialized', { source: config.source.name });
52
+ // Initialize additional sources
53
+ additionalSources = config.additionalSources || [];
54
+ for (const source of additionalSources) {
55
+ await source.initialize();
56
+ log('Additional source initialized', { source: source.name });
57
+ }
58
+ // Store config
59
+ pluginConfig = config;
60
+ // Setup caching if enabled
61
+ cacheEnabled = config.cache?.enabled !== false;
62
+ if (cacheEnabled) {
63
+ try {
64
+ const instanceName = config.cache?.instanceName || 'default';
65
+ cacheInstance = getCache(instanceName);
66
+ cacheKeyPrefix = config.cache?.keyPrefix || 'entitlements:';
67
+ cacheTtl = config.cache?.ttl || 300;
68
+ cacheMappingTtl = config.cache?.mappingTtl || cacheTtl * 2;
69
+ log('Cache configured', { instanceName, prefix: cacheKeyPrefix, ttl: cacheTtl });
70
+ }
71
+ catch {
72
+ log('Cache not available, running without caching');
73
+ cacheEnabled = false;
74
+ cacheInstance = null;
75
+ }
76
+ }
77
+ // Register health check
78
+ registry.registerHealthCheck({
79
+ name: 'entitlements-source',
80
+ type: 'custom',
81
+ check: async () => {
82
+ try {
83
+ // Use source's isHealthy() method if available (avoids API calls)
84
+ // Otherwise just check that source is initialized
85
+ if (config.source.isHealthy) {
86
+ const healthy = await config.source.isHealthy();
87
+ return { healthy };
88
+ }
89
+ // Source is healthy if initialized (we got here means it started)
90
+ return { healthy: primarySource !== null };
91
+ }
92
+ catch {
93
+ return { healthy: false };
94
+ }
95
+ },
96
+ });
97
+ // Add API routes if enabled
98
+ // IMPORTANT: Static paths must be registered BEFORE parameterized paths
99
+ // to prevent /:email from matching "available" and "status"
100
+ if (apiEnabled) {
101
+ // List available entitlements (static path - must be before /:email)
102
+ registry.addRoute({
103
+ method: 'get',
104
+ path: `${apiPrefix}/available`,
105
+ pluginId: 'entitlements',
106
+ handler: async (_req, res) => {
107
+ try {
108
+ const available = await getAvailableEntitlements();
109
+ res.json({ entitlements: available, total: available.length });
110
+ }
111
+ catch (error) {
112
+ console.error('[EntitlementsPlugin] List available error:', error);
113
+ res.status(500).json({ error: 'Failed to list available entitlements' });
114
+ }
115
+ },
116
+ });
117
+ // Get entitlements plugin status (static path - must be before /:email)
118
+ registry.addRoute({
119
+ method: 'get',
120
+ path: `${apiPrefix}/status`,
121
+ pluginId: 'entitlements',
122
+ handler: async (_req, res) => {
123
+ try {
124
+ const sources = [
125
+ {
126
+ name: config.source.name,
127
+ description: config.source.description,
128
+ readonly: config.source.readonly ?? false,
129
+ primary: true,
130
+ },
131
+ ...additionalSources.map((s) => ({
132
+ name: s.name,
133
+ description: s.description,
134
+ readonly: s.readonly ?? false,
135
+ primary: false,
136
+ })),
137
+ ];
138
+ res.json({
139
+ readonly: config.source.readonly ?? false,
140
+ writeEnabled: enableWriteApi && !config.source.readonly,
141
+ cacheEnabled,
142
+ cacheTtl,
143
+ sources,
144
+ });
145
+ }
146
+ catch (error) {
147
+ console.error('[EntitlementsPlugin] Status error:', error);
148
+ res.status(500).json({ error: 'Failed to get status' });
149
+ }
150
+ },
151
+ });
152
+ // Get entitlements statistics (static path - must be before /:email)
153
+ registry.addRoute({
154
+ method: 'get',
155
+ path: `${apiPrefix}/stats`,
156
+ pluginId: 'entitlements',
157
+ handler: async (_req, res) => {
158
+ try {
159
+ const stats = await getEntitlementStats();
160
+ res.json(stats);
161
+ }
162
+ catch (error) {
163
+ console.error('[EntitlementsPlugin] Stats error:', error);
164
+ res.status(500).json({ error: 'Failed to get entitlement stats' });
165
+ }
166
+ },
167
+ });
168
+ // Invalidate cache for email (static prefix - must be before /:email)
169
+ registry.addRoute({
170
+ method: 'delete',
171
+ path: `${apiPrefix}/cache/:email`,
172
+ pluginId: 'entitlements',
173
+ handler: async (req, res) => {
174
+ try {
175
+ const email = decodeURIComponent(req.params.email);
176
+ await invalidateEntitlementCache(email);
177
+ log('Cache invalidated', { email });
178
+ res.status(204).send();
179
+ }
180
+ catch (error) {
181
+ console.error('[EntitlementsPlugin] Invalidate cache error:', error);
182
+ res.status(500).json({ error: 'Failed to invalidate cache' });
183
+ }
184
+ },
185
+ });
186
+ // Get entitlements for email (parameterized - after static paths)
187
+ // Note: We guard against reserved paths that might accidentally match :email
188
+ const reservedPaths = ['stats', 'available', 'status', 'cache'];
189
+ registry.addRoute({
190
+ method: 'get',
191
+ path: `${apiPrefix}/:email`,
192
+ pluginId: 'entitlements',
193
+ handler: async (req, res) => {
194
+ try {
195
+ const email = decodeURIComponent(req.params.email);
196
+ // Skip reserved paths - they have their own handlers
197
+ if (reservedPaths.includes(email.toLowerCase())) {
198
+ return res.status(404).json({ error: 'Not found' });
199
+ }
200
+ const refresh = req.query.refresh === 'true';
201
+ const result = refresh
202
+ ? await refreshEntitlements(email)
203
+ : await getEntitlements(email);
204
+ res.json(result);
205
+ }
206
+ catch (error) {
207
+ console.error('[EntitlementsPlugin] Get entitlements error:', error);
208
+ res.status(500).json({ error: 'Failed to get entitlements' });
209
+ }
210
+ },
211
+ });
212
+ // Check specific entitlement
213
+ registry.addRoute({
214
+ method: 'get',
215
+ path: `${apiPrefix}/:email/check/:entitlement`,
216
+ pluginId: 'entitlements',
217
+ handler: async (req, res) => {
218
+ try {
219
+ const email = decodeURIComponent(req.params.email);
220
+ const entitlement = decodeURIComponent(req.params.entitlement);
221
+ const has = await hasEntitlement(email, entitlement);
222
+ res.json({ email, entitlement, hasEntitlement: has });
223
+ }
224
+ catch (error) {
225
+ console.error('[EntitlementsPlugin] Check entitlement error:', error);
226
+ res.status(500).json({ error: 'Failed to check entitlement' });
227
+ }
228
+ },
229
+ });
230
+ // Check multiple entitlements
231
+ registry.addRoute({
232
+ method: 'post',
233
+ path: `${apiPrefix}/:email/check`,
234
+ pluginId: 'entitlements',
235
+ handler: async (req, res) => {
236
+ try {
237
+ const email = decodeURIComponent(req.params.email);
238
+ const { entitlements: toCheck, mode = 'any' } = req.body;
239
+ if (!Array.isArray(toCheck) || toCheck.length === 0) {
240
+ return res.status(400).json({ error: 'entitlements array required' });
241
+ }
242
+ const result = await getEntitlements(email);
243
+ const has = toCheck.filter((e) => result.entitlements.includes(e));
244
+ const missing = toCheck.filter((e) => !result.entitlements.includes(e));
245
+ const passed = mode === 'all'
246
+ ? missing.length === 0
247
+ : has.length > 0;
248
+ res.json({
249
+ email,
250
+ mode,
251
+ passed,
252
+ has,
253
+ missing,
254
+ total: result.entitlements.length,
255
+ });
256
+ }
257
+ catch (error) {
258
+ console.error('[EntitlementsPlugin] Check entitlements error:', error);
259
+ res.status(500).json({ error: 'Failed to check entitlements' });
260
+ }
261
+ },
262
+ });
263
+ // Refresh entitlements (bypass cache)
264
+ registry.addRoute({
265
+ method: 'post',
266
+ path: `${apiPrefix}/:email/refresh`,
267
+ pluginId: 'entitlements',
268
+ handler: async (req, res) => {
269
+ try {
270
+ const email = decodeURIComponent(req.params.email);
271
+ const result = await refreshEntitlements(email);
272
+ log('Entitlements refreshed', { email, count: result.entitlements.length });
273
+ res.json(result);
274
+ }
275
+ catch (error) {
276
+ console.error('[EntitlementsPlugin] Refresh entitlements error:', error);
277
+ res.status(500).json({ error: 'Failed to refresh entitlements' });
278
+ }
279
+ },
280
+ });
281
+ // Write endpoints (grant/revoke) - only if enabled and source is writable
282
+ if (enableWriteApi && !config.source.readonly) {
283
+ // Grant entitlement
284
+ registry.addRoute({
285
+ method: 'post',
286
+ path: `${apiPrefix}/:email`,
287
+ pluginId: 'entitlements',
288
+ handler: async (req, res) => {
289
+ try {
290
+ const email = decodeURIComponent(req.params.email);
291
+ const { entitlement } = req.body;
292
+ if (!entitlement) {
293
+ return res.status(400).json({ error: 'entitlement required' });
294
+ }
295
+ const authReq = req;
296
+ const grantedBy = authReq.auth?.user?.email || 'system';
297
+ await grantEntitlement(email, entitlement, grantedBy);
298
+ log('Entitlement granted', { email, entitlement, grantedBy });
299
+ res.status(201).json({ email, entitlement, granted: true });
300
+ }
301
+ catch (error) {
302
+ console.error('[EntitlementsPlugin] Grant entitlement error:', error);
303
+ res.status(500).json({ error: 'Failed to grant entitlement' });
304
+ }
305
+ },
306
+ });
307
+ // Revoke entitlement
308
+ registry.addRoute({
309
+ method: 'delete',
310
+ path: `${apiPrefix}/:email/:entitlement`,
311
+ pluginId: 'entitlements',
312
+ handler: async (req, res) => {
313
+ try {
314
+ const email = decodeURIComponent(req.params.email);
315
+ const entitlement = decodeURIComponent(req.params.entitlement);
316
+ await revokeEntitlement(email, entitlement);
317
+ log('Entitlement revoked', { email, entitlement });
318
+ res.status(204).send();
319
+ }
320
+ catch (error) {
321
+ console.error('[EntitlementsPlugin] Revoke entitlement error:', error);
322
+ res.status(500).json({ error: 'Failed to revoke entitlement' });
323
+ }
324
+ },
325
+ });
326
+ }
327
+ }
328
+ // Register UI menu item
329
+ registry.addMenuItem({
330
+ pluginId: 'entitlements',
331
+ id: 'entitlements:sidebar',
332
+ label: 'Entitlements',
333
+ icon: 'local_offer',
334
+ route: '/entitlements',
335
+ order: 35, // After Users (30)
336
+ });
337
+ log('Entitlements plugin started');
338
+ },
339
+ async onStop() {
340
+ log('Stopping entitlements plugin');
341
+ // Shutdown sources
342
+ if (primarySource) {
343
+ await primarySource.shutdown();
344
+ }
345
+ for (const source of additionalSources) {
346
+ await source.shutdown();
347
+ }
348
+ primarySource = null;
349
+ additionalSources = [];
350
+ pluginConfig = null;
351
+ cacheInstance = null;
352
+ log('Entitlements plugin stopped');
353
+ },
354
+ };
355
+ }
356
+ // ========================================
357
+ // Helper Functions
358
+ // ========================================
359
+ /**
360
+ * Get the primary entitlement source
361
+ */
362
+ export function getEntitlementSource() {
363
+ return primarySource;
364
+ }
365
+ /**
366
+ * Check if the primary source is readonly
367
+ */
368
+ export function isSourceReadonly() {
369
+ return primarySource?.readonly ?? true;
370
+ }
371
+ /**
372
+ * Get entitlements for an email (cache-first)
373
+ */
374
+ export async function getEntitlements(email) {
375
+ if (!primarySource) {
376
+ throw new Error('Entitlements plugin not initialized');
377
+ }
378
+ const normalizedEmail = email.toLowerCase();
379
+ const cacheKey = `entitlements:user:${normalizedEmail}`;
380
+ // Try cache first
381
+ if (cacheEnabled && cacheInstance) {
382
+ try {
383
+ const cached = await cacheInstance.get(cacheKey);
384
+ if (cached && new Date(cached.expiresAt) > new Date()) {
385
+ // Call onFetch callback
386
+ if (pluginConfig?.callbacks?.onFetch) {
387
+ await pluginConfig.callbacks.onFetch(normalizedEmail, cached.entitlements, 'cache');
388
+ }
389
+ return {
390
+ identifier: normalizedEmail,
391
+ entitlements: cached.entitlements,
392
+ source: 'cache',
393
+ cachedAt: cached.cachedAt,
394
+ expiresAt: cached.expiresAt,
395
+ bySource: cached.bySource,
396
+ };
397
+ }
398
+ }
399
+ catch (error) {
400
+ console.error('[EntitlementsPlugin] Cache get error:', error);
401
+ }
402
+ }
403
+ // Fetch from sources
404
+ return fetchFromSources(normalizedEmail);
405
+ }
406
+ /**
407
+ * Fetch entitlements from all sources and cache the result
408
+ */
409
+ async function fetchFromSources(email) {
410
+ if (!primarySource) {
411
+ throw new Error('Entitlements plugin not initialized');
412
+ }
413
+ const bySource = {};
414
+ // Fetch from primary source
415
+ const primaryEntitlements = await primarySource.getEntitlements(email);
416
+ bySource[primarySource.name] = primaryEntitlements;
417
+ // Fetch from additional sources in parallel
418
+ const additionalResults = await Promise.allSettled(additionalSources.map(async (source) => {
419
+ const ents = await source.getEntitlements(email);
420
+ return { name: source.name, entitlements: ents };
421
+ }));
422
+ for (const result of additionalResults) {
423
+ if (result.status === 'fulfilled') {
424
+ bySource[result.value.name] = result.value.entitlements;
425
+ }
426
+ else {
427
+ console.error('[EntitlementsPlugin] Source fetch failed:', result.reason);
428
+ }
429
+ }
430
+ // Merge entitlements (union of all sources, deduplicated)
431
+ const allEntitlements = [...new Set(Object.values(bySource).flat())].sort();
432
+ const now = new Date();
433
+ const expiresAt = new Date(now.getTime() + cacheTtl * 1000);
434
+ const result = {
435
+ identifier: email,
436
+ entitlements: allEntitlements,
437
+ source: primarySource.name,
438
+ cachedAt: now.toISOString(),
439
+ expiresAt: expiresAt.toISOString(),
440
+ bySource,
441
+ };
442
+ // Cache the result
443
+ if (cacheEnabled && cacheInstance) {
444
+ try {
445
+ const cached = {
446
+ email,
447
+ entitlements: allEntitlements,
448
+ bySource,
449
+ cachedAt: result.cachedAt,
450
+ expiresAt: result.expiresAt,
451
+ version: cacheVersion,
452
+ };
453
+ await cacheInstance.set(`entitlements:user:${email}`, cached, cacheTtl);
454
+ }
455
+ catch (error) {
456
+ console.error('[EntitlementsPlugin] Cache set error:', error);
457
+ }
458
+ }
459
+ // Call onFetch callback
460
+ if (pluginConfig?.callbacks?.onFetch) {
461
+ await pluginConfig.callbacks.onFetch(email, allEntitlements, primarySource.name);
462
+ }
463
+ return result;
464
+ }
465
+ /**
466
+ * Refresh entitlements (bypass cache)
467
+ */
468
+ export async function refreshEntitlements(email) {
469
+ await invalidateEntitlementCache(email);
470
+ return fetchFromSources(email.toLowerCase());
471
+ }
472
+ /**
473
+ * Check if user has a specific entitlement
474
+ */
475
+ export async function hasEntitlement(email, entitlement) {
476
+ const result = await getEntitlements(email);
477
+ return result.entitlements.includes(entitlement);
478
+ }
479
+ /**
480
+ * Check if user has any of the specified entitlements
481
+ */
482
+ export async function hasAnyEntitlement(email, entitlements) {
483
+ const result = await getEntitlements(email);
484
+ return entitlements.some((e) => result.entitlements.includes(e));
485
+ }
486
+ /**
487
+ * Check if user has all of the specified entitlements
488
+ */
489
+ export async function hasAllEntitlements(email, entitlements) {
490
+ const result = await getEntitlements(email);
491
+ return entitlements.every((e) => result.entitlements.includes(e));
492
+ }
493
+ /**
494
+ * Grant an entitlement to a user
495
+ * @throws Error if source is read-only
496
+ */
497
+ export async function grantEntitlement(email, entitlement, grantedBy) {
498
+ if (!primarySource) {
499
+ throw new Error('Entitlements plugin not initialized');
500
+ }
501
+ if (primarySource.readonly || !primarySource.addEntitlement) {
502
+ throw new Error('Primary entitlement source is read-only');
503
+ }
504
+ await primarySource.addEntitlement(email, entitlement, grantedBy);
505
+ // Invalidate cache
506
+ await invalidateEntitlementCache(email);
507
+ // Call onGrant callback
508
+ if (pluginConfig?.callbacks?.onGrant) {
509
+ await pluginConfig.callbacks.onGrant(email, entitlement, grantedBy);
510
+ }
511
+ }
512
+ /**
513
+ * Revoke an entitlement from a user
514
+ * @throws Error if source is read-only
515
+ */
516
+ export async function revokeEntitlement(email, entitlement) {
517
+ if (!primarySource) {
518
+ throw new Error('Entitlements plugin not initialized');
519
+ }
520
+ if (primarySource.readonly || !primarySource.removeEntitlement) {
521
+ throw new Error('Primary entitlement source is read-only');
522
+ }
523
+ await primarySource.removeEntitlement(email, entitlement);
524
+ // Invalidate cache
525
+ await invalidateEntitlementCache(email);
526
+ // Call onRevoke callback
527
+ if (pluginConfig?.callbacks?.onRevoke) {
528
+ await pluginConfig.callbacks.onRevoke(email, entitlement);
529
+ }
530
+ }
531
+ /**
532
+ * Set all entitlements for a user (replaces existing)
533
+ * Used by sync services to bulk-update user entitlements from external sources
534
+ * @throws Error if source is read-only or doesn't support setEntitlements
535
+ */
536
+ export async function setEntitlements(email, entitlements) {
537
+ if (!primarySource) {
538
+ throw new Error('Entitlements plugin not initialized');
539
+ }
540
+ if (primarySource.readonly) {
541
+ throw new Error('Primary entitlement source is read-only');
542
+ }
543
+ if (!primarySource.setEntitlements) {
544
+ throw new Error('Primary entitlement source does not support setEntitlements');
545
+ }
546
+ await primarySource.setEntitlements(email, entitlements);
547
+ // Invalidate cache
548
+ await invalidateEntitlementCache(email);
549
+ }
550
+ /**
551
+ * Get all available entitlement definitions
552
+ */
553
+ export async function getAvailableEntitlements() {
554
+ if (!primarySource) {
555
+ throw new Error('Entitlements plugin not initialized');
556
+ }
557
+ const allDefinitions = [];
558
+ // Get from primary source
559
+ if (primarySource.getAllAvailable) {
560
+ const defs = await primarySource.getAllAvailable();
561
+ allDefinitions.push(...defs);
562
+ }
563
+ // Get from additional sources
564
+ for (const source of additionalSources) {
565
+ if (source.getAllAvailable) {
566
+ try {
567
+ const defs = await source.getAllAvailable();
568
+ // Add source prefix to avoid collisions
569
+ allDefinitions.push(...defs.map((d) => ({
570
+ ...d,
571
+ id: `${source.name}:${d.id}`,
572
+ category: d.category || source.name,
573
+ })));
574
+ }
575
+ catch (error) {
576
+ console.error(`[EntitlementsPlugin] Failed to get available from ${source.name}:`, error);
577
+ }
578
+ }
579
+ }
580
+ return allDefinitions;
581
+ }
582
+ /**
583
+ * Get entitlement statistics from the primary source
584
+ */
585
+ export async function getEntitlementStats() {
586
+ if (!primarySource) {
587
+ throw new Error('Entitlements plugin not initialized');
588
+ }
589
+ // If source has getStats method, use it
590
+ if (primarySource.getStats) {
591
+ return primarySource.getStats();
592
+ }
593
+ // Fallback: return zeros if source doesn't support stats
594
+ return {
595
+ usersWithEntitlements: 0,
596
+ totalEntitlements: 0,
597
+ };
598
+ }
599
+ /**
600
+ * Invalidate cache for an email
601
+ */
602
+ export async function invalidateEntitlementCache(email) {
603
+ if (!cacheEnabled || !cacheInstance)
604
+ return;
605
+ const normalizedEmail = email.toLowerCase();
606
+ try {
607
+ await cacheInstance.delete(`entitlements:user:${normalizedEmail}`);
608
+ }
609
+ catch (error) {
610
+ console.error('[EntitlementsPlugin] Cache delete error:', error);
611
+ }
612
+ }
613
+ /**
614
+ * Store a mapping from external ID to email (for webhook invalidation)
615
+ */
616
+ export async function storeExternalIdMapping(source, externalId, email) {
617
+ if (!cacheEnabled || !cacheInstance)
618
+ return;
619
+ try {
620
+ await cacheInstance.set(`entitlements:mapping:${source}:${externalId}`, email.toLowerCase(), cacheMappingTtl);
621
+ }
622
+ catch (error) {
623
+ console.error('[EntitlementsPlugin] Store mapping error:', error);
624
+ }
625
+ }
626
+ /**
627
+ * Invalidate cache by external ID (for webhook handling)
628
+ */
629
+ export async function invalidateByExternalId(source, externalId) {
630
+ if (!cacheEnabled || !cacheInstance)
631
+ return;
632
+ try {
633
+ const email = await cacheInstance.get(`entitlements:mapping:${source}:${externalId}`);
634
+ if (email) {
635
+ await invalidateEntitlementCache(email);
636
+ }
637
+ }
638
+ catch (error) {
639
+ console.error('[EntitlementsPlugin] Invalidate by external ID error:', error);
640
+ }
641
+ }
642
+ // ========================================
643
+ // Middleware Helpers
644
+ // ========================================
645
+ /**
646
+ * Express middleware to require a specific entitlement
647
+ */
648
+ export function requireEntitlement(entitlement) {
649
+ return async (req, res, next) => {
650
+ const authReq = req;
651
+ const email = authReq.auth?.user?.email;
652
+ if (!email) {
653
+ return res.status(401).json({ error: 'Authentication required' });
654
+ }
655
+ const has = await hasEntitlement(email, entitlement);
656
+ if (!has) {
657
+ return res.status(403).json({
658
+ error: 'Insufficient entitlements',
659
+ required: entitlement,
660
+ });
661
+ }
662
+ next();
663
+ };
664
+ }
665
+ /**
666
+ * Express middleware to require any of the specified entitlements
667
+ */
668
+ export function requireAnyEntitlement(entitlements) {
669
+ return async (req, res, next) => {
670
+ const authReq = req;
671
+ const email = authReq.auth?.user?.email;
672
+ if (!email) {
673
+ return res.status(401).json({ error: 'Authentication required' });
674
+ }
675
+ const has = await hasAnyEntitlement(email, entitlements);
676
+ if (!has) {
677
+ return res.status(403).json({
678
+ error: 'Insufficient entitlements',
679
+ required: entitlements,
680
+ mode: 'any',
681
+ });
682
+ }
683
+ next();
684
+ };
685
+ }
686
+ /**
687
+ * Express middleware to require all of the specified entitlements
688
+ */
689
+ export function requireAllEntitlements(entitlements) {
690
+ return async (req, res, next) => {
691
+ const authReq = req;
692
+ const email = authReq.auth?.user?.email;
693
+ if (!email) {
694
+ return res.status(401).json({ error: 'Authentication required' });
695
+ }
696
+ const has = await hasAllEntitlements(email, entitlements);
697
+ if (!has) {
698
+ return res.status(403).json({
699
+ error: 'Insufficient entitlements',
700
+ required: entitlements,
701
+ mode: 'all',
702
+ });
703
+ }
704
+ next();
705
+ };
706
+ }
707
+ //# sourceMappingURL=entitlements-plugin.js.map