@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,1122 @@
1
+ /**
2
+ * Auth Plugin Environment Configuration
3
+ *
4
+ * Factory function and utilities for configuring auth adapters via environment variables.
5
+ * Supports all adapters: Auth0, Supabase, Supertokens, Basic.
6
+ *
7
+ * Copyright (c) 2025 QwickApps.com. All rights reserved.
8
+ */
9
+ import { createAuthPlugin } from './auth-plugin.js';
10
+ import { auth0Adapter } from './adapters/auth0-adapter.js';
11
+ import { supabaseAdapter } from './adapters/supabase-adapter.js';
12
+ import { supertokensAdapter } from './adapters/supertokens-adapter.js';
13
+ import { basicAdapter } from './adapters/basic-adapter.js';
14
+ // ═══════════════════════════════════════════════════════════════════════════
15
+ // Module State
16
+ // ═══════════════════════════════════════════════════════════════════════════
17
+ let currentStatus = {
18
+ state: 'disabled',
19
+ adapter: null,
20
+ };
21
+ // Runtime configuration store (PostgreSQL-backed)
22
+ let configStore = null;
23
+ // Adapter wrapper for hot-reload support
24
+ let adapterWrapper = null;
25
+ // Unsubscribe function for config change listener
26
+ let configUnsubscribe = null;
27
+ // ═══════════════════════════════════════════════════════════════════════════
28
+ // Environment Variable Helpers
29
+ // ═══════════════════════════════════════════════════════════════════════════
30
+ /**
31
+ * Get an environment variable, treating empty strings as undefined
32
+ */
33
+ function getEnv(key) {
34
+ const value = process.env[key];
35
+ if (value === undefined || value === null || value.trim() === '') {
36
+ return undefined;
37
+ }
38
+ return value.trim();
39
+ }
40
+ /**
41
+ * Parse a boolean environment variable
42
+ * Supports: true/false, 1/0, yes/no (case-insensitive)
43
+ */
44
+ function getEnvBool(key, defaultValue) {
45
+ const value = getEnv(key);
46
+ if (value === undefined) {
47
+ return defaultValue;
48
+ }
49
+ const lower = value.toLowerCase();
50
+ if (['true', '1', 'yes'].includes(lower)) {
51
+ return true;
52
+ }
53
+ if (['false', '0', 'no'].includes(lower)) {
54
+ return false;
55
+ }
56
+ return defaultValue;
57
+ }
58
+ /**
59
+ * Parse a comma-separated list environment variable
60
+ */
61
+ function getEnvList(key) {
62
+ const value = getEnv(key);
63
+ if (value === undefined) {
64
+ return undefined;
65
+ }
66
+ return value
67
+ .split(',')
68
+ .map((s) => s.trim())
69
+ .filter((s) => s.length > 0);
70
+ }
71
+ /**
72
+ * Mask a sensitive value for display
73
+ */
74
+ function maskValue(value) {
75
+ if (value.length <= 4) {
76
+ return '****';
77
+ }
78
+ return value.substring(0, 2) + '*'.repeat(Math.min(value.length - 4, 20)) + value.substring(value.length - 2);
79
+ }
80
+ /**
81
+ * Parse Supertokens configuration from environment variables
82
+ */
83
+ function parseSupertokensEnv() {
84
+ const errors = [];
85
+ const connectionUri = getEnv('SUPERTOKENS_CONNECTION_URI');
86
+ const appName = getEnv('SUPERTOKENS_APP_NAME');
87
+ const apiDomain = getEnv('SUPERTOKENS_API_DOMAIN');
88
+ const websiteDomain = getEnv('SUPERTOKENS_WEBSITE_DOMAIN');
89
+ // Check required vars
90
+ if (!connectionUri)
91
+ errors.push('SUPERTOKENS_CONNECTION_URI');
92
+ if (!appName)
93
+ errors.push('SUPERTOKENS_APP_NAME');
94
+ if (!apiDomain)
95
+ errors.push('SUPERTOKENS_API_DOMAIN');
96
+ if (!websiteDomain)
97
+ errors.push('SUPERTOKENS_WEBSITE_DOMAIN');
98
+ if (errors.length > 0) {
99
+ return { config: null, errors };
100
+ }
101
+ // Build config
102
+ const config = {
103
+ connectionUri: connectionUri,
104
+ appName: appName,
105
+ apiDomain: apiDomain,
106
+ websiteDomain: websiteDomain,
107
+ apiKey: getEnv('SUPERTOKENS_API_KEY'),
108
+ apiBasePath: getEnv('SUPERTOKENS_API_BASE_PATH') ?? '/auth',
109
+ websiteBasePath: getEnv('SUPERTOKENS_WEBSITE_BASE_PATH') ?? '/auth',
110
+ enableEmailPassword: getEnvBool('SUPERTOKENS_ENABLE_EMAIL_PASSWORD', true),
111
+ };
112
+ // Parse social providers
113
+ const googleClientId = getEnv('SUPERTOKENS_GOOGLE_CLIENT_ID');
114
+ const googleClientSecret = getEnv('SUPERTOKENS_GOOGLE_CLIENT_SECRET');
115
+ const githubClientId = getEnv('SUPERTOKENS_GITHUB_CLIENT_ID');
116
+ const githubClientSecret = getEnv('SUPERTOKENS_GITHUB_CLIENT_SECRET');
117
+ const appleClientId = getEnv('SUPERTOKENS_APPLE_CLIENT_ID');
118
+ const appleClientSecret = getEnv('SUPERTOKENS_APPLE_CLIENT_SECRET');
119
+ const appleKeyId = getEnv('SUPERTOKENS_APPLE_KEY_ID');
120
+ const appleTeamId = getEnv('SUPERTOKENS_APPLE_TEAM_ID');
121
+ // Only add social providers if both client ID and secret are provided
122
+ if (googleClientId && googleClientSecret) {
123
+ config.socialProviders = config.socialProviders || {};
124
+ config.socialProviders.google = {
125
+ clientId: googleClientId,
126
+ clientSecret: googleClientSecret,
127
+ };
128
+ }
129
+ if (githubClientId && githubClientSecret) {
130
+ config.socialProviders = config.socialProviders || {};
131
+ config.socialProviders.github = {
132
+ clientId: githubClientId,
133
+ clientSecret: githubClientSecret,
134
+ };
135
+ }
136
+ if (appleClientId && appleClientSecret && appleKeyId && appleTeamId) {
137
+ config.socialProviders = config.socialProviders || {};
138
+ config.socialProviders.apple = {
139
+ clientId: appleClientId,
140
+ clientSecret: appleClientSecret,
141
+ keyId: appleKeyId,
142
+ teamId: appleTeamId,
143
+ };
144
+ }
145
+ return { config, errors: [] };
146
+ }
147
+ /**
148
+ * Parse Auth0 configuration from environment variables
149
+ */
150
+ function parseAuth0Env() {
151
+ const errors = [];
152
+ const domain = getEnv('AUTH0_DOMAIN');
153
+ const clientId = getEnv('AUTH0_CLIENT_ID');
154
+ const clientSecret = getEnv('AUTH0_CLIENT_SECRET');
155
+ const baseUrl = getEnv('AUTH0_BASE_URL');
156
+ const secret = getEnv('AUTH0_SECRET');
157
+ // Check required vars
158
+ if (!domain)
159
+ errors.push('AUTH0_DOMAIN');
160
+ if (!clientId)
161
+ errors.push('AUTH0_CLIENT_ID');
162
+ if (!clientSecret)
163
+ errors.push('AUTH0_CLIENT_SECRET');
164
+ if (!baseUrl)
165
+ errors.push('AUTH0_BASE_URL');
166
+ if (!secret)
167
+ errors.push('AUTH0_SECRET');
168
+ if (errors.length > 0) {
169
+ return { config: null, errors };
170
+ }
171
+ // Build config
172
+ const config = {
173
+ domain: domain,
174
+ clientId: clientId,
175
+ clientSecret: clientSecret,
176
+ baseUrl: baseUrl,
177
+ secret: secret,
178
+ audience: getEnv('AUTH0_AUDIENCE'),
179
+ scopes: getEnvList('AUTH0_SCOPES') ?? ['openid', 'profile', 'email'],
180
+ allowedRoles: getEnvList('AUTH0_ALLOWED_ROLES'),
181
+ allowedDomains: getEnvList('AUTH0_ALLOWED_DOMAINS'),
182
+ exposeAccessToken: getEnvBool('AUTH0_EXPOSE_ACCESS_TOKEN', false),
183
+ routes: {
184
+ login: getEnv('AUTH0_LOGIN_PATH') ?? '/login',
185
+ logout: getEnv('AUTH0_LOGOUT_PATH') ?? '/logout',
186
+ callback: getEnv('AUTH0_CALLBACK_PATH') ?? '/callback',
187
+ },
188
+ };
189
+ return { config, errors: [] };
190
+ }
191
+ /**
192
+ * Parse Supabase configuration from environment variables
193
+ */
194
+ function parseSupabaseEnv() {
195
+ const errors = [];
196
+ const url = getEnv('SUPABASE_URL');
197
+ const anonKey = getEnv('SUPABASE_ANON_KEY');
198
+ // Check required vars
199
+ if (!url)
200
+ errors.push('SUPABASE_URL');
201
+ if (!anonKey)
202
+ errors.push('SUPABASE_ANON_KEY');
203
+ if (errors.length > 0) {
204
+ return { config: null, errors };
205
+ }
206
+ const config = {
207
+ url: url,
208
+ anonKey: anonKey,
209
+ };
210
+ return { config, errors: [] };
211
+ }
212
+ /**
213
+ * Parse Basic Auth configuration from environment variables
214
+ */
215
+ function parseBasicAuthEnv() {
216
+ const errors = [];
217
+ const username = getEnv('BASIC_AUTH_USERNAME');
218
+ const password = getEnv('BASIC_AUTH_PASSWORD');
219
+ // Check required vars
220
+ if (!username)
221
+ errors.push('BASIC_AUTH_USERNAME');
222
+ if (!password)
223
+ errors.push('BASIC_AUTH_PASSWORD');
224
+ if (errors.length > 0) {
225
+ return { config: null, errors };
226
+ }
227
+ const config = {
228
+ username: username,
229
+ password: password,
230
+ realm: getEnv('BASIC_AUTH_REALM') ?? 'Protected',
231
+ };
232
+ return { config, errors: [] };
233
+ }
234
+ // ═══════════════════════════════════════════════════════════════════════════
235
+ // Factory Function
236
+ // ═══════════════════════════════════════════════════════════════════════════
237
+ /**
238
+ * Valid adapter names
239
+ */
240
+ const VALID_ADAPTERS = ['supertokens', 'auth0', 'supabase', 'basic'];
241
+ /**
242
+ * Create an auth plugin configured from environment variables.
243
+ *
244
+ * The plugin state depends on environment configuration:
245
+ * - **disabled**: AUTH_ADAPTER not set - no authentication middleware is applied
246
+ * - **enabled**: Valid configuration - adapter is active and working
247
+ * - **error**: Invalid configuration - plugin is disabled with error details
248
+ *
249
+ * @example
250
+ * ```typescript
251
+ * // Zero-config setup - reads everything from env vars
252
+ * const authPlugin = createAuthPluginFromEnv();
253
+ *
254
+ * // With overrides
255
+ * const authPlugin = createAuthPluginFromEnv({
256
+ * excludePaths: ['/health', '/metrics'],
257
+ * authRequired: true,
258
+ * });
259
+ * ```
260
+ *
261
+ * @param options - Optional overrides (env vars take precedence for adapter config)
262
+ * @returns A Plugin instance
263
+ */
264
+ export function createAuthPluginFromEnv(options) {
265
+ const adapterName = getEnv('AUTH_ADAPTER')?.toLowerCase();
266
+ // No adapter specified - return disabled plugin
267
+ if (!adapterName) {
268
+ currentStatus = {
269
+ state: 'disabled',
270
+ adapter: null,
271
+ };
272
+ return createDisabledPlugin();
273
+ }
274
+ // Validate adapter name
275
+ if (!VALID_ADAPTERS.includes(adapterName)) {
276
+ const error = `Invalid AUTH_ADAPTER value: "${adapterName}". Valid options: ${VALID_ADAPTERS.join(', ')}`;
277
+ currentStatus = {
278
+ state: 'error',
279
+ adapter: null,
280
+ error,
281
+ };
282
+ return createErrorPlugin(error);
283
+ }
284
+ // Parse adapter-specific configuration
285
+ let parseResult;
286
+ switch (adapterName) {
287
+ case 'supertokens':
288
+ parseResult = parseSupertokensEnv();
289
+ break;
290
+ case 'auth0':
291
+ parseResult = parseAuth0Env();
292
+ break;
293
+ case 'supabase':
294
+ parseResult = parseSupabaseEnv();
295
+ break;
296
+ case 'basic':
297
+ parseResult = parseBasicAuthEnv();
298
+ break;
299
+ }
300
+ // Check for parsing errors
301
+ if (parseResult.errors.length > 0) {
302
+ const error = `Missing required environment variables for ${adapterName} adapter: ${parseResult.errors.join(', ')}`;
303
+ currentStatus = {
304
+ state: 'error',
305
+ adapter: adapterName,
306
+ error,
307
+ missingVars: parseResult.errors,
308
+ };
309
+ return createErrorPlugin(error);
310
+ }
311
+ // Create the adapter
312
+ let adapter;
313
+ switch (adapterName) {
314
+ case 'supertokens':
315
+ adapter = supertokensAdapter(parseResult.config);
316
+ break;
317
+ case 'auth0':
318
+ adapter = auth0Adapter(parseResult.config);
319
+ break;
320
+ case 'supabase':
321
+ adapter = supabaseAdapter(parseResult.config);
322
+ break;
323
+ case 'basic':
324
+ adapter = basicAdapter(parseResult.config);
325
+ break;
326
+ }
327
+ // Build plugin configuration
328
+ const excludePaths = options?.excludePaths ?? getEnvList('AUTH_EXCLUDE_PATHS') ?? [];
329
+ const authRequired = options?.authRequired ?? getEnvBool('AUTH_REQUIRED', true);
330
+ const debug = options?.debug ?? getEnvBool('AUTH_DEBUG', false);
331
+ const pluginConfig = {
332
+ adapter,
333
+ excludePaths,
334
+ authRequired,
335
+ debug,
336
+ onUnauthorized: options?.onUnauthorized,
337
+ };
338
+ // Update status
339
+ currentStatus = {
340
+ state: 'enabled',
341
+ adapter: adapterName,
342
+ config: getMaskedConfig(adapterName),
343
+ };
344
+ // Create the plugin
345
+ const basePlugin = createAuthPlugin(pluginConfig);
346
+ // Wrap to add config status routes
347
+ return {
348
+ ...basePlugin,
349
+ async onStart(pluginConfig, registry) {
350
+ // Call base plugin onStart
351
+ await basePlugin.onStart?.(pluginConfig, registry);
352
+ // Add config status routes
353
+ registerConfigRoutes(registry);
354
+ },
355
+ };
356
+ }
357
+ // ═══════════════════════════════════════════════════════════════════════════
358
+ // Status & Config API
359
+ // ═══════════════════════════════════════════════════════════════════════════
360
+ /**
361
+ * Get current auth plugin status
362
+ */
363
+ export function getAuthStatus() {
364
+ return { ...currentStatus };
365
+ }
366
+ /**
367
+ * Get masked configuration for the current adapter
368
+ */
369
+ function getMaskedConfig(adapter) {
370
+ const config = {};
371
+ // Sensitive keys that should be masked
372
+ const sensitiveKeys = [
373
+ 'SECRET',
374
+ 'PASSWORD',
375
+ 'KEY',
376
+ 'TOKEN',
377
+ 'CREDENTIAL',
378
+ 'ANON_KEY',
379
+ 'API_KEY',
380
+ 'CLIENT_SECRET',
381
+ ];
382
+ const isSensitive = (key) => {
383
+ const upper = key.toUpperCase();
384
+ return sensitiveKeys.some((s) => upper.includes(s));
385
+ };
386
+ // Get all env vars for the adapter
387
+ const prefixes = {
388
+ supertokens: ['SUPERTOKENS_'],
389
+ auth0: ['AUTH0_'],
390
+ supabase: ['SUPABASE_'],
391
+ basic: ['BASIC_AUTH_'],
392
+ };
393
+ for (const [key, value] of Object.entries(process.env)) {
394
+ const matchesPrefix = prefixes[adapter].some((p) => key.startsWith(p));
395
+ if (matchesPrefix && value) {
396
+ config[key] = isSensitive(key) ? maskValue(value) : value;
397
+ }
398
+ }
399
+ // Add general auth vars
400
+ const generalVars = ['AUTH_ADAPTER', 'AUTH_REQUIRED', 'AUTH_EXCLUDE_PATHS', 'AUTH_DEBUG'];
401
+ for (const key of generalVars) {
402
+ const value = process.env[key];
403
+ if (value) {
404
+ config[key] = value;
405
+ }
406
+ }
407
+ return config;
408
+ }
409
+ /**
410
+ * Register config API routes
411
+ */
412
+ function registerConfigRoutes(registry) {
413
+ // GET /auth/config/status - Get current auth status
414
+ registry.addRoute({
415
+ method: 'get',
416
+ path: '/auth/config/status',
417
+ pluginId: 'auth',
418
+ handler: (_req, res) => {
419
+ res.json(getAuthStatus());
420
+ },
421
+ });
422
+ // GET /auth/config - Get current configuration (masked)
423
+ registry.addRoute({
424
+ method: 'get',
425
+ path: '/auth/config',
426
+ pluginId: 'auth',
427
+ handler: async (_req, res) => {
428
+ const status = getAuthStatus();
429
+ const response = {
430
+ state: status.state,
431
+ adapter: status.adapter,
432
+ config: status.config || {},
433
+ error: status.error,
434
+ missingVars: status.missingVars,
435
+ };
436
+ // Include runtime config if available from store
437
+ if (configStore) {
438
+ try {
439
+ const runtimeConfig = await configStore.load();
440
+ if (runtimeConfig) {
441
+ response.runtimeConfig = runtimeConfig;
442
+ }
443
+ }
444
+ catch (err) {
445
+ console.error('[AuthPlugin] Failed to load runtime config:', err);
446
+ }
447
+ }
448
+ res.json(response);
449
+ },
450
+ });
451
+ // PUT /auth/config - Save new configuration
452
+ registry.addRoute({
453
+ method: 'put',
454
+ path: '/auth/config',
455
+ pluginId: 'auth',
456
+ handler: async (req, res) => {
457
+ try {
458
+ // Check if config store is available
459
+ if (!configStore) {
460
+ return res.status(503).json({
461
+ error: 'Configuration store not available',
462
+ message: 'Runtime configuration requires a config store (PostgreSQL)',
463
+ });
464
+ }
465
+ // Parse and validate request body
466
+ const body = req.body;
467
+ if (!body || !body.adapter) {
468
+ return res.status(400).json({
469
+ error: 'Invalid request',
470
+ message: 'Request body must include adapter type',
471
+ });
472
+ }
473
+ // Validate adapter type
474
+ if (!VALID_ADAPTERS.includes(body.adapter)) {
475
+ return res.status(400).json({
476
+ error: 'Invalid adapter',
477
+ message: `Valid adapters: ${VALID_ADAPTERS.join(', ')}`,
478
+ });
479
+ }
480
+ // Validate adapter config
481
+ const validation = validateAdapterConfig(body.adapter, body.config || {});
482
+ if (!validation.valid) {
483
+ return res.status(400).json({
484
+ error: 'Invalid configuration',
485
+ message: validation.errors.join(', '),
486
+ errors: validation.errors,
487
+ });
488
+ }
489
+ // Build runtime config
490
+ const runtimeConfig = {
491
+ adapter: body.adapter,
492
+ config: {
493
+ [body.adapter]: body.config,
494
+ },
495
+ settings: body.settings || {},
496
+ updatedAt: new Date().toISOString(),
497
+ };
498
+ // Save to store
499
+ await configStore.save(runtimeConfig);
500
+ // Apply config immediately (hot-reload)
501
+ if (adapterWrapper) {
502
+ const adapter = createAdapterFromConfig(body.adapter, runtimeConfig.config);
503
+ if (adapter) {
504
+ await adapterWrapper.setAdapter(adapter);
505
+ }
506
+ }
507
+ // Update status
508
+ currentStatus = {
509
+ state: 'enabled',
510
+ adapter: body.adapter,
511
+ config: getMaskedRuntimeConfig(runtimeConfig),
512
+ };
513
+ res.json({
514
+ success: true,
515
+ status: getAuthStatus(),
516
+ });
517
+ }
518
+ catch (error) {
519
+ console.error('[AuthPlugin] Failed to save config:', error);
520
+ res.status(500).json({
521
+ error: 'Failed to save configuration',
522
+ message: error instanceof Error ? error.message : 'Unknown error',
523
+ });
524
+ }
525
+ },
526
+ });
527
+ // DELETE /auth/config - Revert to environment variables
528
+ registry.addRoute({
529
+ method: 'delete',
530
+ path: '/auth/config',
531
+ pluginId: 'auth',
532
+ handler: async (_req, res) => {
533
+ try {
534
+ // Check if config store is available
535
+ if (!configStore) {
536
+ return res.status(503).json({
537
+ error: 'Configuration store not available',
538
+ message: 'Runtime configuration requires a config store (PostgreSQL)',
539
+ });
540
+ }
541
+ // Delete from store
542
+ await configStore.delete();
543
+ // Revert to env vars
544
+ const adapterName = getEnv('AUTH_ADAPTER')?.toLowerCase();
545
+ if (adapterName && VALID_ADAPTERS.includes(adapterName)) {
546
+ const parseResult = getParseResultForAdapter(adapterName);
547
+ if (parseResult.config && adapterWrapper) {
548
+ const adapter = createAdapterForName(adapterName, parseResult.config);
549
+ await adapterWrapper.setAdapter(adapter);
550
+ currentStatus = {
551
+ state: 'enabled',
552
+ adapter: adapterName,
553
+ config: getMaskedConfig(adapterName),
554
+ };
555
+ }
556
+ else if (parseResult.errors.length > 0) {
557
+ currentStatus = {
558
+ state: 'error',
559
+ adapter: adapterName,
560
+ error: `Missing env vars: ${parseResult.errors.join(', ')}`,
561
+ missingVars: parseResult.errors,
562
+ };
563
+ }
564
+ }
565
+ else {
566
+ currentStatus = {
567
+ state: 'disabled',
568
+ adapter: null,
569
+ };
570
+ }
571
+ res.json({
572
+ success: true,
573
+ status: getAuthStatus(),
574
+ });
575
+ }
576
+ catch (error) {
577
+ console.error('[AuthPlugin] Failed to delete config:', error);
578
+ res.status(500).json({
579
+ error: 'Failed to delete configuration',
580
+ message: error instanceof Error ? error.message : 'Unknown error',
581
+ });
582
+ }
583
+ },
584
+ });
585
+ // POST /auth/test-provider - Test provider connection
586
+ registry.addRoute({
587
+ method: 'post',
588
+ path: '/auth/test-provider',
589
+ pluginId: 'auth',
590
+ handler: async (req, res) => {
591
+ try {
592
+ const body = req.body;
593
+ if (!body || !body.adapter) {
594
+ return res.status(400).json({
595
+ error: 'Invalid request',
596
+ message: 'Request body must include adapter type',
597
+ });
598
+ }
599
+ // Validate adapter type
600
+ if (!VALID_ADAPTERS.includes(body.adapter)) {
601
+ return res.status(400).json({
602
+ error: 'Invalid adapter',
603
+ message: `Valid adapters: ${VALID_ADAPTERS.join(', ')}`,
604
+ });
605
+ }
606
+ // Test the connection
607
+ const result = await testProviderConnection(body.adapter, body.config || {});
608
+ res.json(result);
609
+ }
610
+ catch (error) {
611
+ console.error('[AuthPlugin] Test provider error:', error);
612
+ res.status(500).json({
613
+ success: false,
614
+ message: error instanceof Error ? error.message : 'Unknown error',
615
+ });
616
+ }
617
+ },
618
+ });
619
+ // POST /auth/test-current - Test current auth configuration (env-based or runtime)
620
+ registry.addRoute({
621
+ method: 'post',
622
+ path: '/auth/test-current',
623
+ pluginId: 'auth',
624
+ handler: async (_req, res) => {
625
+ try {
626
+ const status = getAuthStatus();
627
+ if (status.state !== 'enabled' || !status.adapter) {
628
+ return res.status(400).json({
629
+ success: false,
630
+ message: 'No active auth configuration to test',
631
+ });
632
+ }
633
+ // Get the current configuration from env vars
634
+ const adapterName = status.adapter;
635
+ const parseResult = getParseResultForAdapter(adapterName);
636
+ if (parseResult.errors.length > 0) {
637
+ return res.json({
638
+ success: false,
639
+ message: `Missing configuration: ${parseResult.errors.join(', ')}`,
640
+ });
641
+ }
642
+ // Test the connection using the current env-based config
643
+ // Use JSON round-trip for safe conversion to plain object
644
+ const configObj = JSON.parse(JSON.stringify(parseResult.config));
645
+ const result = await testProviderConnection(adapterName, configObj);
646
+ res.json(result);
647
+ }
648
+ catch (error) {
649
+ console.error('[AuthPlugin] Test current provider error:', error);
650
+ res.status(500).json({
651
+ success: false,
652
+ message: error instanceof Error ? error.message : 'Unknown error',
653
+ });
654
+ }
655
+ },
656
+ });
657
+ // Register UI menu item for auth configuration
658
+ registry.addMenuItem({
659
+ pluginId: 'auth',
660
+ id: 'auth:sidebar',
661
+ label: 'Authentication',
662
+ icon: 'security',
663
+ route: '/auth',
664
+ order: 50, // After Entitlements (35)
665
+ });
666
+ // Register page contribution
667
+ registry.addPage({
668
+ pluginId: 'auth',
669
+ id: 'auth:config-page',
670
+ route: '/auth',
671
+ component: 'AuthPage',
672
+ });
673
+ // Register dashboard widget
674
+ registry.addWidget({
675
+ id: 'auth-status',
676
+ title: 'Authentication',
677
+ component: 'AuthStatusWidget',
678
+ priority: 40, // Show before integrations
679
+ showByDefault: true,
680
+ pluginId: 'auth',
681
+ });
682
+ // Register health check for auth provider connection
683
+ const status = getAuthStatus();
684
+ if (status.state === 'enabled' && status.adapter) {
685
+ registry.registerHealthCheck({
686
+ name: `auth-${status.adapter}`,
687
+ type: 'custom',
688
+ check: async () => {
689
+ const currentStatus = getAuthStatus();
690
+ if (currentStatus.state !== 'enabled' || !currentStatus.adapter) {
691
+ return { healthy: false, message: 'Auth not configured' };
692
+ }
693
+ const adapterName = currentStatus.adapter;
694
+ const parseResult = getParseResultForAdapter(adapterName);
695
+ if (parseResult.errors.length > 0) {
696
+ return { healthy: false, message: `Missing config: ${parseResult.errors.join(', ')}` };
697
+ }
698
+ const configObj = JSON.parse(JSON.stringify(parseResult.config));
699
+ const result = await testProviderConnection(adapterName, configObj);
700
+ return {
701
+ healthy: result.success,
702
+ message: result.message,
703
+ details: result.details,
704
+ };
705
+ },
706
+ });
707
+ }
708
+ }
709
+ // ═══════════════════════════════════════════════════════════════════════════
710
+ // Disabled & Error Plugins
711
+ // ═══════════════════════════════════════════════════════════════════════════
712
+ /**
713
+ * Create a disabled plugin (no auth middleware)
714
+ */
715
+ function createDisabledPlugin() {
716
+ return {
717
+ id: 'auth',
718
+ name: 'Auth Plugin (Disabled)',
719
+ version: '1.0.0',
720
+ async onStart(_pluginConfig, registry) {
721
+ const logger = registry.getLogger('auth');
722
+ logger.info('Auth plugin disabled - AUTH_ADAPTER not set');
723
+ // Register status routes even when disabled
724
+ registerConfigRoutes(registry);
725
+ },
726
+ async onStop() {
727
+ // Nothing to cleanup
728
+ },
729
+ };
730
+ }
731
+ /**
732
+ * Create an error plugin (auth disabled due to configuration error)
733
+ */
734
+ function createErrorPlugin(error) {
735
+ return {
736
+ id: 'auth',
737
+ name: 'Auth Plugin (Error)',
738
+ version: '1.0.0',
739
+ async onStart(_pluginConfig, registry) {
740
+ const logger = registry.getLogger('auth');
741
+ logger.error(`Auth plugin error: ${error}`);
742
+ // Register status routes so admin can see the error
743
+ registerConfigRoutes(registry);
744
+ },
745
+ async onStop() {
746
+ // Nothing to cleanup
747
+ },
748
+ };
749
+ }
750
+ /**
751
+ * Create an adapter from runtime configuration
752
+ */
753
+ function createAdapterFromConfig(adapterType, config) {
754
+ switch (adapterType) {
755
+ case 'supertokens':
756
+ if (config.supertokens) {
757
+ return supertokensAdapter(config.supertokens);
758
+ }
759
+ break;
760
+ case 'auth0':
761
+ if (config.auth0) {
762
+ return auth0Adapter(config.auth0);
763
+ }
764
+ break;
765
+ case 'supabase':
766
+ if (config.supabase) {
767
+ return supabaseAdapter(config.supabase);
768
+ }
769
+ break;
770
+ case 'basic':
771
+ if (config.basic) {
772
+ return basicAdapter(config.basic);
773
+ }
774
+ break;
775
+ }
776
+ return null;
777
+ }
778
+ /**
779
+ * Validate adapter configuration
780
+ */
781
+ function validateAdapterConfig(adapterType, config) {
782
+ const errors = [];
783
+ switch (adapterType) {
784
+ case 'supertokens':
785
+ if (!config.connectionUri)
786
+ errors.push('connectionUri is required');
787
+ if (!config.appName)
788
+ errors.push('appName is required');
789
+ if (!config.apiDomain)
790
+ errors.push('apiDomain is required');
791
+ if (!config.websiteDomain)
792
+ errors.push('websiteDomain is required');
793
+ break;
794
+ case 'auth0':
795
+ if (!config.domain)
796
+ errors.push('domain is required');
797
+ if (!config.clientId)
798
+ errors.push('clientId is required');
799
+ if (!config.clientSecret)
800
+ errors.push('clientSecret is required');
801
+ if (!config.baseUrl)
802
+ errors.push('baseUrl is required');
803
+ if (!config.secret)
804
+ errors.push('secret is required');
805
+ break;
806
+ case 'supabase':
807
+ if (!config.url)
808
+ errors.push('url is required');
809
+ if (!config.anonKey)
810
+ errors.push('anonKey is required');
811
+ break;
812
+ case 'basic':
813
+ if (!config.username)
814
+ errors.push('username is required');
815
+ if (!config.password)
816
+ errors.push('password is required');
817
+ break;
818
+ }
819
+ return { valid: errors.length === 0, errors };
820
+ }
821
+ /**
822
+ * Validate a URL for security (prevent SSRF attacks)
823
+ */
824
+ function validateUrl(urlString) {
825
+ const url = new URL(urlString);
826
+ if (!['http:', 'https:'].includes(url.protocol)) {
827
+ throw new Error('URL must use http or https protocol');
828
+ }
829
+ // Block private/internal IPs (basic SSRF protection)
830
+ const hostname = url.hostname.toLowerCase();
831
+ if (hostname === 'localhost' ||
832
+ hostname === '127.0.0.1' ||
833
+ hostname === '::1' ||
834
+ hostname.startsWith('192.168.') ||
835
+ hostname.startsWith('10.') ||
836
+ hostname.startsWith('172.') ||
837
+ hostname.endsWith('.internal') ||
838
+ hostname.endsWith('.local')) {
839
+ // Allow localhost for dev/testing, but log it
840
+ console.warn(`[AuthPlugin] Testing connection to internal address: ${hostname}`);
841
+ }
842
+ return url;
843
+ }
844
+ /**
845
+ * Test a provider connection
846
+ */
847
+ async function testProviderConnection(adapterType, config) {
848
+ const startTime = Date.now();
849
+ try {
850
+ // Validate config first
851
+ const validation = validateAdapterConfig(adapterType, config);
852
+ if (!validation.valid) {
853
+ return {
854
+ success: false,
855
+ message: `Invalid configuration: ${validation.errors.join(', ')}`,
856
+ };
857
+ }
858
+ // Test connection based on adapter type
859
+ switch (adapterType) {
860
+ case 'supertokens': {
861
+ // Try to connect to Supertokens core
862
+ const connectionUri = config.connectionUri;
863
+ validateUrl(connectionUri); // Validate URL before making request
864
+ const apiKey = config.apiKey;
865
+ const headers = {
866
+ 'Content-Type': 'application/json',
867
+ };
868
+ if (apiKey) {
869
+ headers['api-key'] = apiKey;
870
+ }
871
+ const response = await fetch(`${connectionUri}/apiversion`, { headers });
872
+ if (!response.ok) {
873
+ return {
874
+ success: false,
875
+ message: `Failed to connect to Supertokens core: ${response.status} ${response.statusText}`,
876
+ };
877
+ }
878
+ const data = (await response.json());
879
+ return {
880
+ success: true,
881
+ message: 'Successfully connected to Supertokens core',
882
+ details: {
883
+ latency: Date.now() - startTime,
884
+ version: data.versions?.[0],
885
+ },
886
+ };
887
+ }
888
+ case 'auth0': {
889
+ // Test Auth0 domain is reachable
890
+ const domain = config.domain;
891
+ const response = await fetch(`https://${domain}/.well-known/openid-configuration`);
892
+ if (!response.ok) {
893
+ return {
894
+ success: false,
895
+ message: `Failed to connect to Auth0: ${response.status} ${response.statusText}`,
896
+ };
897
+ }
898
+ return {
899
+ success: true,
900
+ message: 'Successfully connected to Auth0',
901
+ details: {
902
+ latency: Date.now() - startTime,
903
+ },
904
+ };
905
+ }
906
+ case 'supabase': {
907
+ // Test Supabase endpoint
908
+ const url = config.url;
909
+ validateUrl(url); // Validate URL before making request
910
+ const anonKey = config.anonKey;
911
+ const response = await fetch(`${url}/rest/v1/`, {
912
+ headers: {
913
+ apikey: anonKey,
914
+ Authorization: `Bearer ${anonKey}`,
915
+ },
916
+ });
917
+ // Supabase returns 200 even without tables
918
+ if (!response.ok && response.status !== 404) {
919
+ return {
920
+ success: false,
921
+ message: `Failed to connect to Supabase: ${response.status} ${response.statusText}`,
922
+ };
923
+ }
924
+ return {
925
+ success: true,
926
+ message: 'Successfully connected to Supabase',
927
+ details: {
928
+ latency: Date.now() - startTime,
929
+ },
930
+ };
931
+ }
932
+ case 'basic': {
933
+ // Basic auth just validates credentials are present
934
+ return {
935
+ success: true,
936
+ message: 'Basic auth configuration is valid',
937
+ details: {
938
+ latency: Date.now() - startTime,
939
+ },
940
+ };
941
+ }
942
+ default:
943
+ return {
944
+ success: false,
945
+ message: `Unknown adapter type: ${adapterType}`,
946
+ };
947
+ }
948
+ }
949
+ catch (error) {
950
+ // Provide more helpful error messages for common network errors
951
+ let message = 'Connection test failed';
952
+ if (error instanceof Error) {
953
+ const errorWithCause = error;
954
+ if (error.message === 'fetch failed' || errorWithCause.cause) {
955
+ // Network error - server not reachable
956
+ const cause = errorWithCause.cause;
957
+ if (cause?.message?.includes('ECONNREFUSED')) {
958
+ const uri = adapterType === 'supertokens' ? config.connectionUri :
959
+ adapterType === 'supabase' ? config.url :
960
+ adapterType === 'auth0' ? `https://${config.domain}` : 'server';
961
+ message = `Cannot connect to ${adapterType} at ${uri}. Is the server running?`;
962
+ }
963
+ else {
964
+ message = `Network error: Unable to reach the ${adapterType} server. Check if it's running and accessible.`;
965
+ }
966
+ }
967
+ else {
968
+ message = `Connection test failed: ${error.message}`;
969
+ }
970
+ }
971
+ return {
972
+ success: false,
973
+ message,
974
+ };
975
+ }
976
+ }
977
+ /**
978
+ * Handle configuration change (from database)
979
+ * Called when pg_notify fires on another instance
980
+ */
981
+ async function handleConfigChange(newConfig) {
982
+ if (!adapterWrapper)
983
+ return;
984
+ if (!newConfig || !newConfig.adapter) {
985
+ // No config in database - revert to env vars
986
+ const adapterName = getEnv('AUTH_ADAPTER')?.toLowerCase();
987
+ if (adapterName && VALID_ADAPTERS.includes(adapterName)) {
988
+ // Re-create adapter from env vars
989
+ const parseResult = getParseResultForAdapter(adapterName);
990
+ if (parseResult.config) {
991
+ const adapter = createAdapterForName(adapterName, parseResult.config);
992
+ if (adapter) {
993
+ await adapterWrapper.setAdapter(adapter);
994
+ currentStatus = {
995
+ state: 'enabled',
996
+ adapter: adapterName,
997
+ config: getMaskedConfig(adapterName),
998
+ };
999
+ }
1000
+ }
1001
+ }
1002
+ else {
1003
+ // No env config either - disable
1004
+ currentStatus = {
1005
+ state: 'disabled',
1006
+ adapter: null,
1007
+ };
1008
+ }
1009
+ return;
1010
+ }
1011
+ // Apply new config
1012
+ const adapter = createAdapterFromConfig(newConfig.adapter, newConfig.config);
1013
+ if (adapter) {
1014
+ await adapterWrapper.setAdapter(adapter);
1015
+ currentStatus = {
1016
+ state: 'enabled',
1017
+ adapter: newConfig.adapter,
1018
+ config: getMaskedRuntimeConfig(newConfig),
1019
+ };
1020
+ }
1021
+ }
1022
+ /**
1023
+ * Get masked runtime configuration
1024
+ */
1025
+ function getMaskedRuntimeConfig(config) {
1026
+ const result = {};
1027
+ if (!config.adapter)
1028
+ return result;
1029
+ const sensitiveKeys = ['secret', 'password', 'key', 'token', 'anonKey', 'clientSecret', 'apiKey'];
1030
+ const isSensitive = (key) => sensitiveKeys.some((s) => key.toLowerCase().includes(s.toLowerCase()));
1031
+ // Get adapter-specific config
1032
+ const adapterConfig = config.config[config.adapter];
1033
+ if (adapterConfig) {
1034
+ for (const [key, value] of Object.entries(adapterConfig)) {
1035
+ if (typeof value === 'string') {
1036
+ result[key] = isSensitive(key) ? maskValue(value) : value;
1037
+ }
1038
+ else if (typeof value === 'boolean' || typeof value === 'number') {
1039
+ result[key] = String(value);
1040
+ }
1041
+ }
1042
+ }
1043
+ result['AUTH_ADAPTER'] = config.adapter;
1044
+ return result;
1045
+ }
1046
+ /**
1047
+ * Helper to get parse result for a given adapter
1048
+ */
1049
+ function getParseResultForAdapter(adapterName) {
1050
+ switch (adapterName) {
1051
+ case 'supertokens':
1052
+ return parseSupertokensEnv();
1053
+ case 'auth0':
1054
+ return parseAuth0Env();
1055
+ case 'supabase':
1056
+ return parseSupabaseEnv();
1057
+ case 'basic':
1058
+ return parseBasicAuthEnv();
1059
+ }
1060
+ }
1061
+ /**
1062
+ * Helper to create adapter for a given name and config
1063
+ */
1064
+ function createAdapterForName(adapterName, config) {
1065
+ switch (adapterName) {
1066
+ case 'supertokens':
1067
+ return supertokensAdapter(config);
1068
+ case 'auth0':
1069
+ return auth0Adapter(config);
1070
+ case 'supabase':
1071
+ return supabaseAdapter(config);
1072
+ case 'basic':
1073
+ return basicAdapter(config);
1074
+ }
1075
+ }
1076
+ /**
1077
+ * Set the configuration store for runtime auth config persistence.
1078
+ *
1079
+ * This must be called during application startup to enable runtime configuration
1080
+ * features. Without a config store, the PUT/DELETE endpoints will return 503.
1081
+ *
1082
+ * @param store - PostgreSQL-backed config store from `postgresAuthConfigStore()`
1083
+ *
1084
+ * @example
1085
+ * ```typescript
1086
+ * import { Pool } from 'pg';
1087
+ * import { setAuthConfigStore, postgresAuthConfigStore } from '@qwickapps/server';
1088
+ *
1089
+ * const pool = new Pool({ connectionString: process.env.DATABASE_URL });
1090
+ * const configStore = postgresAuthConfigStore({ pool });
1091
+ * await configStore.initialize();
1092
+ * setAuthConfigStore(configStore);
1093
+ * ```
1094
+ */
1095
+ export function setAuthConfigStore(store) {
1096
+ configStore = store;
1097
+ }
1098
+ /**
1099
+ * Get the current adapter wrapper
1100
+ */
1101
+ export function getAdapterWrapper() {
1102
+ return adapterWrapper;
1103
+ }
1104
+ // ═══════════════════════════════════════════════════════════════════════════
1105
+ // Exports for Testing
1106
+ // ═══════════════════════════════════════════════════════════════════════════
1107
+ // Export internal functions for testing
1108
+ export const __testing = {
1109
+ parseSupertokensEnv,
1110
+ parseAuth0Env,
1111
+ parseSupabaseEnv,
1112
+ parseBasicAuthEnv,
1113
+ getEnv,
1114
+ getEnvBool,
1115
+ getEnvList,
1116
+ maskValue,
1117
+ VALID_ADAPTERS,
1118
+ validateAdapterConfig,
1119
+ testProviderConnection,
1120
+ createAdapterFromConfig,
1121
+ };
1122
+ //# sourceMappingURL=env-config.js.map