@memberjunction/server 5.43.0 → 5.45.0

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 (309) hide show
  1. package/dist/agentSessions/ReturningVisitorRecap.d.ts +39 -0
  2. package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -0
  3. package/dist/agentSessions/ReturningVisitorRecap.js +198 -0
  4. package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -0
  5. package/dist/agentSessions/SessionJanitor.d.ts +13 -0
  6. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
  7. package/dist/agentSessions/SessionJanitor.js +65 -7
  8. package/dist/agentSessions/SessionJanitor.js.map +1 -1
  9. package/dist/agentSessions/SessionManager.d.ts.map +1 -1
  10. package/dist/agentSessions/SessionManager.js +40 -0
  11. package/dist/agentSessions/SessionManager.js.map +1 -1
  12. package/dist/agentSessions/index.d.ts.map +1 -1
  13. package/dist/agentSessions/index.js +3 -1
  14. package/dist/agentSessions/index.js.map +1 -1
  15. package/dist/auth/magicLink/MagicLinkRouter.d.ts +10 -0
  16. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  17. package/dist/auth/magicLink/MagicLinkRouter.js +16 -0
  18. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  19. package/dist/auth/magicLink/index.d.ts +1 -1
  20. package/dist/auth/magicLink/index.d.ts.map +1 -1
  21. package/dist/auth/magicLink/index.js +1 -1
  22. package/dist/auth/magicLink/index.js.map +1 -1
  23. package/dist/auth/magicLink/types.d.ts +26 -0
  24. package/dist/auth/magicLink/types.d.ts.map +1 -1
  25. package/dist/config.d.ts +1883 -144
  26. package/dist/config.d.ts.map +1 -1
  27. package/dist/config.js +160 -36
  28. package/dist/config.js.map +1 -1
  29. package/dist/context.d.ts.map +1 -1
  30. package/dist/context.js +45 -2
  31. package/dist/context.js.map +1 -1
  32. package/dist/generated/generated.d.ts +1684 -54
  33. package/dist/generated/generated.d.ts.map +1 -1
  34. package/dist/generated/generated.js +57105 -48029
  35. package/dist/generated/generated.js.map +1 -1
  36. package/dist/generic/ResolverBase.d.ts +13 -0
  37. package/dist/generic/ResolverBase.d.ts.map +1 -1
  38. package/dist/generic/ResolverBase.js +22 -0
  39. package/dist/generic/ResolverBase.js.map +1 -1
  40. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  41. package/dist/generic/RunViewResolver.js +4 -0
  42. package/dist/generic/RunViewResolver.js.map +1 -1
  43. package/dist/index.d.ts +2 -2
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +110 -3
  46. package/dist/index.js.map +1 -1
  47. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  48. package/dist/integration/CustomColumnPromoter.js +6 -0
  49. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  50. package/dist/realtimeWidget/WidgetRouter.d.ts +26 -0
  51. package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -0
  52. package/dist/realtimeWidget/WidgetRouter.js +182 -0
  53. package/dist/realtimeWidget/WidgetRouter.js.map +1 -0
  54. package/dist/realtimeWidget/WidgetSessionService.d.ts +265 -0
  55. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -0
  56. package/dist/realtimeWidget/WidgetSessionService.js +477 -0
  57. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -0
  58. package/dist/realtimeWidget/host-identity.d.ts +40 -0
  59. package/dist/realtimeWidget/host-identity.d.ts.map +1 -0
  60. package/dist/realtimeWidget/host-identity.js +58 -0
  61. package/dist/realtimeWidget/host-identity.js.map +1 -0
  62. package/dist/realtimeWidget/index.d.ts +10 -0
  63. package/dist/realtimeWidget/index.d.ts.map +1 -0
  64. package/dist/realtimeWidget/index.js +9 -0
  65. package/dist/realtimeWidget/index.js.map +1 -0
  66. package/dist/realtimeWidget/visitorIdentity.d.ts +76 -0
  67. package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -0
  68. package/dist/realtimeWidget/visitorIdentity.js +202 -0
  69. package/dist/realtimeWidget/visitorIdentity.js.map +1 -0
  70. package/dist/realtimeWidget/widgetCore.d.ts +106 -0
  71. package/dist/realtimeWidget/widgetCore.d.ts.map +1 -0
  72. package/dist/realtimeWidget/widgetCore.js +173 -0
  73. package/dist/realtimeWidget/widgetCore.js.map +1 -0
  74. package/dist/realtimeWidget/widgetGuestElevation.d.ts +51 -0
  75. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -0
  76. package/dist/realtimeWidget/widgetGuestElevation.js +79 -0
  77. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -0
  78. package/dist/resolvers/ComponentRegistryResolver.d.ts +7 -0
  79. package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
  80. package/dist/resolvers/ComponentRegistryResolver.js +31 -8
  81. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  82. package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -1
  83. package/dist/resolvers/EntityPermissionResolver.js +1 -2
  84. package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
  85. package/dist/resolvers/FileResolver.d.ts +74 -0
  86. package/dist/resolvers/FileResolver.d.ts.map +1 -1
  87. package/dist/resolvers/FileResolver.js +211 -2
  88. package/dist/resolvers/FileResolver.js.map +1 -1
  89. package/dist/resolvers/QueryResolver.d.ts +6 -4
  90. package/dist/resolvers/QueryResolver.d.ts.map +1 -1
  91. package/dist/resolvers/QueryResolver.js +29 -14
  92. package/dist/resolvers/QueryResolver.js.map +1 -1
  93. package/dist/resolvers/QuerySystemUserResolver.d.ts +9 -6
  94. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  95. package/dist/resolvers/QuerySystemUserResolver.js +15 -13
  96. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  97. package/dist/resolvers/RealtimeBridgeResolver.d.ts +8 -1
  98. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
  99. package/dist/resolvers/RealtimeBridgeResolver.js +28 -4
  100. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
  101. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +91 -2
  102. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  103. package/dist/resolvers/RealtimeClientSessionResolver.js +337 -17
  104. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  105. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
  106. package/dist/resolvers/RemoteBrowserActionResolver.js +17 -2
  107. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
  108. package/dist/resolvers/RingCentralTelephonyResolver.d.ts +27 -0
  109. package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +1 -0
  110. package/dist/resolvers/RingCentralTelephonyResolver.js +87 -0
  111. package/dist/resolvers/RingCentralTelephonyResolver.js.map +1 -0
  112. package/dist/resolvers/RunAIAgentResolver.d.ts +12 -3
  113. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  114. package/dist/resolvers/RunAIAgentResolver.js +50 -17
  115. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  116. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  117. package/dist/resolvers/SearchKnowledgeResolver.js +2 -0
  118. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  119. package/dist/resolvers/TeamsMeetingsResolver.d.ts +28 -0
  120. package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +1 -0
  121. package/dist/resolvers/TeamsMeetingsResolver.js +91 -0
  122. package/dist/resolvers/TeamsMeetingsResolver.js.map +1 -0
  123. package/dist/resolvers/TelephonyResolver.d.ts +26 -0
  124. package/dist/resolvers/TelephonyResolver.d.ts.map +1 -0
  125. package/dist/resolvers/TelephonyResolver.js +86 -0
  126. package/dist/resolvers/TelephonyResolver.js.map +1 -0
  127. package/dist/resolvers/TestQuerySQLResolver.d.ts +1 -1
  128. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  129. package/dist/resolvers/TestQuerySQLResolver.js +2 -3
  130. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  131. package/dist/resolvers/VonageTelephonyResolver.d.ts +26 -0
  132. package/dist/resolvers/VonageTelephonyResolver.d.ts.map +1 -0
  133. package/dist/resolvers/VonageTelephonyResolver.js +86 -0
  134. package/dist/resolvers/VonageTelephonyResolver.js.map +1 -0
  135. package/dist/resolvers/meetingRecordingRegistration.d.ts +124 -0
  136. package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -0
  137. package/dist/resolvers/meetingRecordingRegistration.js +311 -0
  138. package/dist/resolvers/meetingRecordingRegistration.js.map +1 -0
  139. package/dist/resolvers/peaksSidecar.d.ts +30 -0
  140. package/dist/resolvers/peaksSidecar.d.ts.map +1 -0
  141. package/dist/resolvers/peaksSidecar.js +51 -0
  142. package/dist/resolvers/peaksSidecar.js.map +1 -0
  143. package/dist/rest/MediaAccessKeys.d.ts +68 -0
  144. package/dist/rest/MediaAccessKeys.d.ts.map +1 -0
  145. package/dist/rest/MediaAccessKeys.js +96 -0
  146. package/dist/rest/MediaAccessKeys.js.map +1 -0
  147. package/dist/rest/MediaStreamHandler.d.ts +26 -0
  148. package/dist/rest/MediaStreamHandler.d.ts.map +1 -0
  149. package/dist/rest/MediaStreamHandler.js +195 -0
  150. package/dist/rest/MediaStreamHandler.js.map +1 -0
  151. package/dist/rest/mediaRange.d.ts +41 -0
  152. package/dist/rest/mediaRange.d.ts.map +1 -0
  153. package/dist/rest/mediaRange.js +60 -0
  154. package/dist/rest/mediaRange.js.map +1 -0
  155. package/dist/telephony/RingCentralTelephonyService.d.ts +115 -0
  156. package/dist/telephony/RingCentralTelephonyService.d.ts.map +1 -0
  157. package/dist/telephony/RingCentralTelephonyService.js +262 -0
  158. package/dist/telephony/RingCentralTelephonyService.js.map +1 -0
  159. package/dist/telephony/TeamsMeetingsRouter.d.ts +40 -0
  160. package/dist/telephony/TeamsMeetingsRouter.d.ts.map +1 -0
  161. package/dist/telephony/TeamsMeetingsRouter.js +96 -0
  162. package/dist/telephony/TeamsMeetingsRouter.js.map +1 -0
  163. package/dist/telephony/TeamsMeetingsService.d.ts +113 -0
  164. package/dist/telephony/TeamsMeetingsService.d.ts.map +1 -0
  165. package/dist/telephony/TeamsMeetingsService.js +196 -0
  166. package/dist/telephony/TeamsMeetingsService.js.map +1 -0
  167. package/dist/telephony/TwilioTelephonyRouter.d.ts +36 -0
  168. package/dist/telephony/TwilioTelephonyRouter.d.ts.map +1 -0
  169. package/dist/telephony/TwilioTelephonyRouter.js +143 -0
  170. package/dist/telephony/TwilioTelephonyRouter.js.map +1 -0
  171. package/dist/telephony/TwilioTelephonyService.d.ts +95 -0
  172. package/dist/telephony/TwilioTelephonyService.d.ts.map +1 -0
  173. package/dist/telephony/TwilioTelephonyService.js +193 -0
  174. package/dist/telephony/TwilioTelephonyService.js.map +1 -0
  175. package/dist/telephony/VonageTelephonyRouter.d.ts +41 -0
  176. package/dist/telephony/VonageTelephonyRouter.d.ts.map +1 -0
  177. package/dist/telephony/VonageTelephonyRouter.js +201 -0
  178. package/dist/telephony/VonageTelephonyRouter.js.map +1 -0
  179. package/dist/telephony/VonageTelephonyService.d.ts +90 -0
  180. package/dist/telephony/VonageTelephonyService.d.ts.map +1 -0
  181. package/dist/telephony/VonageTelephonyService.js +191 -0
  182. package/dist/telephony/VonageTelephonyService.js.map +1 -0
  183. package/dist/telephony/calendar-scheduler.d.ts +67 -0
  184. package/dist/telephony/calendar-scheduler.d.ts.map +1 -0
  185. package/dist/telephony/calendar-scheduler.js +133 -0
  186. package/dist/telephony/calendar-scheduler.js.map +1 -0
  187. package/dist/telephony/index.d.ts +29 -0
  188. package/dist/telephony/index.d.ts.map +1 -0
  189. package/dist/telephony/index.js +38 -0
  190. package/dist/telephony/index.js.map +1 -0
  191. package/dist/telephony/media-upgrade-router.d.ts +33 -0
  192. package/dist/telephony/media-upgrade-router.d.ts.map +1 -0
  193. package/dist/telephony/media-upgrade-router.js +56 -0
  194. package/dist/telephony/media-upgrade-router.js.map +1 -0
  195. package/dist/telephony/ringcentral-runtime.d.ts +14 -0
  196. package/dist/telephony/ringcentral-runtime.d.ts.map +1 -0
  197. package/dist/telephony/ringcentral-runtime.js +18 -0
  198. package/dist/telephony/ringcentral-runtime.js.map +1 -0
  199. package/dist/telephony/teams-meetings-runtime.d.ts +16 -0
  200. package/dist/telephony/teams-meetings-runtime.d.ts.map +1 -0
  201. package/dist/telephony/teams-meetings-runtime.js +20 -0
  202. package/dist/telephony/teams-meetings-runtime.js.map +1 -0
  203. package/dist/telephony/teamsAcsMediaRegistry.d.ts +69 -0
  204. package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +1 -0
  205. package/dist/telephony/teamsAcsMediaRegistry.js +118 -0
  206. package/dist/telephony/teamsAcsMediaRegistry.js.map +1 -0
  207. package/dist/telephony/telephony-runtime.d.ts +14 -0
  208. package/dist/telephony/telephony-runtime.d.ts.map +1 -0
  209. package/dist/telephony/telephony-runtime.js +18 -0
  210. package/dist/telephony/telephony-runtime.js.map +1 -0
  211. package/dist/telephony/twilioMediaRegistry.d.ts +60 -0
  212. package/dist/telephony/twilioMediaRegistry.d.ts.map +1 -0
  213. package/dist/telephony/twilioMediaRegistry.js +106 -0
  214. package/dist/telephony/twilioMediaRegistry.js.map +1 -0
  215. package/dist/telephony/vonage-runtime.d.ts +14 -0
  216. package/dist/telephony/vonage-runtime.d.ts.map +1 -0
  217. package/dist/telephony/vonage-runtime.js +18 -0
  218. package/dist/telephony/vonage-runtime.js.map +1 -0
  219. package/dist/telephony/vonageMediaRegistry.d.ts +78 -0
  220. package/dist/telephony/vonageMediaRegistry.d.ts.map +1 -0
  221. package/dist/telephony/vonageMediaRegistry.js +158 -0
  222. package/dist/telephony/vonageMediaRegistry.js.map +1 -0
  223. package/package.json +88 -82
  224. package/src/__tests__/FileResolverPeaks.test.ts +64 -0
  225. package/src/__tests__/MediaAccessKeys.test.ts +68 -0
  226. package/src/__tests__/MediaStreamHandler.test.ts +91 -0
  227. package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -1
  228. package/src/__tests__/RealtimeClientSessionResolver.test.ts +69 -0
  229. package/src/__tests__/RealtimeCoAgentAppAwareness.integration.test.ts +191 -0
  230. package/src/__tests__/RemoteBrowserSnapshot.test.ts +123 -0
  231. package/src/__tests__/SessionManager.test.ts +2 -0
  232. package/src/__tests__/meetingRecordingRegistration.test.ts +248 -0
  233. package/src/__tests__/search-knowledge-tags.test.ts +9 -9
  234. package/src/__tests__/teams-meetings-service.test.ts +65 -0
  235. package/src/__tests__/teamsAcsMediaRegistry.test.ts +82 -0
  236. package/src/__tests__/twilio-telephony-service.test.ts +23 -0
  237. package/src/__tests__/twilioMediaRegistry.test.ts +103 -0
  238. package/src/__tests__/vonageMediaRegistry.test.ts +180 -0
  239. package/src/__tests__/widget.test.ts +204 -0
  240. package/src/__tests__/widgetGuestElevation.test.ts +57 -0
  241. package/src/__tests__/widgetHostIdentity.test.ts +117 -0
  242. package/src/agentSessions/ReturningVisitorRecap.ts +242 -0
  243. package/src/agentSessions/SessionJanitor.ts +66 -6
  244. package/src/agentSessions/SessionManager.ts +41 -0
  245. package/src/agentSessions/index.ts +3 -1
  246. package/src/auth/magicLink/MagicLinkRouter.ts +17 -0
  247. package/src/auth/magicLink/index.ts +1 -0
  248. package/src/auth/magicLink/types.ts +26 -0
  249. package/src/config.ts +172 -38
  250. package/src/context.ts +50 -3
  251. package/src/generated/generated.ts +26726 -20464
  252. package/src/generic/ResolverBase.ts +28 -0
  253. package/src/generic/RunViewResolver.ts +4 -0
  254. package/src/index.ts +119 -3
  255. package/src/integration/CustomColumnPromoter.ts +6 -0
  256. package/src/realtimeWidget/WidgetRouter.ts +204 -0
  257. package/src/realtimeWidget/WidgetSessionService.ts +714 -0
  258. package/src/realtimeWidget/host-identity.ts +84 -0
  259. package/src/realtimeWidget/index.ts +15 -0
  260. package/src/realtimeWidget/visitorIdentity.ts +258 -0
  261. package/src/realtimeWidget/widgetCore.ts +231 -0
  262. package/src/realtimeWidget/widgetGuestElevation.ts +115 -0
  263. package/src/resolvers/ComponentRegistryResolver.ts +36 -10
  264. package/src/resolvers/EntityPermissionResolver.ts +1 -2
  265. package/src/resolvers/FileResolver.ts +199 -1
  266. package/src/resolvers/QueryResolver.ts +33 -19
  267. package/src/resolvers/QuerySystemUserResolver.ts +14 -10
  268. package/src/resolvers/RealtimeBridgeResolver.ts +36 -4
  269. package/src/resolvers/RealtimeClientSessionResolver.ts +383 -9
  270. package/src/resolvers/RemoteBrowserActionResolver.ts +18 -2
  271. package/src/resolvers/RingCentralTelephonyResolver.ts +64 -0
  272. package/src/resolvers/RunAIAgentResolver.ts +60 -15
  273. package/src/resolvers/SearchKnowledgeResolver.ts +2 -0
  274. package/src/resolvers/TeamsMeetingsResolver.ts +68 -0
  275. package/src/resolvers/TelephonyResolver.ts +63 -0
  276. package/src/resolvers/TestQuerySQLResolver.ts +2 -3
  277. package/src/resolvers/VonageTelephonyResolver.ts +63 -0
  278. package/src/resolvers/meetingRecordingRegistration.ts +432 -0
  279. package/src/resolvers/peaksSidecar.ts +52 -0
  280. package/src/rest/MediaAccessKeys.ts +121 -0
  281. package/src/rest/MediaStreamHandler.ts +223 -0
  282. package/src/rest/mediaRange.ts +76 -0
  283. package/src/telephony/RingCentralTelephonyService.ts +342 -0
  284. package/src/telephony/TeamsMeetingsRouter.ts +124 -0
  285. package/src/telephony/TeamsMeetingsService.ts +268 -0
  286. package/src/telephony/TwilioTelephonyRouter.ts +184 -0
  287. package/src/telephony/TwilioTelephonyService.ts +261 -0
  288. package/src/telephony/VonageTelephonyRouter.ts +239 -0
  289. package/src/telephony/VonageTelephonyService.ts +259 -0
  290. package/src/telephony/calendar-scheduler.ts +176 -0
  291. package/src/telephony/index.ts +43 -0
  292. package/src/telephony/media-upgrade-router.ts +62 -0
  293. package/src/telephony/ringcentral-runtime.ts +22 -0
  294. package/src/telephony/teams-meetings-runtime.ts +24 -0
  295. package/src/telephony/teamsAcsMediaRegistry.ts +152 -0
  296. package/src/telephony/telephony-runtime.ts +22 -0
  297. package/src/telephony/twilioMediaRegistry.ts +137 -0
  298. package/src/telephony/vonage-runtime.ts +22 -0
  299. package/src/telephony/vonageMediaRegistry.ts +200 -0
  300. package/dist/agents/skip-agent.d.ts +0 -111
  301. package/dist/agents/skip-agent.d.ts.map +0 -1
  302. package/dist/agents/skip-agent.js +0 -1487
  303. package/dist/agents/skip-agent.js.map +0 -1
  304. package/dist/agents/skip-sdk.d.ts +0 -261
  305. package/dist/agents/skip-sdk.d.ts.map +0 -1
  306. package/dist/agents/skip-sdk.js +0 -909
  307. package/dist/agents/skip-sdk.js.map +0 -1
  308. package/src/agents/skip-agent.ts +0 -1594
  309. package/src/agents/skip-sdk.ts +0 -1210
@@ -170,6 +170,49 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
170
170
  return closed;
171
171
  }
172
172
 
173
+ /**
174
+ * Server-authoritative MAX-DURATION enforcement (public web-widget voice cap, public-web-widget.md
175
+ * W4). Closes any `Active`/`Idle` session whose stored absolute deadline (`Config_.maxSessionDeadlineIso`)
176
+ * has passed — a hard wall-clock ceiling that fires regardless of activity, so a public voice guest
177
+ * cannot run a session indefinitely (cost-bombing) even if the client ignores its own abuse guard.
178
+ *
179
+ * Only sessions that CARRY a deadline are loaded (a cheap `Config_ LIKE` pre-filter), then the exact
180
+ * ISO deadline is parsed and compared in JS (`Config_` is JSON, not a queryable column). Stamped
181
+ * `CloseReason = 'Janitor'`, idempotent + concurrency-safe like the other sweeps. Returns the count closed.
182
+ */
183
+ public async RunMaxDurationSweep(provider: IMetadataProvider, systemUser: UserInfo): Promise<number> {
184
+ const nowMs = Date.now();
185
+ // Only consider sessions that carry a deadline marker — keeps this sweep cheap (it never touches
186
+ // the far-more-common uncapped sessions) while the JS check below enforces the exact deadline.
187
+ const filter = `Status IN ('Active','Idle') AND Config LIKE '%maxSessionDeadlineIso%'`;
188
+ const isExpired = (session: MJAIAgentSessionEntity): boolean => {
189
+ const deadlineMs = this.parseSessionDeadlineMs(session.Config_);
190
+ return deadlineMs != null && deadlineMs <= nowMs;
191
+ };
192
+ const closed = await this.sweepAndClose(filter, provider, systemUser, 'Janitor', isExpired);
193
+ if (closed > 0) {
194
+ LogStatus(`[SessionJanitor] Max-duration sweep closed ${closed} session(s) past their hard duration cap`);
195
+ }
196
+ return closed;
197
+ }
198
+
199
+ /** Parses the absolute deadline (ms) from a session's `Config_` JSON, or null when absent/malformed. */
200
+ private parseSessionDeadlineMs(configJson: string | null | undefined): number | null {
201
+ if (!configJson) {
202
+ return null;
203
+ }
204
+ try {
205
+ const parsed = JSON.parse(configJson) as { maxSessionDeadlineIso?: string };
206
+ if (typeof parsed.maxSessionDeadlineIso !== 'string') {
207
+ return null;
208
+ }
209
+ const ms = Date.parse(parsed.maxSessionDeadlineIso);
210
+ return Number.isNaN(ms) ? null : ms;
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+
173
216
  // ----- internals -------------------------------------------------------------------------
174
217
 
175
218
  /** Register for graceful shutdown exactly once. */
@@ -197,6 +240,9 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
197
240
  this._sweepRunning = true;
198
241
  try {
199
242
  await this.RunStalenessSweep(this._provider, this._systemUser);
243
+ // Hard wall-clock cap (public web-widget voice). Runs alongside the idle sweep so a capped
244
+ // session is finalized within one sweep interval of its deadline even if it's still "active".
245
+ await this.RunMaxDurationSweep(this._provider, this._systemUser);
200
246
  } catch (err) {
201
247
  LogError(`SessionJanitor periodic sweep failed: ${err instanceof Error ? err.message : String(err)}`);
202
248
  } finally {
@@ -215,19 +261,33 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
215
261
  provider: IMetadataProvider,
216
262
  systemUser: UserInfo,
217
263
  closeReason: SessionCloseReason,
264
+ shouldClose?: (session: MJAIAgentSessionEntity) => boolean,
218
265
  ): Promise<number> {
219
266
  let closedCount = 0;
220
267
  let afterKey: CompositeKey | undefined;
221
268
 
222
269
  // eslint-disable-next-line no-constant-condition
223
270
  while (true) {
224
- const page = await this.fetchPage(filter, afterKey, provider, systemUser);
225
- if (page == null) {
271
+ const fetched = await this.fetchPage(filter, afterKey, provider, systemUser);
272
+ if (fetched == null) {
226
273
  break; // load failure already logged
227
274
  }
228
- if (page.length === 0) {
275
+ if (fetched.length === 0) {
229
276
  break;
230
277
  }
278
+ // Advance the keyset BEFORE the optional JS predicate narrows the page (we must page by the
279
+ // SQL-matched set, not the post-filtered subset, or pagination would stall/skip).
280
+ const lastFetchedId = fetched[fetched.length - 1].ID;
281
+ const fetchedCount = fetched.length;
282
+ // Optional in-JS narrowing (e.g. exact deadline check the SQL pre-filter can't express).
283
+ const page = shouldClose ? fetched.filter(shouldClose) : fetched;
284
+ if (page.length === 0) {
285
+ if (fetchedCount < SWEEP_PAGE_SIZE) {
286
+ break;
287
+ }
288
+ afterKey = CompositeKey.FromID(lastFetchedId);
289
+ continue;
290
+ }
231
291
  // Batch-load every channel for this whole page of sessions in ONE query, then hand each
232
292
  // session its own slice to CloseSession — avoids the N+1 channel read (one RunView per
233
293
  // closing session) that tripped the sequential / multiple-same-entity telemetry.
@@ -248,10 +308,10 @@ export class SessionJanitor extends BaseSingleton<SessionJanitor> implements ISh
248
308
  closedCount++;
249
309
  }
250
310
  }
251
- if (page.length < SWEEP_PAGE_SIZE) {
252
- break; // partial page => end of data
311
+ if (fetchedCount < SWEEP_PAGE_SIZE) {
312
+ break; // partial page (by the SQL-matched set) => end of data
253
313
  }
254
- afterKey = CompositeKey.FromID(page[page.length - 1].ID);
314
+ afterKey = CompositeKey.FromID(lastFetchedId);
255
315
  }
256
316
  return closedCount;
257
317
  }
@@ -7,6 +7,7 @@ import {
7
7
  import { AIAgentPermissionHelper } from '@memberjunction/ai-engine-base';
8
8
  import { RealtimeClientSessionService, RealtimeChannelServerHost } from '@memberjunction/ai-agents';
9
9
  import { GetHostInstanceID } from './HostInstance.js';
10
+ import { writeReturningVisitorRecap } from './ReturningVisitorRecap.js';
10
11
 
11
12
  /** Entity names — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
12
13
  const SESSION_ENTITY = 'MJ: AI Agent Sessions';
@@ -160,6 +161,10 @@ export class SessionManager {
160
161
  await this.disconnectChannels(agentSessionID, contextUser, provider, preloadedChannels);
161
162
  await this.finalizeObservabilityRuns(session, contextUser, provider);
162
163
  await this.notifyChannelPluginsSessionClosed(agentSessionID, closeReason);
164
+ // Returning-visitor recap (RV2): if this session's conversation is returning-visitor-enabled,
165
+ // summarize it into an Active memory note so the visitor's next session opens with prior context.
166
+ // Best-effort + no-op for non-returning-visitor conversations; never blocks teardown.
167
+ await writeReturningVisitorRecap(session.ConversationID, session.AgentID, contextUser, provider);
163
168
  return true;
164
169
  }
165
170
 
@@ -181,6 +186,9 @@ export class SessionManager {
181
186
  AgentID: session.AgentID,
182
187
  UserID: session.UserID,
183
188
  ConversationID: session.ConversationID ?? null,
189
+ // Hand the verbatim per-session config blob to data-aware channels (e.g. the Media
190
+ // channel reads a per-session mediaCollectionID override from it). Opaque here.
191
+ AgentSessionConfig: session.Config_ ?? null,
184
192
  },
185
193
  contextUser,
186
194
  provider,
@@ -313,6 +321,31 @@ export class SessionManager {
313
321
  conversation.NewRecord();
314
322
  conversation.UserID = input.userID;
315
323
  conversation.Name = 'Agent Session';
324
+ // For a magic-link-anonymous principal (e.g. a public web-widget voice guest), stamp the
325
+ // session's conversation with the signed per-session scope so the Widget Guest RLS filters
326
+ // ({{ScopeResourceID}}) isolate this session — and everything chained to it (AI Agent
327
+ // Sessions via ConversationID, Session Channels via the session) — from other guests
328
+ // sharing the Anonymous UserID. No-op for named principals (no scope → default ExternalID).
329
+ if (contextUser.MagicLinkScope?.ResourceID) {
330
+ conversation.ExternalID = contextUser.MagicLinkScope.ResourceID;
331
+ }
332
+
333
+ // Returning-visitor memory (RV1/RV2/RV4) for the VOICE path: the widget guest token carries the
334
+ // resolved VisitorKey / prior-conversation / linked identity (surfaced on ReturningVisitorContext),
335
+ // so a server-created voice conversation gets the same anchor the text path stamps client-side.
336
+ // The resolved counterparty reuses the existing polymorphic LinkedEntityID/LinkedRecordID pair.
337
+ // Absent for non-widget sessions and widgets with returning-visitor memory off — a no-op default.
338
+ const visitor = contextUser.ReturningVisitorContext;
339
+ if (visitor?.VisitorKey) {
340
+ conversation.VisitorKey = visitor.VisitorKey;
341
+ if (visitor.LastConversationID) {
342
+ conversation.LastConversationID = visitor.LastConversationID;
343
+ }
344
+ if (visitor.LinkedEntityID && visitor.LinkedRecordID) {
345
+ conversation.LinkedEntityID = visitor.LinkedEntityID;
346
+ conversation.LinkedRecordID = visitor.LinkedRecordID;
347
+ }
348
+ }
316
349
 
317
350
  const saved = await conversation.Save();
318
351
  if (!saved) {
@@ -340,6 +373,14 @@ export class SessionManager {
340
373
  session.HostInstanceID = GetHostInstanceID();
341
374
  session.Config_ = input.config ?? null;
342
375
  session.LastActiveAt = new Date();
376
+ // Mirror the conversation's resolved counterparty onto the session's own polymorphic pair, so a
377
+ // session carries its linked identity directly (parallels Conversation.LinkedEntityID/RecordID).
378
+ // Sourced from the same widget visitor context; a no-op for non-widget / anonymous sessions.
379
+ const visitor = contextUser.ReturningVisitorContext;
380
+ if (visitor?.LinkedEntityID && visitor.LinkedRecordID) {
381
+ session.LinkedEntityID = visitor.LinkedEntityID;
382
+ session.LinkedRecordID = visitor.LinkedRecordID;
383
+ }
343
384
 
344
385
  const saved = await session.Save();
345
386
  if (!saved) {
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * @module @memberjunction/server
9
9
  */
10
- import { LoadWhiteboardChannelServer, LoadMeetingControlsChannelServer } from '@memberjunction/ai-agents';
10
+ import { LoadWhiteboardChannelServer, LoadMeetingControlsChannelServer, LoadMediaChannelServer, LoadClientContextChannelServer } from '@memberjunction/ai-agents';
11
11
  import { LoadRemoteBrowserChannel } from '@memberjunction/remote-browser-server';
12
12
  import { LoadSelfHostRemoteBrowser } from '@memberjunction/remote-browser-selfhost';
13
13
  import { BindRemoteBrowserGoalEngine } from './remoteBrowserGoalEngine.js';
@@ -17,6 +17,8 @@ import { BindRemoteBrowserGoalEngine } from './remoteBrowserGoalEngine.js';
17
17
  // the session lifecycle — `SessionManager.CreateSession` resolves them via the ClassFactory.
18
18
  LoadWhiteboardChannelServer();
19
19
  LoadMeetingControlsChannelServer();
20
+ LoadMediaChannelServer();
21
+ LoadClientContextChannelServer();
20
22
  // Remote Browser native channel (client-direct): the lifecycle-only server channel plugin + the
21
23
  // Self-Hosted Chrome backend driver (whose default runner launches a local headless Chromium via
22
24
  // Playwright — pulled in transitively through the SelfHost package, documented and acceptable).
@@ -177,6 +177,23 @@ export function createMagicLinkHandler(publicUrl: string, config: MagicLinkConfi
177
177
  return { publicRouter, authenticatedRouter };
178
178
  }
179
179
 
180
+ /**
181
+ * A minimal PUBLIC router that serves ONLY the JWKS endpoint (the magic-link
182
+ * signing key's public half). Mount this when the full magic-link flow is
183
+ * disabled but another feature reuses the same RS256 key + `magic-link` auth
184
+ * provider — today, the public web widget. Without the published public key the
185
+ * unified auth middleware cannot validate the reused-key guest tokens and every
186
+ * request 401s. Assumes the key is already initialized (the widget handler does
187
+ * this via `MagicLinkKeyManager.Instance.Initialize` at startup).
188
+ */
189
+ export function createMagicLinkJwksRouter(): Router {
190
+ const router = Router();
191
+ router.get('/jwks.json', (_req: Request, res: Response) => {
192
+ res.status(200).json(MagicLinkKeyManager.Instance.GetJWKS());
193
+ });
194
+ return router;
195
+ }
196
+
180
197
  /**
181
198
  * Registers the `magic-link` auth provider so MJServer's issuer-driven JWT
182
199
  * validation accepts MJ-issued session tokens. Issuer = public URL, JWKS URL =
@@ -11,6 +11,7 @@ export { MagicLinkKeyManager } from './MagicLinkKeys.js';
11
11
  export { MagicLinkService } from './MagicLinkService.js';
12
12
  export {
13
13
  createMagicLinkHandler,
14
+ createMagicLinkJwksRouter,
14
15
  registerMagicLinkAuthProvider,
15
16
  MAGIC_LINK_MOUNT_PATH,
16
17
  MAGIC_LINK_JWKS_PATH,
@@ -58,6 +58,32 @@ export interface MagicLinkJWTClaims {
58
58
  mj_anon?: boolean;
59
59
  /** Opaque per-session id — correlates one anonymous session's activity across audit rows without a real user. */
60
60
  mj_sid?: string;
61
+ /**
62
+ * Public web widget instance id (additive — set only for widget guest sessions
63
+ * minted by WidgetSessionService). Binds the synthesized guest principal to one
64
+ * widget instance so its pinned agent / guest role can be locked down. Absent on
65
+ * ordinary magic-link sessions.
66
+ */
67
+ mj_widget_id?: string;
68
+ /**
69
+ * Host-asserted visitor email for a widget `host-identity` session (additive). The
70
+ * authoritative `email`/`sub` still resolve the constrained shared Anonymous principal;
71
+ * this carries WHO the host says the visitor is, for the agent to look up their account —
72
+ * without granting that account's permissions. Absent on anonymous/ordinary sessions.
73
+ */
74
+ mj_host_email?: string;
75
+ /**
76
+ * Returning-visitor anchor for a widget guest session (additive, RV1). Carried so the VOICE path —
77
+ * whose conversation is created server-side — can stamp the same Conversation.VisitorKey the text
78
+ * path stamps client-side. Present only when the widget remembers returning visitors.
79
+ */
80
+ mj_visitor_key?: string;
81
+ /** The prior conversation this visit chains from (RV2), for the voice path to stamp Conversation.LastConversationID. */
82
+ mj_last_conversation_id?: string;
83
+ /** Resolved polymorphic identity entity id (RV4), for the voice path to stamp the existing Conversation.LinkedEntityID / AIAgentSession.LinkedEntityID. */
84
+ mj_linked_entity_id?: string;
85
+ /** Resolved polymorphic identity record id (RV4), for the voice path to stamp the existing Conversation.LinkedRecordID / AIAgentSession.LinkedRecordID. */
86
+ mj_linked_record_id?: string;
61
87
  /** Marks the session as magic-link so the Explorer can confine the UI. */
62
88
  mj_magic_link: true;
63
89
  }
package/src/config.ts CHANGED
@@ -68,23 +68,6 @@ const zodBooleanWithTransforms = () => {
68
68
  })
69
69
  }
70
70
 
71
- const askSkipInfoSchema = z.object({
72
- url: z.string().optional(), // Base URL for Skip API
73
- apiKey: z.string().optional(),
74
- orgID: z.string().optional(),
75
- organizationInfo: z.string().optional(),
76
- entitiesToSend: z
77
- .object({
78
- excludeSchemas: z.array(z.string()).optional(),
79
- includeEntitiesFromExcludedSchemas: z.array(z.string()).optional(),
80
- })
81
- .optional(),
82
- chatURL: z.string().optional(),
83
- learningCycleRunUponStartup: zodBooleanWithTransforms(),
84
- learningCycleEnabled: zodBooleanWithTransforms(),
85
- learningCycleURL: z.string().optional(),
86
- learningCycleIntervalInMinutes: z.coerce.number().optional(),
87
- });
88
71
 
89
72
  const sqlLoggingOptionsSchema = z.object({
90
73
  formatAsMigration: z.boolean().optional().default(false),
@@ -330,13 +313,178 @@ const magicLinkSchema = z.object({
330
313
  explorerUrl: z.string().optional(),
331
314
  }).passthrough();
332
315
 
316
+ /**
317
+ * Public web widget config (plans/realtime/bridges-and-widget/public-web-widget.md).
318
+ * The widget REUSES the magic-link RS256 key + `anonymous-embed` synthesis, so it
319
+ * shares the magic-link `audience`/issuer/JWKS by default — the same auth provider
320
+ * validates both. When `enabled`, the server ensures the magic-link key manager is
321
+ * initialized and the provider registered even if `magicLink.enabled` is false.
322
+ */
323
+ const widgetSchema = z.object({
324
+ /** Master switch. When false, the /widget routes are not mounted. */
325
+ enabled: zodBooleanWithTransforms().default(false),
326
+ /**
327
+ * Which signing mechanism the widget reuses. Only `magic-link` is supported today
328
+ * (shares MagicLinkKeyManager + the magic-link auth provider). Recorded so a future
329
+ * dedicated widget key is a config flip, not a code change.
330
+ */
331
+ signingReuse: z.enum(['magic-link']).optional().default('magic-link'),
332
+ /**
333
+ * Audience claim for minted guest JWTs. MUST equal the magic-link audience so the
334
+ * auto-registered `magic-link` auth provider validates widget tokens.
335
+ */
336
+ audience: z.string().optional().default('mj-magic-link'),
337
+ /** Email of the shared Anonymous principal used in guest claims (resolves the synthesized guest user). */
338
+ anonymousEmail: z.string().optional().default('anonymous@magic-link.local'),
339
+ /** Fallback session-token TTL (minutes) when a widget instance does not set its own. */
340
+ defaultSessionTtlMinutes: z.coerce.number().optional().default(15),
341
+ /** Fallback mint rate-limit (per IP per window) when a widget instance does not set its own. */
342
+ defaultRateLimitPerMinute: z.coerce.number().optional().default(30),
343
+ /** Rate-limit window (ms) for the public /widget/session endpoints. Default 60s. */
344
+ rateLimitWindowMs: z.coerce.number().optional().default(60_000),
345
+ /** Server-wide default hard ceiling (minutes) on a voice session when an instance omits one (W4). */
346
+ voiceDefaultMaxSessionMinutes: z.coerce.number().optional().default(10),
347
+ /** Email/name of the internal user whose context READS widget config at mint time (falls back to system/Owner). */
348
+ contextUserForLookup: z.string().optional(),
349
+ /**
350
+ * Host-identity public keys (PEM), keyed by widget PublicKey, for the `host-identity` auth
351
+ * strategy (D1). The host signs a short-lived RS256 assertion; the mint verifies it against
352
+ * the key here. INTERIM location — production should store the host key per widget instance
353
+ * (a HostPublicKey column on WidgetInstance, pending a migration). When a widget's strategy is
354
+ * HostIdentity and no key is configured here, host mints fail closed.
355
+ */
356
+ hostPublicKeys: z.record(z.string(), z.string()).optional().default({}),
357
+ /**
358
+ * Returning-visitor identity-resolution target (RV4). When a visitor verifies (magic-link upgrade)
359
+ * or a host asserts their identity, their email is resolved to a record `(entityName, recordId)`
360
+ * and prior anonymous memory is merged onto that polymorphic pair. Deployment-configurable so the
361
+ * resolved record need NOT be an MJ User — point it at any entity that carries the visitor's email
362
+ * (e.g. a CRM `Persons` table). Defaults to the core `Users` entity keyed by `Email`.
363
+ */
364
+ identityResolution: z.object({
365
+ entityName: z.string().optional().default('Users'),
366
+ emailField: z.string().optional().default('Email'),
367
+ }).optional(),
368
+ }).passthrough();
369
+
370
+ /**
371
+ * Telephony bridge config (plans/realtime/bridges-and-widget/telephony-vendor-bindings.md).
372
+ * Holds the per-vendor credentials + public stream URL the MJAPI telephony ingress wires onto
373
+ * the already-stubbed native SDK seams. Credentials resolve here from `mj.config.cjs` / env —
374
+ * never inlined in code. When `twilio` is omitted, the Twilio telephony routes are not mounted.
375
+ */
376
+ const twilioTelephonySchema = z.object({
377
+ /** Twilio Account SID (`AC…`). */
378
+ accountSid: z.string(),
379
+ /** Account auth token — REST auth (when no API key pair) AND the HMAC key for X-Twilio-Signature verification. */
380
+ authToken: z.string().optional(),
381
+ /** API Key SID (`SK…`) — preferred over the auth token for REST auth when paired with apiKeySecret. */
382
+ apiKeySid: z.string().optional(),
383
+ /** API Key secret — paired with apiKeySid. */
384
+ apiKeySecret: z.string().optional(),
385
+ /** The publicly reachable `wss://…/telephony/twilio/media` URL Twilio's <Connect><Stream> connects to. */
386
+ streamPublicUrl: z.string(),
387
+ /** Optional shared secret gating the public webhook/WSS endpoints (defense-in-depth beyond signature verification). */
388
+ webhookSigningSecret: z.string().optional(),
389
+ /** Optional status-callback URL Twilio posts call lifecycle events to. */
390
+ statusCallbackUrl: z.string().optional(),
391
+ }).passthrough();
392
+
393
+ /**
394
+ * Vonage Voice + WebSocket-media telephony binding. Voice-API auth is application-scoped
395
+ * (application id + RSA private key) with an optional account API key pair for key-scoped ops;
396
+ * signatureSecret is the HMAC key for signed-request `sig` AND HS256 webhook-JWT verification.
397
+ * When `vonage` is omitted, the Vonage telephony routes are not mounted.
398
+ */
399
+ const vonageTelephonySchema = z.object({
400
+ /** Vonage Application ID (UUID) — the JWT-auth identity for the Voice API. */
401
+ applicationId: z.string().optional(),
402
+ /** The application's RSA private key (PEM) used to sign Voice-API JWTs. */
403
+ privateKey: z.string().optional(),
404
+ /** Account API key — used for key-scoped operations when no application credential pair is supplied. */
405
+ apiKey: z.string().optional(),
406
+ /** Account API secret — paired with apiKey. */
407
+ apiSecret: z.string().optional(),
408
+ /** The publicly reachable `wss://…/telephony/vonage/media` URL the call's connect NCCO opens. */
409
+ mediaPublicUrl: z.string(),
410
+ /** Vonage account signature secret — HMAC key for signed-request `sig` AND HS256 webhook-JWT verification. */
411
+ signatureSecret: z.string().optional(),
412
+ /** Optional event-webhook URL Vonage posts call lifecycle events to (passed on outbound createCall). */
413
+ eventUrl: z.string().optional(),
414
+ }).passthrough();
415
+
416
+ /**
417
+ * RingCentral telephony binding: **SIP device credentials** for the headless softphone (the
418
+ * `ringcentral-softphone` SIP/RTP path — the only RingCentral product that carries bidirectional call
419
+ * audio; the WebSocket "Call Streaming" product is receive-only). These come from a RingCentral
420
+ * "Other Phone" (BYO-device) registration's SIP info, resolved here from `mj.config.cjs` / env — never
421
+ * inlined. When `ringcentral` is omitted, the RingCentral softphone is not started.
422
+ *
423
+ * Unlike Twilio/Vonage, there is NO public HTTP webhook or media WSS: the softphone is an outbound SIP
424
+ * registration that receives inbound INVITEs directly over its own SIP/TLS connection. So this block has
425
+ * no signing secret or public URL — just the SIP device creds + codec.
426
+ */
427
+ const ringcentralTelephonySchema = z.object({
428
+ /** SIP domain (e.g. `sip.ringcentral.com`). */
429
+ sipDomain: z.string(),
430
+ /** SIP outbound proxy (`host:port`, e.g. `sip10.ringcentral.com:5096`). */
431
+ sipOutboundProxy: z.string(),
432
+ /** SIP auth username (the device's phone number / extension). */
433
+ sipUsername: z.string(),
434
+ /** SIP auth password (resolved upstream — never inlined). */
435
+ sipPassword: z.string(),
436
+ /** SIP authorization id (the device's RingCentral authorization id). */
437
+ sipAuthorizationId: z.string(),
438
+ /** Codec to negotiate. Defaults to `OPUS/16000` (clean wideband PCM16 — the least-friction realtime path). */
439
+ codec: z.enum(['OPUS/16000', 'OPUS/48000/2', 'PCMU/8000']).optional(),
440
+ /** Skip TLS cert validation (sandbox/test only — never in production). */
441
+ ignoreTlsCertErrors: zodBooleanWithTransforms().optional().default(false),
442
+ }).passthrough();
443
+
444
+ /**
445
+ * Teams meetings bridge config (plans/realtime/bridges-and-widget/meeting-vendor-bindings-teams-slack.md M1).
446
+ * Holds the bot credentials + Graph webhook shared secret + ACS audio rate the MJAPI meetings ingress wires
447
+ * onto the Teams binding seams. Credentials resolve here from `mj.config.cjs` / env — never inlined in code.
448
+ * When `teams` is omitted (or disabled), the Teams meetings routes are not mounted.
449
+ */
450
+ const teamsMeetingsSchema = z.object({
451
+ /** Master switch for the Teams meetings ingress. */
452
+ enabled: zodBooleanWithTransforms().default(false),
453
+ /** The bot's Azure AD application (client) id (resolved upstream; carried for diagnostics). */
454
+ appId: z.string().optional(),
455
+ /** The Azure tenant id the bot operates in. */
456
+ tenantId: z.string().optional(),
457
+ /** A pre-resolved OAuth bearer / application token for the bot's Graph calls (resolved upstream — never inline). */
458
+ botAccessToken: z.string().optional(),
459
+ /** The shared secret set as `clientState` on the Graph subscription; gates the change-notification webhook. */
460
+ notificationClientState: z.string().optional(),
461
+ /** The ACS application-hosted-media PCM sample rate (Hz) the audio plane negotiates. Defaults to 16000. */
462
+ acsSampleRate: z.coerce.number().optional().default(16000),
463
+ /** The realtime model's PCM16 sample rate (Hz) the binding resamples to/from. Defaults to 16000. */
464
+ modelSampleRate: z.coerce.number().optional().default(16000),
465
+ }).passthrough();
466
+
467
+ const telephonySchema = z.object({
468
+ /** Master switch. When false (or when no vendor block is present), telephony routes are not mounted. */
469
+ enabled: zodBooleanWithTransforms().default(false),
470
+ /** Twilio Programmable Voice + Media Streams binding. */
471
+ twilio: twilioTelephonySchema.optional(),
472
+ /** Vonage Voice + WebSocket-media binding. */
473
+ vonage: vonageTelephonySchema.optional(),
474
+ /** RingCentral Call Control + media-stream binding. */
475
+ ringcentral: ringcentralTelephonySchema.optional(),
476
+ /** Microsoft Teams meetings (Graph cloud-communications + ACS application-hosted media) binding. */
477
+ teams: teamsMeetingsSchema.optional(),
478
+ }).passthrough();
479
+
333
480
  const configInfoSchema = z.object({
334
481
  userHandling: userHandlingInfoSchema,
335
482
  magicLink: magicLinkSchema.optional().default({}),
483
+ widget: widgetSchema.optional().default({}),
484
+ telephony: telephonySchema.optional().default({}),
336
485
  databaseSettings: databaseSettingsInfoSchema,
337
486
  viewingSystem: viewingSystemInfoSchema.optional(),
338
487
  restApiOptions: restApiOptionsSchema.optional().default({}),
339
- askSkip: askSkipInfoSchema.optional(),
340
488
  sqlLogging: sqlLoggingSchema.optional(),
341
489
  authProviders: z.array(authProviderSchema).optional(),
342
490
  componentRegistries: z.array(componentRegistrySchema).optional(),
@@ -384,10 +532,15 @@ const configInfoSchema = z.object({
384
532
 
385
533
  export type UserHandlingInfo = z.infer<typeof userHandlingInfoSchema>;
386
534
  export type MagicLinkConfig = z.infer<typeof magicLinkSchema>;
535
+ export type WidgetConfig = z.infer<typeof widgetSchema>;
536
+ export type TelephonyConfig = z.infer<typeof telephonySchema>;
537
+ export type TwilioTelephonyConfig = z.infer<typeof twilioTelephonySchema>;
538
+ export type VonageTelephonyConfig = z.infer<typeof vonageTelephonySchema>;
539
+ export type RingCentralTelephonyConfig = z.infer<typeof ringcentralTelephonySchema>;
540
+ export type TeamsMeetingsConfig = z.infer<typeof teamsMeetingsSchema>;
387
541
  export type DatabaseSettingsInfo = z.infer<typeof databaseSettingsInfoSchema>;
388
542
  export type ViewingSystemSettingsInfo = z.infer<typeof viewingSystemInfoSchema>;
389
543
  export type RESTApiOptions = z.infer<typeof restApiOptionsSchema>;
390
- export type AskSkipInfo = z.infer<typeof askSkipInfoSchema>;
391
544
  export type SqlLoggingOptions = z.infer<typeof sqlLoggingOptionsSchema>;
392
545
  export type SqlLoggingInfo = z.infer<typeof sqlLoggingSchema>;
393
546
  export type AuthProviderConfig = z.infer<typeof authProviderSchema>;
@@ -477,25 +630,6 @@ export const DEFAULT_SERVER_CONFIG: Partial<ConfigInfo> = {
477
630
  enabled: false,
478
631
  },
479
632
 
480
- // Ask Skip configuration (environment-driven)
481
- askSkip: {
482
- url: process.env.ASK_SKIP_URL,
483
- chatURL: process.env.ASK_SKIP_CHAT_URL,
484
- learningCycleURL: process.env.ASK_SKIP_LEARNING_URL,
485
- learningCycleIntervalInMinutes: process.env.ASK_SKIP_LEARNING_CYCLE_INTERVAL_IN_MINUTES
486
- ? parseInt(process.env.ASK_SKIP_LEARNING_CYCLE_INTERVAL_IN_MINUTES, 10)
487
- : undefined,
488
- learningCycleEnabled: process.env.ASK_SKIP_RUN_LEARNING_CYCLES === 'true',
489
- learningCycleRunUponStartup: process.env.ASK_SKIP_RUN_LEARNING_CYCLES_UPON_STARTUP === 'true',
490
- orgID: process.env.ASK_SKIP_ORGANIZATION_ID,
491
- apiKey: process.env.ASK_SKIP_API_KEY,
492
- organizationInfo: process.env.ASK_SKIP_ORGANIZATION_INFO,
493
- entitiesToSend: {
494
- excludeSchemas: [],
495
- includeEntitiesFromExcludedSchemas: [],
496
- },
497
- },
498
-
499
633
  // SQL logging defaults
500
634
  sqlLogging: {
501
635
  enabled: true,
package/src/context.ts CHANGED
@@ -15,7 +15,7 @@ import { GetReadOnlyDataSource, GetReadWriteDataSource } from './util.js';
15
15
  import { v4 as uuidv4 } from 'uuid';
16
16
  import e from 'express';
17
17
  import type { RequestHandler, Request, Response, NextFunction } from 'express';
18
- import { DatabaseProviderBase, UserInfo, type MagicLinkScope } from '@memberjunction/core';
18
+ import { DatabaseProviderBase, UserInfo, type MagicLinkScope, type ReturningVisitorContext, type WidgetGuestContext } from '@memberjunction/core';
19
19
  import { SQLServerDataProvider, SQLServerProviderConfigData, UserCache } from '@memberjunction/sqlserver-dataprovider';
20
20
  import { Metadata } from '@memberjunction/core';
21
21
  import { UUIDsEqual } from '@memberjunction/global';
@@ -242,8 +242,17 @@ function buildMagicLinkSessionUser(userRecord: UserInfo, payload: jwt.JwtPayload
242
242
  }
243
243
  }
244
244
 
245
- // Named session with no resource scope no per-request state needed, use the cached user.
246
- if (!isAnon && !scope) {
245
+ // Returning-visitor context (RV1/RV2/RV4): carried on a widget guest token so the voice path
246
+ // (server-created conversation) stamps the same returning-visitor anchor + resolved identity.
247
+ const visitorContext = extractReturningVisitorContext(payload);
248
+
249
+ // Widget-instance identity (mj_widget_id): present on every public web-widget guest token. The
250
+ // privileged agent-dispatch path reads it to resolve the AUTHORITATIVE pinned agent for the guest
251
+ // (so a guest can never run an arbitrary agent under the elevated server principal).
252
+ const widgetGuestContext = extractWidgetGuestContext(payload);
253
+
254
+ // Named session with no resource scope and no widget/visitor context → no per-request state needed.
255
+ if (!isAnon && !scope && !visitorContext && !widgetGuestContext) {
247
256
  return userRecord;
248
257
  }
249
258
 
@@ -255,6 +264,12 @@ function buildMagicLinkSessionUser(userRecord: UserInfo, payload: jwt.JwtPayload
255
264
  if (scope) {
256
265
  sessionUser.MagicLinkScope = scope;
257
266
  }
267
+ if (visitorContext) {
268
+ sessionUser.ReturningVisitorContext = visitorContext;
269
+ }
270
+ if (widgetGuestContext) {
271
+ sessionUser.WidgetGuestContext = widgetGuestContext;
272
+ }
258
273
  if (isAnon) {
259
274
  // Mark the session so the CurrentUser field resolver serves these synthesized roles
260
275
  // (the shared Anonymous principal holds none in the DB). See UserInfo.IsMagicLinkAnonymous.
@@ -263,6 +278,38 @@ function buildMagicLinkSessionUser(userRecord: UserInfo, payload: jwt.JwtPayload
263
278
  return sessionUser;
264
279
  }
265
280
 
281
+ /**
282
+ * Extracts the returning-visitor context (RV1/RV2/RV4) from a widget guest token's claims, or
283
+ * undefined when the token carries no VisitorKey (the default, remembering-off case). Used so the
284
+ * voice path can stamp the conversation with the same anchor/identity the text path stamps client-side.
285
+ */
286
+ function extractReturningVisitorContext(payload: jwt.JwtPayload): ReturningVisitorContext | undefined {
287
+ const visitorKey = payload['mj_visitor_key'];
288
+ if (typeof visitorKey !== 'string' || !visitorKey) {
289
+ return undefined;
290
+ }
291
+ const str = (v: unknown): string | undefined => (typeof v === 'string' && v ? v : undefined);
292
+ return {
293
+ VisitorKey: visitorKey,
294
+ LastConversationID: str(payload['mj_last_conversation_id']),
295
+ LinkedEntityID: str(payload['mj_linked_entity_id']),
296
+ LinkedRecordID: str(payload['mj_linked_record_id']),
297
+ };
298
+ }
299
+
300
+ /**
301
+ * Extracts the widget-instance identity (`mj_widget_id`) from a public web-widget guest token, or
302
+ * undefined when the token carries none (every non-widget session). Carried so the privileged
303
+ * agent-dispatch path can resolve the authoritative pinned agent for the guest from the trusted token.
304
+ */
305
+ function extractWidgetGuestContext(payload: jwt.JwtPayload): WidgetGuestContext | undefined {
306
+ const widgetId = payload['mj_widget_id'];
307
+ if (typeof widgetId !== 'string' || !widgetId) {
308
+ return undefined;
309
+ }
310
+ return { WidgetID: widgetId };
311
+ }
312
+
266
313
  const verifyAsync = async (issuer: string, token: string): Promise<jwt.JwtPayload> =>
267
314
  new Promise((resolve, reject) => {
268
315
  const options = getValidationOptions(issuer);