@memberjunction/ai-bridge-twilio 0.0.1 → 5.41.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.
- package/README.md +106 -43
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/twilio-bridge.d.ts +54 -0
- package/dist/twilio-bridge.d.ts.map +1 -0
- package/dist/twilio-bridge.js +75 -0
- package/dist/twilio-bridge.js.map +1 -0
- package/dist/twilio-call-sdk.d.ts +167 -0
- package/dist/twilio-call-sdk.d.ts.map +1 -0
- package/dist/twilio-call-sdk.js +156 -0
- package/dist/twilio-call-sdk.js.map +1 -0
- package/package.json +32 -7
package/README.md
CHANGED
|
@@ -1,45 +1,108 @@
|
|
|
1
1
|
# @memberjunction/ai-bridge-twilio
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
3
|
+
The **Twilio** Realtime Bridge driver — the first **telephony** bridge in MemberJunction's Realtime
|
|
4
|
+
Bridges program, and the reference driver the RingCentral and Vonage telephony drivers copy. It connects
|
|
5
|
+
the one realtime agent engine to a **phone call** over **Twilio Programmable Voice + Media Streams**:
|
|
6
|
+
outbound dial, inbound DID routing, single-party audio in/out, DTMF send/receive, and call transfer — all
|
|
7
|
+
behind an injectable telephony call SDK seam so the driver builds and unit-tests with **no network and no
|
|
8
|
+
real Twilio client**.
|
|
9
|
+
|
|
10
|
+
A phone call goes through the *same* transport seam as every meeting bridge — it is just a single-leg
|
|
11
|
+
audio media source. Telephony is **audio only**: no video, no screen, and **no Meeting Controls /
|
|
12
|
+
facilitator surface** (a 1:1 call has no roster to facilitate).
|
|
13
|
+
|
|
14
|
+
See the [Realtime Bridges Guide](../../../../guides/REALTIME_BRIDGES_GUIDE.md) (§ Telephony bridges) and
|
|
15
|
+
[`/plans/realtime/realtime-bridges-architecture.md`](../../../../plans/realtime/realtime-bridges-architecture.md)
|
|
16
|
+
(§8 Twilio capability row, §9 Phase 6) for the full architecture.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @memberjunction/ai-bridge-twilio
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## What it provides
|
|
25
|
+
|
|
26
|
+
- **`TwilioBridge`** — `@RegisterClass(BaseRealtimeBridge, 'TwilioBridge')`. The `MJ: AI Bridge Providers`
|
|
27
|
+
row with `DriverClass = 'TwilioBridge'` resolves to this driver via the `ClassFactory`. It is a thin
|
|
28
|
+
subclass of [`BaseTelephonyBridge`](../../BridgeBase) — all call lifecycle, the audio media seam, DTMF,
|
|
29
|
+
transfer, the caller+agent roster, and capability gating are inherited. The driver only binds the Twilio
|
|
30
|
+
SDK factory.
|
|
31
|
+
- **`TwilioCallSdk`** — the Twilio binding of the platform-agnostic `ITelephonyCallSdk` seam over Twilio
|
|
32
|
+
Programmable Voice + Media Streams. Ships **unbound** (every op throws "bind the real Twilio client")
|
|
33
|
+
until a deployment supplies `ITwilioClientBindings` over the real `twilio` SDK.
|
|
34
|
+
- **`TwilioCallSdkFactory`** — the creation seam that builds a `TwilioCallSdk` from resolved config.
|
|
35
|
+
|
|
36
|
+
## Capability coverage (the Twilio seed row)
|
|
37
|
+
|
|
38
|
+
| Capability | Status |
|
|
39
|
+
|---|---|
|
|
40
|
+
| Outbound dial | ✅ |
|
|
41
|
+
| Inbound DID routing | ✅ |
|
|
42
|
+
| Invite (DID identity) | ✅ |
|
|
43
|
+
| Audio in / out | ✅ |
|
|
44
|
+
| DTMF send / receive | ✅ |
|
|
45
|
+
| Call transfer | ✅ |
|
|
46
|
+
| Video / screen | ➖ n/a — telephony is audio only (`SendMedia` no-ops video/screen out) |
|
|
47
|
+
| Meeting Controls / facilitator | ➖ n/a — a 1:1 call has no roster (`GetMeetingControlsEventSource` → `null`) |
|
|
48
|
+
| Recording | ➖ not enabled on the seed row — `StartRecording` stays capability-gated and throws |
|
|
49
|
+
|
|
50
|
+
Capability gating is two-layer (defense-in-depth): the engine checks the provider's `SupportedFeatures`
|
|
51
|
+
first, and the base re-asserts each flag with `RequireFeature` at the top of its overrides.
|
|
52
|
+
|
|
53
|
+
## Outbound vs inbound
|
|
54
|
+
|
|
55
|
+
The shipped `RealtimeBridgeContext` is a meeting-shaped contract with no `Direction` field, so the engine
|
|
56
|
+
forwards the session's direction (and the agent's caller-id / the inbound call id) into the driver's
|
|
57
|
+
`Configuration`:
|
|
58
|
+
|
|
59
|
+
| Config key | Purpose |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `Direction` | `'Outbound'` (default) → `sdk.dial(to, from)`; `'Inbound'` → `sdk.answer(callId)` |
|
|
62
|
+
| `FromNumber` | the agent's Twilio number / DID outbound calls originate from |
|
|
63
|
+
| `InboundCallId` | the platform Call SID of the inbound call to answer (from the inbound webhook) |
|
|
64
|
+
|
|
65
|
+
`Connect` requires `AudioIn` + `AudioOut`; outbound additionally requires `OutboundDial`, inbound requires
|
|
66
|
+
`InboundRouting`.
|
|
67
|
+
|
|
68
|
+
## The Twilio binding (deployment TODO)
|
|
69
|
+
|
|
70
|
+
`TwilioCallSdk` maps the telephony seam onto Twilio's two halves — the **REST API** (place/modify/hang-up)
|
|
71
|
+
and the **Media Streams** websocket (bidirectional realtime audio + DTMF):
|
|
72
|
+
|
|
73
|
+
| `ITelephonyCallSdk` op | Twilio binding |
|
|
74
|
+
|---|---|
|
|
75
|
+
| `dial` (outbound) | REST `calls.create({ to, from, twiml })` with `<Connect><Stream>` to the Media-Streams websocket → Call SID |
|
|
76
|
+
| `answer` (inbound) | the inbound voice webhook returns `<Connect><Stream>`; accept the Media-Streams websocket for the Call SID |
|
|
77
|
+
| `hangup` | REST `calls(sid).update({ status: 'completed' })` |
|
|
78
|
+
| `sendAudioFrame` | outbound Media-Streams `media` message (the agent's voice) |
|
|
79
|
+
| `onAudioFrame` | inbound Media-Streams `media` events (single remote party) |
|
|
80
|
+
| `sendDtmf` | REST `calls(sid).update` with `<Play digits>` / `<Dial sendDigits>` |
|
|
81
|
+
| `onDtmf` | `<Gather input="dtmf">` webhook results or Media-Streams `dtmf` events |
|
|
82
|
+
| `transfer` | REST `calls(sid).update({ twiml: '<Dial>+1…</Dial>' })` |
|
|
83
|
+
| `onCallEnded` | the `status-callback` webhook (`completed`/`failed`/`canceled`) or the stream `stop` event |
|
|
84
|
+
|
|
85
|
+
Out of the box `TwilioCallSdk` is **unbound** — every operation throws an explicit "bind the real Twilio
|
|
86
|
+
client" error. Bind the real client by supplying an `ITwilioClientBindings` (REST + Media Streams) when
|
|
87
|
+
constructing the SDK; the driver and its tests do not change, and **none of the `twilio` SDK's types leak
|
|
88
|
+
into this package**. Credentials (Account SID, Auth Token, Media-Streams URL) resolve through MJ's
|
|
89
|
+
credential system referenced by the provider `Configuration` — never inline secrets.
|
|
90
|
+
|
|
91
|
+
## Usage (engine-driven)
|
|
92
|
+
|
|
93
|
+
The bridge is not used directly — `AIBridgeEngine.StartBridgeSession` (`@memberjunction/ai-bridge-server`)
|
|
94
|
+
resolves it from the provider's `DriverClass`, forwards the session `Direction` / caller-id into the
|
|
95
|
+
driver config, and wires the transport seam to the injected `IRealtimeSession`. There is no channel host
|
|
96
|
+
to wire for telephony (no Meeting Controls).
|
|
97
|
+
|
|
98
|
+
## Testing
|
|
99
|
+
|
|
100
|
+
`FakeTwilioCallSdk` (in `src/__tests__/`) is an in-memory `ITelephonyCallSdk` with drive helpers and
|
|
101
|
+
capture sinks. The suite covers outbound dial → connect → audio round-trip (in/out), inbound answer, DTMF
|
|
102
|
+
send + receive, transfer (gated), hangup + `onCallEnded`, the single caller+agent roster, capability
|
|
103
|
+
gating (video/screen/Meeting Controls correctly absent), and the unbound-`TwilioCallSdk` bind-me throw —
|
|
104
|
+
all with no network.
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
cd packages/AI/Providers/BridgeTwilio && npm run test
|
|
108
|
+
```
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export * from './twilio-call-sdk.js';
|
|
2
|
+
export * from './twilio-bridge.js';
|
|
3
|
+
import { LoadTwilioBridge } from './twilio-bridge.js';
|
|
4
|
+
// Static reference so bundlers cannot tree-shake the @RegisterClass(BaseRealtimeBridge, 'TwilioBridge')
|
|
5
|
+
// registration. Calling the no-op here keeps the driver resolvable by the engine's ClassFactory.
|
|
6
|
+
LoadTwilioBridge();
|
|
7
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,iBAAiB,CAAC;AAEhC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAEnD,wGAAwG;AACxG,iGAAiG;AACjG,gBAAgB,EAAE,CAAC"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `TwilioBridge` — the first telephony Realtime Bridge driver, connecting the one realtime
|
|
3
|
+
* agent engine to a **phone call over Twilio Programmable Voice + Media Streams**. It is the reference
|
|
4
|
+
* telephony driver the RingCentral + Vonage drivers copy.
|
|
5
|
+
*
|
|
6
|
+
* `TwilioBridge` is intentionally **thin**: nearly all behavior — outbound dial / inbound answer, the
|
|
7
|
+
* single-party audio media seam, DTMF send/receive, call transfer, the trivial caller+agent roster, and
|
|
8
|
+
* the capability gating — is inherited from {@link BaseTelephonyBridge} (`@memberjunction/ai-bridge-base`).
|
|
9
|
+
* The driver supplies only the **Twilio binding**: it points the base's SDK creation seam at the
|
|
10
|
+
* {@link TwilioCallSdkFactory}, which builds a {@link TwilioCallSdk} over the real Twilio client (REST +
|
|
11
|
+
* Media Streams). Until a real Twilio client is bound (a deployment concern), the SDK's operations throw
|
|
12
|
+
* an explicit "bind the real Twilio client" error, so `Connect` fails loudly rather than pretending.
|
|
13
|
+
*
|
|
14
|
+
* Twilio capability coverage (per the §8 seed row): inbound DID routing + outbound dial, audio in/out,
|
|
15
|
+
* DTMF, and call transfer. Telephony is **audio only** — no video, no screen, and no Meeting Controls /
|
|
16
|
+
* facilitator surface (a 1:1 call has no roster to facilitate). Those features are absent from the
|
|
17
|
+
* provider row, and the base's `GetMeetingControlsEventSource` returns `null`.
|
|
18
|
+
*
|
|
19
|
+
* @module @memberjunction/ai-bridge-twilio
|
|
20
|
+
* @author MemberJunction.com
|
|
21
|
+
*/
|
|
22
|
+
import { BaseTelephonyBridge } from '@memberjunction/ai-bridge-base';
|
|
23
|
+
/**
|
|
24
|
+
* The `DriverClass` key {@link TwilioBridge} registers under. A `MJ: AI Bridge Providers` row with
|
|
25
|
+
* `DriverClass = 'TwilioBridge'` resolves to this driver via the `ClassFactory`.
|
|
26
|
+
*/
|
|
27
|
+
export declare const TWILIO_BRIDGE_DRIVER_CLASS = "TwilioBridge";
|
|
28
|
+
/**
|
|
29
|
+
* Realtime Bridge driver for **Twilio** telephony (Programmable Voice + Media Streams).
|
|
30
|
+
*
|
|
31
|
+
* Construct the driver with the default constructor (the engine's `ClassFactory` path); it binds the
|
|
32
|
+
* {@link TwilioCallSdkFactory} so a real deployment only needs to wire the Twilio client behind it (or a
|
|
33
|
+
* test injects a `FakeTwilioCallSdk` via {@link BaseTelephonyBridge.SetSdkFactory}). Everything else is
|
|
34
|
+
* inherited from {@link BaseTelephonyBridge}.
|
|
35
|
+
*
|
|
36
|
+
* Registered via `@RegisterClass(BaseRealtimeBridge, 'TwilioBridge')` — the engine resolves any bridge
|
|
37
|
+
* driver against the `BaseRealtimeBridge` root, so a telephony driver registers under that same root.
|
|
38
|
+
*/
|
|
39
|
+
export declare class TwilioBridge extends BaseTelephonyBridge {
|
|
40
|
+
/**
|
|
41
|
+
* Wires the Twilio SDK factory onto the telephony base. The factory builds a {@link TwilioCallSdk}
|
|
42
|
+
* which — until a real Twilio client is bound — throws an explicit "bind the real Twilio client"
|
|
43
|
+
* error from every operation, so `Connect` fails loudly. A deployment binds the real client behind
|
|
44
|
+
* the factory; tests override it via {@link BaseTelephonyBridge.SetSdkFactory}.
|
|
45
|
+
*/
|
|
46
|
+
constructor();
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Tree-shaking-prevention loader. Modern bundlers cannot see the `@RegisterClass` dynamic registration
|
|
50
|
+
* of {@link TwilioBridge} and may eliminate it. Import and call this no-op from a static code path (the
|
|
51
|
+
* package entry point does) so the `ClassFactory` can resolve `'TwilioBridge'`.
|
|
52
|
+
*/
|
|
53
|
+
export declare function LoadTwilioBridge(): void;
|
|
54
|
+
//# sourceMappingURL=twilio-bridge.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twilio-bridge.d.ts","sourceRoot":"","sources":["../src/twilio-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,EAAsB,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAGzF;;;GAGG;AACH,eAAO,MAAM,0BAA0B,iBAAiB,CAAC;AAEzD;;;;;;;;;;GAUG;AACH,qBACa,YAAa,SAAQ,mBAAmB;IACjD;;;;;OAKG;;CAKN;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,IAAI,CAEvC"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `TwilioBridge` — the first telephony Realtime Bridge driver, connecting the one realtime
|
|
3
|
+
* agent engine to a **phone call over Twilio Programmable Voice + Media Streams**. It is the reference
|
|
4
|
+
* telephony driver the RingCentral + Vonage drivers copy.
|
|
5
|
+
*
|
|
6
|
+
* `TwilioBridge` is intentionally **thin**: nearly all behavior — outbound dial / inbound answer, the
|
|
7
|
+
* single-party audio media seam, DTMF send/receive, call transfer, the trivial caller+agent roster, and
|
|
8
|
+
* the capability gating — is inherited from {@link BaseTelephonyBridge} (`@memberjunction/ai-bridge-base`).
|
|
9
|
+
* The driver supplies only the **Twilio binding**: it points the base's SDK creation seam at the
|
|
10
|
+
* {@link TwilioCallSdkFactory}, which builds a {@link TwilioCallSdk} over the real Twilio client (REST +
|
|
11
|
+
* Media Streams). Until a real Twilio client is bound (a deployment concern), the SDK's operations throw
|
|
12
|
+
* an explicit "bind the real Twilio client" error, so `Connect` fails loudly rather than pretending.
|
|
13
|
+
*
|
|
14
|
+
* Twilio capability coverage (per the §8 seed row): inbound DID routing + outbound dial, audio in/out,
|
|
15
|
+
* DTMF, and call transfer. Telephony is **audio only** — no video, no screen, and no Meeting Controls /
|
|
16
|
+
* facilitator surface (a 1:1 call has no roster to facilitate). Those features are absent from the
|
|
17
|
+
* provider row, and the base's `GetMeetingControlsEventSource` returns `null`.
|
|
18
|
+
*
|
|
19
|
+
* @module @memberjunction/ai-bridge-twilio
|
|
20
|
+
* @author MemberJunction.com
|
|
21
|
+
*/
|
|
22
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
23
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
24
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
25
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
26
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
27
|
+
};
|
|
28
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
29
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
30
|
+
};
|
|
31
|
+
import { RegisterClass } from '@memberjunction/global';
|
|
32
|
+
import { BaseRealtimeBridge, BaseTelephonyBridge } from '@memberjunction/ai-bridge-base';
|
|
33
|
+
import { TwilioCallSdkFactory } from './twilio-call-sdk.js';
|
|
34
|
+
/**
|
|
35
|
+
* The `DriverClass` key {@link TwilioBridge} registers under. A `MJ: AI Bridge Providers` row with
|
|
36
|
+
* `DriverClass = 'TwilioBridge'` resolves to this driver via the `ClassFactory`.
|
|
37
|
+
*/
|
|
38
|
+
export const TWILIO_BRIDGE_DRIVER_CLASS = 'TwilioBridge';
|
|
39
|
+
/**
|
|
40
|
+
* Realtime Bridge driver for **Twilio** telephony (Programmable Voice + Media Streams).
|
|
41
|
+
*
|
|
42
|
+
* Construct the driver with the default constructor (the engine's `ClassFactory` path); it binds the
|
|
43
|
+
* {@link TwilioCallSdkFactory} so a real deployment only needs to wire the Twilio client behind it (or a
|
|
44
|
+
* test injects a `FakeTwilioCallSdk` via {@link BaseTelephonyBridge.SetSdkFactory}). Everything else is
|
|
45
|
+
* inherited from {@link BaseTelephonyBridge}.
|
|
46
|
+
*
|
|
47
|
+
* Registered via `@RegisterClass(BaseRealtimeBridge, 'TwilioBridge')` — the engine resolves any bridge
|
|
48
|
+
* driver against the `BaseRealtimeBridge` root, so a telephony driver registers under that same root.
|
|
49
|
+
*/
|
|
50
|
+
let TwilioBridge = class TwilioBridge extends BaseTelephonyBridge {
|
|
51
|
+
/**
|
|
52
|
+
* Wires the Twilio SDK factory onto the telephony base. The factory builds a {@link TwilioCallSdk}
|
|
53
|
+
* which — until a real Twilio client is bound — throws an explicit "bind the real Twilio client"
|
|
54
|
+
* error from every operation, so `Connect` fails loudly. A deployment binds the real client behind
|
|
55
|
+
* the factory; tests override it via {@link BaseTelephonyBridge.SetSdkFactory}.
|
|
56
|
+
*/
|
|
57
|
+
constructor() {
|
|
58
|
+
super();
|
|
59
|
+
this.SetSdkFactory(TwilioCallSdkFactory);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
TwilioBridge = __decorate([
|
|
63
|
+
RegisterClass(BaseRealtimeBridge, TWILIO_BRIDGE_DRIVER_CLASS),
|
|
64
|
+
__metadata("design:paramtypes", [])
|
|
65
|
+
], TwilioBridge);
|
|
66
|
+
export { TwilioBridge };
|
|
67
|
+
/**
|
|
68
|
+
* Tree-shaking-prevention loader. Modern bundlers cannot see the `@RegisterClass` dynamic registration
|
|
69
|
+
* of {@link TwilioBridge} and may eliminate it. Import and call this no-op from a static code path (the
|
|
70
|
+
* package entry point does) so the `ClassFactory` can resolve `'TwilioBridge'`.
|
|
71
|
+
*/
|
|
72
|
+
export function LoadTwilioBridge() {
|
|
73
|
+
// Intentionally empty — referencing the module is what prevents tree-shaking.
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=twilio-bridge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twilio-bridge.js","sourceRoot":"","sources":["../src/twilio-bridge.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;;;;;;;;;;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAC;AACvD,OAAO,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACzF,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAEzD;;;GAGG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG,cAAc,CAAC;AAEzD;;;;;;;;;;GAUG;AAEI,IAAM,YAAY,GAAlB,MAAM,YAAa,SAAQ,mBAAmB;IACjD;;;;;OAKG;IACH;QACI,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC;IAC7C,CAAC;CACJ,CAAA;AAXY,YAAY;IADxB,aAAa,CAAC,kBAAkB,EAAE,0BAA0B,CAAC;;GACjD,YAAY,CAWxB;;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB;IAC5B,8EAA8E;AAClF,CAAC"}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The **Twilio** binding of the telephony call SDK seam — `TwilioCallSdk` +
|
|
3
|
+
* `TwilioCallSdkFactory`.
|
|
4
|
+
*
|
|
5
|
+
* {@link TwilioCallSdk} implements the platform-agnostic {@link ITelephonyCallSdk} (from
|
|
6
|
+
* `@memberjunction/ai-bridge-base`) over **Twilio Programmable Voice + Media Streams**, so
|
|
7
|
+
* {@link import('./twilio-bridge.js').TwilioBridge} drives a real phone call without ever importing the
|
|
8
|
+
* Twilio SDK directly. As shipped, every operation throws an explicit **"bind the real Twilio client"**
|
|
9
|
+
* error — the real Twilio binding is a documented deployment seam (below). Production supplies the bound
|
|
10
|
+
* client via {@link TwilioCallSdk}'s constructor / the factory; tests inject a `FakeTwilioCallSdk`.
|
|
11
|
+
*
|
|
12
|
+
* ## How the Twilio operations map (production binding — deployment TODO)
|
|
13
|
+
* Twilio has two halves: a **REST API** (place/modify/hang-up calls) and a **Media Streams** websocket
|
|
14
|
+
* (bidirectional realtime audio + DTMF events). This SDK adapter sits over both:
|
|
15
|
+
*
|
|
16
|
+
* | {@link ITelephonyCallSdk} op | Twilio binding |
|
|
17
|
+
* |---|---|
|
|
18
|
+
* | {@link TwilioCallSdk.dial} (outbound) | REST `client.calls.create({ to, from, twiml })` where the TwiML opens a bidirectional `<Connect><Stream>` to the Media-Streams websocket. Returns the Call SID. |
|
|
19
|
+
* | {@link TwilioCallSdk.answer} (inbound) | The inbound webhook (a call to the agent's DID) returns TwiML with `<Connect><Stream>`; "answering" = accepting that Media-Streams websocket for the delivered Call SID. |
|
|
20
|
+
* | {@link TwilioCallSdk.hangup} | REST `client.calls(callSid).update({ status: 'completed' })`. |
|
|
21
|
+
* | {@link TwilioCallSdk.sendAudioFrame} | A Media-Streams **outbound `media`** message (base64 μ-law/PCM payload) on the call's websocket — the agent's voice. |
|
|
22
|
+
* | {@link TwilioCallSdk.onAudioFrame} | Inbound Media-Streams **`media`** events on the websocket — what the agent hears (single remote party). |
|
|
23
|
+
* | {@link TwilioCallSdk.sendDtmf} | REST `client.calls(callSid).update({ twiml: '<Play digits="...">' })` (or a `<Dial>` `sendDigits`). |
|
|
24
|
+
* | {@link TwilioCallSdk.onDtmf} | `<Gather input="dtmf">` results delivered to the voice webhook, OR Media-Streams DTMF `dtmf` events. |
|
|
25
|
+
* | {@link TwilioCallSdk.transfer} | REST `client.calls(callSid).update({ twiml: '<Dial>+1...</Dial>' })` redirecting the live call. |
|
|
26
|
+
* | {@link TwilioCallSdk.onCallEnded} | The `status-callback` webhook for `completed` / `failed` / `canceled`, OR the Media-Streams `stop` event. |
|
|
27
|
+
*
|
|
28
|
+
* Binding the real Twilio client is a thin adapter that supplies an {@link ITwilioClientBindings}
|
|
29
|
+
* implementation (REST + websocket) to {@link TwilioCallSdk}; the driver and its tests do not change.
|
|
30
|
+
* **None of the `twilio` SDK's types leak into this package.**
|
|
31
|
+
*
|
|
32
|
+
* Credentials (Account SID, Auth Token, the Media-Streams websocket URL) resolve through MJ's credential
|
|
33
|
+
* system referenced by the provider `Configuration` — never inline secrets.
|
|
34
|
+
*
|
|
35
|
+
* @module @memberjunction/ai-bridge-twilio
|
|
36
|
+
* @see {@link ITelephonyCallSdk} — the platform-agnostic seam this binds.
|
|
37
|
+
* @see `/plans/realtime/realtime-bridges-architecture.md` §8 (Twilio capability row) and §9 Phase 6.
|
|
38
|
+
*/
|
|
39
|
+
import { ITelephonyCallSdk } from '@memberjunction/ai-bridge-base';
|
|
40
|
+
/**
|
|
41
|
+
* The minimal Twilio client surface {@link TwilioCallSdk} drives. A production deployment implements
|
|
42
|
+
* this over the real `twilio` SDK (REST) + a Media-Streams websocket handler; this package never imports
|
|
43
|
+
* `twilio` so it builds and tests with no network. When no bindings are supplied, {@link TwilioCallSdk}
|
|
44
|
+
* throws the explicit "bind the real Twilio client" error from every operation.
|
|
45
|
+
*
|
|
46
|
+
* The shapes are intentionally tiny and provider-neutral at the value level (strings / byte buffers) so
|
|
47
|
+
* the Twilio SDK's types do not leak into the bridge package.
|
|
48
|
+
*/
|
|
49
|
+
export interface ITwilioClientBindings {
|
|
50
|
+
/**
|
|
51
|
+
* Places an outbound call via the Twilio REST API (`calls.create`) with TwiML that opens a
|
|
52
|
+
* bidirectional Media-Streams `<Connect><Stream>` to the agent's websocket.
|
|
53
|
+
*
|
|
54
|
+
* @param toNumber Destination number (E.164).
|
|
55
|
+
* @param fromNumber The agent's Twilio number / verified caller-id the call originates from.
|
|
56
|
+
* @param args Provider-specific options (status-callback URL, stream URL, recording flags, …).
|
|
57
|
+
* @returns The created Call SID.
|
|
58
|
+
*/
|
|
59
|
+
createCall(toNumber: string, fromNumber: string, args?: Record<string, unknown>): Promise<string>;
|
|
60
|
+
/**
|
|
61
|
+
* Accepts the Media-Streams websocket for an inbound call already delivered by the voice webhook.
|
|
62
|
+
*
|
|
63
|
+
* @param callSid The inbound Call SID from the webhook.
|
|
64
|
+
*/
|
|
65
|
+
acceptInbound(callSid: string): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Ends the call (REST `calls(sid).update({ status: 'completed' })`).
|
|
68
|
+
*
|
|
69
|
+
* @param callSid The Call SID to complete.
|
|
70
|
+
*/
|
|
71
|
+
completeCall(callSid: string): Promise<void>;
|
|
72
|
+
/**
|
|
73
|
+
* Pushes one outbound audio payload onto the call's Media-Streams websocket (the agent's voice).
|
|
74
|
+
*
|
|
75
|
+
* @param callSid The Call SID whose stream to write to.
|
|
76
|
+
* @param pcm The audio bytes (the adapter encodes to the Media-Streams μ-law/PCM `media` frame).
|
|
77
|
+
*/
|
|
78
|
+
pushStreamAudio(callSid: string, pcm: ArrayBuffer): void;
|
|
79
|
+
/**
|
|
80
|
+
* Registers the inbound Media-Streams audio callback for the call (what the agent hears).
|
|
81
|
+
*
|
|
82
|
+
* @param callSid The Call SID whose inbound stream to subscribe to.
|
|
83
|
+
* @param cb Invoked with each inbound PCM audio frame.
|
|
84
|
+
*/
|
|
85
|
+
onStreamAudio(callSid: string, cb: (pcm: ArrayBuffer) => void): void;
|
|
86
|
+
/**
|
|
87
|
+
* Sends DTMF digits on the call (REST `calls(sid).update` with `<Play digits>` / `<Dial sendDigits>`).
|
|
88
|
+
*
|
|
89
|
+
* @param callSid The Call SID.
|
|
90
|
+
* @param digits The DTMF digit string.
|
|
91
|
+
*/
|
|
92
|
+
playDigits(callSid: string, digits: string): Promise<void>;
|
|
93
|
+
/**
|
|
94
|
+
* Registers the inbound DTMF callback (`<Gather>` webhook results or Media-Streams `dtmf` events).
|
|
95
|
+
*
|
|
96
|
+
* @param callSid The Call SID.
|
|
97
|
+
* @param cb Invoked with each received DTMF digit string.
|
|
98
|
+
*/
|
|
99
|
+
onDigits(callSid: string, cb: (digits: string) => void): void;
|
|
100
|
+
/**
|
|
101
|
+
* Transfers the live call (REST `calls(sid).update({ twiml: '<Dial>...' })`).
|
|
102
|
+
*
|
|
103
|
+
* @param callSid The Call SID to redirect.
|
|
104
|
+
* @param toNumber The transfer destination.
|
|
105
|
+
*/
|
|
106
|
+
redirectCall(callSid: string, toNumber: string): Promise<void>;
|
|
107
|
+
/**
|
|
108
|
+
* Registers the call-ended callback (status-callback `completed`/`failed`/`canceled` or stream `stop`).
|
|
109
|
+
*
|
|
110
|
+
* @param callSid The Call SID.
|
|
111
|
+
* @param cb Invoked when the call ends.
|
|
112
|
+
*/
|
|
113
|
+
onCallStatus(callSid: string, cb: () => void): void;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Twilio's implementation of the {@link ITelephonyCallSdk} telephony seam over **Twilio Programmable
|
|
117
|
+
* Voice + Media Streams**. {@link import('./twilio-bridge.js').TwilioBridge} drives this; production
|
|
118
|
+
* constructs it with real {@link ITwilioClientBindings}, tests inject a `FakeTwilioCallSdk` directly
|
|
119
|
+
* (no need for this adapter at all in tests).
|
|
120
|
+
*
|
|
121
|
+
* The adapter tracks the active Call SID so the per-call websocket / REST operations can be addressed.
|
|
122
|
+
* It never imports the `twilio` SDK — all Twilio specifics live behind {@link ITwilioClientBindings}.
|
|
123
|
+
*/
|
|
124
|
+
export declare class TwilioCallSdk implements ITelephonyCallSdk {
|
|
125
|
+
private readonly bindings;
|
|
126
|
+
/** The active call's SID once dialled/answered, so per-call operations can address it. */
|
|
127
|
+
private activeCallSid;
|
|
128
|
+
private audioCb?;
|
|
129
|
+
private dtmfCb?;
|
|
130
|
+
private endedCb?;
|
|
131
|
+
/**
|
|
132
|
+
* @param bindings The real Twilio client bindings (REST + Media Streams). Defaults to an unbound
|
|
133
|
+
* implementation whose every method throws the explicit "bind the real Twilio client" error.
|
|
134
|
+
*/
|
|
135
|
+
constructor(bindings?: ITwilioClientBindings);
|
|
136
|
+
/** @inheritdoc */
|
|
137
|
+
dial(toNumber: string, fromNumber: string, args?: Record<string, unknown>): Promise<string>;
|
|
138
|
+
/** @inheritdoc */
|
|
139
|
+
answer(callId: string): Promise<void>;
|
|
140
|
+
/** @inheritdoc */
|
|
141
|
+
hangup(callId: string): Promise<void>;
|
|
142
|
+
/** @inheritdoc */
|
|
143
|
+
sendAudioFrame(pcm: ArrayBuffer): void;
|
|
144
|
+
/** @inheritdoc */
|
|
145
|
+
onAudioFrame(cb: (pcm: ArrayBuffer) => void): void;
|
|
146
|
+
/** @inheritdoc */
|
|
147
|
+
sendDtmf(digits: string): Promise<void>;
|
|
148
|
+
/** @inheritdoc */
|
|
149
|
+
onDtmf(cb: (digits: string) => void): void;
|
|
150
|
+
/** @inheritdoc */
|
|
151
|
+
transfer(callId: string, toNumber: string): Promise<void>;
|
|
152
|
+
/** @inheritdoc */
|
|
153
|
+
onCallEnded(cb: () => void): void;
|
|
154
|
+
/** Binds the active Call SID and (re-)registers any handlers the driver set before the call existed. */
|
|
155
|
+
private bindCall;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Builds a {@link TwilioCallSdk} for a session — the Twilio creation seam. Production supplies a factory
|
|
159
|
+
* that constructs the SDK with real {@link ITwilioClientBindings} (built from the resolved provider
|
|
160
|
+
* `Configuration` — Account SID, Auth Token, Media-Streams URL, all resolved upstream). Out of the box
|
|
161
|
+
* this returns an **unbound** `TwilioCallSdk` whose operations throw until the real client is bound.
|
|
162
|
+
*
|
|
163
|
+
* @param _config The resolved provider/session configuration (credential refs already resolved upstream).
|
|
164
|
+
* @returns An (unbound) {@link TwilioCallSdk}. Override in deployment / inject a fake in tests.
|
|
165
|
+
*/
|
|
166
|
+
export declare const TwilioCallSdkFactory: (_config?: Record<string, unknown>) => TwilioCallSdk;
|
|
167
|
+
//# sourceMappingURL=twilio-call-sdk.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twilio-call-sdk.d.ts","sourceRoot":"","sources":["../src/twilio-call-sdk.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview The **Twilio** binding of the telephony call SDK seam — `TwilioCallSdk` +
|
|
3
|
+
* `TwilioCallSdkFactory`.
|
|
4
|
+
*
|
|
5
|
+
* {@link TwilioCallSdk} implements the platform-agnostic {@link ITelephonyCallSdk} (from
|
|
6
|
+
* `@memberjunction/ai-bridge-base`) over **Twilio Programmable Voice + Media Streams**, so
|
|
7
|
+
* {@link import('./twilio-bridge.js').TwilioBridge} drives a real phone call without ever importing the
|
|
8
|
+
* Twilio SDK directly. As shipped, every operation throws an explicit **"bind the real Twilio client"**
|
|
9
|
+
* error — the real Twilio binding is a documented deployment seam (below). Production supplies the bound
|
|
10
|
+
* client via {@link TwilioCallSdk}'s constructor / the factory; tests inject a `FakeTwilioCallSdk`.
|
|
11
|
+
*
|
|
12
|
+
* ## How the Twilio operations map (production binding — deployment TODO)
|
|
13
|
+
* Twilio has two halves: a **REST API** (place/modify/hang-up calls) and a **Media Streams** websocket
|
|
14
|
+
* (bidirectional realtime audio + DTMF events). This SDK adapter sits over both:
|
|
15
|
+
*
|
|
16
|
+
* | {@link ITelephonyCallSdk} op | Twilio binding |
|
|
17
|
+
* |---|---|
|
|
18
|
+
* | {@link TwilioCallSdk.dial} (outbound) | REST `client.calls.create({ to, from, twiml })` where the TwiML opens a bidirectional `<Connect><Stream>` to the Media-Streams websocket. Returns the Call SID. |
|
|
19
|
+
* | {@link TwilioCallSdk.answer} (inbound) | The inbound webhook (a call to the agent's DID) returns TwiML with `<Connect><Stream>`; "answering" = accepting that Media-Streams websocket for the delivered Call SID. |
|
|
20
|
+
* | {@link TwilioCallSdk.hangup} | REST `client.calls(callSid).update({ status: 'completed' })`. |
|
|
21
|
+
* | {@link TwilioCallSdk.sendAudioFrame} | A Media-Streams **outbound `media`** message (base64 μ-law/PCM payload) on the call's websocket — the agent's voice. |
|
|
22
|
+
* | {@link TwilioCallSdk.onAudioFrame} | Inbound Media-Streams **`media`** events on the websocket — what the agent hears (single remote party). |
|
|
23
|
+
* | {@link TwilioCallSdk.sendDtmf} | REST `client.calls(callSid).update({ twiml: '<Play digits="...">' })` (or a `<Dial>` `sendDigits`). |
|
|
24
|
+
* | {@link TwilioCallSdk.onDtmf} | `<Gather input="dtmf">` results delivered to the voice webhook, OR Media-Streams DTMF `dtmf` events. |
|
|
25
|
+
* | {@link TwilioCallSdk.transfer} | REST `client.calls(callSid).update({ twiml: '<Dial>+1...</Dial>' })` redirecting the live call. |
|
|
26
|
+
* | {@link TwilioCallSdk.onCallEnded} | The `status-callback` webhook for `completed` / `failed` / `canceled`, OR the Media-Streams `stop` event. |
|
|
27
|
+
*
|
|
28
|
+
* Binding the real Twilio client is a thin adapter that supplies an {@link ITwilioClientBindings}
|
|
29
|
+
* implementation (REST + websocket) to {@link TwilioCallSdk}; the driver and its tests do not change.
|
|
30
|
+
* **None of the `twilio` SDK's types leak into this package.**
|
|
31
|
+
*
|
|
32
|
+
* Credentials (Account SID, Auth Token, the Media-Streams websocket URL) resolve through MJ's credential
|
|
33
|
+
* system referenced by the provider `Configuration` — never inline secrets.
|
|
34
|
+
*
|
|
35
|
+
* @module @memberjunction/ai-bridge-twilio
|
|
36
|
+
* @see {@link ITelephonyCallSdk} — the platform-agnostic seam this binds.
|
|
37
|
+
* @see `/plans/realtime/realtime-bridges-architecture.md` §8 (Twilio capability row) and §9 Phase 6.
|
|
38
|
+
*/
|
|
39
|
+
/** The default bindings used when none are supplied — every operation throws the bind-me error. */
|
|
40
|
+
const UNBOUND_BINDINGS = {
|
|
41
|
+
createCall: () => throwUnbound('createCall (outbound dial)'),
|
|
42
|
+
acceptInbound: () => throwUnbound('acceptInbound (inbound answer)'),
|
|
43
|
+
completeCall: () => throwUnbound('completeCall (hangup)'),
|
|
44
|
+
pushStreamAudio: () => throwUnboundVoid('pushStreamAudio (outbound media)'),
|
|
45
|
+
onStreamAudio: () => throwUnboundVoid('onStreamAudio (inbound media)'),
|
|
46
|
+
playDigits: () => throwUnbound('playDigits (send DTMF)'),
|
|
47
|
+
onDigits: () => throwUnboundVoid('onDigits (receive DTMF)'),
|
|
48
|
+
redirectCall: () => throwUnbound('redirectCall (transfer)'),
|
|
49
|
+
onCallStatus: () => throwUnboundVoid('onCallStatus (call ended)'),
|
|
50
|
+
};
|
|
51
|
+
function throwUnbound(op) {
|
|
52
|
+
throw new Error(`TwilioCallSdk has no real Twilio client bound (operation '${op}'). Construct it with an ` +
|
|
53
|
+
'ITwilioClientBindings over the real twilio SDK (REST + Media Streams), or inject a fake in tests.');
|
|
54
|
+
}
|
|
55
|
+
function throwUnboundVoid(op) {
|
|
56
|
+
throwUnbound(op);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Twilio's implementation of the {@link ITelephonyCallSdk} telephony seam over **Twilio Programmable
|
|
60
|
+
* Voice + Media Streams**. {@link import('./twilio-bridge.js').TwilioBridge} drives this; production
|
|
61
|
+
* constructs it with real {@link ITwilioClientBindings}, tests inject a `FakeTwilioCallSdk` directly
|
|
62
|
+
* (no need for this adapter at all in tests).
|
|
63
|
+
*
|
|
64
|
+
* The adapter tracks the active Call SID so the per-call websocket / REST operations can be addressed.
|
|
65
|
+
* It never imports the `twilio` SDK — all Twilio specifics live behind {@link ITwilioClientBindings}.
|
|
66
|
+
*/
|
|
67
|
+
export class TwilioCallSdk {
|
|
68
|
+
/**
|
|
69
|
+
* @param bindings The real Twilio client bindings (REST + Media Streams). Defaults to an unbound
|
|
70
|
+
* implementation whose every method throws the explicit "bind the real Twilio client" error.
|
|
71
|
+
*/
|
|
72
|
+
constructor(bindings = UNBOUND_BINDINGS) {
|
|
73
|
+
this.bindings = bindings;
|
|
74
|
+
/** The active call's SID once dialled/answered, so per-call operations can address it. */
|
|
75
|
+
this.activeCallSid = null;
|
|
76
|
+
}
|
|
77
|
+
/** @inheritdoc */
|
|
78
|
+
async dial(toNumber, fromNumber, args) {
|
|
79
|
+
const sid = await this.bindings.createCall(toNumber, fromNumber, args);
|
|
80
|
+
this.bindCall(sid);
|
|
81
|
+
return sid;
|
|
82
|
+
}
|
|
83
|
+
/** @inheritdoc */
|
|
84
|
+
async answer(callId) {
|
|
85
|
+
await this.bindings.acceptInbound(callId);
|
|
86
|
+
this.bindCall(callId);
|
|
87
|
+
}
|
|
88
|
+
/** @inheritdoc */
|
|
89
|
+
async hangup(callId) {
|
|
90
|
+
await this.bindings.completeCall(callId);
|
|
91
|
+
this.activeCallSid = null;
|
|
92
|
+
}
|
|
93
|
+
/** @inheritdoc */
|
|
94
|
+
sendAudioFrame(pcm) {
|
|
95
|
+
if (this.activeCallSid) {
|
|
96
|
+
this.bindings.pushStreamAudio(this.activeCallSid, pcm);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/** @inheritdoc */
|
|
100
|
+
onAudioFrame(cb) {
|
|
101
|
+
this.audioCb = cb;
|
|
102
|
+
if (this.activeCallSid) {
|
|
103
|
+
this.bindings.onStreamAudio(this.activeCallSid, cb);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/** @inheritdoc */
|
|
107
|
+
async sendDtmf(digits) {
|
|
108
|
+
if (this.activeCallSid) {
|
|
109
|
+
await this.bindings.playDigits(this.activeCallSid, digits);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/** @inheritdoc */
|
|
113
|
+
onDtmf(cb) {
|
|
114
|
+
this.dtmfCb = cb;
|
|
115
|
+
if (this.activeCallSid) {
|
|
116
|
+
this.bindings.onDigits(this.activeCallSid, cb);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/** @inheritdoc */
|
|
120
|
+
async transfer(callId, toNumber) {
|
|
121
|
+
await this.bindings.redirectCall(callId, toNumber);
|
|
122
|
+
}
|
|
123
|
+
/** @inheritdoc */
|
|
124
|
+
onCallEnded(cb) {
|
|
125
|
+
this.endedCb = cb;
|
|
126
|
+
if (this.activeCallSid) {
|
|
127
|
+
this.bindings.onCallStatus(this.activeCallSid, cb);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/** Binds the active Call SID and (re-)registers any handlers the driver set before the call existed. */
|
|
131
|
+
bindCall(callSid) {
|
|
132
|
+
this.activeCallSid = callSid;
|
|
133
|
+
if (this.audioCb) {
|
|
134
|
+
this.bindings.onStreamAudio(callSid, this.audioCb);
|
|
135
|
+
}
|
|
136
|
+
if (this.dtmfCb) {
|
|
137
|
+
this.bindings.onDigits(callSid, this.dtmfCb);
|
|
138
|
+
}
|
|
139
|
+
if (this.endedCb) {
|
|
140
|
+
this.bindings.onCallStatus(callSid, this.endedCb);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Builds a {@link TwilioCallSdk} for a session — the Twilio creation seam. Production supplies a factory
|
|
146
|
+
* that constructs the SDK with real {@link ITwilioClientBindings} (built from the resolved provider
|
|
147
|
+
* `Configuration` — Account SID, Auth Token, Media-Streams URL, all resolved upstream). Out of the box
|
|
148
|
+
* this returns an **unbound** `TwilioCallSdk` whose operations throw until the real client is bound.
|
|
149
|
+
*
|
|
150
|
+
* @param _config The resolved provider/session configuration (credential refs already resolved upstream).
|
|
151
|
+
* @returns An (unbound) {@link TwilioCallSdk}. Override in deployment / inject a fake in tests.
|
|
152
|
+
*/
|
|
153
|
+
export const TwilioCallSdkFactory = (_config) => {
|
|
154
|
+
return new TwilioCallSdk();
|
|
155
|
+
};
|
|
156
|
+
//# sourceMappingURL=twilio-call-sdk.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twilio-call-sdk.js","sourceRoot":"","sources":["../src/twilio-call-sdk.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,10 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ai-bridge-twilio",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "5.41.0",
|
|
5
|
+
"description": "MemberJunction: Twilio Realtime Bridge driver (telephony). Connects the realtime agent engine to a phone call over Twilio Programmable Voice + Media Streams — outbound dial, inbound DID routing, single-party audio in/out, DTMF send/receive, and call transfer — via an injectable telephony call SDK seam. Audio only: no video, no screen, no Meeting Controls.",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"/dist"
|
|
10
|
+
],
|
|
11
|
+
"scripts": {
|
|
12
|
+
"start": "ts-node-dev src/index.ts",
|
|
13
|
+
"build": "tsc && tsc-alias -f",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest"
|
|
16
|
+
},
|
|
17
|
+
"author": "MemberJunction.com",
|
|
18
|
+
"license": "ISC",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"@memberjunction/core": "5.41.0",
|
|
21
|
+
"@memberjunction/global": "5.41.0",
|
|
22
|
+
"@memberjunction/core-entities": "5.41.0",
|
|
23
|
+
"@memberjunction/ai-bridge-base": "5.41.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/node": "24.10.11",
|
|
27
|
+
"ts-node-dev": "^2.0.0",
|
|
28
|
+
"typescript": "^5.9.3",
|
|
29
|
+
"vitest": "^4.0.18"
|
|
30
|
+
},
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/MemberJunction/MJ"
|
|
34
|
+
}
|
|
10
35
|
}
|