@memberjunction/server 6.1.0-edge.5 → 6.1.0-edge.7

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 (250) hide show
  1. package/README.md +30 -11
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/SessionJanitor.js +2 -2
  4. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  5. package/dist/auth/exampleNewUserSubClass.d.ts.map +1 -1
  6. package/dist/auth/exampleNewUserSubClass.js +9 -4
  7. package/dist/auth/exampleNewUserSubClass.js.map +1 -1
  8. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  9. package/dist/auth/magicLink/MagicLinkService.js +2 -8
  10. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  11. package/dist/auth/newUsers.d.ts.map +1 -1
  12. package/dist/auth/newUsers.js +4 -12
  13. package/dist/auth/newUsers.js.map +1 -1
  14. package/dist/auth/principals.d.ts +92 -0
  15. package/dist/auth/principals.d.ts.map +1 -0
  16. package/dist/auth/principals.js +193 -0
  17. package/dist/auth/principals.js.map +1 -0
  18. package/dist/config.d.ts +106 -12
  19. package/dist/config.d.ts.map +1 -1
  20. package/dist/config.js +41 -4
  21. package/dist/config.js.map +1 -1
  22. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts +30 -0
  23. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts.map +1 -1
  24. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js +58 -5
  25. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js.map +1 -1
  26. package/dist/generated/graphql-schemas/__mj.d.ts +7607 -6853
  27. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  28. package/dist/generated/graphql-schemas/__mj.js +25141 -21645
  29. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  30. package/dist/generic/ResolverBase.d.ts +152 -2
  31. package/dist/generic/ResolverBase.d.ts.map +1 -1
  32. package/dist/generic/ResolverBase.js +389 -10
  33. package/dist/generic/ResolverBase.js.map +1 -1
  34. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  35. package/dist/generic/RunViewResolver.js +26 -2
  36. package/dist/generic/RunViewResolver.js.map +1 -1
  37. package/dist/generic/refusalExtensions.d.ts +52 -0
  38. package/dist/generic/refusalExtensions.d.ts.map +1 -0
  39. package/dist/generic/refusalExtensions.js +16 -0
  40. package/dist/generic/refusalExtensions.js.map +1 -0
  41. package/dist/index.d.ts +11 -3
  42. package/dist/index.d.ts.map +1 -1
  43. package/dist/index.js +131 -93
  44. package/dist/index.js.map +1 -1
  45. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -1
  46. package/dist/realtimeWidget/WidgetSessionService.js +2 -12
  47. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -1
  48. package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -1
  49. package/dist/realtimeWidget/visitorIdentity.js +20 -5
  50. package/dist/realtimeWidget/visitorIdentity.js.map +1 -1
  51. package/dist/resolvers/EntityRecordNameResolver.d.ts +34 -5
  52. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  53. package/dist/resolvers/EntityRecordNameResolver.js +64 -7
  54. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  55. package/dist/resolvers/FileCategoryResolver.d.ts.map +1 -1
  56. package/dist/resolvers/FileCategoryResolver.js +1 -2
  57. package/dist/resolvers/FileCategoryResolver.js.map +1 -1
  58. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  59. package/dist/resolvers/IntegrationDiscoveryResolver.js +2 -0
  60. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  61. package/dist/resolvers/QuerySystemUserResolver.js +1 -1
  62. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  63. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -2
  64. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  65. package/dist/resolvers/RealtimeClientSessionResolver.js +80 -4
  66. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  67. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +13 -8
  68. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  69. package/dist/resolvers/RemoteBrowserActionResolver.js +45 -8
  70. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  71. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  72. package/dist/resolvers/SyncRolesUsersResolver.js +11 -2
  73. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  74. package/dist/resolvers/UserViewResolver.d.ts.map +1 -1
  75. package/dist/resolvers/UserViewResolver.js +8 -4
  76. package/dist/resolvers/UserViewResolver.js.map +1 -1
  77. package/dist/rest/EntityCRUDHandler.d.ts +2 -1
  78. package/dist/rest/EntityCRUDHandler.d.ts.map +1 -1
  79. package/dist/rest/EntityCRUDHandler.js +3 -18
  80. package/dist/rest/EntityCRUDHandler.js.map +1 -1
  81. package/dist/rest/RESTEndpointHandler.d.ts +2 -1
  82. package/dist/rest/RESTEndpointHandler.d.ts.map +1 -1
  83. package/dist/rest/RESTEndpointHandler.js +3 -16
  84. package/dist/rest/RESTEndpointHandler.js.map +1 -1
  85. package/dist/rest/RealtimeSdpBrokerHandler.d.ts +23 -0
  86. package/dist/rest/RealtimeSdpBrokerHandler.d.ts.map +1 -0
  87. package/dist/rest/RealtimeSdpBrokerHandler.js +78 -0
  88. package/dist/rest/RealtimeSdpBrokerHandler.js.map +1 -0
  89. package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
  90. package/dist/serverExtensionReservedRoots.js +2 -3
  91. package/dist/serverExtensionReservedRoots.js.map +1 -1
  92. package/package.json +92 -91
  93. package/src/__tests__/EntityCRUDHandler.test.ts +18 -5
  94. package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +118 -0
  95. package/src/__tests__/RealtimeClientSessionResolver.test.ts +20 -0
  96. package/src/__tests__/RealtimeSdpBrokerHandler.test.ts +191 -0
  97. package/src/__tests__/RemoteBrowserAudioStream.test.ts +81 -2
  98. package/src/__tests__/ResolverBase.filterEscaping.test.ts +190 -3
  99. package/src/__tests__/ResolverBase.readableFieldsTransport.test.ts +106 -0
  100. package/src/__tests__/UserViewResolver.updateWhereClause.test.ts +114 -0
  101. package/src/__tests__/WidgetSessionService.test.ts +4 -1
  102. package/src/__tests__/generatedGraphQLSchema.structural.test.ts +46 -0
  103. package/src/__tests__/newUserRolesDefault.test.ts +81 -0
  104. package/src/__tests__/newUsers.test.ts +36 -14
  105. package/src/__tests__/principals.callSites.test.ts +112 -0
  106. package/src/__tests__/principals.logging.test.ts +160 -0
  107. package/src/__tests__/principals.test.ts +305 -0
  108. package/src/__tests__/resolverBase.fls.test.ts +483 -0
  109. package/src/__tests__/resolverBase.recordChangesFls.test.ts +358 -0
  110. package/src/__tests__/resolverBase.refusalExtensions.test.ts +101 -0
  111. package/src/__tests__/resolverBase.refusalMessage.test.ts +88 -0
  112. package/src/__tests__/serverExtensionReservedRoots.test.ts +79 -11
  113. package/src/__tests__/visitorIdentity.resolve.test.ts +61 -0
  114. package/src/agentSessions/SessionJanitor.ts +2 -2
  115. package/src/auth/exampleNewUserSubClass.ts +9 -6
  116. package/src/auth/magicLink/MagicLinkService.ts +2 -8
  117. package/src/auth/newUsers.ts +7 -15
  118. package/src/auth/principals.ts +257 -0
  119. package/src/config.ts +44 -4
  120. package/src/entitySubclasses/MJEntityPermissionEntityServer.server.ts +65 -6
  121. package/src/generated/graphql-schemas/__mj.ts +18966 -16452
  122. package/src/generic/ResolverBase.ts +467 -9
  123. package/src/generic/RunViewResolver.ts +28 -2
  124. package/src/generic/refusalExtensions.ts +59 -0
  125. package/src/index.ts +154 -102
  126. package/src/realtimeWidget/WidgetSessionService.ts +2 -14
  127. package/src/realtimeWidget/visitorIdentity.ts +21 -6
  128. package/src/resolvers/EntityRecordNameResolver.ts +74 -8
  129. package/src/resolvers/FileCategoryResolver.ts +2 -3
  130. package/src/resolvers/IntegrationDiscoveryResolver.ts +2 -0
  131. package/src/resolvers/QuerySystemUserResolver.ts +1 -1
  132. package/src/resolvers/RealtimeClientSessionResolver.ts +128 -2
  133. package/src/resolvers/RemoteBrowserActionResolver.ts +47 -8
  134. package/src/resolvers/SyncRolesUsersResolver.ts +11 -2
  135. package/src/resolvers/UserViewResolver.ts +8 -4
  136. package/src/rest/EntityCRUDHandler.ts +3 -19
  137. package/src/rest/RESTEndpointHandler.ts +3 -17
  138. package/src/rest/RealtimeSdpBrokerHandler.ts +101 -0
  139. package/src/serverExtensionReservedRoots.ts +2 -3
  140. package/dist/resolvers/RingCentralTelephonyResolver.d.ts +0 -27
  141. package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +0 -1
  142. package/dist/resolvers/RingCentralTelephonyResolver.js +0 -87
  143. package/dist/resolvers/RingCentralTelephonyResolver.js.map +0 -1
  144. package/dist/resolvers/TeamsMeetingsResolver.d.ts +0 -28
  145. package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +0 -1
  146. package/dist/resolvers/TeamsMeetingsResolver.js +0 -91
  147. package/dist/resolvers/TeamsMeetingsResolver.js.map +0 -1
  148. package/dist/resolvers/TelephonyResolver.d.ts +0 -26
  149. package/dist/resolvers/TelephonyResolver.d.ts.map +0 -1
  150. package/dist/resolvers/TelephonyResolver.js +0 -86
  151. package/dist/resolvers/TelephonyResolver.js.map +0 -1
  152. package/dist/resolvers/VonageTelephonyResolver.d.ts +0 -26
  153. package/dist/resolvers/VonageTelephonyResolver.d.ts.map +0 -1
  154. package/dist/resolvers/VonageTelephonyResolver.js +0 -86
  155. package/dist/resolvers/VonageTelephonyResolver.js.map +0 -1
  156. package/dist/telephony/RingCentralTelephonyService.d.ts +0 -115
  157. package/dist/telephony/RingCentralTelephonyService.d.ts.map +0 -1
  158. package/dist/telephony/RingCentralTelephonyService.js +0 -262
  159. package/dist/telephony/RingCentralTelephonyService.js.map +0 -1
  160. package/dist/telephony/TeamsMeetingsRouter.d.ts +0 -40
  161. package/dist/telephony/TeamsMeetingsRouter.d.ts.map +0 -1
  162. package/dist/telephony/TeamsMeetingsRouter.js +0 -96
  163. package/dist/telephony/TeamsMeetingsRouter.js.map +0 -1
  164. package/dist/telephony/TeamsMeetingsService.d.ts +0 -113
  165. package/dist/telephony/TeamsMeetingsService.d.ts.map +0 -1
  166. package/dist/telephony/TeamsMeetingsService.js +0 -196
  167. package/dist/telephony/TeamsMeetingsService.js.map +0 -1
  168. package/dist/telephony/TwilioTelephonyRouter.d.ts +0 -36
  169. package/dist/telephony/TwilioTelephonyRouter.d.ts.map +0 -1
  170. package/dist/telephony/TwilioTelephonyRouter.js +0 -143
  171. package/dist/telephony/TwilioTelephonyRouter.js.map +0 -1
  172. package/dist/telephony/TwilioTelephonyService.d.ts +0 -95
  173. package/dist/telephony/TwilioTelephonyService.d.ts.map +0 -1
  174. package/dist/telephony/TwilioTelephonyService.js +0 -193
  175. package/dist/telephony/TwilioTelephonyService.js.map +0 -1
  176. package/dist/telephony/VonageTelephonyRouter.d.ts +0 -41
  177. package/dist/telephony/VonageTelephonyRouter.d.ts.map +0 -1
  178. package/dist/telephony/VonageTelephonyRouter.js +0 -201
  179. package/dist/telephony/VonageTelephonyRouter.js.map +0 -1
  180. package/dist/telephony/VonageTelephonyService.d.ts +0 -90
  181. package/dist/telephony/VonageTelephonyService.d.ts.map +0 -1
  182. package/dist/telephony/VonageTelephonyService.js +0 -191
  183. package/dist/telephony/VonageTelephonyService.js.map +0 -1
  184. package/dist/telephony/calendar-scheduler.d.ts +0 -67
  185. package/dist/telephony/calendar-scheduler.d.ts.map +0 -1
  186. package/dist/telephony/calendar-scheduler.js +0 -133
  187. package/dist/telephony/calendar-scheduler.js.map +0 -1
  188. package/dist/telephony/index.d.ts +0 -29
  189. package/dist/telephony/index.d.ts.map +0 -1
  190. package/dist/telephony/index.js +0 -38
  191. package/dist/telephony/index.js.map +0 -1
  192. package/dist/telephony/media-upgrade-router.d.ts +0 -47
  193. package/dist/telephony/media-upgrade-router.d.ts.map +0 -1
  194. package/dist/telephony/media-upgrade-router.js +0 -72
  195. package/dist/telephony/media-upgrade-router.js.map +0 -1
  196. package/dist/telephony/ringcentral-runtime.d.ts +0 -14
  197. package/dist/telephony/ringcentral-runtime.d.ts.map +0 -1
  198. package/dist/telephony/ringcentral-runtime.js +0 -18
  199. package/dist/telephony/ringcentral-runtime.js.map +0 -1
  200. package/dist/telephony/teams-meetings-runtime.d.ts +0 -16
  201. package/dist/telephony/teams-meetings-runtime.d.ts.map +0 -1
  202. package/dist/telephony/teams-meetings-runtime.js +0 -20
  203. package/dist/telephony/teams-meetings-runtime.js.map +0 -1
  204. package/dist/telephony/teamsAcsMediaRegistry.d.ts +0 -69
  205. package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +0 -1
  206. package/dist/telephony/teamsAcsMediaRegistry.js +0 -118
  207. package/dist/telephony/teamsAcsMediaRegistry.js.map +0 -1
  208. package/dist/telephony/telephony-runtime.d.ts +0 -14
  209. package/dist/telephony/telephony-runtime.d.ts.map +0 -1
  210. package/dist/telephony/telephony-runtime.js +0 -18
  211. package/dist/telephony/telephony-runtime.js.map +0 -1
  212. package/dist/telephony/twilioMediaRegistry.d.ts +0 -60
  213. package/dist/telephony/twilioMediaRegistry.d.ts.map +0 -1
  214. package/dist/telephony/twilioMediaRegistry.js +0 -106
  215. package/dist/telephony/twilioMediaRegistry.js.map +0 -1
  216. package/dist/telephony/vonage-runtime.d.ts +0 -14
  217. package/dist/telephony/vonage-runtime.d.ts.map +0 -1
  218. package/dist/telephony/vonage-runtime.js +0 -18
  219. package/dist/telephony/vonage-runtime.js.map +0 -1
  220. package/dist/telephony/vonageMediaRegistry.d.ts +0 -78
  221. package/dist/telephony/vonageMediaRegistry.d.ts.map +0 -1
  222. package/dist/telephony/vonageMediaRegistry.js +0 -158
  223. package/dist/telephony/vonageMediaRegistry.js.map +0 -1
  224. package/src/__tests__/media-upgrade-router.test.ts +0 -43
  225. package/src/__tests__/teams-meetings-service.test.ts +0 -65
  226. package/src/__tests__/teamsAcsMediaRegistry.test.ts +0 -82
  227. package/src/__tests__/twilio-telephony-service.test.ts +0 -23
  228. package/src/__tests__/twilioMediaRegistry.test.ts +0 -103
  229. package/src/__tests__/vonageMediaRegistry.test.ts +0 -180
  230. package/src/resolvers/RingCentralTelephonyResolver.ts +0 -64
  231. package/src/resolvers/TeamsMeetingsResolver.ts +0 -68
  232. package/src/resolvers/TelephonyResolver.ts +0 -63
  233. package/src/resolvers/VonageTelephonyResolver.ts +0 -63
  234. package/src/telephony/RingCentralTelephonyService.ts +0 -342
  235. package/src/telephony/TeamsMeetingsRouter.ts +0 -124
  236. package/src/telephony/TeamsMeetingsService.ts +0 -268
  237. package/src/telephony/TwilioTelephonyRouter.ts +0 -184
  238. package/src/telephony/TwilioTelephonyService.ts +0 -261
  239. package/src/telephony/VonageTelephonyRouter.ts +0 -239
  240. package/src/telephony/VonageTelephonyService.ts +0 -259
  241. package/src/telephony/calendar-scheduler.ts +0 -176
  242. package/src/telephony/index.ts +0 -43
  243. package/src/telephony/media-upgrade-router.ts +0 -79
  244. package/src/telephony/ringcentral-runtime.ts +0 -22
  245. package/src/telephony/teams-meetings-runtime.ts +0 -24
  246. package/src/telephony/teamsAcsMediaRegistry.ts +0 -152
  247. package/src/telephony/telephony-runtime.ts +0 -22
  248. package/src/telephony/twilioMediaRegistry.ts +0 -137
  249. package/src/telephony/vonage-runtime.ts +0 -22
  250. package/src/telephony/vonageMediaRegistry.ts +0 -200
@@ -21,7 +21,7 @@ import type { UserInfo } from '@memberjunction/core';
21
21
  const {
22
22
  mockConfig,
23
23
  mockCacheUsers,
24
- mockUserByName,
24
+ mockSystemUserId,
25
25
  mockRefresh,
26
26
  mockGetEntityObject,
27
27
  mockRunViewFn,
@@ -39,6 +39,7 @@ const {
39
39
  Name: string;
40
40
  Email: string;
41
41
  Type: string;
42
+ IsActive?: boolean;
42
43
  }
43
44
  return {
44
45
  mockConfig: {
@@ -55,7 +56,11 @@ const {
55
56
  },
56
57
  },
57
58
  mockCacheUsers: [] as HoistedCacheUser[],
58
- mockUserByName: vi.fn(),
59
+ // The system user's ID as the cache reports it. A well-formed UUID that matches NO fixture
60
+ // below, so the resolver's system rung stays inert unless a test opts into it. Well-formed
61
+ // matters: `UUIDsEqual` is a string compare with no shape check, so a malformed value here
62
+ // would read as real data while being unrepresentable in the column it stands for.
63
+ mockSystemUserId: { value: '00000000-0000-0000-0000-00000000d1f5' },
59
64
  mockRefresh: vi.fn(),
60
65
  mockGetEntityObject: vi.fn(),
61
66
  mockRunViewFn: vi.fn(),
@@ -76,10 +81,12 @@ vi.mock('../config.js', () => ({ configInfo: mockConfig }));
76
81
 
77
82
  vi.mock('@memberjunction/generic-database-provider', () => {
78
83
  const instance = {
79
- UserByName: mockUserByName,
80
84
  get Users() {
81
85
  return mockCacheUsers;
82
86
  },
87
+ get SYSTEM_USER_ID() {
88
+ return mockSystemUserId.value;
89
+ },
83
90
  Refresh: mockRefresh,
84
91
  GetSystemUser: vi.fn(),
85
92
  };
@@ -234,7 +241,9 @@ function entitiesOf(name: string): MockEntity[] {
234
241
  return createdEntities.filter((e) => e.TestEntityName === name);
235
242
  }
236
243
 
237
- const CONTEXT_USER = { ID: 'sys-1', Name: 'system@test.com', Email: 'system@test.com', Type: 'Owner' } as unknown as UserInfo;
244
+ /** The cache row for the configured context user; `CONTEXT_USER` is the same object, typed. */
245
+ const CONTEXT_USER_ROW = { ID: 'sys-1', Name: 'system@test.com', Email: 'system@test.com', Type: 'Owner', IsActive: true };
246
+ const CONTEXT_USER = CONTEXT_USER_ROW as unknown as UserInfo;
238
247
 
239
248
  function resetConfig(): void {
240
249
  mockConfig.userHandling = {
@@ -261,14 +270,22 @@ beforeEach(() => {
261
270
  getEntityObjectCalls = [];
262
271
  saveBehavior = {};
263
272
  entitySeq = 0;
273
+ // defineProperty rather than assignment: one test replaces this with a throwing getter.
274
+ Object.defineProperty(mockSystemUserId, 'value', {
275
+ value: '00000000-0000-0000-0000-00000000d1f5',
276
+ writable: true,
277
+ configurable: true,
278
+ });
279
+ // The configured context user must be IN the cache: resolution reads the cache directly rather
280
+ // than going through a UserByName stub that answered regardless of what the cache held.
264
281
  mockCacheUsers.length = 0;
282
+ mockCacheUsers.push(CONTEXT_USER_ROW);
265
283
 
266
284
  mockRolesArray.length = 0;
267
285
  mockRolesArray.push({ ID: 'role-ui', Name: 'UI' }, { ID: 'role-dev', Name: 'Developer' });
268
286
  mockApplicationsArray.length = 0;
269
287
  mockApplicationsArray.push({ ID: 'app-crm', Name: ' CRM ' }, { ID: 'app-admin', Name: 'Admin' });
270
288
 
271
- mockUserByName.mockImplementation((name: string) => (name === 'system@test.com' ? CONTEXT_USER : undefined));
272
289
  mockGetEntityObject.mockImplementation(async (entityName: string, contextUser?: UserInfo) => {
273
290
  const entity = makeMockEntity(entityName);
274
291
  createdEntities.push(entity);
@@ -290,7 +307,6 @@ describe('NewUserBase.createNewUser', () => {
290
307
  const user = await create();
291
308
 
292
309
  expect(user).not.toBeNull();
293
- expect(mockUserByName).toHaveBeenCalledWith('system@test.com');
294
310
  expect(getEntityObjectCalls[0]).toEqual({ entityName: 'MJ: Users', contextUser: CONTEXT_USER });
295
311
  // Role records are scoped to the same creation context user
296
312
  for (const call of getEntityObjectCalls) {
@@ -299,9 +315,9 @@ describe('NewUserBase.createNewUser', () => {
299
315
  });
300
316
 
301
317
  it('falls back to an Owner-typed cache user (case-insensitive, trimmed) when the configured user is missing', async () => {
302
- mockUserByName.mockReturnValue(undefined);
303
- const owner = { ID: 'owner-9', Name: 'Fallback Owner', Email: 'owner@x.com', Type: ' OWNER ' };
304
- mockCacheUsers.push({ ID: 'u-1', Name: 'Plain', Email: 'p@x.com', Type: 'User' }, owner);
318
+ const owner = { ID: 'owner-9', Name: 'Fallback Owner', Email: 'owner@x.com', Type: ' OWNER ', IsActive: true };
319
+ mockCacheUsers.length = 0; // the configured user is absent from this deployment
320
+ mockCacheUsers.push({ ID: 'u-1', Name: 'Plain', Email: 'p@x.com', Type: 'User', IsActive: true }, owner);
305
321
 
306
322
  const user = await create();
307
323
 
@@ -311,8 +327,8 @@ describe('NewUserBase.createNewUser', () => {
311
327
  });
312
328
 
313
329
  it('returns null without opening a transaction when no context user can be resolved at all', async () => {
314
- mockUserByName.mockReturnValue(undefined);
315
- mockCacheUsers.push({ ID: 'u-1', Name: 'Plain', Email: 'p@x.com', Type: 'User' });
330
+ mockCacheUsers.length = 0; // no configured user, no system user, no Owner
331
+ mockCacheUsers.push({ ID: 'u-1', Name: 'Plain', Email: 'p@x.com', Type: 'User', IsActive: true });
316
332
 
317
333
  const user = await create();
318
334
 
@@ -322,8 +338,12 @@ describe('NewUserBase.createNewUser', () => {
322
338
  });
323
339
 
324
340
  it('returns undefined (outer catch) when context resolution throws', async () => {
325
- mockUserByName.mockImplementation(() => {
326
- throw new Error('cache exploded');
341
+ // A cache that cannot report its own system user id fails resolution outright.
342
+ Object.defineProperty(mockSystemUserId, 'value', {
343
+ get() {
344
+ throw new Error('cache exploded');
345
+ },
346
+ configurable: true,
327
347
  });
328
348
 
329
349
  const user = await create();
@@ -604,7 +624,9 @@ describe('verifyUserRecord', () => {
604
624
  const user = await verifyUserRecord('new@example.com', 'New', 'User');
605
625
 
606
626
  expect(user).toBeUndefined();
607
- expect(mockCacheUsers).toHaveLength(0);
627
+ // The cache is no longer empty at rest (it holds the configured context user), so
628
+ // assert the thing that actually matters: the failed user was not added to it.
629
+ expect(mockCacheUsers.some((u) => u.Email === 'new@example.com')).toBe(false);
608
630
  });
609
631
  });
610
632
 
@@ -0,0 +1,112 @@
1
+ /**
2
+ * There is ONE resolver for the three config-named context users — issue #4209, second half.
3
+ *
4
+ * The bug was not that a lookup used the wrong column; it was that the lookup was written out by
5
+ * hand at every consumer. Six copies existed across MJServer in three mutually inconsistent
6
+ * variants, and two of them could not resolve the value MJ itself ships. `auth/principals.ts`
7
+ * exists to make that impossible, and this test is what keeps it that way: any file that reads
8
+ * one of the three settings must go through the ladder rather than growing a fourth variant.
9
+ *
10
+ * Source-scanning rather than behavioural because the failure mode is *a new call site*, which no
11
+ * behavioural test of the existing call sites can see. Same technique as
12
+ * `AdhocQueryResolver.scopeGuard.test.ts` and `serverExtensionReservedRoots.test.ts`.
13
+ */
14
+ import { describe, it, expect } from 'vitest';
15
+ import { readdirSync, readFileSync, statSync } from 'fs';
16
+ import { dirname, join, relative, sep } from 'path';
17
+ import { fileURLToPath } from 'url';
18
+
19
+ const SRC_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
20
+
21
+ /** The three settings that name a principal in config. `config.ts` DEFINES them; it does not resolve them. */
22
+ const CONTEXT_USER_SETTINGS = [
23
+ 'contextUserForNewUserCreation',
24
+ 'contextUserForProvisioning',
25
+ 'contextUserForLookup',
26
+ ] as const;
27
+
28
+ /**
29
+ * Files that legitimately mention a setting without resolving it, as paths RELATIVE TO `src/`.
30
+ * Deliberately not basenames: exempting every file called `config.ts` anywhere under `src/` would
31
+ * let a future `realtimeWidget/config.ts` hand-roll a lookup and still pass this guard.
32
+ */
33
+ const NOT_CONSUMERS = ['config.ts', 'auth/principals.ts'];
34
+
35
+ /** `src/`-relative, forward-slashed, so the assertions below read the same on every platform. */
36
+ function relativeToSrc(absolutePath: string): string {
37
+ return relative(SRC_ROOT, absolutePath).split(sep).join('/');
38
+ }
39
+
40
+ function everyTsFileUnder(dir: string): string[] {
41
+ return readdirSync(dir).flatMap((name) => {
42
+ const full = join(dir, name);
43
+ if (statSync(full).isDirectory()) {
44
+ return name === '__tests__' || name === 'generated' ? [] : everyTsFileUnder(full);
45
+ }
46
+ return name.endsWith('.ts') && !name.endsWith('.d.ts') ? [full] : [];
47
+ });
48
+ }
49
+
50
+ /** Files that READ one of the settings — i.e. resolve it to a user, rather than declaring it. */
51
+ function consumersOfAContextUserSetting(): { path: string; source: string }[] {
52
+ return everyTsFileUnder(SRC_ROOT)
53
+ .filter((p) => !NOT_CONSUMERS.includes(relativeToSrc(p)))
54
+ .map((path) => ({ path: relativeToSrc(path), source: readFileSync(path, 'utf8') }))
55
+ .filter(({ source }) => CONTEXT_USER_SETTINGS.some((s) => source.includes(`.${s}`)));
56
+ }
57
+
58
+ describe('context-user resolution has exactly one implementation', () => {
59
+ it('finds the known consumers, so the scan itself is not silently matching nothing', () => {
60
+ const found = consumersOfAContextUserSetting().map((c) => c.path);
61
+
62
+ expect(found).toContain('auth/newUsers.ts');
63
+ expect(found).toContain('auth/magicLink/MagicLinkService.ts');
64
+ });
65
+
66
+ it('routes every consumer through auth/principals.ts', () => {
67
+ const handRolled = consumersOfAContextUserSetting()
68
+ .filter(({ source }) => !source.includes('ResolveConfiguredPrincipal'))
69
+ .map(({ path }) => path);
70
+
71
+ expect(handRolled).toEqual([]);
72
+ });
73
+
74
+ it('leaves no consumer matching the setting against a single column by hand', () => {
75
+ // The two shapes that produced #4209: `UserByName(candidate)` and `u.Email === candidate`.
76
+ //
77
+ // Scoped to the statement that READS the setting, not the whole file: MagicLinkService
78
+ // also resolves the INVITEE by Email (`provisionUser`, ~270 lines away), which is tier-1
79
+ // identity and correct. A file-wide grep would condemn it, and a guard that cries wolf
80
+ // gets deleted.
81
+ const COLUMN_MATCH = /UserByName\s*\(|\.(Email|Name)\b[^\n]*===/;
82
+ const handRolled = consumersOfAContextUserSetting()
83
+ .filter(({ source }) =>
84
+ source.split('\n').some(
85
+ (line) => CONTEXT_USER_SETTINGS.some((s) => line.includes(`.${s}`)) && COLUMN_MATCH.test(line),
86
+ ),
87
+ )
88
+ .map(({ path }) => path);
89
+
90
+ expect(handRolled).toEqual([]);
91
+ });
92
+ });
93
+
94
+ describe('the ladder is reachable by the integrators the example is written for', () => {
95
+ // `exampleNewUserSubClass.ts` exists to be COPIED into a deployment's own package — its header
96
+ // says so, and it tells you to uncomment `@RegisterClass` in your code. Now that it resolves
97
+ // through `ResolveConfiguredPrincipal`, an integrator who copies it needs to be able to import
98
+ // that. `package.json` publishes only `"."`, so the package index is the whole public surface,
99
+ // and this guard cannot see downstream code — it can only make sure the door exists.
100
+ it('exports ResolveConfiguredPrincipal from the package index', () => {
101
+ const index = readFileSync(join(SRC_ROOT, 'index.ts'), 'utf8');
102
+
103
+ expect(index).toMatch(/ResolveConfiguredPrincipal/);
104
+ });
105
+
106
+ it('exports the types a caller needs to hold the result', () => {
107
+ const index = readFileSync(join(SRC_ROOT, 'index.ts'), 'utf8');
108
+
109
+ expect(index).toMatch(/ResolvablePrincipal/);
110
+ expect(index).toMatch(/PrincipalResolution/);
111
+ });
112
+ });
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Log policy for context-user resolution — the second half of issue #4209.
3
+ *
4
+ * The defect had two costs. One was attribution (covered by principals.test.ts). The other was
5
+ * volume: because the shipped default could never resolve, EVERY magic-link redeem and EVERY
6
+ * auto-provisioned user emitted an error-level line, on every host that took the default. Real
7
+ * errors were buried under it.
8
+ *
9
+ * Resolution is a pure function; this file covers the impure shell around it — reading the user
10
+ * cache and deciding how often a misconfiguration is worth saying out loud.
11
+ */
12
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
13
+
14
+ const { mockLogError, mockUsers, SYSTEM_USER_ID } = vi.hoisted(() => ({
15
+ mockLogError: vi.fn(),
16
+ mockUsers: [] as Array<Record<string, unknown>>,
17
+ SYSTEM_USER_ID: 'ECAFCCEC-6A37-EF11-86D4-000D3A4E707E',
18
+ }));
19
+
20
+ vi.mock('@memberjunction/core', () => ({
21
+ LogError: mockLogError,
22
+ UserInfo: class {},
23
+ }));
24
+
25
+ vi.mock('@memberjunction/generic-database-provider', () => ({
26
+ UserCache: {
27
+ get Users() {
28
+ return mockUsers;
29
+ },
30
+ Instance: {
31
+ get Users() {
32
+ return mockUsers;
33
+ },
34
+ get SYSTEM_USER_ID() {
35
+ return SYSTEM_USER_ID;
36
+ },
37
+ },
38
+ },
39
+ }));
40
+
41
+ /** Stock seed: the system user MJ's own default is aiming at, plus a human Owner. */
42
+ const SYSTEM = { ID: SYSTEM_USER_ID, Name: 'System', Email: 'not.set@nowhere.com', Type: 'Owner ', IsActive: true };
43
+ const HOST_ADMIN = { ID: 'AAAA1111-0000-0000-0000-000000000001', Name: 'jane.admin@acme.com', Email: 'jane.admin@acme.com', Type: 'Owner', IsActive: true };
44
+
45
+ /** Fresh module per test: the de-duplication state is module-scoped by design. */
46
+ async function loadSubject() {
47
+ vi.resetModules();
48
+ return import('../auth/principals.js');
49
+ }
50
+
51
+ beforeEach(() => {
52
+ vi.clearAllMocks();
53
+ mockUsers.length = 0;
54
+ mockUsers.push(SYSTEM, HOST_ADMIN);
55
+ });
56
+
57
+ describe('ResolveConfiguredPrincipal', () => {
58
+ it('resolves MJ\'s own shipped default without logging anything', async () => {
59
+ const { ResolveConfiguredPrincipal } = await loadSubject();
60
+
61
+ const user = ResolveConfiguredPrincipal('not.set@nowhere.com', 'MagicLink');
62
+
63
+ expect(user).toBe(SYSTEM);
64
+ expect(mockLogError).not.toHaveBeenCalled();
65
+ });
66
+
67
+ it('logs an unresolvable candidate ONCE however many times it is asked for', async () => {
68
+ const { ResolveConfiguredPrincipal } = await loadSubject();
69
+
70
+ for (let i = 0; i < 25; i++) {
71
+ ResolveConfiguredPrincipal('ghost@nowhere.example', 'MagicLink');
72
+ }
73
+
74
+ expect(mockLogError).toHaveBeenCalledTimes(1);
75
+ });
76
+
77
+ it('names both the purpose and the candidate so the operator can find the setting', async () => {
78
+ const { ResolveConfiguredPrincipal } = await loadSubject();
79
+
80
+ ResolveConfiguredPrincipal('ghost@nowhere.example', 'MagicLink');
81
+
82
+ const logged = String(mockLogError.mock.calls[0][0]);
83
+ expect(logged).toContain('MagicLink');
84
+ expect(logged).toContain('ghost@nowhere.example');
85
+ });
86
+
87
+ it('still reports a DIFFERENT misconfiguration rather than swallowing it', async () => {
88
+ const { ResolveConfiguredPrincipal } = await loadSubject();
89
+
90
+ ResolveConfiguredPrincipal('ghost@nowhere.example', 'MagicLink');
91
+ ResolveConfiguredPrincipal('other@nowhere.example', 'MagicLink');
92
+
93
+ expect(mockLogError).toHaveBeenCalledTimes(2);
94
+ });
95
+
96
+ it('reports the same candidate separately for a different purpose', async () => {
97
+ const { ResolveConfiguredPrincipal } = await loadSubject();
98
+
99
+ ResolveConfiguredPrincipal('ghost@nowhere.example', 'MagicLink');
100
+ ResolveConfiguredPrincipal('ghost@nowhere.example', 'Widget');
101
+
102
+ expect(mockLogError).toHaveBeenCalledTimes(2);
103
+ });
104
+
105
+ it('does not treat an unset candidate as a misconfiguration', async () => {
106
+ const { ResolveConfiguredPrincipal } = await loadSubject();
107
+
108
+ const user = ResolveConfiguredPrincipal(undefined, 'NewUser');
109
+
110
+ expect(user).toBe(SYSTEM);
111
+ expect(mockLogError).not.toHaveBeenCalled();
112
+ });
113
+
114
+ it('returns null and says so when the deployment offers no principal at all', async () => {
115
+ mockUsers.length = 0;
116
+ const { ResolveConfiguredPrincipal } = await loadSubject();
117
+
118
+ const user = ResolveConfiguredPrincipal('ghost@nowhere.example', 'MagicLink');
119
+
120
+ expect(user).toBeNull();
121
+ expect(mockLogError).toHaveBeenCalledTimes(1);
122
+ });
123
+ });
124
+
125
+ describe('the misconfiguration report stays bounded WITHOUT regressing to per-request logging', () => {
126
+ it('publishes its bound, so this test cannot silently pass against a different one', async () => {
127
+ const { MAX_REPORTED_MISCONFIGURATIONS } = await loadSubject();
128
+
129
+ expect(typeof MAX_REPORTED_MISCONFIGURATIONS).toBe('number');
130
+ expect(MAX_REPORTED_MISCONFIGURATIONS).toBeGreaterThan(0);
131
+ });
132
+
133
+ it('still says a NEW misconfiguration exactly once after the bound is reached', async () => {
134
+ // The bound exists so a caller passing a per-request candidate cannot grow the tracker
135
+ // forever. But whatever it does at the limit must not be "log every time" — that is
136
+ // precisely the per-request error line issue #4209 was filed about, reintroduced for the
137
+ // one caller the bound was added for.
138
+ const { ResolveConfiguredPrincipal, MAX_REPORTED_MISCONFIGURATIONS } = await loadSubject();
139
+ expect(typeof MAX_REPORTED_MISCONFIGURATIONS).toBe('number'); // else the loop below is a no-op and this passes for free
140
+ for (let i = 0; i < MAX_REPORTED_MISCONFIGURATIONS + 5; i++) {
141
+ ResolveConfiguredPrincipal(`filler-${i}@nowhere.example`, 'MagicLink');
142
+ }
143
+ mockLogError.mockClear();
144
+
145
+ ResolveConfiguredPrincipal('late@nowhere.example', 'MagicLink');
146
+ ResolveConfiguredPrincipal('late@nowhere.example', 'MagicLink');
147
+ ResolveConfiguredPrincipal('late@nowhere.example', 'MagicLink');
148
+
149
+ expect(mockLogError).toHaveBeenCalledTimes(1);
150
+ });
151
+
152
+ it('does not grow without bound', async () => {
153
+ const { ResolveConfiguredPrincipal, ReportedMisconfigurationCount, MAX_REPORTED_MISCONFIGURATIONS } = await loadSubject();
154
+ for (let i = 0; i < MAX_REPORTED_MISCONFIGURATIONS * 3; i++) {
155
+ ResolveConfiguredPrincipal(`filler-${i}@nowhere.example`, 'MagicLink');
156
+ }
157
+
158
+ expect(ReportedMisconfigurationCount()).toBeLessThanOrEqual(MAX_REPORTED_MISCONFIGURATIONS);
159
+ });
160
+ });