@memberjunction/server 6.1.0-edge.6 → 6.1.0-edge.7

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 (177) hide show
  1. package/README.md +24 -9
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/config.d.ts +36 -0
  4. package/dist/config.d.ts.map +1 -1
  5. package/dist/config.js +9 -0
  6. package/dist/config.js.map +1 -1
  7. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts +30 -0
  8. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.d.ts.map +1 -1
  9. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js +58 -5
  10. package/dist/entitySubclasses/MJEntityPermissionEntityServer.server.js.map +1 -1
  11. package/dist/generated/graphql-schemas/__mj.d.ts +2842 -2144
  12. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  13. package/dist/generated/graphql-schemas/__mj.js +5334 -2143
  14. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  15. package/dist/generic/ResolverBase.d.ts +119 -4
  16. package/dist/generic/ResolverBase.d.ts.map +1 -1
  17. package/dist/generic/ResolverBase.js +264 -11
  18. package/dist/generic/ResolverBase.js.map +1 -1
  19. package/dist/generic/RunViewResolver.d.ts.map +1 -1
  20. package/dist/generic/RunViewResolver.js +26 -2
  21. package/dist/generic/RunViewResolver.js.map +1 -1
  22. package/dist/generic/refusalExtensions.d.ts +52 -0
  23. package/dist/generic/refusalExtensions.d.ts.map +1 -0
  24. package/dist/generic/refusalExtensions.js +16 -0
  25. package/dist/generic/refusalExtensions.js.map +1 -0
  26. package/dist/index.d.ts +9 -3
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +115 -91
  29. package/dist/index.js.map +1 -1
  30. package/dist/resolvers/EntityRecordNameResolver.d.ts +34 -5
  31. package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
  32. package/dist/resolvers/EntityRecordNameResolver.js +64 -7
  33. package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
  34. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +10 -2
  35. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  36. package/dist/resolvers/RealtimeClientSessionResolver.js +80 -4
  37. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  38. package/dist/rest/RealtimeSdpBrokerHandler.d.ts +23 -0
  39. package/dist/rest/RealtimeSdpBrokerHandler.d.ts.map +1 -0
  40. package/dist/rest/RealtimeSdpBrokerHandler.js +78 -0
  41. package/dist/rest/RealtimeSdpBrokerHandler.js.map +1 -0
  42. package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
  43. package/dist/serverExtensionReservedRoots.js +2 -3
  44. package/dist/serverExtensionReservedRoots.js.map +1 -1
  45. package/package.json +92 -92
  46. package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +118 -0
  47. package/src/__tests__/RealtimeClientSessionResolver.test.ts +20 -0
  48. package/src/__tests__/RealtimeSdpBrokerHandler.test.ts +191 -0
  49. package/src/__tests__/ResolverBase.filterEscaping.test.ts +63 -0
  50. package/src/__tests__/ResolverBase.readableFieldsTransport.test.ts +106 -0
  51. package/src/__tests__/generatedGraphQLSchema.structural.test.ts +46 -0
  52. package/src/__tests__/resolverBase.fls.test.ts +483 -0
  53. package/src/__tests__/resolverBase.recordChangesFls.test.ts +358 -0
  54. package/src/__tests__/resolverBase.refusalExtensions.test.ts +101 -0
  55. package/src/__tests__/serverExtensionReservedRoots.test.ts +79 -11
  56. package/src/config.ts +12 -0
  57. package/src/entitySubclasses/MJEntityPermissionEntityServer.server.ts +65 -6
  58. package/src/generated/graphql-schemas/__mj.ts +6590 -4287
  59. package/src/generic/ResolverBase.ts +313 -10
  60. package/src/generic/RunViewResolver.ts +28 -2
  61. package/src/generic/refusalExtensions.ts +59 -0
  62. package/src/index.ts +137 -100
  63. package/src/resolvers/EntityRecordNameResolver.ts +74 -8
  64. package/src/resolvers/RealtimeClientSessionResolver.ts +128 -2
  65. package/src/rest/RealtimeSdpBrokerHandler.ts +101 -0
  66. package/src/serverExtensionReservedRoots.ts +2 -3
  67. package/dist/resolvers/RingCentralTelephonyResolver.d.ts +0 -27
  68. package/dist/resolvers/RingCentralTelephonyResolver.d.ts.map +0 -1
  69. package/dist/resolvers/RingCentralTelephonyResolver.js +0 -87
  70. package/dist/resolvers/RingCentralTelephonyResolver.js.map +0 -1
  71. package/dist/resolvers/TeamsMeetingsResolver.d.ts +0 -28
  72. package/dist/resolvers/TeamsMeetingsResolver.d.ts.map +0 -1
  73. package/dist/resolvers/TeamsMeetingsResolver.js +0 -91
  74. package/dist/resolvers/TeamsMeetingsResolver.js.map +0 -1
  75. package/dist/resolvers/TelephonyResolver.d.ts +0 -26
  76. package/dist/resolvers/TelephonyResolver.d.ts.map +0 -1
  77. package/dist/resolvers/TelephonyResolver.js +0 -86
  78. package/dist/resolvers/TelephonyResolver.js.map +0 -1
  79. package/dist/resolvers/VonageTelephonyResolver.d.ts +0 -26
  80. package/dist/resolvers/VonageTelephonyResolver.d.ts.map +0 -1
  81. package/dist/resolvers/VonageTelephonyResolver.js +0 -86
  82. package/dist/resolvers/VonageTelephonyResolver.js.map +0 -1
  83. package/dist/telephony/RingCentralTelephonyService.d.ts +0 -115
  84. package/dist/telephony/RingCentralTelephonyService.d.ts.map +0 -1
  85. package/dist/telephony/RingCentralTelephonyService.js +0 -262
  86. package/dist/telephony/RingCentralTelephonyService.js.map +0 -1
  87. package/dist/telephony/TeamsMeetingsRouter.d.ts +0 -40
  88. package/dist/telephony/TeamsMeetingsRouter.d.ts.map +0 -1
  89. package/dist/telephony/TeamsMeetingsRouter.js +0 -112
  90. package/dist/telephony/TeamsMeetingsRouter.js.map +0 -1
  91. package/dist/telephony/TeamsMeetingsService.d.ts +0 -113
  92. package/dist/telephony/TeamsMeetingsService.d.ts.map +0 -1
  93. package/dist/telephony/TeamsMeetingsService.js +0 -196
  94. package/dist/telephony/TeamsMeetingsService.js.map +0 -1
  95. package/dist/telephony/TwilioTelephonyRouter.d.ts +0 -36
  96. package/dist/telephony/TwilioTelephonyRouter.d.ts.map +0 -1
  97. package/dist/telephony/TwilioTelephonyRouter.js +0 -143
  98. package/dist/telephony/TwilioTelephonyRouter.js.map +0 -1
  99. package/dist/telephony/TwilioTelephonyService.d.ts +0 -95
  100. package/dist/telephony/TwilioTelephonyService.d.ts.map +0 -1
  101. package/dist/telephony/TwilioTelephonyService.js +0 -193
  102. package/dist/telephony/TwilioTelephonyService.js.map +0 -1
  103. package/dist/telephony/VonageTelephonyRouter.d.ts +0 -41
  104. package/dist/telephony/VonageTelephonyRouter.d.ts.map +0 -1
  105. package/dist/telephony/VonageTelephonyRouter.js +0 -201
  106. package/dist/telephony/VonageTelephonyRouter.js.map +0 -1
  107. package/dist/telephony/VonageTelephonyService.d.ts +0 -90
  108. package/dist/telephony/VonageTelephonyService.d.ts.map +0 -1
  109. package/dist/telephony/VonageTelephonyService.js +0 -191
  110. package/dist/telephony/VonageTelephonyService.js.map +0 -1
  111. package/dist/telephony/calendar-scheduler.d.ts +0 -67
  112. package/dist/telephony/calendar-scheduler.d.ts.map +0 -1
  113. package/dist/telephony/calendar-scheduler.js +0 -133
  114. package/dist/telephony/calendar-scheduler.js.map +0 -1
  115. package/dist/telephony/index.d.ts +0 -29
  116. package/dist/telephony/index.d.ts.map +0 -1
  117. package/dist/telephony/index.js +0 -38
  118. package/dist/telephony/index.js.map +0 -1
  119. package/dist/telephony/media-upgrade-router.d.ts +0 -47
  120. package/dist/telephony/media-upgrade-router.d.ts.map +0 -1
  121. package/dist/telephony/media-upgrade-router.js +0 -72
  122. package/dist/telephony/media-upgrade-router.js.map +0 -1
  123. package/dist/telephony/ringcentral-runtime.d.ts +0 -14
  124. package/dist/telephony/ringcentral-runtime.d.ts.map +0 -1
  125. package/dist/telephony/ringcentral-runtime.js +0 -18
  126. package/dist/telephony/ringcentral-runtime.js.map +0 -1
  127. package/dist/telephony/teams-meetings-runtime.d.ts +0 -16
  128. package/dist/telephony/teams-meetings-runtime.d.ts.map +0 -1
  129. package/dist/telephony/teams-meetings-runtime.js +0 -20
  130. package/dist/telephony/teams-meetings-runtime.js.map +0 -1
  131. package/dist/telephony/teamsAcsMediaRegistry.d.ts +0 -69
  132. package/dist/telephony/teamsAcsMediaRegistry.d.ts.map +0 -1
  133. package/dist/telephony/teamsAcsMediaRegistry.js +0 -118
  134. package/dist/telephony/teamsAcsMediaRegistry.js.map +0 -1
  135. package/dist/telephony/telephony-runtime.d.ts +0 -14
  136. package/dist/telephony/telephony-runtime.d.ts.map +0 -1
  137. package/dist/telephony/telephony-runtime.js +0 -18
  138. package/dist/telephony/telephony-runtime.js.map +0 -1
  139. package/dist/telephony/twilioMediaRegistry.d.ts +0 -60
  140. package/dist/telephony/twilioMediaRegistry.d.ts.map +0 -1
  141. package/dist/telephony/twilioMediaRegistry.js +0 -106
  142. package/dist/telephony/twilioMediaRegistry.js.map +0 -1
  143. package/dist/telephony/vonage-runtime.d.ts +0 -14
  144. package/dist/telephony/vonage-runtime.d.ts.map +0 -1
  145. package/dist/telephony/vonage-runtime.js +0 -18
  146. package/dist/telephony/vonage-runtime.js.map +0 -1
  147. package/dist/telephony/vonageMediaRegistry.d.ts +0 -78
  148. package/dist/telephony/vonageMediaRegistry.d.ts.map +0 -1
  149. package/dist/telephony/vonageMediaRegistry.js +0 -158
  150. package/dist/telephony/vonageMediaRegistry.js.map +0 -1
  151. package/src/__tests__/media-upgrade-router.test.ts +0 -43
  152. package/src/__tests__/teams-meetings-service.test.ts +0 -65
  153. package/src/__tests__/teamsAcsMediaRegistry.test.ts +0 -82
  154. package/src/__tests__/twilio-telephony-service.test.ts +0 -23
  155. package/src/__tests__/twilioMediaRegistry.test.ts +0 -103
  156. package/src/__tests__/vonageMediaRegistry.test.ts +0 -180
  157. package/src/resolvers/RingCentralTelephonyResolver.ts +0 -64
  158. package/src/resolvers/TeamsMeetingsResolver.ts +0 -68
  159. package/src/resolvers/TelephonyResolver.ts +0 -63
  160. package/src/resolvers/VonageTelephonyResolver.ts +0 -63
  161. package/src/telephony/RingCentralTelephonyService.ts +0 -342
  162. package/src/telephony/TeamsMeetingsRouter.ts +0 -143
  163. package/src/telephony/TeamsMeetingsService.ts +0 -268
  164. package/src/telephony/TwilioTelephonyRouter.ts +0 -184
  165. package/src/telephony/TwilioTelephonyService.ts +0 -261
  166. package/src/telephony/VonageTelephonyRouter.ts +0 -239
  167. package/src/telephony/VonageTelephonyService.ts +0 -259
  168. package/src/telephony/calendar-scheduler.ts +0 -176
  169. package/src/telephony/index.ts +0 -43
  170. package/src/telephony/media-upgrade-router.ts +0 -79
  171. package/src/telephony/ringcentral-runtime.ts +0 -22
  172. package/src/telephony/teams-meetings-runtime.ts +0 -24
  173. package/src/telephony/teamsAcsMediaRegistry.ts +0 -152
  174. package/src/telephony/telephony-runtime.ts +0 -22
  175. package/src/telephony/twilioMediaRegistry.ts +0 -137
  176. package/src/telephony/vonage-runtime.ts +0 -22
  177. package/src/telephony/vonageMediaRegistry.ts +0 -200
@@ -1,82 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { TeamsAcsMediaRegistry, type IAcsMediaTransport } from '../telephony/teamsAcsMediaRegistry.js';
3
- import type { AcsInboundAudioFrame } from '@memberjunction/ai-bridge-teams';
4
-
5
- class FakeTransport implements IAcsMediaTransport {
6
- public readonly Sent: ArrayBuffer[] = [];
7
- public Closed = false;
8
- public sendAudioFrame(pcm: ArrayBuffer): void {
9
- this.Sent.push(pcm);
10
- }
11
- public close(): void {
12
- this.Closed = true;
13
- }
14
- }
15
-
16
- const frame = (bytes: number): ArrayBuffer => new ArrayBuffer(bytes);
17
-
18
- describe('TeamsAcsMediaRegistry', () => {
19
- it('exposes the configured ACS sample rate', () => {
20
- expect(new TeamsAcsMediaRegistry().SampleRate).toBe(16000);
21
- expect(new TeamsAcsMediaRegistry(8000).SampleRate).toBe(8000);
22
- });
23
-
24
- it('buffers outbound PCM before the transport attaches, then flushes on attach', () => {
25
- const registry = new TeamsAcsMediaRegistry();
26
- const a = frame(4);
27
- const b = frame(8);
28
- registry.Send('call-1', a);
29
- registry.Send('call-1', b);
30
-
31
- const transport = new FakeTransport();
32
- registry.AttachTransport('call-1', transport);
33
-
34
- expect(transport.Sent).toEqual([a, b]);
35
- });
36
-
37
- it('sends immediately once the transport is attached', () => {
38
- const registry = new TeamsAcsMediaRegistry();
39
- const transport = new FakeTransport();
40
- registry.AttachTransport('call-1', transport);
41
- const a = frame(4);
42
- registry.Send('call-1', a);
43
- expect(transport.Sent).toEqual([a]);
44
- });
45
-
46
- it('dispatches inbound frames to all registered handlers for the call', () => {
47
- const registry = new TeamsAcsMediaRegistry();
48
- const seen: AcsInboundAudioFrame[] = [];
49
- registry.OnFrame('call-1', (f) => seen.push(f));
50
- registry.OnFrame('call-1', (f) => seen.push(f));
51
- const f: AcsInboundAudioFrame = { Pcm: frame(2), ParticipantId: 'p1' };
52
- registry.DispatchInbound('call-1', f);
53
- expect(seen).toEqual([f, f]);
54
- });
55
-
56
- it('dispatches hand-raise signals to registered hand-raise handlers', () => {
57
- const registry = new TeamsAcsMediaRegistry();
58
- let raised: boolean | undefined;
59
- registry.OnHandRaise('call-1', (_p, r) => (raised = r));
60
- registry.DispatchHandRaise('call-1', 'p1', true);
61
- expect(raised).toBe(true);
62
- });
63
-
64
- it('ignores inbound dispatch + hand-raise for unknown calls (no throw)', () => {
65
- const registry = new TeamsAcsMediaRegistry();
66
- expect(() => registry.DispatchInbound('nope', { Pcm: frame(2), ParticipantId: 'p1' })).not.toThrow();
67
- expect(() => registry.DispatchHandRaise('nope', 'p1', true)).not.toThrow();
68
- });
69
-
70
- it('closes the transport and forgets the call on EndCall', () => {
71
- const registry = new TeamsAcsMediaRegistry();
72
- const transport = new FakeTransport();
73
- registry.AttachTransport('call-1', transport);
74
- expect(registry.HasCall('call-1')).toBe(true);
75
-
76
- registry.EndCall('call-1');
77
- expect(transport.Closed).toBe(true);
78
- expect(registry.HasCall('call-1')).toBe(false);
79
- // EndCall on an unknown call is a silent no-op.
80
- expect(() => registry.EndCall('unknown')).not.toThrow();
81
- });
82
- });
@@ -1,23 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { TwilioTelephonyService } from '../telephony/TwilioTelephonyService.js';
3
- import { TwilioCallMediaRegistry } from '../telephony/twilioMediaRegistry.js';
4
- import { TwilioCallSdk } from '@memberjunction/ai-bridge-twilio';
5
- import type { BaseTelephonyBridge } from '@memberjunction/ai-bridge-base';
6
-
7
- const CONFIG = { accountSid: 'AC1', authToken: 'tok', streamPublicUrl: 'wss://api.test/telephony/twilio/media' };
8
-
9
- describe('TwilioTelephonyService.buildBindSdk', () => {
10
- it('binds a TwilioCallSdk factory onto the telephony driver (the live REST + media wiring seam)', () => {
11
- const service = new TwilioTelephonyService(CONFIG, new TwilioCallMediaRegistry());
12
- const setSdkFactory = vi.fn();
13
- const fakeDriver = { SetSdkFactory: setSdkFactory } as unknown as BaseTelephonyBridge;
14
-
15
- service.buildBindSdk()(fakeDriver);
16
-
17
- expect(setSdkFactory).toHaveBeenCalledTimes(1);
18
- const factory = setSdkFactory.mock.calls[0][0] as () => unknown;
19
- expect(typeof factory).toBe('function');
20
- // The factory yields a real (bound) TwilioCallSdk — proving the offline seam meets live plumbing.
21
- expect(factory()).toBeInstanceOf(TwilioCallSdk);
22
- });
23
- });
@@ -1,103 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { TwilioCallMediaRegistry, type ITelephonyMediaSocket } from '../telephony/twilioMediaRegistry.js';
3
- import type { TwilioMediaFrame } from '@memberjunction/ai-bridge-twilio';
4
-
5
- /** A fake Media-Streams socket capturing sends + tracking close. */
6
- function fakeSocket(): ITelephonyMediaSocket & { sent: string[]; closed: boolean } {
7
- const sent: string[] = [];
8
- return {
9
- sent,
10
- closed: false,
11
- send(data: string) {
12
- sent.push(data);
13
- },
14
- close() {
15
- (this as { closed: boolean }).closed = true;
16
- },
17
- };
18
- }
19
-
20
- const mediaFrame = (payload: string): TwilioMediaFrame => ({ event: 'media', streamSid: 'MZ1', media: { payload } });
21
-
22
- describe('TwilioCallMediaRegistry', () => {
23
- describe('outbound buffering', () => {
24
- it('buffers outbound frames before the socket connects, then flushes on AttachSocket in order', () => {
25
- const reg = new TwilioCallMediaRegistry();
26
- reg.RegisterCall('CA1');
27
- reg.Send('CA1', mediaFrame('a'));
28
- reg.Send('CA1', mediaFrame('b'));
29
-
30
- const sock = fakeSocket();
31
- reg.AttachSocket('CA1', sock, 'MZ1');
32
-
33
- expect(sock.sent).toHaveLength(2);
34
- expect(JSON.parse(sock.sent[0]).media.payload).toBe('a');
35
- expect(JSON.parse(sock.sent[1]).media.payload).toBe('b');
36
- });
37
-
38
- it('sends immediately once the socket is attached', () => {
39
- const reg = new TwilioCallMediaRegistry();
40
- const sock = fakeSocket();
41
- reg.AttachSocket('CA1', sock, 'MZ1');
42
- reg.Send('CA1', mediaFrame('c'));
43
- expect(sock.sent).toHaveLength(1);
44
- expect(JSON.parse(sock.sent[0]).media.payload).toBe('c');
45
- });
46
- });
47
-
48
- describe('inbound dispatch', () => {
49
- it('delivers inbound frames to handlers registered before the socket existed', () => {
50
- const reg = new TwilioCallMediaRegistry();
51
- const received: TwilioMediaFrame[] = [];
52
- reg.OnFrame('CA1', (f) => received.push(f));
53
-
54
- reg.DispatchInbound('CA1', mediaFrame('in'));
55
-
56
- expect(received).toHaveLength(1);
57
- expect(received[0].media?.payload).toBe('in');
58
- });
59
-
60
- it('captures the stream SID from a start frame and exposes it via GetStreamSid', () => {
61
- const reg = new TwilioCallMediaRegistry();
62
- reg.RegisterCall('CA1');
63
- expect(reg.GetStreamSid('CA1')).toBe('');
64
- reg.DispatchInbound('CA1', { event: 'start', streamSid: 'MZ-XYZ' });
65
- expect(reg.GetStreamSid('CA1')).toBe('MZ-XYZ');
66
- });
67
-
68
- it('drops inbound frames for an unknown call (no channel) without throwing', () => {
69
- const reg = new TwilioCallMediaRegistry();
70
- expect(() => reg.DispatchInbound('UNKNOWN', mediaFrame('x'))).not.toThrow();
71
- });
72
-
73
- it('fans out to multiple handlers (audio + dtmf + status all registered on one call)', () => {
74
- const reg = new TwilioCallMediaRegistry();
75
- const h1 = vi.fn();
76
- const h2 = vi.fn();
77
- reg.OnFrame('CA1', h1);
78
- reg.OnFrame('CA1', h2);
79
- reg.DispatchInbound('CA1', mediaFrame('y'));
80
- expect(h1).toHaveBeenCalledTimes(1);
81
- expect(h2).toHaveBeenCalledTimes(1);
82
- });
83
- });
84
-
85
- describe('lifecycle', () => {
86
- it('EndCall closes the socket and forgets the channel', () => {
87
- const reg = new TwilioCallMediaRegistry();
88
- const sock = fakeSocket();
89
- reg.AttachSocket('CA1', sock, 'MZ1');
90
- expect(reg.HasCall('CA1')).toBe(true);
91
-
92
- reg.EndCall('CA1');
93
-
94
- expect(sock.closed).toBe(true);
95
- expect(reg.HasCall('CA1')).toBe(false);
96
- });
97
-
98
- it('EndCall on an unknown call is a no-op', () => {
99
- const reg = new TwilioCallMediaRegistry();
100
- expect(() => reg.EndCall('NOPE')).not.toThrow();
101
- });
102
- });
103
- });
@@ -1,180 +0,0 @@
1
- import { describe, it, expect, vi } from 'vitest';
2
- import { VonageCallMediaRegistry, type ITelephonyMediaSocket } from '../telephony/vonageMediaRegistry.js';
3
- import type { VonageControlEvent } from '@memberjunction/ai-bridge-vonage';
4
-
5
- /** A fake media socket capturing outbound BINARY audio + TEXT control frames + tracking close. */
6
- function fakeSocket(): ITelephonyMediaSocket & { sent: Uint8Array[]; text: string[]; closed: boolean } {
7
- const sent: Uint8Array[] = [];
8
- const text: string[] = [];
9
- return {
10
- sent,
11
- text,
12
- closed: false,
13
- sendBinary(data: Uint8Array) {
14
- sent.push(data);
15
- },
16
- sendText(data: string) {
17
- text.push(data);
18
- },
19
- close() {
20
- (this as { closed: boolean }).closed = true;
21
- },
22
- };
23
- }
24
-
25
- /** A small PCM16 ArrayBuffer carrying one recognizable little-endian sample, for ordering/round-trip checks. */
26
- const pcm = (sample: number): ArrayBuffer => {
27
- const buf = new ArrayBuffer(2);
28
- new DataView(buf).setInt16(0, sample, true);
29
- return buf;
30
- };
31
-
32
- /** Vonage's outbound frame size: 20 ms @ 8 kHz / 16-bit = 320 bytes. */
33
- const FRAME_BYTES = 320;
34
-
35
- /** Builds an N-byte PCM ArrayBuffer whose first byte is `tag`, so frames are distinguishable. */
36
- const audio = (bytes: number, tag = 0): ArrayBuffer => {
37
- const buf = new Uint8Array(bytes);
38
- if (bytes > 0) buf[0] = tag;
39
- return buf.buffer;
40
- };
41
-
42
- describe('VonageCallMediaRegistry', () => {
43
- describe('outbound audio framing (20 ms / 320-byte slices)', () => {
44
- it('buffers whole 320-byte frames before the socket connects, then flushes them on AttachSocket in order', () => {
45
- const reg = new VonageCallMediaRegistry();
46
- reg.RegisterCall('CALL-1');
47
- reg.SendAudio('CALL-1', audio(FRAME_BYTES, 0xa1));
48
- reg.SendAudio('CALL-1', audio(FRAME_BYTES, 0xb2));
49
-
50
- const sock = fakeSocket();
51
- reg.AttachSocket('CALL-1', sock);
52
-
53
- expect(sock.sent).toHaveLength(2);
54
- expect(sock.sent[0]).toHaveLength(FRAME_BYTES);
55
- expect(sock.sent[0][0]).toBe(0xa1);
56
- expect(sock.sent[1][0]).toBe(0xb2);
57
- });
58
-
59
- it('slices an oversized delta into multiple 320-byte frames and carries the sub-frame remainder', () => {
60
- const reg = new VonageCallMediaRegistry();
61
- const sock = fakeSocket();
62
- reg.AttachSocket('CALL-1', sock);
63
-
64
- // 800 bytes = two whole frames + 160 left over (held, not sent).
65
- reg.SendAudio('CALL-1', audio(800));
66
- expect(sock.sent).toHaveLength(2);
67
- expect(sock.sent.every((f) => f.length === FRAME_BYTES)).toBe(true);
68
-
69
- // Another 160 bytes completes the carried remainder into a third whole frame.
70
- reg.SendAudio('CALL-1', audio(160));
71
- expect(sock.sent).toHaveLength(3);
72
- expect(sock.sent[2]).toHaveLength(FRAME_BYTES);
73
- });
74
-
75
- it('holds a sub-frame delta until enough bytes accumulate (no silence-padded partial frame)', () => {
76
- const reg = new VonageCallMediaRegistry();
77
- const sock = fakeSocket();
78
- reg.AttachSocket('CALL-1', sock);
79
- reg.SendAudio('CALL-1', audio(100));
80
- expect(sock.sent).toHaveLength(0); // < one frame — nothing sent yet
81
- });
82
- });
83
-
84
- describe('inbound audio dispatch', () => {
85
- it('delivers inbound audio to handlers registered before the socket existed', () => {
86
- const reg = new VonageCallMediaRegistry();
87
- const received: number[] = [];
88
- reg.OnAudio('CALL-1', (p) => received.push(new DataView(p).getInt16(0, true)));
89
-
90
- reg.DispatchInboundAudio('CALL-1', pcm(42));
91
-
92
- expect(received).toEqual([42]);
93
- });
94
-
95
- it('drops inbound audio for an unknown call (no channel) without throwing', () => {
96
- const reg = new VonageCallMediaRegistry();
97
- expect(() => reg.DispatchInboundAudio('UNKNOWN', pcm(1))).not.toThrow();
98
- });
99
-
100
- it('fans out audio to multiple handlers registered on one call', () => {
101
- const reg = new VonageCallMediaRegistry();
102
- const h1 = vi.fn();
103
- const h2 = vi.fn();
104
- reg.OnAudio('CALL-1', h1);
105
- reg.OnAudio('CALL-1', h2);
106
- reg.DispatchInboundAudio('CALL-1', pcm(7));
107
- expect(h1).toHaveBeenCalledTimes(1);
108
- expect(h2).toHaveBeenCalledTimes(1);
109
- });
110
- });
111
-
112
- describe('inbound control-event dispatch', () => {
113
- it('delivers parsed control events (DTMF / close) to event handlers, separate from audio', () => {
114
- const reg = new VonageCallMediaRegistry();
115
- const events: VonageControlEvent[] = [];
116
- const audio = vi.fn();
117
- reg.OnEvent('CALL-1', (e) => events.push(e));
118
- reg.OnAudio('CALL-1', audio);
119
-
120
- reg.DispatchInboundEvent('CALL-1', { event: 'websocket:dtmf', digit: '5', duration: 260 });
121
- reg.DispatchInboundEvent('CALL-1', { event: 'close' });
122
-
123
- expect(audio).not.toHaveBeenCalled();
124
- expect(events).toHaveLength(2);
125
- expect(events[0].digit).toBe('5');
126
- expect(events[1].event).toBe('close');
127
- });
128
-
129
- it('drops inbound events for an unknown call without throwing', () => {
130
- const reg = new VonageCallMediaRegistry();
131
- expect(() => reg.DispatchInboundEvent('UNKNOWN', { event: 'close' })).not.toThrow();
132
- });
133
- });
134
-
135
- describe('barge-in flush (Clear)', () => {
136
- it('sends Vonage {"action":"clear"} on the live socket and drops locally-buffered audio', () => {
137
- const reg = new VonageCallMediaRegistry();
138
- const sock = fakeSocket();
139
- reg.AttachSocket('CALL-1', sock);
140
- reg.Clear('CALL-1');
141
- expect(sock.text).toEqual([JSON.stringify({ action: 'clear' })]);
142
- });
143
-
144
- it('drops outbound frames buffered before the socket connected, sending nothing on attach', () => {
145
- const reg = new VonageCallMediaRegistry();
146
- reg.RegisterCall('CALL-1');
147
- reg.SendAudio('CALL-1', audio(FRAME_BYTES)); // a whole frame, buffered (no socket yet)
148
- reg.SendAudio('CALL-1', audio(100)); // sub-frame remainder, held in partial
149
- reg.Clear('CALL-1'); // barge-in before the media socket connected
150
-
151
- const sock = fakeSocket();
152
- reg.AttachSocket('CALL-1', sock);
153
- expect(sock.sent).toHaveLength(0); // both the buffered frame and the partial were cleared
154
- });
155
-
156
- it('Clear on an unknown call is a no-op', () => {
157
- const reg = new VonageCallMediaRegistry();
158
- expect(() => reg.Clear('NOPE')).not.toThrow();
159
- });
160
- });
161
-
162
- describe('lifecycle', () => {
163
- it('EndCall closes the socket and forgets the channel', () => {
164
- const reg = new VonageCallMediaRegistry();
165
- const sock = fakeSocket();
166
- reg.AttachSocket('CALL-1', sock);
167
- expect(reg.HasCall('CALL-1')).toBe(true);
168
-
169
- reg.EndCall('CALL-1');
170
-
171
- expect(sock.closed).toBe(true);
172
- expect(reg.HasCall('CALL-1')).toBe(false);
173
- });
174
-
175
- it('EndCall on an unknown call is a no-op', () => {
176
- const reg = new VonageCallMediaRegistry();
177
- expect(() => reg.EndCall('NOPE')).not.toThrow();
178
- });
179
- });
180
- });
@@ -1,64 +0,0 @@
1
- /**
2
- * @fileoverview GraphQL surface for OUTBOUND RingCentral telephony: `PlaceRingCentralCall` rings a real
3
- * phone from an agent identity and connects the pinned agent. Thin — it authorizes the caller, then
4
- * delegates to the startup-constructed {@link RingCentralTelephonyService} (which owns the bridge
5
- * orchestration + the shared media registry). Inbound calls arrive via the public
6
- * `POST /telephony/ringcentral/webhook` notification, not GraphQL.
7
- *
8
- * @module @memberjunction/server
9
- */
10
-
11
- import { Resolver, Mutation, Arg, Ctx, ObjectType, Field } from 'type-graphql';
12
- import { LogError, IMetadataProvider } from '@memberjunction/core';
13
- import { AppContext } from '../types.js';
14
- import { ResolverBase } from '../generic/ResolverBase.js';
15
- import { GetReadWriteProvider } from '../util.js';
16
- import { GetRingCentralTelephonyService } from '../telephony/ringcentral-runtime.js';
17
-
18
- /** Result of an outbound RingCentral place-call attempt. */
19
- @ObjectType()
20
- export class PlaceRingCentralCallResult {
21
- @Field(() => Boolean)
22
- Success: boolean;
23
-
24
- @Field(() => String, { nullable: true })
25
- ErrorMessage?: string;
26
-
27
- /** The placed RingCentral telephony session id (the bridge's external connection id). */
28
- @Field(() => String)
29
- SessionId: string;
30
- }
31
-
32
- @Resolver()
33
- export class RingCentralTelephonyResolver extends ResolverBase {
34
- /**
35
- * Places an outbound call from the given agent identity (its phone number is the caller-id) to a
36
- * destination number, connecting the pinned agent over the realtime bridge. Returns the telephony
37
- * session id.
38
- */
39
- @Mutation(() => PlaceRingCentralCallResult)
40
- async PlaceRingCentralCall(
41
- @Arg('agentIdentityId', () => String) agentIdentityId: string,
42
- @Arg('toNumber', () => String) toNumber: string,
43
- @Ctx() context: AppContext = {} as AppContext,
44
- ): Promise<PlaceRingCentralCallResult> {
45
- const failure = (msg: string): PlaceRingCentralCallResult => ({ Success: false, ErrorMessage: msg, SessionId: '' });
46
- try {
47
- const user = this.GetUserFromPayload(context.userPayload);
48
- if (!user) {
49
- return failure('Unable to determine current user.');
50
- }
51
- const service = GetRingCentralTelephonyService();
52
- if (!service) {
53
- return failure('RingCentral telephony is not configured on this server.');
54
- }
55
- const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
56
- const sessionId = await service.PlaceOutboundCall(agentIdentityId, toNumber, user, provider);
57
- return { Success: true, SessionId: sessionId };
58
- } catch (error) {
59
- const msg = error instanceof Error ? error.message : String(error);
60
- LogError(`PlaceRingCentralCall failed: ${msg}`);
61
- return failure(msg);
62
- }
63
- }
64
- }
@@ -1,68 +0,0 @@
1
- /**
2
- * @fileoverview GraphQL surface for Teams meeting joins: `StartTeamsMeetingSession` has an agent identity
3
- * join a Teams meeting by URL and connects the pinned agent over the realtime bridge. Thin — it authorizes
4
- * the caller, then delegates to the startup-constructed {@link TeamsMeetingsService} (which owns the bridge
5
- * orchestration + the shared ACS media registry). Roster/call-ended updates arrive via the public
6
- * `POST /meetings/teams/notifications` Graph webhook, not GraphQL.
7
- *
8
- * Mirrors `TelephonyResolver.ts` (the Twilio `PlaceTwilioCall` equivalent).
9
- *
10
- * @module @memberjunction/server
11
- */
12
-
13
- import { Resolver, Mutation, Arg, Ctx, ObjectType, Field } from 'type-graphql';
14
- import { LogError, IMetadataProvider } from '@memberjunction/core';
15
- import { AppContext } from '../types.js';
16
- import { ResolverBase } from '../generic/ResolverBase.js';
17
- import { GetReadWriteProvider } from '../util.js';
18
- import { GetTeamsMeetingsService } from '../telephony/teams-meetings-runtime.js';
19
-
20
- /** Result of a Teams meeting join attempt. */
21
- @ObjectType()
22
- export class StartTeamsMeetingResult {
23
- @Field(() => Boolean)
24
- Success: boolean;
25
-
26
- @Field(() => String, { nullable: true })
27
- ErrorMessage?: string;
28
-
29
- /** The Graph call id the bot joined (the bridge's external connection id), when successful. */
30
- @Field(() => String)
31
- CallId: string;
32
- }
33
-
34
- @Resolver()
35
- export class TeamsMeetingsResolver extends ResolverBase {
36
- /**
37
- * Has the given agent identity join a Teams meeting by URL, connecting the pinned agent over the realtime
38
- * bridge. Returns the Graph call id.
39
- */
40
- @Mutation(() => StartTeamsMeetingResult)
41
- async StartTeamsMeetingSession(
42
- @Arg('agentIdentityId', () => String) agentIdentityId: string,
43
- @Arg('joinUrl', () => String) joinUrl: string,
44
- @Ctx() context: AppContext = {} as AppContext,
45
- ): Promise<StartTeamsMeetingResult> {
46
- const failure = (msg: string): StartTeamsMeetingResult => ({ Success: false, ErrorMessage: msg, CallId: '' });
47
- try {
48
- const user = this.GetUserFromPayload(context.userPayload);
49
- if (!user) {
50
- return failure('Unable to determine current user.');
51
- }
52
- const service = GetTeamsMeetingsService();
53
- if (!service) {
54
- return failure('Teams meetings are not configured on this server.');
55
- }
56
- const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
57
- const result = await service.JoinMeetingByUrl(agentIdentityId, joinUrl, user, provider);
58
- if (!result.accepted) {
59
- return failure(result.reason ?? 'The meeting join was not accepted.');
60
- }
61
- return { Success: true, CallId: result.callId ?? '' };
62
- } catch (error) {
63
- const msg = error instanceof Error ? error.message : String(error);
64
- LogError(`StartTeamsMeetingSession failed: ${msg}`);
65
- return failure(msg);
66
- }
67
- }
68
- }
@@ -1,63 +0,0 @@
1
- /**
2
- * @fileoverview GraphQL surface for OUTBOUND telephony: `PlaceTwilioCall` rings a real phone from
3
- * an agent identity and connects the pinned agent. Thin — it authorizes the caller, then delegates to
4
- * the startup-constructed {@link TwilioTelephonyService} (which owns the bridge orchestration + the
5
- * shared media registry). Inbound calls arrive via the public `POST /telephony/twilio/voice` webhook,
6
- * not GraphQL.
7
- *
8
- * @module @memberjunction/server
9
- */
10
-
11
- import { Resolver, Mutation, Arg, Ctx, ObjectType, Field } from 'type-graphql';
12
- import { LogError, IMetadataProvider } from '@memberjunction/core';
13
- import { AppContext } from '../types.js';
14
- import { ResolverBase } from '../generic/ResolverBase.js';
15
- import { GetReadWriteProvider } from '../util.js';
16
- import { GetTwilioTelephonyService } from '../telephony/telephony-runtime.js';
17
-
18
- /** Result of an outbound place-call attempt. */
19
- @ObjectType()
20
- export class PlaceCallResult {
21
- @Field(() => Boolean)
22
- Success: boolean;
23
-
24
- @Field(() => String, { nullable: true })
25
- ErrorMessage?: string;
26
-
27
- /** The placed Twilio Call SID (the bridge's external connection id). */
28
- @Field(() => String)
29
- CallSid: string;
30
- }
31
-
32
- @Resolver()
33
- export class TelephonyResolver extends ResolverBase {
34
- /**
35
- * Places an outbound call from the given agent identity (its phone number is the caller-id) to a
36
- * destination number, connecting the pinned agent over the realtime bridge. Returns the Call SID.
37
- */
38
- @Mutation(() => PlaceCallResult)
39
- async PlaceTwilioCall(
40
- @Arg('agentIdentityId', () => String) agentIdentityId: string,
41
- @Arg('toNumber', () => String) toNumber: string,
42
- @Ctx() context: AppContext = {} as AppContext,
43
- ): Promise<PlaceCallResult> {
44
- const failure = (msg: string): PlaceCallResult => ({ Success: false, ErrorMessage: msg, CallSid: '' });
45
- try {
46
- const user = this.GetUserFromPayload(context.userPayload);
47
- if (!user) {
48
- return failure('Unable to determine current user.');
49
- }
50
- const service = GetTwilioTelephonyService();
51
- if (!service) {
52
- return failure('Twilio telephony is not configured on this server.');
53
- }
54
- const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
55
- const callSid = await service.PlaceOutboundCall(agentIdentityId, toNumber, user, provider);
56
- return { Success: true, CallSid: callSid };
57
- } catch (error) {
58
- const msg = error instanceof Error ? error.message : String(error);
59
- LogError(`PlaceTwilioCall failed: ${msg}`);
60
- return failure(msg);
61
- }
62
- }
63
- }
@@ -1,63 +0,0 @@
1
- /**
2
- * @fileoverview GraphQL surface for OUTBOUND Vonage telephony: `PlaceVonageCall` rings a real phone from
3
- * an agent identity and connects the pinned agent. Thin — it authorizes the caller, then delegates to
4
- * the startup-constructed {@link VonageTelephonyService} (which owns the bridge orchestration + the
5
- * shared media registry). Inbound calls arrive via the public `POST /telephony/vonage/answer` webhook,
6
- * not GraphQL.
7
- *
8
- * @module @memberjunction/server
9
- */
10
-
11
- import { Resolver, Mutation, Arg, Ctx, ObjectType, Field } from 'type-graphql';
12
- import { LogError, IMetadataProvider } from '@memberjunction/core';
13
- import { AppContext } from '../types.js';
14
- import { ResolverBase } from '../generic/ResolverBase.js';
15
- import { GetReadWriteProvider } from '../util.js';
16
- import { GetVonageTelephonyService } from '../telephony/vonage-runtime.js';
17
-
18
- /** Result of an outbound place-call attempt. */
19
- @ObjectType()
20
- export class PlaceVonageCallResult {
21
- @Field(() => Boolean)
22
- Success: boolean;
23
-
24
- @Field(() => String, { nullable: true })
25
- ErrorMessage?: string;
26
-
27
- /** The placed Vonage call UUID (the bridge's external connection id). */
28
- @Field(() => String)
29
- CallId: string;
30
- }
31
-
32
- @Resolver()
33
- export class VonageTelephonyResolver extends ResolverBase {
34
- /**
35
- * Places an outbound call from the given agent identity (its phone number is the caller-id) to a
36
- * destination number, connecting the pinned agent over the realtime bridge. Returns the call UUID.
37
- */
38
- @Mutation(() => PlaceVonageCallResult)
39
- async PlaceVonageCall(
40
- @Arg('agentIdentityId', () => String) agentIdentityId: string,
41
- @Arg('toNumber', () => String) toNumber: string,
42
- @Ctx() context: AppContext = {} as AppContext,
43
- ): Promise<PlaceVonageCallResult> {
44
- const failure = (msg: string): PlaceVonageCallResult => ({ Success: false, ErrorMessage: msg, CallId: '' });
45
- try {
46
- const user = this.GetUserFromPayload(context.userPayload);
47
- if (!user) {
48
- return failure('Unable to determine current user.');
49
- }
50
- const service = GetVonageTelephonyService();
51
- if (!service) {
52
- return failure('Vonage telephony is not configured on this server.');
53
- }
54
- const provider = GetReadWriteProvider(context.providers) as unknown as IMetadataProvider;
55
- const callId = await service.PlaceOutboundCall(agentIdentityId, toNumber, user, provider);
56
- return { Success: true, CallId: callId };
57
- } catch (error) {
58
- const msg = error instanceof Error ? error.message : String(error);
59
- LogError(`PlaceVonageCall failed: ${msg}`);
60
- return failure(msg);
61
- }
62
- }
63
- }