@qwickapps/server 1.3.0 → 1.4.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 (241) hide show
  1. package/README.md +311 -0
  2. package/dist/core/control-panel.d.ts.map +1 -1
  3. package/dist/core/control-panel.js +144 -2
  4. package/dist/core/control-panel.js.map +1 -1
  5. package/dist/core/plugin-registry.d.ts +36 -0
  6. package/dist/core/plugin-registry.d.ts.map +1 -1
  7. package/dist/core/plugin-registry.js +26 -0
  8. package/dist/core/plugin-registry.js.map +1 -1
  9. package/dist/core/types.d.ts +19 -0
  10. package/dist/core/types.d.ts.map +1 -1
  11. package/dist/index.d.ts +2 -2
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +4 -2
  14. package/dist/index.js.map +1 -1
  15. package/dist/plugins/auth/adapter-wrapper.d.ts +47 -0
  16. package/dist/plugins/auth/adapter-wrapper.d.ts.map +1 -0
  17. package/dist/plugins/auth/adapter-wrapper.js +166 -0
  18. package/dist/plugins/auth/adapter-wrapper.js.map +1 -0
  19. package/dist/plugins/auth/adapter-wrapper.test.d.ts +7 -0
  20. package/dist/plugins/auth/adapter-wrapper.test.d.ts.map +1 -0
  21. package/dist/plugins/auth/adapter-wrapper.test.js +303 -0
  22. package/dist/plugins/auth/adapter-wrapper.test.js.map +1 -0
  23. package/dist/plugins/auth/adapters/index.d.ts +1 -0
  24. package/dist/plugins/auth/adapters/index.d.ts.map +1 -1
  25. package/dist/plugins/auth/adapters/index.js +1 -0
  26. package/dist/plugins/auth/adapters/index.js.map +1 -1
  27. package/dist/plugins/auth/adapters/supabase-adapter.d.ts.map +1 -1
  28. package/dist/plugins/auth/adapters/supabase-adapter.js.map +1 -1
  29. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts +18 -0
  30. package/dist/plugins/auth/adapters/supertokens-adapter.d.ts.map +1 -0
  31. package/dist/plugins/auth/adapters/supertokens-adapter.js +267 -0
  32. package/dist/plugins/auth/adapters/supertokens-adapter.js.map +1 -0
  33. package/dist/plugins/auth/config-store.d.ts +11 -0
  34. package/dist/plugins/auth/config-store.d.ts.map +1 -0
  35. package/dist/plugins/auth/config-store.js +232 -0
  36. package/dist/plugins/auth/config-store.js.map +1 -0
  37. package/dist/plugins/auth/config-store.test.d.ts +7 -0
  38. package/dist/plugins/auth/config-store.test.d.ts.map +1 -0
  39. package/dist/plugins/auth/config-store.test.js +299 -0
  40. package/dist/plugins/auth/config-store.test.js.map +1 -0
  41. package/dist/plugins/auth/env-config.d.ts +138 -0
  42. package/dist/plugins/auth/env-config.d.ts.map +1 -0
  43. package/dist/plugins/auth/env-config.js +1122 -0
  44. package/dist/plugins/auth/env-config.js.map +1 -0
  45. package/dist/plugins/auth/index.d.ts +7 -1
  46. package/dist/plugins/auth/index.d.ts.map +1 -1
  47. package/dist/plugins/auth/index.js +7 -0
  48. package/dist/plugins/auth/index.js.map +1 -1
  49. package/dist/plugins/auth/supertokens-adapter.test.d.ts +10 -0
  50. package/dist/plugins/auth/supertokens-adapter.test.d.ts.map +1 -0
  51. package/dist/plugins/auth/supertokens-adapter.test.js +486 -0
  52. package/dist/plugins/auth/supertokens-adapter.test.js.map +1 -0
  53. package/dist/plugins/auth/types.d.ts +176 -0
  54. package/dist/plugins/auth/types.d.ts.map +1 -1
  55. package/dist/plugins/auth/types.js.map +1 -1
  56. package/dist/plugins/cache-plugin.test.js +3 -0
  57. package/dist/plugins/cache-plugin.test.js.map +1 -1
  58. package/dist/plugins/index.d.ts +6 -2
  59. package/dist/plugins/index.d.ts.map +1 -1
  60. package/dist/plugins/index.js +5 -1
  61. package/dist/plugins/index.js.map +1 -1
  62. package/dist/plugins/postgres-plugin.test.js +3 -0
  63. package/dist/plugins/postgres-plugin.test.js.map +1 -1
  64. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts +7 -0
  65. package/dist/plugins/preferences/__tests__/deep-merge.test.d.ts.map +1 -0
  66. package/dist/plugins/preferences/__tests__/deep-merge.test.js +215 -0
  67. package/dist/plugins/preferences/__tests__/deep-merge.test.js.map +1 -0
  68. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts +7 -0
  69. package/dist/plugins/preferences/__tests__/preferences-plugin.test.d.ts.map +1 -0
  70. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js +265 -0
  71. package/dist/plugins/preferences/__tests__/preferences-plugin.test.js.map +1 -0
  72. package/dist/plugins/preferences/index.d.ts +12 -0
  73. package/dist/plugins/preferences/index.d.ts.map +1 -0
  74. package/dist/plugins/preferences/index.js +13 -0
  75. package/dist/plugins/preferences/index.js.map +1 -0
  76. package/dist/plugins/preferences/preferences-plugin.d.ts +39 -0
  77. package/dist/plugins/preferences/preferences-plugin.d.ts.map +1 -0
  78. package/dist/plugins/preferences/preferences-plugin.js +226 -0
  79. package/dist/plugins/preferences/preferences-plugin.js.map +1 -0
  80. package/dist/plugins/preferences/stores/index.d.ts +9 -0
  81. package/dist/plugins/preferences/stores/index.d.ts.map +1 -0
  82. package/dist/plugins/preferences/stores/index.js +9 -0
  83. package/dist/plugins/preferences/stores/index.js.map +1 -0
  84. package/dist/plugins/preferences/stores/postgres-store.d.ts +41 -0
  85. package/dist/plugins/preferences/stores/postgres-store.d.ts.map +1 -0
  86. package/dist/plugins/preferences/stores/postgres-store.js +181 -0
  87. package/dist/plugins/preferences/stores/postgres-store.js.map +1 -0
  88. package/dist/plugins/preferences/types.d.ts +91 -0
  89. package/dist/plugins/preferences/types.d.ts.map +1 -0
  90. package/dist/plugins/preferences/types.js +10 -0
  91. package/dist/plugins/preferences/types.js.map +1 -0
  92. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts +7 -0
  93. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.d.ts.map +1 -0
  94. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js +220 -0
  95. package/dist/plugins/rate-limit/__tests__/rate-limit-plugin.test.js.map +1 -0
  96. package/dist/plugins/rate-limit/cleanup.d.ts +40 -0
  97. package/dist/plugins/rate-limit/cleanup.d.ts.map +1 -0
  98. package/dist/plugins/rate-limit/cleanup.js +72 -0
  99. package/dist/plugins/rate-limit/cleanup.js.map +1 -0
  100. package/dist/plugins/rate-limit/env-config.d.ts +91 -0
  101. package/dist/plugins/rate-limit/env-config.d.ts.map +1 -0
  102. package/dist/plugins/rate-limit/env-config.js +318 -0
  103. package/dist/plugins/rate-limit/env-config.js.map +1 -0
  104. package/dist/plugins/rate-limit/index.d.ts +76 -0
  105. package/dist/plugins/rate-limit/index.d.ts.map +1 -0
  106. package/dist/plugins/rate-limit/index.js +79 -0
  107. package/dist/plugins/rate-limit/index.js.map +1 -0
  108. package/dist/plugins/rate-limit/middleware.d.ts +40 -0
  109. package/dist/plugins/rate-limit/middleware.d.ts.map +1 -0
  110. package/dist/plugins/rate-limit/middleware.js +169 -0
  111. package/dist/plugins/rate-limit/middleware.js.map +1 -0
  112. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts +44 -0
  113. package/dist/plugins/rate-limit/rate-limit-plugin.d.ts.map +1 -0
  114. package/dist/plugins/rate-limit/rate-limit-plugin.js +354 -0
  115. package/dist/plugins/rate-limit/rate-limit-plugin.js.map +1 -0
  116. package/dist/plugins/rate-limit/rate-limit-service.d.ts +110 -0
  117. package/dist/plugins/rate-limit/rate-limit-service.d.ts.map +1 -0
  118. package/dist/plugins/rate-limit/rate-limit-service.js +172 -0
  119. package/dist/plugins/rate-limit/rate-limit-service.js.map +1 -0
  120. package/dist/plugins/rate-limit/stores/cache-store.d.ts +33 -0
  121. package/dist/plugins/rate-limit/stores/cache-store.d.ts.map +1 -0
  122. package/dist/plugins/rate-limit/stores/cache-store.js +225 -0
  123. package/dist/plugins/rate-limit/stores/cache-store.js.map +1 -0
  124. package/dist/plugins/rate-limit/stores/index.d.ts +8 -0
  125. package/dist/plugins/rate-limit/stores/index.d.ts.map +1 -0
  126. package/dist/plugins/rate-limit/stores/index.js +8 -0
  127. package/dist/plugins/rate-limit/stores/index.js.map +1 -0
  128. package/dist/plugins/rate-limit/stores/postgres-store.d.ts +34 -0
  129. package/dist/plugins/rate-limit/stores/postgres-store.d.ts.map +1 -0
  130. package/dist/plugins/rate-limit/stores/postgres-store.js +320 -0
  131. package/dist/plugins/rate-limit/stores/postgres-store.js.map +1 -0
  132. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts +21 -0
  133. package/dist/plugins/rate-limit/strategies/fixed-window.d.ts.map +1 -0
  134. package/dist/plugins/rate-limit/strategies/fixed-window.js +97 -0
  135. package/dist/plugins/rate-limit/strategies/fixed-window.js.map +1 -0
  136. package/dist/plugins/rate-limit/strategies/index.d.ts +14 -0
  137. package/dist/plugins/rate-limit/strategies/index.d.ts.map +1 -0
  138. package/dist/plugins/rate-limit/strategies/index.js +27 -0
  139. package/dist/plugins/rate-limit/strategies/index.js.map +1 -0
  140. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts +22 -0
  141. package/dist/plugins/rate-limit/strategies/sliding-window.d.ts.map +1 -0
  142. package/dist/plugins/rate-limit/strategies/sliding-window.js +122 -0
  143. package/dist/plugins/rate-limit/strategies/sliding-window.js.map +1 -0
  144. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts +28 -0
  145. package/dist/plugins/rate-limit/strategies/token-bucket.d.ts.map +1 -0
  146. package/dist/plugins/rate-limit/strategies/token-bucket.js +121 -0
  147. package/dist/plugins/rate-limit/strategies/token-bucket.js.map +1 -0
  148. package/dist/plugins/rate-limit/types.d.ts +265 -0
  149. package/dist/plugins/rate-limit/types.d.ts.map +1 -0
  150. package/dist/plugins/rate-limit/types.js +9 -0
  151. package/dist/plugins/rate-limit/types.js.map +1 -0
  152. package/dist/plugins/users/__tests__/users-plugin.test.d.ts +9 -0
  153. package/dist/plugins/users/__tests__/users-plugin.test.d.ts.map +1 -0
  154. package/dist/plugins/users/__tests__/users-plugin.test.js +546 -0
  155. package/dist/plugins/users/__tests__/users-plugin.test.js.map +1 -0
  156. package/dist/plugins/users/index.d.ts +2 -2
  157. package/dist/plugins/users/index.d.ts.map +1 -1
  158. package/dist/plugins/users/index.js +1 -1
  159. package/dist/plugins/users/index.js.map +1 -1
  160. package/dist/plugins/users/types.d.ts +36 -0
  161. package/dist/plugins/users/types.d.ts.map +1 -1
  162. package/dist/plugins/users/users-plugin.d.ts +8 -2
  163. package/dist/plugins/users/users-plugin.d.ts.map +1 -1
  164. package/dist/plugins/users/users-plugin.js +122 -5
  165. package/dist/plugins/users/users-plugin.js.map +1 -1
  166. package/dist-ui/assets/index-D7DoZ9rL.js +478 -0
  167. package/dist-ui/assets/index-D7DoZ9rL.js.map +1 -0
  168. package/dist-ui/index.html +1 -1
  169. package/dist-ui-lib/api/controlPanelApi.d.ts +194 -7
  170. package/dist-ui-lib/dashboard/WidgetComponentRegistry.d.ts +9 -5
  171. package/dist-ui-lib/dashboard/builtInWidgets.d.ts +7 -1
  172. package/dist-ui-lib/dashboard/widgets/AuthStatusWidget.d.ts +9 -0
  173. package/dist-ui-lib/dashboard/widgets/IntegrationStatusWidget.d.ts +9 -0
  174. package/dist-ui-lib/dashboard/widgets/index.d.ts +2 -0
  175. package/dist-ui-lib/index.js +3665 -3945
  176. package/dist-ui-lib/index.js.map +1 -1
  177. package/dist-ui-lib/pages/AuthPage.d.ts +1 -0
  178. package/dist-ui-lib/pages/IntegrationsPage.d.ts +1 -0
  179. package/dist-ui-lib/pages/PluginsPage.d.ts +1 -0
  180. package/dist-ui-lib/pages/RateLimitPage.d.ts +1 -0
  181. package/package.json +7 -2
  182. package/src/core/control-panel.ts +161 -2
  183. package/src/core/plugin-registry.ts +63 -0
  184. package/src/core/types.ts +17 -0
  185. package/src/index.ts +45 -0
  186. package/src/plugins/auth/adapter-wrapper.test.ts +395 -0
  187. package/src/plugins/auth/adapter-wrapper.ts +205 -0
  188. package/src/plugins/auth/adapters/index.ts +1 -0
  189. package/src/plugins/auth/adapters/supabase-adapter.ts +22 -14
  190. package/src/plugins/auth/adapters/supertokens-adapter.ts +326 -0
  191. package/src/plugins/auth/config-store.test.ts +417 -0
  192. package/src/plugins/auth/config-store.ts +305 -0
  193. package/src/plugins/auth/env-config.ts +1279 -0
  194. package/src/plugins/auth/index.ts +30 -0
  195. package/src/plugins/auth/supertokens-adapter.test.ts +621 -0
  196. package/src/plugins/auth/types.ts +218 -0
  197. package/src/plugins/cache-plugin.test.ts +3 -0
  198. package/src/plugins/index.ts +75 -0
  199. package/src/plugins/postgres-plugin.test.ts +3 -0
  200. package/src/plugins/preferences/__tests__/deep-merge.test.ts +242 -0
  201. package/src/plugins/preferences/__tests__/preferences-plugin.test.ts +350 -0
  202. package/src/plugins/preferences/index.ts +30 -0
  203. package/src/plugins/preferences/preferences-plugin.ts +270 -0
  204. package/src/plugins/preferences/stores/index.ts +9 -0
  205. package/src/plugins/preferences/stores/postgres-store.ts +252 -0
  206. package/src/plugins/preferences/types.ts +100 -0
  207. package/src/plugins/rate-limit/__tests__/rate-limit-plugin.test.ts +259 -0
  208. package/src/plugins/rate-limit/cleanup.ts +117 -0
  209. package/src/plugins/rate-limit/env-config.ts +400 -0
  210. package/src/plugins/rate-limit/index.ts +128 -0
  211. package/src/plugins/rate-limit/middleware.ts +212 -0
  212. package/src/plugins/rate-limit/rate-limit-plugin.ts +400 -0
  213. package/src/plugins/rate-limit/rate-limit-service.ts +228 -0
  214. package/src/plugins/rate-limit/stores/cache-store.ts +261 -0
  215. package/src/plugins/rate-limit/stores/index.ts +8 -0
  216. package/src/plugins/rate-limit/stores/postgres-store.ts +402 -0
  217. package/src/plugins/rate-limit/strategies/fixed-window.ts +116 -0
  218. package/src/plugins/rate-limit/strategies/index.ts +30 -0
  219. package/src/plugins/rate-limit/strategies/sliding-window.ts +157 -0
  220. package/src/plugins/rate-limit/strategies/token-bucket.ts +154 -0
  221. package/src/plugins/rate-limit/types.ts +338 -0
  222. package/src/plugins/users/__tests__/users-plugin.test.ts +690 -0
  223. package/src/plugins/users/index.ts +3 -0
  224. package/src/plugins/users/types.ts +38 -0
  225. package/src/plugins/users/users-plugin.ts +142 -5
  226. package/ui/src/App.tsx +35 -14
  227. package/ui/src/api/controlPanelApi.ts +326 -1
  228. package/ui/src/components/ControlPanelApp.tsx +3 -0
  229. package/ui/src/dashboard/PluginWidgetRenderer.tsx +13 -10
  230. package/ui/src/dashboard/WidgetComponentRegistry.tsx +13 -9
  231. package/ui/src/dashboard/builtInWidgets.tsx +13 -3
  232. package/ui/src/dashboard/widgets/AuthStatusWidget.tsx +143 -0
  233. package/ui/src/dashboard/widgets/IntegrationStatusWidget.tsx +135 -0
  234. package/ui/src/dashboard/widgets/index.ts +2 -0
  235. package/ui/src/pages/AuthPage.tsx +1103 -0
  236. package/ui/src/pages/IntegrationsPage.tsx +288 -0
  237. package/ui/src/pages/PluginsPage.tsx +394 -0
  238. package/ui/src/pages/RateLimitPage.tsx +292 -0
  239. package/ui/vite.lib.config.ts +5 -0
  240. package/dist-ui/assets/index-Bsp2ntcw.js +0 -465
  241. package/dist-ui/assets/index-Bsp2ntcw.js.map +0 -1
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Auth Configuration Store
3
+ *
4
+ * PostgreSQL-based storage for runtime auth configuration.
5
+ * Supports pg_notify for cross-instance hot-reload in scaled deployments.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+
10
+ import type {
11
+ AuthConfigStore,
12
+ RuntimeAuthConfig,
13
+ PostgresAuthConfigStoreConfig,
14
+ } from './types.js';
15
+
16
+ // Pool interface (from pg package)
17
+ interface PgPool {
18
+ query(text: string, values?: unknown[]): Promise<{ rows: unknown[]; rowCount: number | null }>;
19
+ connect(): Promise<PgPoolClient>;
20
+ on(event: string, callback: (err?: Error, client?: PgPoolClient) => void): void;
21
+ }
22
+
23
+ interface PgPoolClient {
24
+ query(text: string, values?: unknown[]): Promise<{ rows: unknown[]; rowCount: number | null }>;
25
+ on(event: string, callback: (msg: { channel: string; payload?: string }) => void): void;
26
+ release(destroy?: boolean): void;
27
+ }
28
+
29
+ /**
30
+ * Create a PostgreSQL-backed auth configuration store
31
+ *
32
+ * @param config Configuration including a pg Pool instance
33
+ * @returns AuthConfigStore implementation
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * import { Pool } from 'pg';
38
+ * import { postgresAuthConfigStore } from '@qwickapps/server';
39
+ *
40
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
41
+ * const store = postgresAuthConfigStore({ pool });
42
+ *
43
+ * // Or with lazy initialization:
44
+ * const store = postgresAuthConfigStore({ pool: () => getPostgres().getPool() });
45
+ * ```
46
+ */
47
+ // Valid identifier pattern (alphanumeric + underscore, starting with letter or underscore)
48
+ const VALID_IDENTIFIER = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
49
+
50
+ /**
51
+ * Validate SQL identifier to prevent SQL injection
52
+ */
53
+ function validateIdentifier(name: string, type: string): void {
54
+ if (!VALID_IDENTIFIER.test(name)) {
55
+ throw new Error(`Invalid ${type}: must be alphanumeric with underscores, starting with letter or underscore`);
56
+ }
57
+ if (name.length > 63) {
58
+ throw new Error(`Invalid ${type}: must be 63 characters or less`);
59
+ }
60
+ }
61
+
62
+ export function postgresAuthConfigStore(config: PostgresAuthConfigStoreConfig): AuthConfigStore {
63
+ const {
64
+ pool: poolOrFn,
65
+ tableName = 'auth_config',
66
+ schema = 'public',
67
+ autoCreateTable = true,
68
+ enableNotify = true,
69
+ notifyChannel = 'auth_config_changed',
70
+ } = config;
71
+
72
+ // Validate identifiers to prevent SQL injection
73
+ validateIdentifier(tableName, 'table name');
74
+ validateIdentifier(schema, 'schema name');
75
+ validateIdentifier(notifyChannel, 'notify channel');
76
+
77
+ // Helper to get pool (supports lazy initialization via function)
78
+ const getPool = (): PgPool => {
79
+ const pool = typeof poolOrFn === 'function' ? poolOrFn() : poolOrFn;
80
+ if (!pool || typeof (pool as PgPool).query !== 'function') {
81
+ throw new Error('Invalid pool: must have query method');
82
+ }
83
+ return pool as PgPool;
84
+ };
85
+
86
+ const tableFullName = `"${schema}"."${tableName}"`;
87
+
88
+ // Listeners for config changes
89
+ const listeners: Set<(config: RuntimeAuthConfig | null) => void> = new Set();
90
+
91
+ // Client dedicated to listening for notifications
92
+ let listenerClient: PgPoolClient | null = null;
93
+
94
+ // Reconnection state for exponential backoff
95
+ let reconnectAttempt = 0;
96
+ const maxReconnectDelay = 60000; // Max 60 seconds
97
+ const baseReconnectDelay = 1000; // Start at 1 second
98
+
99
+ /**
100
+ * Calculate reconnect delay with exponential backoff
101
+ */
102
+ function getReconnectDelay(): number {
103
+ // Exponential backoff: 1s, 2s, 4s, 8s, 16s, 32s, 60s (capped)
104
+ const delay = Math.min(baseReconnectDelay * Math.pow(2, reconnectAttempt), maxReconnectDelay);
105
+ reconnectAttempt++;
106
+ return delay;
107
+ }
108
+
109
+ /**
110
+ * Reset reconnection state after successful connection
111
+ */
112
+ function resetReconnectState(): void {
113
+ reconnectAttempt = 0;
114
+ }
115
+
116
+ /**
117
+ * Start listening for pg_notify events
118
+ */
119
+ async function startListening(): Promise<void> {
120
+ if (!enableNotify || listenerClient) return;
121
+
122
+ try {
123
+ const pool = getPool();
124
+ listenerClient = await pool.connect();
125
+
126
+ // Subscribe to the notification channel
127
+ await listenerClient.query(`LISTEN ${notifyChannel}`);
128
+
129
+ // Reset backoff on successful connection
130
+ resetReconnectState();
131
+
132
+ // Handle notifications
133
+ listenerClient.on('notification', async (msg: { channel: string; payload?: string }) => {
134
+ if (msg.channel === notifyChannel) {
135
+ // Reload config from database and notify listeners
136
+ const newConfig = await loadFromDb();
137
+ for (const listener of listeners) {
138
+ try {
139
+ listener(newConfig);
140
+ } catch (err) {
141
+ console.error('[AuthConfigStore] Listener error:', err);
142
+ }
143
+ }
144
+ }
145
+ });
146
+
147
+ // Handle errors - try to reconnect with exponential backoff
148
+ // Note: pg client emits 'error' events with Error objects, but our interface
149
+ // only defines 'notification'. We cast to any to handle this.
150
+ (listenerClient as unknown as { on(event: 'error', cb: (err: Error) => void): void }).on(
151
+ 'error',
152
+ (err: Error) => {
153
+ console.error('[AuthConfigStore] Listener connection error:', err);
154
+ listenerClient?.release(true);
155
+ listenerClient = null;
156
+ // Try to reconnect with exponential backoff
157
+ const delay = getReconnectDelay();
158
+ console.log(`[AuthConfigStore] Reconnecting in ${delay}ms (attempt ${reconnectAttempt})`);
159
+ setTimeout(() => startListening(), delay);
160
+ }
161
+ );
162
+ } catch (err) {
163
+ console.error('[AuthConfigStore] Failed to start listener:', err);
164
+ listenerClient = null;
165
+ // Also apply backoff on initial connection failure
166
+ const delay = getReconnectDelay();
167
+ console.log(`[AuthConfigStore] Retrying connection in ${delay}ms (attempt ${reconnectAttempt})`);
168
+ setTimeout(() => startListening(), delay);
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Load config from database
174
+ */
175
+ async function loadFromDb(): Promise<RuntimeAuthConfig | null> {
176
+ const pool = getPool();
177
+ const result = await pool.query(
178
+ `SELECT adapter, config, settings, updated_at, updated_by
179
+ FROM ${tableFullName}
180
+ LIMIT 1`
181
+ );
182
+
183
+ if (result.rows.length === 0) {
184
+ return null;
185
+ }
186
+
187
+ const row = result.rows[0] as {
188
+ adapter: string | null;
189
+ config: Record<string, unknown>;
190
+ settings: Record<string, unknown>;
191
+ updated_at: Date;
192
+ updated_by: string | null;
193
+ };
194
+
195
+ return {
196
+ adapter: row.adapter as RuntimeAuthConfig['adapter'],
197
+ config: row.config as RuntimeAuthConfig['config'],
198
+ settings: row.settings as RuntimeAuthConfig['settings'],
199
+ updatedAt: row.updated_at.toISOString(),
200
+ updatedBy: row.updated_by || undefined,
201
+ };
202
+ }
203
+
204
+ return {
205
+ name: 'postgres',
206
+
207
+ async initialize(): Promise<void> {
208
+ if (!autoCreateTable) {
209
+ await startListening();
210
+ return;
211
+ }
212
+
213
+ const pool = getPool();
214
+
215
+ // Create table with singleton pattern (only one row allowed)
216
+ await pool.query(`
217
+ CREATE TABLE IF NOT EXISTS ${tableFullName} (
218
+ id SERIAL PRIMARY KEY,
219
+ adapter VARCHAR(50),
220
+ config JSONB NOT NULL DEFAULT '{}',
221
+ settings JSONB NOT NULL DEFAULT '{}',
222
+ created_at TIMESTAMPTZ DEFAULT NOW(),
223
+ updated_at TIMESTAMPTZ DEFAULT NOW(),
224
+ updated_by VARCHAR(255)
225
+ );
226
+
227
+ -- Ensure only one config row (singleton pattern)
228
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_${tableName}_singleton
229
+ ON ${tableFullName} ((true));
230
+ `);
231
+
232
+ // Start listening for notifications
233
+ await startListening();
234
+ },
235
+
236
+ async load(): Promise<RuntimeAuthConfig | null> {
237
+ return loadFromDb();
238
+ },
239
+
240
+ async save(runtimeConfig: RuntimeAuthConfig): Promise<void> {
241
+ const pool = getPool();
242
+
243
+ // Upsert the configuration
244
+ await pool.query(
245
+ `INSERT INTO ${tableFullName} (adapter, config, settings, updated_at, updated_by)
246
+ VALUES ($1, $2, $3, NOW(), $4)
247
+ ON CONFLICT ((true)) DO UPDATE SET
248
+ adapter = $1,
249
+ config = $2,
250
+ settings = $3,
251
+ updated_at = NOW(),
252
+ updated_by = $4`,
253
+ [
254
+ runtimeConfig.adapter,
255
+ JSON.stringify(runtimeConfig.config),
256
+ JSON.stringify(runtimeConfig.settings),
257
+ runtimeConfig.updatedBy || null,
258
+ ]
259
+ );
260
+
261
+ // Notify other instances
262
+ if (enableNotify) {
263
+ await pool.query(`NOTIFY ${notifyChannel}`);
264
+ }
265
+ },
266
+
267
+ async delete(): Promise<boolean> {
268
+ const pool = getPool();
269
+
270
+ const result = await pool.query(`DELETE FROM ${tableFullName}`);
271
+
272
+ // Notify other instances
273
+ if (enableNotify) {
274
+ await pool.query(`NOTIFY ${notifyChannel}`);
275
+ }
276
+
277
+ return (result.rowCount ?? 0) > 0;
278
+ },
279
+
280
+ onChange(callback: (config: RuntimeAuthConfig | null) => void): () => void {
281
+ listeners.add(callback);
282
+
283
+ // Return unsubscribe function
284
+ return () => {
285
+ listeners.delete(callback);
286
+ };
287
+ },
288
+
289
+ async shutdown(): Promise<void> {
290
+ // Release the listener client
291
+ if (listenerClient) {
292
+ try {
293
+ await listenerClient.query(`UNLISTEN ${notifyChannel}`);
294
+ } catch {
295
+ // Ignore errors during shutdown
296
+ }
297
+ listenerClient.release(true);
298
+ listenerClient = null;
299
+ }
300
+
301
+ // Clear listeners
302
+ listeners.clear();
303
+ },
304
+ };
305
+ }