@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,645 @@
1
+ import { WebSocket as WsClient, type RawData } from 'ws';
2
+ import {
3
+ REALTIME_RELAY_TIME_LIMIT_CLOSE_CODE,
4
+ RealtimeProxyRegistry,
5
+ type IRealtimeRelayPolicy,
6
+ type RealtimeRelayFrameVerdict,
7
+ type RealtimeRelayGrant,
8
+ type RealtimeRelayOpenIntent,
9
+ } from '@memberjunction/ai';
10
+
11
+ /** The relay's deadlines, pings, buffers and caps. */
12
+ export interface RealtimeRelayLimits {
13
+ /** How long the browser has to send its first frame after the upgrade, in ms. */
14
+ FirstFrameTimeoutMs: number;
15
+ /** How long the upstream has to open, from the header fetch to the end of its handshake, in ms. */
16
+ UpstreamOpenTimeoutMs: number;
17
+ /** How often the relay pings each leg, in ms. */
18
+ PingIntervalMs: number;
19
+ /**
20
+ * Pings a leg may leave unanswered in a row. At the next ping a leg that answered none of them is terminated and the
21
+ * other leg is closed (any pong resets the count).
22
+ */
23
+ MaxMissedPongs: number;
24
+ /** Client frames held while the upstream opens; past this the oldest are dropped. */
25
+ MaxPendingFrames: number;
26
+ /** Bytes of client frames held while the upstream opens; past this the oldest are dropped. */
27
+ MaxPendingBytes: number;
28
+ /**
29
+ * Bytes the browser leg may have queued after a downstream send. Past this the browser cannot keep up: the tunnel
30
+ * closes (1013 to the browser, 1001 upstream) and the client resumes on a new connection.
31
+ */
32
+ MaxBrowserBufferedBytes: number;
33
+ /**
34
+ * Bytes the upstream leg may have queued after an upstream send. Past this the provider has stopped reading: the
35
+ * tunnel closes (1013 to the browser, 1001 upstream) and the client resumes on a new connection.
36
+ */
37
+ MaxUpstreamBufferedBytes: number;
38
+ }
39
+
40
+ /** The limits MJAPI runs the relay with. */
41
+ export const REALTIME_RELAY_LIMITS: Readonly<RealtimeRelayLimits> = Object.freeze({
42
+ FirstFrameTimeoutMs: 10_000,
43
+ UpstreamOpenTimeoutMs: 15_000,
44
+ PingIntervalMs: 20_000,
45
+ MaxMissedPongs: 2,
46
+ MaxPendingFrames: 512,
47
+ MaxPendingBytes: 8 * 1024 * 1024,
48
+ MaxBrowserBufferedBytes: 16 * 1024 * 1024,
49
+ MaxUpstreamBufferedBytes: 16 * 1024 * 1024,
50
+ });
51
+
52
+ /**
53
+ * The largest client message the relay accepts (2 MiB). The relay's websocket server enforces it while reading, so a
54
+ * larger message closes the browser leg with 1009 before it is buffered.
55
+ */
56
+ export const REALTIME_RELAY_MAX_CLIENT_FRAME_BYTES = 2 * 1024 * 1024;
57
+
58
+ /** Close codes the relay sends itself (RFC 6455 §7.4.1). */
59
+ const CLOSE_NORMAL = 1000; // the session ended, or a resume replaced this connection
60
+ const CLOSE_GOING_AWAY = 1001; // MJAPI is shutting down, or the browser left, stopped answering or fell behind
61
+ const CLOSE_NO_STATUS = 1005; // a close without a code: passed on as a close without a code
62
+ const CLOSE_POLICY = 1008; // no opening frame, or the session refused the connection
63
+ const CLOSE_INTERNAL = 1011; // the policy failed (headers, opening frames), or a send failed
64
+ const CLOSE_TRY_AGAIN = 1013; // a leg fell too far behind; the browser may resume on a new connection
65
+ const CLOSE_BAD_GATEWAY = 1014; // the upstream did not open, failed, stopped answering, or dropped without a close code
66
+
67
+ /** The two legs of a tunnel. */
68
+ type RelayLeg = 'browser' | 'upstream';
69
+ const RELAY_LEGS: readonly RelayLeg[] = ['browser', 'upstream'];
70
+
71
+ /** The longest delay `setTimeout` takes (about 24.8 days); a longer one would fire at once. */
72
+ const MAX_TIMER_MS = 2_147_483_647;
73
+
74
+ /** Distinct drop labels counted per connection; later new labels count as `other`. */
75
+ const MAX_DROP_LABELS = 32;
76
+
77
+ /** A close to send on a leg: no code means a close frame without one; `Terminate` drops the socket without a close frame. */
78
+ interface RelayClose {
79
+ Code?: number;
80
+ Reason?: string | Buffer;
81
+ Terminate?: boolean;
82
+ }
83
+
84
+ /** How a relay tunnel reports to the server that owns it. */
85
+ export interface RealtimeRelayTunnelHost {
86
+ /** The session granted this connection: the server makes it the session's live one and closes the older one. */
87
+ OnGranted(tunnel: RealtimeRelayTunnel): void;
88
+ /** The tunnel closed; called once. */
89
+ OnClosed(tunnel: RealtimeRelayTunnel): void;
90
+ }
91
+
92
+ /** The first 8 characters of a ticket id, safe to log (never the whole id). */
93
+ export function RelayTicketPrefix(id: string): string {
94
+ return id.replace(/[^A-Za-z0-9-]/g, '').slice(0, 8) || '-';
95
+ }
96
+
97
+ /**
98
+ * One browser connection through MJAPI's realtime relay (`/realtime/relay/<ticket>/…`).
99
+ *
100
+ * It waits for the browser's first frame, asks the session's policy what that frame opens, and lets the registry
101
+ * decide (one fresh connection, then resumes with a forwarded handle). Then it opens the upstream with headers the
102
+ * policy mints for this connection, sends the policy's opening frames before anything else, and pumps both ways: client
103
+ * frames through the policy's filter (text only; binary client frames are dropped), server frames unchanged once the
104
+ * policy has looked for a resumption handle. It pings both legs and terminates one that stops answering, closes a
105
+ * browser that cannot keep up with what it is sent and an upstream that stops reading, passes close codes through, and closes when the session ends. Logs
106
+ * carry counts, sizes, codes and an 8-character ticket prefix, never payloads, headers or tokens.
107
+ */
108
+ export class RealtimeRelayTunnel {
109
+ private upstream: WsClient | null = null;
110
+ private grant: RealtimeRelayGrant | null = null;
111
+ private intent: RealtimeRelayOpenIntent | null = null;
112
+ /** Client frames that passed the filter before the upstream opened, in arrival order. */
113
+ private readonly pending: string[] = [];
114
+ private pendingBytes = 0;
115
+ /** Dropped client frames per type label. */
116
+ private readonly drops = new Map<string, number>();
117
+ private readonly traffic = { FramesUp: 0, BytesUp: 0, FramesDown: 0, BytesDown: 0 };
118
+ /** Pings each leg has left unanswered since its last pong. */
119
+ private readonly unanswered: Record<RelayLeg, number> = { browser: 0, upstream: 0 };
120
+ private firstFrameTimer: ReturnType<typeof setTimeout> | null = null;
121
+ private upstreamTimer: ReturnType<typeof setTimeout> | null = null;
122
+ private lifetimeTimer: ReturnType<typeof setTimeout> | null = null;
123
+ private pingTimer: ReturnType<typeof setInterval> | null = null;
124
+ private openedAtMs = 0;
125
+ private browserCloseCode: number | null = null;
126
+ private upstreamCloseNote = '';
127
+ private closed = false;
128
+
129
+ constructor(
130
+ private readonly browser: WsClient,
131
+ private readonly sessionId: string,
132
+ private readonly policy: IRealtimeRelayPolicy,
133
+ private readonly limits: RealtimeRelayLimits,
134
+ private readonly host: RealtimeRelayTunnelHost
135
+ ) {}
136
+
137
+ /** The relay session this connection belongs to. */
138
+ public get SessionID(): string {
139
+ return this.sessionId;
140
+ }
141
+
142
+ /** Starts the first-frame deadline and the pings, and listens to the browser leg. */
143
+ public Start(): void {
144
+ this.firstFrameTimer = RealtimeRelayTunnel.after(this.limits.FirstFrameTimeoutMs, () => this.refuse('no opening frame in time'));
145
+ this.pingTimer = setInterval(() => this.pingBoth(), this.limits.PingIntervalMs);
146
+ (this.pingTimer as { unref?: () => void }).unref?.();
147
+ this.browser.on('message', (data: RawData, isBinary: boolean) => this.onBrowserMessage(data, isBinary));
148
+ this.browser.on('pong', () => (this.unanswered.browser = 0));
149
+ this.browser.on('close', (code: number, reason: Buffer) => this.onBrowserClose(code, reason));
150
+ this.browser.on('error', (err: Error) => this.onBrowserError(err));
151
+ }
152
+
153
+ /** Closes both legs with `code` and `reason` (no code: a close frame without one). Idempotent. */
154
+ public Close(code?: number, reason?: string): void {
155
+ this.closeWith({ Code: code, Reason: reason });
156
+ }
157
+
158
+ // ── Opening ─────────────────────────────────────────────────────────────────────────────────────
159
+
160
+ private onBrowserMessage(data: RawData, isBinary: boolean): void {
161
+ if (this.closed) {
162
+ return;
163
+ }
164
+ if (this.grant === null) {
165
+ this.openFromFirstFrame(data, isBinary);
166
+ return;
167
+ }
168
+ this.fromBrowser(data, isBinary);
169
+ }
170
+
171
+ /** Reads the first frame's intent, asks the registry, and opens the upstream when the connection is granted. */
172
+ private openFromFirstFrame(data: RawData, isBinary: boolean): void {
173
+ RealtimeRelayTunnel.cancel(this.firstFrameTimer);
174
+ this.firstFrameTimer = null;
175
+ const intent = isBinary ? null : this.readIntent(bytesOf(data).toString('utf8'));
176
+ if (!intent) {
177
+ this.refuse('the first frame opens no session');
178
+ return;
179
+ }
180
+ const result = RealtimeProxyRegistry.Instance.OpenRelaySession(this.sessionId, intent);
181
+ if ('Refused' in result) {
182
+ this.refuse(result.Refused);
183
+ return;
184
+ }
185
+ this.intent = intent;
186
+ this.grant = result.Granted;
187
+ this.openedAtMs = Date.now();
188
+ this.host.OnGranted(this);
189
+ // The browser gets the time-limit code, so the call ends with its own message rather than a resume attempt.
190
+ this.lifetimeTimer = RealtimeRelayTunnel.after(this.grant.ExpiresAtMs - this.openedAtMs, () =>
191
+ this.closeWith(
192
+ { Code: REALTIME_RELAY_TIME_LIMIT_CLOSE_CODE, Reason: 'Call time limit reached' },
193
+ { Code: CLOSE_NORMAL, Reason: 'Relay session ended' },
194
+ )
195
+ );
196
+ this.logOpened(this.grant, intent);
197
+ void this.openUpstream(this.grant);
198
+ }
199
+
200
+ /** The policy's reading of the first frame; a policy that throws opens nothing (only the error's name is logged). */
201
+ private readIntent(text: string): RealtimeRelayOpenIntent | null {
202
+ try {
203
+ return this.policy.ReadOpenIntent(text);
204
+ } catch (err) {
205
+ this.warn(`the policy could not read the first frame (${errorName(err)})`);
206
+ return null;
207
+ }
208
+ }
209
+
210
+ /** Opens the upstream with headers the policy mints now, under the upstream deadline. */
211
+ private async openUpstream(grant: RealtimeRelayGrant): Promise<void> {
212
+ this.upstreamTimer = RealtimeRelayTunnel.after(this.limits.UpstreamOpenTimeoutMs, () => {
213
+ this.warn(`the upstream did not open within ${this.limits.UpstreamOpenTimeoutMs}ms`);
214
+ this.closeWith({ Code: CLOSE_BAD_GATEWAY, Reason: 'Upstream did not open in time' });
215
+ });
216
+ const headers = await this.fetchHeaders(grant);
217
+ if (this.closed || headers === null) {
218
+ return;
219
+ }
220
+ const upstream = this.connectUpstream(grant.UpstreamUrl, headers);
221
+ if (!upstream) {
222
+ return;
223
+ }
224
+ this.upstream = upstream;
225
+ upstream.on('open', () => this.onUpstreamOpen());
226
+ upstream.on('message', (data: RawData, isBinary: boolean) => this.onUpstreamMessage(data, isBinary));
227
+ upstream.on('pong', () => (this.unanswered.upstream = 0));
228
+ upstream.on('close', (code: number, reason: Buffer) => this.onUpstreamClose(code, reason));
229
+ upstream.on('error', (err: Error) => this.onUpstreamError(err));
230
+ }
231
+
232
+ /** Asks the policy for this connection's upstream headers; a failure closes the tunnel with 1011. */
233
+ private async fetchHeaders(grant: RealtimeRelayGrant): Promise<Record<string, string> | null> {
234
+ try {
235
+ return await grant.Policy.UpstreamHeaders();
236
+ } catch (err) {
237
+ this.warn(`the policy could not supply upstream headers (${errorSummary(err)})`);
238
+ this.closeWith({ Code: CLOSE_INTERNAL, Reason: 'Upstream credentials unavailable' });
239
+ return null;
240
+ }
241
+ }
242
+
243
+ /** Starts the upstream handshake; a refused URL closes the tunnel with 1014. */
244
+ private connectUpstream(url: string, headers: Record<string, string>): WsClient | null {
245
+ try {
246
+ return new WsClient(url, { headers });
247
+ } catch (err) {
248
+ // The constructor's message can quote the URL, and a URL can carry a key, so only the name is logged.
249
+ this.warn(`the upstream URL was refused (${errorName(err)})`);
250
+ this.closeWith({ Code: CLOSE_BAD_GATEWAY, Reason: 'Upstream unavailable' });
251
+ return null;
252
+ }
253
+ }
254
+
255
+ /** The upstream is open: the policy's opening frames go first, then what the browser sent meanwhile. */
256
+ private onUpstreamOpen(): void {
257
+ if (this.closed || !this.intent) {
258
+ return;
259
+ }
260
+ RealtimeRelayTunnel.cancel(this.upstreamTimer);
261
+ this.upstreamTimer = null;
262
+ const opening = this.openingFrames(this.intent);
263
+ if (opening === null) {
264
+ return;
265
+ }
266
+ for (const frame of opening) {
267
+ this.sendUp(frame);
268
+ }
269
+ this.pendingBytes = 0;
270
+ for (const frame of this.pending.splice(0)) {
271
+ this.sendUp(frame);
272
+ }
273
+ }
274
+
275
+ /** The policy's opening frames; a policy that throws closes the tunnel with 1011. */
276
+ private openingFrames(intent: RealtimeRelayOpenIntent): string[] | null {
277
+ try {
278
+ return this.policy.OpeningFrames(intent);
279
+ } catch (err) {
280
+ this.warn(`the policy could not write the opening frames (${errorName(err)})`);
281
+ this.closeWith({ Code: CLOSE_INTERNAL, Reason: 'Relay could not open the session' });
282
+ return null;
283
+ }
284
+ }
285
+
286
+ // ── Pumping ─────────────────────────────────────────────────────────────────────────────────────
287
+
288
+ /** A client frame after the first: binary is dropped, text goes through the policy's filter. */
289
+ private fromBrowser(data: RawData, isBinary: boolean): void {
290
+ if (isBinary) {
291
+ this.countDrop('binary');
292
+ return;
293
+ }
294
+ const verdict = this.filter(bytesOf(data).toString('utf8'));
295
+ if ('Drop' in verdict) {
296
+ this.countDrop(verdict.Drop);
297
+ return;
298
+ }
299
+ if (this.upstream?.readyState === WsClient.OPEN) {
300
+ this.sendUp(verdict.Forward);
301
+ return;
302
+ }
303
+ this.hold(verdict.Forward);
304
+ }
305
+
306
+ /** The policy's verdict on a client frame; a policy that throws drops the frame. */
307
+ private filter(text: string): RealtimeRelayFrameVerdict {
308
+ try {
309
+ return this.policy.FilterClientFrame(text);
310
+ } catch {
311
+ return { Drop: 'policy-error' };
312
+ }
313
+ }
314
+
315
+ /**
316
+ * Holds a frame until the upstream opens; past either cap (frames or bytes) the oldest go (stale audio is worthless
317
+ * anyway). A frame larger than the byte cap on its own is dropped.
318
+ */
319
+ private hold(frame: string): void {
320
+ this.pending.push(frame);
321
+ this.pendingBytes += Buffer.byteLength(frame);
322
+ while (
323
+ this.pending.length > 0 &&
324
+ (this.pending.length > this.limits.MaxPendingFrames || this.pendingBytes > this.limits.MaxPendingBytes)
325
+ ) {
326
+ this.pendingBytes -= Buffer.byteLength(this.pending.shift()!);
327
+ this.countDrop('pre-open-overflow');
328
+ }
329
+ }
330
+
331
+ /**
332
+ * A server frame: a handle it carries is recorded before the frame goes to the browser, unchanged; then the browser
333
+ * leg's queue is checked against the cap.
334
+ */
335
+ private onUpstreamMessage(data: RawData, isBinary: boolean): void {
336
+ if (this.closed) {
337
+ return;
338
+ }
339
+ const bytes = bytesOf(data);
340
+ const handle = this.observe(bytes, isBinary);
341
+ if (handle) {
342
+ RealtimeProxyRegistry.Instance.RecordRelayHandle(this.sessionId, handle);
343
+ }
344
+ if (this.send(this.browser, bytes, isBinary)) {
345
+ this.traffic.FramesDown += 1;
346
+ this.traffic.BytesDown += bytes.length;
347
+ this.closeIfBrowserBehind();
348
+ }
349
+ }
350
+
351
+ /**
352
+ * A browser that cannot keep up (its queued bytes pass `MaxBrowserBufferedBytes` after a downstream send) is closed
353
+ * with 1013 and the upstream with 1001; the client resumes on a new connection.
354
+ */
355
+ private closeIfBrowserBehind(): void {
356
+ const queued = this.browser.bufferedAmount;
357
+ if (queued <= this.limits.MaxBrowserBufferedBytes) {
358
+ return;
359
+ }
360
+ const t = this.traffic;
361
+ this.warn(
362
+ `the browser leg has ${queued} B queued (cap ${this.limits.MaxBrowserBufferedBytes} B) after ` +
363
+ `${t.FramesDown} frames / ${t.BytesDown} B down; closing`
364
+ );
365
+ this.closeWith({ Code: CLOSE_TRY_AGAIN, Reason: 'Relay buffer full' }, { Code: CLOSE_GOING_AWAY, Reason: 'Browser too slow' });
366
+ }
367
+
368
+ /** The resumption handle the policy finds in a server frame, if any; a policy that throws finds none. */
369
+ private observe(bytes: Buffer, isBinary: boolean): string | null {
370
+ try {
371
+ const handle = this.policy.ObserveServerFrame(bytes, isBinary);
372
+ return typeof handle === 'string' && handle.length > 0 ? handle : null;
373
+ } catch {
374
+ return null;
375
+ }
376
+ }
377
+
378
+ /** Sends one text frame upstream and counts it; then the upstream leg's queue is checked against the cap. */
379
+ private sendUp(frame: string): void {
380
+ if (this.send(this.upstream, frame, false)) {
381
+ this.traffic.FramesUp += 1;
382
+ this.traffic.BytesUp += Buffer.byteLength(frame);
383
+ this.closeIfUpstreamBehind();
384
+ }
385
+ }
386
+
387
+ /**
388
+ * An upstream that has stopped reading (its queued bytes pass `MaxUpstreamBufferedBytes` after an upstream send) is
389
+ * closed with 1001 and the browser with 1013, before the ping watchdog would notice; the client resumes on a new
390
+ * connection.
391
+ */
392
+ private closeIfUpstreamBehind(): void {
393
+ const queued = this.upstream?.bufferedAmount ?? 0;
394
+ if (queued <= this.limits.MaxUpstreamBufferedBytes) {
395
+ return;
396
+ }
397
+ const t = this.traffic;
398
+ this.warn(
399
+ `the upstream leg has ${queued} B queued (cap ${this.limits.MaxUpstreamBufferedBytes} B) after ` +
400
+ `${t.FramesUp} frames / ${t.BytesUp} B up; closing`
401
+ );
402
+ this.closeWith({ Code: CLOSE_TRY_AGAIN, Reason: 'Relay buffer full' }, { Code: CLOSE_GOING_AWAY, Reason: 'Upstream too slow' });
403
+ }
404
+
405
+ /** Sends on a leg that is open, keeping text as text and binary as binary; a failed send closes the tunnel. */
406
+ private send(target: WsClient | null, data: string | Buffer, isBinary: boolean): boolean {
407
+ if (!target || target.readyState !== WsClient.OPEN) {
408
+ return false;
409
+ }
410
+ try {
411
+ target.send(data, { binary: isBinary });
412
+ return true;
413
+ } catch {
414
+ this.closeWith({ Code: CLOSE_INTERNAL, Reason: 'Relay send failed' });
415
+ return false;
416
+ }
417
+ }
418
+
419
+ /**
420
+ * Pings each open leg, so idle timeouts on load balancers and ingresses do not close it. A leg that answered none of
421
+ * its last `MaxMissedPongs` pings is gone: it is terminated and the other leg closed.
422
+ */
423
+ private pingBoth(): void {
424
+ for (const leg of RELAY_LEGS) {
425
+ const sock = leg === 'browser' ? this.browser : this.upstream;
426
+ if (this.closed || sock?.readyState !== WsClient.OPEN) {
427
+ continue;
428
+ }
429
+ if (this.unanswered[leg] >= this.limits.MaxMissedPongs) {
430
+ this.closeSilentLeg(leg);
431
+ return;
432
+ }
433
+ this.unanswered[leg] += 1;
434
+ try {
435
+ sock.ping();
436
+ } catch {
437
+ /* closing */
438
+ }
439
+ }
440
+ }
441
+
442
+ /** A leg stopped answering pings: it is terminated, and the other leg closes (1001 upstream, 1014 to the browser). */
443
+ private closeSilentLeg(leg: RelayLeg): void {
444
+ this.warn(`the ${leg} leg answered none of the last ${this.limits.MaxMissedPongs} pings; closing`);
445
+ if (leg === 'browser') {
446
+ this.closeWith({ Terminate: true }, { Code: CLOSE_GOING_AWAY, Reason: 'Browser stopped answering' });
447
+ return;
448
+ }
449
+ this.upstreamCloseNote = 'stopped answering pings';
450
+ this.closeWith({ Code: CLOSE_BAD_GATEWAY, Reason: 'Upstream stopped answering' }, { Terminate: true });
451
+ }
452
+
453
+ // ── Closing ─────────────────────────────────────────────────────────────────────────────────────
454
+
455
+ /** The upstream closed: the browser gets the same code and reason (1014 when the upstream sent none). */
456
+ private onUpstreamClose(code: number, reason: Buffer): void {
457
+ this.upstreamCloseNote = reason.length > 0 ? `${code} (${safeText(reason)})` : String(code);
458
+ this.closeWith(passClose(code, reason, { Code: CLOSE_BAD_GATEWAY, Reason: 'Upstream connection lost' }));
459
+ }
460
+
461
+ /** The browser closed: the upstream gets the same code and reason (1001 when the browser sent none). */
462
+ private onBrowserClose(code: number, reason: Buffer): void {
463
+ this.browserCloseCode = code;
464
+ this.closeWith(passClose(code, reason, { Code: CLOSE_GOING_AWAY, Reason: 'Browser connection lost' }));
465
+ }
466
+
467
+ /** The upstream failed (a refused handshake, a dropped socket): the browser gets 1014. */
468
+ private onUpstreamError(err: Error): void {
469
+ if (this.closed) {
470
+ return;
471
+ }
472
+ this.upstreamCloseNote = 'error';
473
+ this.warn(`upstream error (${errorSummary(err)})`);
474
+ this.closeWith({ Code: CLOSE_BAD_GATEWAY, Reason: 'Upstream connection failed' });
475
+ }
476
+
477
+ /** The browser leg failed; `ws` has already closed it with the right code (1009 for an oversized frame). */
478
+ private onBrowserError(err: Error): void {
479
+ if (this.closed) {
480
+ return;
481
+ }
482
+ this.warn(`browser leg error (${errorSummary(err)})`);
483
+ this.closeWith({ Code: CLOSE_GOING_AWAY, Reason: 'Browser connection lost' });
484
+ }
485
+
486
+ /**
487
+ * Closes both legs (the upstream with the browser's close unless it gets its own), stops the timers and reports to
488
+ * the server. Idempotent.
489
+ */
490
+ private closeWith(browserClose: RelayClose, upstreamClose: RelayClose = browserClose): void {
491
+ if (this.closed) {
492
+ return;
493
+ }
494
+ this.closed = true;
495
+ this.stopTimers();
496
+ this.pending.length = 0;
497
+ RealtimeRelayTunnel.closeLeg(this.browser, browserClose);
498
+ RealtimeRelayTunnel.closeLeg(this.upstream, upstreamClose);
499
+ if (this.grant) {
500
+ this.logClosed(this.grant);
501
+ }
502
+ this.host.OnClosed(this);
503
+ }
504
+
505
+ /** Closes one leg unless it is already closing; `Terminate`, or a handshake in progress, drops the socket. */
506
+ private static closeLeg(sock: WsClient | null, close: RelayClose): void {
507
+ if (!sock || sock.readyState === WsClient.CLOSED) {
508
+ return;
509
+ }
510
+ if (close.Terminate || sock.readyState === WsClient.CONNECTING) {
511
+ RealtimeRelayTunnel.terminate(sock);
512
+ return;
513
+ }
514
+ if (sock.readyState === WsClient.CLOSING) {
515
+ return;
516
+ }
517
+ try {
518
+ sock.close(close.Code, close.Reason);
519
+ } catch {
520
+ RealtimeRelayTunnel.terminate(sock);
521
+ }
522
+ }
523
+
524
+ private static terminate(sock: WsClient): void {
525
+ try {
526
+ sock.terminate();
527
+ } catch {
528
+ /* already gone */
529
+ }
530
+ }
531
+
532
+ /** Refuses the connection with 1008; the reason is logged, the browser only learns it was refused. */
533
+ private refuse(reason: string): void {
534
+ this.warn(`connection refused (${reason})`);
535
+ this.closeWith({ Code: CLOSE_POLICY, Reason: 'Relay session refused' });
536
+ }
537
+
538
+ // ── Timers and logs ─────────────────────────────────────────────────────────────────────────────
539
+
540
+ private static after(ms: number, run: () => void): ReturnType<typeof setTimeout> {
541
+ const timer = setTimeout(run, Math.max(0, Math.min(ms, MAX_TIMER_MS)));
542
+ (timer as { unref?: () => void }).unref?.();
543
+ return timer;
544
+ }
545
+
546
+ private static cancel(timer: ReturnType<typeof setTimeout> | null): void {
547
+ if (timer) {
548
+ clearTimeout(timer);
549
+ }
550
+ }
551
+
552
+ private stopTimers(): void {
553
+ RealtimeRelayTunnel.cancel(this.firstFrameTimer);
554
+ RealtimeRelayTunnel.cancel(this.upstreamTimer);
555
+ RealtimeRelayTunnel.cancel(this.lifetimeTimer);
556
+ if (this.pingTimer) {
557
+ clearInterval(this.pingTimer);
558
+ }
559
+ this.firstFrameTimer = this.upstreamTimer = this.lifetimeTimer = this.pingTimer = null;
560
+ }
561
+
562
+ /** Counts a dropped client frame under a sanitized label (the label is the policy's, never the payload). */
563
+ private countDrop(label: string): void {
564
+ const clean = dropLabel(label);
565
+ const key = this.drops.has(clean) || this.drops.size < MAX_DROP_LABELS ? clean : 'other';
566
+ const count = (this.drops.get(key) ?? 0) + 1;
567
+ this.drops.set(key, count);
568
+ if (key === 'pre-open-overflow' && count === 1) {
569
+ const l = this.limits;
570
+ this.warn(`the pre-open buffer is full (${l.MaxPendingFrames} frames or ${l.MaxPendingBytes} B); dropping the oldest`);
571
+ }
572
+ }
573
+
574
+ private get label(): string {
575
+ return `relay ${RelayTicketPrefix(this.sessionId)}`;
576
+ }
577
+
578
+ private warn(message: string): void {
579
+ console.warn(`[RealtimeRelay] ${this.label}: ${message}`);
580
+ }
581
+
582
+ private logOpened(grant: RealtimeRelayGrant, intent: RealtimeRelayOpenIntent): void {
583
+ const kind = grant.Resumed ? 'resume' : 'fresh';
584
+ const driver = grant.DriverClass ? ` for ${grant.DriverClass}` : '';
585
+ const audio = intent.AudioOnly ? '; audio only (the browser asked)' : '';
586
+ console.log(`[RealtimeRelay] ${this.label}: connection ${grant.ConnectionNumber} of ${grant.MaxConnections} opened (${kind})${driver}${audio}`);
587
+ }
588
+
589
+ private logClosed(grant: RealtimeRelayGrant): void {
590
+ const seconds = ((Date.now() - this.openedAtMs) / 1000).toFixed(1);
591
+ const t = this.traffic;
592
+ const drops = [...this.drops].map(([label, count]) => `${label}=${count}`).join(', ');
593
+ console.log(
594
+ `[RealtimeRelay] ${this.label}: connection ${grant.ConnectionNumber} closed after ${seconds}s; ` +
595
+ `browser ${this.browserCloseCode ?? '-'}, upstream ${this.upstreamCloseNote || '-'}; ` +
596
+ `up ${t.FramesUp} frames / ${t.BytesUp} B, down ${t.FramesDown} frames / ${t.BytesDown} B` +
597
+ (drops ? `; dropped ${drops}` : '')
598
+ );
599
+ }
600
+ }
601
+
602
+ /**
603
+ * The close to send on the other leg for a close received with `code`: the same code and reason when a close frame may
604
+ * carry the code, none for "no code" (1005), else `fallback` (1006 and 1015 mean the leg dropped without a close).
605
+ */
606
+ function passClose(code: number, reason: Buffer, fallback: RelayClose): RelayClose {
607
+ if (code === CLOSE_NO_STATUS) {
608
+ return {};
609
+ }
610
+ return isSendableCloseCode(code) ? { Code: code, Reason: reason } : fallback;
611
+ }
612
+
613
+ /** Whether a close frame may carry `code` (the codes `ws` sends). */
614
+ function isSendableCloseCode(code: number): boolean {
615
+ return (code >= 1000 && code <= 1014 && code !== 1004 && code !== 1005 && code !== 1006) || (code >= 3000 && code <= 4999);
616
+ }
617
+
618
+ /** The bytes of a received message (`ws` hands over a Buffer, an ArrayBuffer, or fragments). */
619
+ function bytesOf(data: RawData): Buffer {
620
+ if (Buffer.isBuffer(data)) {
621
+ return data;
622
+ }
623
+ return Array.isArray(data) ? Buffer.concat(data) : Buffer.from(data);
624
+ }
625
+
626
+ /** A drop label safe to count and log: letters, digits, `_`, `.`, `-`, at most 40 characters. */
627
+ function dropLabel(label: string): string {
628
+ const clean = String(label).replace(/[^A-Za-z0-9_.-]/g, '').slice(0, 40);
629
+ return clean.length > 0 ? clean : 'unlabelled';
630
+ }
631
+
632
+ /** An upstream close reason fit for a log line: printable characters only. */
633
+ function safeText(reason: Buffer): string {
634
+ return reason.toString('utf8').replace(/[^\x20-\x7E]/g, '?').slice(0, 123);
635
+ }
636
+
637
+ /** An error's name only (for errors whose message could quote a payload or a URL). */
638
+ function errorName(err: unknown): string {
639
+ return err instanceof Error ? err.name : 'error';
640
+ }
641
+
642
+ /** An error's message, shortened, for errors that carry no payload (credentials, sockets). */
643
+ function errorSummary(err: unknown): string {
644
+ return err instanceof Error ? err.message.slice(0, 160) : 'unknown error';
645
+ }