@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
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Resolution of an operator-named context user — regression suite for issue #4209.
3
+ *
4
+ * MJServer has three settings that name a user in config (`contextUserForNewUserCreation`,
5
+ * `contextUserForProvisioning`, `contextUserForLookup`). All three were resolved through
6
+ * `UserCache.UserByName`, which matches `User.Name` — but the shipped default for the first is
7
+ * `'not.set@nowhere.com'`, an EMAIL. On a stock database the seeded system user is
8
+ * `Name='System'` / `Email='not.set@nowhere.com'`, so the default names the very user it was
9
+ * aiming at and can never reach it. Every magic-link redeem logged an error and provisioned
10
+ * under whichever user happened to sort first as an Owner.
11
+ *
12
+ * These tests drive the pure core of the resolver, so they need neither the `UserCache`
13
+ * singleton nor a database — the seam is `(candidate, users) -> resolution`.
14
+ */
15
+ import { describe, it, expect } from 'vitest';
16
+ import { resolvePrincipalFrom, type ResolvablePrincipal } from '../auth/principals.js';
17
+
18
+ /**
19
+ * The system user's ID, pinned by `UserCache` (`SYSTEM_USER_ID`) and seeded by every baseline.
20
+ * Spelled in the mixed case the migration writes to prove the match is case-insensitive.
21
+ */
22
+ const SYSTEM_USER_ID = 'ECAFCCEC-6A37-EF11-86D4-000D3A4E707E';
23
+
24
+ /**
25
+ * The shipped default for `userHandling.contextUserForNewUserCreation` — see `src/config.ts`.
26
+ * It is email-shaped, which is the whole defect: it is matched against `Name`.
27
+ */
28
+ const SHIPPED_DEFAULT = 'not.set@nowhere.com';
29
+
30
+ /**
31
+ * Rows exactly as seeded by `migrations/v5/B202607091514__v5.46.x__Baseline.sql`. `Type` is
32
+ * `NCHAR(15)`, so the seeded values carry trailing padding — the resolver must trim before
33
+ * comparing, and these fixtures keep the padding so that stays tested.
34
+ */
35
+ const SYSTEM: ResolvablePrincipal = {
36
+ ID: SYSTEM_USER_ID,
37
+ Name: 'System',
38
+ Email: 'not.set@nowhere.com',
39
+ Type: 'Owner ',
40
+ IsActive: true,
41
+ };
42
+
43
+ const ANONYMOUS: ResolvablePrincipal = {
44
+ ID: '273910DF-28F1-45C1-A8F8-6E9AD8E5F008',
45
+ Name: 'Anonymous',
46
+ Email: 'anonymous@magic-link.local',
47
+ Type: 'User ',
48
+ IsActive: true,
49
+ };
50
+
51
+ /** A human admin, as every real host has: created by MJ, so `Name` IS the email address. */
52
+ const HOST_ADMIN: ResolvablePrincipal = {
53
+ ID: 'AAAA1111-0000-0000-0000-000000000001',
54
+ Name: 'jane.admin@acme.com',
55
+ Email: 'jane.admin@acme.com',
56
+ Type: 'Owner',
57
+ IsActive: true,
58
+ };
59
+
60
+ /** The stock seed plus one human admin — the shape of an ordinary deployment. */
61
+ const STOCK_HOST: ResolvablePrincipal[] = [SYSTEM, ANONYMOUS, HOST_ADMIN];
62
+
63
+ describe('resolvePrincipalFrom', () => {
64
+ describe('the shipped default (#4209)', () => {
65
+ it('resolves the email-shaped default to the seeded System user', () => {
66
+ const result = resolvePrincipalFrom(SHIPPED_DEFAULT, STOCK_HOST, SYSTEM_USER_ID);
67
+
68
+ expect(result.user).toBe(SYSTEM);
69
+ expect(result.reason).toBe('email');
70
+ });
71
+
72
+ it('reports no warning when the default resolves, so a stock host logs nothing', () => {
73
+ const result = resolvePrincipalFrom(SHIPPED_DEFAULT, STOCK_HOST, SYSTEM_USER_ID);
74
+
75
+ expect(result.warning).toBeUndefined();
76
+ });
77
+ });
78
+
79
+ describe('attribution determinism', () => {
80
+ it('resolves identically however the user cache happens to be ordered', () => {
81
+ // `SELECT * FROM vwUsers` carries no ORDER BY and the cache is mutated in place at
82
+ // runtime, so row order differs between boots AND within a process. Attribution must
83
+ // not depend on it: this is what made CreatedByUserID incidental.
84
+ const ordered = resolvePrincipalFrom(SHIPPED_DEFAULT, STOCK_HOST, SYSTEM_USER_ID);
85
+ const reversed = resolvePrincipalFrom(SHIPPED_DEFAULT, [...STOCK_HOST].reverse(), SYSTEM_USER_ID);
86
+
87
+ expect(reversed.user).toBe(ordered.user);
88
+ });
89
+ });
90
+
91
+ describe('explicitly configured principals', () => {
92
+ it('keeps a Name match ahead of another user whose Email is that same string', () => {
93
+ // The compatibility guarantee: a host that resolves today must resolve to the SAME
94
+ // user after the Email fallback is added. Name is tried first for exactly this reason.
95
+ const namedSupport: ResolvablePrincipal = {
96
+ ID: 'BBBB2222-0000-0000-0000-000000000002',
97
+ Name: 'support@acme.com',
98
+ Email: 'someone.else@acme.com',
99
+ Type: 'User',
100
+ IsActive: true,
101
+ };
102
+ const emailSupport: ResolvablePrincipal = {
103
+ ID: 'CCCC3333-0000-0000-0000-000000000003',
104
+ Name: 'Bob Support',
105
+ Email: 'support@acme.com',
106
+ Type: 'User',
107
+ IsActive: true,
108
+ };
109
+
110
+ const result = resolvePrincipalFrom('support@acme.com', [emailSupport, namedSupport], SYSTEM_USER_ID);
111
+
112
+ expect(result.user).toBe(namedSupport);
113
+ expect(result.reason).toBe('name');
114
+ });
115
+ });
116
+
117
+ describe('duplicate matches resolve deterministically', () => {
118
+ // `User.Name` carries NO unique constraint. MJ's own rows do not collide — `NewUserBase`
119
+ // sets `Name = email` and `UQ_User_Email` makes Email unique, so MJ-created Names are
120
+ // unique BY CONSEQUENCE, not by rule — but a hand-created or externally-synced row can
121
+ // collide, and then `find()` returns whichever the cache happens to hold first. That is
122
+ // the same cache-order dependence the Owner rung is sorted to remove, so every rung
123
+ // resolves ties the same way: lowest ID.
124
+ const LOW: ResolvablePrincipal = { ID: '11111111-0000-0000-0000-000000000001', Name: 'shared.name', Email: 'low@acme.com', Type: 'User', IsActive: true };
125
+ const HIGH: ResolvablePrincipal = { ID: 'FFFFFFFF-0000-0000-0000-00000000000F', Name: 'shared.name', Email: 'high@acme.com', Type: 'User', IsActive: true };
126
+
127
+ it('picks the lowest-ID user when two users share the configured Name', () => {
128
+ const result = resolvePrincipalFrom('shared.name', [HIGH, LOW, SYSTEM], SYSTEM_USER_ID);
129
+
130
+ expect(result.user).toBe(LOW);
131
+ expect(result.reason).toBe('name');
132
+ });
133
+
134
+ it('resolves a duplicated Name identically however the cache is ordered', () => {
135
+ const forward = resolvePrincipalFrom('shared.name', [HIGH, LOW, SYSTEM], SYSTEM_USER_ID);
136
+ const reversed = resolvePrincipalFrom('shared.name', [SYSTEM, LOW, HIGH], SYSTEM_USER_ID);
137
+
138
+ expect(reversed.user).toBe(forward.user);
139
+ });
140
+
141
+ it('applies the same tiebreak to the Email rung', () => {
142
+ // `UQ_User_Email` should make this unreachable from the database, but `UserCache` is
143
+ // also written through `SetUsers` and mutated in place by `Users.push`, so the rung
144
+ // must not depend on the constraint holding in memory.
145
+ const lowEmail: ResolvablePrincipal = { ...LOW, Name: 'a.person', Email: 'shared@acme.com' };
146
+ const highEmail: ResolvablePrincipal = { ...HIGH, Name: 'b.person', Email: 'shared@acme.com' };
147
+
148
+ const result = resolvePrincipalFrom('shared@acme.com', [highEmail, lowEmail, SYSTEM], SYSTEM_USER_ID);
149
+
150
+ expect(result.user).toBe(lowEmail);
151
+ expect(result.reason).toBe('email');
152
+ });
153
+ });
154
+
155
+ describe('a DEACTIVATED configured candidate', () => {
156
+ /**
157
+ * The case an operator actually hits: the admin whose address sits in
158
+ * `contextUserForProvisioning` leaves, and their account is deactivated. Every other rung
159
+ * requires `IsActive`, so without this one the ladder would read "we never act as a
160
+ * disabled user, except the one the operator named" — and provisioning would keep running,
161
+ * silently, as someone who no longer works here.
162
+ */
163
+ const RETIRED: ResolvablePrincipal = {
164
+ ID: 'BBBB2222-0000-0000-0000-000000000002',
165
+ Name: 'departed@acme.com',
166
+ Email: 'departed@acme.com',
167
+ Type: 'Owner ',
168
+ IsActive: false,
169
+ };
170
+
171
+ it('does not resolve a deactivated candidate by Email', () => {
172
+ const result = resolvePrincipalFrom('departed@acme.com', [...STOCK_HOST, RETIRED], SYSTEM_USER_ID);
173
+
174
+ expect(result.user).toBe(SYSTEM);
175
+ expect(result.reason).toBe('system');
176
+ });
177
+
178
+ it('does not resolve a deactivated candidate by Name', () => {
179
+ // The Name rung is the compatibility rung and is tried first, so it needs the guard
180
+ // most: without it the guarantee holds only for the rungs nobody configures.
181
+ const renamed: ResolvablePrincipal = { ...RETIRED, Name: 'ops-service-account', Email: 'ops@acme.com' };
182
+
183
+ const result = resolvePrincipalFrom('ops-service-account', [...STOCK_HOST, renamed], SYSTEM_USER_ID);
184
+
185
+ expect(result.user).toBe(SYSTEM);
186
+ expect(result.reason).toBe('system');
187
+ });
188
+
189
+ it('reports that the candidate matched but is inactive, not that it matched nothing', () => {
190
+ const result = resolvePrincipalFrom('departed@acme.com', [...STOCK_HOST, RETIRED], SYSTEM_USER_ID);
191
+
192
+ // "matched no user's Name or Email" would send the operator hunting for a typo in a
193
+ // value that is spelled correctly. The fix here is to reactivate the account or name a
194
+ // different user, and the message has to point at that one.
195
+ expect(result.warning).toMatch(/inactive/i);
196
+ expect(result.warning).not.toMatch(/matched no user/i);
197
+ });
198
+
199
+ it('advances to the next rung rather than stopping at the inactive match', () => {
200
+ // An inactive Name match must not shadow an ACTIVE Email match. The ladder's job is to
201
+ // find the user the string names, and at that point it has not run out of places to
202
+ // look — this is #4209's own shape, with the Name holder deactivated.
203
+ const inactiveByName: ResolvablePrincipal = {
204
+ ID: 'CCCC3333-0000-0000-0000-000000000003',
205
+ Name: 'ops@acme.com',
206
+ Email: 'old-ops@acme.com',
207
+ Type: 'User ',
208
+ IsActive: false,
209
+ };
210
+ const activeByEmail: ResolvablePrincipal = {
211
+ ID: 'DDDD4444-0000-0000-0000-000000000004',
212
+ Name: 'Ops Service Account',
213
+ Email: 'ops@acme.com',
214
+ Type: 'User ',
215
+ IsActive: true,
216
+ };
217
+
218
+ const result = resolvePrincipalFrom('ops@acme.com', [inactiveByName, activeByEmail], SYSTEM_USER_ID);
219
+
220
+ expect(result.user).toBe(activeByEmail);
221
+ expect(result.reason).toBe('email');
222
+ expect(result.warning).toBeUndefined();
223
+ });
224
+ });
225
+ describe('fallbacks', () => {
226
+ it('falls back to the System user rather than an arbitrary Owner when the candidate is unresolvable', () => {
227
+ const result = resolvePrincipalFrom('nobody@nowhere.example', STOCK_HOST, SYSTEM_USER_ID);
228
+
229
+ expect(result.user).toBe(SYSTEM);
230
+ expect(result.reason).toBe('system');
231
+ });
232
+
233
+ it('warns when a configured candidate did not resolve, naming the candidate', () => {
234
+ const result = resolvePrincipalFrom('nobody@nowhere.example', STOCK_HOST, SYSTEM_USER_ID);
235
+
236
+ expect(result.warning).toContain('nobody@nowhere.example');
237
+ });
238
+
239
+ it('falls back to the System user WITHOUT warning when no candidate is configured', () => {
240
+ // Zod defaults this setting to '', so an unset/blank value must not be reported as a
241
+ // misconfiguration — but it must still land somewhere deterministic.
242
+ const result = resolvePrincipalFrom('', STOCK_HOST, SYSTEM_USER_ID);
243
+
244
+ expect(result.user).toBe(SYSTEM);
245
+ expect(result.warning).toBeUndefined();
246
+ });
247
+
248
+ it('picks the lowest-ID active Owner when the deployment has no System user', () => {
249
+ // Last resort, and still deterministic — never "whichever Owner sorts first in the cache".
250
+ const laterOwner: ResolvablePrincipal = {
251
+ ID: 'FFFF9999-0000-0000-0000-000000000009',
252
+ Name: 'zoe@acme.com',
253
+ Email: 'zoe@acme.com',
254
+ Type: 'Owner ',
255
+ IsActive: true,
256
+ };
257
+ const users = [laterOwner, HOST_ADMIN, ANONYMOUS];
258
+
259
+ const result = resolvePrincipalFrom('nobody@nowhere.example', users, SYSTEM_USER_ID);
260
+
261
+ expect(result.user).toBe(HOST_ADMIN);
262
+ expect(result.reason).toBe('owner');
263
+ });
264
+
265
+ it('never returns an inactive user as the Owner fallback', () => {
266
+ const inactiveOwner: ResolvablePrincipal = {
267
+ ID: '11111111-0000-0000-0000-000000000000', // sorts first, so only IsActive can exclude it
268
+ Name: 'retired@acme.com',
269
+ Email: 'retired@acme.com',
270
+ Type: 'Owner',
271
+ IsActive: false,
272
+ };
273
+
274
+ const result = resolvePrincipalFrom('nobody@nowhere.example', [inactiveOwner, HOST_ADMIN], SYSTEM_USER_ID);
275
+
276
+ expect(result.user).toBe(HOST_ADMIN);
277
+ });
278
+
279
+ it('does not fall back to a DEACTIVATED system user', () => {
280
+ // The Owner rung requires IsActive; the system rung must too, or the guarantee is
281
+ // "we never act as a disabled user, except as the one user we reach first".
282
+ const inactiveSystem: ResolvablePrincipal = { ...SYSTEM, IsActive: false };
283
+
284
+ const result = resolvePrincipalFrom('nobody@nowhere.example', [inactiveSystem, HOST_ADMIN], SYSTEM_USER_ID);
285
+
286
+ expect(result.user).toBe(HOST_ADMIN);
287
+ expect(result.reason).toBe('owner');
288
+ });
289
+
290
+ it('returns no principal when the system user is deactivated and no active Owner remains', () => {
291
+ const inactiveSystem: ResolvablePrincipal = { ...SYSTEM, IsActive: false };
292
+
293
+ const result = resolvePrincipalFrom('nobody@nowhere.example', [inactiveSystem, ANONYMOUS], SYSTEM_USER_ID);
294
+
295
+ expect(result.user).toBeNull();
296
+ });
297
+
298
+ it('returns no principal at all when the cache is empty', () => {
299
+ const result = resolvePrincipalFrom(SHIPPED_DEFAULT, [], SYSTEM_USER_ID);
300
+
301
+ expect(result.user).toBeNull();
302
+ expect(result.reason).toBe('none');
303
+ });
304
+ });
305
+ });