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

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 (543) hide show
  1. package/README.md +20 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/agentSessions/BridgeHostInstance.d.ts +25 -0
  4. package/dist/agentSessions/BridgeHostInstance.d.ts.map +1 -0
  5. package/dist/agentSessions/BridgeHostInstance.js +30 -0
  6. package/dist/agentSessions/BridgeHostInstance.js.map +1 -0
  7. package/dist/agentSessions/HostInstance.d.ts +75 -11
  8. package/dist/agentSessions/HostInstance.d.ts.map +1 -1
  9. package/dist/agentSessions/HostInstance.js +99 -28
  10. package/dist/agentSessions/HostInstance.js.map +1 -1
  11. package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -1
  12. package/dist/agentSessions/ReturningVisitorRecap.js +7 -3
  13. package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -1
  14. package/dist/agentSessions/SessionJanitor.d.ts +236 -27
  15. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  16. package/dist/agentSessions/SessionJanitor.js +682 -54
  17. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  18. package/dist/agentSessions/SessionManager.d.ts +48 -3
  19. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  20. package/dist/agentSessions/SessionManager.js +112 -9
  21. package/dist/agentSessions/SessionManager.js.map +1 -1
  22. package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
  23. package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
  24. package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
  25. package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
  26. package/dist/agentSessions/index.d.ts +2 -0
  27. package/dist/agentSessions/index.d.ts.map +1 -1
  28. package/dist/agentSessions/index.js +10 -3
  29. package/dist/agentSessions/index.js.map +1 -1
  30. package/dist/apiRoot.d.ts +32 -0
  31. package/dist/apiRoot.d.ts.map +1 -0
  32. package/dist/apiRoot.js +67 -0
  33. package/dist/apiRoot.js.map +1 -0
  34. package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
  35. package/dist/auth/AuthProviderCatalogRouter.js +25 -0
  36. package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
  37. package/dist/auth/LoginScreenEngine.d.ts +22 -0
  38. package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
  39. package/dist/auth/LoginScreenEngine.js +57 -0
  40. package/dist/auth/LoginScreenEngine.js.map +1 -0
  41. package/dist/auth/actionAuthorization.d.ts +40 -0
  42. package/dist/auth/actionAuthorization.d.ts.map +1 -0
  43. package/dist/auth/actionAuthorization.js +94 -0
  44. package/dist/auth/actionAuthorization.js.map +1 -0
  45. package/dist/auth/index.d.ts +1 -0
  46. package/dist/auth/index.d.ts.map +1 -1
  47. package/dist/auth/index.js +4 -10
  48. package/dist/auth/index.js.map +1 -1
  49. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  50. package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
  51. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  52. package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
  53. package/dist/auth/magicLink/MagicLinkService.js +6 -3
  54. package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
  55. package/dist/auth/newUsers.d.ts +10 -0
  56. package/dist/auth/newUsers.d.ts.map +1 -1
  57. package/dist/auth/newUsers.js +80 -11
  58. package/dist/auth/newUsers.js.map +1 -1
  59. package/dist/config.d.ts +2514 -22
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/config.js +166 -4
  62. package/dist/config.js.map +1 -1
  63. package/dist/context.d.ts +8 -0
  64. package/dist/context.d.ts.map +1 -1
  65. package/dist/context.js +14 -11
  66. package/dist/context.js.map +1 -1
  67. package/dist/coreRoutes.d.ts +29 -0
  68. package/dist/coreRoutes.d.ts.map +1 -0
  69. package/dist/coreRoutes.js +142 -0
  70. package/dist/coreRoutes.js.map +1 -0
  71. package/dist/generated/graphql-schemas/__mj.d.ts +504 -3
  72. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  73. package/dist/generated/graphql-schemas/__mj.js +2456 -7
  74. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  75. package/dist/generic/CacheInvalidationResolver.d.ts +43 -8
  76. package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
  77. package/dist/generic/CacheInvalidationResolver.js +59 -19
  78. package/dist/generic/CacheInvalidationResolver.js.map +1 -1
  79. package/dist/generic/OrphanedConversationDetailReconciler.js +3 -3
  80. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -1
  81. package/dist/generic/PushStatusResolver.js +1 -1
  82. package/dist/generic/PushStatusResolver.js.map +1 -1
  83. package/dist/generic/ResolverBase.d.ts +123 -48
  84. package/dist/generic/ResolverBase.d.ts.map +1 -1
  85. package/dist/generic/ResolverBase.js +293 -305
  86. package/dist/generic/ResolverBase.js.map +1 -1
  87. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  88. package/dist/generic/RunViewResolver.js +16 -0
  89. package/dist/generic/RunViewResolver.js.map +1 -1
  90. package/dist/globalCors.d.ts +33 -0
  91. package/dist/globalCors.d.ts.map +1 -0
  92. package/dist/globalCors.js +58 -0
  93. package/dist/globalCors.js.map +1 -0
  94. package/dist/index.d.ts +17 -0
  95. package/dist/index.d.ts.map +1 -1
  96. package/dist/index.js +219 -278
  97. package/dist/index.js.map +1 -1
  98. package/dist/isolatedProvider.d.ts +20 -0
  99. package/dist/isolatedProvider.d.ts.map +1 -0
  100. package/dist/isolatedProvider.js +22 -0
  101. package/dist/isolatedProvider.js.map +1 -0
  102. package/dist/multiTenancy/index.d.ts.map +1 -1
  103. package/dist/multiTenancy/index.js +22 -2
  104. package/dist/multiTenancy/index.js.map +1 -1
  105. package/dist/realtimeProxy/RealtimeProxyServer.d.ts +44 -4
  106. package/dist/realtimeProxy/RealtimeProxyServer.d.ts.map +1 -1
  107. package/dist/realtimeProxy/RealtimeProxyServer.js +121 -16
  108. package/dist/realtimeProxy/RealtimeProxyServer.js.map +1 -1
  109. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts +171 -0
  110. package/dist/realtimeProxy/RealtimeRelayTunnel.d.ts.map +1 -0
  111. package/dist/realtimeProxy/RealtimeRelayTunnel.js +532 -0
  112. package/dist/realtimeProxy/RealtimeRelayTunnel.js.map +1 -0
  113. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts +148 -0
  114. package/dist/realtimeSessions/RealtimeSessionEventService.d.ts.map +1 -0
  115. package/dist/realtimeSessions/RealtimeSessionEventService.js +191 -0
  116. package/dist/realtimeSessions/RealtimeSessionEventService.js.map +1 -0
  117. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts +66 -0
  118. package/dist/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -0
  119. package/dist/realtimeSessions/RealtimeSessionVerificationService.js +139 -0
  120. package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -0
  121. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts +38 -0
  122. package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -0
  123. package/dist/realtimeSessions/RealtimeVerifyRouter.js +99 -0
  124. package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -0
  125. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
  126. package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
  127. package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
  128. package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
  129. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts +61 -0
  130. package/dist/realtimeSessions/SessionVerificationPortsBase.d.ts.map +1 -0
  131. package/dist/realtimeSessions/SessionVerificationPortsBase.js +149 -0
  132. package/dist/realtimeSessions/SessionVerificationPortsBase.js.map +1 -0
  133. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts +57 -0
  134. package/dist/realtimeSessions/SlidingWindowRateLimiter.d.ts.map +1 -0
  135. package/dist/realtimeSessions/SlidingWindowRateLimiter.js +69 -0
  136. package/dist/realtimeSessions/SlidingWindowRateLimiter.js.map +1 -0
  137. package/dist/realtimeSessions/index.d.ts +19 -0
  138. package/dist/realtimeSessions/index.d.ts.map +1 -0
  139. package/dist/realtimeSessions/index.js +19 -0
  140. package/dist/realtimeSessions/index.js.map +1 -0
  141. package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
  142. package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
  143. package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
  144. package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
  145. package/dist/realtimeSessions/sessionAccess.d.ts +92 -0
  146. package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -0
  147. package/dist/realtimeSessions/sessionAccess.js +94 -0
  148. package/dist/realtimeSessions/sessionAccess.js.map +1 -0
  149. package/dist/realtimeSessions/sessionConfigStamps.d.ts +70 -0
  150. package/dist/realtimeSessions/sessionConfigStamps.d.ts.map +1 -0
  151. package/dist/realtimeSessions/sessionConfigStamps.js +93 -0
  152. package/dist/realtimeSessions/sessionConfigStamps.js.map +1 -0
  153. package/dist/realtimeSessions/subscriptionAuthorization.d.ts +65 -0
  154. package/dist/realtimeSessions/subscriptionAuthorization.d.ts.map +1 -0
  155. package/dist/realtimeSessions/subscriptionAuthorization.js +71 -0
  156. package/dist/realtimeSessions/subscriptionAuthorization.js.map +1 -0
  157. package/dist/realtimeSessions/verificationCore.d.ts +243 -0
  158. package/dist/realtimeSessions/verificationCore.d.ts.map +1 -0
  159. package/dist/realtimeSessions/verificationCore.js +356 -0
  160. package/dist/realtimeSessions/verificationCore.js.map +1 -0
  161. package/dist/realtimeSessions/verificationPages.d.ts +66 -0
  162. package/dist/realtimeSessions/verificationPages.d.ts.map +1 -0
  163. package/dist/realtimeSessions/verificationPages.js +110 -0
  164. package/dist/realtimeSessions/verificationPages.js.map +1 -0
  165. package/dist/realtimeSessions/verificationPolicy.d.ts +147 -0
  166. package/dist/realtimeSessions/verificationPolicy.d.ts.map +1 -0
  167. package/dist/realtimeSessions/verificationPolicy.js +249 -0
  168. package/dist/realtimeSessions/verificationPolicy.js.map +1 -0
  169. package/dist/realtimeSessions/verificationWorkflow.d.ts +263 -0
  170. package/dist/realtimeSessions/verificationWorkflow.d.ts.map +1 -0
  171. package/dist/realtimeSessions/verificationWorkflow.js +435 -0
  172. package/dist/realtimeSessions/verificationWorkflow.js.map +1 -0
  173. package/dist/realtimeWidget/widgetGuestElevation.d.ts +20 -2
  174. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
  175. package/dist/realtimeWidget/widgetGuestElevation.js +31 -2
  176. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
  177. package/dist/resolvers/ActionResolver.d.ts +31 -5
  178. package/dist/resolvers/ActionResolver.d.ts.map +1 -1
  179. package/dist/resolvers/ActionResolver.js +75 -14
  180. package/dist/resolvers/ActionResolver.js.map +1 -1
  181. package/dist/resolvers/AdhocQueryResolver.d.ts +14 -1
  182. package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
  183. package/dist/resolvers/AdhocQueryResolver.js +21 -5
  184. package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
  185. package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
  186. package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
  187. package/dist/resolvers/ArtifactFileResolver.js +9 -7
  188. package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
  189. package/dist/resolvers/CacheStatsResolver.d.ts +27 -0
  190. package/dist/resolvers/CacheStatsResolver.d.ts.map +1 -1
  191. package/dist/resolvers/CacheStatsResolver.js +113 -2
  192. package/dist/resolvers/CacheStatsResolver.js.map +1 -1
  193. package/dist/resolvers/ClientToolRequestResolver.d.ts +4 -2
  194. package/dist/resolvers/ClientToolRequestResolver.d.ts.map +1 -1
  195. package/dist/resolvers/ClientToolRequestResolver.js +28 -10
  196. package/dist/resolvers/ClientToolRequestResolver.js.map +1 -1
  197. package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
  198. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
  199. package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
  200. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -1
  201. package/dist/resolvers/DatasetResolver.d.ts +24 -3
  202. package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
  203. package/dist/resolvers/DatasetResolver.js +57 -8
  204. package/dist/resolvers/DatasetResolver.js.map +1 -1
  205. package/dist/resolvers/EntityRecordNameResolver.d.ts +2 -1
  206. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  207. package/dist/resolvers/EntityRecordNameResolver.js +3 -2
  208. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  209. package/dist/resolvers/FetchEntityVectorsResolver.d.ts +27 -1
  210. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  211. package/dist/resolvers/FetchEntityVectorsResolver.js +148 -2
  212. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  213. package/dist/resolvers/FileResolver.d.ts +82 -11
  214. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  215. package/dist/resolvers/FileResolver.js +279 -101
  216. package/dist/resolvers/FileResolver.js.map +1 -1
  217. package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
  218. package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
  219. package/dist/resolvers/GetDataContextDataResolver.js +117 -38
  220. package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
  221. package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
  222. package/dist/resolvers/GetDataResolver.js +6 -3
  223. package/dist/resolvers/GetDataResolver.js.map +1 -1
  224. package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -1
  225. package/dist/resolvers/HumanHandoffResolver.js +7 -1
  226. package/dist/resolvers/HumanHandoffResolver.js.map +1 -1
  227. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  228. package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
  229. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  230. package/dist/resolvers/MCPResolver.d.ts.map +1 -1
  231. package/dist/resolvers/MCPResolver.js +9 -4
  232. package/dist/resolvers/MCPResolver.js.map +1 -1
  233. package/dist/resolvers/QueryResolver.d.ts +2 -0
  234. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  235. package/dist/resolvers/QueryResolver.js +13 -0
  236. package/dist/resolvers/QueryResolver.js.map +1 -1
  237. package/dist/resolvers/RealtimeBridgeResolver.d.ts +54 -2
  238. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  239. package/dist/resolvers/RealtimeBridgeResolver.js +155 -10
  240. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  241. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +157 -33
  242. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  243. package/dist/resolvers/RealtimeClientSessionResolver.js +404 -116
  244. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  245. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts +61 -0
  246. package/dist/resolvers/RealtimeSessionEventsResolver.d.ts.map +1 -0
  247. package/dist/resolvers/RealtimeSessionEventsResolver.js +130 -0
  248. package/dist/resolvers/RealtimeSessionEventsResolver.js.map +1 -0
  249. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts +85 -0
  250. package/dist/resolvers/RealtimeSessionVerificationResolver.d.ts.map +1 -0
  251. package/dist/resolvers/RealtimeSessionVerificationResolver.js +188 -0
  252. package/dist/resolvers/RealtimeSessionVerificationResolver.js.map +1 -0
  253. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
  254. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  255. package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
  256. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  257. package/dist/resolvers/RunAIAgentResolver.d.ts +10 -0
  258. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  259. package/dist/resolvers/RunAIAgentResolver.js +100 -14
  260. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  261. package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
  262. package/dist/resolvers/RunTemplateResolver.js +12 -11
  263. package/dist/resolvers/RunTemplateResolver.js.map +1 -1
  264. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
  265. package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
  266. package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
  267. package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
  268. package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
  269. package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
  270. package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
  271. package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
  272. package/dist/resolvers/SyncDataResolver.js +6 -2
  273. package/dist/resolvers/SyncDataResolver.js.map +1 -1
  274. package/dist/resolvers/SyncRolesUsersResolver.d.ts +2 -0
  275. package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
  276. package/dist/resolvers/SyncRolesUsersResolver.js +23 -6
  277. package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
  278. package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
  279. package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
  280. package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
  281. package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
  282. package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
  283. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  284. package/dist/resolvers/TestQuerySQLResolver.js +65 -13
  285. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  286. package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
  287. package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
  288. package/dist/resolvers/TransactionGroupResolver.js +102 -24
  289. package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
  290. package/dist/resolvers/UserAvatarResolver.d.ts +16 -0
  291. package/dist/resolvers/UserAvatarResolver.d.ts.map +1 -0
  292. package/dist/resolvers/UserAvatarResolver.js +124 -0
  293. package/dist/resolvers/UserAvatarResolver.js.map +1 -0
  294. package/dist/resolvers/UserResolver.d.ts.map +1 -1
  295. package/dist/resolvers/UserResolver.js +2 -17
  296. package/dist/resolvers/UserResolver.js.map +1 -1
  297. package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
  298. package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
  299. package/dist/resolvers/agent-run-data-guard.js +72 -0
  300. package/dist/resolvers/agent-run-data-guard.js.map +1 -0
  301. package/dist/resolvers/agentVisionConsentAudit.d.ts +37 -0
  302. package/dist/resolvers/agentVisionConsentAudit.d.ts.map +1 -0
  303. package/dist/resolvers/agentVisionConsentAudit.js +63 -0
  304. package/dist/resolvers/agentVisionConsentAudit.js.map +1 -0
  305. package/dist/resolvers/avatarInputValidation.d.ts +40 -0
  306. package/dist/resolvers/avatarInputValidation.d.ts.map +1 -0
  307. package/dist/resolvers/avatarInputValidation.js +97 -0
  308. package/dist/resolvers/avatarInputValidation.js.map +1 -0
  309. package/dist/resolvers/currentUserRoles.d.ts +29 -0
  310. package/dist/resolvers/currentUserRoles.d.ts.map +1 -1
  311. package/dist/resolvers/currentUserRoles.js +30 -0
  312. package/dist/resolvers/currentUserRoles.js.map +1 -1
  313. package/dist/resolvers/meetings/meetingMutations.d.ts +6 -0
  314. package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -1
  315. package/dist/resolvers/meetings/meetingMutations.js +21 -0
  316. package/dist/resolvers/meetings/meetingMutations.js.map +1 -1
  317. package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
  318. package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
  319. package/dist/rest/RealtimeRecordingHandler.js +383 -0
  320. package/dist/rest/RealtimeRecordingHandler.js.map +1 -0
  321. package/dist/rest/ViewOperationsHandler.d.ts +3 -1
  322. package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
  323. package/dist/rest/ViewOperationsHandler.js +21 -12
  324. package/dist/rest/ViewOperationsHandler.js.map +1 -1
  325. package/dist/rest/setupRESTEndpoints.d.ts +6 -0
  326. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  327. package/dist/rest/setupRESTEndpoints.js +6 -0
  328. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  329. package/dist/serverExtensionReservedRoots.d.ts +12 -10
  330. package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
  331. package/dist/serverExtensionReservedRoots.js +14 -17
  332. package/dist/serverExtensionReservedRoots.js.map +1 -1
  333. package/dist/services/MJStorageBlobStore.d.ts +8 -1
  334. package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
  335. package/dist/services/MJStorageBlobStore.js +25 -19
  336. package/dist/services/MJStorageBlobStore.js.map +1 -1
  337. package/dist/services/StartTaskGraphDispatcher.js +2 -2
  338. package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
  339. package/dist/services/TaskGraphActionRunner.d.ts +5 -3
  340. package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
  341. package/dist/services/TaskGraphActionRunner.js +27 -5
  342. package/dist/services/TaskGraphActionRunner.js.map +1 -1
  343. package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
  344. package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
  345. package/dist/services/TaskGraphAgentRunner.js +157 -9
  346. package/dist/services/TaskGraphAgentRunner.js.map +1 -1
  347. package/dist/services/TaskGraphContinuationDeliverer.d.ts +57 -4
  348. package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
  349. package/dist/services/TaskGraphContinuationDeliverer.js +607 -18
  350. package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
  351. package/dist/sharedCache.d.ts +105 -0
  352. package/dist/sharedCache.d.ts.map +1 -0
  353. package/dist/sharedCache.js +279 -0
  354. package/dist/sharedCache.js.map +1 -0
  355. package/dist/types.d.ts +6 -0
  356. package/dist/types.d.ts.map +1 -1
  357. package/dist/types.js.map +1 -1
  358. package/package.json +99 -96
  359. package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
  360. package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
  361. package/src/__tests__/BridgeHostInstance.test.ts +232 -0
  362. package/src/__tests__/ClientToolRequestResolver.test.ts +116 -0
  363. package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
  364. package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
  365. package/src/__tests__/FetchEntityVectorsResolver.test.ts +433 -0
  366. package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
  367. package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
  368. package/src/__tests__/HostInstance.test.ts +84 -0
  369. package/src/__tests__/HumanHandoffResolver.test.ts +17 -0
  370. package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
  371. package/src/__tests__/LoginScreenEngine.test.ts +86 -0
  372. package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
  373. package/src/__tests__/MeetingResolver.test.ts +75 -1
  374. package/src/__tests__/RealtimeBridgeResolver.test.ts +415 -6
  375. package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
  376. package/src/__tests__/RealtimeClientSessionResolver.test.ts +682 -58
  377. package/src/__tests__/RealtimeProxyServer.test.ts +16 -1
  378. package/src/__tests__/RealtimeRelay.test.ts +876 -0
  379. package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
  380. package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
  381. package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
  382. package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
  383. package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
  384. package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
  385. package/src/__tests__/ResolverBase.clientOldValues.test.ts +212 -6
  386. package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
  387. package/src/__tests__/ReturningVisitorRecap.test.ts +115 -0
  388. package/src/__tests__/RunAIAgentResolver.branch.test.ts +170 -0
  389. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +6 -4
  390. package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
  391. package/src/__tests__/RunAIAgentResolver.stoppedPredecessor.test.ts +166 -0
  392. package/src/__tests__/RunTemplateResolver.test.ts +132 -0
  393. package/src/__tests__/RunViewResolver.cacheCheckForwarding.test.ts +63 -0
  394. package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
  395. package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
  396. package/src/__tests__/SessionJanitor.test.ts +1203 -21
  397. package/src/__tests__/SessionManager.test.ts +240 -17
  398. package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
  399. package/src/__tests__/SyncRolesUsersResolver.userRoles.test.ts +97 -0
  400. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
  401. package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
  402. package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +403 -22
  403. package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
  404. package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
  405. package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
  406. package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
  407. package/src/__tests__/UserAvatarResolver.test.ts +255 -0
  408. package/src/__tests__/UserResolver.currentUser.test.ts +16 -1
  409. package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
  410. package/src/__tests__/actionAuthorization.test.ts +130 -0
  411. package/src/__tests__/agent-run-data-guard.test.ts +130 -0
  412. package/src/__tests__/agentVisionConsentAudit.test.ts +118 -0
  413. package/src/__tests__/apiRootBoot.test.ts +202 -0
  414. package/src/__tests__/avatarInputValidation.test.ts +160 -0
  415. package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
  416. package/src/__tests__/conversationRunEvents.test.ts +56 -14
  417. package/src/__tests__/globalCors.test.ts +217 -0
  418. package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
  419. package/src/__tests__/magicLinkService.consume.test.ts +6 -0
  420. package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
  421. package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
  422. package/src/__tests__/multiTenancy.security.test.ts +51 -0
  423. package/src/__tests__/multiTenancy.test.ts +1 -1
  424. package/src/__tests__/newUsers.template.test.ts +151 -0
  425. package/src/__tests__/newUsers.test.ts +74 -5
  426. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +17 -2
  427. package/src/__tests__/ownedRealtimeSession.test.ts +39 -0
  428. package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
  429. package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
  430. package/src/__tests__/realtimeSessions/RealtimeSessionEventService.test.ts +201 -0
  431. package/src/__tests__/realtimeSessions/RealtimeSessionEventsResolver.test.ts +215 -0
  432. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationResolver.test.ts +102 -0
  433. package/src/__tests__/realtimeSessions/RealtimeSessionVerificationService.test.ts +349 -0
  434. package/src/__tests__/realtimeSessions/RealtimeVerifyRouter.test.ts +131 -0
  435. package/src/__tests__/realtimeSessions/SlidingWindowRateLimiter.test.ts +56 -0
  436. package/src/__tests__/realtimeSessions/helpers.ts +4 -0
  437. package/src/__tests__/realtimeSessions/sessionAccess.test.ts +129 -0
  438. package/src/__tests__/realtimeSessions/sessionConfigStamps.test.ts +124 -0
  439. package/src/__tests__/realtimeSessions/verificationCore.test.ts +355 -0
  440. package/src/__tests__/realtimeSessions/verificationPages.test.ts +84 -0
  441. package/src/__tests__/realtimeSessions/verificationPolicy.test.ts +226 -0
  442. package/src/__tests__/realtimeSessions/verificationWorkflow.test.ts +633 -0
  443. package/src/__tests__/resolverBase.fls.test.ts +87 -4
  444. package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
  445. package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -96
  446. package/src/__tests__/sharedCache.test.ts +312 -0
  447. package/src/__tests__/widgetGuestElevation.test.ts +44 -0
  448. package/src/agentSessions/BridgeHostInstance.ts +32 -0
  449. package/src/agentSessions/HostInstance.ts +124 -28
  450. package/src/agentSessions/ReturningVisitorRecap.ts +7 -3
  451. package/src/agentSessions/SessionJanitor.ts +821 -59
  452. package/src/agentSessions/SessionManager.ts +138 -26
  453. package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
  454. package/src/agentSessions/index.ts +10 -3
  455. package/src/apiRoot.ts +78 -0
  456. package/src/auth/AuthProviderCatalogRouter.ts +26 -0
  457. package/src/auth/LoginScreenEngine.ts +50 -0
  458. package/src/auth/actionAuthorization.ts +116 -0
  459. package/src/auth/index.ts +4 -8
  460. package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
  461. package/src/auth/magicLink/MagicLinkService.ts +6 -3
  462. package/src/auth/newUsers.ts +99 -11
  463. package/src/config.ts +171 -5
  464. package/src/context.ts +15 -10
  465. package/src/coreRoutes.ts +166 -0
  466. package/src/generated/graphql-schemas/__mj.ts +1815 -57
  467. package/src/generic/CacheInvalidationResolver.ts +73 -18
  468. package/src/generic/OrphanedConversationDetailReconciler.ts +3 -3
  469. package/src/generic/PushStatusResolver.ts +1 -1
  470. package/src/generic/ResolverBase.ts +335 -326
  471. package/src/generic/RunViewResolver.ts +21 -0
  472. package/src/globalCors.ts +71 -0
  473. package/src/index.ts +231 -300
  474. package/src/isolatedProvider.ts +22 -0
  475. package/src/multiTenancy/index.ts +24 -2
  476. package/src/realtimeProxy/RealtimeProxyServer.ts +148 -16
  477. package/src/realtimeProxy/RealtimeRelayTunnel.ts +645 -0
  478. package/src/realtimeSessions/README.md +179 -0
  479. package/src/realtimeSessions/RealtimeSessionEventService.ts +274 -0
  480. package/src/realtimeSessions/RealtimeSessionVerificationService.ts +168 -0
  481. package/src/realtimeSessions/RealtimeVerifyRouter.ts +109 -0
  482. package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
  483. package/src/realtimeSessions/SessionVerificationPortsBase.ts +189 -0
  484. package/src/realtimeSessions/SlidingWindowRateLimiter.ts +92 -0
  485. package/src/realtimeSessions/index.ts +18 -0
  486. package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
  487. package/src/realtimeSessions/sessionAccess.ts +145 -0
  488. package/src/realtimeSessions/sessionConfigStamps.ts +123 -0
  489. package/src/realtimeSessions/subscriptionAuthorization.ts +116 -0
  490. package/src/realtimeSessions/verificationCore.ts +502 -0
  491. package/src/realtimeSessions/verificationPages.ts +149 -0
  492. package/src/realtimeSessions/verificationPolicy.ts +321 -0
  493. package/src/realtimeSessions/verificationWorkflow.ts +702 -0
  494. package/src/realtimeWidget/widgetGuestElevation.ts +34 -2
  495. package/src/resolvers/ActionResolver.ts +84 -21
  496. package/src/resolvers/AdhocQueryResolver.ts +24 -6
  497. package/src/resolvers/ArtifactFileResolver.ts +9 -8
  498. package/src/resolvers/CacheStatsResolver.ts +89 -2
  499. package/src/resolvers/ClientToolRequestResolver.ts +25 -7
  500. package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
  501. package/src/resolvers/DatasetResolver.ts +70 -10
  502. package/src/resolvers/EntityRecordNameResolver.ts +3 -2
  503. package/src/resolvers/FetchEntityVectorsResolver.ts +179 -2
  504. package/src/resolvers/FileResolver.ts +321 -106
  505. package/src/resolvers/GetDataContextDataResolver.ts +139 -41
  506. package/src/resolvers/GetDataResolver.ts +6 -3
  507. package/src/resolvers/HumanHandoffResolver.ts +8 -0
  508. package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
  509. package/src/resolvers/MCPResolver.ts +8 -5
  510. package/src/resolvers/QueryResolver.ts +11 -0
  511. package/src/resolvers/RealtimeBridgeResolver.ts +163 -11
  512. package/src/resolvers/RealtimeClientSessionResolver.ts +511 -129
  513. package/src/resolvers/RealtimeSessionEventsResolver.ts +133 -0
  514. package/src/resolvers/RealtimeSessionVerificationResolver.ts +174 -0
  515. package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
  516. package/src/resolvers/RunAIAgentResolver.ts +116 -14
  517. package/src/resolvers/RunTemplateResolver.ts +13 -12
  518. package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
  519. package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
  520. package/src/resolvers/SyncDataResolver.ts +6 -2
  521. package/src/resolvers/SyncRolesUsersResolver.ts +24 -6
  522. package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
  523. package/src/resolvers/TestQuerySQLResolver.ts +68 -13
  524. package/src/resolvers/TransactionGroupResolver.ts +131 -27
  525. package/src/resolvers/UserAvatarResolver.ts +111 -0
  526. package/src/resolvers/UserResolver.ts +2 -16
  527. package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
  528. package/src/resolvers/agent-run-data-guard.ts +89 -0
  529. package/src/resolvers/agentVisionConsentAudit.ts +82 -0
  530. package/src/resolvers/avatarInputValidation.ts +123 -0
  531. package/src/resolvers/currentUserRoles.ts +32 -1
  532. package/src/resolvers/meetings/meetingMutations.ts +21 -0
  533. package/src/rest/RealtimeRecordingHandler.ts +464 -0
  534. package/src/rest/ViewOperationsHandler.ts +29 -13
  535. package/src/rest/setupRESTEndpoints.ts +6 -0
  536. package/src/serverExtensionReservedRoots.ts +14 -17
  537. package/src/services/MJStorageBlobStore.ts +31 -30
  538. package/src/services/StartTaskGraphDispatcher.ts +2 -2
  539. package/src/services/TaskGraphActionRunner.ts +25 -5
  540. package/src/services/TaskGraphAgentRunner.ts +188 -11
  541. package/src/services/TaskGraphContinuationDeliverer.ts +713 -17
  542. package/src/sharedCache.ts +305 -0
  543. package/src/types.ts +7 -0
@@ -0,0 +1,652 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+
3
+ const { logErrorMock, logStatusMock } = vi.hoisted(() => ({
4
+ logErrorMock: vi.fn(),
5
+ logStatusMock: vi.fn(),
6
+ }));
7
+ vi.mock('@memberjunction/core', async (importOriginal) => {
8
+ const actual = await importOriginal<typeof import('@memberjunction/core')>();
9
+ return { ...actual, LogError: logErrorMock, LogStatus: logStatusMock };
10
+ });
11
+
12
+ import { createHash } from 'node:crypto';
13
+ import type { AddressInfo } from 'node:net';
14
+ import { request as httpRequest, type Server } from 'node:http';
15
+ import express from 'express';
16
+ import type { DatabaseProviderBase, UserInfo } from '@memberjunction/core';
17
+ import type { MJAIAgentSessionEntity } from '@memberjunction/core-entities';
18
+ import { EncodeRecordingShard, RECORDING_SHARD_MAX_BYTES } from '@memberjunction/ai-core-plus';
19
+ import type {
20
+ AssembleRealtimeRecordingInput, AssembleRealtimeRecordingOutcome, FinishAssemblyInput, IRealtimeRecordingLedger,
21
+ LedgerShard, LedgerShardMetadata, RecordShardInput, RecordShardResult,
22
+ } from '@memberjunction/ai-agents';
23
+ import { MAX_ASSEMBLY_SHARDS } from '@memberjunction/ai-agents';
24
+ import {
25
+ CreateRealtimeRecordingRouter, FINAL_TERMINAL_RECHECK_MS, REALTIME_RECORDING_MOUNT_PATH, type RealtimeRecordingRouterDeps,
26
+ } from '../rest/RealtimeRecordingHandler.js';
27
+ import { RealtimeSessionNotFoundError, RealtimeSessionOwnershipError } from '../realtimeSessions/ownedRealtimeSession.js';
28
+ import { RecordingAssemblyScheduler, type IRecordingAssemblyScheduler } from '../realtimeSessions/RecordingAssemblyScheduler.js';
29
+
30
+ const SESSION = '11111111-1111-4111-8111-111111111111';
31
+ const MISSING = '99999999-9999-4999-8999-999999999999';
32
+ const OWNER_ID = 'aaaaaaaa-0000-4000-8000-000000000001';
33
+ const STRANGER_ID = 'bbbbbbbb-0000-4000-8000-000000000002';
34
+ const GRACE_MS = 90_000;
35
+ const HOUR_MS = 3_600_000;
36
+ const SHARD_SAMPLES = 240_000;
37
+
38
+ type Status = 'Pending' | 'Assembling' | 'Complete' | 'Partial' | 'Failed' | null;
39
+
40
+ interface FakeSessionRow {
41
+ UserID: string;
42
+ RecordingStatus: Status;
43
+ RecordingExpectedShards: number | null;
44
+ RecordingCapturedSamples: number | null;
45
+ RecordingAssemblyVersion: number;
46
+ ClosedAt: Date | null;
47
+ /** When the server created the session: no recording can hold more audio than the session has lived. */
48
+ CreatedAt: Date;
49
+ /** The session conversation's ExternalID: an anonymous widget guest's per-session scope. */
50
+ ConversationExternalID?: string | null;
51
+ }
52
+
53
+ interface StoredShard {
54
+ Index: number;
55
+ StartSample: number;
56
+ SampleCount: number;
57
+ Sha256: string;
58
+ Data: Buffer;
59
+ }
60
+
61
+ /**
62
+ * In-memory ledger mirroring the guarded procs' semantics (see the shard-ledger migration). Every method
63
+ * does its check-and-set without an await in between, which is what makes the fake claim atomic.
64
+ */
65
+ class FakeLedger implements IRealtimeRecordingLedger {
66
+ public readonly Sessions = new Map<string, FakeSessionRow>();
67
+ public readonly Shards = new Map<string, Map<number, StoredShard>>();
68
+
69
+ public async RecordShard(input: RecordShardInput): Promise<RecordShardResult> {
70
+ const row = this.row(input.SessionID);
71
+ const shards = this.shardsOf(input.SessionID);
72
+ const existing = shards.get(input.Index);
73
+ let outcome: RecordShardResult['Outcome'];
74
+ if (!existing) {
75
+ shards.set(input.Index, {
76
+ Index: input.Index, StartSample: input.StartSample, SampleCount: input.SampleCount,
77
+ Sha256: input.Sha256, Data: Buffer.from(input.Data),
78
+ });
79
+ outcome = 'Inserted';
80
+ const reopen = row.RecordingStatus === null || row.RecordingStatus === 'Assembling'
81
+ || ((row.RecordingStatus === 'Complete' || row.RecordingStatus === 'Partial') && input.Index >= (row.RecordingExpectedShards ?? 0));
82
+ if (reopen) {
83
+ if (row.RecordingStatus === 'Assembling') {
84
+ row.RecordingAssemblyVersion++;
85
+ }
86
+ row.RecordingStatus = 'Pending';
87
+ }
88
+ } else if (existing.Sha256 === input.Sha256) {
89
+ outcome = 'Duplicate';
90
+ } else {
91
+ outcome = 'Conflict';
92
+ row.RecordingStatus = 'Failed';
93
+ }
94
+ return { Outcome: outcome, RecordingStatus: row.RecordingStatus, ExpectedShards: row.RecordingExpectedShards, ClosedAt: row.ClosedAt };
95
+ }
96
+
97
+ public async StampTerminal(sessionID: string, expected: number, captured: number): Promise<{ Outcome: 'Stamped' | 'Ignored'; RecordingStatus: string | null }> {
98
+ const row = this.row(sessionID);
99
+ const accepted = (row.RecordingExpectedShards === null || expected >= row.RecordingExpectedShards)
100
+ && (row.RecordingCapturedSamples === null || captured >= row.RecordingCapturedSamples);
101
+ if (!accepted) {
102
+ return { Outcome: 'Ignored', RecordingStatus: row.RecordingStatus };
103
+ }
104
+ const higher = expected > (row.RecordingExpectedShards ?? -1) || captured > (row.RecordingCapturedSamples ?? -1);
105
+ if (row.RecordingStatus === 'Assembling' && higher) {
106
+ row.RecordingAssemblyVersion++;
107
+ }
108
+ if (row.RecordingStatus === null || (higher && (row.RecordingStatus === 'Complete' || row.RecordingStatus === 'Partial' || row.RecordingStatus === 'Assembling'))) {
109
+ row.RecordingStatus = 'Pending';
110
+ }
111
+ row.RecordingExpectedShards = expected;
112
+ row.RecordingCapturedSamples = captured;
113
+ return { Outcome: 'Stamped', RecordingStatus: row.RecordingStatus };
114
+ }
115
+
116
+ public async ClaimAssembly(sessionID: string): Promise<number | null> {
117
+ const row = this.row(sessionID);
118
+ if (row.RecordingStatus !== 'Pending' && row.RecordingStatus !== 'Partial') {
119
+ return null;
120
+ }
121
+ row.RecordingStatus = 'Assembling';
122
+ return ++row.RecordingAssemblyVersion;
123
+ }
124
+
125
+ public async FinishAssembly(input: FinishAssemblyInput): Promise<boolean> {
126
+ const row = this.row(input.SessionID);
127
+ if (row.RecordingAssemblyVersion !== input.Version) {
128
+ return false;
129
+ }
130
+ row.RecordingStatus = input.Status;
131
+ return true;
132
+ }
133
+
134
+ public async StampAssemblyGaps(): Promise<boolean> {
135
+ throw new Error('not used by the routes');
136
+ }
137
+
138
+ public async LoadShards(sessionID: string): Promise<LedgerShard[]> {
139
+ return [...this.shardsOf(sessionID).values()].sort((a, b) => a.Index - b.Index)
140
+ .map(s => ({ Index: s.Index, StartSample: s.StartSample, SampleCount: s.SampleCount, Data: s.Data }));
141
+ }
142
+
143
+ public async LoadShardMetadata(sessionID: string): Promise<LedgerShardMetadata[]> {
144
+ return (await this.LoadShards(sessionID)).map(({ Index, StartSample, SampleCount }) => ({ Index, StartSample, SampleCount }));
145
+ }
146
+
147
+ public async CountShardsBelow(sessionID: string, indexBelow: number): Promise<number> {
148
+ return [...this.shardsOf(sessionID).keys()].filter(index => index < indexBelow).length;
149
+ }
150
+
151
+ public async DeleteShardsBelow(): Promise<number> {
152
+ throw new Error('not used by the routes');
153
+ }
154
+
155
+ public async PurgeClosedBefore(): Promise<number> {
156
+ throw new Error('not used by the routes');
157
+ }
158
+
159
+ private row(sessionID: string): FakeSessionRow {
160
+ const row = this.Sessions.get(sessionID);
161
+ if (!row) {
162
+ throw new Error(`fake ledger: no session ${sessionID}`);
163
+ }
164
+ return row;
165
+ }
166
+
167
+ private shardsOf(sessionID: string): Map<number, StoredShard> {
168
+ let shards = this.Shards.get(sessionID);
169
+ if (!shards) {
170
+ shards = new Map();
171
+ this.Shards.set(sessionID, shards);
172
+ }
173
+ return shards;
174
+ }
175
+ }
176
+
177
+ /** A valid MJOPUS1 shard; `seed` changes the packet bytes (and so the hash) without changing the header. */
178
+ function shardBytes(index: number, seed = 1, sampleCount = SHARD_SAMPLES, packetBytes = 80): Uint8Array {
179
+ const packetCount = Math.ceil(sampleCount / 960);
180
+ const packets = Array.from({ length: packetCount }, () => new Uint8Array(packetBytes).fill(seed));
181
+ return EncodeRecordingShard({ ShardIndex: index, StartSample: index * SHARD_SAMPLES, SampleCount: sampleCount, PacketCount: packetCount, Packets: packets });
182
+ }
183
+
184
+ /** The server's system user: the only principal with read access to the shard entity. */
185
+ const SYSTEM_USER = { ID: 'system-user' } as unknown as UserInfo;
186
+
187
+ /**
188
+ * The ledger as a request's caller sees it: the guarded procs work, but a read of the shard entity is
189
+ * refused, exactly as `CheckUserReadPermissions` refuses UI users and widget guests (no CanRead).
190
+ */
191
+ function callerLedger(inner: FakeLedger): IRealtimeRecordingLedger {
192
+ const refuse = async (): Promise<never> => {
193
+ throw new Error('User does not have read permissions on MJ: AI Agent Session Recording Shards');
194
+ };
195
+ return {
196
+ RecordShard: input => inner.RecordShard(input),
197
+ StampTerminal: (id, expected, captured) => inner.StampTerminal(id, expected, captured),
198
+ ClaimAssembly: id => inner.ClaimAssembly(id),
199
+ FinishAssembly: input => inner.FinishAssembly(input),
200
+ StampAssemblyGaps: () => inner.StampAssemblyGaps(),
201
+ LoadShards: refuse,
202
+ LoadShardMetadata: refuse,
203
+ CountShardsBelow: refuse,
204
+ DeleteShardsBelow: () => inner.DeleteShardsBelow(),
205
+ PurgeClosedBefore: () => inner.PurgeClosedBefore(),
206
+ };
207
+ }
208
+
209
+ function sha256(bytes: Uint8Array): string {
210
+ return createHash('sha256').update(bytes).digest('hex');
211
+ }
212
+
213
+ describe('realtime recording REST routes', () => {
214
+ let ledger: FakeLedger;
215
+ let scheduler: { ScheduleAssembly: ReturnType<typeof vi.fn<IRecordingAssemblyScheduler['ScheduleAssembly']>> };
216
+ let deps: RealtimeRecordingRouterDeps;
217
+ let server: Server | null;
218
+ let baseUrl: string;
219
+
220
+ async function listen(): Promise<void> {
221
+ const app = express();
222
+ app.use(REALTIME_RECORDING_MOUNT_PATH, CreateRealtimeRecordingRouter(deps));
223
+ server = await new Promise<Server>(resolve => {
224
+ const s = app.listen(0, () => resolve(s));
225
+ });
226
+ baseUrl = `http://127.0.0.1:${(server.address() as AddressInfo).port}${REALTIME_RECORDING_MOUNT_PATH}`;
227
+ }
228
+
229
+ function addSession(id: string, overrides: Partial<FakeSessionRow> = {}): FakeSessionRow {
230
+ const row: FakeSessionRow = {
231
+ UserID: OWNER_ID, RecordingStatus: null, RecordingExpectedShards: null, RecordingCapturedSamples: null,
232
+ RecordingAssemblyVersion: 0, ClosedAt: null, CreatedAt: new Date(Date.now() - 48 * HOUR_MS), ...overrides,
233
+ };
234
+ ledger.Sessions.set(id, row);
235
+ return row;
236
+ }
237
+
238
+ function post(path: string, body: Uint8Array | null, user = OWNER_ID, headers: Record<string, string> = {}): Promise<Response> {
239
+ return fetch(`${baseUrl}${path}`, {
240
+ method: 'POST',
241
+ headers: { 'Content-Type': 'application/octet-stream', 'x-test-user': user, ...headers },
242
+ body: (body ?? new Uint8Array(0)) as Uint8Array<ArrayBuffer>,
243
+ });
244
+ }
245
+
246
+ function postShard(index: number, bytes: Uint8Array = shardBytes(index), user = OWNER_ID): Promise<Response> {
247
+ return post(`/${SESSION}/shard/${index}`, bytes, user);
248
+ }
249
+
250
+ function postEnd(lastShardIndex: number, capturedSamples: number, tail: Uint8Array | null = null, user = OWNER_ID): Promise<Response> {
251
+ return post(`/${SESSION}/end?lastShardIndex=${lastShardIndex}&capturedSamples=${capturedSamples}`, tail, user);
252
+ }
253
+
254
+ beforeEach(async () => {
255
+ logErrorMock.mockReset();
256
+ logStatusMock.mockReset();
257
+ ledger = new FakeLedger();
258
+ addSession(SESSION);
259
+ scheduler = { ScheduleAssembly: vi.fn() };
260
+ deps = {
261
+ ResolveCaller: req => {
262
+ const id = req.header('x-test-user');
263
+ const scope = req.header('x-test-scope');
264
+ const user = {
265
+ ID: id,
266
+ IsMagicLinkAnonymous: req.header('x-test-anonymous') === 'true',
267
+ MagicLinkScope: scope ? { ResourceID: scope } : undefined,
268
+ };
269
+ return id ? { ContextUser: user as unknown as UserInfo, Provider: {} as unknown as DatabaseProviderBase } : null;
270
+ },
271
+ LoadConversationExternalID: async session => ledger.Sessions.get(session.ID)?.ConversationExternalID ?? null,
272
+ LoadOwnedSession: async (agentSessionId, contextUser) => {
273
+ const row = ledger.Sessions.get(agentSessionId);
274
+ if (!row) {
275
+ throw new RealtimeSessionNotFoundError(agentSessionId);
276
+ }
277
+ if (row.UserID !== contextUser.ID) {
278
+ throw new RealtimeSessionOwnershipError(agentSessionId);
279
+ }
280
+ return { ID: agentSessionId, __mj_CreatedAt: row.CreatedAt, ...row } as unknown as MJAIAgentSessionEntity;
281
+ },
282
+ CreateLedger: (_provider, contextUser) => (contextUser === SYSTEM_USER ? ledger : callerLedger(ledger)),
283
+ SystemUser: () => SYSTEM_USER,
284
+ Scheduler: scheduler,
285
+ GraceMs: () => GRACE_MS,
286
+ };
287
+ server = null;
288
+ await listen();
289
+ });
290
+
291
+ afterEach(async () => {
292
+ await new Promise<void>(resolve => (server ? server.close(() => resolve()) : resolve()));
293
+ });
294
+
295
+ describe('POST /:agentSessionId/shard/:index', () => {
296
+ it('stores the shard with its Sha256, StartSample and SampleCount', async () => {
297
+ const bytes = shardBytes(0);
298
+ const res = await postShard(0, bytes);
299
+ expect(res.status).toBe(200);
300
+ expect(await res.json()).toEqual({ stored: true, duplicate: false });
301
+ const stored = ledger.Shards.get(SESSION)?.get(0);
302
+ expect(stored).toMatchObject({ Index: 0, StartSample: 0, SampleCount: SHARD_SAMPLES, Sha256: sha256(bytes) });
303
+ expect(Buffer.compare(stored!.Data, Buffer.from(bytes))).toBe(0);
304
+ });
305
+
306
+ it('acknowledges a retry of identical bytes as a duplicate and keeps one row', async () => {
307
+ await postShard(0);
308
+ const res = await postShard(0);
309
+ expect(res.status).toBe(200);
310
+ expect(await res.json()).toEqual({ stored: true, duplicate: true });
311
+ expect(ledger.Shards.get(SESSION)?.size).toBe(1);
312
+ });
313
+
314
+ it('answers 409 to the same index with different bytes, logs it, and the session is Failed', async () => {
315
+ await postShard(3);
316
+ const res = await postShard(3, shardBytes(3, 2));
317
+ expect(res.status).toBe(409);
318
+ expect(ledger.Sessions.get(SESSION)?.RecordingStatus).toBe('Failed');
319
+ const logged = logErrorMock.mock.calls.map(call => String(call[0]));
320
+ expect(logged.some(line => line.includes(SESSION) && line.includes('3'))).toBe(true);
321
+ });
322
+
323
+ it('answers 413 to a body over 128 KiB without storing anything', async () => {
324
+ const res = await postShard(0, new Uint8Array(RECORDING_SHARD_MAX_BYTES + 1));
325
+ expect(res.status).toBe(413);
326
+ expect(ledger.Shards.get(SESSION)).toBeUndefined();
327
+ });
328
+
329
+ it('answers 400 to a malformed shard, a header index that differs from the path, and bad params', async () => {
330
+ expect((await postShard(0, new Uint8Array([1, 2, 3]))).status).toBe(400);
331
+ expect((await postShard(1, shardBytes(0))).status).toBe(400);
332
+ expect((await post(`/not-a-uuid/shard/0`, shardBytes(0))).status).toBe(400);
333
+ expect((await post(`/${SESSION}/shard/-1`, shardBytes(0))).status).toBe(400);
334
+ expect((await post(`/${SESSION}/shard/1.5`, shardBytes(0))).status).toBe(400);
335
+ // The ledger's Index is a SQL INT: an index past it is the client's error, not a 500 from the proc.
336
+ expect((await postShard(2 ** 31, shardBytes(2 ** 31, 1, 960))).status).toBe(400);
337
+ expect(ledger.Shards.get(SESSION)).toBeUndefined();
338
+ });
339
+
340
+ it('answers 403 to a caller who does not own the session and 404 to a missing session', async () => {
341
+ expect((await postShard(0, shardBytes(0), STRANGER_ID)).status).toBe(403);
342
+ expect((await post(`/${MISSING}/shard/0`, shardBytes(0))).status).toBe(404);
343
+ expect(ledger.Shards.get(SESSION)).toBeUndefined();
344
+ });
345
+
346
+ it('answers 413 to an over-cap chunked body (no Content-Length) through the streaming limit', async () => {
347
+ const url = new URL(`${baseUrl}/${SESSION}/shard/0`);
348
+ const status = await new Promise<number>((resolve, reject) => {
349
+ const req = httpRequest({
350
+ host: url.hostname, port: url.port, path: url.pathname, method: 'POST',
351
+ headers: { 'Content-Type': 'application/octet-stream', 'Transfer-Encoding': 'chunked', 'x-test-user': OWNER_ID },
352
+ }, res => { res.resume(); resolve(res.statusCode ?? 0); });
353
+ // The server may answer and close before the whole body is written.
354
+ req.on('error', error => (error.message.includes('EPIPE') || error.message.includes('ECONNRESET') ? undefined : reject(error)));
355
+ for (let i = 0; i < 5; i++) {
356
+ req.write(Buffer.alloc(32 * 1024));
357
+ }
358
+ req.end();
359
+ });
360
+ expect(status).toBe(413);
361
+ expect(logStatusMock.mock.calls.some(call => String(call[0]).includes('over the cap'))).toBe(true);
362
+ expect(ledger.Shards.get(SESSION)).toBeUndefined();
363
+ });
364
+
365
+ describe('anonymous widget guests (per-session scope)', () => {
366
+ const SCOPE = 'scope-Abc_123';
367
+
368
+ beforeEach(() => {
369
+ addSession(SESSION, { ConversationExternalID: SCOPE });
370
+ });
371
+
372
+ it('refuses an anonymous principal without a scope with 403 and logs the reason', async () => {
373
+ const res = await post(`/${SESSION}/shard/0`, shardBytes(0), OWNER_ID, { 'x-test-anonymous': 'true' });
374
+ expect(res.status).toBe(403);
375
+ expect(ledger.Shards.get(SESSION)).toBeUndefined();
376
+ expect(logErrorMock.mock.calls.some(call => String(call[0]).includes(SESSION) && String(call[0]).includes('scope_missing'))).toBe(true);
377
+ });
378
+
379
+ it('refuses a mismatched scope with 403, on the shard and the terminal route', async () => {
380
+ const headers = { 'x-test-anonymous': 'true', 'x-test-scope': 'another-guest' };
381
+ expect((await post(`/${SESSION}/shard/0`, shardBytes(0), OWNER_ID, headers)).status).toBe(403);
382
+ expect((await post(`/${SESSION}/end?lastShardIndex=0&capturedSamples=1`, null, OWNER_ID, headers)).status).toBe(403);
383
+ expect(ledger.Shards.get(SESSION)).toBeUndefined();
384
+ expect(ledger.Sessions.get(SESSION)?.RecordingExpectedShards).toBeNull();
385
+ expect(logErrorMock.mock.calls.some(call => String(call[0]).includes(SESSION) && String(call[0]).includes('scope_mismatch'))).toBe(true);
386
+ });
387
+
388
+ it('accepts the guest that carries the matching scope', async () => {
389
+ const headers = { 'x-test-anonymous': 'true', 'x-test-scope': SCOPE };
390
+ expect((await post(`/${SESSION}/shard/0`, shardBytes(0), OWNER_ID, headers)).status).toBe(200);
391
+ expect((await post(`/${SESSION}/end?lastShardIndex=0&capturedSamples=${SHARD_SAMPLES}`, null, OWNER_ID, headers)).status).toBe(200);
392
+ });
393
+
394
+ it('leaves a named owner (no scope, not anonymous) unaffected', async () => {
395
+ expect((await postShard(0)).status).toBe(200);
396
+ });
397
+ });
398
+
399
+ it('answers 401 when no caller can be resolved', async () => {
400
+ const res = await fetch(`${baseUrl}/${SESSION}/shard/0`, {
401
+ method: 'POST', headers: { 'Content-Type': 'application/octet-stream' }, body: shardBytes(0) as Uint8Array<ArrayBuffer>,
402
+ });
403
+ expect(res.status).toBe(401);
404
+ });
405
+
406
+ it('schedules a re-assembly now for a late gap shard on a Partial session closed under 24 h ago', async () => {
407
+ addSession(SESSION, { RecordingStatus: 'Partial', RecordingExpectedShards: 4, RecordingCapturedSamples: 4 * SHARD_SAMPLES, ClosedAt: new Date(Date.now() - HOUR_MS) });
408
+ expect((await postShard(2)).status).toBe(200);
409
+ expect(scheduler.ScheduleAssembly).toHaveBeenCalledWith(SESSION, 0, false);
410
+ });
411
+
412
+ it('answers 410 to a shard for a session closed over 24 h ago and stores nothing (the recording is settled)', async () => {
413
+ addSession(SESSION, { RecordingStatus: 'Partial', RecordingExpectedShards: 4, RecordingCapturedSamples: 4 * SHARD_SAMPLES, ClosedAt: new Date(Date.now() - 25 * HOUR_MS) });
414
+ expect((await postShard(2)).status).toBe(410);
415
+ expect(ledger.Shards.get(SESSION)?.has(2) ?? false).toBe(false);
416
+ expect(scheduler.ScheduleAssembly).not.toHaveBeenCalled();
417
+ });
418
+
419
+ it('answers 400 to an index at or past the most shards a recording can assemble, storing nothing', async () => {
420
+ // Live smoke C2: 2100 shards were accepted (and kept) into one session, then one at index 50000.
421
+ expect((await postShard(MAX_ASSEMBLY_SHARDS)).status).toBe(400);
422
+ expect(ledger.Shards.get(SESSION)?.size ?? 0).toBe(0);
423
+ });
424
+
425
+ it("answers 400 to a shard whose audio ends after more time than the session has existed", async () => {
426
+ addSession(SESSION, { CreatedAt: new Date(Date.now() - 60_000) });
427
+ // index 100 starts at 500 s of audio; the session is a minute old
428
+ expect((await postShard(100)).status).toBe(400);
429
+ expect(ledger.Shards.get(SESSION)?.size ?? 0).toBe(0);
430
+ expect((await postShard(0)).status).toBe(200);
431
+ });
432
+
433
+ it('schedules now when the last missing shard of a closed Pending (grace-waiting) session arrives', async () => {
434
+ addSession(SESSION, { RecordingStatus: 'Pending', RecordingExpectedShards: 2, RecordingCapturedSamples: 2 * SHARD_SAMPLES, ClosedAt: new Date(Date.now() - 60_000) });
435
+ await postShard(1);
436
+ expect(scheduler.ScheduleAssembly).not.toHaveBeenCalled();
437
+ await postShard(0);
438
+ expect(scheduler.ScheduleAssembly).toHaveBeenCalledWith(SESSION, 0, false);
439
+ });
440
+
441
+ it('while the call is still open, a shard that completes a Pending recording schedules after the grace, not now', async () => {
442
+ // A hidden tab's terminal, then its keepalive re-sends: assembling mid-call on every tab switch
443
+ // would rewrite recording.wav each time.
444
+ addSession(SESSION, { RecordingStatus: 'Pending', RecordingExpectedShards: 2, RecordingCapturedSamples: 2 * SHARD_SAMPLES });
445
+ await postShard(1);
446
+ await postShard(0);
447
+ expect(scheduler.ScheduleAssembly).toHaveBeenCalledWith(SESSION, GRACE_MS, true);
448
+ expect(scheduler.ScheduleAssembly).not.toHaveBeenCalledWith(SESSION, 0, false);
449
+ });
450
+
451
+ it('while the call is still open, a late gap shard of a Partial recording re-assembles after the grace, not now', async () => {
452
+ addSession(SESSION, { RecordingStatus: 'Partial', RecordingExpectedShards: 4, RecordingCapturedSamples: 4 * SHARD_SAMPLES });
453
+ expect((await postShard(2)).status).toBe(200);
454
+ expect(scheduler.ScheduleAssembly).toHaveBeenCalledWith(SESSION, GRACE_MS, true);
455
+ });
456
+
457
+ it('does not schedule an ordinary live-call shard', async () => {
458
+ await postShard(0);
459
+ await postShard(1);
460
+ expect(scheduler.ScheduleAssembly).not.toHaveBeenCalled();
461
+ });
462
+ });
463
+
464
+ describe('POST /:agentSessionId/end', () => {
465
+ it('stamps expected = lastShardIndex + 1 and the captured samples, and schedules now when every shard is present', async () => {
466
+ await postShard(0);
467
+ await postShard(1);
468
+ const res = await postEnd(1, 2 * SHARD_SAMPLES);
469
+ expect(res.status).toBe(200);
470
+ expect(await res.json()).toEqual({ accepted: true, ignored: false });
471
+ expect(ledger.Sessions.get(SESSION)).toMatchObject({ RecordingExpectedShards: 2, RecordingCapturedSamples: 2 * SHARD_SAMPLES, RecordingStatus: 'Pending' });
472
+ expect(scheduler.ScheduleAssembly).toHaveBeenCalledWith(SESSION, 0, false);
473
+ });
474
+
475
+ it('records the tail like a shard before stamping', async () => {
476
+ await postShard(0);
477
+ const tail = shardBytes(1, 1, 48_000);
478
+ const res = await postEnd(1, SHARD_SAMPLES + 48_000, tail);
479
+ expect(res.status).toBe(200);
480
+ expect(ledger.Shards.get(SESSION)?.get(1)).toMatchObject({ SampleCount: 48_000, Sha256: sha256(tail) });
481
+ expect(scheduler.ScheduleAssembly).toHaveBeenCalledWith(SESSION, 0, false);
482
+ });
483
+
484
+ it('applies the late-shard rule to a tail: a missing tail re-sent to a Partial recording closed under 24 h ago re-assembles now', async () => {
485
+ addSession(SESSION, { RecordingStatus: 'Partial', RecordingExpectedShards: 4, RecordingCapturedSamples: 3 * SHARD_SAMPLES + 48_000, ClosedAt: new Date(Date.now() - HOUR_MS) });
486
+ const res = await postEnd(3, 3 * SHARD_SAMPLES + 48_000, shardBytes(3, 1, 48_000));
487
+ expect(res.status).toBe(200);
488
+ expect(ledger.Shards.get(SESSION)?.has(3)).toBe(true);
489
+ expect(scheduler.ScheduleAssembly).toHaveBeenCalledWith(SESSION, 0, false);
490
+ });
491
+
492
+ it('answers 410 to a terminal for a session closed over 24 h ago and stamps nothing', async () => {
493
+ addSession(SESSION, { RecordingStatus: 'Partial', RecordingExpectedShards: 4, RecordingCapturedSamples: 3 * SHARD_SAMPLES + 48_000, ClosedAt: new Date(Date.now() - 25 * HOUR_MS) });
494
+ expect((await postEnd(3, 3 * SHARD_SAMPLES + 48_000, shardBytes(3, 1, 48_000))).status).toBe(410);
495
+ expect(ledger.Shards.get(SESSION)?.has(3) ?? false).toBe(false);
496
+ expect(scheduler.ScheduleAssembly).not.toHaveBeenCalled();
497
+ });
498
+
499
+ it('answers 400 to a terminal claiming more audio than the session has existed, and stamps nothing', async () => {
500
+ // Live smoke C1: one 5 s shard plus a 3 h claim made the server write a 1 GB recording.wav.
501
+ addSession(SESSION, { CreatedAt: new Date(Date.now() - 60_000) });
502
+ expect((await postShard(0)).status).toBe(200);
503
+ expect((await postEnd(0, 518_400_000)).status).toBe(400);
504
+ expect(ledger.Sessions.get(SESSION)?.RecordingExpectedShards).toBeNull();
505
+ expect(scheduler.ScheduleAssembly).not.toHaveBeenCalled();
506
+ });
507
+
508
+ it('answers 400 to a terminal listing more shards than a recording can assemble', async () => {
509
+ expect((await postEnd(MAX_ASSEMBLY_SHARDS, SHARD_SAMPLES)).status).toBe(400);
510
+ expect(ledger.Sessions.get(SESSION)?.RecordingExpectedShards).toBeNull();
511
+ });
512
+
513
+ it('without a system user still stamps the terminal and answers 200, leaving assembly to the janitor', async () => {
514
+ deps.SystemUser = () => null;
515
+ expect((await postShard(0)).status).toBe(200);
516
+ const res = await postEnd(0, SHARD_SAMPLES);
517
+ expect(res.status).toBe(200);
518
+ expect(ledger.Sessions.get(SESSION)?.RecordingExpectedShards).toBe(1);
519
+ expect(scheduler.ScheduleAssembly).not.toHaveBeenCalled();
520
+ expect(logErrorMock).toHaveBeenCalledWith(expect.stringContaining('no system user'));
521
+ });
522
+
523
+ it('a non-final terminal (the tab was hidden) schedules after the grace even when every shard is present', async () => {
524
+ await postShard(0);
525
+ const res = await post(`/${SESSION}/end?lastShardIndex=0&capturedSamples=${SHARD_SAMPLES}&final=false`, null);
526
+ expect(res.status).toBe(200);
527
+ expect(ledger.Sessions.get(SESSION)?.RecordingExpectedShards).toBe(1);
528
+ expect(scheduler.ScheduleAssembly).toHaveBeenCalledWith(SESSION, GRACE_MS, true);
529
+ expect(scheduler.ScheduleAssembly).not.toHaveBeenCalledWith(SESSION, 0, false);
530
+ });
531
+
532
+ it('rejects a final flag that is not true or false', async () => {
533
+ const res = await post(`/${SESSION}/end?lastShardIndex=0&capturedSamples=${SHARD_SAMPLES}&final=maybe`, null);
534
+ expect(res.status).toBe(400);
535
+ });
536
+
537
+ it('a final terminal with shards missing re-checks shortly (their keepalive re-sends follow it), not after the grace', async () => {
538
+ // End then the tab closes: the close is never sent, and the shards still unacknowledged at End
539
+ // land just after the terminal. A Deferred re-check arms the forced-Partial grace (scheduler).
540
+ await postShard(0);
541
+ await postEnd(2, 3 * SHARD_SAMPLES);
542
+ expect(scheduler.ScheduleAssembly).toHaveBeenCalledWith(SESSION, FINAL_TERMINAL_RECHECK_MS, false);
543
+ expect(FINAL_TERMINAL_RECHECK_MS).toBeLessThan(10_000);
544
+ });
545
+
546
+ it('treats a duplicate /end as idempotent', async () => {
547
+ await postShard(0);
548
+ expect((await postEnd(0, SHARD_SAMPLES)).status).toBe(200);
549
+ const res = await postEnd(0, SHARD_SAMPLES);
550
+ expect(res.status).toBe(200);
551
+ expect(await res.json()).toEqual({ accepted: true, ignored: false });
552
+ expect(ledger.Sessions.get(SESSION)).toMatchObject({ RecordingExpectedShards: 1, RecordingCapturedSamples: SHARD_SAMPLES });
553
+ });
554
+
555
+ it('ignores and logs a lower /end (stored vs received), still answering 200', async () => {
556
+ await postShard(0);
557
+ await postShard(1);
558
+ await postEnd(1, 2 * SHARD_SAMPLES);
559
+ scheduler.ScheduleAssembly.mockClear();
560
+ const res = await postEnd(0, SHARD_SAMPLES);
561
+ expect(res.status).toBe(200);
562
+ expect(await res.json()).toEqual({ accepted: true, ignored: true });
563
+ expect(ledger.Sessions.get(SESSION)).toMatchObject({ RecordingExpectedShards: 2, RecordingCapturedSamples: 2 * SHARD_SAMPLES });
564
+ const logged = logStatusMock.mock.calls.map(call => String(call[0]));
565
+ expect(logged.some(line => line.includes(SESSION) && line.includes(String(2 * SHARD_SAMPLES)) && line.includes(String(SHARD_SAMPLES)))).toBe(true);
566
+ });
567
+
568
+ it('accepts an empty recording (lastShardIndex -1, no samples, no tail) and does nothing else', async () => {
569
+ const res = await postEnd(-1, 0);
570
+ expect(res.status).toBe(200);
571
+ expect(ledger.Sessions.get(SESSION)).toMatchObject({ RecordingStatus: null, RecordingExpectedShards: null });
572
+ expect(scheduler.ScheduleAssembly).not.toHaveBeenCalled();
573
+ });
574
+
575
+ it('rejects bad terminal params, a tail whose index differs, and a conflicting tail', async () => {
576
+ expect((await post(`/${SESSION}/end?lastShardIndex=-2&capturedSamples=0`, null)).status).toBe(400);
577
+ expect((await post(`/${SESSION}/end?lastShardIndex=1&capturedSamples=-5`, null)).status).toBe(400);
578
+ expect((await post(`/${SESSION}/end?lastShardIndex=abc&capturedSamples=0`, null)).status).toBe(400);
579
+ expect((await post(`/${SESSION}/end?capturedSamples=0`, null)).status).toBe(400);
580
+ expect((await postEnd(-1, 5)).status).toBe(400);
581
+ // Expected shards (lastShardIndex + 1) must fit the INT column too.
582
+ expect((await postEnd(2 ** 31 - 1, 0)).status).toBe(400);
583
+ expect((await postEnd(2, 3 * SHARD_SAMPLES, shardBytes(1))).status).toBe(400);
584
+ await postShard(1);
585
+ expect((await postEnd(1, 2 * SHARD_SAMPLES, shardBytes(1, 9))).status).toBe(409);
586
+ expect(ledger.Sessions.get(SESSION)?.RecordingExpectedShards).toBeNull();
587
+ });
588
+
589
+ it('answers 413 to a terminal body over 48 KiB, 403 to a non-owner and 404 to a missing session', async () => {
590
+ expect((await postEnd(0, SHARD_SAMPLES, new Uint8Array(48 * 1024 + 1))).status).toBe(413);
591
+ expect((await postEnd(0, SHARD_SAMPLES, null, STRANGER_ID)).status).toBe(403);
592
+ expect((await post(`/${MISSING}/end?lastShardIndex=0&capturedSamples=1`, null)).status).toBe(404);
593
+ });
594
+ });
595
+
596
+ describe('with the real scheduler', () => {
597
+ let assemble: ReturnType<typeof vi.fn<(input: AssembleRealtimeRecordingInput) => Promise<AssembleRealtimeRecordingOutcome>>>;
598
+
599
+ beforeEach(() => {
600
+ assemble = vi.fn(async (input: AssembleRealtimeRecordingInput) => {
601
+ await input.Ledger.FinishAssembly({ SessionID: input.SessionID, Version: input.Version, Status: 'Complete', Gaps: [], FileID: null });
602
+ return 'Complete' as AssembleRealtimeRecordingOutcome;
603
+ });
604
+ RecordingAssemblyScheduler.Instance.Start({
605
+ Provider: {} as unknown as DatabaseProviderBase,
606
+ SystemUser: { ID: 'system' } as unknown as UserInfo,
607
+ GraceMs: () => GRACE_MS,
608
+ CreateLedger: () => ledger,
609
+ Assemble: assemble,
610
+ });
611
+ deps.Scheduler = RecordingAssemblyScheduler.Instance;
612
+ });
613
+
614
+ afterEach(() => {
615
+ RecordingAssemblyScheduler.Instance.Shutdown();
616
+ });
617
+
618
+ it('two concurrent /end calls assemble exactly once (the claim winner), after the responses', async () => {
619
+ await postShard(0);
620
+ await postShard(1);
621
+ const [first, second] = await Promise.all([postEnd(1, 2 * SHARD_SAMPLES), postEnd(1, 2 * SHARD_SAMPLES)]);
622
+ expect(first.status).toBe(200);
623
+ expect(second.status).toBe(200);
624
+ await vi.waitFor(() => expect(ledger.Sessions.get(SESSION)?.RecordingStatus).toBe('Complete'));
625
+ // Give a second (losing) claim time to run, if one were scheduled.
626
+ await new Promise(resolve => setTimeout(resolve, 50));
627
+ expect(assemble).toHaveBeenCalledTimes(1);
628
+ expect(assemble.mock.calls[0][0]).toMatchObject({ SessionID: SESSION, ForcePartial: false });
629
+ });
630
+
631
+ it('never decodes inside the request: the response comes back before the assembly timer fires', async () => {
632
+ const timers: Array<() => void> = [];
633
+ RecordingAssemblyScheduler.Instance.Start({
634
+ Provider: {} as unknown as DatabaseProviderBase,
635
+ SystemUser: { ID: 'system' } as unknown as UserInfo,
636
+ GraceMs: () => GRACE_MS,
637
+ CreateLedger: () => ledger,
638
+ Assemble: assemble,
639
+ Clock: { Now: () => 0, StartTimer: callback => { timers.push(callback); return { Cancel: () => undefined }; } },
640
+ });
641
+ await postShard(0);
642
+ const res = await postEnd(0, SHARD_SAMPLES);
643
+ expect(res.status).toBe(200);
644
+ expect(assemble).not.toHaveBeenCalled();
645
+ expect(ledger.Sessions.get(SESSION)?.RecordingStatus).toBe('Pending');
646
+ expect(timers).toHaveLength(1);
647
+
648
+ timers[0]();
649
+ await vi.waitFor(() => expect(assemble).toHaveBeenCalledTimes(1));
650
+ });
651
+ });
652
+ });