@openclaw/voice-call 2026.5.2 → 2026.5.3-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.
Files changed (126) hide show
  1. package/dist/api.js +2 -0
  2. package/dist/call-status-CXldV5o8.js +32 -0
  3. package/dist/cli-metadata.js +12 -0
  4. package/dist/config-7w04YpHh.js +548 -0
  5. package/dist/config-compat-B0me39_4.js +129 -0
  6. package/dist/guarded-json-api-Btx5EE4w.js +591 -0
  7. package/dist/http-headers-BrnxBasF.js +10 -0
  8. package/dist/index.js +1284 -0
  9. package/dist/mock-CeKvfVEd.js +135 -0
  10. package/dist/plivo-B-a7KFoT.js +393 -0
  11. package/dist/realtime-handler-B63CIDP2.js +325 -0
  12. package/dist/realtime-transcription.runtime-B2h70y2W.js +2 -0
  13. package/dist/realtime-voice.runtime-Bkh4nvLn.js +2 -0
  14. package/dist/response-generator-BrcmwDZU.js +182 -0
  15. package/dist/response-model-CyF5K80p.js +12 -0
  16. package/dist/runtime-api.js +6 -0
  17. package/dist/runtime-entry-88ytYAQa.js +3119 -0
  18. package/dist/runtime-entry.js +2 -0
  19. package/dist/setup-api.js +37 -0
  20. package/dist/telnyx-jjBE8boz.js +260 -0
  21. package/dist/twilio-1OqbcXLL.js +676 -0
  22. package/dist/voice-mapping-BYDGdWGx.js +40 -0
  23. package/package.json +14 -6
  24. package/api.ts +0 -16
  25. package/cli-metadata.ts +0 -10
  26. package/config-api.ts +0 -12
  27. package/index.test.ts +0 -943
  28. package/index.ts +0 -794
  29. package/runtime-api.ts +0 -20
  30. package/runtime-entry.ts +0 -1
  31. package/setup-api.ts +0 -47
  32. package/src/allowlist.test.ts +0 -18
  33. package/src/allowlist.ts +0 -19
  34. package/src/cli.ts +0 -845
  35. package/src/config-compat.test.ts +0 -120
  36. package/src/config-compat.ts +0 -227
  37. package/src/config.test.ts +0 -479
  38. package/src/config.ts +0 -808
  39. package/src/core-bridge.ts +0 -14
  40. package/src/deep-merge.test.ts +0 -40
  41. package/src/deep-merge.ts +0 -23
  42. package/src/gateway-continue-operation.ts +0 -200
  43. package/src/http-headers.test.ts +0 -16
  44. package/src/http-headers.ts +0 -15
  45. package/src/manager/context.ts +0 -42
  46. package/src/manager/events.test.ts +0 -581
  47. package/src/manager/events.ts +0 -288
  48. package/src/manager/lifecycle.ts +0 -53
  49. package/src/manager/lookup.test.ts +0 -52
  50. package/src/manager/lookup.ts +0 -35
  51. package/src/manager/outbound.test.ts +0 -528
  52. package/src/manager/outbound.ts +0 -486
  53. package/src/manager/state.ts +0 -48
  54. package/src/manager/store.ts +0 -106
  55. package/src/manager/timers.test.ts +0 -129
  56. package/src/manager/timers.ts +0 -113
  57. package/src/manager/twiml.test.ts +0 -13
  58. package/src/manager/twiml.ts +0 -17
  59. package/src/manager.closed-loop.test.ts +0 -236
  60. package/src/manager.inbound-allowlist.test.ts +0 -188
  61. package/src/manager.notify.test.ts +0 -377
  62. package/src/manager.restore.test.ts +0 -183
  63. package/src/manager.test-harness.ts +0 -127
  64. package/src/manager.ts +0 -392
  65. package/src/media-stream.test.ts +0 -768
  66. package/src/media-stream.ts +0 -708
  67. package/src/providers/base.ts +0 -97
  68. package/src/providers/mock.test.ts +0 -78
  69. package/src/providers/mock.ts +0 -185
  70. package/src/providers/plivo.test.ts +0 -93
  71. package/src/providers/plivo.ts +0 -601
  72. package/src/providers/shared/call-status.test.ts +0 -24
  73. package/src/providers/shared/call-status.ts +0 -24
  74. package/src/providers/shared/guarded-json-api.test.ts +0 -106
  75. package/src/providers/shared/guarded-json-api.ts +0 -42
  76. package/src/providers/telnyx.test.ts +0 -340
  77. package/src/providers/telnyx.ts +0 -394
  78. package/src/providers/twilio/api.test.ts +0 -145
  79. package/src/providers/twilio/api.ts +0 -93
  80. package/src/providers/twilio/twiml-policy.test.ts +0 -84
  81. package/src/providers/twilio/twiml-policy.ts +0 -87
  82. package/src/providers/twilio/webhook.ts +0 -34
  83. package/src/providers/twilio.test.ts +0 -591
  84. package/src/providers/twilio.ts +0 -861
  85. package/src/providers/twilio.types.ts +0 -17
  86. package/src/realtime-defaults.ts +0 -3
  87. package/src/realtime-fast-context.test.ts +0 -88
  88. package/src/realtime-fast-context.ts +0 -165
  89. package/src/realtime-transcription.runtime.ts +0 -4
  90. package/src/realtime-voice.runtime.ts +0 -5
  91. package/src/response-generator.test.ts +0 -321
  92. package/src/response-generator.ts +0 -318
  93. package/src/response-model.test.ts +0 -71
  94. package/src/response-model.ts +0 -23
  95. package/src/runtime.test.ts +0 -536
  96. package/src/runtime.ts +0 -510
  97. package/src/telephony-audio.test.ts +0 -61
  98. package/src/telephony-audio.ts +0 -12
  99. package/src/telephony-tts.test.ts +0 -196
  100. package/src/telephony-tts.ts +0 -235
  101. package/src/test-fixtures.ts +0 -73
  102. package/src/tts-provider-voice.test.ts +0 -34
  103. package/src/tts-provider-voice.ts +0 -21
  104. package/src/tunnel.test.ts +0 -166
  105. package/src/tunnel.ts +0 -314
  106. package/src/types.ts +0 -291
  107. package/src/utils.test.ts +0 -17
  108. package/src/utils.ts +0 -14
  109. package/src/voice-mapping.test.ts +0 -34
  110. package/src/voice-mapping.ts +0 -68
  111. package/src/webhook/realtime-handler.test.ts +0 -598
  112. package/src/webhook/realtime-handler.ts +0 -485
  113. package/src/webhook/stale-call-reaper.test.ts +0 -88
  114. package/src/webhook/stale-call-reaper.ts +0 -38
  115. package/src/webhook/tailscale.test.ts +0 -214
  116. package/src/webhook/tailscale.ts +0 -129
  117. package/src/webhook-exposure.test.ts +0 -33
  118. package/src/webhook-exposure.ts +0 -84
  119. package/src/webhook-security.test.ts +0 -770
  120. package/src/webhook-security.ts +0 -994
  121. package/src/webhook.hangup-once.lifecycle.test.ts +0 -135
  122. package/src/webhook.test.ts +0 -1470
  123. package/src/webhook.ts +0 -908
  124. package/src/webhook.types.ts +0 -5
  125. package/src/websocket-test-support.ts +0 -72
  126. package/tsconfig.json +0 -16
@@ -1,601 +0,0 @@
1
- import crypto from "node:crypto";
2
- import {
3
- normalizeLowercaseStringOrEmpty,
4
- normalizeOptionalString,
5
- } from "openclaw/plugin-sdk/text-runtime";
6
- import type { PlivoConfig, WebhookSecurityConfig } from "../config.js";
7
- import { getHeader } from "../http-headers.js";
8
- import type {
9
- GetCallStatusInput,
10
- GetCallStatusResult,
11
- HangupCallInput,
12
- InitiateCallInput,
13
- InitiateCallResult,
14
- NormalizedEvent,
15
- PlayTtsInput,
16
- ProviderWebhookParseResult,
17
- StartListeningInput,
18
- StopListeningInput,
19
- WebhookContext,
20
- WebhookParseOptions,
21
- WebhookVerificationResult,
22
- } from "../types.js";
23
- import { escapeXml } from "../voice-mapping.js";
24
- import { reconstructWebhookUrl, verifyPlivoWebhook } from "../webhook-security.js";
25
- import type { VoiceCallProvider } from "./base.js";
26
- import { guardedJsonApiRequest } from "./shared/guarded-json-api.js";
27
-
28
- export interface PlivoProviderOptions {
29
- /** Override public URL origin for signature verification */
30
- publicUrl?: string;
31
- /** Skip webhook signature verification (development only) */
32
- skipVerification?: boolean;
33
- /** Outbound ring timeout in seconds */
34
- ringTimeoutSec?: number;
35
- /** Webhook security options (forwarded headers/allowlist) */
36
- webhookSecurity?: WebhookSecurityConfig;
37
- }
38
-
39
- type PendingSpeak = { text: string; locale?: string };
40
- type PendingListen = { language?: string };
41
-
42
- function createPlivoRequestDedupeKey(ctx: WebhookContext): string {
43
- const nonceV3 = getHeader(ctx.headers, "x-plivo-signature-v3-nonce");
44
- if (nonceV3) {
45
- return `plivo:v3:${nonceV3}`;
46
- }
47
- const nonceV2 = getHeader(ctx.headers, "x-plivo-signature-v2-nonce");
48
- if (nonceV2) {
49
- return `plivo:v2:${nonceV2}`;
50
- }
51
- return `plivo:fallback:${crypto.createHash("sha256").update(ctx.rawBody).digest("hex")}`;
52
- }
53
-
54
- export class PlivoProvider implements VoiceCallProvider {
55
- readonly name = "plivo" as const;
56
-
57
- private readonly authId: string;
58
- private readonly authToken: string;
59
- private readonly baseUrl: string;
60
- private readonly options: PlivoProviderOptions;
61
- private readonly apiHost: string;
62
-
63
- // Best-effort mapping between create-call request UUID and call UUID.
64
- private requestUuidToCallUuid = new Map<string, string>();
65
-
66
- // Used for transfer URLs and GetInput action URLs.
67
- private callIdToWebhookUrl = new Map<string, string>();
68
- private callUuidToWebhookUrl = new Map<string, string>();
69
-
70
- private pendingSpeakByCallId = new Map<string, PendingSpeak>();
71
- private pendingListenByCallId = new Map<string, PendingListen>();
72
-
73
- constructor(config: PlivoConfig, options: PlivoProviderOptions = {}) {
74
- if (!config.authId) {
75
- throw new Error("Plivo Auth ID is required");
76
- }
77
- if (!config.authToken) {
78
- throw new Error("Plivo Auth Token is required");
79
- }
80
-
81
- this.authId = config.authId;
82
- this.authToken = config.authToken;
83
- this.baseUrl = `https://api.plivo.com/v1/Account/${this.authId}`;
84
- this.apiHost = new URL(this.baseUrl).hostname;
85
- this.options = options;
86
- }
87
-
88
- private async apiRequest<T = unknown>(params: {
89
- method: "GET" | "POST" | "DELETE";
90
- endpoint: string;
91
- body?: Record<string, unknown>;
92
- allowNotFound?: boolean;
93
- }): Promise<T> {
94
- const { method, endpoint, body, allowNotFound } = params;
95
- return await guardedJsonApiRequest<T>({
96
- url: `${this.baseUrl}${endpoint}`,
97
- method,
98
- headers: {
99
- Authorization: `Basic ${Buffer.from(`${this.authId}:${this.authToken}`).toString("base64")}`,
100
- "Content-Type": "application/json",
101
- },
102
- body,
103
- allowNotFound,
104
- allowedHostnames: [this.apiHost],
105
- auditContext: "voice-call.plivo.api",
106
- errorPrefix: "Plivo API error",
107
- });
108
- }
109
-
110
- verifyWebhook(ctx: WebhookContext): WebhookVerificationResult {
111
- const result = verifyPlivoWebhook(ctx, this.authToken, {
112
- publicUrl: this.options.publicUrl,
113
- skipVerification: this.options.skipVerification,
114
- allowedHosts: this.options.webhookSecurity?.allowedHosts,
115
- trustForwardingHeaders: this.options.webhookSecurity?.trustForwardingHeaders,
116
- trustedProxyIPs: this.options.webhookSecurity?.trustedProxyIPs,
117
- remoteIP: ctx.remoteAddress,
118
- });
119
-
120
- if (!result.ok) {
121
- console.warn(`[plivo] Webhook verification failed: ${result.reason}`);
122
- }
123
-
124
- return {
125
- ok: result.ok,
126
- reason: result.reason,
127
- isReplay: result.isReplay,
128
- verifiedRequestKey: result.verifiedRequestKey,
129
- };
130
- }
131
-
132
- parseWebhookEvent(
133
- ctx: WebhookContext,
134
- options?: WebhookParseOptions,
135
- ): ProviderWebhookParseResult {
136
- const flow = normalizeOptionalString(ctx.query?.flow) ?? "";
137
-
138
- const parsed = this.parseBody(ctx.rawBody);
139
- if (!parsed) {
140
- return { events: [], statusCode: 400 };
141
- }
142
-
143
- // Keep providerCallId mapping for later call control.
144
- const callUuid = parsed.get("CallUUID") || undefined;
145
- if (callUuid) {
146
- const webhookBase = this.baseWebhookUrlFromCtx(ctx);
147
- if (webhookBase) {
148
- this.callUuidToWebhookUrl.set(callUuid, webhookBase);
149
- }
150
- }
151
-
152
- // Special flows that exist only to return Plivo XML (no events).
153
- if (flow === "xml-speak") {
154
- const callId = this.getCallIdFromQuery(ctx);
155
- const pending = callId ? this.pendingSpeakByCallId.get(callId) : undefined;
156
- if (callId) {
157
- this.pendingSpeakByCallId.delete(callId);
158
- }
159
-
160
- const xml = pending
161
- ? PlivoProvider.xmlSpeak(pending.text, pending.locale)
162
- : PlivoProvider.xmlKeepAlive();
163
- return {
164
- events: [],
165
- providerResponseBody: xml,
166
- providerResponseHeaders: { "Content-Type": "text/xml" },
167
- statusCode: 200,
168
- };
169
- }
170
-
171
- if (flow === "xml-listen") {
172
- const callId = this.getCallIdFromQuery(ctx);
173
- const pending = callId ? this.pendingListenByCallId.get(callId) : undefined;
174
- if (callId) {
175
- this.pendingListenByCallId.delete(callId);
176
- }
177
-
178
- const actionUrl = this.buildActionUrl(ctx, {
179
- flow: "getinput",
180
- callId,
181
- });
182
-
183
- const xml =
184
- actionUrl && callId
185
- ? PlivoProvider.xmlGetInputSpeech({
186
- actionUrl,
187
- language: pending?.language,
188
- })
189
- : PlivoProvider.xmlKeepAlive();
190
-
191
- return {
192
- events: [],
193
- providerResponseBody: xml,
194
- providerResponseHeaders: { "Content-Type": "text/xml" },
195
- statusCode: 200,
196
- };
197
- }
198
-
199
- // Normal events.
200
- const callIdFromQuery = this.getCallIdFromQuery(ctx);
201
- const dedupeKey = options?.verifiedRequestKey ?? createPlivoRequestDedupeKey(ctx);
202
- const event = this.normalizeEvent(parsed, callIdFromQuery, dedupeKey);
203
-
204
- return {
205
- events: event ? [event] : [],
206
- providerResponseBody:
207
- flow === "answer" || flow === "getinput"
208
- ? PlivoProvider.xmlKeepAlive()
209
- : PlivoProvider.xmlEmpty(),
210
- providerResponseHeaders: { "Content-Type": "text/xml" },
211
- statusCode: 200,
212
- };
213
- }
214
-
215
- private normalizeEvent(
216
- params: URLSearchParams,
217
- callIdOverride?: string,
218
- dedupeKey?: string,
219
- ): NormalizedEvent | null {
220
- const callUuid = params.get("CallUUID") || "";
221
- const requestUuid = params.get("RequestUUID") || "";
222
-
223
- if (requestUuid && callUuid) {
224
- this.requestUuidToCallUuid.set(requestUuid, callUuid);
225
- }
226
-
227
- const direction = params.get("Direction");
228
- const from = params.get("From") || undefined;
229
- const to = params.get("To") || undefined;
230
- const callStatus = params.get("CallStatus");
231
-
232
- const baseEvent = {
233
- id: crypto.randomUUID(),
234
- dedupeKey,
235
- callId: callIdOverride || callUuid || requestUuid,
236
- providerCallId: callUuid || requestUuid || undefined,
237
- timestamp: Date.now(),
238
- direction:
239
- direction === "inbound"
240
- ? ("inbound" as const)
241
- : direction === "outbound"
242
- ? ("outbound" as const)
243
- : undefined,
244
- from,
245
- to,
246
- };
247
-
248
- const digits = params.get("Digits");
249
- if (digits) {
250
- return { ...baseEvent, type: "call.dtmf", digits };
251
- }
252
-
253
- const transcript = PlivoProvider.extractTranscript(params);
254
- if (transcript) {
255
- return {
256
- ...baseEvent,
257
- type: "call.speech",
258
- transcript,
259
- isFinal: true,
260
- };
261
- }
262
-
263
- // Call lifecycle.
264
- if (callStatus === "ringing") {
265
- return { ...baseEvent, type: "call.ringing" };
266
- }
267
-
268
- if (callStatus === "in-progress") {
269
- return { ...baseEvent, type: "call.answered" };
270
- }
271
-
272
- if (
273
- callStatus === "completed" ||
274
- callStatus === "busy" ||
275
- callStatus === "no-answer" ||
276
- callStatus === "failed"
277
- ) {
278
- return {
279
- ...baseEvent,
280
- type: "call.ended",
281
- reason:
282
- callStatus === "completed"
283
- ? "completed"
284
- : callStatus === "busy"
285
- ? "busy"
286
- : callStatus === "no-answer"
287
- ? "no-answer"
288
- : "failed",
289
- };
290
- }
291
-
292
- // Plivo will call our answer_url when the call is answered; if we don't have
293
- // a CallStatus for some reason, treat it as answered so the call can proceed.
294
- if (params.get("Event") === "StartApp" && callUuid) {
295
- return { ...baseEvent, type: "call.answered" };
296
- }
297
-
298
- return null;
299
- }
300
-
301
- async initiateCall(input: InitiateCallInput): Promise<InitiateCallResult> {
302
- const webhookUrl = new URL(input.webhookUrl);
303
- webhookUrl.searchParams.set("provider", "plivo");
304
- webhookUrl.searchParams.set("callId", input.callId);
305
-
306
- const answerUrl = new URL(webhookUrl);
307
- answerUrl.searchParams.set("flow", "answer");
308
-
309
- const hangupUrl = new URL(webhookUrl);
310
- hangupUrl.searchParams.set("flow", "hangup");
311
-
312
- this.callIdToWebhookUrl.set(input.callId, input.webhookUrl);
313
-
314
- const ringTimeoutSec = this.options.ringTimeoutSec ?? 30;
315
-
316
- const result = await this.apiRequest<PlivoCreateCallResponse>({
317
- method: "POST",
318
- endpoint: "/Call/",
319
- body: {
320
- from: PlivoProvider.normalizeNumber(input.from),
321
- to: PlivoProvider.normalizeNumber(input.to),
322
- answer_url: answerUrl.toString(),
323
- answer_method: "POST",
324
- hangup_url: hangupUrl.toString(),
325
- hangup_method: "POST",
326
- // Plivo's API uses `hangup_on_ring` for outbound ring timeout.
327
- hangup_on_ring: ringTimeoutSec,
328
- },
329
- });
330
-
331
- const requestUuid = Array.isArray(result.request_uuid)
332
- ? result.request_uuid[0]
333
- : result.request_uuid;
334
- if (!requestUuid) {
335
- throw new Error("Plivo call create returned no request_uuid");
336
- }
337
-
338
- return { providerCallId: requestUuid, status: "initiated" };
339
- }
340
-
341
- async hangupCall(input: HangupCallInput): Promise<void> {
342
- const callUuid = this.requestUuidToCallUuid.get(input.providerCallId);
343
- if (callUuid) {
344
- await this.apiRequest({
345
- method: "DELETE",
346
- endpoint: `/Call/${callUuid}/`,
347
- allowNotFound: true,
348
- });
349
- return;
350
- }
351
-
352
- // Best-effort: try hangup (call UUID), then cancel (request UUID).
353
- await this.apiRequest({
354
- method: "DELETE",
355
- endpoint: `/Call/${input.providerCallId}/`,
356
- allowNotFound: true,
357
- });
358
- await this.apiRequest({
359
- method: "DELETE",
360
- endpoint: `/Request/${input.providerCallId}/`,
361
- allowNotFound: true,
362
- });
363
- }
364
-
365
- private resolveCallContext(params: {
366
- providerCallId: string;
367
- callId: string;
368
- operation: string;
369
- }): {
370
- callUuid: string;
371
- webhookBase: string;
372
- } {
373
- const callUuid = this.requestUuidToCallUuid.get(params.providerCallId) ?? params.providerCallId;
374
- const webhookBase =
375
- this.callUuidToWebhookUrl.get(callUuid) || this.callIdToWebhookUrl.get(params.callId);
376
- if (!webhookBase) {
377
- throw new Error("Missing webhook URL for this call (provider state missing)");
378
- }
379
- if (!callUuid) {
380
- throw new Error(`Missing Plivo CallUUID for ${params.operation}`);
381
- }
382
- return { callUuid, webhookBase };
383
- }
384
-
385
- private async transferCallLeg(params: {
386
- callUuid: string;
387
- webhookBase: string;
388
- callId: string;
389
- flow: "xml-speak" | "xml-listen";
390
- }): Promise<void> {
391
- const transferUrl = new URL(params.webhookBase);
392
- transferUrl.searchParams.set("provider", "plivo");
393
- transferUrl.searchParams.set("flow", params.flow);
394
- transferUrl.searchParams.set("callId", params.callId);
395
-
396
- await this.apiRequest({
397
- method: "POST",
398
- endpoint: `/Call/${params.callUuid}/`,
399
- body: {
400
- legs: "aleg",
401
- aleg_url: transferUrl.toString(),
402
- aleg_method: "POST",
403
- },
404
- });
405
- }
406
-
407
- async playTts(input: PlayTtsInput): Promise<void> {
408
- const { callUuid, webhookBase } = this.resolveCallContext({
409
- providerCallId: input.providerCallId,
410
- callId: input.callId,
411
- operation: "playTts",
412
- });
413
-
414
- this.pendingSpeakByCallId.set(input.callId, {
415
- text: input.text,
416
- locale: input.locale,
417
- });
418
-
419
- await this.transferCallLeg({
420
- callUuid,
421
- webhookBase,
422
- callId: input.callId,
423
- flow: "xml-speak",
424
- });
425
- }
426
-
427
- async startListening(input: StartListeningInput): Promise<void> {
428
- const { callUuid, webhookBase } = this.resolveCallContext({
429
- providerCallId: input.providerCallId,
430
- callId: input.callId,
431
- operation: "startListening",
432
- });
433
-
434
- this.pendingListenByCallId.set(input.callId, {
435
- language: input.language,
436
- });
437
-
438
- await this.transferCallLeg({
439
- callUuid,
440
- webhookBase,
441
- callId: input.callId,
442
- flow: "xml-listen",
443
- });
444
- }
445
-
446
- async stopListening(_input: StopListeningInput): Promise<void> {
447
- // GetInput ends automatically when speech ends.
448
- }
449
-
450
- async getCallStatus(input: GetCallStatusInput): Promise<GetCallStatusResult> {
451
- const terminalStatuses = new Set([
452
- "completed",
453
- "busy",
454
- "failed",
455
- "timeout",
456
- "no-answer",
457
- "cancel",
458
- "machine",
459
- "hangup",
460
- ]);
461
- try {
462
- const data = await guardedJsonApiRequest<{ call_status?: string }>({
463
- url: `${this.baseUrl}/Call/${input.providerCallId}/`,
464
- method: "GET",
465
- headers: {
466
- Authorization: `Basic ${Buffer.from(`${this.authId}:${this.authToken}`).toString("base64")}`,
467
- },
468
- allowNotFound: true,
469
- allowedHostnames: [this.apiHost],
470
- auditContext: "plivo-get-call-status",
471
- errorPrefix: "Plivo get call status error",
472
- });
473
-
474
- if (!data) {
475
- return { status: "not-found", isTerminal: true };
476
- }
477
-
478
- const status = data.call_status ?? "unknown";
479
- return { status, isTerminal: terminalStatuses.has(status) };
480
- } catch {
481
- return { status: "error", isTerminal: false, isUnknown: true };
482
- }
483
- }
484
-
485
- private static normalizeNumber(numberOrSip: string): string {
486
- const trimmed = numberOrSip.trim();
487
- if (normalizeLowercaseStringOrEmpty(trimmed).startsWith("sip:")) {
488
- return trimmed;
489
- }
490
- return trimmed.replace(/[^\d+]/g, "");
491
- }
492
-
493
- private static xmlEmpty(): string {
494
- return `<?xml version="1.0" encoding="UTF-8"?><Response></Response>`;
495
- }
496
-
497
- private static xmlKeepAlive(): string {
498
- return `<?xml version="1.0" encoding="UTF-8"?>
499
- <Response>
500
- <Wait length="300" />
501
- </Response>`;
502
- }
503
-
504
- private static xmlSpeak(text: string, locale?: string): string {
505
- const language = locale || "en-US";
506
- return `<?xml version="1.0" encoding="UTF-8"?>
507
- <Response>
508
- <Speak language="${escapeXml(language)}">${escapeXml(text)}</Speak>
509
- <Wait length="300" />
510
- </Response>`;
511
- }
512
-
513
- private static xmlGetInputSpeech(params: { actionUrl: string; language?: string }): string {
514
- const language = params.language || "en-US";
515
- return `<?xml version="1.0" encoding="UTF-8"?>
516
- <Response>
517
- <GetInput inputType="speech" method="POST" action="${escapeXml(params.actionUrl)}" language="${escapeXml(language)}" executionTimeout="30" speechEndTimeout="1" redirect="false">
518
- </GetInput>
519
- <Wait length="300" />
520
- </Response>`;
521
- }
522
-
523
- private getCallIdFromQuery(ctx: WebhookContext): string | undefined {
524
- const callId = normalizeOptionalString(ctx.query?.callId);
525
- return callId || undefined;
526
- }
527
-
528
- private buildActionUrl(
529
- ctx: WebhookContext,
530
- opts: { flow: string; callId?: string },
531
- ): string | null {
532
- const base = this.baseWebhookUrlFromCtx(ctx);
533
- if (!base) {
534
- return null;
535
- }
536
-
537
- const u = new URL(base);
538
- u.searchParams.set("provider", "plivo");
539
- u.searchParams.set("flow", opts.flow);
540
- if (opts.callId) {
541
- u.searchParams.set("callId", opts.callId);
542
- }
543
- return u.toString();
544
- }
545
-
546
- private baseWebhookUrlFromCtx(ctx: WebhookContext): string | null {
547
- try {
548
- if (this.options.publicUrl) {
549
- const base = new URL(this.options.publicUrl);
550
- const requestUrl = new URL(ctx.url);
551
- base.pathname = requestUrl.pathname;
552
- return `${base.origin}${base.pathname}`;
553
- }
554
-
555
- const u = new URL(
556
- reconstructWebhookUrl(ctx, {
557
- allowedHosts: this.options.webhookSecurity?.allowedHosts,
558
- trustForwardingHeaders: this.options.webhookSecurity?.trustForwardingHeaders,
559
- trustedProxyIPs: this.options.webhookSecurity?.trustedProxyIPs,
560
- remoteIP: ctx.remoteAddress,
561
- }),
562
- );
563
- return `${u.origin}${u.pathname}`;
564
- } catch {
565
- return null;
566
- }
567
- }
568
-
569
- private parseBody(rawBody: string): URLSearchParams | null {
570
- try {
571
- return new URLSearchParams(rawBody);
572
- } catch {
573
- return null;
574
- }
575
- }
576
-
577
- private static extractTranscript(params: URLSearchParams): string | null {
578
- const candidates = [
579
- "Speech",
580
- "Transcription",
581
- "TranscriptionText",
582
- "SpeechResult",
583
- "RecognizedSpeech",
584
- "Text",
585
- ] as const;
586
-
587
- for (const key of candidates) {
588
- const value = params.get(key);
589
- if (value && value.trim()) {
590
- return value.trim();
591
- }
592
- }
593
- return null;
594
- }
595
- }
596
-
597
- type PlivoCreateCallResponse = {
598
- api_id?: string;
599
- message?: string;
600
- request_uuid?: string | string[];
601
- };
@@ -1,24 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- isProviderStatusTerminal,
4
- mapProviderStatusToEndReason,
5
- normalizeProviderStatus,
6
- } from "./call-status.js";
7
-
8
- describe("provider call status mapping", () => {
9
- it("normalizes missing statuses to unknown", () => {
10
- expect(normalizeProviderStatus(undefined)).toBe("unknown");
11
- expect(normalizeProviderStatus(" ")).toBe("unknown");
12
- });
13
-
14
- it("maps terminal provider statuses to end reasons", () => {
15
- expect(mapProviderStatusToEndReason("completed")).toBe("completed");
16
- expect(mapProviderStatusToEndReason("CANCELED")).toBe("hangup-bot");
17
- expect(mapProviderStatusToEndReason("no-answer")).toBe("no-answer");
18
- });
19
-
20
- it("flags terminal provider statuses", () => {
21
- expect(isProviderStatusTerminal("busy")).toBe(true);
22
- expect(isProviderStatusTerminal("in-progress")).toBe(false);
23
- });
24
- });
@@ -1,24 +0,0 @@
1
- import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
2
- import type { EndReason } from "../../types.js";
3
-
4
- const TERMINAL_PROVIDER_STATUS_TO_END_REASON: Record<string, EndReason> = {
5
- completed: "completed",
6
- failed: "failed",
7
- busy: "busy",
8
- "no-answer": "no-answer",
9
- canceled: "hangup-bot",
10
- };
11
-
12
- export function normalizeProviderStatus(status: string | null | undefined): string {
13
- const normalized = normalizeOptionalLowercaseString(status);
14
- return normalized && normalized.length > 0 ? normalized : "unknown";
15
- }
16
-
17
- export function mapProviderStatusToEndReason(status: string | null | undefined): EndReason | null {
18
- const normalized = normalizeProviderStatus(status);
19
- return TERMINAL_PROVIDER_STATUS_TO_END_REASON[normalized] ?? null;
20
- }
21
-
22
- export function isProviderStatusTerminal(status: string | null | undefined): boolean {
23
- return mapProviderStatusToEndReason(status) !== null;
24
- }