@memberjunction/server 6.2.0-edge.3 → 6.2.0-edge.4

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 (346) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/agentSessions/BridgeHostInstance.d.ts +25 -0
  3. package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -0
  4. package/dist/agentSessions/BridgeHostInstance.js +30 -0
  5. package/dist/agentSessions/BridgeHostInstance.js.map +1 -0
  6. package/dist/agentSessions/HostInstance.d.ts +75 -11
  7. package/dist/agentSessions/HostInstance.d.ts.map +1 -1
  8. package/dist/agentSessions/HostInstance.js +99 -28
  9. package/dist/agentSessions/HostInstance.js.map +1 -1
  10. package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
  11. package/dist/agentSessions/ReturningVisitorRecap.js +7 -3
  12. package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
  13. package/dist/agentSessions/SessionJanitor.d.ts +173 -25
  14. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  15. package/dist/agentSessions/SessionJanitor.js +452 -54
  16. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  17. package/dist/agentSessions/SessionManager.d.ts +43 -2
  18. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  19. package/dist/agentSessions/SessionManager.js +99 -8
  20. package/dist/agentSessions/SessionManager.js.map +1 -1
  21. package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
  22. package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
  23. package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
  24. package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
  25. package/dist/agentSessions/index.d.ts +2 -0
  26. package/dist/agentSessions/index.d.ts.map +1 -1
  27. package/dist/agentSessions/index.js +10 -3
  28. package/dist/agentSessions/index.js.map +1 -1
  29. package/dist/auth/actionAuthorization.d.ts +40 -0
  30. package/dist/auth/actionAuthorization.d.ts.map +1 -0
  31. package/dist/auth/actionAuthorization.js +94 -0
  32. package/dist/auth/actionAuthorization.js.map +1 -0
  33. package/dist/auth/index.d.ts.map +1 -1
  34. package/dist/auth/index.js +3 -10
  35. package/dist/auth/index.js.map +1 -1
  36. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  37. package/dist/auth/magicLink/MagicLinkService.js +6 -3
  38. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  39. package/dist/auth/newUsers.d.ts +10 -0
  40. package/dist/auth/newUsers.d.ts.map +1 -1
  41. package/dist/auth/newUsers.js +80 -11
  42. package/dist/auth/newUsers.js.map +1 -1
  43. package/dist/config.d.ts +2378 -0
  44. package/dist/config.d.ts.map +1 -1
  45. package/dist/config.js +123 -0
  46. package/dist/config.js.map +1 -1
  47. package/dist/context.d.ts +8 -0
  48. package/dist/context.d.ts.map +1 -1
  49. package/dist/context.js +14 -11
  50. package/dist/context.js.map +1 -1
  51. package/dist/generated/graphql-schemas/__mj.d.ts +135 -0
  52. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  53. package/dist/generated/graphql-schemas/__mj.js +706 -0
  54. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  55. package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
  56. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
  57. package/dist/generic/ResolverBase.d.ts +20 -4
  58. package/dist/generic/ResolverBase.d.ts.map +1 -1
  59. package/dist/generic/ResolverBase.js +39 -10
  60. package/dist/generic/ResolverBase.js.map +1 -1
  61. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  62. package/dist/generic/RunViewResolver.js +4 -0
  63. package/dist/generic/RunViewResolver.js.map +1 -1
  64. package/dist/index.d.ts +6 -0
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +120 -131
  67. package/dist/index.js.map +1 -1
  68. package/dist/isolatedProvider.d.ts +20 -0
  69. package/dist/isolatedProvider.d.ts.map +1 -0
  70. package/dist/isolatedProvider.js +22 -0
  71. package/dist/isolatedProvider.js.map +1 -0
  72. package/dist/realtimeProxy/RealtimeProxyServer.d.ts +44 -4
  73. package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
  74. package/dist/realtimeProxy/RealtimeProxyServer.js +121 -16
  75. package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
  76. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts +171 -0
  77. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts.map +1 -0
  78. package/dist/realtimeProxy/RealtimeRelayTunnel.js +532 -0
  79. package/dist/realtimeProxy/RealtimeRelayTunnel.js.map +1 -0
  80. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts +148 -0
  81. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts.map +1 -0
  82. package/dist/realtimeSessions/RealtimeSessionEventService.js +191 -0
  83. package/dist/realtimeSessions/RealtimeSessionEventService.js.map +1 -0
  84. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts +66 -0
  85. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -0
  86. package/dist/realtimeSessions/RealtimeSessionVerificationService.js +138 -0
  87. package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -0
  88. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts +38 -0
  89. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -0
  90. package/dist/realtimeSessions/RealtimeVerifyRouter.js +98 -0
  91. package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
  92. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts +61 -0
  93. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -0
  94. package/dist/realtimeSessions/SessionVerificationPortsBase.js +149 -0
  95. package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -0
  96. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts +57 -0
  97. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts.map +1 -0
  98. package/dist/realtimeSessions/SlidingWindowRateLimiter.js +69 -0
  99. package/dist/realtimeSessions/SlidingWindowRateLimiter.js.map +1 -0
  100. package/dist/realtimeSessions/index.d.ts +17 -0
  101. package/dist/realtimeSessions/index.d.ts.map +1 -0
  102. package/dist/realtimeSessions/index.js +17 -0
  103. package/dist/realtimeSessions/index.js.map +1 -0
  104. package/dist/realtimeSessions/sessionAccess.d.ts +86 -0
  105. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
  106. package/dist/realtimeSessions/sessionAccess.js +90 -0
  107. package/dist/realtimeSessions/sessionAccess.js.map +1 -0
  108. package/dist/realtimeSessions/sessionConfigStamps.d.ts +70 -0
  109. package/dist/realtimeSessions/sessionConfigStamps.d.ts.map +1 -0
  110. package/dist/realtimeSessions/sessionConfigStamps.js +93 -0
  111. package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -0
  112. package/dist/realtimeSessions/subscriptionAuthorization.d.ts +65 -0
  113. package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -0
  114. package/dist/realtimeSessions/subscriptionAuthorization.js +71 -0
  115. package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -0
  116. package/dist/realtimeSessions/verificationCore.d.ts +243 -0
  117. package/dist/realtimeSessions/verificationCore.d.ts.map +1 -0
  118. package/dist/realtimeSessions/verificationCore.js +356 -0
  119. package/dist/realtimeSessions/verificationCore.js.map +1 -0
  120. package/dist/realtimeSessions/verificationPages.d.ts +66 -0
  121. package/dist/realtimeSessions/verificationPages.d.ts.map +1 -0
  122. package/dist/realtimeSessions/verificationPages.js +110 -0
  123. package/dist/realtimeSessions/verificationPages.js.map +1 -0
  124. package/dist/realtimeSessions/verificationPolicy.d.ts +147 -0
  125. package/dist/realtimeSessions/verificationPolicy.d.ts.map +1 -0
  126. package/dist/realtimeSessions/verificationPolicy.js +249 -0
  127. package/dist/realtimeSessions/verificationPolicy.js.map +1 -0
  128. package/dist/realtimeSessions/verificationWorkflow.d.ts +263 -0
  129. package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -0
  130. package/dist/realtimeSessions/verificationWorkflow.js +435 -0
  131. package/dist/realtimeSessions/verificationWorkflow.js.map +1 -0
  132. package/dist/realtimeWidget/widgetGuestElevation.d.ts +20 -2
  133. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  134. package/dist/realtimeWidget/widgetGuestElevation.js +31 -2
  135. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  136. package/dist/resolvers/ActionResolver.d.ts +31 -5
  137. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  138. package/dist/resolvers/ActionResolver.js +75 -14
  139. package/dist/resolvers/ActionResolver.js.map +1 -1
  140. package/dist/resolvers/AdhocQueryResolver.d.ts +14 -1
  141. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  142. package/dist/resolvers/AdhocQueryResolver.js +21 -5
  143. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  144. package/dist/resolvers/CacheStatsResolver.d.ts +27 -0
  145. package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -1
  146. package/dist/resolvers/CacheStatsResolver.js +113 -2
  147. package/dist/resolvers/CacheStatsResolver.js.map +1 -1
  148. package/dist/resolvers/ClientToolRequestResolver.d.ts +4 -2
  149. package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -1
  150. package/dist/resolvers/ClientToolRequestResolver.js +28 -10
  151. package/dist/resolvers/ClientToolRequestResolver.js.map +1 -1
  152. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +12 -1
  153. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  154. package/dist/resolvers/FetchEntityVectorsResolver.js +83 -2
  155. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  156. package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -1
  157. package/dist/resolvers/HumanHandoffResolver.js +7 -1
  158. package/dist/resolvers/HumanHandoffResolver.js.map +1 -1
  159. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  160. package/dist/resolvers/MCPResolver.js +9 -4
  161. package/dist/resolvers/MCPResolver.js.map +1 -1
  162. package/dist/resolvers/RealtimeBridgeResolver.d.ts +54 -2
  163. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  164. package/dist/resolvers/RealtimeBridgeResolver.js +155 -10
  165. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  166. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +155 -25
  167. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  168. package/dist/resolvers/RealtimeClientSessionResolver.js +394 -90
  169. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  170. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts +61 -0
  171. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts.map +1 -0
  172. package/dist/resolvers/RealtimeSessionEventsResolver.js +130 -0
  173. package/dist/resolvers/RealtimeSessionEventsResolver.js.map +1 -0
  174. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts +85 -0
  175. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts.map +1 -0
  176. package/dist/resolvers/RealtimeSessionVerificationResolver.js +188 -0
  177. package/dist/resolvers/RealtimeSessionVerificationResolver.js.map +1 -0
  178. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
  179. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  180. package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
  181. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  182. package/dist/resolvers/RunAIAgentResolver.d.ts +10 -0
  183. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  184. package/dist/resolvers/RunAIAgentResolver.js +68 -11
  185. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  186. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  187. package/dist/resolvers/RunTemplateResolver.js +12 -11
  188. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  189. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  190. package/dist/resolvers/SyncDataResolver.js +6 -2
  191. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  192. package/dist/resolvers/SyncRolesUsersResolver.d.ts +2 -0
  193. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  194. package/dist/resolvers/SyncRolesUsersResolver.js +23 -6
  195. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  196. package/dist/resolvers/UserAvatarResolver.d.ts +16 -0
  197. package/dist/resolvers/UserAvatarResolver.d.ts.map +1 -0
  198. package/dist/resolvers/UserAvatarResolver.js +124 -0
  199. package/dist/resolvers/UserAvatarResolver.js.map +1 -0
  200. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  201. package/dist/resolvers/UserResolver.js +2 -17
  202. package/dist/resolvers/UserResolver.js.map +1 -1
  203. package/dist/resolvers/agentVisionConsentAudit.d.ts +37 -0
  204. package/dist/resolvers/agentVisionConsentAudit.d.ts.map +1 -0
  205. package/dist/resolvers/agentVisionConsentAudit.js +63 -0
  206. package/dist/resolvers/agentVisionConsentAudit.js.map +1 -0
  207. package/dist/resolvers/avatarInputValidation.d.ts +40 -0
  208. package/dist/resolvers/avatarInputValidation.d.ts.map +1 -0
  209. package/dist/resolvers/avatarInputValidation.js +97 -0
  210. package/dist/resolvers/avatarInputValidation.js.map +1 -0
  211. package/dist/resolvers/currentUserRoles.d.ts +29 -0
  212. package/dist/resolvers/currentUserRoles.d.ts.map +1 -1
  213. package/dist/resolvers/currentUserRoles.js +30 -0
  214. package/dist/resolvers/currentUserRoles.js.map +1 -1
  215. package/dist/resolvers/meetings/meetingMutations.d.ts +6 -0
  216. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
  217. package/dist/resolvers/meetings/meetingMutations.js +21 -0
  218. package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
  219. package/dist/services/TaskGraphContinuationDeliverer.d.ts +4 -3
  220. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  221. package/dist/services/TaskGraphContinuationDeliverer.js +13 -7
  222. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  223. package/dist/sharedCache.d.ts +105 -0
  224. package/dist/sharedCache.d.ts.map +1 -0
  225. package/dist/sharedCache.js +276 -0
  226. package/dist/sharedCache.js.map +1 -0
  227. package/dist/types.d.ts +6 -0
  228. package/dist/types.d.ts.map +1 -1
  229. package/dist/types.js.map +1 -1
  230. package/package.json +97 -95
  231. package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
  232. package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
  233. package/src/__tests__/BridgeHostInstance.test.ts +232 -0
  234. package/src/__tests__/ClientToolRequestResolver.test.ts +116 -0
  235. package/src/__tests__/FetchEntityVectorsResolver.test.ts +285 -0
  236. package/src/__tests__/HostInstance.test.ts +84 -0
  237. package/src/__tests__/HumanHandoffResolver.test.ts +17 -0
  238. package/src/__tests__/MeetingResolver.test.ts +75 -1
  239. package/src/__tests__/RealtimeBridgeResolver.test.ts +415 -6
  240. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
  241. package/src/__tests__/RealtimeClientSessionResolver.test.ts +682 -58
  242. package/src/__tests__/RealtimeProxyServer.test.ts +16 -1
  243. package/src/__tests__/RealtimeRelay.test.ts +876 -0
  244. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
  245. package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
  246. package/src/__tests__/ResolverBase.clientOldValues.test.ts +6 -3
  247. package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
  248. package/src/__tests__/RunAIAgentResolver.branch.test.ts +169 -0
  249. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +4 -4
  250. package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +166 -0
  251. package/src/__tests__/RunTemplateResolver.test.ts +132 -0
  252. package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +63 -0
  253. package/src/__tests__/SessionJanitor.test.ts +845 -18
  254. package/src/__tests__/SessionManager.test.ts +213 -17
  255. package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
  256. package/src/__tests__/SyncRolesUsersResolver.userRoles.test.ts +97 -0
  257. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +50 -3
  258. package/src/__tests__/UserAvatarResolver.test.ts +255 -0
  259. package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
  260. package/src/__tests__/actionAuthorization.test.ts +130 -0
  261. package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
  262. package/src/__tests__/avatarInputValidation.test.ts +160 -0
  263. package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
  264. package/src/__tests__/magicLinkService.consume.test.ts +6 -0
  265. package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
  266. package/src/__tests__/newUsers.template.test.ts +151 -0
  267. package/src/__tests__/newUsers.test.ts +74 -5
  268. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +17 -2
  269. package/src/__tests__/realtimeSessions/RealtimeSessionEventService.test.ts +201 -0
  270. package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +215 -0
  271. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationResolver.test.ts +102 -0
  272. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationService.test.ts +349 -0
  273. package/src/__tests__/realtimeSessions/RealtimeVerifyRouter.test.ts +131 -0
  274. package/src/__tests__/realtimeSessions/SlidingWindowRateLimiter.test.ts +56 -0
  275. package/src/__tests__/realtimeSessions/helpers.ts +4 -0
  276. package/src/__tests__/realtimeSessions/sessionAccess.test.ts +129 -0
  277. package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +124 -0
  278. package/src/__tests__/realtimeSessions/verificationCore.test.ts +355 -0
  279. package/src/__tests__/realtimeSessions/verificationPages.test.ts +84 -0
  280. package/src/__tests__/realtimeSessions/verificationPolicy.test.ts +226 -0
  281. package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +633 -0
  282. package/src/__tests__/resolverBase.fls.test.ts +87 -4
  283. package/src/__tests__/serverExtensionReservedRoots.test.ts +2 -0
  284. package/src/__tests__/sharedCache.test.ts +310 -0
  285. package/src/__tests__/widgetGuestElevation.test.ts +44 -0
  286. package/src/agentSessions/BridgeHostInstance.ts +32 -0
  287. package/src/agentSessions/HostInstance.ts +124 -28
  288. package/src/agentSessions/ReturningVisitorRecap.ts +7 -3
  289. package/src/agentSessions/SessionJanitor.ts +543 -57
  290. package/src/agentSessions/SessionManager.ts +125 -25
  291. package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
  292. package/src/agentSessions/index.ts +10 -3
  293. package/src/auth/actionAuthorization.ts +116 -0
  294. package/src/auth/index.ts +3 -8
  295. package/src/auth/magicLink/MagicLinkService.ts +6 -3
  296. package/src/auth/newUsers.ts +99 -11
  297. package/src/config.ts +125 -0
  298. package/src/context.ts +15 -10
  299. package/src/generated/graphql-schemas/__mj.ts +496 -1
  300. package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
  301. package/src/generic/ResolverBase.ts +39 -9
  302. package/src/generic/RunViewResolver.ts +4 -0
  303. package/src/index.ts +132 -143
  304. package/src/isolatedProvider.ts +22 -0
  305. package/src/realtimeProxy/RealtimeProxyServer.ts +148 -16
  306. package/src/realtimeProxy/RealtimeRelayTunnel.ts +645 -0
  307. package/src/realtimeSessions/README.md +179 -0
  308. package/src/realtimeSessions/RealtimeSessionEventService.ts +274 -0
  309. package/src/realtimeSessions/RealtimeSessionVerificationService.ts +167 -0
  310. package/src/realtimeSessions/RealtimeVerifyRouter.ts +108 -0
  311. package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
  312. package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
  313. package/src/realtimeSessions/index.ts +16 -0
  314. package/src/realtimeSessions/sessionAccess.ts +141 -0
  315. package/src/realtimeSessions/sessionConfigStamps.ts +123 -0
  316. package/src/realtimeSessions/subscriptionAuthorization.ts +116 -0
  317. package/src/realtimeSessions/verificationCore.ts +502 -0
  318. package/src/realtimeSessions/verificationPages.ts +149 -0
  319. package/src/realtimeSessions/verificationPolicy.ts +321 -0
  320. package/src/realtimeSessions/verificationWorkflow.ts +702 -0
  321. package/src/realtimeWidget/widgetGuestElevation.ts +34 -2
  322. package/src/resolvers/ActionResolver.ts +84 -21
  323. package/src/resolvers/AdhocQueryResolver.ts +24 -6
  324. package/src/resolvers/CacheStatsResolver.ts +89 -2
  325. package/src/resolvers/ClientToolRequestResolver.ts +25 -7
  326. package/src/resolvers/FetchEntityVectorsResolver.ts +97 -2
  327. package/src/resolvers/HumanHandoffResolver.ts +8 -0
  328. package/src/resolvers/MCPResolver.ts +8 -5
  329. package/src/resolvers/RealtimeBridgeResolver.ts +163 -11
  330. package/src/resolvers/RealtimeClientSessionResolver.ts +501 -96
  331. package/src/resolvers/RealtimeSessionEventsResolver.ts +133 -0
  332. package/src/resolvers/RealtimeSessionVerificationResolver.ts +174 -0
  333. package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
  334. package/src/resolvers/RunAIAgentResolver.ts +78 -11
  335. package/src/resolvers/RunTemplateResolver.ts +13 -12
  336. package/src/resolvers/SyncDataResolver.ts +6 -2
  337. package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
  338. package/src/resolvers/UserAvatarResolver.ts +111 -0
  339. package/src/resolvers/UserResolver.ts +2 -16
  340. package/src/resolvers/agentVisionConsentAudit.ts +82 -0
  341. package/src/resolvers/avatarInputValidation.ts +123 -0
  342. package/src/resolvers/currentUserRoles.ts +32 -1
  343. package/src/resolvers/meetings/meetingMutations.ts +21 -0
  344. package/src/services/TaskGraphContinuationDeliverer.ts +13 -7
  345. package/src/sharedCache.ts +302 -0
  346. package/src/types.ts +7 -0
@@ -0,0 +1,102 @@
1
+ import 'reflect-metadata';
2
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
+ import type { IMetadataProvider, UserInfo } from '@memberjunction/core';
4
+ import type { AppContext, ProviderInfo, UserPayload } from '../../types.js';
5
+
6
+ const service = vi.hoisted(() => ({
7
+ RequestVerification: vi.fn(),
8
+ SubmitCode: vi.fn(),
9
+ GetStatus: vi.fn(),
10
+ }));
11
+ vi.mock('../../realtimeSessions/RealtimeSessionVerificationService.js', () => ({ RealtimeSessionVerificationService: { Instance: service } }));
12
+
13
+ import { RealtimeSessionVerificationResolver } from '../../resolvers/RealtimeSessionVerificationResolver.js';
14
+ import { HasNoLogParameter } from '../../logging/NoLog.js';
15
+
16
+ const SESSION_ID = 'BBBBBBBB-0000-4000-8000-000000000001';
17
+ const OWNER = 'AAAAAAAA-0000-4000-8000-000000000001';
18
+ const PROVIDER = { id: 'rw' } as unknown as IMetadataProvider;
19
+
20
+ function context(over: { userRecord?: Partial<UserInfo> | null; email?: string; providers?: ProviderInfo[]; clientIp?: string } = {}): AppContext {
21
+ const userPayload = {
22
+ email: over.email ?? 'owner@example.com',
23
+ sessionId: 's',
24
+ userRecord: over.userRecord === null ? undefined : (over.userRecord ?? { ID: OWNER }),
25
+ } as UserPayload;
26
+ return {
27
+ userPayload,
28
+ providers: over.providers ?? ([{ provider: PROVIDER, type: 'Read-Write' }] as unknown as ProviderInfo[]),
29
+ ClientIp: over.clientIp ?? '203.0.113.7',
30
+ } as unknown as AppContext;
31
+ }
32
+
33
+ const OK = { Success: true, VerificationState: 'pending', Message: 'We emailed you a link and a code.', SendsRemaining: 2 };
34
+
35
+ describe('RealtimeSessionVerificationResolver', () => {
36
+ const resolver = new RealtimeSessionVerificationResolver();
37
+
38
+ beforeEach(() => {
39
+ service.RequestVerification.mockReset().mockResolvedValue(OK);
40
+ service.SubmitCode.mockReset().mockResolvedValue({ Success: true, VerificationState: 'verified' });
41
+ service.GetStatus.mockReset().mockResolvedValue({ Success: true, VerificationState: 'unverified' });
42
+ });
43
+
44
+ describe('RequestRealtimeSessionVerification', () => {
45
+ it('calls the service as the authenticated principal with the request provider and client IP', async () => {
46
+ const result = await resolver.RequestRealtimeSessionVerification(SESSION_ID, 'Pat', 'pat@acme.com', context());
47
+ expect(result).toEqual(OK);
48
+ expect(service.RequestVerification).toHaveBeenCalledWith(
49
+ { AgentSessionID: SESSION_ID, Name: 'Pat', Email: 'pat@acme.com' },
50
+ { Kind: 'principal', ContextUser: { ID: OWNER }, Provider: PROVIDER, ClientIp: '203.0.113.7' },
51
+ );
52
+ });
53
+
54
+ it('refuses a caller with no authenticated userRecord — it does NOT fall back to an email lookup that would drop an anonymous guest\'s scope', async () => {
55
+ const result = await resolver.RequestRealtimeSessionVerification(SESSION_ID, 'Pat', 'pat@acme.com', context({ userRecord: null, email: 'anonymous@magic-link.local' }));
56
+ expect(result).toMatchObject({ Success: false, ErrorCode: 'session_not_found' });
57
+ expect(service.RequestVerification).not.toHaveBeenCalled();
58
+ });
59
+
60
+ it('refuses when there is no read-write provider', async () => {
61
+ const result = await resolver.RequestRealtimeSessionVerification(SESSION_ID, 'Pat', 'pat@acme.com', context({ providers: [] }));
62
+ expect(result.Success).toBe(false);
63
+ expect(service.RequestVerification).not.toHaveBeenCalled();
64
+ });
65
+
66
+ it('passes a failure result straight through', async () => {
67
+ service.RequestVerification.mockResolvedValue({ Success: false, VerificationState: 'unverified', ErrorCode: 'consumer_domain', Message: 'Please use your work email address.' });
68
+ expect(await resolver.RequestRealtimeSessionVerification(SESSION_ID, 'Pat', 'pat@gmail.com', context())).toMatchObject({ Success: false, ErrorCode: 'consumer_domain' });
69
+ });
70
+ });
71
+
72
+ describe('SubmitRealtimeSessionVerificationCode', () => {
73
+ it('calls the service with the code and the same principal', async () => {
74
+ await resolver.SubmitRealtimeSessionVerificationCode(SESSION_ID, '123456', context());
75
+ expect(service.SubmitCode).toHaveBeenCalledWith({ AgentSessionID: SESSION_ID, Code: '123456' }, expect.objectContaining({ Kind: 'principal', ContextUser: { ID: OWNER } }));
76
+ });
77
+
78
+ it('refuses an unauthenticated caller', async () => {
79
+ expect(await resolver.SubmitRealtimeSessionVerificationCode(SESSION_ID, '123456', context({ userRecord: null }))).toMatchObject({ Success: false });
80
+ expect(service.SubmitCode).not.toHaveBeenCalled();
81
+ });
82
+ });
83
+
84
+ describe('RealtimeSessionVerificationStatus', () => {
85
+ it('reads the status as the principal', async () => {
86
+ expect(await resolver.RealtimeSessionVerificationStatus(SESSION_ID, context())).toEqual({ Success: true, VerificationState: 'unverified' });
87
+ expect(service.GetStatus).toHaveBeenCalledWith({ AgentSessionID: SESSION_ID }, expect.objectContaining({ ContextUser: { ID: OWNER } }));
88
+ });
89
+
90
+ it('refuses an unauthenticated caller', async () => {
91
+ expect(await resolver.RealtimeSessionVerificationStatus(SESSION_ID, context({ userRecord: null }))).toMatchObject({ Success: false });
92
+ expect(service.GetStatus).not.toHaveBeenCalled();
93
+ });
94
+ });
95
+
96
+ it('marks the email and the code @NoLog so they never reach variable logging', () => {
97
+ // Parameter index 2 of the request (email) and index 1 of the code submission.
98
+ expect(HasNoLogParameter(RealtimeSessionVerificationResolver, 'RequestRealtimeSessionVerification', 2)).toBe(true);
99
+ expect(HasNoLogParameter(RealtimeSessionVerificationResolver, 'SubmitRealtimeSessionVerificationCode', 1)).toBe(true);
100
+ expect(HasNoLogParameter(RealtimeSessionVerificationResolver, 'RequestRealtimeSessionVerification', 0)).toBe(false);
101
+ });
102
+ });
@@ -0,0 +1,349 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
2
+
3
+ /** Mutable server config the mocked `config.js` serves. */
4
+ const config = vi.hoisted(() => ({
5
+ configInfo: {
6
+ publicUrl: 'https://api.example.com',
7
+ baseUrl: 'http://localhost',
8
+ graphqlPort: 4000,
9
+ graphqlRootPath: '/',
10
+ realtime: {
11
+ enabled: true,
12
+ identityVerification: {
13
+ enabled: true as boolean,
14
+ communicationProvider: 'SendGrid' as string | undefined,
15
+ hmacSecret: 'unit-test-hmac-secret-0123456789' as string | undefined,
16
+ deliveryMode: 'send' as 'send' | 'dry-run',
17
+ fromAddress: 'noreply@example.com' as string | undefined,
18
+ publicBaseUrl: undefined as string | undefined,
19
+ contextUserForVerification: undefined as string | undefined,
20
+ policy: {} as Record<string, unknown>,
21
+ rateLimits: {} as Record<string, number>,
22
+ },
23
+ },
24
+ },
25
+ }));
26
+ vi.mock('../../config.js', () => config);
27
+
28
+ const mocks = vi.hoisted(() => ({
29
+ serviceUser: { ID: 'SERVICE-USER' } as { ID: string } | null,
30
+ sendSingleMessage: vi.fn(),
31
+ engineConfig: vi.fn(),
32
+ }));
33
+
34
+ vi.mock('../../auth/principals.js', () => ({ ResolveConfiguredPrincipal: vi.fn(() => mocks.serviceUser) }));
35
+ vi.mock('@memberjunction/generic-database-provider', () => ({
36
+ UserCache: { Instance: { GetSystemUser: () => mocks.serviceUser, Users: [], SYSTEM_USER_ID: 'SYS' } },
37
+ }));
38
+ vi.mock('@memberjunction/communication-engine', () => ({
39
+ CommunicationEngine: { Instance: { Config: mocks.engineConfig, SendSingleMessage: mocks.sendSingleMessage } },
40
+ }));
41
+ import { Metadata, type IMetadataProvider, type UserInfo } from '@memberjunction/core';
42
+ import { IsTrustedSessionConfigWriteActive } from '@memberjunction/core-entities-server';
43
+ import { RealtimeSessionVerificationService, RuntimeVerificationPorts } from '../../realtimeSessions/RealtimeSessionVerificationService.js';
44
+ import { REALTIME_IDENTITY_VERIFIED_AUDIT_TYPE } from '../../realtimeSessions/SessionVerificationPortsBase.js';
45
+ import { RealtimeSessionEventService } from '../../realtimeSessions/RealtimeSessionEventService.js';
46
+ import type { VerificationCaller } from '../../realtimeSessions/verificationWorkflow.js';
47
+
48
+ const OWNER = 'AAAAAAAA-0000-4000-8000-000000000001';
49
+ const SESSION_ID = 'BBBBBBBB-0000-4000-8000-000000000001';
50
+
51
+ /** A session entity double that records Save() calls and whether the trusted scope was active during them. */
52
+ interface FakeSessionEntity {
53
+ ID: string;
54
+ UserID: string;
55
+ Status: 'Active' | 'Idle' | 'Closed';
56
+ ConversationID: string | null;
57
+ Config_: string | null;
58
+ __mj_CreatedAt: Date;
59
+ LatestResult?: { CompleteMessage: string };
60
+ Load: ReturnType<typeof vi.fn>;
61
+ Save: ReturnType<typeof vi.fn>;
62
+ }
63
+
64
+ interface Harness {
65
+ provider: IMetadataProvider;
66
+ session: FakeSessionEntity;
67
+ saveScopes: boolean[];
68
+ auditRows: Array<Record<string, unknown>>;
69
+ loadedAs: Array<string>;
70
+ }
71
+
72
+ function harness(over: { externalId?: string | null; saveResult?: boolean; auditTypes?: Array<{ ID: string; Name: string }>; auditSaves?: boolean } = {}): Harness {
73
+ const saveScopes: boolean[] = [];
74
+ const auditRows: Array<Record<string, unknown>> = [];
75
+ const loadedAs: string[] = [];
76
+ const session: FakeSessionEntity = {
77
+ ID: SESSION_ID,
78
+ UserID: OWNER,
79
+ Status: 'Active',
80
+ ConversationID: 'CONV-1',
81
+ Config_: JSON.stringify({ targetAgentID: 't' }),
82
+ __mj_CreatedAt: new Date('2026-10-02T12:00:00.000Z'),
83
+ Load: vi.fn(async () => true),
84
+ Save: vi.fn(async () => {
85
+ saveScopes.push(IsTrustedSessionConfigWriteActive());
86
+ return over.saveResult ?? true;
87
+ }),
88
+ };
89
+ if (over.saveResult === false) {
90
+ session.LatestResult = { CompleteMessage: 'validation refused' };
91
+ }
92
+ const provider = {
93
+ AuditLogTypes: over.auditTypes ?? [{ ID: 'TYPE-1', Name: REALTIME_IDENTITY_VERIFIED_AUDIT_TYPE }],
94
+ EntityByName: (name: string) => (name === 'MJ: AI Agent Sessions' ? { ID: 'ENTITY-SESSIONS' } : undefined),
95
+ GetEntityObject: vi.fn(async (name: string, user: { ID: string }) => {
96
+ loadedAs.push(`${name}@${user.ID}`);
97
+ if (name === 'MJ: AI Agent Sessions') return session;
98
+ if (name === 'MJ: Conversations') return { ExternalID: over.externalId ?? null, Load: vi.fn(async () => true) };
99
+ if (name === 'MJ: Audit Logs') {
100
+ const row: Record<string, unknown> = {
101
+ NewRecord: vi.fn(),
102
+ Save: vi.fn(async () => over.auditSaves ?? true),
103
+ LatestResult: { CompleteMessage: 'audit refused' },
104
+ };
105
+ auditRows.push(row);
106
+ return row;
107
+ }
108
+ throw new Error(`unexpected entity ${name}`);
109
+ }),
110
+ } as unknown as IMetadataProvider;
111
+ Metadata.Provider = provider; // the public link and the audit write use the global provider
112
+ return { provider, session, saveScopes, auditRows, loadedAs };
113
+ }
114
+
115
+ const principalCaller = (provider: IMetadataProvider): VerificationCaller => ({
116
+ Kind: 'principal',
117
+ ContextUser: { ID: OWNER } as UserInfo,
118
+ Provider: provider,
119
+ ClientIp: '203.0.113.7',
120
+ });
121
+
122
+ describe('RuntimeVerificationPorts', () => {
123
+ let ports: RuntimeVerificationPorts;
124
+
125
+ beforeEach(() => {
126
+ ports = new RuntimeVerificationPorts();
127
+ mocks.serviceUser = { ID: 'SERVICE-USER' };
128
+ mocks.sendSingleMessage.mockReset();
129
+ mocks.engineConfig.mockReset();
130
+ const cfg = config.configInfo.realtime.identityVerification;
131
+ cfg.enabled = true;
132
+ cfg.communicationProvider = 'SendGrid';
133
+ cfg.hmacSecret = 'unit-test-hmac-secret-0123456789';
134
+ cfg.deliveryMode = 'send';
135
+ cfg.fromAddress = 'noreply@example.com';
136
+ cfg.publicBaseUrl = undefined;
137
+ cfg.policy = {};
138
+ cfg.rateLimits = {};
139
+ config.configInfo.publicUrl = 'https://api.example.com';
140
+ });
141
+
142
+ describe('GetSettings', () => {
143
+ it('is enabled only when switched on AND a communication provider is configured', () => {
144
+ expect(ports.GetSettings().Enabled).toBe(true);
145
+ config.configInfo.realtime.identityVerification.communicationProvider = undefined;
146
+ expect(ports.GetSettings().Enabled).toBe(false);
147
+ config.configInfo.realtime.identityVerification.communicationProvider = 'SendGrid';
148
+ config.configInfo.realtime.identityVerification.enabled = false;
149
+ expect(ports.GetSettings().Enabled).toBe(false);
150
+ });
151
+
152
+ it('is unavailable without a usable hmacSecret (fail closed), and exposes the key only as CodeHashKey', () => {
153
+ const cfg = config.configInfo.realtime.identityVerification;
154
+ expect(ports.GetSettings().CodeHashKey).toBe('unit-test-hmac-secret-0123456789');
155
+ cfg.hmacSecret = undefined;
156
+ expect(ports.GetSettings().Enabled).toBe(false);
157
+ cfg.hmacSecret = 'too-short';
158
+ expect(ports.GetSettings().Enabled).toBe(false);
159
+ });
160
+
161
+ it('builds the public base URL from the explicit setting, else publicUrl, else baseUrl:port — without a trailing slash', () => {
162
+ const cfg = config.configInfo.realtime.identityVerification;
163
+ expect(ports.GetSettings().PublicBaseUrl).toBe('https://api.example.com');
164
+ cfg.publicBaseUrl = 'https://verify.example.com//';
165
+ expect(ports.GetSettings().PublicBaseUrl).toBe('https://verify.example.com');
166
+ cfg.publicBaseUrl = undefined;
167
+ config.configInfo.publicUrl = '';
168
+ expect(ports.GetSettings().PublicBaseUrl).toBe('http://localhost:4000');
169
+ });
170
+
171
+ it('reads the policy base layer and the rate limits, with defaults', () => {
172
+ const cfg = config.configInfo.realtime.identityVerification;
173
+ cfg.policy = { requireBusinessDomain: true, linkTtlMinutes: 10, maxSendsPerSession: 'nope' };
174
+ cfg.rateLimits = { perIpSends: 4 };
175
+ const settings = ports.GetSettings();
176
+ expect(settings.PolicyDefaults).toEqual({ requireBusinessDomain: true, linkTtlMinutes: 10 });
177
+ expect(settings.RateLimits).toMatchObject({ PerIpSends: 4, PerEmailDomainSends: 30, PerEmailSends: 3, PerSessionCodeAttempts: 10, SendWindowMs: 3_600_000 });
178
+ });
179
+ });
180
+
181
+ describe('LoadSession', () => {
182
+ it('loads a principal\'s session AS THE PRINCIPAL on their own provider (RLS is the first gate)', async () => {
183
+ const h = harness({ externalId: 'scope-1' });
184
+ const snapshot = await ports.LoadSession(SESSION_ID, principalCaller(h.provider));
185
+ expect(snapshot).toMatchObject({ AgentSessionID: SESSION_ID, OwnerUserID: OWNER, ConversationExternalID: 'scope-1', Status: 'Active', StartedAtMs: Date.parse('2026-10-02T12:00:00.000Z') });
186
+ expect(h.loadedAs).toEqual([`MJ: AI Agent Sessions@${OWNER}`, `MJ: Conversations@${OWNER}`]);
187
+ });
188
+
189
+ it('loads for the public link as the SERVICE principal on the global provider', async () => {
190
+ const h = harness();
191
+ await ports.LoadSession(SESSION_ID, { Kind: 'link' });
192
+ expect(h.loadedAs[0]).toBe('MJ: AI Agent Sessions@SERVICE-USER');
193
+ });
194
+
195
+ it('returns null when there is no service principal for the link', async () => {
196
+ harness();
197
+ mocks.serviceUser = null;
198
+ expect(await ports.LoadSession(SESSION_ID, { Kind: 'link' })).toBeNull();
199
+ });
200
+ });
201
+
202
+ describe('MutateSessionConfig', () => {
203
+ it('re-reads the session as the SERVICE principal, writes the mutation INSIDE the trusted scope, and leaves the scope afterwards', async () => {
204
+ const h = harness();
205
+ const result = await ports.MutateSessionConfig(SESSION_ID, principalCaller(h.provider), (current) => ({
206
+ NextConfigRaw: JSON.stringify({ ...JSON.parse(current.ConfigRaw as string), identityVerification: { SendCount: 1 } }),
207
+ Outcome: 'done',
208
+ }));
209
+ expect(result).toMatchObject({ Persisted: true, Outcome: 'done' });
210
+ expect(h.loadedAs[0]).toBe('MJ: AI Agent Sessions@SERVICE-USER'); // never the owner: the owner's own write is exactly what the entity guard refuses
211
+ expect(JSON.parse(h.session.Config_ as string)).toEqual({ targetAgentID: 't', identityVerification: { SendCount: 1 } });
212
+ expect(h.saveScopes).toEqual([true]);
213
+ expect(IsTrustedSessionConfigWriteActive()).toBe(false);
214
+ });
215
+
216
+ it('does not write when the mutation declines', async () => {
217
+ const h = harness();
218
+ const result = await ports.MutateSessionConfig(SESSION_ID, principalCaller(h.provider), () => ({ NextConfigRaw: null, Outcome: 'noop' }));
219
+ expect(result).toMatchObject({ Persisted: false, Outcome: 'noop' });
220
+ expect(h.session.Save).not.toHaveBeenCalled();
221
+ });
222
+
223
+ it('reports a refused save with its reason', async () => {
224
+ const h = harness({ saveResult: false });
225
+ const result = await ports.MutateSessionConfig(SESSION_ID, principalCaller(h.provider), () => ({ NextConfigRaw: '{}', Outcome: 1 }));
226
+ expect(result).toMatchObject({ Persisted: false, Outcome: 1, ErrorMessage: 'validation refused' });
227
+ });
228
+
229
+ it('reports a missing session and a missing service principal without writing', async () => {
230
+ const h = harness();
231
+ h.session.Load.mockResolvedValueOnce(false);
232
+ expect(await ports.MutateSessionConfig(SESSION_ID, principalCaller(h.provider), () => ({ NextConfigRaw: '{}', Outcome: 1 }))).toMatchObject({ Persisted: false, ErrorMessage: expect.stringContaining('could not be read') });
233
+ mocks.serviceUser = null;
234
+ expect(await ports.MutateSessionConfig(SESSION_ID, principalCaller(h.provider), () => ({ NextConfigRaw: '{}', Outcome: 1 }))).toMatchObject({ Persisted: false, ErrorMessage: expect.stringContaining('service principal') });
235
+ expect(h.session.Save).not.toHaveBeenCalled();
236
+ });
237
+
238
+ it('turns a thrown error into a failed result instead of rejecting', async () => {
239
+ const h = harness();
240
+ h.session.Save.mockRejectedValueOnce(new Error('connection reset'));
241
+ const result = await ports.MutateSessionConfig(SESSION_ID, principalCaller(h.provider), () => ({ NextConfigRaw: '{}', Outcome: 1 }));
242
+ expect(result).toMatchObject({ Persisted: false, ErrorMessage: 'connection reset' });
243
+ });
244
+ });
245
+
246
+ describe('SendEmail', () => {
247
+ const email = { To: 'pat@acme.com', Subject: 'Confirm', Text: 'text', Html: '<p>html</p>' };
248
+
249
+ it('sends through the configured communication provider as the service principal', async () => {
250
+ mocks.sendSingleMessage.mockResolvedValue({ Success: true });
251
+ expect(await ports.SendEmail(email)).toEqual({ Success: true });
252
+ expect(mocks.engineConfig).toHaveBeenCalledWith(false, mocks.serviceUser);
253
+ const [providerName, type, message] = mocks.sendSingleMessage.mock.calls[0];
254
+ expect(providerName).toBe('SendGrid');
255
+ expect(type).toBe('Email');
256
+ expect(message).toMatchObject({ From: 'noreply@example.com', To: 'pat@acme.com', Subject: 'Confirm', Body: 'text', HTMLBody: '<p>html</p>' });
257
+ });
258
+
259
+ it('rehearsal delivery mode builds and logs the message but marks it DryRun so nothing is delivered', async () => {
260
+ config.configInfo.realtime.identityVerification.deliveryMode = 'dry-run';
261
+ mocks.sendSingleMessage.mockResolvedValue({ Success: true, DryRun: true });
262
+ expect(await ports.SendEmail(email)).toEqual({ Success: true });
263
+ expect(mocks.sendSingleMessage.mock.calls[0][2]).toMatchObject({ DryRun: true });
264
+ });
265
+
266
+ it('delivers for real by default (DryRun off)', async () => {
267
+ mocks.sendSingleMessage.mockResolvedValue({ Success: true });
268
+ await ports.SendEmail(email);
269
+ expect(mocks.sendSingleMessage.mock.calls[0][2].DryRun).toBe(false);
270
+ });
271
+
272
+ it('reports a provider failure', async () => {
273
+ mocks.sendSingleMessage.mockResolvedValue({ Success: false, Error: 'quota' });
274
+ expect(await ports.SendEmail(email)).toEqual({ Success: false, ErrorMessage: 'quota' });
275
+ });
276
+
277
+ it('reports a thrown error and a missing configuration without throwing', async () => {
278
+ mocks.sendSingleMessage.mockRejectedValue(new Error('network'));
279
+ expect(await ports.SendEmail(email)).toEqual({ Success: false, ErrorMessage: 'network' });
280
+ config.configInfo.realtime.identityVerification.communicationProvider = undefined;
281
+ expect(await ports.SendEmail(email)).toMatchObject({ Success: false, ErrorMessage: expect.stringContaining('No communication provider') });
282
+ });
283
+ });
284
+
285
+ describe('PublishVerified', () => {
286
+ it('publishes identity.verified through the event service with the routing it was given', () => {
287
+ const spy = vi.spyOn(RealtimeSessionEventService.Instance, 'PublishRouted').mockReturnValue({ Success: true });
288
+ const payload = { VerifiedEmail: 'a@b.co', VerifiedName: 'A', VerifiedAt: '2026-10-02T12:00:00.000Z', Method: 'link' as const };
289
+ const routing = { AgentSessionID: SESSION_ID, OwnerUserID: OWNER, ScopeKey: 'scope-1' };
290
+ expect(ports.PublishVerified(routing, payload)).toEqual({ Success: true });
291
+ expect(spy).toHaveBeenCalledWith(routing, 'identity.verified', payload);
292
+ spy.mockRestore();
293
+ });
294
+ });
295
+
296
+ describe('WriteAudit', () => {
297
+ const entry = { AgentSessionID: SESSION_ID, OwnerUserID: OWNER, Email: 'pat@acme.com', Method: 'code' as const, ClientIp: '203.0.113.7', MaxSessionDeadlineIso: '2026-10-02T13:00:00.000Z' };
298
+
299
+ it('writes an MJ audit-log row as the service principal, about the session', async () => {
300
+ const h = harness();
301
+ await ports.WriteAudit(entry);
302
+ expect(h.auditRows).toHaveLength(1);
303
+ expect(h.loadedAs).toContain('MJ: Audit Logs@SERVICE-USER');
304
+ expect(h.auditRows[0]).toMatchObject({
305
+ UserID: OWNER,
306
+ AuditLogTypeID: 'TYPE-1',
307
+ Status: 'Success',
308
+ EntityID: 'ENTITY-SESSIONS',
309
+ RecordID: SESSION_ID,
310
+ });
311
+ expect(String(h.auditRows[0].Description)).toContain('pat@acme.com');
312
+ expect(JSON.parse(h.auditRows[0].Details as string)).toEqual({ email: 'pat@acme.com', method: 'code', ipAddress: '203.0.113.7', maxSessionDeadlineIso: '2026-10-02T13:00:00.000Z' });
313
+ });
314
+
315
+ it('degrades to a server-log line (never throws, never silently drops) when the audit type is not seeded', async () => {
316
+ const h = harness({ auditTypes: [] });
317
+ await expect(ports.WriteAudit(entry)).resolves.toBeUndefined();
318
+ expect(h.auditRows).toHaveLength(0);
319
+ });
320
+
321
+ it('survives a refused audit save', async () => {
322
+ harness({ auditSaves: false });
323
+ await expect(ports.WriteAudit(entry)).resolves.toBeUndefined();
324
+ });
325
+ });
326
+ });
327
+
328
+ describe('RealtimeSessionVerificationService', () => {
329
+ it('is a process-wide singleton and reflects whether verification is enabled', () => {
330
+ expect(RealtimeSessionVerificationService.Instance).toBe(RealtimeSessionVerificationService.Instance);
331
+ config.configInfo.realtime.identityVerification.enabled = true;
332
+ config.configInfo.realtime.identityVerification.communicationProvider = 'SendGrid';
333
+ config.configInfo.realtime.identityVerification.hmacSecret = 'unit-test-hmac-secret-0123456789';
334
+ expect(RealtimeSessionVerificationService.Instance.IsEnabled).toBe(true);
335
+ config.configInfo.realtime.identityVerification.enabled = false;
336
+ expect(RealtimeSessionVerificationService.Instance.IsEnabled).toBe(false);
337
+ });
338
+
339
+ it('answers every operation with a uniform "unavailable" when disabled', async () => {
340
+ config.configInfo.realtime.identityVerification.enabled = false;
341
+ const service = RealtimeSessionVerificationService.Instance;
342
+ const h = harness();
343
+ const caller = principalCaller(h.provider) as Extract<VerificationCaller, { Kind: 'principal' }>;
344
+ expect(await service.RequestVerification({ AgentSessionID: SESSION_ID, Name: 'Pat', Email: 'pat@acme.com' }, caller)).toMatchObject({ ErrorCode: 'verification_unavailable' });
345
+ expect(await service.SubmitCode({ AgentSessionID: SESSION_ID, Code: '123456' }, caller)).toMatchObject({ ErrorCode: 'verification_unavailable' });
346
+ expect(await service.GetStatus({ AgentSessionID: SESSION_ID }, caller)).toMatchObject({ ErrorCode: 'verification_unavailable' });
347
+ expect(await service.RedeemLink('mj_rv_x', '192.0.2.1')).toMatchObject({ ErrorCode: 'verification_unavailable' });
348
+ });
349
+ });
@@ -0,0 +1,131 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import express from 'express';
3
+ import type { Server } from 'node:http';
4
+ import type { AddressInfo } from 'node:net';
5
+
6
+ vi.mock('../../config.js', () => ({
7
+ configInfo: { realtime: { identityVerification: { rateLimits: { redeemWindowMs: 60_000, redeemPerIp: 5 } } } },
8
+ }));
9
+ // The router only needs the service's RedeemLink; keep the heavy service module out of this test.
10
+ vi.mock('../../realtimeSessions/RealtimeSessionVerificationService.js', () => ({ RealtimeSessionVerificationService: { Instance: {} } }));
11
+
12
+ import { CreateRealtimeVerifyRouter, REALTIME_VERIFY_MOUNT_PATH } from '../../realtimeSessions/RealtimeVerifyRouter.js';
13
+ import type { VerificationOperationResult } from '../../realtimeSessions/verificationWorkflow.js';
14
+
15
+ const TOKEN = `mj_rv_${'a'.repeat(32)}_${'b'.repeat(64)}`;
16
+
17
+ describe('RealtimeVerifyRouter', () => {
18
+ let server: Server;
19
+ let base: string;
20
+ const redeemLink = vi.fn<(token: string, clientIp?: string) => Promise<VerificationOperationResult>>();
21
+
22
+ beforeEach(async () => {
23
+ redeemLink.mockReset();
24
+ const app = express();
25
+ app.use(REALTIME_VERIFY_MOUNT_PATH, CreateRealtimeVerifyRouter({ RedeemLink: redeemLink }));
26
+ await new Promise<void>((resolve) => {
27
+ server = app.listen(0, '127.0.0.1', resolve);
28
+ });
29
+ base = `http://127.0.0.1:${(server.address() as AddressInfo).port}${REALTIME_VERIFY_MOUNT_PATH}`;
30
+ });
31
+
32
+ afterEach(async () => {
33
+ await new Promise<void>((resolve) => server.close(() => resolve()));
34
+ });
35
+
36
+ const post = (token: string) =>
37
+ fetch(base, { method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ token }).toString() });
38
+
39
+ describe('GET /:token', () => {
40
+ it('renders the confirmation page and NEVER redeems (mail scanners GET every link)', async () => {
41
+ const res = await fetch(`${base}/${TOKEN}`);
42
+ expect(res.status).toBe(200);
43
+ const html = await res.text();
44
+ expect(html).toContain('Confirm your email address');
45
+ expect(html).toContain(`name="token" value="${TOKEN}"`);
46
+ expect(redeemLink).not.toHaveBeenCalled();
47
+ });
48
+
49
+ it('does the same for a garbage token — the page cannot tell a real link from a fake one', async () => {
50
+ const real = await (await fetch(`${base}/${TOKEN}`)).text();
51
+ const fake = await (await fetch(`${base}/not-a-token`)).text();
52
+ expect(fake.replace('not-a-token', TOKEN)).toBe(real);
53
+ expect(redeemLink).not.toHaveBeenCalled();
54
+ });
55
+
56
+ it('escapes a hostile token', async () => {
57
+ const res = await fetch(`${base}/${encodeURIComponent('"><script>alert(1)</script>')}`);
58
+ expect(await res.text()).not.toContain('<script>');
59
+ });
60
+
61
+ it('sends the hardening headers on every response', async () => {
62
+ const res = await fetch(`${base}/${TOKEN}`);
63
+ expect(res.headers.get('cache-control')).toBe('no-store');
64
+ expect(res.headers.get('referrer-policy')).toBe('no-referrer');
65
+ expect(res.headers.get('x-frame-options')).toBe('DENY');
66
+ expect(res.headers.get('x-content-type-options')).toBe('nosniff');
67
+ expect(res.headers.get('content-security-policy')).toContain("default-src 'none'");
68
+ expect(res.headers.get('content-security-policy')).toContain("frame-ancestors 'none'");
69
+ expect(res.headers.get('content-type')).toContain('text/html');
70
+ });
71
+ });
72
+
73
+ describe('POST /', () => {
74
+ it('redeems the token from the form body and renders the success page', async () => {
75
+ redeemLink.mockResolvedValue({ Success: true, VerificationState: 'verified' });
76
+ const res = await post(TOKEN);
77
+ expect(res.status).toBe(200);
78
+ expect(await res.text()).toContain("You're verified");
79
+ expect(redeemLink).toHaveBeenCalledWith(TOKEN, expect.any(String));
80
+ });
81
+
82
+ it('renders ONE uniform 410 page for every refusal — the response reveals nothing about any session', async () => {
83
+ const bodies: string[] = [];
84
+ for (const code of ['invalid_link', 'verification_unavailable', 'session_not_found'] as const) {
85
+ redeemLink.mockResolvedValueOnce({ Success: false, VerificationState: 'unverified', ErrorCode: code });
86
+ const res = await post(TOKEN);
87
+ expect(res.status).toBe(410);
88
+ bodies.push(await res.text());
89
+ }
90
+ expect(new Set(bodies).size).toBe(1);
91
+ expect(bodies[0]).toContain('no longer valid');
92
+ });
93
+
94
+ it('says "try again" (500) when a valid link could not be recorded', async () => {
95
+ redeemLink.mockResolvedValue({ Success: false, VerificationState: 'unverified', ErrorCode: 'persist_failed' });
96
+ const res = await post(TOKEN);
97
+ expect(res.status).toBe(500);
98
+ expect(await res.text()).toContain('open the link from your email again');
99
+ });
100
+
101
+ it('survives the service throwing', async () => {
102
+ redeemLink.mockRejectedValue(new Error('db down'));
103
+ const res = await post(TOKEN);
104
+ expect(res.status).toBe(500);
105
+ expect(await res.text()).not.toContain('db down');
106
+ });
107
+
108
+ it('passes an empty token through rather than inventing one, and ignores a query-string token', async () => {
109
+ redeemLink.mockResolvedValue({ Success: false, VerificationState: 'unverified', ErrorCode: 'invalid_link' });
110
+ await fetch(`${base}?token=${TOKEN}`, { method: 'POST' });
111
+ expect(redeemLink).toHaveBeenCalledWith('', expect.any(String));
112
+ });
113
+
114
+ it('rejects an oversized body', async () => {
115
+ const res = await post('x'.repeat(10_000));
116
+ expect(res.status).toBeGreaterThanOrEqual(400);
117
+ expect(redeemLink).not.toHaveBeenCalled();
118
+ });
119
+ });
120
+
121
+ it('throttles per IP and answers with the failure page', async () => {
122
+ redeemLink.mockResolvedValue({ Success: true, VerificationState: 'verified' });
123
+ const statuses: number[] = [];
124
+ for (let i = 0; i < 7; i++) {
125
+ statuses.push((await post(TOKEN)).status);
126
+ }
127
+ expect(statuses.slice(0, 5)).toEqual([200, 200, 200, 200, 200]);
128
+ expect(statuses.slice(5)).toEqual([429, 429]);
129
+ expect(redeemLink).toHaveBeenCalledTimes(5);
130
+ });
131
+ });
@@ -0,0 +1,56 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SlidingWindowRateLimiter } from '../../realtimeSessions/SlidingWindowRateLimiter.js';
3
+
4
+ describe('SlidingWindowRateLimiter', () => {
5
+ it('allows up to the limit within the window, then refuses with a retry-after', () => {
6
+ const limiter = new SlidingWindowRateLimiter({ Limit: 3, WindowMs: 1000 });
7
+ expect(limiter.TryConsume('k', 0).Allowed).toBe(true);
8
+ expect(limiter.TryConsume('k', 100).Allowed).toBe(true);
9
+ expect(limiter.TryConsume('k', 200).Allowed).toBe(true);
10
+ const refused = limiter.TryConsume('k', 300);
11
+ expect(refused).toEqual({ Allowed: false, RetryAfterMs: 700 });
12
+ });
13
+
14
+ it('lets events back in as they slide out of the window', () => {
15
+ const limiter = new SlidingWindowRateLimiter({ Limit: 2, WindowMs: 1000 });
16
+ limiter.TryConsume('k', 0);
17
+ limiter.TryConsume('k', 500);
18
+ expect(limiter.TryConsume('k', 999).Allowed).toBe(false);
19
+ expect(limiter.TryConsume('k', 1001).Allowed).toBe(true); // the t=0 event has left the window
20
+ expect(limiter.TryConsume('k', 1002).Allowed).toBe(false); // 500 and 1001 are still inside
21
+ });
22
+
23
+ it('does not count refused attempts against the budget', () => {
24
+ const limiter = new SlidingWindowRateLimiter({ Limit: 1, WindowMs: 1000 });
25
+ limiter.TryConsume('k', 0);
26
+ for (let t = 1; t < 900; t += 100) {
27
+ expect(limiter.TryConsume('k', t).Allowed).toBe(false);
28
+ }
29
+ expect(limiter.TryConsume('k', 1001).Allowed).toBe(true);
30
+ });
31
+
32
+ it('keeps keys independent', () => {
33
+ const limiter = new SlidingWindowRateLimiter({ Limit: 1, WindowMs: 1000 });
34
+ expect(limiter.TryConsume('a', 0).Allowed).toBe(true);
35
+ expect(limiter.TryConsume('b', 0).Allowed).toBe(true);
36
+ expect(limiter.TryConsume('a', 1).Allowed).toBe(false);
37
+ });
38
+
39
+ it('treats a non-positive limit as disabled', () => {
40
+ const limiter = new SlidingWindowRateLimiter({ Limit: 0, WindowMs: 1000 });
41
+ for (let i = 0; i < 50; i++) {
42
+ expect(limiter.TryConsume('k', i).Allowed).toBe(true);
43
+ }
44
+ expect(limiter.TrackedKeyCount).toBe(0);
45
+ });
46
+
47
+ it('bounds memory: past MaxKeys the oldest keys are evicted (and simply start fresh)', () => {
48
+ const limiter = new SlidingWindowRateLimiter({ Limit: 1, WindowMs: 10_000, MaxKeys: 3 });
49
+ for (const key of ['a', 'b', 'c', 'd']) {
50
+ limiter.TryConsume(key, 0);
51
+ }
52
+ expect(limiter.TrackedKeyCount).toBe(3);
53
+ expect(limiter.TryConsume('a', 1).Allowed).toBe(true); // evicted, so it starts a new window
54
+ expect(limiter.TryConsume('d', 1).Allowed).toBe(false); // still tracked
55
+ });
56
+ });
@@ -0,0 +1,4 @@
1
+ /** Test helper: parse a session `Config` JSON string into a plain object. */
2
+ export function parseConfigObjectForTest(raw: string | null): Record<string, unknown> {
3
+ return raw ? (JSON.parse(raw) as Record<string, unknown>) : {};
4
+ }