@openclaw/voice-call 2026.9.8 → 2026.10.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.setup/{cli-D6uwMXyS.mjs → cli-Cn1leykM.mjs} +57 -37
- package/dist/.setup/{config-Ds7xoZbI.mjs → config-CX9J-053.mjs} +18 -119
- package/dist/.setup/{guarded-json-api-Bqc6QS8A.mjs → guarded-json-api-x55yqHvW.mjs} +39 -94
- package/dist/.setup/{mock-ClWW6Ufu.mjs → mock-eLv_Dc4o.mjs} +3 -6
- package/dist/.setup/{plivo-69ObrQPk.mjs → plivo-CpPIENu4.mjs} +48 -101
- package/dist/.setup/{realtime-handler-BgDPcPZt.mjs → realtime-handler-Do7Sz7Gh.mjs} +123 -235
- package/dist/.setup/{response-generator-BzKrJMEb.mjs → response-generator-DAhbXEta.mjs} +6 -25
- package/dist/.setup/{runtime-entry-BNOQRXii.mjs → runtime-entry-CbSKxSKk.mjs} +554 -425
- package/dist/.setup/state-retention-api-ONgqE-_V.mjs +201 -0
- package/dist/.setup/store-path-Cfwff6j7.mjs +33 -0
- package/dist/.setup/{telnyx-DjFGCfy1.mjs → telnyx-DPilutCH.mjs} +10 -29
- package/dist/.setup/{twilio-Bp3gfmUB.mjs → twilio-BWFNGyVx.mjs} +78 -201
- package/dist/.setup/{twilio-region-4fkgz3UG.mjs → twilio-region-wBOx-8sz.mjs} +1 -4
- package/dist/.setup/voice-mapping-B_iAUkER.mjs +31 -0
- package/dist/.setup/{webhook-replay-CCQSoBNF.mjs → webhook-replay-B7RJNWNG.mjs} +2 -5
- package/dist/doctor-contract-api.js +7 -330
- package/dist/index.js +18 -16
- package/dist/runtime-entry.js +1 -1
- package/dist/state-retention-api.js +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +4 -6
- package/dist/.setup/utils-B5pnM_Ij.mjs +0 -437
- package/node_modules/ws/LICENSE +0 -20
- package/node_modules/ws/README.md +0 -548
- package/node_modules/ws/browser.js +0 -8
- package/node_modules/ws/index.js +0 -22
- package/node_modules/ws/lib/buffer-util.js +0 -131
- package/node_modules/ws/lib/constants.js +0 -19
- package/node_modules/ws/lib/event-target.js +0 -292
- package/node_modules/ws/lib/extension.js +0 -203
- package/node_modules/ws/lib/limiter.js +0 -55
- package/node_modules/ws/lib/permessage-deflate.js +0 -530
- package/node_modules/ws/lib/receiver.js +0 -743
- package/node_modules/ws/lib/sender.js +0 -607
- package/node_modules/ws/lib/stream.js +0 -161
- package/node_modules/ws/lib/subprotocol.js +0 -62
- package/node_modules/ws/lib/validation.js +0 -152
- package/node_modules/ws/lib/websocket-server.js +0 -562
- package/node_modules/ws/lib/websocket.js +0 -1407
- package/node_modules/ws/package.json +0 -74
- package/node_modules/ws/wrapper.mjs +0 -21
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "../api.js";
|
|
2
|
-
import {
|
|
3
|
-
import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-
|
|
2
|
+
import { n as normalizeProxyIp, s as getHeader } from "./runtime-entry-CbSKxSKk.mjs";
|
|
3
|
+
import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-B7RJNWNG.mjs";
|
|
4
4
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
5
5
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
6
6
|
import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
@@ -15,8 +15,13 @@ const telnyxReplayCache = createWebhookReplayCache();
|
|
|
15
15
|
function sha256Hex(input) {
|
|
16
16
|
return crypto.createHash("sha256").update(input).digest("hex");
|
|
17
17
|
}
|
|
18
|
-
function
|
|
19
|
-
|
|
18
|
+
function skipWebhookVerification(provider, ctx, cache) {
|
|
19
|
+
const replayKey = `${provider}:skip:${sha256Hex(`${ctx.method}\n${ctx.url}\n${ctx.rawBody}`)}`;
|
|
20
|
+
return {
|
|
21
|
+
ok: true,
|
|
22
|
+
reason: "verification skipped (dev mode)",
|
|
23
|
+
...reserveWebhookReplay(cache, replayKey)
|
|
24
|
+
};
|
|
20
25
|
}
|
|
21
26
|
/**
|
|
22
27
|
* Validate Twilio webhook signature using HMAC-SHA1.
|
|
@@ -41,18 +46,10 @@ function buildTwilioDataToSign(url, params) {
|
|
|
41
46
|
function buildCanonicalTwilioParamString(params) {
|
|
42
47
|
return Array.from(params.entries()).toSorted((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0).map(([key, value]) => `${key}=${value}`).join("&");
|
|
43
48
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Validate that a hostname matches RFC 1123 format.
|
|
46
|
-
* Prevents injection of malformed hostnames.
|
|
47
|
-
*/
|
|
48
49
|
function isValidHostname(hostname) {
|
|
49
50
|
if (!hostname || hostname.length > 253) return false;
|
|
50
51
|
return /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)*[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$/.test(hostname);
|
|
51
52
|
}
|
|
52
|
-
/**
|
|
53
|
-
* Safely extract hostname from a host header value.
|
|
54
|
-
* Handles IPv6 addresses and prevents injection via malformed values.
|
|
55
|
-
*/
|
|
56
53
|
function extractHostname(hostHeader) {
|
|
57
54
|
if (!hostHeader) return null;
|
|
58
55
|
if (hostHeader.startsWith("[")) {
|
|
@@ -155,13 +152,12 @@ function reconstructWebhookUrl(ctx, options) {
|
|
|
155
152
|
} catch {
|
|
156
153
|
host = "";
|
|
157
154
|
}
|
|
158
|
-
if (!host) host = "";
|
|
159
155
|
let path = "/";
|
|
160
156
|
try {
|
|
161
157
|
const parsed = new URL(ctx.url);
|
|
162
158
|
path = parsed.pathname + parsed.search;
|
|
163
159
|
} catch {}
|
|
164
|
-
return `${proto}://${formatHostnameForUrl(host)}${path}`;
|
|
160
|
+
return `${proto}://${formatHostnameForUrl(host ?? "")}${path}`;
|
|
165
161
|
}
|
|
166
162
|
function buildTwilioVerificationUrl(ctx, publicUrl, urlOptions) {
|
|
167
163
|
if (!publicUrl) return reconstructWebhookUrl(ctx, urlOptions);
|
|
@@ -242,14 +238,7 @@ function importEd25519PublicKey(publicKey) {
|
|
|
242
238
|
* - `telnyx-timestamp` (Unix seconds)
|
|
243
239
|
*/
|
|
244
240
|
function verifyTelnyxWebhook(ctx, publicKey, options) {
|
|
245
|
-
if (options?.skipVerification)
|
|
246
|
-
const replayKey = createSkippedVerificationReplayKey("telnyx", ctx);
|
|
247
|
-
return {
|
|
248
|
-
ok: true,
|
|
249
|
-
reason: "verification skipped (dev mode)",
|
|
250
|
-
...reserveWebhookReplay(telnyxReplayCache, replayKey)
|
|
251
|
-
};
|
|
252
|
-
}
|
|
241
|
+
if (options?.skipVerification) return skipWebhookVerification("telnyx", ctx, telnyxReplayCache);
|
|
253
242
|
if (!publicKey) return {
|
|
254
243
|
ok: false,
|
|
255
244
|
reason: "Missing telnyx.publicKey (configure to verify webhooks)"
|
|
@@ -292,18 +281,8 @@ function verifyTelnyxWebhook(ctx, publicKey, options) {
|
|
|
292
281
|
};
|
|
293
282
|
}
|
|
294
283
|
}
|
|
295
|
-
/**
|
|
296
|
-
* Verify Twilio webhook with full context and detailed result.
|
|
297
|
-
*/
|
|
298
284
|
function verifyTwilioWebhook(ctx, authToken, options) {
|
|
299
|
-
if (options?.skipVerification)
|
|
300
|
-
const replayKey = createSkippedVerificationReplayKey("twilio", ctx);
|
|
301
|
-
return {
|
|
302
|
-
ok: true,
|
|
303
|
-
reason: "verification skipped (dev mode)",
|
|
304
|
-
...reserveWebhookReplay(twilioReplayCache, replayKey)
|
|
305
|
-
};
|
|
306
|
-
}
|
|
285
|
+
if (options?.skipVerification) return skipWebhookVerification("twilio", ctx, twilioReplayCache);
|
|
307
286
|
const signature = getHeader(ctx.headers, "x-twilio-signature");
|
|
308
287
|
if (!signature) return {
|
|
309
288
|
ok: false,
|
|
@@ -376,28 +355,18 @@ function validatePlivoV2Signature(params) {
|
|
|
376
355
|
const provided = normalizeSignatureBase64(params.signature);
|
|
377
356
|
return safeEqualSecret(expected, provided);
|
|
378
357
|
}
|
|
379
|
-
function toParamMapFromSearchParams(sp) {
|
|
380
|
-
const map = {};
|
|
381
|
-
for (const [key, value] of sp.entries()) {
|
|
382
|
-
if (!map[key]) map[key] = [];
|
|
383
|
-
map[key].push(value);
|
|
384
|
-
}
|
|
385
|
-
return map;
|
|
386
|
-
}
|
|
387
358
|
function sortedPlivoParams(params, format) {
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
}
|
|
394
|
-
return parts.join(format === "query" ? "&" : "");
|
|
359
|
+
return [...params].toSorted(([leftKey, leftValue], [rightKey, rightValue]) => {
|
|
360
|
+
const left = leftKey === rightKey ? leftValue : leftKey;
|
|
361
|
+
const right = leftKey === rightKey ? rightValue : rightKey;
|
|
362
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
363
|
+
}).map(([key, value]) => format === "query" ? `${key}=${value}` : `${key}${value}`).join(format === "query" ? "&" : "");
|
|
395
364
|
}
|
|
396
365
|
function constructPlivoV3BaseUrl(params) {
|
|
397
|
-
const hasPostParams =
|
|
366
|
+
const hasPostParams = params.postParams.size > 0;
|
|
398
367
|
const u = new URL(params.url);
|
|
399
368
|
const baseNoQuery = `${u.protocol}//${u.host}${u.pathname}`;
|
|
400
|
-
const queryString = sortedPlivoParams(
|
|
369
|
+
const queryString = sortedPlivoParams(u.searchParams, "query");
|
|
401
370
|
let baseUrl = baseNoQuery;
|
|
402
371
|
if (queryString.length > 0 || hasPostParams) baseUrl = `${baseNoQuery}?${queryString}`;
|
|
403
372
|
if (queryString.length > 0 && hasPostParams) baseUrl = `${baseUrl}.`;
|
|
@@ -407,9 +376,7 @@ function constructPlivoV3BaseUrl(params) {
|
|
|
407
376
|
function validatePlivoV3Signature(params) {
|
|
408
377
|
const hmacBase = `${params.baseUrl}.${params.nonce}`;
|
|
409
378
|
const expected = normalizeSignatureBase64(crypto.createHmac("sha256", params.authToken).update(hmacBase).digest("base64"));
|
|
410
|
-
|
|
411
|
-
for (const sig of provided) if (safeEqualSecret(expected, sig)) return true;
|
|
412
|
-
return false;
|
|
379
|
+
return normalizeStringEntries(params.signatureHeader.split(",")).map((s) => normalizeSignatureBase64(s)).some((signature) => safeEqualSecret(expected, signature));
|
|
413
380
|
}
|
|
414
381
|
/**
|
|
415
382
|
* Verify Plivo webhooks using V3 signature if present; fall back to V2.
|
|
@@ -419,14 +386,7 @@ function validatePlivoV3Signature(params) {
|
|
|
419
386
|
* - V2: X-Plivo-Signature-V2 / X-Plivo-Signature-V2-Nonce
|
|
420
387
|
*/
|
|
421
388
|
function verifyPlivoWebhook(ctx, authToken, options) {
|
|
422
|
-
if (options?.skipVerification)
|
|
423
|
-
const replayKey = createSkippedVerificationReplayKey("plivo", ctx);
|
|
424
|
-
return {
|
|
425
|
-
ok: true,
|
|
426
|
-
reason: "verification skipped (dev mode)",
|
|
427
|
-
...reserveWebhookReplay(plivoReplayCache, replayKey)
|
|
428
|
-
};
|
|
429
|
-
}
|
|
389
|
+
if (options?.skipVerification) return skipWebhookVerification("plivo", ctx, plivoReplayCache);
|
|
430
390
|
const signatureV3 = getHeader(ctx.headers, "x-plivo-signature-v3");
|
|
431
391
|
const nonceV3 = getHeader(ctx.headers, "x-plivo-signature-v3-nonce");
|
|
432
392
|
const signatureV2 = getHeader(ctx.headers, "x-plivo-signature-v2");
|
|
@@ -454,7 +414,7 @@ function verifyPlivoWebhook(ctx, authToken, options) {
|
|
|
454
414
|
verificationUrl,
|
|
455
415
|
reason: `Unsupported HTTP method for Plivo V3 signature: ${ctx.method}`
|
|
456
416
|
};
|
|
457
|
-
const postParams =
|
|
417
|
+
const postParams = new URLSearchParams(ctx.rawBody);
|
|
458
418
|
const baseUrl = constructPlivoV3BaseUrl({
|
|
459
419
|
method,
|
|
460
420
|
url: verificationUrl,
|
|
@@ -506,34 +466,10 @@ function verifyPlivoWebhook(ctx, authToken, options) {
|
|
|
506
466
|
};
|
|
507
467
|
}
|
|
508
468
|
//#endregion
|
|
509
|
-
//#region extensions/voice-call/src/providers/shared/response-body.ts
|
|
510
|
-
const PROVIDER_JSON_RESPONSE_MAX_BYTES = 1048576;
|
|
511
|
-
const PROVIDER_ERROR_RESPONSE_MAX_BYTES = 8192;
|
|
512
|
-
const TRUNCATED_SUFFIX = "... [truncated]";
|
|
513
|
-
async function cancelProviderResponseBody(response) {
|
|
514
|
-
await response.body?.cancel().catch(() => void 0);
|
|
515
|
-
}
|
|
516
|
-
function appendTruncatedSuffix(text) {
|
|
517
|
-
return `${text.trimEnd()}${TRUNCATED_SUFFIX}`;
|
|
518
|
-
}
|
|
519
|
-
async function readVoiceCallProviderJsonResponse(response, malformedJsonMessage) {
|
|
520
|
-
const body = await readResponseWithLimit(response, PROVIDER_JSON_RESPONSE_MAX_BYTES, { onOverflow: ({ size, maxBytes }) => /* @__PURE__ */ new Error(`provider response body too large: ${size} bytes (limit: ${maxBytes} bytes)`) });
|
|
521
|
-
if (body.byteLength === 0) return;
|
|
522
|
-
try {
|
|
523
|
-
const text = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
524
|
-
return JSON.parse(text);
|
|
525
|
-
} catch (cause) {
|
|
526
|
-
throw new Error(malformedJsonMessage, { cause });
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
async function readProviderErrorResponseSnippet(response) {
|
|
530
|
-
const prefix = await readResponseTextPrefix(response, PROVIDER_ERROR_RESPONSE_MAX_BYTES);
|
|
531
|
-
const text = redactSensitiveText(prefix.text, { mode: "tools" });
|
|
532
|
-
return prefix.truncated ? appendTruncatedSuffix(text) : text;
|
|
533
|
-
}
|
|
534
|
-
//#endregion
|
|
535
469
|
//#region extensions/voice-call/src/providers/shared/guarded-json-api.ts
|
|
536
470
|
const VOICE_CALL_PROVIDER_API_TIMEOUT_MS = 3e4;
|
|
471
|
+
const PROVIDER_JSON_RESPONSE_MAX_BYTES = 1048576;
|
|
472
|
+
const PROVIDER_ERROR_RESPONSE_MAX_BYTES = 8192;
|
|
537
473
|
/** Send a provider JSON request through the SSRF guard and parse bounded JSON responses. */
|
|
538
474
|
async function guardedJsonApiRequest(params) {
|
|
539
475
|
const { response, release } = await fetchWithSsrFGuard({
|
|
@@ -541,7 +477,7 @@ async function guardedJsonApiRequest(params) {
|
|
|
541
477
|
init: {
|
|
542
478
|
method: params.method,
|
|
543
479
|
headers: params.headers,
|
|
544
|
-
body: params.body ? JSON.stringify(params.body) : void 0
|
|
480
|
+
body: params.body instanceof URLSearchParams ? params.body : params.body ? JSON.stringify(params.body) : void 0
|
|
545
481
|
},
|
|
546
482
|
policy: { allowedHostnames: params.allowedHostnames },
|
|
547
483
|
auditContext: params.auditContext,
|
|
@@ -550,13 +486,22 @@ async function guardedJsonApiRequest(params) {
|
|
|
550
486
|
try {
|
|
551
487
|
if (!response.ok) {
|
|
552
488
|
if (params.allowNotFound && response.status === 404) {
|
|
553
|
-
await
|
|
489
|
+
await response.body?.cancel().catch(() => void 0);
|
|
554
490
|
return;
|
|
555
491
|
}
|
|
556
|
-
const
|
|
557
|
-
|
|
492
|
+
const prefix = await readResponseTextPrefix(response, PROVIDER_ERROR_RESPONSE_MAX_BYTES);
|
|
493
|
+
const text = redactSensitiveText(prefix.text, { mode: "tools" });
|
|
494
|
+
const errorText = prefix.truncated ? `${text.trimEnd()}... [truncated]` : text;
|
|
495
|
+
throw params.createError ? params.createError(response.status, errorText) : /* @__PURE__ */ new Error(`${params.errorPrefix}: ${response.status} ${errorText}`);
|
|
496
|
+
}
|
|
497
|
+
const body = await readResponseWithLimit(response, PROVIDER_JSON_RESPONSE_MAX_BYTES, { onOverflow: ({ size, maxBytes }) => /* @__PURE__ */ new Error(`provider response body too large: ${size} bytes (limit: ${maxBytes} bytes)`) });
|
|
498
|
+
if (body.byteLength === 0) return;
|
|
499
|
+
try {
|
|
500
|
+
const text = new TextDecoder("utf-8", { fatal: true }).decode(body);
|
|
501
|
+
return JSON.parse(text);
|
|
502
|
+
} catch (cause) {
|
|
503
|
+
throw new Error(params.malformedJsonMessage ?? `${params.errorPrefix}: malformed JSON response`, { cause });
|
|
558
504
|
}
|
|
559
|
-
return await readVoiceCallProviderJsonResponse(response, `${params.errorPrefix}: malformed JSON response`);
|
|
560
505
|
} finally {
|
|
561
506
|
await release();
|
|
562
507
|
}
|
|
@@ -578,4 +523,4 @@ async function readProviderCallStatus(request, describe) {
|
|
|
578
523
|
}
|
|
579
524
|
}
|
|
580
525
|
//#endregion
|
|
581
|
-
export {
|
|
526
|
+
export { verifyTelnyxWebhook as a, verifyPlivoWebhook as i, readProviderCallStatus as n, verifyTwilioWebhook as o, reconstructWebhookUrl as r, guardedJsonApiRequest as t };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-
|
|
1
|
+
import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-B7RJNWNG.mjs";
|
|
2
2
|
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
3
3
|
import crypto from "node:crypto";
|
|
4
4
|
//#region extensions/voice-call/src/providers/mock.ts
|
|
@@ -26,14 +26,11 @@ var MockProvider = class {
|
|
|
26
26
|
try {
|
|
27
27
|
const payload = JSON.parse(ctx.rawBody);
|
|
28
28
|
const events = [];
|
|
29
|
-
|
|
29
|
+
const candidates = Array.isArray(payload.events) ? payload.events : payload.event ? [payload.event] : [];
|
|
30
|
+
for (const evt of candidates) {
|
|
30
31
|
const normalized = this.normalizeEvent(evt);
|
|
31
32
|
if (normalized) events.push(normalized);
|
|
32
33
|
}
|
|
33
|
-
else if (payload.event) {
|
|
34
|
-
const normalized = this.normalizeEvent(payload.event);
|
|
35
|
-
if (normalized) events.push(normalized);
|
|
36
|
-
}
|
|
37
34
|
return {
|
|
38
35
|
events,
|
|
39
36
|
statusCode: 200
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { s as getHeader } from "./runtime-entry-CbSKxSKk.mjs";
|
|
2
|
+
import { t as escapeXml } from "./voice-mapping-B_iAUkER.mjs";
|
|
3
|
+
import { i as verifyPlivoWebhook, n as readProviderCallStatus, r as reconstructWebhookUrl, t as guardedJsonApiRequest } from "./guarded-json-api-x55yqHvW.mjs";
|
|
3
4
|
import { normalizeLowercaseStringOrEmpty, normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
4
5
|
import crypto from "node:crypto";
|
|
5
6
|
//#region extensions/voice-call/src/providers/plivo.ts
|
|
@@ -87,49 +88,34 @@ var PlivoProvider = class PlivoProvider {
|
|
|
87
88
|
const webhookBase = this.baseWebhookUrlFromCtx(ctx);
|
|
88
89
|
if (webhookBase) this.callUuidToWebhookUrl.set(callUuid, webhookBase);
|
|
89
90
|
}
|
|
91
|
+
const callId = normalizeOptionalString(ctx.query?.callId);
|
|
92
|
+
let event = null;
|
|
93
|
+
let providerResponseBody;
|
|
90
94
|
if (flow === "xml-speak") {
|
|
91
|
-
const callId = normalizeOptionalString(ctx.query?.callId);
|
|
92
95
|
const pending = callId ? this.pendingSpeakByCallId.get(callId) : void 0;
|
|
93
96
|
if (callId) this.pendingSpeakByCallId.delete(callId);
|
|
94
|
-
const actionUrl = pending?.listenAfterPlayback && callId ? this.buildActionUrl(ctx,
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
text: pending.text,
|
|
102
|
-
language: pending.locale,
|
|
103
|
-
actionUrl
|
|
104
|
-
}) : PlivoProvider.xmlSpeak(pending.text, pending.locale) : PlivoProvider.xmlKeepAlive(),
|
|
105
|
-
providerResponseHeaders: { "Content-Type": "text/xml" },
|
|
106
|
-
statusCode: 200
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
if (flow === "xml-listen") {
|
|
110
|
-
const callId = normalizeOptionalString(ctx.query?.callId);
|
|
97
|
+
const actionUrl = pending?.listenAfterPlayback && callId ? this.buildActionUrl(ctx, callId) : null;
|
|
98
|
+
providerResponseBody = pending ? actionUrl ? PlivoProvider.xmlGetInputSpeech({
|
|
99
|
+
text: pending.text,
|
|
100
|
+
language: pending.locale,
|
|
101
|
+
actionUrl
|
|
102
|
+
}) : PlivoProvider.xmlSpeak(pending.text, pending.locale) : PlivoProvider.xmlKeepAlive();
|
|
103
|
+
} else if (flow === "xml-listen") {
|
|
111
104
|
const pending = callId ? this.pendingListenByCallId.get(callId) : void 0;
|
|
112
105
|
if (callId) this.pendingListenByCallId.delete(callId);
|
|
113
|
-
const actionUrl = this.buildActionUrl(ctx,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
}) : PlivoProvider.xmlKeepAlive(),
|
|
123
|
-
providerResponseHeaders: { "Content-Type": "text/xml" },
|
|
124
|
-
statusCode: 200
|
|
125
|
-
};
|
|
106
|
+
const actionUrl = this.buildActionUrl(ctx, callId);
|
|
107
|
+
providerResponseBody = actionUrl && callId ? PlivoProvider.xmlGetInputSpeech({
|
|
108
|
+
actionUrl,
|
|
109
|
+
language: pending?.language
|
|
110
|
+
}) : PlivoProvider.xmlKeepAlive();
|
|
111
|
+
} else {
|
|
112
|
+
const dedupeKey = options?.verifiedRequestKey ?? createPlivoRequestDedupeKey(ctx);
|
|
113
|
+
event = this.normalizeEvent(parsed, callId, dedupeKey);
|
|
114
|
+
providerResponseBody = flow === "answer" || flow === "getinput" ? PlivoProvider.xmlKeepAlive() : PlivoProvider.xmlEmpty();
|
|
126
115
|
}
|
|
127
|
-
const callIdFromQuery = normalizeOptionalString(ctx.query?.callId);
|
|
128
|
-
const dedupeKey = options?.verifiedRequestKey ?? createPlivoRequestDedupeKey(ctx);
|
|
129
|
-
const event = this.normalizeEvent(parsed, callIdFromQuery, dedupeKey);
|
|
130
116
|
return {
|
|
131
117
|
events: event ? [event] : [],
|
|
132
|
-
providerResponseBody
|
|
118
|
+
providerResponseBody,
|
|
133
119
|
providerResponseHeaders: { "Content-Type": "text/xml" },
|
|
134
120
|
statusCode: 200
|
|
135
121
|
};
|
|
@@ -139,8 +125,6 @@ var PlivoProvider = class PlivoProvider {
|
|
|
139
125
|
const requestUuid = params.get("RequestUUID") || "";
|
|
140
126
|
if (requestUuid && callUuid) this.requestUuidToCallUuid.set(requestUuid, callUuid);
|
|
141
127
|
const direction = params.get("Direction");
|
|
142
|
-
const from = params.get("From") || void 0;
|
|
143
|
-
const to = params.get("To") || void 0;
|
|
144
128
|
const callStatus = params.get("CallStatus");
|
|
145
129
|
const baseEvent = {
|
|
146
130
|
id: crypto.randomUUID(),
|
|
@@ -149,8 +133,8 @@ var PlivoProvider = class PlivoProvider {
|
|
|
149
133
|
providerCallId: callUuid || requestUuid || void 0,
|
|
150
134
|
timestamp: Date.now(),
|
|
151
135
|
direction: direction === "inbound" ? "inbound" : direction === "outbound" ? "outbound" : void 0,
|
|
152
|
-
from,
|
|
153
|
-
to
|
|
136
|
+
from: params.get("From") || void 0,
|
|
137
|
+
to: params.get("To") || void 0
|
|
154
138
|
};
|
|
155
139
|
const digits = params.get("Digits");
|
|
156
140
|
if (digits) return {
|
|
@@ -201,7 +185,6 @@ var PlivoProvider = class PlivoProvider {
|
|
|
201
185
|
const hangupUrl = new URL(webhookUrl);
|
|
202
186
|
hangupUrl.searchParams.set("flow", "hangup");
|
|
203
187
|
this.callIdToWebhookUrl.set(input.callId, input.webhookUrl);
|
|
204
|
-
const ringTimeoutSec = this.options.ringTimeoutSec ?? 30;
|
|
205
188
|
const result = await this.apiRequest({
|
|
206
189
|
method: "POST",
|
|
207
190
|
endpoint: "/Call/",
|
|
@@ -212,7 +195,7 @@ var PlivoProvider = class PlivoProvider {
|
|
|
212
195
|
answer_method: "POST",
|
|
213
196
|
hangup_url: hangupUrl.toString(),
|
|
214
197
|
hangup_method: "POST",
|
|
215
|
-
hangup_on_ring: ringTimeoutSec
|
|
198
|
+
hangup_on_ring: this.options.ringTimeoutSec ?? 30
|
|
216
199
|
}
|
|
217
200
|
});
|
|
218
201
|
const requestUuid = Array.isArray(result.request_uuid) ? result.request_uuid[0] : result.request_uuid;
|
|
@@ -224,42 +207,31 @@ var PlivoProvider = class PlivoProvider {
|
|
|
224
207
|
}
|
|
225
208
|
async hangupCall(input) {
|
|
226
209
|
const callUuid = this.requestUuidToCallUuid.get(input.providerCallId);
|
|
227
|
-
if (callUuid) {
|
|
228
|
-
await this.apiRequest({
|
|
229
|
-
method: "DELETE",
|
|
230
|
-
endpoint: `/Call/${callUuid}/`,
|
|
231
|
-
allowNotFound: true
|
|
232
|
-
});
|
|
233
|
-
this.releaseCallState({
|
|
234
|
-
callId: input.callId,
|
|
235
|
-
providerCallId: input.providerCallId,
|
|
236
|
-
callUuid
|
|
237
|
-
});
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
210
|
await this.apiRequest({
|
|
241
211
|
method: "DELETE",
|
|
242
|
-
endpoint: `/Call/${input.providerCallId}/`,
|
|
212
|
+
endpoint: `/Call/${callUuid || input.providerCallId}/`,
|
|
243
213
|
allowNotFound: true
|
|
244
214
|
});
|
|
245
|
-
await this.apiRequest({
|
|
215
|
+
if (!callUuid) await this.apiRequest({
|
|
246
216
|
method: "DELETE",
|
|
247
217
|
endpoint: `/Request/${input.providerCallId}/`,
|
|
248
218
|
allowNotFound: true
|
|
249
219
|
});
|
|
250
220
|
this.releaseCallState({
|
|
251
221
|
callId: input.callId,
|
|
252
|
-
providerCallId: input.providerCallId
|
|
222
|
+
providerCallId: input.providerCallId,
|
|
223
|
+
callUuid
|
|
253
224
|
});
|
|
254
225
|
}
|
|
255
|
-
resolveCallContext(
|
|
256
|
-
const callUuid = this.requestUuidToCallUuid.get(
|
|
257
|
-
const webhookBase = this.callUuidToWebhookUrl.get(callUuid) || this.callIdToWebhookUrl.get(
|
|
226
|
+
resolveCallContext(input, operation) {
|
|
227
|
+
const callUuid = this.requestUuidToCallUuid.get(input.providerCallId) ?? input.providerCallId;
|
|
228
|
+
const webhookBase = this.callUuidToWebhookUrl.get(callUuid) || this.callIdToWebhookUrl.get(input.callId);
|
|
258
229
|
if (!webhookBase) throw new Error("Missing webhook URL for this call (provider state missing)");
|
|
259
|
-
if (!callUuid) throw new Error(`Missing Plivo CallUUID for ${
|
|
230
|
+
if (!callUuid) throw new Error(`Missing Plivo CallUUID for ${operation}`);
|
|
260
231
|
return {
|
|
261
232
|
callUuid,
|
|
262
|
-
webhookBase
|
|
233
|
+
webhookBase,
|
|
234
|
+
callId: input.callId
|
|
263
235
|
};
|
|
264
236
|
}
|
|
265
237
|
async transferCallLeg(params) {
|
|
@@ -278,34 +250,22 @@ var PlivoProvider = class PlivoProvider {
|
|
|
278
250
|
});
|
|
279
251
|
}
|
|
280
252
|
async playTts(input) {
|
|
281
|
-
const
|
|
282
|
-
providerCallId: input.providerCallId,
|
|
283
|
-
callId: input.callId,
|
|
284
|
-
operation: "playTts"
|
|
285
|
-
});
|
|
253
|
+
const context = this.resolveCallContext(input, "playTts");
|
|
286
254
|
this.pendingSpeakByCallId.set(input.callId, {
|
|
287
255
|
text: input.text,
|
|
288
256
|
locale: input.locale,
|
|
289
257
|
listenAfterPlayback: input.listenAfterPlayback
|
|
290
258
|
});
|
|
291
259
|
await this.transferCallLeg({
|
|
292
|
-
|
|
293
|
-
webhookBase,
|
|
294
|
-
callId: input.callId,
|
|
260
|
+
...context,
|
|
295
261
|
flow: "xml-speak"
|
|
296
262
|
});
|
|
297
263
|
}
|
|
298
264
|
async startListening(input) {
|
|
299
|
-
const
|
|
300
|
-
providerCallId: input.providerCallId,
|
|
301
|
-
callId: input.callId,
|
|
302
|
-
operation: "startListening"
|
|
303
|
-
});
|
|
265
|
+
const context = this.resolveCallContext(input, "startListening");
|
|
304
266
|
this.pendingListenByCallId.set(input.callId, { language: input.language });
|
|
305
267
|
await this.transferCallLeg({
|
|
306
|
-
|
|
307
|
-
webhookBase,
|
|
308
|
-
callId: input.callId,
|
|
268
|
+
...context,
|
|
309
269
|
flow: "xml-listen"
|
|
310
270
|
});
|
|
311
271
|
}
|
|
@@ -360,39 +320,26 @@ var PlivoProvider = class PlivoProvider {
|
|
|
360
320
|
}
|
|
361
321
|
static xmlGetInputSpeech(params) {
|
|
362
322
|
const language = params.language || "en-US";
|
|
323
|
+
const prompt = params.text === void 0 ? "" : ` <Speak language="${escapeXml(language)}">${escapeXml(params.text)}</Speak>\n`;
|
|
363
324
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
364
325
|
<Response>
|
|
365
326
|
<GetInput inputType="speech" method="POST" action="${escapeXml(params.actionUrl)}" language="${escapeXml(language)}" executionTimeout="30" speechEndTimeout="2" redirect="false">
|
|
366
|
-
</GetInput>
|
|
327
|
+
${prompt} </GetInput>
|
|
367
328
|
<Wait length="300" />
|
|
368
329
|
</Response>`;
|
|
369
330
|
}
|
|
370
|
-
|
|
371
|
-
const language = params.language || "en-US";
|
|
372
|
-
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
373
|
-
<Response>
|
|
374
|
-
<GetInput inputType="speech" method="POST" action="${escapeXml(params.actionUrl)}" language="${escapeXml(language)}" executionTimeout="30" speechEndTimeout="2" redirect="false">
|
|
375
|
-
<Speak language="${escapeXml(language)}">${escapeXml(params.text)}</Speak>
|
|
376
|
-
</GetInput>
|
|
377
|
-
<Wait length="300" />
|
|
378
|
-
</Response>`;
|
|
379
|
-
}
|
|
380
|
-
buildActionUrl(ctx, opts) {
|
|
331
|
+
buildActionUrl(ctx, callId) {
|
|
381
332
|
const base = this.baseWebhookUrlFromCtx(ctx);
|
|
382
333
|
if (!base) return null;
|
|
383
334
|
const u = new URL(base);
|
|
384
335
|
u.searchParams.set("provider", "plivo");
|
|
385
|
-
u.searchParams.set("flow",
|
|
386
|
-
if (
|
|
336
|
+
u.searchParams.set("flow", "getinput");
|
|
337
|
+
if (callId) u.searchParams.set("callId", callId);
|
|
387
338
|
return u.toString();
|
|
388
339
|
}
|
|
389
340
|
baseWebhookUrlFromCtx(ctx) {
|
|
390
341
|
try {
|
|
391
|
-
|
|
392
|
-
const base = new URL(this.options.publicUrl);
|
|
393
|
-
return `${base.origin}${base.pathname}`;
|
|
394
|
-
}
|
|
395
|
-
const u = new URL(reconstructWebhookUrl(ctx, {
|
|
342
|
+
const u = new URL(this.options.publicUrl || reconstructWebhookUrl(ctx, {
|
|
396
343
|
allowedHosts: this.options.webhookSecurity?.allowedHosts,
|
|
397
344
|
trustForwardingHeaders: this.options.webhookSecurity?.trustForwardingHeaders,
|
|
398
345
|
trustedProxyIPs: this.options.webhookSecurity?.trustedProxyIPs,
|
|
@@ -412,8 +359,8 @@ var PlivoProvider = class PlivoProvider {
|
|
|
412
359
|
"RecognizedSpeech",
|
|
413
360
|
"Text"
|
|
414
361
|
]) {
|
|
415
|
-
const value = params.get(key);
|
|
416
|
-
if (value
|
|
362
|
+
const value = params.get(key)?.trim();
|
|
363
|
+
if (value) return value;
|
|
417
364
|
}
|
|
418
365
|
return null;
|
|
419
366
|
}
|