@memberjunction/server 5.44.0 → 5.45.1
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.
- package/dist/agentSessions/ReturningVisitorRecap.d.ts +39 -0
- package/dist/agentSessions/ReturningVisitorRecap.d.ts.map +1 -0
- package/dist/agentSessions/ReturningVisitorRecap.js +198 -0
- package/dist/agentSessions/ReturningVisitorRecap.js.map +1 -0
- package/dist/agentSessions/SessionJanitor.d.ts +13 -0
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +65 -7
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +37 -0
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.d.ts +10 -0
- package/dist/auth/magicLink/MagicLinkRouter.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +16 -0
- package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts +7 -0
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +13 -3
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/magicLink/index.d.ts +1 -1
- package/dist/auth/magicLink/index.d.ts.map +1 -1
- package/dist/auth/magicLink/index.js +1 -1
- package/dist/auth/magicLink/index.js.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.d.ts +38 -1
- package/dist/auth/magicLink/magicLinkCore.d.ts.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.js +42 -19
- package/dist/auth/magicLink/magicLinkCore.js.map +1 -1
- package/dist/auth/magicLink/types.d.ts +26 -0
- package/dist/auth/magicLink/types.d.ts.map +1 -1
- package/dist/config.d.ts +1883 -144
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +160 -36
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +45 -2
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +522 -0
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +2860 -0
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +13 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +22 -0
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +4 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +101 -3
- package/dist/index.js.map +1 -1
- package/dist/integration/CustomColumnPromoter.d.ts.map +1 -1
- package/dist/integration/CustomColumnPromoter.js +6 -0
- package/dist/integration/CustomColumnPromoter.js.map +1 -1
- package/dist/realtimeWidget/WidgetRouter.d.ts +26 -0
- package/dist/realtimeWidget/WidgetRouter.d.ts.map +1 -0
- package/dist/realtimeWidget/WidgetRouter.js +182 -0
- package/dist/realtimeWidget/WidgetRouter.js.map +1 -0
- package/dist/realtimeWidget/WidgetSessionService.d.ts +265 -0
- package/dist/realtimeWidget/WidgetSessionService.d.ts.map +1 -0
- package/dist/realtimeWidget/WidgetSessionService.js +477 -0
- package/dist/realtimeWidget/WidgetSessionService.js.map +1 -0
- package/dist/realtimeWidget/host-identity.d.ts +40 -0
- package/dist/realtimeWidget/host-identity.d.ts.map +1 -0
- package/dist/realtimeWidget/host-identity.js +58 -0
- package/dist/realtimeWidget/host-identity.js.map +1 -0
- package/dist/realtimeWidget/index.d.ts +10 -0
- package/dist/realtimeWidget/index.d.ts.map +1 -0
- package/dist/realtimeWidget/index.js +9 -0
- package/dist/realtimeWidget/index.js.map +1 -0
- package/dist/realtimeWidget/visitorIdentity.d.ts +76 -0
- package/dist/realtimeWidget/visitorIdentity.d.ts.map +1 -0
- package/dist/realtimeWidget/visitorIdentity.js +202 -0
- package/dist/realtimeWidget/visitorIdentity.js.map +1 -0
- package/dist/realtimeWidget/widgetCore.d.ts +106 -0
- package/dist/realtimeWidget/widgetCore.d.ts.map +1 -0
- package/dist/realtimeWidget/widgetCore.js +173 -0
- package/dist/realtimeWidget/widgetCore.js.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts +51 -0
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -0
- package/dist/realtimeWidget/widgetGuestElevation.js +79 -0
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -0
- package/dist/resolvers/ComponentRegistryResolver.d.ts +7 -0
- package/dist/resolvers/ComponentRegistryResolver.d.ts.map +1 -1
- package/dist/resolvers/ComponentRegistryResolver.js +31 -8
- package/dist/resolvers/ComponentRegistryResolver.js.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityPermissionResolver.js +1 -2
- package/dist/resolvers/EntityPermissionResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts +9 -6
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +15 -13
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -0
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +47 -3
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RingCentralTelephonyResolver.d.ts +27 -0
- package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/RingCentralTelephonyResolver.js +87 -0
- package/dist/resolvers/RingCentralTelephonyResolver.js.map +1 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts +9 -0
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +20 -4
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +2 -0
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/TeamsMeetingsResolver.d.ts +28 -0
- package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +1 -0
- package/dist/resolvers/TeamsMeetingsResolver.js +91 -0
- package/dist/resolvers/TeamsMeetingsResolver.js.map +1 -0
- package/dist/resolvers/TelephonyResolver.d.ts +26 -0
- package/dist/resolvers/TelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/TelephonyResolver.js +86 -0
- package/dist/resolvers/TelephonyResolver.js.map +1 -0
- package/dist/resolvers/TestQuerySQLResolver.d.ts +1 -1
- package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.js +2 -3
- package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
- package/dist/resolvers/VonageTelephonyResolver.d.ts +26 -0
- package/dist/resolvers/VonageTelephonyResolver.d.ts.map +1 -0
- package/dist/resolvers/VonageTelephonyResolver.js +86 -0
- package/dist/resolvers/VonageTelephonyResolver.js.map +1 -0
- package/dist/rest/MediaStreamHandler.js +4 -1
- package/dist/rest/MediaStreamHandler.js.map +1 -1
- package/dist/telephony/RingCentralTelephonyService.d.ts +115 -0
- package/dist/telephony/RingCentralTelephonyService.d.ts.map +1 -0
- package/dist/telephony/RingCentralTelephonyService.js +262 -0
- package/dist/telephony/RingCentralTelephonyService.js.map +1 -0
- package/dist/telephony/TeamsMeetingsRouter.d.ts +40 -0
- package/dist/telephony/TeamsMeetingsRouter.d.ts.map +1 -0
- package/dist/telephony/TeamsMeetingsRouter.js +96 -0
- package/dist/telephony/TeamsMeetingsRouter.js.map +1 -0
- package/dist/telephony/TeamsMeetingsService.d.ts +113 -0
- package/dist/telephony/TeamsMeetingsService.d.ts.map +1 -0
- package/dist/telephony/TeamsMeetingsService.js +196 -0
- package/dist/telephony/TeamsMeetingsService.js.map +1 -0
- package/dist/telephony/TwilioTelephonyRouter.d.ts +36 -0
- package/dist/telephony/TwilioTelephonyRouter.d.ts.map +1 -0
- package/dist/telephony/TwilioTelephonyRouter.js +143 -0
- package/dist/telephony/TwilioTelephonyRouter.js.map +1 -0
- package/dist/telephony/TwilioTelephonyService.d.ts +95 -0
- package/dist/telephony/TwilioTelephonyService.d.ts.map +1 -0
- package/dist/telephony/TwilioTelephonyService.js +193 -0
- package/dist/telephony/TwilioTelephonyService.js.map +1 -0
- package/dist/telephony/VonageTelephonyRouter.d.ts +41 -0
- package/dist/telephony/VonageTelephonyRouter.d.ts.map +1 -0
- package/dist/telephony/VonageTelephonyRouter.js +201 -0
- package/dist/telephony/VonageTelephonyRouter.js.map +1 -0
- package/dist/telephony/VonageTelephonyService.d.ts +90 -0
- package/dist/telephony/VonageTelephonyService.d.ts.map +1 -0
- package/dist/telephony/VonageTelephonyService.js +191 -0
- package/dist/telephony/VonageTelephonyService.js.map +1 -0
- package/dist/telephony/calendar-scheduler.d.ts +67 -0
- package/dist/telephony/calendar-scheduler.d.ts.map +1 -0
- package/dist/telephony/calendar-scheduler.js +133 -0
- package/dist/telephony/calendar-scheduler.js.map +1 -0
- package/dist/telephony/index.d.ts +29 -0
- package/dist/telephony/index.d.ts.map +1 -0
- package/dist/telephony/index.js +38 -0
- package/dist/telephony/index.js.map +1 -0
- package/dist/telephony/media-upgrade-router.d.ts +33 -0
- package/dist/telephony/media-upgrade-router.d.ts.map +1 -0
- package/dist/telephony/media-upgrade-router.js +56 -0
- package/dist/telephony/media-upgrade-router.js.map +1 -0
- package/dist/telephony/ringcentral-runtime.d.ts +14 -0
- package/dist/telephony/ringcentral-runtime.d.ts.map +1 -0
- package/dist/telephony/ringcentral-runtime.js +18 -0
- package/dist/telephony/ringcentral-runtime.js.map +1 -0
- package/dist/telephony/teams-meetings-runtime.d.ts +16 -0
- package/dist/telephony/teams-meetings-runtime.d.ts.map +1 -0
- package/dist/telephony/teams-meetings-runtime.js +20 -0
- package/dist/telephony/teams-meetings-runtime.js.map +1 -0
- package/dist/telephony/teamsAcsMediaRegistry.d.ts +69 -0
- package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/teamsAcsMediaRegistry.js +118 -0
- package/dist/telephony/teamsAcsMediaRegistry.js.map +1 -0
- package/dist/telephony/telephony-runtime.d.ts +14 -0
- package/dist/telephony/telephony-runtime.d.ts.map +1 -0
- package/dist/telephony/telephony-runtime.js +18 -0
- package/dist/telephony/telephony-runtime.js.map +1 -0
- package/dist/telephony/twilioMediaRegistry.d.ts +60 -0
- package/dist/telephony/twilioMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/twilioMediaRegistry.js +106 -0
- package/dist/telephony/twilioMediaRegistry.js.map +1 -0
- package/dist/telephony/vonage-runtime.d.ts +14 -0
- package/dist/telephony/vonage-runtime.d.ts.map +1 -0
- package/dist/telephony/vonage-runtime.js +18 -0
- package/dist/telephony/vonage-runtime.js.map +1 -0
- package/dist/telephony/vonageMediaRegistry.d.ts +78 -0
- package/dist/telephony/vonageMediaRegistry.d.ts.map +1 -0
- package/dist/telephony/vonageMediaRegistry.js +158 -0
- package/dist/telephony/vonageMediaRegistry.js.map +1 -0
- package/package.json +88 -83
- package/src/__tests__/RunAIAgentResolver.streaming.test.ts +128 -0
- package/src/__tests__/magicLink.test.ts +53 -3
- package/src/__tests__/search-knowledge-tags.test.ts +9 -9
- package/src/__tests__/teams-meetings-service.test.ts +65 -0
- package/src/__tests__/teamsAcsMediaRegistry.test.ts +82 -0
- package/src/__tests__/twilio-telephony-service.test.ts +23 -0
- package/src/__tests__/twilioMediaRegistry.test.ts +103 -0
- package/src/__tests__/vonageMediaRegistry.test.ts +180 -0
- package/src/__tests__/widget.test.ts +204 -0
- package/src/__tests__/widgetGuestElevation.test.ts +57 -0
- package/src/__tests__/widgetHostIdentity.test.ts +117 -0
- package/src/agentSessions/ReturningVisitorRecap.ts +242 -0
- package/src/agentSessions/SessionJanitor.ts +66 -6
- package/src/agentSessions/SessionManager.ts +38 -0
- package/src/auth/magicLink/MagicLinkRouter.ts +17 -0
- package/src/auth/magicLink/MagicLinkService.ts +13 -3
- package/src/auth/magicLink/index.ts +1 -0
- package/src/auth/magicLink/magicLinkCore.ts +48 -20
- package/src/auth/magicLink/types.ts +26 -0
- package/src/config.ts +172 -38
- package/src/context.ts +50 -3
- package/src/generated/generated.ts +1991 -1
- package/src/generic/ResolverBase.ts +28 -0
- package/src/generic/RunViewResolver.ts +4 -0
- package/src/index.ts +109 -3
- package/src/integration/CustomColumnPromoter.ts +6 -0
- package/src/realtimeWidget/WidgetRouter.ts +204 -0
- package/src/realtimeWidget/WidgetSessionService.ts +714 -0
- package/src/realtimeWidget/host-identity.ts +84 -0
- package/src/realtimeWidget/index.ts +15 -0
- package/src/realtimeWidget/visitorIdentity.ts +258 -0
- package/src/realtimeWidget/widgetCore.ts +231 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +115 -0
- package/src/resolvers/ComponentRegistryResolver.ts +36 -10
- package/src/resolvers/EntityPermissionResolver.ts +1 -2
- package/src/resolvers/QuerySystemUserResolver.ts +14 -10
- package/src/resolvers/RealtimeClientSessionResolver.ts +59 -2
- package/src/resolvers/RingCentralTelephonyResolver.ts +64 -0
- package/src/resolvers/RunAIAgentResolver.ts +32 -8
- package/src/resolvers/SearchKnowledgeResolver.ts +2 -0
- package/src/resolvers/TeamsMeetingsResolver.ts +68 -0
- package/src/resolvers/TelephonyResolver.ts +63 -0
- package/src/resolvers/TestQuerySQLResolver.ts +2 -3
- package/src/resolvers/VonageTelephonyResolver.ts +63 -0
- package/src/rest/MediaStreamHandler.ts +4 -1
- package/src/telephony/RingCentralTelephonyService.ts +342 -0
- package/src/telephony/TeamsMeetingsRouter.ts +124 -0
- package/src/telephony/TeamsMeetingsService.ts +268 -0
- package/src/telephony/TwilioTelephonyRouter.ts +184 -0
- package/src/telephony/TwilioTelephonyService.ts +261 -0
- package/src/telephony/VonageTelephonyRouter.ts +239 -0
- package/src/telephony/VonageTelephonyService.ts +259 -0
- package/src/telephony/calendar-scheduler.ts +176 -0
- package/src/telephony/index.ts +43 -0
- package/src/telephony/media-upgrade-router.ts +62 -0
- package/src/telephony/ringcentral-runtime.ts +22 -0
- package/src/telephony/teams-meetings-runtime.ts +24 -0
- package/src/telephony/teamsAcsMediaRegistry.ts +152 -0
- package/src/telephony/telephony-runtime.ts +22 -0
- package/src/telephony/twilioMediaRegistry.ts +137 -0
- package/src/telephony/vonage-runtime.ts +22 -0
- package/src/telephony/vonageMediaRegistry.ts +200 -0
- package/dist/agents/skip-agent.d.ts +0 -111
- package/dist/agents/skip-agent.d.ts.map +0 -1
- package/dist/agents/skip-agent.js +0 -1487
- package/dist/agents/skip-agent.js.map +0 -1
- package/dist/agents/skip-sdk.d.ts +0 -261
- package/dist/agents/skip-sdk.d.ts.map +0 -1
- package/dist/agents/skip-sdk.js +0 -909
- package/dist/agents/skip-sdk.js.map +0 -1
- package/src/agents/skip-agent.ts +0 -1594
- package/src/agents/skip-sdk.ts +0 -1210
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Orchestrates Vonage telephony bridge sessions (inbound + outbound) for MJAPI.
|
|
3
|
+
*
|
|
4
|
+
* This is the composition layer between the carrier ingress (the public answer/event webhooks + the
|
|
5
|
+
* bidirectional media WSS) and the ONE unified realtime-agent pathway. It does NOT re-implement any
|
|
6
|
+
* bridge logic: it resolves the Vonage provider + the agent identity, opens a server-side realtime
|
|
7
|
+
* session via the agent layer's `CreateBridgeRealtimeSession` factory, binds `RealVonageBindings` (real
|
|
8
|
+
* REST Voice client + the per-call media registry) onto the driver, and hands everything to
|
|
9
|
+
* `AIBridgeEngine.StartBridgeSession` — exactly the seam the Twilio service + LiveKit room coordinator use.
|
|
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 Vonage
|
|
13
|
+
* credentials + a publicly reachable media URL — see telephony-vendor-bindings.md §T2).
|
|
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 { VonageCallSdk, RealVonageBindings, RealVonageVoiceClient } from '@memberjunction/ai-bridge-vonage';
|
|
30
|
+
import type { VonageTelephonyConfig } from '../config.js';
|
|
31
|
+
import { SessionManager } from '../agentSessions/SessionManager.js';
|
|
32
|
+
import { VonageCallMediaRegistry } from './vonageMediaRegistry.js';
|
|
33
|
+
|
|
34
|
+
const VONAGE_PROVIDER_DRIVER = 'VonageBridge';
|
|
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 Vonage answer/event webhook). */
|
|
39
|
+
export interface InboundCallInput {
|
|
40
|
+
/** The Vonage call UUID (`uuid` / `conversation_uuid`). */
|
|
41
|
+
callId: string;
|
|
42
|
+
/** The caller's number (Vonage `from`). */
|
|
43
|
+
from: string;
|
|
44
|
+
/** The dialed DID (Vonage `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 VonageTelephonyServiceDeps {
|
|
58
|
+
engine?: Pick<AIBridgeEngine, 'ProviderByName' | 'ProviderByDriverClass' | 'StartBridgeSession' | 'Config'>;
|
|
59
|
+
sessionFactory?: typeof CreateBridgeRealtimeSession;
|
|
60
|
+
sessionManager?: Pick<SessionManager, 'CreateSession'>;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Starts Vonage bridge sessions. One instance per server, constructed alongside the telephony router and
|
|
65
|
+
* sharing the {@link VonageCallMediaRegistry} with the media WSS server.
|
|
66
|
+
*/
|
|
67
|
+
export class VonageTelephonyService {
|
|
68
|
+
private readonly voice: RealVonageVoiceClient;
|
|
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: VonageTelephonyConfig,
|
|
75
|
+
private readonly registry: VonageCallMediaRegistry,
|
|
76
|
+
deps: VonageTelephonyServiceDeps = {},
|
|
77
|
+
) {
|
|
78
|
+
this.voice = new RealVonageVoiceClient({
|
|
79
|
+
ApplicationId: config.applicationId,
|
|
80
|
+
PrivateKey: config.privateKey,
|
|
81
|
+
ApiKey: config.apiKey,
|
|
82
|
+
ApiSecret: config.apiSecret,
|
|
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
|
+
* `RealVonageBindings` will accept the media socket (opened by the answer webhook's `connect` NCCO)
|
|
92
|
+
* for the call UUID. Returns `{ accepted:false }` (never throws) when no agent identity matches the
|
|
93
|
+
* DID, so the webhook can answer with a polite "no agent" NCCO 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.callId);
|
|
102
|
+
await this.startBridge({
|
|
103
|
+
agentID: identity.AgentID,
|
|
104
|
+
direction: 'Inbound',
|
|
105
|
+
address: input.from,
|
|
106
|
+
inboundCallId: input.callId,
|
|
107
|
+
contextUser,
|
|
108
|
+
provider,
|
|
109
|
+
});
|
|
110
|
+
return { accepted: true };
|
|
111
|
+
} catch (e) {
|
|
112
|
+
LogError(`[Telephony][Vonage] inbound call ${input.callId} 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
|
+
* `RealVonageBindings.createCall` issues the Voice REST `POST /v1/calls` with the `connect`-websocket
|
|
120
|
+
* NCCO; the returned call UUID is the bridge's external connection id.
|
|
121
|
+
*
|
|
122
|
+
* @returns The placed call UUID.
|
|
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 vonageProvider = 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: vonageProvider,
|
|
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 Vonage provider row (by driver class, falling back to display name). */
|
|
182
|
+
private resolveProvider(): MJAIBridgeProviderEntity {
|
|
183
|
+
const provider = this.engine.ProviderByDriverClass(VONAGE_PROVIDER_DRIVER) ?? this.engine.ProviderByName('Vonage');
|
|
184
|
+
if (!provider) {
|
|
185
|
+
throw new Error(`No active 'MJ: AI Bridge Providers' row for the Vonage bridge (DriverClass '${VONAGE_PROVIDER_DRIVER}').`);
|
|
186
|
+
}
|
|
187
|
+
return provider;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Builds the per-session SDK binding that wires the REAL Vonage bindings (REST Voice client + the
|
|
192
|
+
* per-call media registry) onto the telephony driver — overriding the package's default unbound SDK.
|
|
193
|
+
* This is 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 VonageCallSdk(
|
|
200
|
+
new RealVonageBindings({
|
|
201
|
+
Voice: this.voice,
|
|
202
|
+
MediaPump: this.registry,
|
|
203
|
+
MediaWssUrl: this.config.mediaPublicUrl,
|
|
204
|
+
EventUrl: this.config.eventUrl,
|
|
205
|
+
}),
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Assembles the per-session bridge configuration (direction/from-number/inbound-call-id config keys). */
|
|
212
|
+
private buildSessionConfiguration(direction: 'Inbound' | 'Outbound', fromNumber?: string, inboundCallId?: string): Record<string, unknown> {
|
|
213
|
+
// The engine does NOT forward the top-level Direction into the driver's Configuration; the bridge
|
|
214
|
+
// defaults to 'Outbound' when the key is absent. Set it here so an inbound call answers instead of
|
|
215
|
+
// wrongly taking the outbound-dial path ("No 'From' number is specified").
|
|
216
|
+
const config: Record<string, unknown> = { [DIRECTION_CONFIG_KEY]: direction };
|
|
217
|
+
if (fromNumber) {
|
|
218
|
+
config[FROM_NUMBER_CONFIG_KEY] = fromNumber;
|
|
219
|
+
}
|
|
220
|
+
if (inboundCallId) {
|
|
221
|
+
config[INBOUND_CALL_ID_CONFIG_KEY] = inboundCallId;
|
|
222
|
+
}
|
|
223
|
+
return config;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Finds the active agent identity whose phone number matches the dialed DID. */
|
|
227
|
+
private async resolveAgentIdentityByPhone(dialedNumber: string, contextUser: UserInfo): Promise<MJAIBridgeAgentIdentityEntity | null> {
|
|
228
|
+
const normalized = (dialedNumber ?? '').trim();
|
|
229
|
+
if (!normalized) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
const rv = new RunView();
|
|
233
|
+
const result = await rv.RunView<MJAIBridgeAgentIdentityEntity>(
|
|
234
|
+
{
|
|
235
|
+
EntityName: AGENT_IDENTITY_ENTITY,
|
|
236
|
+
ExtraFilter: `IdentityType='${PHONE_IDENTITY_TYPE}' AND IdentityValue='${normalized.replace(/'/g, "''")}' AND IsActive=1`,
|
|
237
|
+
MaxRows: 1,
|
|
238
|
+
ResultType: 'entity_object',
|
|
239
|
+
},
|
|
240
|
+
contextUser,
|
|
241
|
+
);
|
|
242
|
+
if (!result.Success) {
|
|
243
|
+
LogError(`[Telephony][Vonage] agent-identity lookup failed: ${result.ErrorMessage}`);
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
return result.Results?.[0] ?? null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Loads a specific agent identity by id (used by outbound PlaceCall), requiring it be active. */
|
|
250
|
+
private async loadAgentIdentity(agentIdentityId: string, contextUser: UserInfo, provider: IMetadataProvider): Promise<MJAIBridgeAgentIdentityEntity | null> {
|
|
251
|
+
// Use the request-scoped provider (multi-provider safe), not the global Metadata.
|
|
252
|
+
const entity = await provider.GetEntityObject<MJAIBridgeAgentIdentityEntity>(AGENT_IDENTITY_ENTITY, contextUser);
|
|
253
|
+
const loaded = await entity.Load(agentIdentityId);
|
|
254
|
+
if (!loaded || !entity.IsActive) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
return entity;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Host wiring for the scheduled/invite-driven meeting join loop (program M2).
|
|
3
|
+
*
|
|
4
|
+
* Composes the offline-complete seams from `@memberjunction/ai-bridge-server` into a running
|
|
5
|
+
* background loop, mirroring the {@link SessionJanitor} startup pattern (run once at boot, then on an
|
|
6
|
+
* interval):
|
|
7
|
+
*
|
|
8
|
+
* 1. {@link CalendarWatcher} polls each agent identity's calendar (via the injected
|
|
9
|
+
* {@link CalendarSourceResolver}, bound here to the real {@link GraphCalendarClient} /
|
|
10
|
+
* {@link GoogleCalendarClient}) and creates a `Scheduled` `AIAgentSessionBridge` for every invite
|
|
11
|
+
* addressed to the agent.
|
|
12
|
+
* 2. {@link ScheduledBridgeRunner} starts each bridge whose `ScheduledStartTime` is due, building the
|
|
13
|
+
* realtime session + the provider bind-sdk via the per-provider meeting service (Teams today).
|
|
14
|
+
*
|
|
15
|
+
* Calendar credentials are PRE-RESOLVED upstream (the bot's Graph access token comes from the Teams
|
|
16
|
+
* meetings config — `botAccessToken`, resolved via MJ config, never inline). Google calendar polling
|
|
17
|
+
* binds only when a Google auth client is supplied; otherwise those identities are skipped (the watcher
|
|
18
|
+
* tolerates a `null` source per identity). Live verification requires a real tenant + the Azure app /
|
|
19
|
+
* Google service account — the same access the meeting bridges need.
|
|
20
|
+
*
|
|
21
|
+
* @module @memberjunction/server/telephony
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { UserInfo, IMetadataProvider, LogStatus, LogError } from '@memberjunction/core';
|
|
25
|
+
import type { MJAIAgentSessionEntity } from '@memberjunction/core-entities';
|
|
26
|
+
import {
|
|
27
|
+
CalendarWatcher,
|
|
28
|
+
ScheduledBridgeRunner,
|
|
29
|
+
GraphCalendarSource,
|
|
30
|
+
GraphCalendarClient,
|
|
31
|
+
type CalendarSourceResolver,
|
|
32
|
+
type ScheduledBridgeSessionFactory,
|
|
33
|
+
type StartBridgeSessionParams,
|
|
34
|
+
} from '@memberjunction/ai-bridge-server';
|
|
35
|
+
import type { TeamsMeetingsConfig } from '../config.js';
|
|
36
|
+
import { TeamsMeetingsService } from './TeamsMeetingsService.js';
|
|
37
|
+
|
|
38
|
+
/** The Teams bridge driver class — identities/bridges on this provider poll Graph + join via the Teams service. */
|
|
39
|
+
const TEAMS_PROVIDER_DRIVER = 'TeamsBridge';
|
|
40
|
+
|
|
41
|
+
/** Default cadence (ms) for the calendar sweep + due-bridge run. 60s balances freshness vs. API quota. */
|
|
42
|
+
const DEFAULT_POLL_INTERVAL_MS = 60_000;
|
|
43
|
+
|
|
44
|
+
/** Options for {@link StartCalendarScheduler}. */
|
|
45
|
+
export interface CalendarSchedulerOptions {
|
|
46
|
+
/** The server metadata provider all reads/writes run under. */
|
|
47
|
+
Provider: IMetadataProvider;
|
|
48
|
+
/** The system user the watcher + runner (and every created/started bridge) run as. */
|
|
49
|
+
ContextUser: UserInfo;
|
|
50
|
+
/** The Teams meetings service (shared with the ingress) — supplies the scheduled-join bind-sdk. */
|
|
51
|
+
TeamsService: TeamsMeetingsService;
|
|
52
|
+
/** The Teams meetings config — supplies the bot Graph access token for calendar polling. */
|
|
53
|
+
TeamsConfig: TeamsMeetingsConfig;
|
|
54
|
+
/** Poll cadence (ms). Defaults to {@link DEFAULT_POLL_INTERVAL_MS}. */
|
|
55
|
+
PollIntervalMs?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** A running scheduler handle — call {@link CalendarSchedulerHandle.Stop} to clear the interval. */
|
|
59
|
+
export interface CalendarSchedulerHandle {
|
|
60
|
+
/** Stops the periodic sweep/run loop (clears the timer). Safe to call more than once. */
|
|
61
|
+
Stop(): void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Builds the {@link CalendarSourceResolver} that binds the real calendar clients per identity. Graph
|
|
66
|
+
* (Teams/Microsoft) identities bind a {@link GraphCalendarClient} authenticated with the bot's
|
|
67
|
+
* pre-resolved Graph token; any identity without configured credentials resolves to `null` and is
|
|
68
|
+
* skipped this sweep (the watcher tolerates that). Google binding activates when a Google auth client
|
|
69
|
+
* is wired into the deployment (none configured today → Google-backed identities are skipped).
|
|
70
|
+
*/
|
|
71
|
+
export function buildCalendarSourceResolver(teamsConfig: TeamsMeetingsConfig): CalendarSourceResolver {
|
|
72
|
+
return (_identity, provider) => {
|
|
73
|
+
const driver = provider?.DriverClass?.trim();
|
|
74
|
+
if (driver === TEAMS_PROVIDER_DRIVER && teamsConfig.botAccessToken) {
|
|
75
|
+
return new GraphCalendarSource(new GraphCalendarClient({ Credentials: { AccessToken: teamsConfig.botAccessToken } }));
|
|
76
|
+
}
|
|
77
|
+
// Google Calendar is a deliberate, CLOSED seam — intentionally not bound here. The
|
|
78
|
+
// GoogleCalendarClient is built + unit-tested, but binding it would be dead code today:
|
|
79
|
+
// 1. there is no Google Meet *join* path for a detected invite to trigger (the scheduled-join
|
|
80
|
+
// factory is Teams-only), and
|
|
81
|
+
// 2. Google's Meet Media API is Developer-Preview + allowlist-gated regardless (every meeting
|
|
82
|
+
// participant must be enrolled), so it isn't a shippable target.
|
|
83
|
+
// To activate: define a Google auth client from config AND wire a Google Meet join. Until then
|
|
84
|
+
// Google identities are skipped rather than erroring the sweep.
|
|
85
|
+
return null;
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Builds the {@link ScheduledBridgeSessionFactory} that turns a due bridge into start params. Resolves
|
|
91
|
+
* the bridge's agent (via its `AIAgentSession`) and, for a Teams bridge, delegates to the shared
|
|
92
|
+
* {@link TeamsMeetingsService} so the scheduled join reuses the exact provider/session/bind-sdk path as
|
|
93
|
+
* an on-demand join. Returns `null` (skip) for a provider with no scheduled-join wiring yet, or when the
|
|
94
|
+
* bridge has no join address.
|
|
95
|
+
*/
|
|
96
|
+
export function buildScheduledSessionFactory(teamsService: TeamsMeetingsService): ScheduledBridgeSessionFactory {
|
|
97
|
+
return async (ctx) => {
|
|
98
|
+
const joinUrl = ctx.BridgeRow.Address?.trim();
|
|
99
|
+
if (!joinUrl) {
|
|
100
|
+
LogStatus(`[CalendarScheduler] bridge ${ctx.BridgeRow.ID} has no join address; skipping.`);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
if (ctx.Provider.DriverClass?.trim() !== TEAMS_PROVIDER_DRIVER) {
|
|
104
|
+
LogStatus(`[CalendarScheduler] no scheduled-join wiring for provider '${ctx.Provider.Name}'; skipping bridge ${ctx.BridgeRow.ID}.`);
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const agentID = await resolveAgentID(ctx.BridgeRow.AgentSessionID, ctx.ContextUser, ctx.MetadataProvider);
|
|
108
|
+
if (!agentID) {
|
|
109
|
+
LogStatus(`[CalendarScheduler] could not resolve agent for session ${ctx.BridgeRow.AgentSessionID}; skipping.`);
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
const params: StartBridgeSessionParams = await teamsService.BuildScheduledStartParams({
|
|
113
|
+
agentID,
|
|
114
|
+
joinUrl,
|
|
115
|
+
agentSessionID: ctx.BridgeRow.AgentSessionID,
|
|
116
|
+
contextUser: ctx.ContextUser,
|
|
117
|
+
provider: ctx.MetadataProvider,
|
|
118
|
+
});
|
|
119
|
+
return params;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Loads the `AIAgentSession` to resolve the agent the scheduled bridge belongs to. */
|
|
124
|
+
async function resolveAgentID(
|
|
125
|
+
agentSessionID: string,
|
|
126
|
+
contextUser: UserInfo,
|
|
127
|
+
provider: IMetadataProvider,
|
|
128
|
+
): Promise<string | null> {
|
|
129
|
+
const session = await provider.GetEntityObject<MJAIAgentSessionEntity>('MJ: AI Agent Sessions', contextUser);
|
|
130
|
+
if (!(await session.Load(agentSessionID))) {
|
|
131
|
+
return null;
|
|
132
|
+
}
|
|
133
|
+
return session.AgentID;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Starts the calendar/scheduled-bridge loop: sweeps calendars and runs due bridges once immediately,
|
|
138
|
+
* then on a fixed interval. Mirrors {@link SessionJanitor.Start}. A sweep/run failure is logged and the
|
|
139
|
+
* loop continues — a transient calendar/transport error never kills the scheduler.
|
|
140
|
+
*/
|
|
141
|
+
export function StartCalendarScheduler(options: CalendarSchedulerOptions): CalendarSchedulerHandle {
|
|
142
|
+
const watcher = new CalendarWatcher({
|
|
143
|
+
SourceResolver: buildCalendarSourceResolver(options.TeamsConfig),
|
|
144
|
+
ContextUser: options.ContextUser,
|
|
145
|
+
MetadataProvider: options.Provider,
|
|
146
|
+
});
|
|
147
|
+
const runner = new ScheduledBridgeRunner({
|
|
148
|
+
SessionFactory: buildScheduledSessionFactory(options.TeamsService),
|
|
149
|
+
ContextUser: options.ContextUser,
|
|
150
|
+
MetadataProvider: options.Provider,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const tick = async (): Promise<void> => {
|
|
154
|
+
try {
|
|
155
|
+
const swept = await watcher.Sweep();
|
|
156
|
+
const ran = await runner.RunDueBridges();
|
|
157
|
+
if (swept.BridgesCreated > 0 || ran.Started > 0) {
|
|
158
|
+
LogStatus(`[CalendarScheduler] swept ${swept.BridgesCreated} new bridge(s); started ${ran.Started} due bridge(s).`);
|
|
159
|
+
}
|
|
160
|
+
} catch (err) {
|
|
161
|
+
LogError(`[CalendarScheduler] sweep/run pass failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
void tick(); // run-once at boot
|
|
166
|
+
const timer = setInterval(() => void tick(), options.PollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS);
|
|
167
|
+
// Don't let the poll timer keep the process alive at shutdown (the loop is best-effort background work).
|
|
168
|
+
timer.unref?.();
|
|
169
|
+
LogStatus(`[CalendarScheduler] started (every ${(options.PollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS) / 1000}s).`);
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
Stop(): void {
|
|
173
|
+
clearInterval(timer);
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Public exports for the MJAPI telephony + meetings ingress.
|
|
3
|
+
*
|
|
4
|
+
* Each vendor ingress is a quartet: a per-call media registry, a bridge-orchestration service, an
|
|
5
|
+
* Express/WSS router (the `create*Handler` factory + mount-path consts), and a process-wide runtime
|
|
6
|
+
* holder (`Set*`/`Get*`). The routers + runtimes are star-exported; the service + registry modules are
|
|
7
|
+
* re-exported by name to avoid star-export ambiguity on their internal, per-vendor-identical helper
|
|
8
|
+
* types (`InboundCallInput`, `InboundCallResult`, `ITelephonyMediaSocket`). Those internal types are
|
|
9
|
+
* not part of the package's public surface — code that needs them imports from the source module.
|
|
10
|
+
*
|
|
11
|
+
* @module @memberjunction/server/telephony
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
// ── Shared WebSocket-upgrade dispatcher (routes GraphQL + all media sockets by path) ──────
|
|
15
|
+
export * from './media-upgrade-router.js';
|
|
16
|
+
|
|
17
|
+
// ── Twilio Programmable Voice + Media Streams ──────────────────────────────────────────
|
|
18
|
+
export { TwilioCallMediaRegistry } from './twilioMediaRegistry.js';
|
|
19
|
+
export { TwilioTelephonyService, type TwilioTelephonyServiceDeps } from './TwilioTelephonyService.js';
|
|
20
|
+
export * from './TwilioTelephonyRouter.js';
|
|
21
|
+
export * from './telephony-runtime.js';
|
|
22
|
+
|
|
23
|
+
// ── Vonage Voice + WebSocket-media ─────────────────────────────────────────────────────
|
|
24
|
+
export { VonageCallMediaRegistry } from './vonageMediaRegistry.js';
|
|
25
|
+
export { VonageTelephonyService, type VonageTelephonyServiceDeps } from './VonageTelephonyService.js';
|
|
26
|
+
export * from './VonageTelephonyRouter.js';
|
|
27
|
+
export * from './vonage-runtime.js';
|
|
28
|
+
|
|
29
|
+
// ── RingCentral SIP softphone (the only RingCentral transport with bidirectional call audio) ──
|
|
30
|
+
// No media registry / webhook router: the softphone is an outbound SIP registration that receives inbound
|
|
31
|
+
// INVITEs directly. The service owns the registration; the runtime holder shares it with the outbound
|
|
32
|
+
// PlaceRingCentralCall resolver.
|
|
33
|
+
export { RingCentralTelephonyService, type RingCentralTelephonyServiceDeps } from './RingCentralTelephonyService.js';
|
|
34
|
+
export * from './ringcentral-runtime.js';
|
|
35
|
+
|
|
36
|
+
// ── Teams meetings (Microsoft Graph cloud-communications + ACS application-hosted media) ──
|
|
37
|
+
export * from './teamsAcsMediaRegistry.js';
|
|
38
|
+
export * from './TeamsMeetingsService.js';
|
|
39
|
+
export * from './TeamsMeetingsRouter.js';
|
|
40
|
+
export * from './teams-meetings-runtime.js';
|
|
41
|
+
|
|
42
|
+
// ── Scheduled / invite-driven meeting join loop (M2: calendar watcher + due-bridge runner) ──
|
|
43
|
+
export * from './calendar-scheduler.js';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Single WebSocket-upgrade dispatcher for the shared HTTP server.
|
|
3
|
+
*
|
|
4
|
+
* Why this exists: in `ws` 8.x, every `new WebSocketServer({ server })` attaches its OWN `upgrade`
|
|
5
|
+
* listener that calls `handleUpgrade` UNCONDITIONALLY, and `handleUpgrade` does `abortHandshake(400)` for
|
|
6
|
+
* any request whose path it doesn't own. So two `{server}`-bound servers on one HTTP server (e.g. the
|
|
7
|
+
* GraphQL subscriptions socket on `/` AND a telephony Media-Streams socket on `/telephony/.../media`)
|
|
8
|
+
* fight on every upgrade — the non-owning server 400s the request. That's exactly what made Twilio's
|
|
9
|
+
* `<Connect><Stream>` fail with error 31920 ("Stream WebSocket failed to connect").
|
|
10
|
+
*
|
|
11
|
+
* The fix: telephony/meetings media servers are created with `{ noServer: true }` (no auto-listener) and
|
|
12
|
+
* register their path here; at boot we strip the auto-listeners and install ONE `upgrade` listener that
|
|
13
|
+
* routes by path — GraphQL on its root path, each media socket on its own. When no media routes are
|
|
14
|
+
* registered (telephony disabled), we leave the GraphQL server's own listener untouched (zero change).
|
|
15
|
+
*
|
|
16
|
+
* @module @memberjunction/server/telephony
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { IncomingMessage, Server as HttpServer } from 'node:http';
|
|
20
|
+
import type { Duplex } from 'node:stream';
|
|
21
|
+
import type { WebSocketServer } from 'ws';
|
|
22
|
+
|
|
23
|
+
/** path → the `{ noServer:true }` WebSocketServer that owns it. */
|
|
24
|
+
const mediaRoutes = new Map<string, WebSocketServer>();
|
|
25
|
+
|
|
26
|
+
/** Registers a media WebSocket route. Called by each vendor router's attach step. */
|
|
27
|
+
export function RegisterMediaUpgradeRoute(path: string, wss: WebSocketServer): void {
|
|
28
|
+
mediaRoutes.set(path, wss);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Installs the single path-routing `upgrade` dispatcher. No-op when no media routes are registered, so
|
|
33
|
+
* the GraphQL server keeps its own listener untouched. Otherwise it removes the auto-attached listeners
|
|
34
|
+
* (GraphQL's included) and dispatches every upgrade by pathname — GraphQL on `graphqlPath`, each media
|
|
35
|
+
* socket on its registered path; unknown paths are destroyed.
|
|
36
|
+
*
|
|
37
|
+
* @param httpServer the shared HTTP server.
|
|
38
|
+
* @param graphqlWss the GraphQL subscriptions WebSocketServer (kept, re-dispatched on its path).
|
|
39
|
+
* @param graphqlPath the GraphQL root path (the path GraphQL's socket listens on).
|
|
40
|
+
*/
|
|
41
|
+
export function InstallMediaUpgradeDispatcher(httpServer: HttpServer, graphqlWss: WebSocketServer, graphqlPath: string): void {
|
|
42
|
+
if (mediaRoutes.size === 0) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
httpServer.removeAllListeners('upgrade');
|
|
46
|
+
const dispatch = (wss: WebSocketServer, req: IncomingMessage, socket: Duplex, head: Buffer): void => {
|
|
47
|
+
wss.handleUpgrade(req, socket, head, (ws) => wss.emit('connection', ws, req));
|
|
48
|
+
};
|
|
49
|
+
httpServer.on('upgrade', (req: IncomingMessage, socket: Duplex, head: Buffer) => {
|
|
50
|
+
const pathname = (req.url ?? '').split('?')[0];
|
|
51
|
+
if (pathname === graphqlPath) {
|
|
52
|
+
dispatch(graphqlWss, req, socket, head);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const wss = mediaRoutes.get(pathname);
|
|
56
|
+
if (wss) {
|
|
57
|
+
dispatch(wss, req, socket, head);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
socket.destroy();
|
|
61
|
+
});
|
|
62
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Process-wide holder for the startup-constructed RingCentral telephony service, so the
|
|
3
|
+
* outbound `PlaceRingCentralCall` GraphQL resolver reaches the SAME {@link RingCentralTelephonyService}
|
|
4
|
+
* (and thus the same long-lived SIP-softphone registration) that the inbound INVITE coordinator uses. Set
|
|
5
|
+
* once during server boot; read by the resolver.
|
|
6
|
+
*
|
|
7
|
+
* @module @memberjunction/server/telephony
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { RingCentralTelephonyService } from './RingCentralTelephonyService.js';
|
|
11
|
+
|
|
12
|
+
let ringCentralService: RingCentralTelephonyService | undefined;
|
|
13
|
+
|
|
14
|
+
/** Binds the startup-constructed RingCentral telephony service (called from server boot). */
|
|
15
|
+
export function SetRingCentralTelephonyService(service: RingCentralTelephonyService): void {
|
|
16
|
+
ringCentralService = service;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Returns the bound RingCentral telephony service, or `undefined` when telephony is not configured. */
|
|
20
|
+
export function GetRingCentralTelephonyService(): RingCentralTelephonyService | undefined {
|
|
21
|
+
return ringCentralService;
|
|
22
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Process-wide holder for the startup-constructed Teams meetings service, so the
|
|
3
|
+
* `StartTeamsMeetingSession` GraphQL resolver reaches the SAME {@link TeamsMeetingsService} (and thus the
|
|
4
|
+
* same {@link TeamsAcsMediaRegistry} the ACS media adapter attaches transports to) that the Graph
|
|
5
|
+
* change-notification webhook router uses. Set once during server boot; read by the resolver + the router.
|
|
6
|
+
*
|
|
7
|
+
* Mirrors `telephony-runtime.ts` (the Twilio equivalent).
|
|
8
|
+
*
|
|
9
|
+
* @module @memberjunction/server/telephony
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { TeamsMeetingsService } from './TeamsMeetingsService.js';
|
|
13
|
+
|
|
14
|
+
let teamsService: TeamsMeetingsService | undefined;
|
|
15
|
+
|
|
16
|
+
/** Binds the startup-constructed Teams meetings service (called from server boot). */
|
|
17
|
+
export function SetTeamsMeetingsService(service: TeamsMeetingsService): void {
|
|
18
|
+
teamsService = service;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Returns the bound Teams meetings service, or `undefined` when Teams meetings are not configured. */
|
|
22
|
+
export function GetTeamsMeetingsService(): TeamsMeetingsService | undefined {
|
|
23
|
+
return teamsService;
|
|
24
|
+
}
|