@memberjunction/ai-bridge-twilio 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/README.md +27 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/real-twilio-bindings.d.ts +174 -0
- package/dist/real-twilio-bindings.d.ts.map +1 -0
- package/dist/real-twilio-bindings.js +208 -0
- package/dist/real-twilio-bindings.js.map +1 -0
- package/dist/twilio-ingress.d.ts +73 -0
- package/dist/twilio-ingress.d.ts.map +1 -0
- package/dist/twilio-ingress.js +102 -0
- package/dist/twilio-ingress.js.map +1 -0
- package/dist/twilio-rest-client.d.ts +93 -0
- package/dist/twilio-rest-client.d.ts.map +1 -0
- package/dist/twilio-rest-client.js +108 -0
- package/dist/twilio-rest-client.js.map +1 -0
- package/package.json +9 -5
package/README.md
CHANGED
|
@@ -106,3 +106,30 @@ all with no network.
|
|
|
106
106
|
```bash
|
|
107
107
|
cd packages/AI/Providers/BridgeTwilio && npm run test
|
|
108
108
|
```
|
|
109
|
+
|
|
110
|
+
### Production REST wiring (`RealTwilioRestClient`)
|
|
111
|
+
|
|
112
|
+
`RealTwilioRestClient` is the production `ITwilioRestLike` over the real `twilio` SDK (an
|
|
113
|
+
`optionalDependency`, lazily loaded — the package still builds/tests with no `twilio` install). It maps
|
|
114
|
+
`CreateCall` → `client.calls.create({ to, from, twiml, statusCallback })` and `UpdateCall` →
|
|
115
|
+
`client.calls(sid).update({ status, twiml })`. Construct it with credentials resolved from MJ config
|
|
116
|
+
(API-key pair preferred over the account auth token); pass it as the `Rest` half of `RealTwilioBindings`
|
|
117
|
+
(the `MediaPump` half is the Media-Streams websocket, owned by the MJAPI telephony ingress). Unit-tested
|
|
118
|
+
via an injected fake `twilio` factory — no network.
|
|
119
|
+
|
|
120
|
+
### Credential-gated integration test (NOT in CI)
|
|
121
|
+
|
|
122
|
+
`real-twilio-bindings.integration.test.ts` places a real outbound call and ends it. It is `skipIf`-gated
|
|
123
|
+
on these env vars and never runs in CI (it spends real money + dials real PSTN):
|
|
124
|
+
|
|
125
|
+
| Env var | Purpose |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `TWILIO_TEST_ACCOUNT_SID` | Twilio Account SID (`AC…`). |
|
|
128
|
+
| `TWILIO_TEST_AUTH_TOKEN` | Account auth token. |
|
|
129
|
+
| `TWILIO_TEST_FROM` | A Twilio number you own (caller-id). |
|
|
130
|
+
| `TWILIO_TEST_TO` | A capped-spend test number that auto-answers. |
|
|
131
|
+
| `TWILIO_TEST_STREAM_URL` | (optional) the `wss://…/telephony/twilio/media` URL for the `<Connect><Stream>`. |
|
|
132
|
+
|
|
133
|
+
Run locally: `npm run test -- real-twilio-bindings.integration`. The full media round-trip (audio through
|
|
134
|
+
the agent) needs the MJAPI ingress + a publicly reachable stream URL — see the T1 ingress notes + manual
|
|
135
|
+
runbook.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export * from './twilio-call-sdk.js';
|
|
2
2
|
export * from './twilio-native-call-sdk.js';
|
|
3
|
+
export * from './real-twilio-bindings.js';
|
|
4
|
+
export * from './twilio-rest-client.js';
|
|
5
|
+
export * from './twilio-ingress.js';
|
|
3
6
|
export * from './twilio-bridge.js';
|
|
4
7
|
export * from './register-native.js';
|
|
5
8
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export * from './twilio-call-sdk.js';
|
|
2
2
|
export * from './twilio-native-call-sdk.js';
|
|
3
|
+
export * from './real-twilio-bindings.js';
|
|
4
|
+
export * from './twilio-rest-client.js';
|
|
5
|
+
export * from './twilio-ingress.js';
|
|
3
6
|
export * from './twilio-bridge.js';
|
|
4
7
|
export * from './register-native.js';
|
|
5
8
|
import { LoadTwilioBridge } from './twilio-bridge.js';
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,wGAAwG;AACxG,iGAAiG;AACjG,gBAAgB,EAAE,CAAC;AACnB,uGAAuG;AACvG,uBAAuB,EAAE,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,0BAA0B,CAAC;AACzC,cAAc,wBAAwB,CAAC;AACvC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC;AAChC,cAAc,mBAAmB,CAAC;AAElC,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,wGAAwG;AACxG,iGAAiG;AACjG,gBAAgB,EAAE,CAAC;AACnB,uGAAuG;AACvG,uBAAuB,EAAE,CAAC"}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `RealTwilioBindings` — a production {@link ITwilioClientBindings} implementation over the
|
|
3
|
+
* real **Twilio Programmable Voice REST API** + **Media Streams** websocket, plus the **pure** TwiML /
|
|
4
|
+
* media-frame helpers it (and the MJAPI ingress) build on.
|
|
5
|
+
*
|
|
6
|
+
* ## Why an injected client surface (not `import twilio from 'twilio'`)
|
|
7
|
+
* To stay buildable + unit-testable WITHOUT the `twilio` npm package installed and WITHOUT any network,
|
|
8
|
+
* this module does NOT import `twilio` directly. Instead it depends on a tiny, local structural surface —
|
|
9
|
+
* {@link ITwilioRestLike} (the few REST calls we use: `calls.create(...)`, `calls(sid).update(...)`) and
|
|
10
|
+
* {@link ITwilioMediaPump} (the bidirectional Media-Streams frame pump). Production wires these over the
|
|
11
|
+
* real `twilio` SDK + a websocket; tests inject fakes. `twilio` is declared in `optionalDependencies`
|
|
12
|
+
* (CLAUDE rule 8, category 2 — optional peer SDK loaded only when the provider is configured) and is
|
|
13
|
+
* resolved by the host's native-adapter wiring, never statically imported here.
|
|
14
|
+
*
|
|
15
|
+
* ## The audio contract (T0 codec)
|
|
16
|
+
* Twilio Media Streams deliver **base64-encoded G.711 μ-law @ 8 kHz mono** on the wire; the
|
|
17
|
+
* {@link ITwilioClientBindings} seam speaks **PCM16 `ArrayBuffer`**. All transcoding goes through the
|
|
18
|
+
* shared T0 codec ({@link muLawToPcm16Buffer} / {@link pcm16ToMuLawBuffer} from
|
|
19
|
+
* `@memberjunction/ai-bridge-base`) — never reimplemented here. The pure helpers
|
|
20
|
+
* {@link parseTwilioMediaFrame} / {@link encodeTwilioMediaFrame} do the base64↔codec mapping so they
|
|
21
|
+
* unit-test with no network.
|
|
22
|
+
*
|
|
23
|
+
* @module @memberjunction/ai-bridge-twilio
|
|
24
|
+
* @author MemberJunction.com
|
|
25
|
+
* @see {@link ITwilioClientBindings} — the seam this implements.
|
|
26
|
+
* @see `/plans/realtime/bridges-and-widget/telephony-vendor-bindings.md` §2, §3 (T1).
|
|
27
|
+
*/
|
|
28
|
+
import { ITwilioClientBindings } from './twilio-call-sdk.js';
|
|
29
|
+
/**
|
|
30
|
+
* Builds the outbound-dial / inbound-answer TwiML that opens a **bidirectional** Media-Streams websocket
|
|
31
|
+
* to the agent's media endpoint. `<Connect><Stream>` (not `<Start><Stream>`) is the bidirectional form:
|
|
32
|
+
* Twilio both sends inbound audio AND accepts outbound `media` frames on the same socket — the full-duplex
|
|
33
|
+
* leg the agent needs.
|
|
34
|
+
*
|
|
35
|
+
* Pure + exported so it unit-tests with no network and the MJAPI router can reuse it verbatim.
|
|
36
|
+
*
|
|
37
|
+
* @param streamUrl The `wss://…` Media-Streams endpoint Twilio connects the call's audio to.
|
|
38
|
+
* @returns The TwiML document string to return to Twilio (REST `twiml` param or webhook response body).
|
|
39
|
+
*/
|
|
40
|
+
export declare function buildConnectStreamTwiML(streamUrl: string): string;
|
|
41
|
+
/** One Twilio Media-Streams `media` frame as it appears (post-JSON-parse) on the websocket. */
|
|
42
|
+
export interface TwilioMediaFrame {
|
|
43
|
+
/** Always `'media'` for an audio frame (vs `'start'`/`'stop'`/`'dtmf'` events). */
|
|
44
|
+
event: string;
|
|
45
|
+
/** The Media-Streams stream identifier (present on `media`/`start`/`stop`). */
|
|
46
|
+
streamSid?: string;
|
|
47
|
+
/** The media payload — `payload` is base64-encoded μ-law/8k audio. */
|
|
48
|
+
media?: {
|
|
49
|
+
/** Base64-encoded G.711 μ-law @ 8 kHz mono audio bytes. */
|
|
50
|
+
payload: string;
|
|
51
|
+
/** Monotonic frame timestamp (ms since stream start), Twilio-supplied. */
|
|
52
|
+
timestamp?: string;
|
|
53
|
+
/** Monotonic chunk counter, Twilio-supplied. */
|
|
54
|
+
chunk?: string;
|
|
55
|
+
};
|
|
56
|
+
/** The DTMF payload (present on `dtmf` events) — the pressed key. */
|
|
57
|
+
dtmf?: {
|
|
58
|
+
/** The DTMF digit Twilio detected on the stream (`0`-`9`, `*`, `#`). */
|
|
59
|
+
digit: string;
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* **Pure** decode of one inbound Twilio Media-Streams `media` frame to a PCM16 `ArrayBuffer` — base64
|
|
64
|
+
* μ-law → μ-law bytes → PCM16 via the T0 codec ({@link muLawToPcm16Buffer}). Returns `null` for any
|
|
65
|
+
* non-`media` event or a frame with no payload, so the caller can ignore `start`/`stop`/`dtmf` cleanly.
|
|
66
|
+
*
|
|
67
|
+
* @param frame The parsed Media-Streams frame object.
|
|
68
|
+
* @returns The decoded PCM16 audio, or `null` when the frame carries no audio payload.
|
|
69
|
+
*/
|
|
70
|
+
export declare function parseTwilioMediaFrame(frame: TwilioMediaFrame): ArrayBuffer | null;
|
|
71
|
+
/**
|
|
72
|
+
* **Pure** encode of one outbound PCM16 `ArrayBuffer` into the Twilio Media-Streams outbound `media` frame
|
|
73
|
+
* shape — PCM16 → μ-law via the T0 codec ({@link pcm16ToMuLawBuffer}) → base64 payload. The returned object
|
|
74
|
+
* is `JSON.stringify`-ready for the websocket send.
|
|
75
|
+
*
|
|
76
|
+
* @param pcm The agent's outbound PCM16 audio.
|
|
77
|
+
* @param streamSid The Media-Streams stream SID the frame is addressed to.
|
|
78
|
+
* @returns A Media-Streams outbound `media` frame ready to JSON-serialize and send.
|
|
79
|
+
*/
|
|
80
|
+
export declare function encodeTwilioMediaFrame(pcm: ArrayBuffer, streamSid: string): TwilioMediaFrame;
|
|
81
|
+
/** The REST payload {@link ITwilioRestLike.calls.create} accepts (the subset we use). */
|
|
82
|
+
export interface TwilioCreateCallParams {
|
|
83
|
+
/** Destination number (E.164). Maps to Twilio's `to`. */
|
|
84
|
+
To: string;
|
|
85
|
+
/** Originating caller-id / DID. Maps to Twilio's `from`. */
|
|
86
|
+
From: string;
|
|
87
|
+
/** The TwiML executed when the call connects (our `<Connect><Stream>`). Maps to Twilio's `twiml`. */
|
|
88
|
+
Twiml: string;
|
|
89
|
+
/** Optional status-callback URL for lifecycle events. Maps to Twilio's `statusCallback`. */
|
|
90
|
+
StatusCallback?: string;
|
|
91
|
+
}
|
|
92
|
+
/** The REST update payload {@link ITwilioRestLike} `calls(sid).update` accepts (the subset we use). */
|
|
93
|
+
export interface TwilioUpdateCallParams {
|
|
94
|
+
/** New call status — `'completed'` ends the call. */
|
|
95
|
+
Status?: 'completed' | 'canceled';
|
|
96
|
+
/** Replacement TwiML — used for transfer (`<Dial>…`) and DTMF (`<Play digits>…`). */
|
|
97
|
+
Twiml?: string;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The minimal Twilio REST surface {@link RealTwilioBindings} drives — just the two calls we need:
|
|
101
|
+
* `calls.create(...)` (outbound dial) and `calls(sid).update(...)` (hangup / transfer / DTMF). A
|
|
102
|
+
* production wiring implements this over `twilio(accountSid, authToken).calls`; tests inject a fake.
|
|
103
|
+
*/
|
|
104
|
+
export interface ITwilioRestLike {
|
|
105
|
+
/** Creates an outbound call; resolves the created Call SID. */
|
|
106
|
+
CreateCall(params: TwilioCreateCallParams): Promise<string>;
|
|
107
|
+
/** Updates an existing call (end / transfer / send-DTMF) by SID. */
|
|
108
|
+
UpdateCall(callSid: string, params: TwilioUpdateCallParams): Promise<void>;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* The bidirectional Media-Streams frame pump for a single call's websocket. Production wires this over the
|
|
112
|
+
* accepted websocket for the Call SID; tests inject a fake. The pump speaks raw frame objects — transcode
|
|
113
|
+
* happens in {@link RealTwilioBindings} via the pure helpers above.
|
|
114
|
+
*/
|
|
115
|
+
export interface ITwilioMediaPump {
|
|
116
|
+
/** Sends one outbound Media-Streams frame on the call's websocket. */
|
|
117
|
+
Send(callSid: string, frame: TwilioMediaFrame): void;
|
|
118
|
+
/** Registers the inbound-frame handler for the call's websocket (latest handler wins). */
|
|
119
|
+
OnFrame(callSid: string, handler: (frame: TwilioMediaFrame) => void): void;
|
|
120
|
+
/** Registers the call's stream-SID resolver, so outbound frames address the right stream. */
|
|
121
|
+
GetStreamSid(callSid: string): string;
|
|
122
|
+
}
|
|
123
|
+
/** Options {@link RealTwilioBindings} needs at construction — the injected client surfaces + the stream URL. */
|
|
124
|
+
export interface RealTwilioBindingsOptions {
|
|
125
|
+
/** The REST client surface (outbound dial / update). */
|
|
126
|
+
Rest: ITwilioRestLike;
|
|
127
|
+
/** The Media-Streams frame pump (bidirectional audio). */
|
|
128
|
+
MediaPump: ITwilioMediaPump;
|
|
129
|
+
/** The `wss://…` Media-Streams endpoint the call's `<Connect><Stream>` connects to. */
|
|
130
|
+
StreamUrl: string;
|
|
131
|
+
/** Optional status-callback URL passed on outbound `createCall`. */
|
|
132
|
+
StatusCallbackUrl?: string;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Production {@link ITwilioClientBindings} over the real Twilio REST API + Media Streams, expressed against
|
|
136
|
+
* the injected {@link ITwilioRestLike} / {@link ITwilioMediaPump} surfaces so it builds and unit-tests with
|
|
137
|
+
* no `twilio` install and no network.
|
|
138
|
+
*
|
|
139
|
+
* - `createCall` → REST `calls.create` with the `<Connect><Stream>` TwiML.
|
|
140
|
+
* - `acceptInbound` → no REST call (the inbound webhook already returned the connect TwiML); the websocket
|
|
141
|
+
* for the delivered Call SID is what "accepting" means — handled by {@link onStreamAudio}.
|
|
142
|
+
* - `completeCall` / `redirectCall` / `playDigits` → REST `calls(sid).update`.
|
|
143
|
+
* - `pushStreamAudio` / `onStreamAudio` → Media-Streams frames, transcoded through the T0 codec.
|
|
144
|
+
*/
|
|
145
|
+
export declare class RealTwilioBindings implements ITwilioClientBindings {
|
|
146
|
+
private readonly rest;
|
|
147
|
+
private readonly mediaPump;
|
|
148
|
+
private readonly streamUrl;
|
|
149
|
+
private readonly statusCallbackUrl?;
|
|
150
|
+
constructor(options: RealTwilioBindingsOptions);
|
|
151
|
+
/** @inheritdoc */
|
|
152
|
+
createCall(toNumber: string, fromNumber: string, args?: Record<string, unknown>): Promise<string>;
|
|
153
|
+
/** @inheritdoc */
|
|
154
|
+
acceptInbound(_callSid: string): Promise<void>;
|
|
155
|
+
/** @inheritdoc */
|
|
156
|
+
completeCall(callSid: string): Promise<void>;
|
|
157
|
+
/** @inheritdoc */
|
|
158
|
+
pushStreamAudio(callSid: string, pcm: ArrayBuffer): void;
|
|
159
|
+
/** @inheritdoc */
|
|
160
|
+
onStreamAudio(callSid: string, cb: (pcm: ArrayBuffer) => void): void;
|
|
161
|
+
/** @inheritdoc */
|
|
162
|
+
playDigits(callSid: string, digits: string): Promise<void>;
|
|
163
|
+
/** @inheritdoc */
|
|
164
|
+
onDigits(callSid: string, cb: (digits: string) => void): void;
|
|
165
|
+
/** @inheritdoc */
|
|
166
|
+
redirectCall(callSid: string, toNumber: string): Promise<void>;
|
|
167
|
+
/** @inheritdoc */
|
|
168
|
+
onCallStatus(callSid: string, cb: () => void): void;
|
|
169
|
+
}
|
|
170
|
+
/** Builds the `<Play digits>` TwiML used to emit DTMF tones on a live call (REST update). */
|
|
171
|
+
export declare function buildPlayDigitsTwiML(digits: string): string;
|
|
172
|
+
/** Builds the `<Dial>` TwiML used to transfer a live call to another number (REST update). */
|
|
173
|
+
export declare function buildDialTwiML(toNumber: string): string;
|
|
174
|
+
//# sourceMappingURL=real-twilio-bindings.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"real-twilio-bindings.d.ts","sourceRoot":"","sources":["../src/real-twilio-bindings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview `RealTwilioBindings` — a production {@link ITwilioClientBindings} implementation over the
|
|
3
|
+
* real **Twilio Programmable Voice REST API** + **Media Streams** websocket, plus the **pure** TwiML /
|
|
4
|
+
* media-frame helpers it (and the MJAPI ingress) build on.
|
|
5
|
+
*
|
|
6
|
+
* ## Why an injected client surface (not `import twilio from 'twilio'`)
|
|
7
|
+
* To stay buildable + unit-testable WITHOUT the `twilio` npm package installed and WITHOUT any network,
|
|
8
|
+
* this module does NOT import `twilio` directly. Instead it depends on a tiny, local structural surface —
|
|
9
|
+
* {@link ITwilioRestLike} (the few REST calls we use: `calls.create(...)`, `calls(sid).update(...)`) and
|
|
10
|
+
* {@link ITwilioMediaPump} (the bidirectional Media-Streams frame pump). Production wires these over the
|
|
11
|
+
* real `twilio` SDK + a websocket; tests inject fakes. `twilio` is declared in `optionalDependencies`
|
|
12
|
+
* (CLAUDE rule 8, category 2 — optional peer SDK loaded only when the provider is configured) and is
|
|
13
|
+
* resolved by the host's native-adapter wiring, never statically imported here.
|
|
14
|
+
*
|
|
15
|
+
* ## The audio contract (T0 codec)
|
|
16
|
+
* Twilio Media Streams deliver **base64-encoded G.711 μ-law @ 8 kHz mono** on the wire; the
|
|
17
|
+
* {@link ITwilioClientBindings} seam speaks **PCM16 `ArrayBuffer`**. All transcoding goes through the
|
|
18
|
+
* shared T0 codec ({@link muLawToPcm16Buffer} / {@link pcm16ToMuLawBuffer} from
|
|
19
|
+
* `@memberjunction/ai-bridge-base`) — never reimplemented here. The pure helpers
|
|
20
|
+
* {@link parseTwilioMediaFrame} / {@link encodeTwilioMediaFrame} do the base64↔codec mapping so they
|
|
21
|
+
* unit-test with no network.
|
|
22
|
+
*
|
|
23
|
+
* @module @memberjunction/ai-bridge-twilio
|
|
24
|
+
* @author MemberJunction.com
|
|
25
|
+
* @see {@link ITwilioClientBindings} — the seam this implements.
|
|
26
|
+
* @see `/plans/realtime/bridges-and-widget/telephony-vendor-bindings.md` §2, §3 (T1).
|
|
27
|
+
*/
|
|
28
|
+
import { muLawToPcm16Buffer, pcm16ToMuLawBuffer } from '@memberjunction/ai-bridge-base';
|
|
29
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
30
|
+
// Pure helpers — TwiML + Media-Streams frame transcode. No network, no SDK.
|
|
31
|
+
// ──────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
/**
|
|
33
|
+
* Builds the outbound-dial / inbound-answer TwiML that opens a **bidirectional** Media-Streams websocket
|
|
34
|
+
* to the agent's media endpoint. `<Connect><Stream>` (not `<Start><Stream>`) is the bidirectional form:
|
|
35
|
+
* Twilio both sends inbound audio AND accepts outbound `media` frames on the same socket — the full-duplex
|
|
36
|
+
* leg the agent needs.
|
|
37
|
+
*
|
|
38
|
+
* Pure + exported so it unit-tests with no network and the MJAPI router can reuse it verbatim.
|
|
39
|
+
*
|
|
40
|
+
* @param streamUrl The `wss://…` Media-Streams endpoint Twilio connects the call's audio to.
|
|
41
|
+
* @returns The TwiML document string to return to Twilio (REST `twiml` param or webhook response body).
|
|
42
|
+
*/
|
|
43
|
+
export function buildConnectStreamTwiML(streamUrl) {
|
|
44
|
+
const escaped = escapeXmlAttribute(streamUrl);
|
|
45
|
+
return ('<?xml version="1.0" encoding="UTF-8"?>' +
|
|
46
|
+
'<Response>' +
|
|
47
|
+
'<Connect>' +
|
|
48
|
+
`<Stream url="${escaped}" />` +
|
|
49
|
+
'</Connect>' +
|
|
50
|
+
'</Response>');
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* **Pure** decode of one inbound Twilio Media-Streams `media` frame to a PCM16 `ArrayBuffer` — base64
|
|
54
|
+
* μ-law → μ-law bytes → PCM16 via the T0 codec ({@link muLawToPcm16Buffer}). Returns `null` for any
|
|
55
|
+
* non-`media` event or a frame with no payload, so the caller can ignore `start`/`stop`/`dtmf` cleanly.
|
|
56
|
+
*
|
|
57
|
+
* @param frame The parsed Media-Streams frame object.
|
|
58
|
+
* @returns The decoded PCM16 audio, or `null` when the frame carries no audio payload.
|
|
59
|
+
*/
|
|
60
|
+
export function parseTwilioMediaFrame(frame) {
|
|
61
|
+
if (frame.event !== 'media' || !frame.media || !frame.media.payload) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
const mulaw = base64ToArrayBuffer(frame.media.payload);
|
|
65
|
+
return muLawToPcm16Buffer(mulaw);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* **Pure** encode of one outbound PCM16 `ArrayBuffer` into the Twilio Media-Streams outbound `media` frame
|
|
69
|
+
* shape — PCM16 → μ-law via the T0 codec ({@link pcm16ToMuLawBuffer}) → base64 payload. The returned object
|
|
70
|
+
* is `JSON.stringify`-ready for the websocket send.
|
|
71
|
+
*
|
|
72
|
+
* @param pcm The agent's outbound PCM16 audio.
|
|
73
|
+
* @param streamSid The Media-Streams stream SID the frame is addressed to.
|
|
74
|
+
* @returns A Media-Streams outbound `media` frame ready to JSON-serialize and send.
|
|
75
|
+
*/
|
|
76
|
+
export function encodeTwilioMediaFrame(pcm, streamSid) {
|
|
77
|
+
const mulaw = pcm16ToMuLawBuffer(pcm);
|
|
78
|
+
return {
|
|
79
|
+
event: 'media',
|
|
80
|
+
streamSid,
|
|
81
|
+
media: { payload: arrayBufferToBase64(mulaw) },
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/** Escapes the five XML attribute-significant characters so a stream URL is safe inside the TwiML attribute. */
|
|
85
|
+
function escapeXmlAttribute(value) {
|
|
86
|
+
return value
|
|
87
|
+
.replace(/&/g, '&')
|
|
88
|
+
.replace(/</g, '<')
|
|
89
|
+
.replace(/>/g, '>')
|
|
90
|
+
.replace(/"/g, '"')
|
|
91
|
+
.replace(/'/g, ''');
|
|
92
|
+
}
|
|
93
|
+
/** Decodes a base64 string to a fresh `ArrayBuffer` (Node `Buffer` path; works in any Node runtime). */
|
|
94
|
+
function base64ToArrayBuffer(base64) {
|
|
95
|
+
const buf = Buffer.from(base64, 'base64');
|
|
96
|
+
const out = new ArrayBuffer(buf.length);
|
|
97
|
+
new Uint8Array(out).set(buf);
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
/** Encodes an `ArrayBuffer` to a base64 string (Node `Buffer` path; works in any Node runtime). */
|
|
101
|
+
function arrayBufferToBase64(buffer) {
|
|
102
|
+
return Buffer.from(new Uint8Array(buffer)).toString('base64');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Production {@link ITwilioClientBindings} over the real Twilio REST API + Media Streams, expressed against
|
|
106
|
+
* the injected {@link ITwilioRestLike} / {@link ITwilioMediaPump} surfaces so it builds and unit-tests with
|
|
107
|
+
* no `twilio` install and no network.
|
|
108
|
+
*
|
|
109
|
+
* - `createCall` → REST `calls.create` with the `<Connect><Stream>` TwiML.
|
|
110
|
+
* - `acceptInbound` → no REST call (the inbound webhook already returned the connect TwiML); the websocket
|
|
111
|
+
* for the delivered Call SID is what "accepting" means — handled by {@link onStreamAudio}.
|
|
112
|
+
* - `completeCall` / `redirectCall` / `playDigits` → REST `calls(sid).update`.
|
|
113
|
+
* - `pushStreamAudio` / `onStreamAudio` → Media-Streams frames, transcoded through the T0 codec.
|
|
114
|
+
*/
|
|
115
|
+
export class RealTwilioBindings {
|
|
116
|
+
constructor(options) {
|
|
117
|
+
this.rest = options.Rest;
|
|
118
|
+
this.mediaPump = options.MediaPump;
|
|
119
|
+
this.streamUrl = options.StreamUrl;
|
|
120
|
+
this.statusCallbackUrl = options.StatusCallbackUrl;
|
|
121
|
+
}
|
|
122
|
+
/** @inheritdoc */
|
|
123
|
+
async createCall(toNumber, fromNumber, args) {
|
|
124
|
+
const statusCallback = readStatusCallback(args) ?? this.statusCallbackUrl;
|
|
125
|
+
return this.rest.CreateCall({
|
|
126
|
+
To: toNumber,
|
|
127
|
+
From: fromNumber,
|
|
128
|
+
Twiml: buildConnectStreamTwiML(this.streamUrl),
|
|
129
|
+
...(statusCallback ? { StatusCallback: statusCallback } : {}),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
/** @inheritdoc */
|
|
133
|
+
async acceptInbound(_callSid) {
|
|
134
|
+
// No REST call: the inbound voice webhook already returned the <Connect><Stream> TwiML that connects
|
|
135
|
+
// the media socket. "Accepting" is consuming that socket — wired via onStreamAudio/pushStreamAudio.
|
|
136
|
+
}
|
|
137
|
+
/** @inheritdoc */
|
|
138
|
+
async completeCall(callSid) {
|
|
139
|
+
await this.rest.UpdateCall(callSid, { Status: 'completed' });
|
|
140
|
+
}
|
|
141
|
+
/** @inheritdoc */
|
|
142
|
+
pushStreamAudio(callSid, pcm) {
|
|
143
|
+
const streamSid = this.mediaPump.GetStreamSid(callSid);
|
|
144
|
+
this.mediaPump.Send(callSid, encodeTwilioMediaFrame(pcm, streamSid));
|
|
145
|
+
}
|
|
146
|
+
/** @inheritdoc */
|
|
147
|
+
onStreamAudio(callSid, cb) {
|
|
148
|
+
this.mediaPump.OnFrame(callSid, (frame) => {
|
|
149
|
+
const pcm = parseTwilioMediaFrame(frame);
|
|
150
|
+
if (pcm) {
|
|
151
|
+
cb(pcm);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/** @inheritdoc */
|
|
156
|
+
async playDigits(callSid, digits) {
|
|
157
|
+
await this.rest.UpdateCall(callSid, { Twiml: buildPlayDigitsTwiML(digits) });
|
|
158
|
+
}
|
|
159
|
+
/** @inheritdoc */
|
|
160
|
+
onDigits(callSid, cb) {
|
|
161
|
+
this.mediaPump.OnFrame(callSid, (frame) => {
|
|
162
|
+
const digit = readDtmfDigit(frame);
|
|
163
|
+
if (digit) {
|
|
164
|
+
cb(digit);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
/** @inheritdoc */
|
|
169
|
+
async redirectCall(callSid, toNumber) {
|
|
170
|
+
await this.rest.UpdateCall(callSid, { Twiml: buildDialTwiML(toNumber) });
|
|
171
|
+
}
|
|
172
|
+
/** @inheritdoc */
|
|
173
|
+
onCallStatus(callSid, cb) {
|
|
174
|
+
this.mediaPump.OnFrame(callSid, (frame) => {
|
|
175
|
+
if (frame.event === 'stop') {
|
|
176
|
+
cb();
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** Builds the `<Play digits>` TwiML used to emit DTMF tones on a live call (REST update). */
|
|
182
|
+
export function buildPlayDigitsTwiML(digits) {
|
|
183
|
+
return ('<?xml version="1.0" encoding="UTF-8"?>' +
|
|
184
|
+
'<Response>' +
|
|
185
|
+
`<Play digits="${escapeXmlAttribute(digits)}" />` +
|
|
186
|
+
'</Response>');
|
|
187
|
+
}
|
|
188
|
+
/** Builds the `<Dial>` TwiML used to transfer a live call to another number (REST update). */
|
|
189
|
+
export function buildDialTwiML(toNumber) {
|
|
190
|
+
return ('<?xml version="1.0" encoding="UTF-8"?>' +
|
|
191
|
+
'<Response>' +
|
|
192
|
+
`<Dial>${escapeXmlAttribute(toNumber)}</Dial>` +
|
|
193
|
+
'</Response>');
|
|
194
|
+
}
|
|
195
|
+
/** Reads an optional `StatusCallback` string out of the loose dial args without widening to `any`. */
|
|
196
|
+
function readStatusCallback(args) {
|
|
197
|
+
const value = args?.['StatusCallback'];
|
|
198
|
+
return typeof value === 'string' && value.length > 0 ? value : undefined;
|
|
199
|
+
}
|
|
200
|
+
/** Reads a DTMF digit out of a Media-Streams `dtmf` event frame, or `undefined` for non-DTMF frames. */
|
|
201
|
+
function readDtmfDigit(frame) {
|
|
202
|
+
if (frame.event !== 'dtmf') {
|
|
203
|
+
return undefined;
|
|
204
|
+
}
|
|
205
|
+
const digit = frame.dtmf?.digit;
|
|
206
|
+
return typeof digit === 'string' && digit.length > 0 ? digit : undefined;
|
|
207
|
+
}
|
|
208
|
+
//# sourceMappingURL=real-twilio-bindings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"real-twilio-bindings.js","sourceRoot":"","sources":["../src/real-twilio-bindings.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Twilio inbound-ingress **pure** helpers — request-signature verification, the inbound
|
|
3
|
+
* voice TwiML, and the webhook→call mapper. These are the framework-free pieces of the MJAPI Twilio
|
|
4
|
+
* ingress: no Express, no network, no DB, so they unit-test directly and the MJAPI router can call them
|
|
5
|
+
* verbatim once the live wiring lands.
|
|
6
|
+
*
|
|
7
|
+
* The remaining **live** ingress — `POST /telephony/twilio/voice`, `WSS /telephony/twilio/media`, the
|
|
8
|
+
* `Telephony.PlaceCall` mutation, and the credential config block — is documented in
|
|
9
|
+
* `plans/realtime/bridges-and-widget/spikes/T1-twilio-ingress-notes.md` and is gated on real Twilio
|
|
10
|
+
* credentials + a publicly reachable URL.
|
|
11
|
+
*
|
|
12
|
+
* @module @memberjunction/ai-bridge-twilio
|
|
13
|
+
* @author MemberJunction.com
|
|
14
|
+
* @see `/plans/realtime/bridges-and-widget/telephony-vendor-bindings.md` §2 (B), §3 (T1 B/C).
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* Verifies a Twilio webhook request signature per Twilio's documented scheme:
|
|
18
|
+
*
|
|
19
|
+
* 1. Start from the full request URL (scheme + host + path + query, exactly as Twilio called it).
|
|
20
|
+
* 2. Append each POST param, sorted by parameter name, as `key + value` concatenated (no separators).
|
|
21
|
+
* 3. HMAC-SHA1 the resulting string with the account **Auth Token** as the key; base64-encode the digest.
|
|
22
|
+
* 4. Constant-time-compare against the `X-Twilio-Signature` header.
|
|
23
|
+
*
|
|
24
|
+
* A missing/empty signature header rejects. Comparison is constant-time ({@link timingSafeEqual}) to avoid
|
|
25
|
+
* leaking the expected signature via timing. Pure + exported so it unit-tests with a known vector and the
|
|
26
|
+
* MJAPI router calls it on every public Twilio endpoint (these can't carry an MJ JWT).
|
|
27
|
+
*
|
|
28
|
+
* @param authToken The Twilio account Auth Token (the HMAC key; resolved upstream — never inlined).
|
|
29
|
+
* @param signatureHeader The `X-Twilio-Signature` request header value.
|
|
30
|
+
* @param url The full request URL exactly as Twilio invoked it.
|
|
31
|
+
* @param params The POST form parameters Twilio sent.
|
|
32
|
+
* @returns `true` when the signature is valid; `false` otherwise (including a missing header).
|
|
33
|
+
*/
|
|
34
|
+
export declare function verifyTwilioSignature(authToken: string, signatureHeader: string | undefined, url: string, params: Record<string, string>): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Computes the Twilio request signature (base64 HMAC-SHA1) for a URL + sorted params. Exported so tests can
|
|
37
|
+
* construct a known-good vector and the live router can sign/verify without duplicating the concat rule.
|
|
38
|
+
*
|
|
39
|
+
* @param authToken The Twilio Auth Token (HMAC key).
|
|
40
|
+
* @param url The full request URL exactly as Twilio invoked it.
|
|
41
|
+
* @param params The POST form parameters.
|
|
42
|
+
* @returns The base64-encoded HMAC-SHA1 signature.
|
|
43
|
+
*/
|
|
44
|
+
export declare function computeTwilioSignature(authToken: string, url: string, params: Record<string, string>): string;
|
|
45
|
+
/**
|
|
46
|
+
* The TwiML returned to an inbound call's voice webhook to connect its bidirectional Media-Streams socket.
|
|
47
|
+
* Identical `<Connect><Stream>` shape as the outbound path (it reuses {@link buildConnectStreamTwiML}), so
|
|
48
|
+
* inbound and outbound legs share one media contract.
|
|
49
|
+
*
|
|
50
|
+
* @param streamWssUrl The `wss://…/telephony/twilio/media` endpoint to connect the inbound call's audio to.
|
|
51
|
+
* @returns The TwiML document string to return to Twilio as the webhook response.
|
|
52
|
+
*/
|
|
53
|
+
export declare function buildInboundVoiceTwiML(streamWssUrl: string): string;
|
|
54
|
+
/** The resolved identity of an inbound Twilio call, mapped from the voice-webhook params. */
|
|
55
|
+
export interface ResolvedInboundCall {
|
|
56
|
+
/** The inbound Call SID (Twilio `CallSid`). */
|
|
57
|
+
callSid: string;
|
|
58
|
+
/** The caller's number (Twilio `From`). */
|
|
59
|
+
from: string;
|
|
60
|
+
/** The dialed DID the call came in on (Twilio `To`). */
|
|
61
|
+
to: string;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Maps the Twilio inbound voice-webhook params to a {@link ResolvedInboundCall}. Pure + exported so the
|
|
65
|
+
* MJAPI router resolves the dialed DID → agent identity → `AIBridgeEngine.StartBridgeSession` without
|
|
66
|
+
* re-parsing Twilio's param names. Throws when a required param is absent so a malformed webhook fails loud.
|
|
67
|
+
*
|
|
68
|
+
* @param params The Twilio inbound voice-webhook POST params.
|
|
69
|
+
* @returns The `{ callSid, from, to }` mapping.
|
|
70
|
+
* @throws When `CallSid`, `From`, or `To` is missing.
|
|
71
|
+
*/
|
|
72
|
+
export declare function resolveInboundCall(params: Record<string, string>): ResolvedInboundCall;
|
|
73
|
+
//# sourceMappingURL=twilio-ingress.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twilio-ingress.d.ts","sourceRoot":"","sources":["../src/twilio-ingress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAKH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACjC,SAAS,EAAE,MAAM,EACjB,eAAe,EAAE,MAAM,GAAG,SAAS,EACnC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC/B,OAAO,CAMT;AAED;;;;;;;;GAQG;AACH,wBAAgB,sBAAsB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAG7G;AAED;;;;;;;GAOG;AACH,wBAAgB,sBAAsB,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,CAEnE;AAED,6FAA6F;AAC7F,MAAM,WAAW,mBAAmB;IAChC,+CAA+C;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,wDAAwD;IACxD,EAAE,EAAE,MAAM,CAAC;CACd;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,mBAAmB,CAWtF"}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Twilio inbound-ingress **pure** helpers — request-signature verification, the inbound
|
|
3
|
+
* voice TwiML, and the webhook→call mapper. These are the framework-free pieces of the MJAPI Twilio
|
|
4
|
+
* ingress: no Express, no network, no DB, so they unit-test directly and the MJAPI router can call them
|
|
5
|
+
* verbatim once the live wiring lands.
|
|
6
|
+
*
|
|
7
|
+
* The remaining **live** ingress — `POST /telephony/twilio/voice`, `WSS /telephony/twilio/media`, the
|
|
8
|
+
* `Telephony.PlaceCall` mutation, and the credential config block — is documented in
|
|
9
|
+
* `plans/realtime/bridges-and-widget/spikes/T1-twilio-ingress-notes.md` and is gated on real Twilio
|
|
10
|
+
* credentials + a publicly reachable URL.
|
|
11
|
+
*
|
|
12
|
+
* @module @memberjunction/ai-bridge-twilio
|
|
13
|
+
* @author MemberJunction.com
|
|
14
|
+
* @see `/plans/realtime/bridges-and-widget/telephony-vendor-bindings.md` §2 (B), §3 (T1 B/C).
|
|
15
|
+
*/
|
|
16
|
+
import { createHmac, timingSafeEqual } from 'node:crypto';
|
|
17
|
+
import { buildConnectStreamTwiML } from './real-twilio-bindings.js';
|
|
18
|
+
/**
|
|
19
|
+
* Verifies a Twilio webhook request signature per Twilio's documented scheme:
|
|
20
|
+
*
|
|
21
|
+
* 1. Start from the full request URL (scheme + host + path + query, exactly as Twilio called it).
|
|
22
|
+
* 2. Append each POST param, sorted by parameter name, as `key + value` concatenated (no separators).
|
|
23
|
+
* 3. HMAC-SHA1 the resulting string with the account **Auth Token** as the key; base64-encode the digest.
|
|
24
|
+
* 4. Constant-time-compare against the `X-Twilio-Signature` header.
|
|
25
|
+
*
|
|
26
|
+
* A missing/empty signature header rejects. Comparison is constant-time ({@link timingSafeEqual}) to avoid
|
|
27
|
+
* leaking the expected signature via timing. Pure + exported so it unit-tests with a known vector and the
|
|
28
|
+
* MJAPI router calls it on every public Twilio endpoint (these can't carry an MJ JWT).
|
|
29
|
+
*
|
|
30
|
+
* @param authToken The Twilio account Auth Token (the HMAC key; resolved upstream — never inlined).
|
|
31
|
+
* @param signatureHeader The `X-Twilio-Signature` request header value.
|
|
32
|
+
* @param url The full request URL exactly as Twilio invoked it.
|
|
33
|
+
* @param params The POST form parameters Twilio sent.
|
|
34
|
+
* @returns `true` when the signature is valid; `false` otherwise (including a missing header).
|
|
35
|
+
*/
|
|
36
|
+
export function verifyTwilioSignature(authToken, signatureHeader, url, params) {
|
|
37
|
+
if (!signatureHeader) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const expected = computeTwilioSignature(authToken, url, params);
|
|
41
|
+
return constantTimeEquals(expected, signatureHeader);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Computes the Twilio request signature (base64 HMAC-SHA1) for a URL + sorted params. Exported so tests can
|
|
45
|
+
* construct a known-good vector and the live router can sign/verify without duplicating the concat rule.
|
|
46
|
+
*
|
|
47
|
+
* @param authToken The Twilio Auth Token (HMAC key).
|
|
48
|
+
* @param url The full request URL exactly as Twilio invoked it.
|
|
49
|
+
* @param params The POST form parameters.
|
|
50
|
+
* @returns The base64-encoded HMAC-SHA1 signature.
|
|
51
|
+
*/
|
|
52
|
+
export function computeTwilioSignature(authToken, url, params) {
|
|
53
|
+
const data = url + concatSortedParams(params);
|
|
54
|
+
return createHmac('sha1', authToken).update(data, 'utf8').digest('base64');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The TwiML returned to an inbound call's voice webhook to connect its bidirectional Media-Streams socket.
|
|
58
|
+
* Identical `<Connect><Stream>` shape as the outbound path (it reuses {@link buildConnectStreamTwiML}), so
|
|
59
|
+
* inbound and outbound legs share one media contract.
|
|
60
|
+
*
|
|
61
|
+
* @param streamWssUrl The `wss://…/telephony/twilio/media` endpoint to connect the inbound call's audio to.
|
|
62
|
+
* @returns The TwiML document string to return to Twilio as the webhook response.
|
|
63
|
+
*/
|
|
64
|
+
export function buildInboundVoiceTwiML(streamWssUrl) {
|
|
65
|
+
return buildConnectStreamTwiML(streamWssUrl);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Maps the Twilio inbound voice-webhook params to a {@link ResolvedInboundCall}. Pure + exported so the
|
|
69
|
+
* MJAPI router resolves the dialed DID → agent identity → `AIBridgeEngine.StartBridgeSession` without
|
|
70
|
+
* re-parsing Twilio's param names. Throws when a required param is absent so a malformed webhook fails loud.
|
|
71
|
+
*
|
|
72
|
+
* @param params The Twilio inbound voice-webhook POST params.
|
|
73
|
+
* @returns The `{ callSid, from, to }` mapping.
|
|
74
|
+
* @throws When `CallSid`, `From`, or `To` is missing.
|
|
75
|
+
*/
|
|
76
|
+
export function resolveInboundCall(params) {
|
|
77
|
+
const callSid = params['CallSid'];
|
|
78
|
+
const from = params['From'];
|
|
79
|
+
const to = params['To'];
|
|
80
|
+
if (!callSid || !from || !to) {
|
|
81
|
+
throw new Error('resolveInboundCall: Twilio inbound webhook missing a required param (CallSid / From / To). ' +
|
|
82
|
+
`Got keys: [${Object.keys(params).join(', ')}].`);
|
|
83
|
+
}
|
|
84
|
+
return { callSid, from, to };
|
|
85
|
+
}
|
|
86
|
+
/** Concatenates params sorted by key as `key + value` (Twilio's signature input), with no separators. */
|
|
87
|
+
function concatSortedParams(params) {
|
|
88
|
+
return Object.keys(params)
|
|
89
|
+
.sort()
|
|
90
|
+
.map((key) => key + params[key])
|
|
91
|
+
.join('');
|
|
92
|
+
}
|
|
93
|
+
/** Constant-time string compare on the base64 signatures, length-safe (mismatched lengths return false). */
|
|
94
|
+
function constantTimeEquals(a, b) {
|
|
95
|
+
const bufA = new Uint8Array(Buffer.from(a, 'utf8'));
|
|
96
|
+
const bufB = new Uint8Array(Buffer.from(b, 'utf8'));
|
|
97
|
+
if (bufA.length !== bufB.length) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return timingSafeEqual(bufA, bufB);
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=twilio-ingress.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twilio-ingress.js","sourceRoot":"","sources":["../src/twilio-ingress.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,UAAU,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,qBAAqB,CACjC,SAAiB,EACjB,eAAmC,EACnC,GAAW,EACX,MAA8B;IAE9B,IAAI,CAAC,eAAe,EAAE,CAAC;QACnB,OAAO,KAAK,CAAC;IACjB,CAAC;IACD,MAAM,QAAQ,GAAG,sBAAsB,CAAC,SAAS,EAAE,GAAG,EAAE,MAAM,CAAC,CAAC;IAChE,OAAO,kBAAkB,CAAC,QAAQ,EAAE,eAAe,CAAC,CAAC;AACzD,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,UAAU,sBAAsB,CAAC,SAAiB,EAAE,GAAW,EAAE,MAA8B;IACjG,MAAM,IAAI,GAAG,GAAG,GAAG,kBAAkB,CAAC,MAAM,CAAC,CAAC;IAC9C,OAAO,UAAU,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,MAAM,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC;AAC/E,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,sBAAsB,CAAC,YAAoB;IACvD,OAAO,uBAAuB,CAAC,YAAY,CAAC,CAAC;AACjD,CAAC;AAYD;;;;;;;;GAQG;AACH,MAAM,UAAU,kBAAkB,CAAC,MAA8B;IAC7D,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;IAClC,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAC5B,MAAM,EAAE,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC;IACxB,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,IAAI,CAAC,EAAE,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CACX,6FAA6F;YACzF,cAAc,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CACvD,CAAC;IACN,CAAC;IACD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC;AACjC,CAAC;AAED,yGAAyG;AACzG,SAAS,kBAAkB,CAAC,MAA8B;IACtD,OAAO,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC;SACrB,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;SAC/B,IAAI,CAAC,EAAE,CAAC,CAAC;AAClB,CAAC;AAED,4GAA4G;AAC5G,SAAS,kBAAkB,CAAC,CAAS,EAAE,CAAS;IAC5C,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,IAAI,IAAI,CAAC,MAAM,KAAK,IAAI,CAAC,MAAM,EAAE,CAAC;QAC9B,OAAO,KAAK,CAAC;IACjB,CAAC;IACD,OAAO,eAAe,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AACvC,CAAC"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Production REST half of the Twilio binding: a real {@link ITwilioRestLike}
|
|
3
|
+
* over the `twilio` npm SDK's Programmable Voice REST API (outbound dial + call update).
|
|
4
|
+
*
|
|
5
|
+
* The `twilio` package is an OPTIONAL PEER SDK (CLAUDE rule 8, category 2) — it is NEVER
|
|
6
|
+
* statically imported, so this provider package builds and unit-tests with no `twilio`
|
|
7
|
+
* install and no network. The SDK is lazily loaded (once, memoized) at first use via the
|
|
8
|
+
* injectable {@link TwilioRestModuleLoader}; tests inject a fake factory instead. None of
|
|
9
|
+
* the `twilio` SDK's own types leak past this file — everything crosses the seam through
|
|
10
|
+
* the minimal structural interfaces below, keeping {@link RealTwilioBindings} SDK-agnostic.
|
|
11
|
+
*
|
|
12
|
+
* The OTHER half — {@link ITwilioMediaPump} (the bidirectional Media-Streams websocket) —
|
|
13
|
+
* is owned by the MJAPI telephony ingress, not this package: the carrier websocket is a
|
|
14
|
+
* server concern (the `ws` dependency lives in MJServer), and one socket spans a call's
|
|
15
|
+
* whole media plane. This file is REST only.
|
|
16
|
+
*
|
|
17
|
+
* @module @memberjunction/ai-bridge-twilio
|
|
18
|
+
*/
|
|
19
|
+
import type { ITwilioRestLike, TwilioCreateCallParams, TwilioUpdateCallParams } from './real-twilio-bindings.js';
|
|
20
|
+
/** The created-call resource the SDK's `calls.create(...)` resolves to (we read only `sid`). */
|
|
21
|
+
export interface TwilioCallInstanceLike {
|
|
22
|
+
sid: string;
|
|
23
|
+
}
|
|
24
|
+
/** A single call's context (`client.calls(sid)`) — supports `update(...)` for hangup/transfer/DTMF. */
|
|
25
|
+
export interface TwilioCallContextLike {
|
|
26
|
+
update(params: Record<string, unknown>): Promise<unknown>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The `client.calls` surface: callable as `calls(sid)` to address one call AND carrying a
|
|
30
|
+
* `create(...)` method for outbound dial — exactly the dual shape the real `twilio` SDK exposes.
|
|
31
|
+
*/
|
|
32
|
+
export interface TwilioCallsResourceLike {
|
|
33
|
+
(callSid: string): TwilioCallContextLike;
|
|
34
|
+
create(params: Record<string, unknown>): Promise<TwilioCallInstanceLike>;
|
|
35
|
+
}
|
|
36
|
+
/** The subset of a constructed `twilio` REST client we drive. */
|
|
37
|
+
export interface TwilioRestClientLike {
|
|
38
|
+
calls: TwilioCallsResourceLike;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The `twilio` module's default export: a factory constructing a REST client. Supports both
|
|
42
|
+
* auth forms — `twilio(accountSid, authToken)` and `twilio(apiKeySid, apiKeySecret, { accountSid })`.
|
|
43
|
+
*/
|
|
44
|
+
export type TwilioModuleFactory = (usernameOrAccountSid: string, password: string, opts?: {
|
|
45
|
+
accountSid?: string;
|
|
46
|
+
}) => TwilioRestClientLike;
|
|
47
|
+
/** Loads the `twilio` module's client factory. Overridable in tests; defaults to a lazy dynamic import. */
|
|
48
|
+
export type TwilioRestModuleLoader = () => Promise<TwilioModuleFactory>;
|
|
49
|
+
/** Credentials the REST client constructs the `twilio` SDK with. Resolved upstream via MJ config — never inlined. */
|
|
50
|
+
export interface TwilioRestCredentials {
|
|
51
|
+
/** Twilio Account SID (`AC…`). Always required (used directly, or as the `accountSid` option under API-key auth). */
|
|
52
|
+
AccountSid: string;
|
|
53
|
+
/** Account auth token — used when no API key pair is supplied. Also the HMAC key for webhook signature verification. */
|
|
54
|
+
AuthToken?: string;
|
|
55
|
+
/** API Key SID (`SK…`) — preferred over the auth token for REST auth when present (with {@link ApiKeySecret}). */
|
|
56
|
+
ApiKeySid?: string;
|
|
57
|
+
/** API Key secret — paired with {@link ApiKeySid}. */
|
|
58
|
+
ApiKeySecret?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Lazily loads the `twilio` module's client factory exactly once and memoizes it. A static import
|
|
62
|
+
* is impossible here (optional peer SDK, may be uninstalled in non-telephony deployments); the
|
|
63
|
+
* `optionalDependencies` entry keeps it in the dependency graph (CLAUDE rule 8, category 2).
|
|
64
|
+
*/
|
|
65
|
+
export declare const defaultTwilioRestModuleLoader: TwilioRestModuleLoader;
|
|
66
|
+
/**
|
|
67
|
+
* A real {@link ITwilioRestLike} over the `twilio` SDK's Programmable Voice REST API.
|
|
68
|
+
*
|
|
69
|
+
* - `CreateCall` → `client.calls.create({ to, from, twiml, statusCallback })`, resolving the new Call SID.
|
|
70
|
+
* - `UpdateCall` → `client.calls(sid).update({ status, twiml })` for hangup / transfer / DTMF.
|
|
71
|
+
*
|
|
72
|
+
* The constructed client is built once on first use and reused for the life of the instance.
|
|
73
|
+
*/
|
|
74
|
+
export declare class RealTwilioRestClient implements ITwilioRestLike {
|
|
75
|
+
private readonly credentials;
|
|
76
|
+
private readonly loadModule;
|
|
77
|
+
/** Memoized client-build promise so concurrent callers share one construction. */
|
|
78
|
+
private clientPromise?;
|
|
79
|
+
/**
|
|
80
|
+
* @param credentials Resolved Twilio credentials (account SID + auth token or API key pair).
|
|
81
|
+
* @param loadModule The `twilio` module loader (defaults to the lazy dynamic import).
|
|
82
|
+
*/
|
|
83
|
+
constructor(credentials: TwilioRestCredentials, loadModule?: TwilioRestModuleLoader);
|
|
84
|
+
/** @inheritdoc */
|
|
85
|
+
CreateCall(params: TwilioCreateCallParams): Promise<string>;
|
|
86
|
+
/** @inheritdoc */
|
|
87
|
+
UpdateCall(callSid: string, params: TwilioUpdateCallParams): Promise<void>;
|
|
88
|
+
/** Builds (once) and returns the constructed `twilio` REST client, choosing API-key auth when available. */
|
|
89
|
+
private ensureClient;
|
|
90
|
+
/** Constructs the `twilio` REST client with API-key auth when a key pair is present, else the auth token. */
|
|
91
|
+
private buildClient;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=twilio-rest-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twilio-rest-client.d.ts","sourceRoot":"","sources":["../src/twilio-rest-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,sBAAsB,EAAE,sBAAsB,EAAE,MAAM,2BAA2B,CAAC;AAOjH,gGAAgG;AAChG,MAAM,WAAW,sBAAsB;IACnC,GAAG,EAAE,MAAM,CAAC;CACf;AAED,uGAAuG;AACvG,MAAM,WAAW,qBAAqB;IAClC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7D;AAED;;;GAGG;AACH,MAAM,WAAW,uBAAuB;IACpC,CAAC,OAAO,EAAE,MAAM,GAAG,qBAAqB,CAAC;IACzC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAAC;CAC5E;AAED,iEAAiE;AACjE,MAAM,WAAW,oBAAoB;IACjC,KAAK,EAAE,uBAAuB,CAAC;CAClC;AAED;;;GAGG;AACH,MAAM,MAAM,mBAAmB,GAAG,CAC9B,oBAAoB,EAAE,MAAM,EAC5B,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,KAC7B,oBAAoB,CAAC;AAE1B,2GAA2G;AAC3G,MAAM,MAAM,sBAAsB,GAAG,MAAM,OAAO,CAAC,mBAAmB,CAAC,CAAC;AAExE,qHAAqH;AACrH,MAAM,WAAW,qBAAqB;IAClC,qHAAqH;IACrH,UAAU,EAAE,MAAM,CAAC;IACnB,wHAAwH;IACxH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,kHAAkH;IAClH,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,EAAE,sBAe3C,CAAC;AAaF;;;;;;;GAOG;AACH,qBAAa,oBAAqB,YAAW,eAAe;IACxD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAwB;IACpD,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAyB;IACpD,kFAAkF;IAClF,OAAO,CAAC,aAAa,CAAC,CAAgC;IAEtD;;;OAGG;gBACS,WAAW,EAAE,qBAAqB,EAAE,UAAU,GAAE,sBAAsD;IAKlH,kBAAkB;IACL,UAAU,CAAC,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC;IAcxE,kBAAkB;IACL,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,sBAAsB,GAAG,OAAO,CAAC,IAAI,CAAC;IAQvF,4GAA4G;IAC5G,OAAO,CAAC,YAAY;IAOpB,6GAA6G;YAC/F,WAAW;CAW5B"}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Production REST half of the Twilio binding: a real {@link ITwilioRestLike}
|
|
3
|
+
* over the `twilio` npm SDK's Programmable Voice REST API (outbound dial + call update).
|
|
4
|
+
*
|
|
5
|
+
* The `twilio` package is an OPTIONAL PEER SDK (CLAUDE rule 8, category 2) — it is NEVER
|
|
6
|
+
* statically imported, so this provider package builds and unit-tests with no `twilio`
|
|
7
|
+
* install and no network. The SDK is lazily loaded (once, memoized) at first use via the
|
|
8
|
+
* injectable {@link TwilioRestModuleLoader}; tests inject a fake factory instead. None of
|
|
9
|
+
* the `twilio` SDK's own types leak past this file — everything crosses the seam through
|
|
10
|
+
* the minimal structural interfaces below, keeping {@link RealTwilioBindings} SDK-agnostic.
|
|
11
|
+
*
|
|
12
|
+
* The OTHER half — {@link ITwilioMediaPump} (the bidirectional Media-Streams websocket) —
|
|
13
|
+
* is owned by the MJAPI telephony ingress, not this package: the carrier websocket is a
|
|
14
|
+
* server concern (the `ws` dependency lives in MJServer), and one socket spans a call's
|
|
15
|
+
* whole media plane. This file is REST only.
|
|
16
|
+
*
|
|
17
|
+
* @module @memberjunction/ai-bridge-twilio
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Lazily loads the `twilio` module's client factory exactly once and memoizes it. A static import
|
|
21
|
+
* is impossible here (optional peer SDK, may be uninstalled in non-telephony deployments); the
|
|
22
|
+
* `optionalDependencies` entry keeps it in the dependency graph (CLAUDE rule 8, category 2).
|
|
23
|
+
*/
|
|
24
|
+
export const defaultTwilioRestModuleLoader = async () => {
|
|
25
|
+
try {
|
|
26
|
+
const mod = await import('twilio');
|
|
27
|
+
const factory = unwrapTwilioFactory(mod);
|
|
28
|
+
if (typeof factory !== 'function') {
|
|
29
|
+
throw new Error('the twilio module did not export a client factory function');
|
|
30
|
+
}
|
|
31
|
+
return factory;
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
35
|
+
throw new Error("RealTwilioRestClient could not load the 'twilio' SDK. Install it in the deployment that " +
|
|
36
|
+
`runs the Twilio telephony bridge (it is an optional peer dependency). Underlying error: ${message}`);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/** Unwraps the twilio factory from CJS/ESM interop (`module` or `module.default`). */
|
|
40
|
+
function unwrapTwilioFactory(mod) {
|
|
41
|
+
if (typeof mod === 'function') {
|
|
42
|
+
return mod;
|
|
43
|
+
}
|
|
44
|
+
if (mod && typeof mod === 'object' && 'default' in mod) {
|
|
45
|
+
return mod.default;
|
|
46
|
+
}
|
|
47
|
+
return mod;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* A real {@link ITwilioRestLike} over the `twilio` SDK's Programmable Voice REST API.
|
|
51
|
+
*
|
|
52
|
+
* - `CreateCall` → `client.calls.create({ to, from, twiml, statusCallback })`, resolving the new Call SID.
|
|
53
|
+
* - `UpdateCall` → `client.calls(sid).update({ status, twiml })` for hangup / transfer / DTMF.
|
|
54
|
+
*
|
|
55
|
+
* The constructed client is built once on first use and reused for the life of the instance.
|
|
56
|
+
*/
|
|
57
|
+
export class RealTwilioRestClient {
|
|
58
|
+
/**
|
|
59
|
+
* @param credentials Resolved Twilio credentials (account SID + auth token or API key pair).
|
|
60
|
+
* @param loadModule The `twilio` module loader (defaults to the lazy dynamic import).
|
|
61
|
+
*/
|
|
62
|
+
constructor(credentials, loadModule = defaultTwilioRestModuleLoader) {
|
|
63
|
+
this.credentials = credentials;
|
|
64
|
+
this.loadModule = loadModule;
|
|
65
|
+
}
|
|
66
|
+
/** @inheritdoc */
|
|
67
|
+
async CreateCall(params) {
|
|
68
|
+
const client = await this.ensureClient();
|
|
69
|
+
const created = await client.calls.create({
|
|
70
|
+
to: params.To,
|
|
71
|
+
from: params.From,
|
|
72
|
+
twiml: params.Twiml,
|
|
73
|
+
...(params.StatusCallback ? { statusCallback: params.StatusCallback } : {}),
|
|
74
|
+
});
|
|
75
|
+
if (!created?.sid) {
|
|
76
|
+
throw new Error('Twilio calls.create returned no Call SID.');
|
|
77
|
+
}
|
|
78
|
+
return created.sid;
|
|
79
|
+
}
|
|
80
|
+
/** @inheritdoc */
|
|
81
|
+
async UpdateCall(callSid, params) {
|
|
82
|
+
const client = await this.ensureClient();
|
|
83
|
+
await client.calls(callSid).update({
|
|
84
|
+
...(params.Status ? { status: params.Status } : {}),
|
|
85
|
+
...(params.Twiml ? { twiml: params.Twiml } : {}),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/** Builds (once) and returns the constructed `twilio` REST client, choosing API-key auth when available. */
|
|
89
|
+
ensureClient() {
|
|
90
|
+
if (!this.clientPromise) {
|
|
91
|
+
this.clientPromise = this.buildClient();
|
|
92
|
+
}
|
|
93
|
+
return this.clientPromise;
|
|
94
|
+
}
|
|
95
|
+
/** Constructs the `twilio` REST client with API-key auth when a key pair is present, else the auth token. */
|
|
96
|
+
async buildClient() {
|
|
97
|
+
const factory = await this.loadModule();
|
|
98
|
+
const { AccountSid, AuthToken, ApiKeySid, ApiKeySecret } = this.credentials;
|
|
99
|
+
if (ApiKeySid && ApiKeySecret) {
|
|
100
|
+
return factory(ApiKeySid, ApiKeySecret, { accountSid: AccountSid });
|
|
101
|
+
}
|
|
102
|
+
if (!AuthToken) {
|
|
103
|
+
throw new Error('RealTwilioRestClient requires either an API key pair (ApiKeySid + ApiKeySecret) or an AuthToken.');
|
|
104
|
+
}
|
|
105
|
+
return factory(AccountSid, AuthToken);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=twilio-rest-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"twilio-rest-client.js","sourceRoot":"","sources":["../src/twilio-rest-client.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@memberjunction/ai-bridge-twilio",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "5.
|
|
4
|
+
"version": "5.45.1",
|
|
5
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
6
|
"main": "dist/index.js",
|
|
7
7
|
"types": "dist/index.d.ts",
|
|
@@ -17,10 +17,14 @@
|
|
|
17
17
|
"author": "MemberJunction.com",
|
|
18
18
|
"license": "ISC",
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@memberjunction/core": "5.
|
|
21
|
-
"@memberjunction/global": "5.
|
|
22
|
-
"@memberjunction/core-entities": "5.
|
|
23
|
-
"@memberjunction/ai-bridge-base": "5.
|
|
20
|
+
"@memberjunction/core": "5.45.1",
|
|
21
|
+
"@memberjunction/global": "5.45.1",
|
|
22
|
+
"@memberjunction/core-entities": "5.45.1",
|
|
23
|
+
"@memberjunction/ai-bridge-base": "5.45.1"
|
|
24
|
+
},
|
|
25
|
+
"//optionalDependencies": "twilio is an OPTIONAL PEER SDK (CLAUDE rule 8, category 2): the package never statically imports it. Production wires RealTwilioBindings over the real twilio REST client + a Media-Streams websocket through the injected ITwilioRestLike/ITwilioMediaPump surfaces, so the package builds and unit-tests with no twilio install and no network. Loaded only when the Twilio provider is configured at deployment.",
|
|
26
|
+
"optionalDependencies": {
|
|
27
|
+
"twilio": "^5.3.0"
|
|
24
28
|
},
|
|
25
29
|
"devDependencies": {
|
|
26
30
|
"@types/node": "24.10.11",
|