@openclaw/voice-call 2026.9.8 → 2026.10.1-beta.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.
Files changed (41) hide show
  1. package/dist/.setup/{cli-D6uwMXyS.mjs → cli-Cn1leykM.mjs} +57 -37
  2. package/dist/.setup/{config-Ds7xoZbI.mjs → config-CX9J-053.mjs} +18 -119
  3. package/dist/.setup/{guarded-json-api-Bqc6QS8A.mjs → guarded-json-api-x55yqHvW.mjs} +39 -94
  4. package/dist/.setup/{mock-ClWW6Ufu.mjs → mock-eLv_Dc4o.mjs} +3 -6
  5. package/dist/.setup/{plivo-69ObrQPk.mjs → plivo-CpPIENu4.mjs} +48 -101
  6. package/dist/.setup/{realtime-handler-BgDPcPZt.mjs → realtime-handler-Do7Sz7Gh.mjs} +123 -235
  7. package/dist/.setup/{response-generator-BzKrJMEb.mjs → response-generator-DAhbXEta.mjs} +6 -25
  8. package/dist/.setup/{runtime-entry-BNOQRXii.mjs → runtime-entry-CbSKxSKk.mjs} +554 -425
  9. package/dist/.setup/state-retention-api-ONgqE-_V.mjs +201 -0
  10. package/dist/.setup/store-path-Cfwff6j7.mjs +33 -0
  11. package/dist/.setup/{telnyx-DjFGCfy1.mjs → telnyx-DPilutCH.mjs} +10 -29
  12. package/dist/.setup/{twilio-Bp3gfmUB.mjs → twilio-BWFNGyVx.mjs} +78 -201
  13. package/dist/.setup/{twilio-region-4fkgz3UG.mjs → twilio-region-wBOx-8sz.mjs} +1 -4
  14. package/dist/.setup/voice-mapping-B_iAUkER.mjs +31 -0
  15. package/dist/.setup/{webhook-replay-CCQSoBNF.mjs → webhook-replay-B7RJNWNG.mjs} +2 -5
  16. package/dist/doctor-contract-api.js +7 -330
  17. package/dist/index.js +18 -16
  18. package/dist/runtime-entry.js +1 -1
  19. package/dist/state-retention-api.js +2 -0
  20. package/openclaw.plugin.json +1 -1
  21. package/package.json +4 -6
  22. package/dist/.setup/utils-B5pnM_Ij.mjs +0 -437
  23. package/node_modules/ws/LICENSE +0 -20
  24. package/node_modules/ws/README.md +0 -548
  25. package/node_modules/ws/browser.js +0 -8
  26. package/node_modules/ws/index.js +0 -22
  27. package/node_modules/ws/lib/buffer-util.js +0 -131
  28. package/node_modules/ws/lib/constants.js +0 -19
  29. package/node_modules/ws/lib/event-target.js +0 -292
  30. package/node_modules/ws/lib/extension.js +0 -203
  31. package/node_modules/ws/lib/limiter.js +0 -55
  32. package/node_modules/ws/lib/permessage-deflate.js +0 -530
  33. package/node_modules/ws/lib/receiver.js +0 -743
  34. package/node_modules/ws/lib/sender.js +0 -607
  35. package/node_modules/ws/lib/stream.js +0 -161
  36. package/node_modules/ws/lib/subprotocol.js +0 -62
  37. package/node_modules/ws/lib/validation.js +0 -152
  38. package/node_modules/ws/lib/websocket-server.js +0 -562
  39. package/node_modules/ws/lib/websocket.js +0 -1407
  40. package/node_modules/ws/package.json +0 -74
  41. package/node_modules/ws/wrapper.mjs +0 -21
@@ -1,6 +1,6 @@
1
1
  import { fetchWithSsrFGuard } from "../api.js";
2
- import { l as getHeader, n as normalizeProxyIp } from "./runtime-entry-BNOQRXii.mjs";
3
- import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-CCQSoBNF.mjs";
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 createSkippedVerificationReplayKey(provider, ctx) {
19
- return `${provider}:skip:${sha256Hex(`${ctx.method}\n${ctx.url}\n${ctx.rawBody}`)}`;
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
- const parts = [];
389
- const entries = Object.entries(params).toSorted(([left], [right]) => left < right ? -1 : left > right ? 1 : 0);
390
- for (const [key, entryValues] of entries) {
391
- const values = [...entryValues].toSorted();
392
- for (const value of values) parts.push(format === "query" ? `${key}=${value}` : `${key}${value}`);
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 = Object.keys(params.postParams).length > 0;
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(toParamMapFromSearchParams(u.searchParams), "query");
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
- const provided = normalizeStringEntries(params.signatureHeader.split(",")).map((s) => normalizeSignatureBase64(s));
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 = toParamMapFromSearchParams(new URLSearchParams(ctx.rawBody));
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 cancelProviderResponseBody(response);
489
+ await response.body?.cancel().catch(() => void 0);
554
490
  return;
555
491
  }
556
- const errorText = await readProviderErrorResponseSnippet(response);
557
- throw new Error(`${params.errorPrefix}: ${response.status} ${errorText}`);
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 { readVoiceCallProviderJsonResponse as a, verifyTelnyxWebhook as c, readProviderErrorResponseSnippet as i, verifyTwilioWebhook as l, readProviderCallStatus as n, reconstructWebhookUrl as o, cancelProviderResponseBody as r, verifyPlivoWebhook as s, guardedJsonApiRequest as t };
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-CCQSoBNF.mjs";
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
- if (Array.isArray(payload.events)) for (const evt of payload.events) {
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 { l as getHeader, y as escapeXml } from "./runtime-entry-BNOQRXii.mjs";
2
- import { n as readProviderCallStatus, o as reconstructWebhookUrl, s as verifyPlivoWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-Bqc6QS8A.mjs";
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
- flow: "getinput",
96
- callId
97
- }) : null;
98
- return {
99
- events: [],
100
- providerResponseBody: pending ? actionUrl ? PlivoProvider.xmlSpeakAndListen({
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
- flow: "getinput",
115
- callId
116
- });
117
- return {
118
- events: [],
119
- providerResponseBody: actionUrl && callId ? PlivoProvider.xmlGetInputSpeech({
120
- actionUrl,
121
- language: pending?.language
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: flow === "answer" || flow === "getinput" ? PlivoProvider.xmlKeepAlive() : PlivoProvider.xmlEmpty(),
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(params) {
256
- const callUuid = this.requestUuidToCallUuid.get(params.providerCallId) ?? params.providerCallId;
257
- const webhookBase = this.callUuidToWebhookUrl.get(callUuid) || this.callIdToWebhookUrl.get(params.callId);
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 ${params.operation}`);
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 { callUuid, webhookBase } = this.resolveCallContext({
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
- callUuid,
293
- webhookBase,
294
- callId: input.callId,
260
+ ...context,
295
261
  flow: "xml-speak"
296
262
  });
297
263
  }
298
264
  async startListening(input) {
299
- const { callUuid, webhookBase } = this.resolveCallContext({
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
- callUuid,
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
- static xmlSpeakAndListen(params) {
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", opts.flow);
386
- if (opts.callId) u.searchParams.set("callId", opts.callId);
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
- if (this.options.publicUrl) {
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 && value.trim()) return value.trim();
362
+ const value = params.get(key)?.trim();
363
+ if (value) return value;
417
364
  }
418
365
  return null;
419
366
  }