@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
@@ -16,6 +16,9 @@ const { mockUserCacheUsers } = vi.hoisted(() => ({
16
16
  // Stub external deps before imports
17
17
  vi.mock('@memberjunction/sqlserver-dataprovider', () => ({
18
18
  SQLServerDataProvider: class {},
19
+ }));
20
+
21
+ vi.mock('@memberjunction/generic-database-provider', () => ({
19
22
  UserCache: {
20
23
  get Users() { return mockUserCacheUsers; },
21
24
  },
@@ -0,0 +1,135 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import type { IMetadataProvider } from '@memberjunction/core';
3
+ import { redactArg, ResetUnboundCrudInputWarnings } from '../logging/secretRedactor.js';
4
+
5
+ /**
6
+ * `redactArg` binds a GraphQL input type to an entity by NAME:
7
+ *
8
+ * ```ts
9
+ * const match = /^(Create|Update|Delete)(?<name>.+)Input$/.exec(inputTypeName);
10
+ * const entity = provider.Entities.find((e) => e.ClassName === match.groups.name);
11
+ * ```
12
+ *
13
+ * Codegen inputs embed the entity ClassName (`CreateMJCredentialInput` → `MJCredential`)
14
+ * so the lookup succeeds. Hand-written resolver inputs do not (`CreateConnectionInput`
15
+ * → `Connection`, which is no entity), so the lookup fails, no encrypted-field names
16
+ * are contributed, and the arg falls through to `shortenForLog` with values intact.
17
+ *
18
+ * The boot audit tests only the NAME pattern, so it classifies these as metadata-bound
19
+ * and stays silent — the reason the gap went unnoticed. These tests pin both the
20
+ * fail-open behaviour and the warning that now surfaces it.
21
+ */
22
+
23
+ const SECRET = 'sk-live-CREDENTIAL-PAYLOAD';
24
+
25
+ /** Minimal provider exposing one entity with one encrypted column. */
26
+ function providerWith(entities: { ClassName: string; EncryptedFields: { Name: string }[] }[]): IMetadataProvider {
27
+ return { Entities: entities } as unknown as IMetadataProvider;
28
+ }
29
+
30
+ const CREDENTIAL_ENTITY = providerWith([
31
+ { ClassName: 'MJCredential', EncryptedFields: [{ Name: 'Values' }] },
32
+ ]);
33
+
34
+ beforeEach(() => {
35
+ ResetUnboundCrudInputWarnings();
36
+ vi.spyOn(console, 'warn').mockImplementation(() => undefined);
37
+ });
38
+
39
+ afterEach(() => {
40
+ vi.restoreAllMocks();
41
+ });
42
+
43
+ describe('redactArg — entity-bound inputs (the codegen case)', () => {
44
+ it('redacts an encrypted column, keeping the rest', () => {
45
+ const out = redactArg({
46
+ inputTypeName: 'CreateMJCredentialInput',
47
+ rawValue: { Name: 'Stripe', Values: SECRET },
48
+ provider: CREDENTIAL_ENTITY,
49
+ noLogParameter: false,
50
+ noLogFields: new Set(),
51
+ }) as Record<string, unknown>;
52
+
53
+ expect(out.Values).toBe('<redacted>');
54
+ expect(out.Name).toBe('Stripe');
55
+ });
56
+
57
+ it('emits no warning, because the binding resolved', () => {
58
+ redactArg({
59
+ inputTypeName: 'CreateMJCredentialInput',
60
+ rawValue: { Values: SECRET },
61
+ provider: CREDENTIAL_ENTITY,
62
+ noLogParameter: false,
63
+ noLogFields: new Set(),
64
+ });
65
+ expect(console.warn).not.toHaveBeenCalled();
66
+ });
67
+ });
68
+
69
+ describe('redactArg — name matches the convention but no entity exists', () => {
70
+ const unbound = {
71
+ inputTypeName: 'CreateConnectionInput',
72
+ rawValue: { CredentialName: 'acme', CredentialValues: SECRET },
73
+ provider: CREDENTIAL_ENTITY,
74
+ noLogParameter: false,
75
+ noLogFields: new Set<string>(),
76
+ };
77
+
78
+ it('falls open — values are logged, which is the defect being surfaced', () => {
79
+ const out = redactArg({ ...unbound });
80
+ // Documents current behaviour: no entity binding and no @NoLog means no
81
+ // redaction source, so the value survives into the log.
82
+ expect(JSON.stringify(out)).toContain(SECRET);
83
+ });
84
+
85
+ it('now warns, naming the input type and what to do about it', () => {
86
+ redactArg({ ...unbound });
87
+
88
+ expect(console.warn).toHaveBeenCalledTimes(1);
89
+ const message = vi.mocked(console.warn).mock.calls[0][0] as string;
90
+ expect(message).toContain('CreateConnectionInput');
91
+ expect(message).toContain('@NoLog');
92
+ // The warning itself must not become a second leak.
93
+ expect(message).not.toContain(SECRET);
94
+ });
95
+
96
+ it('warns only once per input type, however hot the mutation is', () => {
97
+ redactArg({ ...unbound });
98
+ redactArg({ ...unbound });
99
+ redactArg({ ...unbound });
100
+ expect(console.warn).toHaveBeenCalledTimes(1);
101
+ });
102
+
103
+ it('warns separately for a different unbound input type', () => {
104
+ redactArg({ ...unbound });
105
+ redactArg({ ...unbound, inputTypeName: 'CreateScheduleInput' });
106
+ expect(console.warn).toHaveBeenCalledTimes(2);
107
+ });
108
+ });
109
+
110
+ describe('redactArg — @NoLog closes the gap the metadata cannot', () => {
111
+ it('redacts a marked field on an input with no entity binding', () => {
112
+ const out = redactArg({
113
+ inputTypeName: 'CreateConnectionInput',
114
+ rawValue: { CredentialName: 'acme', CredentialValues: SECRET },
115
+ provider: CREDENTIAL_ENTITY,
116
+ noLogParameter: false,
117
+ noLogFields: new Set(['CredentialValues']),
118
+ }) as Record<string, unknown>;
119
+
120
+ expect(out.CredentialValues).toBe('<redacted>');
121
+ expect(out.CredentialName).toBe('acme');
122
+ expect(JSON.stringify(out)).not.toContain(SECRET);
123
+ });
124
+
125
+ it('a parameter-level mark redacts the whole arg regardless of binding', () => {
126
+ const out = redactArg({
127
+ inputTypeName: 'CreateConnectionInput',
128
+ rawValue: { CredentialValues: SECRET },
129
+ provider: CREDENTIAL_ENTITY,
130
+ noLogParameter: true,
131
+ noLogFields: new Set(),
132
+ });
133
+ expect(out).toBe('<redacted>');
134
+ });
135
+ });
@@ -0,0 +1,159 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { UploadTokenManager } from '../rest/UploadTokenManager.js';
3
+
4
+ describe('UploadTokenManager', () => {
5
+ beforeEach(() => {
6
+ UploadTokenManager.Instance.Clear();
7
+ UploadTokenManager.Instance.maxFileSizeBytes = 100 * 1024 * 1024;
8
+ UploadTokenManager.Instance.maxPoolMemoryBytes = 500 * 1024 * 1024;
9
+ });
10
+
11
+ it('stages and consumes a buffer atomically', () => {
12
+ const buf = Buffer.from('hello world raw binary', 'utf-8');
13
+ const token = UploadTokenManager.Instance.Stage({
14
+ buffer: buf,
15
+ fileName: 'test.txt',
16
+ mimeType: 'text/plain',
17
+ userId: 'user-123',
18
+ });
19
+
20
+ expect(token).toMatch(/^upt_[a-f0-9]{64}$/);
21
+ expect(UploadTokenManager.Instance.ActiveCount).toBe(1);
22
+ expect(UploadTokenManager.Instance.TotalMemoryBytes).toBe(buf.length);
23
+
24
+ // Consume with matching user
25
+ const consumed = UploadTokenManager.Instance.Consume(token, 'user-123');
26
+ expect(consumed).not.toBeNull();
27
+ expect(consumed?.buffer.toString('utf-8')).toBe('hello world raw binary');
28
+ expect(consumed?.fileName).toBe('test.txt');
29
+ expect(consumed?.mimeType).toBe('text/plain');
30
+
31
+ // Single-use: consuming again must return null
32
+ expect(UploadTokenManager.Instance.Consume(token, 'user-123')).toBeNull();
33
+ expect(UploadTokenManager.Instance.ActiveCount).toBe(0);
34
+ expect(UploadTokenManager.Instance.TotalMemoryBytes).toBe(0);
35
+ });
36
+
37
+ it('rejects claim by a different user and evicts token', () => {
38
+ const buf = Buffer.from('secret payload', 'utf-8');
39
+ const token = UploadTokenManager.Instance.Stage({
40
+ buffer: buf,
41
+ fileName: 'secret.pdf',
42
+ mimeType: 'application/pdf',
43
+ userId: 'user-authorized',
44
+ });
45
+
46
+ // Claim attempt by attacker
47
+ const result = UploadTokenManager.Instance.Consume(token, 'user-attacker');
48
+ expect(result).toBeNull();
49
+
50
+ // Token must be evicted after failed attempt
51
+ expect(UploadTokenManager.Instance.ActiveCount).toBe(0);
52
+ expect(UploadTokenManager.Instance.Consume(token, 'user-authorized')).toBeNull();
53
+ });
54
+
55
+ it('enforces single file size limit', () => {
56
+ UploadTokenManager.Instance.maxFileSizeBytes = 10;
57
+ const buf = Buffer.alloc(20);
58
+
59
+ expect(() => {
60
+ UploadTokenManager.Instance.Stage({
61
+ buffer: buf,
62
+ fileName: 'huge.dat',
63
+ mimeType: 'application/octet-stream',
64
+ userId: 'user-123',
65
+ });
66
+ }).toThrow(/exceeds maximum allowed file size/);
67
+ });
68
+
69
+ it('enforces pool memory capacity limit', () => {
70
+ UploadTokenManager.Instance.maxPoolMemoryBytes = 50;
71
+
72
+ UploadTokenManager.Instance.Stage({
73
+ buffer: Buffer.alloc(30),
74
+ fileName: 'file1.dat',
75
+ mimeType: 'application/octet-stream',
76
+ userId: 'user-123',
77
+ });
78
+
79
+ expect(() => {
80
+ UploadTokenManager.Instance.Stage({
81
+ buffer: Buffer.alloc(30),
82
+ fileName: 'file2.dat',
83
+ mimeType: 'application/octet-stream',
84
+ userId: 'user-123',
85
+ });
86
+ }).toThrow(/memory capacity reached/);
87
+ });
88
+
89
+ it('auto-evicts token after TTL expiry', async () => {
90
+ vi.useFakeTimers();
91
+ try {
92
+ const buf = Buffer.from('ephemeral data');
93
+ const token = UploadTokenManager.Instance.Stage({
94
+ buffer: buf,
95
+ fileName: 'temp.txt',
96
+ mimeType: 'text/plain',
97
+ userId: 'user-123',
98
+ ttlSeconds: 5,
99
+ });
100
+
101
+ expect(UploadTokenManager.Instance.ActiveCount).toBe(1);
102
+
103
+ // Fast-forward 6 seconds
104
+ vi.advanceTimersByTime(6000);
105
+
106
+ expect(UploadTokenManager.Instance.ActiveCount).toBe(0);
107
+ expect(UploadTokenManager.Instance.Consume(token, 'user-123')).toBeNull();
108
+ } finally {
109
+ vi.useRealTimers();
110
+ }
111
+ });
112
+
113
+ it('normalizes UUIDs when verifying claiming user', () => {
114
+ const buf = Buffer.from('uuid test data');
115
+ const token = UploadTokenManager.Instance.Stage({
116
+ buffer: buf,
117
+ fileName: 'uuid.txt',
118
+ mimeType: 'text/plain',
119
+ userId: '4A68B0D7-49EE-4D56-82DE-92F66627EE2B',
120
+ });
121
+
122
+ // Claim with lowercase UUID
123
+ const consumed = UploadTokenManager.Instance.Consume(token, '4a68b0d7-49ee-4d56-82de-92f66627ee2b');
124
+ expect(consumed).not.toBeNull();
125
+ expect(consumed?.buffer.toString('utf-8')).toBe('uuid test data');
126
+ });
127
+
128
+ it('enforces per-user memory quota independently of pool memory', () => {
129
+ UploadTokenManager.Instance.maxPoolMemoryBytes = 500;
130
+ UploadTokenManager.Instance.maxUserMemoryBytes = 40;
131
+
132
+ // User A stages 30 bytes (OK)
133
+ UploadTokenManager.Instance.Stage({
134
+ buffer: Buffer.alloc(30),
135
+ fileName: 'userA_1.dat',
136
+ mimeType: 'application/octet-stream',
137
+ userId: 'user-A',
138
+ });
139
+
140
+ // User A tries to stage another 20 bytes (exceeds 40 byte user quota)
141
+ expect(() => {
142
+ UploadTokenManager.Instance.Stage({
143
+ buffer: Buffer.alloc(20),
144
+ fileName: 'userA_2.dat',
145
+ mimeType: 'application/octet-stream',
146
+ userId: 'user-A',
147
+ });
148
+ }).toThrow(/Per-user upload memory quota reached/);
149
+
150
+ // User B can still stage 30 bytes because User B has their own quota
151
+ const tokenB = UploadTokenManager.Instance.Stage({
152
+ buffer: Buffer.alloc(30),
153
+ fileName: 'userB_1.dat',
154
+ mimeType: 'application/octet-stream',
155
+ userId: 'user-B',
156
+ });
157
+ expect(tokenB).toBeDefined();
158
+ });
159
+ });
@@ -4,7 +4,7 @@ import type { DatabaseProviderBase, UserInfo } from '@memberjunction/core';
4
4
  // Controls what UserCache.Instance.GetSystemUser() serves — undefined simulates an unpopulated
5
5
  // cache (no system user available), the fail-closed path.
6
6
  const getSystemUserMock = vi.fn<[], UserInfo | undefined>();
7
- vi.mock('@memberjunction/sqlserver-dataprovider', () => ({
7
+ vi.mock('@memberjunction/generic-database-provider', () => ({
8
8
  UserCache: {
9
9
  get Instance() {
10
10
  return { GetSystemUser: getSystemUserMock };
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @fileoverview Unauthenticated provider-catalog endpoint.
3
+ *
4
+ * The browser must know which identity providers exist BEFORE it can authenticate, so this
5
+ * router is mounted ahead of the unified auth middleware — the same pattern the magic-link JWKS
6
+ * and widget-session routes already use.
7
+ *
8
+ * @module @memberjunction/server/auth
9
+ */
10
+
11
+ import { Router, type Request, type Response } from 'express';
12
+ import { rateLimit } from 'express-rate-limit';
13
+ import { LogError, type PublicAuthProviderCatalog } from '@memberjunction/core';
14
+ import { AuthProviderEngine } from './AuthProviderEngine.js';
15
+
16
+ /** Mount path for the public catalog router. */
17
+ export const AUTH_CATALOG_MOUNT_PATH = '/auth';
18
+
19
+ /** Requests per window per IP. Generous — a browser makes one call per page load. */
20
+ const DEFAULT_RATE_LIMIT_MAX = 60;
21
+
22
+ /** Rate-limit window. */
23
+ const DEFAULT_RATE_LIMIT_WINDOW_MS = 60_000;
24
+
25
+ /**
26
+ * How long a browser may cache the catalog.
27
+ *
28
+ * Short by design: this drives the login screen, so an admin who enables a provider expects to
29
+ * see it without telling users to hard-refresh. A minute keeps the endpoint cheap without making
30
+ * configuration changes feel stuck.
31
+ */
32
+ const CACHE_MAX_AGE_SECONDS = 60;
33
+
34
+ /**
35
+ * Builds the unauthenticated `GET /auth/providers` router.
36
+ *
37
+ * **What it exposes.** Only `Active` + `ClientVisible` rows, projected to the public allow-list
38
+ * by {@link AuthProviderEngine.GetPublicCatalog}. Secret material is structurally unable to reach
39
+ * this response: `CredentialID` and `AdditionalConfiguration` are not part of the public shape,
40
+ * and non-primitive `ClientConfiguration` values are dropped rather than serialized.
41
+ *
42
+ * **Why publishing this is safe.** Every value here was already shipped to anonymous users — a
43
+ * single-provider SPA compiled its client ID, domain and issuer straight into its JavaScript
44
+ * bundle. This endpoint moves that same data behind one source of truth; it does not widen what
45
+ * an anonymous caller can learn.
46
+ *
47
+ * **Failure is empty, not an error.** If the catalog cannot be read, the endpoint returns an empty
48
+ * list so the browser falls back to its compiled `AUTH_TYPE` and the user can still sign in.
49
+ * Returning 500 would take the login screen down for a problem the client can route around.
50
+ */
51
+ export function createAuthProviderCatalogRouter(): Router {
52
+ const router = Router();
53
+
54
+ const limiter = rateLimit({
55
+ windowMs: DEFAULT_RATE_LIMIT_WINDOW_MS,
56
+ limit: DEFAULT_RATE_LIMIT_MAX,
57
+ standardHeaders: 'draft-7',
58
+ legacyHeaders: false,
59
+ message: { providers: [] }
60
+ });
61
+
62
+ router.get('/providers', limiter, (_req: Request, res: Response) => {
63
+ try {
64
+ const body: PublicAuthProviderCatalog = { providers: AuthProviderEngine.Instance.GetPublicCatalog() };
65
+ res.set('Cache-Control', `public, max-age=${CACHE_MAX_AGE_SECONDS}`);
66
+ res.status(200).json(body);
67
+ } catch (error) {
68
+ const message = error instanceof Error ? error.message : String(error);
69
+ LogError(`[Auth] Failed to serve the public provider catalog: ${message}`);
70
+ const empty: PublicAuthProviderCatalog = { providers: [] };
71
+ res.status(200).json(empty);
72
+ }
73
+ });
74
+
75
+ return router;
76
+ }
@@ -0,0 +1,253 @@
1
+ /**
2
+ * @fileoverview Metadata-driven authentication provider catalog.
3
+ *
4
+ * Loads `MJ: Authentication Providers` rows and registers each one with the runtime
5
+ * `AuthProviderFactory`, resolving the concrete driver through `ClassFactory` by `DriverClass`.
6
+ * This is the auth subsystem's equivalent of the File Storage `ServerDriverKey` pattern:
7
+ * adding a provider is a subclass plus a row, never an edit to core code.
8
+ *
9
+ * @module @memberjunction/server/auth
10
+ */
11
+
12
+ import {
13
+ BaseEngine,
14
+ BaseEnginePropertyConfig,
15
+ LogError,
16
+ LogStatusEx,
17
+ RegisterForStartup,
18
+ UserInfo,
19
+ type AuthProviderConfig,
20
+ type IMetadataProvider,
21
+ type PublicAuthProviderInfo
22
+ } from '@memberjunction/core';
23
+ import { MJAuthenticationProviderEntity } from '@memberjunction/core-entities';
24
+ import { AuthProviderFactory } from '@memberjunction/auth-providers';
25
+ import { CredentialEngine } from '@memberjunction/credentials';
26
+
27
+ /**
28
+ * Subsystem label used when this engine asks CredentialEngine to decrypt secret material,
29
+ * so credential-access auditing attributes the read to authentication.
30
+ */
31
+ const CREDENTIAL_SUBSYSTEM = 'Authentication';
32
+
33
+ /**
34
+ * Loads the authentication-provider catalog from metadata and registers it with the
35
+ * {@link AuthProviderFactory}.
36
+ *
37
+ * **Not `deferred`**: no authenticated request may be served before the catalog is registered,
38
+ * or tokens from a metadata-defined provider would be rejected during the startup window.
39
+ *
40
+ * **Severity is `warn`, not `fatal`**, and that is deliberate. This engine is one of two possible
41
+ * sources of providers — `initializeAuthProviders` also registers whatever `mj.config.cjs`
42
+ * declares. Making a catalog load failure fatal would turn a transient metadata problem into a
43
+ * total authentication outage for deployments that never adopted the table. The failure is
44
+ * logged loudly and the config-declared providers still stand.
45
+ */
46
+ @RegisterForStartup({
47
+ priority: 20,
48
+ severity: 'warn',
49
+ deferred: false,
50
+ description: 'Loads the metadata authentication-provider catalog and registers it with AuthProviderFactory'
51
+ })
52
+ export class AuthProviderEngine extends BaseEngine<AuthProviderEngine> {
53
+ private _providers: MJAuthenticationProviderEntity[] = [];
54
+
55
+ public static get Instance(): AuthProviderEngine {
56
+ return super.getInstance<AuthProviderEngine>();
57
+ }
58
+
59
+ /**
60
+ * Loads Active providers only. Inactive rows are excluded at the query rather than filtered
61
+ * later, so a disabled provider is never even instantiated.
62
+ */
63
+ public async Config(forceRefresh?: boolean, contextUser?: UserInfo, provider?: IMetadataProvider): Promise<void> {
64
+ const configs: Partial<BaseEnginePropertyConfig>[] = [
65
+ {
66
+ PropertyName: '_providers',
67
+ EntityName: 'MJ: Authentication Providers',
68
+ Filter: `Status='Active'`,
69
+ OrderBy: 'Sequence ASC, Name ASC',
70
+ CacheLocal: true
71
+ }
72
+ ];
73
+ await this.Load(configs, provider, forceRefresh, contextUser);
74
+ }
75
+
76
+ /** Active providers from metadata, ordered for presentation. */
77
+ public get Providers(): MJAuthenticationProviderEntity[] {
78
+ return this.GetConfigData<MJAuthenticationProviderEntity>('_providers');
79
+ }
80
+
81
+ /**
82
+ * Startup hook: load the catalog, then register every row with the factory.
83
+ */
84
+ public async HandleStartup(contextUser?: UserInfo, provider?: IMetadataProvider): Promise<void> {
85
+ await this.Config(false, contextUser, provider);
86
+ await this.RegisterAll(contextUser);
87
+ }
88
+
89
+ /**
90
+ * Instantiates and registers every catalog row with the {@link AuthProviderFactory}.
91
+ *
92
+ * One bad row must not cost the deployment its other providers, so each registration is
93
+ * isolated: a failure is logged with the offending provider's name and the loop continues.
94
+ *
95
+ * @returns the number of providers successfully registered.
96
+ */
97
+ public async RegisterAll(contextUser?: UserInfo): Promise<number> {
98
+ const factory = AuthProviderFactory.Instance;
99
+ let registered = 0;
100
+
101
+ for (const row of this.Providers) {
102
+ try {
103
+ const config = await this.buildProviderConfig(row, contextUser);
104
+ factory.register(AuthProviderFactory.createProvider(config));
105
+ registered++;
106
+ } catch (error) {
107
+ const message = error instanceof Error ? error.message : String(error);
108
+ LogError(`[Auth] Could not register metadata provider '${row.Name}' (DriverClass '${row.DriverClass}'): ${message}`);
109
+ }
110
+ }
111
+
112
+ if (registered > 0) {
113
+ LogStatusEx({ message: `[Auth] Registered ${registered} provider(s) from metadata.`, verboseOnly: true });
114
+ }
115
+ return registered;
116
+ }
117
+
118
+ /**
119
+ * Projects a catalog row onto the runtime {@link AuthProviderConfig} the driver expects.
120
+ *
121
+ * `AdditionalConfiguration` is spread FIRST so that the modelled columns win over anything a
122
+ * driver-specific blob happens to repeat — the columns are the reviewable, described surface,
123
+ * and a JSON blob must not be able to silently redefine the issuer a token is validated against.
124
+ */
125
+ private async buildProviderConfig(row: MJAuthenticationProviderEntity, contextUser?: UserInfo): Promise<AuthProviderConfig> {
126
+ const additional = this.parseJsonColumn(row.AdditionalConfiguration, row.Name, 'AdditionalConfiguration');
127
+ const secrets = await this.resolveCredential(row, contextUser);
128
+
129
+ const config: AuthProviderConfig = {
130
+ ...additional,
131
+ ...secrets,
132
+ name: row.Name,
133
+ type: row.DriverClass
134
+ };
135
+
136
+ if (row.ClientID) config.clientId = row.ClientID;
137
+ if (row.Domain) config.domain = row.Domain;
138
+ if (row.Issuer) config.issuer = row.Issuer;
139
+ if (row.Audience) config.audience = row.Audience;
140
+ if (row.JWKSUri) config.jwksUri = row.JWKSUri;
141
+ if (row.Scopes) config.scopes = this.parseScopes(row.Scopes);
142
+
143
+ return config;
144
+ }
145
+
146
+ /**
147
+ * Splits the delimited `Scopes` column into the parsed list both projections publish.
148
+ *
149
+ * This is the single home of the delimiter convention: `buildProviderConfig` and
150
+ * `GetPublicCatalog` must agree on it, or the server would validate tokens against one scope
151
+ * set while telling the browser to request another.
152
+ */
153
+ private parseScopes(raw: string | null): string[] {
154
+ return raw ? raw.split(/[\s,]+/).filter((s) => s.length > 0) : [];
155
+ }
156
+
157
+ /**
158
+ * Decrypts the row's linked credential, when it has one.
159
+ *
160
+ * Almost every provider validates tokens against a public JWKS and needs no secret at all, so
161
+ * this is the uncommon path — present for confidential-client flows, management APIs, and SCIM.
162
+ */
163
+ private async resolveCredential(row: MJAuthenticationProviderEntity, contextUser?: UserInfo): Promise<Record<string, string>> {
164
+ if (!row.CredentialID || !row.Credential) {
165
+ return {};
166
+ }
167
+
168
+ await CredentialEngine.Instance.Config(false, contextUser);
169
+ const resolved = await CredentialEngine.Instance.getCredential(row.Credential, {
170
+ contextUser,
171
+ subsystem: CREDENTIAL_SUBSYSTEM
172
+ });
173
+ return resolved.values;
174
+ }
175
+
176
+ /**
177
+ * Parses a JSON configuration column, treating malformed content as absent.
178
+ *
179
+ * Throwing here would take down an otherwise-valid provider over a stray character in an
180
+ * optional extras blob, so the row is registered without the extras and the problem is logged.
181
+ */
182
+ private parseJsonColumn(raw: string | null, providerName: string, columnName: string): Record<string, unknown> {
183
+ if (!raw?.trim()) {
184
+ return {};
185
+ }
186
+ try {
187
+ const parsed: unknown = JSON.parse(raw);
188
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
189
+ return parsed as Record<string, unknown>;
190
+ }
191
+ LogError(`[Auth] ${columnName} on provider '${providerName}' is not a JSON object; ignoring it.`);
192
+ } catch (error) {
193
+ const message = error instanceof Error ? error.message : String(error);
194
+ LogError(`[Auth] ${columnName} on provider '${providerName}' is not valid JSON (${message}); ignoring it.`);
195
+ }
196
+ return {};
197
+ }
198
+
199
+ /**
200
+ * Projects the catalog to the PUBLIC shape served by the unauthenticated catalog endpoint.
201
+ *
202
+ * The projection is an explicit allow-list rather than a redaction of the full row: a column
203
+ * added to this entity later is invisible to anonymous callers until someone deliberately adds
204
+ * it here. `AdditionalConfiguration` and `CredentialID` are structurally absent, never merely
205
+ * omitted at serialization time.
206
+ */
207
+ public GetPublicCatalog(): PublicAuthProviderInfo[] {
208
+ return this.Providers.filter((row) => row.ClientVisible).map((row) => {
209
+ const info: PublicAuthProviderInfo = {
210
+ name: row.Name,
211
+ driverClass: row.DriverClass,
212
+ displayName: row.DisplayName ?? row.Name,
213
+ sequence: row.Sequence,
214
+ isDefault: row.IsDefault
215
+ };
216
+
217
+ if (row.Icon) info.icon = row.Icon;
218
+ if (row.ClientID) info.clientId = row.ClientID;
219
+ if (row.Issuer) info.issuer = row.Issuer;
220
+ if (row.Domain) info.domain = row.Domain;
221
+
222
+ // Published pre-parsed so the browser never re-derives the delimiter convention —
223
+ // the drivers hand this straight to SDK config typed string[].
224
+ const scopes = this.parseScopes(row.Scopes);
225
+ if (scopes.length > 0) info.scopes = scopes;
226
+
227
+ const clientConfig = this.parseJsonColumn(row.ClientConfiguration, row.Name, 'ClientConfiguration');
228
+ if (Object.keys(clientConfig).length > 0) {
229
+ info.clientConfiguration = this.toPublicClientConfig(clientConfig, row.Name);
230
+ }
231
+
232
+ return info;
233
+ });
234
+ }
235
+
236
+ /**
237
+ * Flattens a parsed `ClientConfiguration` blob to the primitive key/value shape the browser
238
+ * contract declares. Nested objects and arrays are dropped with a warning rather than passed
239
+ * through, so a structured blob (the shape a credential would take) cannot ride along to an
240
+ * anonymous caller.
241
+ */
242
+ private toPublicClientConfig(parsed: Record<string, unknown>, providerName: string): Record<string, string | number | boolean | null> {
243
+ const result: Record<string, string | number | boolean | null> = {};
244
+ for (const [key, value] of Object.entries(parsed)) {
245
+ if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) {
246
+ result[key] = value as string | number | boolean | null;
247
+ } else {
248
+ LogError(`[Auth] ClientConfiguration key '${key}' on provider '${providerName}' is not a primitive value; it was NOT published to the public catalog.`);
249
+ }
250
+ }
251
+ return result;
252
+ }
253
+ }
@@ -1,7 +1,7 @@
1
1
  import { RegisterClass } from '@memberjunction/global';
2
2
  import { Metadata, RunView, LogError, EntitySaveOptions } from '@memberjunction/core';
3
3
  import { NewUserBase } from './newUsers.js';
4
- import { UserCache } from '@memberjunction/sqlserver-dataprovider';
4
+ import { UserCache } from '@memberjunction/generic-database-provider';
5
5
  import { configInfo } from '../config.js';
6
6
  import { MJUserEntity } from '@memberjunction/core-entities';
7
7