@memberjunction/server 5.44.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 (254) 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 +37 -0
  11. package/dist/agentSessions/SessionManager.js.map +1 -1
  12. package/dist/auth/magicLink/MagicLinkRouter.d.ts +10 -0
  13. package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
  14. package/dist/auth/magicLink/MagicLinkRouter.js +16 -0
  15. package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
  16. package/dist/auth/magicLink/index.d.ts +1 -1
  17. package/dist/auth/magicLink/index.d.ts.map +1 -1
  18. package/dist/auth/magicLink/index.js +1 -1
  19. package/dist/auth/magicLink/index.js.map +1 -1
  20. package/dist/auth/magicLink/types.d.ts +26 -0
  21. package/dist/auth/magicLink/types.d.ts.map +1 -1
  22. package/dist/config.d.ts +1883 -144
  23. package/dist/config.d.ts.map +1 -1
  24. package/dist/config.js +160 -36
  25. package/dist/config.js.map +1 -1
  26. package/dist/context.d.ts.map +1 -1
  27. package/dist/context.js +45 -2
  28. package/dist/context.js.map +1 -1
  29. package/dist/generated/generated.d.ts +522 -0
  30. package/dist/generated/generated.d.ts.map +1 -1
  31. package/dist/generated/generated.js +2860 -0
  32. package/dist/generated/generated.js.map +1 -1
  33. package/dist/generic/ResolverBase.d.ts +13 -0
  34. package/dist/generic/ResolverBase.d.ts.map +1 -1
  35. package/dist/generic/ResolverBase.js +22 -0
  36. package/dist/generic/ResolverBase.js.map +1 -1
  37. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  38. package/dist/generic/RunViewResolver.js +4 -0
  39. package/dist/generic/RunViewResolver.js.map +1 -1
  40. package/dist/index.d.ts +0 -2
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +101 -3
  43. package/dist/index.js.map +1 -1
  44. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
  45. package/dist/integration/CustomColumnPromoter.js +6 -0
  46. package/dist/integration/CustomColumnPromoter.js.map +1 -1
  47. package/dist/realtimeWidget/WidgetRouter.d.ts +26 -0
  48. package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -0
  49. package/dist/realtimeWidget/WidgetRouter.js +182 -0
  50. package/dist/realtimeWidget/WidgetRouter.js.map +1 -0
  51. package/dist/realtimeWidget/WidgetSessionService.d.ts +265 -0
  52. package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -0
  53. package/dist/realtimeWidget/WidgetSessionService.js +477 -0
  54. package/dist/realtimeWidget/WidgetSessionService.js.map +1 -0
  55. package/dist/realtimeWidget/host-identity.d.ts +40 -0
  56. package/dist/realtimeWidget/host-identity.d.ts.map +1 -0
  57. package/dist/realtimeWidget/host-identity.js +58 -0
  58. package/dist/realtimeWidget/host-identity.js.map +1 -0
  59. package/dist/realtimeWidget/index.d.ts +10 -0
  60. package/dist/realtimeWidget/index.d.ts.map +1 -0
  61. package/dist/realtimeWidget/index.js +9 -0
  62. package/dist/realtimeWidget/index.js.map +1 -0
  63. package/dist/realtimeWidget/visitorIdentity.d.ts +76 -0
  64. package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -0
  65. package/dist/realtimeWidget/visitorIdentity.js +202 -0
  66. package/dist/realtimeWidget/visitorIdentity.js.map +1 -0
  67. package/dist/realtimeWidget/widgetCore.d.ts +106 -0
  68. package/dist/realtimeWidget/widgetCore.d.ts.map +1 -0
  69. package/dist/realtimeWidget/widgetCore.js +173 -0
  70. package/dist/realtimeWidget/widgetCore.js.map +1 -0
  71. package/dist/realtimeWidget/widgetGuestElevation.d.ts +51 -0
  72. package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -0
  73. package/dist/realtimeWidget/widgetGuestElevation.js +79 -0
  74. package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -0
  75. package/dist/resolvers/ComponentRegistryResolver.d.ts +7 -0
  76. package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
  77. package/dist/resolvers/ComponentRegistryResolver.js +31 -8
  78. package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
  79. package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -1
  80. package/dist/resolvers/EntityPermissionResolver.js +1 -2
  81. package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
  82. package/dist/resolvers/QuerySystemUserResolver.d.ts +9 -6
  83. package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
  84. package/dist/resolvers/QuerySystemUserResolver.js +15 -13
  85. package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
  86. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
  87. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  88. package/dist/resolvers/RealtimeClientSessionResolver.js +47 -3
  89. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  90. package/dist/resolvers/RingCentralTelephonyResolver.d.ts +27 -0
  91. package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +1 -0
  92. package/dist/resolvers/RingCentralTelephonyResolver.js +87 -0
  93. package/dist/resolvers/RingCentralTelephonyResolver.js.map +1 -0
  94. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  95. package/dist/resolvers/RunAIAgentResolver.js +14 -3
  96. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  97. package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
  98. package/dist/resolvers/SearchKnowledgeResolver.js +2 -0
  99. package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
  100. package/dist/resolvers/TeamsMeetingsResolver.d.ts +28 -0
  101. package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +1 -0
  102. package/dist/resolvers/TeamsMeetingsResolver.js +91 -0
  103. package/dist/resolvers/TeamsMeetingsResolver.js.map +1 -0
  104. package/dist/resolvers/TelephonyResolver.d.ts +26 -0
  105. package/dist/resolvers/TelephonyResolver.d.ts.map +1 -0
  106. package/dist/resolvers/TelephonyResolver.js +86 -0
  107. package/dist/resolvers/TelephonyResolver.js.map +1 -0
  108. package/dist/resolvers/TestQuerySQLResolver.d.ts +1 -1
  109. package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
  110. package/dist/resolvers/TestQuerySQLResolver.js +2 -3
  111. package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
  112. package/dist/resolvers/VonageTelephonyResolver.d.ts +26 -0
  113. package/dist/resolvers/VonageTelephonyResolver.d.ts.map +1 -0
  114. package/dist/resolvers/VonageTelephonyResolver.js +86 -0
  115. package/dist/resolvers/VonageTelephonyResolver.js.map +1 -0
  116. package/dist/rest/MediaStreamHandler.js +4 -1
  117. package/dist/rest/MediaStreamHandler.js.map +1 -1
  118. package/dist/telephony/RingCentralTelephonyService.d.ts +115 -0
  119. package/dist/telephony/RingCentralTelephonyService.d.ts.map +1 -0
  120. package/dist/telephony/RingCentralTelephonyService.js +262 -0
  121. package/dist/telephony/RingCentralTelephonyService.js.map +1 -0
  122. package/dist/telephony/TeamsMeetingsRouter.d.ts +40 -0
  123. package/dist/telephony/TeamsMeetingsRouter.d.ts.map +1 -0
  124. package/dist/telephony/TeamsMeetingsRouter.js +96 -0
  125. package/dist/telephony/TeamsMeetingsRouter.js.map +1 -0
  126. package/dist/telephony/TeamsMeetingsService.d.ts +113 -0
  127. package/dist/telephony/TeamsMeetingsService.d.ts.map +1 -0
  128. package/dist/telephony/TeamsMeetingsService.js +196 -0
  129. package/dist/telephony/TeamsMeetingsService.js.map +1 -0
  130. package/dist/telephony/TwilioTelephonyRouter.d.ts +36 -0
  131. package/dist/telephony/TwilioTelephonyRouter.d.ts.map +1 -0
  132. package/dist/telephony/TwilioTelephonyRouter.js +143 -0
  133. package/dist/telephony/TwilioTelephonyRouter.js.map +1 -0
  134. package/dist/telephony/TwilioTelephonyService.d.ts +95 -0
  135. package/dist/telephony/TwilioTelephonyService.d.ts.map +1 -0
  136. package/dist/telephony/TwilioTelephonyService.js +193 -0
  137. package/dist/telephony/TwilioTelephonyService.js.map +1 -0
  138. package/dist/telephony/VonageTelephonyRouter.d.ts +41 -0
  139. package/dist/telephony/VonageTelephonyRouter.d.ts.map +1 -0
  140. package/dist/telephony/VonageTelephonyRouter.js +201 -0
  141. package/dist/telephony/VonageTelephonyRouter.js.map +1 -0
  142. package/dist/telephony/VonageTelephonyService.d.ts +90 -0
  143. package/dist/telephony/VonageTelephonyService.d.ts.map +1 -0
  144. package/dist/telephony/VonageTelephonyService.js +191 -0
  145. package/dist/telephony/VonageTelephonyService.js.map +1 -0
  146. package/dist/telephony/calendar-scheduler.d.ts +67 -0
  147. package/dist/telephony/calendar-scheduler.d.ts.map +1 -0
  148. package/dist/telephony/calendar-scheduler.js +133 -0
  149. package/dist/telephony/calendar-scheduler.js.map +1 -0
  150. package/dist/telephony/index.d.ts +29 -0
  151. package/dist/telephony/index.d.ts.map +1 -0
  152. package/dist/telephony/index.js +38 -0
  153. package/dist/telephony/index.js.map +1 -0
  154. package/dist/telephony/media-upgrade-router.d.ts +33 -0
  155. package/dist/telephony/media-upgrade-router.d.ts.map +1 -0
  156. package/dist/telephony/media-upgrade-router.js +56 -0
  157. package/dist/telephony/media-upgrade-router.js.map +1 -0
  158. package/dist/telephony/ringcentral-runtime.d.ts +14 -0
  159. package/dist/telephony/ringcentral-runtime.d.ts.map +1 -0
  160. package/dist/telephony/ringcentral-runtime.js +18 -0
  161. package/dist/telephony/ringcentral-runtime.js.map +1 -0
  162. package/dist/telephony/teams-meetings-runtime.d.ts +16 -0
  163. package/dist/telephony/teams-meetings-runtime.d.ts.map +1 -0
  164. package/dist/telephony/teams-meetings-runtime.js +20 -0
  165. package/dist/telephony/teams-meetings-runtime.js.map +1 -0
  166. package/dist/telephony/teamsAcsMediaRegistry.d.ts +69 -0
  167. package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +1 -0
  168. package/dist/telephony/teamsAcsMediaRegistry.js +118 -0
  169. package/dist/telephony/teamsAcsMediaRegistry.js.map +1 -0
  170. package/dist/telephony/telephony-runtime.d.ts +14 -0
  171. package/dist/telephony/telephony-runtime.d.ts.map +1 -0
  172. package/dist/telephony/telephony-runtime.js +18 -0
  173. package/dist/telephony/telephony-runtime.js.map +1 -0
  174. package/dist/telephony/twilioMediaRegistry.d.ts +60 -0
  175. package/dist/telephony/twilioMediaRegistry.d.ts.map +1 -0
  176. package/dist/telephony/twilioMediaRegistry.js +106 -0
  177. package/dist/telephony/twilioMediaRegistry.js.map +1 -0
  178. package/dist/telephony/vonage-runtime.d.ts +14 -0
  179. package/dist/telephony/vonage-runtime.d.ts.map +1 -0
  180. package/dist/telephony/vonage-runtime.js +18 -0
  181. package/dist/telephony/vonage-runtime.js.map +1 -0
  182. package/dist/telephony/vonageMediaRegistry.d.ts +78 -0
  183. package/dist/telephony/vonageMediaRegistry.d.ts.map +1 -0
  184. package/dist/telephony/vonageMediaRegistry.js +158 -0
  185. package/dist/telephony/vonageMediaRegistry.js.map +1 -0
  186. package/package.json +88 -83
  187. package/src/__tests__/search-knowledge-tags.test.ts +9 -9
  188. package/src/__tests__/teams-meetings-service.test.ts +65 -0
  189. package/src/__tests__/teamsAcsMediaRegistry.test.ts +82 -0
  190. package/src/__tests__/twilio-telephony-service.test.ts +23 -0
  191. package/src/__tests__/twilioMediaRegistry.test.ts +103 -0
  192. package/src/__tests__/vonageMediaRegistry.test.ts +180 -0
  193. package/src/__tests__/widget.test.ts +204 -0
  194. package/src/__tests__/widgetGuestElevation.test.ts +57 -0
  195. package/src/__tests__/widgetHostIdentity.test.ts +117 -0
  196. package/src/agentSessions/ReturningVisitorRecap.ts +242 -0
  197. package/src/agentSessions/SessionJanitor.ts +66 -6
  198. package/src/agentSessions/SessionManager.ts +38 -0
  199. package/src/auth/magicLink/MagicLinkRouter.ts +17 -0
  200. package/src/auth/magicLink/index.ts +1 -0
  201. package/src/auth/magicLink/types.ts +26 -0
  202. package/src/config.ts +172 -38
  203. package/src/context.ts +50 -3
  204. package/src/generated/generated.ts +1991 -1
  205. package/src/generic/ResolverBase.ts +28 -0
  206. package/src/generic/RunViewResolver.ts +4 -0
  207. package/src/index.ts +109 -3
  208. package/src/integration/CustomColumnPromoter.ts +6 -0
  209. package/src/realtimeWidget/WidgetRouter.ts +204 -0
  210. package/src/realtimeWidget/WidgetSessionService.ts +714 -0
  211. package/src/realtimeWidget/host-identity.ts +84 -0
  212. package/src/realtimeWidget/index.ts +15 -0
  213. package/src/realtimeWidget/visitorIdentity.ts +258 -0
  214. package/src/realtimeWidget/widgetCore.ts +231 -0
  215. package/src/realtimeWidget/widgetGuestElevation.ts +115 -0
  216. package/src/resolvers/ComponentRegistryResolver.ts +36 -10
  217. package/src/resolvers/EntityPermissionResolver.ts +1 -2
  218. package/src/resolvers/QuerySystemUserResolver.ts +14 -10
  219. package/src/resolvers/RealtimeClientSessionResolver.ts +59 -2
  220. package/src/resolvers/RingCentralTelephonyResolver.ts +64 -0
  221. package/src/resolvers/RunAIAgentResolver.ts +16 -4
  222. package/src/resolvers/SearchKnowledgeResolver.ts +2 -0
  223. package/src/resolvers/TeamsMeetingsResolver.ts +68 -0
  224. package/src/resolvers/TelephonyResolver.ts +63 -0
  225. package/src/resolvers/TestQuerySQLResolver.ts +2 -3
  226. package/src/resolvers/VonageTelephonyResolver.ts +63 -0
  227. package/src/rest/MediaStreamHandler.ts +4 -1
  228. package/src/telephony/RingCentralTelephonyService.ts +342 -0
  229. package/src/telephony/TeamsMeetingsRouter.ts +124 -0
  230. package/src/telephony/TeamsMeetingsService.ts +268 -0
  231. package/src/telephony/TwilioTelephonyRouter.ts +184 -0
  232. package/src/telephony/TwilioTelephonyService.ts +261 -0
  233. package/src/telephony/VonageTelephonyRouter.ts +239 -0
  234. package/src/telephony/VonageTelephonyService.ts +259 -0
  235. package/src/telephony/calendar-scheduler.ts +176 -0
  236. package/src/telephony/index.ts +43 -0
  237. package/src/telephony/media-upgrade-router.ts +62 -0
  238. package/src/telephony/ringcentral-runtime.ts +22 -0
  239. package/src/telephony/teams-meetings-runtime.ts +24 -0
  240. package/src/telephony/teamsAcsMediaRegistry.ts +152 -0
  241. package/src/telephony/telephony-runtime.ts +22 -0
  242. package/src/telephony/twilioMediaRegistry.ts +137 -0
  243. package/src/telephony/vonage-runtime.ts +22 -0
  244. package/src/telephony/vonageMediaRegistry.ts +200 -0
  245. package/dist/agents/skip-agent.d.ts +0 -111
  246. package/dist/agents/skip-agent.d.ts.map +0 -1
  247. package/dist/agents/skip-agent.js +0 -1487
  248. package/dist/agents/skip-agent.js.map +0 -1
  249. package/dist/agents/skip-sdk.d.ts +0 -261
  250. package/dist/agents/skip-sdk.d.ts.map +0 -1
  251. package/dist/agents/skip-sdk.js +0 -909
  252. package/dist/agents/skip-sdk.js.map +0 -1
  253. package/src/agents/skip-agent.ts +0 -1594
  254. package/src/agents/skip-sdk.ts +0 -1210
@@ -0,0 +1,261 @@
1
+ /**
2
+ * @fileoverview Orchestrates Twilio telephony bridge sessions (inbound + outbound) for MJAPI.
3
+ *
4
+ * This is the composition layer between the carrier ingress (the public webhook + the Media-Streams
5
+ * WSS) and the ONE unified realtime-agent pathway. It does NOT re-implement any bridge logic: it
6
+ * resolves the Twilio provider + the agent identity, opens a server-side realtime session via the
7
+ * agent layer's `CreateBridgeRealtimeSession` factory, binds `RealTwilioBindings` (real REST client +
8
+ * the per-call media registry) onto the driver, and hands everything to
9
+ * `AIBridgeEngine.StartBridgeSession` — exactly the seam the LiveKit room coordinator uses.
10
+ *
11
+ * Collaborators are injected (engine, session factory, session manager) so the resolution/branching
12
+ * logic is unit-testable with fakes; the actual call placement + media flow are live-only (real Twilio
13
+ * credentials + a publicly reachable stream URL — see telephony-vendor-bindings.md T1).
14
+ *
15
+ * @module @memberjunction/server/telephony
16
+ */
17
+
18
+ import { RunView, UserInfo, IMetadataProvider, LogError } from '@memberjunction/core';
19
+ import type { MJAIBridgeAgentIdentityEntity, MJAIBridgeProviderEntity } from '@memberjunction/core-entities';
20
+ import { AIBridgeEngine } from '@memberjunction/ai-bridge-server';
21
+ import { CreateBridgeRealtimeSession } from '@memberjunction/ai-agents';
22
+ import {
23
+ BaseTelephonyBridge,
24
+ type BridgeNativeSdkBinding,
25
+ DIRECTION_CONFIG_KEY,
26
+ FROM_NUMBER_CONFIG_KEY,
27
+ INBOUND_CALL_ID_CONFIG_KEY,
28
+ } from '@memberjunction/ai-bridge-base';
29
+ import { TwilioCallSdk, RealTwilioBindings, RealTwilioRestClient } from '@memberjunction/ai-bridge-twilio';
30
+ import type { TwilioTelephonyConfig } from '../config.js';
31
+ import { SessionManager } from '../agentSessions/SessionManager.js';
32
+ import { TwilioCallMediaRegistry } from './twilioMediaRegistry.js';
33
+
34
+ const TWILIO_PROVIDER_DRIVER = 'TwilioBridge';
35
+ const AGENT_IDENTITY_ENTITY = 'MJ: AI Bridge Agent Identities';
36
+ const PHONE_IDENTITY_TYPE = 'PhoneNumber';
37
+
38
+ /** A resolved inbound call's identifying fields (parsed from the Twilio webhook). */
39
+ export interface InboundCallInput {
40
+ /** The Twilio Call SID. */
41
+ callSid: string;
42
+ /** The caller's number (Twilio `From`). */
43
+ from: string;
44
+ /** The dialed DID (Twilio `To`) — resolved to an agent identity. */
45
+ to: string;
46
+ }
47
+
48
+ /** Result of starting an inbound bridge session. */
49
+ export interface InboundCallResult {
50
+ /** Whether a pinned agent was resolved + a bridge session started. */
51
+ accepted: boolean;
52
+ /** Why it was rejected (no agent identity for the DID, provider missing, etc.). */
53
+ reason?: string;
54
+ }
55
+
56
+ /** Injectable collaborators (production defaults wired in the constructor; fakes in tests). */
57
+ export interface TwilioTelephonyServiceDeps {
58
+ engine?: Pick<AIBridgeEngine, 'ProviderByName' | 'ProviderByDriverClass' | 'StartBridgeSession' | 'Config'>;
59
+ sessionFactory?: typeof CreateBridgeRealtimeSession;
60
+ sessionManager?: Pick<SessionManager, 'CreateSession'>;
61
+ }
62
+
63
+ /**
64
+ * Starts Twilio bridge sessions. One instance per server, constructed alongside the telephony router and
65
+ * sharing the {@link TwilioCallMediaRegistry} with the Media-Streams WSS server.
66
+ */
67
+ export class TwilioTelephonyService {
68
+ private readonly rest: RealTwilioRestClient;
69
+ private readonly engine: Pick<AIBridgeEngine, 'ProviderByName' | 'ProviderByDriverClass' | 'StartBridgeSession' | 'Config'>;
70
+ private readonly sessionFactory: typeof CreateBridgeRealtimeSession;
71
+ private readonly sessionManager: Pick<SessionManager, 'CreateSession'>;
72
+
73
+ constructor(
74
+ private readonly config: TwilioTelephonyConfig,
75
+ private readonly registry: TwilioCallMediaRegistry,
76
+ deps: TwilioTelephonyServiceDeps = {},
77
+ ) {
78
+ this.rest = new RealTwilioRestClient({
79
+ AccountSid: config.accountSid,
80
+ AuthToken: config.authToken,
81
+ ApiKeySid: config.apiKeySid,
82
+ ApiKeySecret: config.apiKeySecret,
83
+ });
84
+ this.engine = deps.engine ?? AIBridgeEngine.Instance;
85
+ this.sessionFactory = deps.sessionFactory ?? CreateBridgeRealtimeSession;
86
+ this.sessionManager = deps.sessionManager ?? new SessionManager();
87
+ }
88
+
89
+ /**
90
+ * Resolves the dialed DID to a pinned agent and starts an INBOUND bridge session. The bound
91
+ * `RealTwilioBindings` will accept the Media-Streams socket (opened by the webhook's TwiML) for the
92
+ * Call SID. Returns `{ accepted:false }` (never throws) when no agent identity matches the DID, so
93
+ * the webhook can answer with a polite "no agent" message instead of a 500.
94
+ */
95
+ public async HandleInboundCall(input: InboundCallInput, contextUser: UserInfo, provider: IMetadataProvider): Promise<InboundCallResult> {
96
+ try {
97
+ const identity = await this.resolveAgentIdentityByPhone(input.to, contextUser);
98
+ if (!identity) {
99
+ return { accepted: false, reason: `No active agent identity for dialed number '${input.to}'.` };
100
+ }
101
+ this.registry.RegisterCall(input.callSid);
102
+ await this.startBridge({
103
+ agentID: identity.AgentID,
104
+ direction: 'Inbound',
105
+ address: input.from,
106
+ inboundCallId: input.callSid,
107
+ contextUser,
108
+ provider,
109
+ });
110
+ return { accepted: true };
111
+ } catch (e) {
112
+ LogError(`[Telephony][Twilio] inbound call ${input.callSid} failed: ${e instanceof Error ? e.message : String(e)}`);
113
+ return { accepted: false, reason: 'Internal error starting the agent.' };
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Places an OUTBOUND call from a given agent identity to a destination number. The bound
119
+ * `RealTwilioBindings.createCall` issues the Twilio REST `POST /Calls` with the `<Connect><Stream>`
120
+ * TwiML; the returned Call SID is the bridge's external connection id.
121
+ *
122
+ * @returns The placed Call SID.
123
+ */
124
+ public async PlaceOutboundCall(agentIdentityId: string, toNumber: string, contextUser: UserInfo, provider: IMetadataProvider): Promise<string> {
125
+ const identity = await this.loadAgentIdentity(agentIdentityId, contextUser, provider);
126
+ if (!identity) {
127
+ throw new Error(`Agent identity '${agentIdentityId}' not found or inactive.`);
128
+ }
129
+ const session = await this.startBridge({
130
+ agentID: identity.AgentID,
131
+ direction: 'Outbound',
132
+ address: toNumber,
133
+ fromNumber: identity.IdentityValue,
134
+ contextUser,
135
+ provider,
136
+ });
137
+ return session.RoomKey ?? '';
138
+ }
139
+
140
+ // ── internals ────────────────────────────────────────────────────────────────
141
+
142
+ /** Shared inbound/outbound bridge-start: resolve provider, open the realtime session, bind the SDK, start. */
143
+ private async startBridge(args: {
144
+ agentID: string;
145
+ direction: 'Inbound' | 'Outbound';
146
+ address: string;
147
+ inboundCallId?: string;
148
+ fromNumber?: string;
149
+ contextUser: UserInfo;
150
+ provider: IMetadataProvider;
151
+ }): Promise<{ RoomKey?: string }> {
152
+ await this.engine.Config(false, args.contextUser, args.provider);
153
+ const twilioProvider = this.resolveProvider();
154
+
155
+ const agentSession = await this.sessionManager.CreateSession({ agentID: args.agentID, userID: args.contextUser.ID }, args.contextUser, args.provider);
156
+ const realtimeSession = await this.sessionFactory({
157
+ AgentID: args.agentID,
158
+ TargetAgentID: args.agentID,
159
+ ContextUser: args.contextUser,
160
+ MetadataProvider: args.provider,
161
+ AgentSessionID: agentSession.ID,
162
+ RoomName: args.address,
163
+ });
164
+
165
+ const active = await this.engine.StartBridgeSession({
166
+ AgentSessionID: agentSession.ID,
167
+ AgentID: args.agentID,
168
+ TargetAgentID: args.agentID,
169
+ Provider: twilioProvider,
170
+ RealtimeSession: realtimeSession,
171
+ Address: args.address,
172
+ Direction: args.direction,
173
+ Configuration: this.buildSessionConfiguration(args.direction, args.fromNumber, args.inboundCallId),
174
+ BindSdk: this.buildBindSdk(),
175
+ ContextUser: args.contextUser,
176
+ MetadataProvider: args.provider,
177
+ });
178
+ return { RoomKey: active.RoomKey };
179
+ }
180
+
181
+ /** Resolves the seeded Twilio provider row (by driver class, falling back to display name). */
182
+ private resolveProvider(): MJAIBridgeProviderEntity {
183
+ const provider = this.engine.ProviderByDriverClass(TWILIO_PROVIDER_DRIVER) ?? this.engine.ProviderByName('Twilio');
184
+ if (!provider) {
185
+ throw new Error(`No active 'MJ: AI Bridge Providers' row for the Twilio bridge (DriverClass '${TWILIO_PROVIDER_DRIVER}').`);
186
+ }
187
+ return provider;
188
+ }
189
+
190
+ /**
191
+ * Builds the per-session SDK binding that wires the REAL Twilio bindings (REST client + the per-call
192
+ * media registry) onto the telephony driver — overriding the package's default unbound SDK. This is
193
+ * where the offline-complete seams meet the live REST + websocket plumbing.
194
+ */
195
+ public buildBindSdk(): BridgeNativeSdkBinding {
196
+ return (driver) => {
197
+ const telephony = driver as BaseTelephonyBridge;
198
+ telephony.SetSdkFactory(() =>
199
+ new TwilioCallSdk(
200
+ new RealTwilioBindings({
201
+ Rest: this.rest,
202
+ MediaPump: this.registry,
203
+ StreamUrl: this.config.streamPublicUrl,
204
+ StatusCallbackUrl: this.config.statusCallbackUrl,
205
+ }),
206
+ ),
207
+ );
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Assembles the per-session bridge Configuration the telephony driver reads. The engine does NOT
213
+ * forward the top-level `Direction` into the driver's Configuration (it only stamps the DB row), and
214
+ * the bridge defaults to `'Outbound'` when the key is absent — so Direction MUST be set here, or an
215
+ * inbound call wrongly takes the outbound-dial path and fails with "No 'From' number is specified".
216
+ */
217
+ private buildSessionConfiguration(direction: 'Inbound' | 'Outbound', fromNumber?: string, inboundCallId?: string): Record<string, unknown> {
218
+ const config: Record<string, unknown> = { [DIRECTION_CONFIG_KEY]: direction };
219
+ if (fromNumber) {
220
+ config[FROM_NUMBER_CONFIG_KEY] = fromNumber;
221
+ }
222
+ if (inboundCallId) {
223
+ config[INBOUND_CALL_ID_CONFIG_KEY] = inboundCallId;
224
+ }
225
+ return config;
226
+ }
227
+
228
+ /** Finds the active agent identity whose phone number matches the dialed DID. */
229
+ private async resolveAgentIdentityByPhone(dialedNumber: string, contextUser: UserInfo): Promise<MJAIBridgeAgentIdentityEntity | null> {
230
+ const normalized = (dialedNumber ?? '').trim();
231
+ if (!normalized) {
232
+ return null;
233
+ }
234
+ const rv = new RunView();
235
+ const result = await rv.RunView<MJAIBridgeAgentIdentityEntity>(
236
+ {
237
+ EntityName: AGENT_IDENTITY_ENTITY,
238
+ ExtraFilter: `IdentityType='${PHONE_IDENTITY_TYPE}' AND IdentityValue='${normalized.replace(/'/g, "''")}' AND IsActive=1`,
239
+ MaxRows: 1,
240
+ ResultType: 'entity_object',
241
+ },
242
+ contextUser,
243
+ );
244
+ if (!result.Success) {
245
+ LogError(`[Telephony][Twilio] agent-identity lookup failed: ${result.ErrorMessage}`);
246
+ return null;
247
+ }
248
+ return result.Results?.[0] ?? null;
249
+ }
250
+
251
+ /** Loads a specific agent identity by id (used by outbound PlaceCall), requiring it be active. */
252
+ private async loadAgentIdentity(agentIdentityId: string, contextUser: UserInfo, provider: IMetadataProvider): Promise<MJAIBridgeAgentIdentityEntity | null> {
253
+ // Use the request-scoped provider (multi-provider safe), not the global Metadata.
254
+ const entity = await provider.GetEntityObject<MJAIBridgeAgentIdentityEntity>(AGENT_IDENTITY_ENTITY, contextUser);
255
+ const loaded = await entity.Load(agentIdentityId);
256
+ if (!loaded || !entity.IsActive) {
257
+ return null;
258
+ }
259
+ return entity;
260
+ }
261
+ }
@@ -0,0 +1,239 @@
1
+ /**
2
+ * @fileoverview Express + WSS wiring for the Vonage telephony ingress.
3
+ *
4
+ * Three public surfaces (carriers cannot present an MJ JWT — Vonage's signed-request HMAC or webhook JWT
5
+ * is the gate):
6
+ * - `POST /telephony/vonage/answer` — inbound answer webhook. Verifies the signature/JWT, resolves the
7
+ * dialed DID to a pinned agent, starts an inbound bridge session, and returns the `connect`-websocket
8
+ * NCCO that opens the bidirectional media leg.
9
+ * - `POST /telephony/vonage/event` — call-lifecycle event webhook (verified, acknowledged 200).
10
+ * - `WSS /telephony/vonage/media` — the bidirectional `audio/l16;rate=8000` media socket. Frames are
11
+ * routed to the active call's `RealVonageBindings` via the shared {@link VonageCallMediaRegistry}.
12
+ *
13
+ * Vonage media frames do not carry the call UUID, so the answer NCCO embeds it on the media URL as a
14
+ * `?call_uuid=…` query param; the WSS reads it from the upgrade request URL to key the socket.
15
+ *
16
+ * Mirrors the Twilio / magic-link / widget public-router pattern: mount the router BEFORE the unified auth
17
+ * middleware and attach the WSS to the shared HTTP server at startup. The signature/JWT-verification gate +
18
+ * inbound resolution use the offline-complete, unit-tested helpers from the Vonage provider package; this
19
+ * module is thin HTTP/WS plumbing over them and the {@link VonageTelephonyService}.
20
+ *
21
+ * @module @memberjunction/server/telephony
22
+ */
23
+
24
+ import { Router, urlencoded, json, type Request, type Response } from 'express';
25
+ import { WebSocketServer, type WebSocket } from 'ws';
26
+ import type { IncomingMessage } from 'node:http';
27
+ import { RegisterMediaUpgradeRoute } from './media-upgrade-router.js';
28
+ import { LogError, LogStatus, UserInfo, IMetadataProvider, Metadata } from '@memberjunction/core';
29
+ import { UserCache } from '@memberjunction/sqlserver-dataprovider';
30
+ import {
31
+ verifyVonageSignature,
32
+ verifyVonageJwt,
33
+ resolveInboundCall,
34
+ buildInboundAnswerNcco,
35
+ parseVonageControlEvent,
36
+ } from '@memberjunction/ai-bridge-vonage';
37
+ import type { VonageTelephonyConfig } from '../config.js';
38
+ import { VonageCallMediaRegistry } from './vonageMediaRegistry.js';
39
+ import { VonageTelephonyService } from './VonageTelephonyService.js';
40
+
41
+ /** The mount path for the Vonage telephony public router. */
42
+ export const VONAGE_TELEPHONY_MOUNT_PATH = '/telephony/vonage';
43
+
44
+ /** The media websocket path Vonage's `connect`-websocket NCCO connects to. */
45
+ export const VONAGE_MEDIA_WSS_PATH = '/telephony/vonage/media';
46
+
47
+ /** A polite NCCO returned when no agent is available for the dialed number. */
48
+ const NO_AGENT_NCCO = [{ action: 'talk', text: 'Sorry, no agent is available to take this call.' }];
49
+
50
+ /**
51
+ * Builds the Vonage telephony handler: the public answer/event router + a function to attach the media
52
+ * WSS to the shared HTTP server. Returns the registry + service for observability/tests.
53
+ */
54
+ export function createVonageTelephonyHandler(
55
+ publicUrl: string,
56
+ config: VonageTelephonyConfig,
57
+ ): {
58
+ publicRouter: Router;
59
+ attachMediaStreamServer: () => void;
60
+ registry: VonageCallMediaRegistry;
61
+ service: VonageTelephonyService;
62
+ } {
63
+ const registry = new VonageCallMediaRegistry();
64
+ const service = new VonageTelephonyService(config, registry);
65
+ const publicRouter = Router();
66
+
67
+ // Vonage posts JSON webhook bodies; also accept urlencoded for signed-request `sig` schemes.
68
+ publicRouter.post('/answer', json(), urlencoded({ extended: false }), async (req: Request, res: Response) => {
69
+ await handleInboundAnswer(service, config, req, res);
70
+ });
71
+ publicRouter.post('/event', json(), urlencoded({ extended: false }), (req: Request, res: Response) => {
72
+ handleCallEvent(config, req, res);
73
+ });
74
+
75
+ return {
76
+ publicRouter,
77
+ attachMediaStreamServer: () => attachMediaStreamServer(registry),
78
+ registry,
79
+ service,
80
+ };
81
+ }
82
+
83
+ /** Handles the inbound answer webhook: verify → resolve → start bridge → return the connect-websocket NCCO. */
84
+ async function handleInboundAnswer(
85
+ service: VonageTelephonyService,
86
+ config: VonageTelephonyConfig,
87
+ req: Request,
88
+ res: Response,
89
+ ): Promise<void> {
90
+ const params = coerceParams(req.body);
91
+ if (!verifyVonageRequest(config, req, params)) {
92
+ res.status(403).type('application/json').json({ error: 'Invalid Vonage signature.' });
93
+ return;
94
+ }
95
+
96
+ let resolved: { callId: string; from: string; to: string };
97
+ try {
98
+ resolved = resolveInboundCall(params);
99
+ } catch (e) {
100
+ LogError(`[Telephony][Vonage] answer webhook unparseable: ${e instanceof Error ? e.message : String(e)}`);
101
+ res.status(200).type('application/json').json(NO_AGENT_NCCO);
102
+ return;
103
+ }
104
+
105
+ const context = resolveServerContext();
106
+ if (!context) {
107
+ LogError('[Telephony][Vonage] no server context user available; cannot start inbound call.');
108
+ res.status(200).type('application/json').json(NO_AGENT_NCCO);
109
+ return;
110
+ }
111
+
112
+ const result = await service.HandleInboundCall(resolved, context.user, context.provider);
113
+ if (!result.accepted) {
114
+ LogStatus(`[Telephony][Vonage] inbound ${resolved.callId} not accepted: ${result.reason ?? 'unknown'}`);
115
+ res.status(200).type('application/json').json(NO_AGENT_NCCO);
116
+ return;
117
+ }
118
+ const mediaUrl = appendCallUuid(config.mediaPublicUrl, resolved.callId);
119
+ res.status(200).type('application/json').json(buildInboundAnswerNcco(mediaUrl));
120
+ }
121
+
122
+ /** Handles a call-lifecycle event webhook: verify then acknowledge (the bridge tracks state via the media socket). */
123
+ function handleCallEvent(config: VonageTelephonyConfig, req: Request, res: Response): void {
124
+ const params = coerceParams(req.body);
125
+ if (!verifyVonageRequest(config, req, params)) {
126
+ res.status(403).type('application/json').json({ error: 'Invalid Vonage signature.' });
127
+ return;
128
+ }
129
+ LogStatus(`[Telephony][Vonage] event: ${params['status'] ?? params['event'] ?? 'unknown'} for ${params['uuid'] ?? params['conversation_uuid'] ?? '?'}`);
130
+ res.status(200).end();
131
+ }
132
+
133
+ /** Verifies a public Vonage webhook via the webhook JWT (preferred) or the signed-request `sig` HMAC. */
134
+ function verifyVonageRequest(config: VonageTelephonyConfig, req: Request, params: Record<string, string>): boolean {
135
+ const secret = config.signatureSecret;
136
+ if (!secret) {
137
+ return false;
138
+ }
139
+ const jwtClaims = verifyVonageJwt(secret, req.get('Authorization'), Math.floor(Date.now() / 1000));
140
+ if (jwtClaims) {
141
+ return true;
142
+ }
143
+ return verifyVonageSignature(secret, params['sig'], params);
144
+ }
145
+
146
+ /**
147
+ * Attaches the media WSS to the shared HTTP server on {@link VONAGE_MEDIA_WSS_PATH}. Each socket is mapped
148
+ * to its call UUID from the `?call_uuid=…` query param the answer NCCO embedded on the media URL, then
149
+ * inbound frames are dispatched to the call's bindings via the registry; the socket is also the registry's
150
+ * outbound pump for that call.
151
+ */
152
+ function attachMediaStreamServer(registry: VonageCallMediaRegistry): void {
153
+ // noServer: the shared path-routing upgrade dispatcher owns the HTTP 'upgrade' event (a second
154
+ // {server}-bound WS server would fight GraphQL's socket and 400 the handshake). The dispatcher
155
+ // passes the original request through, so the call-UUID query on the media URL is preserved.
156
+ const wss = new WebSocketServer({ noServer: true });
157
+ wss.on('connection', (socket: WebSocket, request: IncomingMessage) => wireMediaSocket(socket, request, registry));
158
+ RegisterMediaUpgradeRoute(VONAGE_MEDIA_WSS_PATH, wss);
159
+ LogStatus(`[Telephony][Vonage] media WSS route registered at ${VONAGE_MEDIA_WSS_PATH}`);
160
+ }
161
+
162
+ /** Wires one media socket: key on the `call_uuid` query param, attach, dispatch inbound frames, tear down on close. */
163
+ function wireMediaSocket(socket: WebSocket, request: IncomingMessage, registry: VonageCallMediaRegistry): void {
164
+ const callUuid = readCallUuid(request);
165
+ if (!callUuid) {
166
+ LogError('[Telephony][Vonage] media socket connected with no call_uuid; closing.');
167
+ socket.close();
168
+ return;
169
+ }
170
+ const adapter = {
171
+ sendBinary: (data: Uint8Array) => socket.send(data),
172
+ sendText: (data: string) => socket.send(data),
173
+ close: () => socket.close(),
174
+ };
175
+ registry.AttachSocket(callUuid, adapter);
176
+
177
+ // Vonage splits the media leg by frame type: BINARY frames carry raw L16 PCM audio; TEXT frames carry
178
+ // JSON control events (websocket:connected / websocket:dtmf / close). `ws` reports which via isBinary.
179
+ socket.on('message', (raw: Buffer, isBinary: boolean) => {
180
+ if (isBinary) {
181
+ registry.DispatchInboundAudio(callUuid, toArrayBuffer(raw));
182
+ return;
183
+ }
184
+ const event = parseVonageControlEvent(raw.toString('utf8'));
185
+ if (event) {
186
+ registry.DispatchInboundEvent(callUuid, event);
187
+ }
188
+ });
189
+ socket.on('close', () => registry.EndCall(callUuid));
190
+ }
191
+
192
+ /** Copies a Node `Buffer` into a standalone `ArrayBuffer` (respecting byteOffset/length, never aliasing the pool). */
193
+ function toArrayBuffer(buf: Buffer): ArrayBuffer {
194
+ const out = new ArrayBuffer(buf.byteLength);
195
+ new Uint8Array(out).set(buf);
196
+ return out;
197
+ }
198
+
199
+ /** Reads the `call_uuid` query param off the websocket upgrade request URL, or null when absent. */
200
+ function readCallUuid(request: IncomingMessage): string | null {
201
+ try {
202
+ const url = new URL(request.url ?? '', 'http://localhost');
203
+ const value = url.searchParams.get('call_uuid');
204
+ return value && value.length > 0 ? value : null;
205
+ } catch {
206
+ return null;
207
+ }
208
+ }
209
+
210
+ /** Appends the call UUID as a `call_uuid` query param to the media URL so the WSS can key the socket. */
211
+ function appendCallUuid(mediaUrl: string, callUuid: string): string {
212
+ const sep = mediaUrl.includes('?') ? '&' : '?';
213
+ return `${mediaUrl}${sep}call_uuid=${encodeURIComponent(callUuid)}`;
214
+ }
215
+
216
+ /** Coerces an Express body (JSON object or urlencoded) to the `Record<string,string>` the verifiers expect. */
217
+ function coerceParams(body: unknown): Record<string, string> {
218
+ const out: Record<string, string> = {};
219
+ if (body && typeof body === 'object') {
220
+ for (const [key, value] of Object.entries(body as Record<string, unknown>)) {
221
+ if (typeof value === 'string') {
222
+ out[key] = value;
223
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
224
+ out[key] = String(value);
225
+ }
226
+ }
227
+ }
228
+ return out;
229
+ }
230
+
231
+ /** Resolves the server-side principal + provider for the public webhook (no MJ JWT on a carrier request). */
232
+ function resolveServerContext(): { user: UserInfo; provider: IMetadataProvider } | null {
233
+ const user = UserCache.Instance.GetSystemUser() ?? UserCache.Users.find((u) => u.IsActive && u.Type?.trim().toLowerCase() === 'owner') ?? null;
234
+ const provider = Metadata.Provider as unknown as IMetadataProvider | undefined; // global-provider-ok: public carrier webhook has no MJ JWT / per-request provider; the server's single default provider is correct
235
+ if (!user || !provider) {
236
+ return null;
237
+ }
238
+ return { user, provider };
239
+ }