@memberjunction/server 6.1.0-edge.1 → 6.1.0-edge.3

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 (261) hide show
  1. package/LICENSE +180 -4
  2. package/dist/auth/AuthProviderCatalogRouter.d.ts +31 -0
  3. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -0
  4. package/dist/auth/AuthProviderCatalogRouter.js +69 -0
  5. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -0
  6. package/dist/auth/AuthProviderEngine.d.ts +96 -0
  7. package/dist/auth/AuthProviderEngine.d.ts.map +1 -0
  8. package/dist/auth/AuthProviderEngine.js +245 -0
  9. package/dist/auth/AuthProviderEngine.js.map +1 -0
  10. package/dist/auth/exampleNewUserSubClass.js +1 -1
  11. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  12. package/dist/auth/index.d.ts +11 -0
  13. package/dist/auth/index.d.ts.map +1 -1
  14. package/dist/auth/index.js +65 -18
  15. package/dist/auth/index.js.map +1 -1
  16. package/dist/auth/initializeProviders.d.ts +64 -1
  17. package/dist/auth/initializeProviders.d.ts.map +1 -1
  18. package/dist/auth/initializeProviders.js +117 -5
  19. package/dist/auth/initializeProviders.js.map +1 -1
  20. package/dist/auth/magicLink/MagicLinkService.js +1 -1
  21. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  22. package/dist/auth/newUsers.js +1 -1
  23. package/dist/auth/newUsers.js.map +1 -1
  24. package/dist/config.d.ts +89 -0
  25. package/dist/config.d.ts.map +1 -1
  26. package/dist/config.js +52 -35
  27. package/dist/config.js.map +1 -1
  28. package/dist/context.js +2 -2
  29. package/dist/context.js.map +1 -1
  30. package/dist/generated/generated.d.ts +540 -1595
  31. package/dist/generated/generated.d.ts.map +1 -1
  32. package/dist/generated/generated.js +2755 -16857
  33. package/dist/generated/generated.js.map +1 -1
  34. package/dist/generic/ResolverBase.d.ts +20 -4
  35. package/dist/generic/ResolverBase.d.ts.map +1 -1
  36. package/dist/generic/ResolverBase.js +58 -21
  37. package/dist/generic/ResolverBase.js.map +1 -1
  38. package/dist/generic/RunViewResolver.d.ts +4 -0
  39. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  40. package/dist/generic/RunViewResolver.js +31 -0
  41. package/dist/generic/RunViewResolver.js.map +1 -1
  42. package/dist/index.d.ts +3 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +183 -54
  45. package/dist/index.js.map +1 -1
  46. package/dist/integration/RSUProgressBridge.d.ts +83 -0
  47. package/dist/integration/RSUProgressBridge.d.ts.map +1 -0
  48. package/dist/integration/RSUProgressBridge.js +230 -0
  49. package/dist/integration/RSUProgressBridge.js.map +1 -0
  50. package/dist/integration/SchemaRefreshLaunch.d.ts +52 -0
  51. package/dist/integration/SchemaRefreshLaunch.d.ts.map +1 -0
  52. package/dist/integration/SchemaRefreshLaunch.js +50 -0
  53. package/dist/integration/SchemaRefreshLaunch.js.map +1 -0
  54. package/dist/logging/secretRedactor.d.ts +9 -0
  55. package/dist/logging/secretRedactor.d.ts.map +1 -1
  56. package/dist/logging/secretRedactor.js +39 -0
  57. package/dist/logging/secretRedactor.js.map +1 -1
  58. package/dist/middleware/BaseServerMiddleware.d.ts +5 -0
  59. package/dist/middleware/BaseServerMiddleware.d.ts.map +1 -1
  60. package/dist/middleware/BaseServerMiddleware.js +5 -0
  61. package/dist/middleware/BaseServerMiddleware.js.map +1 -1
  62. package/dist/providerConfigUnits.d.ts +20 -0
  63. package/dist/providerConfigUnits.d.ts.map +1 -0
  64. package/dist/providerConfigUnits.js +22 -0
  65. package/dist/providerConfigUnits.js.map +1 -0
  66. package/dist/realtimeWidget/WidgetSessionService.js +1 -1
  67. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
  68. package/dist/realtimeWidget/widgetGuestElevation.js +1 -1
  69. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  70. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  71. package/dist/resolvers/ActionResolver.js +12 -0
  72. package/dist/resolvers/ActionResolver.js.map +1 -1
  73. package/dist/resolvers/ComponentRegistryResolver.js +1 -1
  74. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  75. package/dist/resolvers/EntityPermissionResolver.js +1 -1
  76. package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
  77. package/dist/resolvers/FileResolver.d.ts +34 -4
  78. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  79. package/dist/resolvers/FileResolver.js +218 -28
  80. package/dist/resolvers/FileResolver.js.map +1 -1
  81. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +85 -3
  82. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  83. package/dist/resolvers/IntegrationDiscoveryResolver.js +302 -82
  84. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  85. package/dist/resolvers/PotentialDuplicateRecordResolver.js +1 -1
  86. package/dist/resolvers/PotentialDuplicateRecordResolver.js.map +1 -1
  87. package/dist/resolvers/QueryResolver.d.ts +5 -4
  88. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  89. package/dist/resolvers/QueryResolver.js +29 -14
  90. package/dist/resolvers/QueryResolver.js.map +1 -1
  91. package/dist/resolvers/QuerySystemUserResolver.d.ts +12 -3
  92. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  93. package/dist/resolvers/QuerySystemUserResolver.js +33 -14
  94. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  95. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
  96. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  97. package/dist/resolvers/RealtimeClientSessionResolver.js +5 -0
  98. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  99. package/dist/resolvers/RunAIAgentResolver.d.ts +3 -1
  100. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  101. package/dist/resolvers/RunAIAgentResolver.js +20 -7
  102. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  103. package/dist/resolvers/SearchEntitiesResolver.js +1 -1
  104. package/dist/resolvers/SearchEntitiesResolver.js.map +1 -1
  105. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  106. package/dist/resolvers/SqlLoggingConfigResolver.js +1 -1
  107. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  108. package/dist/resolvers/SyncRolesUsersResolver.js +2 -2
  109. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  110. package/dist/resolvers/TaskGraphFrameResolver.d.ts +18 -0
  111. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  112. package/dist/resolvers/TaskGraphFrameResolver.js +75 -0
  113. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  114. package/dist/resolvers/UserFavoriteResolver.js +1 -1
  115. package/dist/resolvers/UserFavoriteResolver.js.map +1 -1
  116. package/dist/resolvers/UserResolver.d.ts +19 -17
  117. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  118. package/dist/resolvers/UserResolver.js +66 -67
  119. package/dist/resolvers/UserResolver.js.map +1 -1
  120. package/dist/resolvers/currentUserRoles.d.ts +19 -0
  121. package/dist/resolvers/currentUserRoles.d.ts.map +1 -0
  122. package/dist/resolvers/currentUserRoles.js +36 -0
  123. package/dist/resolvers/currentUserRoles.js.map +1 -0
  124. package/dist/rest/MediaAccessKeys.d.ts +17 -0
  125. package/dist/rest/MediaAccessKeys.d.ts.map +1 -1
  126. package/dist/rest/MediaAccessKeys.js +42 -1
  127. package/dist/rest/MediaAccessKeys.js.map +1 -1
  128. package/dist/rest/MediaStreamHandler.d.ts +3 -2
  129. package/dist/rest/MediaStreamHandler.d.ts.map +1 -1
  130. package/dist/rest/MediaStreamHandler.js +83 -2
  131. package/dist/rest/MediaStreamHandler.js.map +1 -1
  132. package/dist/rest/OAuthCallbackHandler.d.ts +49 -1
  133. package/dist/rest/OAuthCallbackHandler.d.ts.map +1 -1
  134. package/dist/rest/OAuthCallbackHandler.js +130 -44
  135. package/dist/rest/OAuthCallbackHandler.js.map +1 -1
  136. package/dist/rest/SignatureWebhookHandler.js +1 -1
  137. package/dist/rest/SignatureWebhookHandler.js.map +1 -1
  138. package/dist/rest/UploadTokenManager.d.ts +90 -0
  139. package/dist/rest/UploadTokenManager.d.ts.map +1 -0
  140. package/dist/rest/UploadTokenManager.js +156 -0
  141. package/dist/rest/UploadTokenManager.js.map +1 -0
  142. package/dist/services/DurableEntityActionTaskSubmitter.d.ts +26 -0
  143. package/dist/services/DurableEntityActionTaskSubmitter.d.ts.map +1 -0
  144. package/dist/services/DurableEntityActionTaskSubmitter.js +55 -0
  145. package/dist/services/DurableEntityActionTaskSubmitter.js.map +1 -0
  146. package/dist/services/IntegrationSyncWorkerService.d.ts +48 -0
  147. package/dist/services/IntegrationSyncWorkerService.d.ts.map +1 -0
  148. package/dist/services/IntegrationSyncWorkerService.js +132 -0
  149. package/dist/services/IntegrationSyncWorkerService.js.map +1 -0
  150. package/dist/services/ScheduledJobsService.js +1 -1
  151. package/dist/services/ScheduledJobsService.js.map +1 -1
  152. package/dist/services/StartTaskGraphDispatcher.d.ts +0 -10
  153. package/dist/services/StartTaskGraphDispatcher.d.ts.map +1 -1
  154. package/dist/services/StartTaskGraphDispatcher.js +37 -2
  155. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  156. package/dist/services/TaskGraphActionRunner.d.ts +34 -0
  157. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -0
  158. package/dist/services/TaskGraphActionRunner.js +95 -0
  159. package/dist/services/TaskGraphActionRunner.js.map +1 -0
  160. package/dist/services/TaskGraphAgentRunner.d.ts +20 -0
  161. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  162. package/dist/services/TaskGraphAgentRunner.js +53 -0
  163. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  164. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  165. package/dist/services/TaskGraphContinuationDeliverer.js +12 -1
  166. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  167. package/dist/services/TaskGraphPromptRunner.d.ts +31 -0
  168. package/dist/services/TaskGraphPromptRunner.d.ts.map +1 -0
  169. package/dist/services/TaskGraphPromptRunner.js +134 -0
  170. package/dist/services/TaskGraphPromptRunner.js.map +1 -0
  171. package/dist/telephony/RingCentralTelephonyService.js +1 -1
  172. package/dist/telephony/RingCentralTelephonyService.js.map +1 -1
  173. package/dist/telephony/TwilioTelephonyRouter.js +1 -1
  174. package/dist/telephony/TwilioTelephonyRouter.js.map +1 -1
  175. package/dist/telephony/VonageTelephonyRouter.js +1 -1
  176. package/dist/telephony/VonageTelephonyRouter.js.map +1 -1
  177. package/dist/types.d.ts +6 -0
  178. package/dist/types.d.ts.map +1 -1
  179. package/dist/types.js.map +1 -1
  180. package/package.json +91 -91
  181. package/src/__tests__/APIKeyScopeAuth.test.ts +475 -0
  182. package/src/__tests__/AuthProviderEngine.test.ts +278 -0
  183. package/src/__tests__/EntityCRUDHandler.test.ts +378 -0
  184. package/src/__tests__/IntegrationSyncWorkerService.test.ts +275 -0
  185. package/src/__tests__/OAuthCallbackHandler.openRedirect.test.ts +117 -0
  186. package/src/__tests__/OAuthCallbackHandler.xss.test.ts +4 -1
  187. package/src/__tests__/RESTEndpointHandler.test.ts +672 -0
  188. package/src/__tests__/RSUProgressBridge.test.ts +250 -0
  189. package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -2
  190. package/src/__tests__/RealtimeClientSessionResolver.test.ts +1 -1
  191. package/src/__tests__/ResolverBase.frozenRecordMapping.test.ts +133 -0
  192. package/src/__tests__/ResolverBase.transportMapping.test.ts +289 -0
  193. package/src/__tests__/SchemaRefreshLaunch.test.ts +114 -0
  194. package/src/__tests__/SessionManager.test.ts +1 -1
  195. package/src/__tests__/UserResolver.currentUser.test.ts +87 -0
  196. package/src/__tests__/ViewOperationsHandler.test.ts +277 -0
  197. package/src/__tests__/WidgetSessionService.test.ts +1 -1
  198. package/src/__tests__/config-units.test.ts +74 -0
  199. package/src/__tests__/databaseAbstraction.test.ts +1 -1
  200. package/src/__tests__/initializeProviders.refresh.test.ts +146 -0
  201. package/src/__tests__/magicLinkService.provisionLog.test.ts +1 -1
  202. package/src/__tests__/mjapi-bootstrap.test.ts +0 -6
  203. package/src/__tests__/newUsers.test.ts +726 -0
  204. package/src/__tests__/resolverBase.rls.test.ts +3 -0
  205. package/src/__tests__/secretRedactor.failOpen.test.ts +135 -0
  206. package/src/__tests__/uploadTokenManager.test.ts +159 -0
  207. package/src/__tests__/widgetGuestElevation.test.ts +1 -1
  208. package/src/auth/AuthProviderCatalogRouter.ts +76 -0
  209. package/src/auth/AuthProviderEngine.ts +253 -0
  210. package/src/auth/exampleNewUserSubClass.ts +1 -1
  211. package/src/auth/index.ts +69 -21
  212. package/src/auth/initializeProviders.ts +128 -7
  213. package/src/auth/magicLink/MagicLinkService.ts +1 -1
  214. package/src/auth/newUsers.ts +1 -1
  215. package/src/config.ts +55 -36
  216. package/src/context.ts +2 -2
  217. package/src/generated/generated.ts +2038 -10529
  218. package/src/generic/ResolverBase.ts +63 -21
  219. package/src/generic/RunViewResolver.ts +31 -0
  220. package/src/index.ts +199 -56
  221. package/src/integration/RSUProgressBridge.ts +248 -0
  222. package/src/integration/SchemaRefreshLaunch.ts +77 -0
  223. package/src/logging/secretRedactor.ts +43 -0
  224. package/src/middleware/BaseServerMiddleware.ts +5 -0
  225. package/src/providerConfigUnits.ts +21 -0
  226. package/src/realtimeWidget/WidgetSessionService.ts +1 -1
  227. package/src/realtimeWidget/widgetGuestElevation.ts +1 -1
  228. package/src/resolvers/ActionResolver.ts +13 -0
  229. package/src/resolvers/ComponentRegistryResolver.ts +1 -1
  230. package/src/resolvers/EntityPermissionResolver.ts +1 -1
  231. package/src/resolvers/FileResolver.ts +200 -40
  232. package/src/resolvers/IntegrationDiscoveryResolver.ts +328 -70
  233. package/src/resolvers/PotentialDuplicateRecordResolver.ts +1 -1
  234. package/src/resolvers/QueryResolver.ts +25 -11
  235. package/src/resolvers/QuerySystemUserResolver.ts +28 -13
  236. package/src/resolvers/RealtimeClientSessionResolver.ts +13 -0
  237. package/src/resolvers/RunAIAgentResolver.ts +21 -6
  238. package/src/resolvers/SearchEntitiesResolver.ts +1 -1
  239. package/src/resolvers/SqlLoggingConfigResolver.ts +1 -2
  240. package/src/resolvers/SyncRolesUsersResolver.ts +2 -2
  241. package/src/resolvers/TaskGraphFrameResolver.ts +67 -0
  242. package/src/resolvers/UserFavoriteResolver.ts +1 -1
  243. package/src/resolvers/UserResolver.ts +99 -60
  244. package/src/resolvers/currentUserRoles.ts +47 -0
  245. package/src/rest/MediaAccessKeys.ts +47 -1
  246. package/src/rest/MediaStreamHandler.ts +98 -3
  247. package/src/rest/OAuthCallbackHandler.ts +150 -42
  248. package/src/rest/SignatureWebhookHandler.ts +1 -1
  249. package/src/rest/UploadTokenManager.ts +211 -0
  250. package/src/services/DurableEntityActionTaskSubmitter.ts +78 -0
  251. package/src/services/IntegrationSyncWorkerService.ts +141 -0
  252. package/src/services/ScheduledJobsService.ts +1 -1
  253. package/src/services/StartTaskGraphDispatcher.ts +38 -1
  254. package/src/services/TaskGraphActionRunner.ts +99 -0
  255. package/src/services/TaskGraphAgentRunner.ts +58 -1
  256. package/src/services/TaskGraphContinuationDeliverer.ts +11 -1
  257. package/src/services/TaskGraphPromptRunner.ts +144 -0
  258. package/src/telephony/RingCentralTelephonyService.ts +1 -1
  259. package/src/telephony/TwilioTelephonyRouter.ts +1 -1
  260. package/src/telephony/VonageTelephonyRouter.ts +1 -1
  261. package/src/types.ts +6 -0
package/src/auth/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { JwtHeader, SigningKeyCallback, JwtPayload } from 'jsonwebtoken';
2
2
  import { configInfo } from '../config.js';
3
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
3
+ import { UserCache } from '@memberjunction/generic-database-provider';
4
4
  import sql from 'mssql';
5
- import { Metadata, RoleInfo, UserInfo } from '@memberjunction/core';
5
+ import { DatabaseProviderBase, Metadata, RoleInfo, UserInfo } from '@memberjunction/core';
6
6
  import { NewUserBase } from './newUsers.js';
7
7
  import { MJGlobal } from '@memberjunction/global';
8
8
  import { MJUserEntity, MJUserEntityType } from '@memberjunction/core-entities';
@@ -11,6 +11,9 @@ import { initializeAuthProviders } from './initializeProviders.js';
11
11
 
12
12
  export * from './APIKeyScopeAuth.js';
13
13
  export * from './actingContextResolver.js';
14
+ export * from './AuthProviderEngine.js';
15
+ export * from './AuthProviderCatalogRouter.js';
16
+ export { initializeAuthProviders, initializeAuthProvidersFromMetadata, refreshAuthProviders, validateAuthProvidersRegistered } from './initializeProviders.js';
14
17
 
15
18
  // This is a hard-coded forever constant due to internal migrations
16
19
 
@@ -21,9 +24,13 @@ class MissingAuthError extends Error {
21
24
  }
22
25
  }
23
26
 
24
- const refreshUserCache = async (dataSource?: sql.ConnectionPool) => {
27
+ const refreshUserCache = async () => {
25
28
  const startTime: number = Date.now();
26
- await UserCache.Instance.Refresh(dataSource);
29
+ // UserCache is process-global in this phase, and so is the provider MJServer configures at
30
+ // bootstrap — the callers below only hold a ConnectionPool, which they use purely as a
31
+ // "the database is reachable" gate before asking for a refresh.
32
+ const provider = Metadata.Provider as unknown as DatabaseProviderBase; // global-provider-ok: server auth path; runs under the server's single default provider
33
+ await UserCache.Instance.Refresh(provider);
27
34
  const endTime: number = Date.now();
28
35
  const elapsed: number = endTime - startTime;
29
36
 
@@ -179,7 +186,7 @@ export const getSystemUser = async (dataSource?: sql.ConnectionPool, attemptCach
179
186
  if (dataSource && attemptCacheUpdateIfNeeded) {
180
187
  console.warn(`System user not found in cache. Updating cache in attempt to find the user...`);
181
188
 
182
- await refreshUserCache(dataSource);
189
+ await refreshUserCache();
183
190
  return getSystemUser(dataSource, false); // try one more time but do not update cache next time if not found
184
191
  }
185
192
  throw new Error(`System user ID '${UserCache.Instance.SYSTEM_USER_ID}' not found in database`);
@@ -187,6 +194,42 @@ export const getSystemUser = async (dataSource?: sql.ConnectionPool, attemptCach
187
194
  return systemUser;
188
195
  };
189
196
 
197
+ /**
198
+ * Extracts the lowercased domain portion of an email address.
199
+ *
200
+ * @returns the domain, or an empty string when the value is not an email address (e.g. an IdP that
201
+ * issues a bare username). Callers MUST treat an empty result as "cannot be authorized"
202
+ * rather than as a wildcard.
203
+ */
204
+ const extractEmailDomain = (email: string): string => {
205
+ const parts = email.split('@');
206
+ // Reject anything that isn't exactly local@domain — a value with 0 or 2+ '@' is not an address we
207
+ // can make a trust decision about.
208
+ if (parts.length !== 2) return '';
209
+ return parts[1].toLowerCase().trim();
210
+ };
211
+
212
+ /**
213
+ * Tests a domain against `userHandling.newUserAuthorizedDomains`, honoring `*` wildcards.
214
+ *
215
+ * Note that a pattern is matched in full, so `*.example.com` matches `mail.example.com` but NOT
216
+ * `example.com` — list both if you need both.
217
+ */
218
+ const isDomainAuthorized = (domain: string): boolean =>
219
+ configInfo.userHandling.newUserAuthorizedDomains.some((pattern) => {
220
+ // Convert wildcard domain patterns to regular expressions
221
+ const regex = new RegExp('^' + pattern.toLowerCase().trim().replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
222
+ return regex.test(domain);
223
+ });
224
+
225
+ /**
226
+ * Resolves a verified identity to an MJ `UserInfo`, optionally auto-provisioning a new user.
227
+ *
228
+ * @param requestDomain the hostname parsed from the request's `Origin` header. **Not used for any
229
+ * authorization decision** — it is spoofable on non-browser requests, and new-user domain
230
+ * authorization runs against the verified JWT's email domain instead. Retained for audit
231
+ * logging and for the recursive retry call.
232
+ */
190
233
  export const verifyUserRecord = async (
191
234
  email?: string,
192
235
  firstName?: string,
@@ -207,23 +250,21 @@ export const verifyUserRecord = async (
207
250
  });
208
251
 
209
252
  if (!user) {
210
- if (
211
- configInfo.userHandling.autoCreateNewUsers &&
212
- firstName &&
213
- lastName &&
214
- (requestDomain || configInfo.userHandling.newUserLimitedToAuthorizedDomains === false)
215
- ) {
216
- // check to see if the domain that we have a request coming in from matches one of the domains in the autoCreateNewUsersDomains setting
253
+ // NOTE: `requestDomain` (parsed from the spoofable `Origin` header) is deliberately NOT part of
254
+ // this condition. It was previously required here, which meant a non-browser client sending no
255
+ // Origin could never auto-provision while an attacker simply forged one — it gated entry without
256
+ // authorizing anything. Authorization happens below, against the verified identity's email domain.
257
+ if (configInfo.userHandling.autoCreateNewUsers && firstName && lastName) {
258
+ // SECURITY: authorize against the EMAIL DOMAIN of the cryptographically-verified identity,
259
+ // NOT the request `Origin` header. Origin is trivially spoofable on non-browser / bearer-token
260
+ // requests, so gating on it let a holder of any valid IdP token auto-provision an account under
261
+ // an authorized domain by sending a forged Origin. The email comes from the verified JWT.
262
+ const emailDomain: string = extractEmailDomain(email);
217
263
  let passesDomainCheck: boolean =
218
264
  configInfo.userHandling.newUserLimitedToAuthorizedDomains ===
219
265
  false; /*in this first condition, we are set up to NOT care about domain */
220
- if (!passesDomainCheck && requestDomain) {
221
- /*in this second condition, we check the domain against authorized domains*/
222
- passesDomainCheck = configInfo.userHandling.newUserAuthorizedDomains.some((pattern) => {
223
- // Convert wildcard domain patterns to regular expressions
224
- const regex = new RegExp('^' + pattern.toLowerCase().trim().replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
225
- return regex.test(requestDomain?.toLowerCase().trim());
226
- });
266
+ if (!passesDomainCheck) {
267
+ passesDomainCheck = emailDomain.length > 0 && isDomainAuthorized(emailDomain);
227
268
  }
228
269
 
229
270
  if (passesDomainCheck) {
@@ -249,9 +290,16 @@ export const verifyUserRecord = async (
249
290
  UserCache.Instance.Users.push(user);
250
291
  console.warn(` >>> New user ${email} created successfully!`);
251
292
  }
293
+ } else if (emailDomain.length === 0) {
294
+ // The verified identity carries no email domain at all — typically an IdP that issues a bare
295
+ // `preferred_username` with no `email` claim. There is nothing to match against, so the gate
296
+ // denies rather than falling back to anything spoofable.
297
+ console.warn(
298
+ `User ${email} not found in cache and will NOT be auto-created: the verified identity has no email domain (no '@'), so it cannot be matched against newUserAuthorizedDomains. This usually means the identity provider issues a username rather than an email address — configure it to emit an 'email' claim, or set newUserLimitedToAuthorizedDomains to false to disable domain checking.`
299
+ );
252
300
  } else {
253
301
  console.warn(
254
- `User ${email} not found in cache. Request domain '${requestDomain}' does not match any of the domains in the newUserAuthorizedDomains setting. To ignore domain, make sure you set the newUserLimitedToAuthorizedDomains setting to false. In this case we are NOT creating a new user.`
302
+ `User ${email} not found in cache. Email domain '${emailDomain}' does not match any of the domains in the newUserAuthorizedDomains setting. NOTE: this check is against the EMAIL DOMAIN of the verified identity, NOT the browser Origin — if newUserAuthorizedDomains lists frontend hostnames (e.g. 'app.example.com'), replace them with email domains (e.g. 'example.com'). To ignore domain, make sure you set the newUserLimitedToAuthorizedDomains setting to false. In this case we are NOT creating a new user.`
255
303
  );
256
304
  }
257
305
  }
@@ -260,7 +308,7 @@ export const verifyUserRecord = async (
260
308
  // if we get here that means in the above, if we were attempting to create a new user, it did not work, or it wasn't attempted and we have a config that asks us to auto update the cache
261
309
  console.warn(`User ${email} not found in cache. Updating cache in attempt to find the user...`);
262
310
 
263
- await refreshUserCache(dataSource);
311
+ await refreshUserCache();
264
312
 
265
313
  return verifyUserRecord(email, firstName, lastName, requestDomain, dataSource, false); // try one more time but do not update cache next time if not found
266
314
  }
@@ -1,16 +1,45 @@
1
1
  import { configInfo } from '../config.js';
2
- import { AuthProviderConfig, LogError } from '@memberjunction/core';
2
+ import { AuthProviderConfig, LogError, LogStatus, LogStatusEx, UserInfo, type IMetadataProvider } from '@memberjunction/core';
3
3
  import { AuthProviderFactory } from '@memberjunction/auth-providers';
4
+ import { AuthProviderEngine } from './AuthProviderEngine.js';
4
5
 
5
6
  /**
6
- * Initialize authentication providers from configuration
7
+ * Initialize authentication providers from configuration.
8
+ *
9
+ * Registers everything declared in `mj.config.cjs`'s `authProviders` array. This remains the
10
+ * baseline path and runs on every boot: it needs no database, so it is what lets the server
11
+ * authenticate even when metadata is unreachable.
12
+ *
13
+ * Providers defined in the `MJ: Authentication Providers` catalog are layered on top by
14
+ * {@link initializeAuthProvidersFromMetadata}, which runs later in startup once a database
15
+ * connection and a system user exist.
7
16
  */
8
17
  export function initializeAuthProviders(): void {
9
18
  const factory = AuthProviderFactory.Instance;
10
-
19
+
11
20
  // Clear any existing providers
12
21
  factory.clear();
13
22
 
23
+ // Environment-derived providers are DEFAULTS: each provider class maps its own variables via
24
+ // the ConfigFromEnvironment hook, and they apply only when the config file declares no
25
+ // providers of its own. That mirrors the previous behaviour exactly — the env block used to sit
26
+ // in DEFAULT_SERVER_CONFIG, and mergeConfigs replaces (rather than concatenates) arrays, so an
27
+ // explicit `authProviders` in mj.config.cjs has always suppressed the env-derived set.
28
+ //
29
+ // Written back into configInfo.authProviders rather than kept local because that array is the
30
+ // live registry other code reads and appends to (registerMagicLinkAuthProvider pushes into it so
31
+ // a later re-initialize keeps magic-link).
32
+ if (!configInfo.authProviders || configInfo.authProviders.length === 0) {
33
+ const discovered = AuthProviderFactory.DiscoverFromEnvironment();
34
+ if (discovered.length > 0) {
35
+ configInfo.authProviders = discovered;
36
+ LogStatusEx({
37
+ message: `[Auth] Configured ${discovered.length} provider(s) from environment variables: ${discovered.map((p) => p.name).join(', ')}`,
38
+ verboseOnly: true
39
+ });
40
+ }
41
+ }
42
+
14
43
  // Initialize providers from authProviders config
15
44
  if (configInfo.authProviders && configInfo.authProviders.length > 0) {
16
45
  for (const providerConfig of configInfo.authProviders) {
@@ -26,8 +55,100 @@ export function initializeAuthProviders(): void {
26
55
  }
27
56
  }
28
57
 
29
- // Validate we have at least one provider
30
- if (!factory.hasProviders()) {
31
- LogError('No authentication providers configured. Please configure authProviders array in mj.config.cjs');
58
+ // NOTE: the "no providers configured" check deliberately does NOT live here any more — see
59
+ // validateAuthProvidersRegistered(). A metadata-only deployment legitimately has an empty
60
+ // authProviders array at this point, and erroring here would cry wolf on every boot.
61
+ }
62
+
63
+ /**
64
+ * Layers the metadata provider catalog on top of the config-declared providers.
65
+ *
66
+ * Called after the data provider and system user are available. Metadata rows are registered in
67
+ * addition to — not instead of — the config-declared ones: a deployment mid-migration may
68
+ * legitimately have both, and a name collision resolves to the metadata row because it registers
69
+ * last and `AuthProviderFactory.register` keys by provider name.
70
+ *
71
+ * Never throws. An unreachable or empty catalog leaves the config-declared providers exactly as
72
+ * they were, which is what makes adopting this feature a no-op for existing deployments and keeps
73
+ * a metadata problem from turning into a lockout.
74
+ *
75
+ * @returns the number of providers registered from metadata.
76
+ */
77
+ export async function initializeAuthProvidersFromMetadata(contextUser?: UserInfo, provider?: IMetadataProvider): Promise<number> {
78
+ try {
79
+ await AuthProviderEngine.Instance.Config(false, contextUser, provider);
80
+ const count = await AuthProviderEngine.Instance.RegisterAll(contextUser);
81
+ if (count > 0) {
82
+ LogStatus(`[Auth] ${count} authentication provider(s) registered from metadata.`);
83
+ }
84
+ return count;
85
+ } catch (error) {
86
+ const message = error instanceof Error ? error.message : String(error);
87
+ LogError(`[Auth] Could not load the authentication provider catalog from metadata (${message}). Continuing with providers from mj.config.cjs.`);
88
+ return 0;
89
+ }
90
+ }
91
+
92
+ /**
93
+ * Rebuilds the provider registry from scratch: config-declared providers first, then the
94
+ * metadata catalog layered on top — the same two-source order as startup.
95
+ *
96
+ * This is THE entry point for reacting to a catalog edit at runtime (an admin page's Save
97
+ * button, a future invalidation hook). A full rebuild is required because the factory caches
98
+ * issuer→provider resolutions, so an edited row would otherwise keep serving its old
99
+ * configuration until the process restarted. It lives here rather than on `AuthProviderEngine`
100
+ * because a bare `factory.clear()` + metadata re-registration would silently drop everything
101
+ * the config path registered — magic-link included — and no caller should have to know to put
102
+ * those back. `initializeAuthProviders()` clears the factory itself, so no separate clear step
103
+ * exists for a caller to forget.
104
+ *
105
+ * **Failure policy: abort-before-mutate.** Everything that can throw — the catalog reload and
106
+ * the permission probe on its result — runs BEFORE the factory is touched, so a failed refresh
107
+ * leaves the registry serving the previous (stale but complete) provider set and surfaces the
108
+ * error to the caller. This is deliberately the opposite of `initializeAuthProvidersFromMetadata`,
109
+ * which swallows metadata failures: at boot there is no previous registry to preserve and a
110
+ * metadata problem must not become a lockout, while at refresh time there is one and the caller
111
+ * (an admin UI) wants the error. That difference in failure policy is why this function does not
112
+ * simply delegate to its sibling.
113
+ *
114
+ * Known limitation, acceptable for an admin-triggered operation: metadata providers are absent
115
+ * from the registry for the duration of `RegisterAll`'s per-row config builds (config-declared
116
+ * providers are restored synchronously). A request bearing a metadata-declared IdP's token in
117
+ * that window is rejected. If a caller ever needs an atomic swap, build the configs first and
118
+ * register them in one synchronous pass.
119
+ *
120
+ * @returns the number of providers registered from metadata after the refresh.
121
+ */
122
+ export async function refreshAuthProviders(contextUser?: UserInfo, provider?: IMetadataProvider): Promise<number> {
123
+ await AuthProviderEngine.Instance.Config(true, contextUser, provider);
124
+
125
+ // Probe the reloaded catalog BEFORE clearing anything: reading Providers throws
126
+ // PermissionConstrainedError when the load was skipped for permission reasons, and that
127
+ // failure must abort the refresh while the registry is still intact.
128
+ const pendingRows = AuthProviderEngine.Instance.Providers.length;
129
+
130
+ initializeAuthProviders();
131
+ const count = await AuthProviderEngine.Instance.RegisterAll(contextUser);
132
+ LogStatus(
133
+ `[Auth] Provider registry refreshed — ${count} of ${pendingRows} metadata provider(s) registered on top of the config-declared set.`
134
+ );
135
+ return count;
136
+ }
137
+
138
+ /**
139
+ * Reports whether any provider is registered, from either source, and logs a diagnostic when none
140
+ * is.
141
+ *
142
+ * Split out from {@link initializeAuthProviders} because "no providers" is only genuinely wrong
143
+ * once BOTH sources have had their turn.
144
+ */
145
+ export function validateAuthProvidersRegistered(): boolean {
146
+ const hasProviders = AuthProviderFactory.Instance.hasProviders();
147
+ if (!hasProviders) {
148
+ LogError(
149
+ 'No authentication providers are configured. Define them in the "MJ: Authentication Providers" metadata entity, ' +
150
+ 'or in the authProviders array in mj.config.cjs.'
151
+ );
32
152
  }
33
- }
153
+ return hasProviders;
154
+ }
@@ -29,7 +29,7 @@ import {
29
29
  type MJUserEntityType,
30
30
  type MJApplicationRoleEntity,
31
31
  } from '@memberjunction/core-entities';
32
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
32
+ import { UserCache } from '@memberjunction/generic-database-provider';
33
33
  import { CommunicationEngine } from '@memberjunction/communication-engine';
34
34
  import { Message } from '@memberjunction/communication-types';
35
35
  import { configInfo, type MagicLinkConfig } from '../../config.js';
@@ -1,6 +1,6 @@
1
1
  import { ApplicationInfo, DatabaseProviderBase, EntitySaveOptions, LogError, LogStatus, Metadata, RunView, RunViewResult, UserInfo } from "@memberjunction/core";
2
2
  import { RegisterClass } from "@memberjunction/global";
3
- import { UserCache } from "@memberjunction/sqlserver-dataprovider";
3
+ import { UserCache } from "@memberjunction/generic-database-provider";
4
4
  import { configInfo } from "../config.js";
5
5
  import { MJUserEntity, MJUserRoleEntity, MJUserApplicationEntity, MJUserApplicationEntityEntity, MJApplicationEntityType, MJApplicationEntityEntityType, UserInfoEngine } from "@memberjunction/core-entities";
6
6
 
package/src/config.ts CHANGED
@@ -7,7 +7,19 @@ const explorer = cosmiconfigSync('mj', { searchStrategy: 'global' });
7
7
 
8
8
  const userHandlingInfoSchema = z.object({
9
9
  autoCreateNewUsers: z.boolean().optional().default(false),
10
+ /** When true, auto-provisioning is restricted to the domains in `newUserAuthorizedDomains`. */
10
11
  newUserLimitedToAuthorizedDomains: z.boolean().optional().default(false),
12
+ /**
13
+ * Authorized **email domains** for auto-provisioned users — e.g. `['example.com', '*.example.org']`.
14
+ *
15
+ * These are matched against the domain of the email address in the verified identity token, NOT
16
+ * against the browser `Origin` / frontend hostname. If you are upgrading from a build that
17
+ * compared these to the request origin, replace any frontend hostnames here (`app.example.com`,
18
+ * `localhost`) with the email domains your users actually sign in with.
19
+ *
20
+ * `*` wildcards are supported and match in full: `*.example.com` matches `mail.example.com` but
21
+ * NOT `example.com` — list both if you need both.
22
+ */
11
23
  newUserAuthorizedDomains: z.array(z.string()).optional().default([]),
12
24
  newUserRoles: z.array(z.string()).optional().default([]),
13
25
  updateCacheWhenNotFound: z.boolean().optional().default(false),
@@ -131,6 +143,23 @@ const scheduledJobsSchema = z.object({
131
143
  staleLockCleanupInterval: z.number().optional().default(300000), // 5 minutes in ms
132
144
  });
133
145
 
146
+ /**
147
+ * Integration sync worker (PR 1 item 8). When enabled, this process polls
148
+ * `CompanyIntegrationRun` for `Status='Queued'` rows, atomically claims them, executes
149
+ * the sync, and releases. The claim sproc is the mutual exclusion, so any number of
150
+ * processes may run a worker against the same database.
151
+ */
152
+ const integrationSyncWorkerSchema = z.object({
153
+ /** Master switch — off by default so existing deployments keep running syncs inline. */
154
+ enabled: z.boolean().optional().default(false),
155
+ /** Email of the user the worker executes syncs as. */
156
+ systemUserEmail: z.string().optional().default('system@memberjunction.org'),
157
+ /** How often to poll the queue, in ms. */
158
+ pollingIntervalMs: z.number().optional().default(15000),
159
+ /** Maximum runs this worker executes concurrently. */
160
+ maxConcurrentRuns: z.number().optional().default(3),
161
+ });
162
+
134
163
  const queryDialectSchema = z.object({
135
164
  /** When true, saving a Query entity auto-generates QuerySQL entries for configured target dialects */
136
165
  autoConvertOnSave: zodBooleanWithTransforms().default(false),
@@ -501,6 +530,7 @@ const configInfoSchema = z.object({
501
530
  authProviders: z.array(authProviderSchema).optional(),
502
531
  componentRegistries: z.array(componentRegistrySchema).optional(),
503
532
  scheduledJobs: scheduledJobsSchema.optional().default({}),
533
+ integrationSyncWorker: integrationSyncWorkerSchema.optional().default({}),
504
534
  telemetry: telemetrySchema.optional().default({}),
505
535
  queryDialects: queryDialectSchema.optional().default({}),
506
536
  multiTenancy: multiTenancySchema.optional().default({}),
@@ -559,6 +589,7 @@ export type SqlLoggingInfo = z.infer<typeof sqlLoggingSchema>;
559
589
  export type AuthProviderConfig = z.infer<typeof authProviderSchema>;
560
590
  export type ComponentRegistryConfig = z.infer<typeof componentRegistrySchema>;
561
591
  export type ScheduledJobsConfig = z.infer<typeof scheduledJobsSchema>;
592
+ export type IntegrationSyncWorkerConfig = z.infer<typeof integrationSyncWorkerSchema>;
562
593
  export type TelemetryConfig = z.infer<typeof telemetrySchema>;
563
594
  export type QueryDialectConfig = z.infer<typeof queryDialectSchema>;
564
595
  export type MultiTenancyConfig = z.infer<typeof multiTenancySchema>;
@@ -671,6 +702,14 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
671
702
  staleLockCleanupInterval: 300000
672
703
  },
673
704
 
705
+ // Integration sync worker defaults (off — syncs run inline unless a worker is enabled)
706
+ integrationSyncWorker: {
707
+ enabled: false,
708
+ systemUserEmail: 'not.set@nowhere.com',
709
+ pollingIntervalMs: 15000,
710
+ maxConcurrentRuns: 3
711
+ },
712
+
674
713
  // Telemetry defaults
675
714
  telemetry: {
676
715
  enabled: true,
@@ -696,42 +735,22 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
696
735
  },
697
736
  },
698
737
 
699
- // Auth providers (environment-driven)
700
- authProviders: [
701
- // Microsoft Azure AD / Entra ID
702
- process.env.TENANT_ID && process.env.WEB_CLIENT_ID ? {
703
- name: 'azure',
704
- type: 'msal',
705
- issuer: `https://login.microsoftonline.com/${process.env.TENANT_ID}/v2.0`,
706
- audience: process.env.WEB_CLIENT_ID,
707
- jwksUri: `https://login.microsoftonline.com/${process.env.TENANT_ID}/discovery/v2.0/keys`,
708
- clientId: process.env.WEB_CLIENT_ID,
709
- tenantId: process.env.TENANT_ID
710
- } : null,
711
-
712
- // Auth0
713
- process.env.AUTH0_DOMAIN && process.env.AUTH0_CLIENT_ID ? {
714
- name: 'auth0',
715
- type: 'auth0',
716
- issuer: `https://${process.env.AUTH0_DOMAIN}/`,
717
- audience: process.env.AUTH0_CLIENT_ID,
718
- jwksUri: `https://${process.env.AUTH0_DOMAIN}/.well-known/jwks.json`,
719
- clientId: process.env.AUTH0_CLIENT_ID,
720
- clientSecret: process.env.AUTH0_CLIENT_SECRET,
721
- domain: process.env.AUTH0_DOMAIN
722
- } : null,
723
- // AWS Cognito
724
- process.env.COGNITO_USER_POOL_ID && process.env.COGNITO_CLIENT_ID && process.env.AWS_REGION ? {
725
- name: 'cognito',
726
- type: 'cognito',
727
- issuer: `https://cognito-idp.${process.env.AWS_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}`,
728
- audience: process.env.COGNITO_CLIENT_ID,
729
- jwksUri: `https://cognito-idp.${process.env.AWS_REGION}.amazonaws.com/${process.env.COGNITO_USER_POOL_ID}/.well-known/jwks.json`,
730
- clientId: process.env.COGNITO_CLIENT_ID,
731
- region: process.env.AWS_REGION,
732
- userPoolId: process.env.COGNITO_USER_POOL_ID
733
- } : null,
734
- ].filter(Boolean),
738
+ // Auth providers.
739
+ //
740
+ // Empty by design. This used to be a hard-coded block that enumerated Entra / Auth0 / Cognito
741
+ // inline and built each config from its environment variables. That made env-var configuration
742
+ // a closed domain: a third-party provider could register a driver class and take a metadata row
743
+ // or an explicit entry here, but it could never offer the "set two variables and you're done"
744
+ // experience, because the enumeration lived in core.
745
+ //
746
+ // Each provider class now owns its own mapping via the optional static
747
+ // `ConfigFromEnvironment` (see IEnvironmentConfigurableProvider in @memberjunction/auth-providers),
748
+ // and `initializeAuthProviders()` collects them through the ClassFactory registry.
749
+ //
750
+ // Discovery cannot happen here: this literal is evaluated when config.ts is imported, which is
751
+ // BEFORE @memberjunction/auth-providers loads and the driver classes register. It is deferred to
752
+ // registration time, where the registry is populated.
753
+ authProviders: [],
735
754
  };
736
755
 
737
756
  /**
package/src/context.ts CHANGED
@@ -19,10 +19,10 @@ import { v4 as uuidv4 } from 'uuid';
19
19
  import e from 'express';
20
20
  import type { RequestHandler, Request, Response, NextFunction } from 'express';
21
21
  import { DatabaseProviderBase, UserInfo, type MagicLinkScope, type ReturningVisitorContext, type WidgetGuestContext } from '@memberjunction/core';
22
- import { SQLServerDataProvider, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
22
+ import { SQLServerDataProvider, SQLServerProviderConfigData } from '@memberjunction/sqlserver-dataprovider';
23
23
  import { Metadata } from '@memberjunction/core';
24
24
  import { UUIDsEqual } from '@memberjunction/global';
25
- import { resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
25
+ import { UserCache, resolveDbPlatformFromEnv } from '@memberjunction/generic-database-provider';
26
26
  import { GetAPIKeyEngine } from '@memberjunction/api-keys';
27
27
 
28
28
  // ── Session / login audit (Phase 3) ───────────────────────────────────────────