@openclaw/voice-call 2026.5.2 → 2026.5.3-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 (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
package/dist/api.js ADDED
@@ -0,0 +1,2 @@
1
+ import { TtsAutoSchema, TtsConfigSchema, TtsModeSchema, TtsProviderSchema, definePluginEntry, fetchWithSsrFGuard, isBlockedHostnameOrIp, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, sleep } from "./runtime-api.js";
2
+ export { TtsAutoSchema, TtsConfigSchema, TtsModeSchema, TtsProviderSchema, definePluginEntry, fetchWithSsrFGuard, isBlockedHostnameOrIp, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, sleep };
@@ -0,0 +1,32 @@
1
+ import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runtime";
2
+ import { convertPcmToMulaw8k } from "openclaw/plugin-sdk/realtime-voice";
3
+ //#region extensions/voice-call/src/telephony-audio.ts
4
+ /**
5
+ * Chunk audio buffer into 20ms frames for streaming (8kHz mono mu-law).
6
+ */
7
+ function chunkAudio(audio, chunkSize = 160) {
8
+ return (function* () {
9
+ for (let i = 0; i < audio.length; i += chunkSize) yield audio.subarray(i, Math.min(i + chunkSize, audio.length));
10
+ })();
11
+ }
12
+ //#endregion
13
+ //#region extensions/voice-call/src/providers/shared/call-status.ts
14
+ const TERMINAL_PROVIDER_STATUS_TO_END_REASON = {
15
+ completed: "completed",
16
+ failed: "failed",
17
+ busy: "busy",
18
+ "no-answer": "no-answer",
19
+ canceled: "hangup-bot"
20
+ };
21
+ function normalizeProviderStatus(status) {
22
+ const normalized = normalizeOptionalLowercaseString(status);
23
+ return normalized && normalized.length > 0 ? normalized : "unknown";
24
+ }
25
+ function mapProviderStatusToEndReason(status) {
26
+ return TERMINAL_PROVIDER_STATUS_TO_END_REASON[normalizeProviderStatus(status)] ?? null;
27
+ }
28
+ function isProviderStatusTerminal(status) {
29
+ return mapProviderStatusToEndReason(status) !== null;
30
+ }
31
+ //#endregion
32
+ export { convertPcmToMulaw8k as a, chunkAudio as i, mapProviderStatusToEndReason as n, normalizeProviderStatus as r, isProviderStatusTerminal as t };
@@ -0,0 +1,12 @@
1
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
2
+ //#region extensions/voice-call/cli-metadata.ts
3
+ var cli_metadata_default = definePluginEntry({
4
+ id: "voice-call",
5
+ name: "Voice Call",
6
+ description: "Voice call channel plugin",
7
+ register(api) {
8
+ api.registerCli(() => {}, { commands: ["voicecall"] });
9
+ }
10
+ });
11
+ //#endregion
12
+ export { cli_metadata_default as default };
@@ -0,0 +1,548 @@
1
+ import { TtsConfigSchema } from "./runtime-api.js";
2
+ import "./api.js";
3
+ import { REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME, REALTIME_VOICE_AGENT_CONSULT_TOOL_POLICIES } from "openclaw/plugin-sdk/realtime-voice";
4
+ import { buildSecretInputSchema, hasConfiguredSecretInput, normalizeResolvedSecretInputString } from "openclaw/plugin-sdk/secret-input";
5
+ import { z } from "openclaw/plugin-sdk/zod";
6
+ //#region extensions/voice-call/src/deep-merge.ts
7
+ const BLOCKED_MERGE_KEYS = new Set([
8
+ "__proto__",
9
+ "prototype",
10
+ "constructor"
11
+ ]);
12
+ function deepMergeDefined(base, override) {
13
+ if (!isPlainObject(base) || !isPlainObject(override)) return override === void 0 ? base : override;
14
+ const result = { ...base };
15
+ for (const [key, value] of Object.entries(override)) {
16
+ if (BLOCKED_MERGE_KEYS.has(key) || value === void 0) continue;
17
+ const existing = result[key];
18
+ result[key] = key in result ? deepMergeDefined(existing, value) : value;
19
+ }
20
+ return result;
21
+ }
22
+ function isPlainObject(value) {
23
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
24
+ }
25
+ //#endregion
26
+ //#region extensions/voice-call/src/realtime-defaults.ts
27
+ const DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS = `You are OpenClaw's phone-call realtime voice interface. Keep spoken replies brief and natural. When a question needs deeper reasoning, current information, or tools, call ${REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME} before answering.`;
28
+ //#endregion
29
+ //#region extensions/voice-call/src/config.ts
30
+ /**
31
+ * E.164 phone number format: +[country code][number]
32
+ * Examples use 555 prefix (reserved for fictional numbers)
33
+ */
34
+ const E164Schema = z.string().regex(/^\+[1-9]\d{1,14}$/, "Expected E.164 format, e.g. +15550001234");
35
+ /**
36
+ * Controls how inbound calls are handled:
37
+ * - "disabled": Block all inbound calls (outbound only)
38
+ * - "allowlist": Only accept calls from numbers in allowFrom
39
+ * - "pairing": Unknown callers can request pairing (future)
40
+ * - "open": Accept all inbound calls (dangerous!)
41
+ */
42
+ const InboundPolicySchema = z.enum([
43
+ "disabled",
44
+ "allowlist",
45
+ "pairing",
46
+ "open"
47
+ ]);
48
+ const SecretInputSchema = buildSecretInputSchema();
49
+ const TelnyxConfigSchema = z.object({
50
+ /** Telnyx API v2 key */
51
+ apiKey: z.string().min(1).optional(),
52
+ /** Telnyx connection ID (from Call Control app) */
53
+ connectionId: z.string().min(1).optional(),
54
+ /** Public key for webhook signature verification */
55
+ publicKey: z.string().min(1).optional()
56
+ }).strict();
57
+ const TwilioConfigSchema = z.object({
58
+ /** Twilio Account SID */
59
+ accountSid: z.string().min(1).optional(),
60
+ /** Twilio Auth Token */
61
+ authToken: SecretInputSchema.optional()
62
+ }).strict();
63
+ const PlivoConfigSchema = z.object({
64
+ /** Plivo Auth ID (starts with MA/SA) */
65
+ authId: z.string().min(1).optional(),
66
+ /** Plivo Auth Token */
67
+ authToken: z.string().min(1).optional()
68
+ }).strict();
69
+ const VoiceCallNumberRouteConfigSchema = z.object({
70
+ /** Greeting message for inbound calls to this number. */
71
+ inboundGreeting: z.string().optional(),
72
+ /** TTS override for inbound calls to this number. Deep-merges with global voice-call TTS. */
73
+ tts: TtsConfigSchema,
74
+ /** Agent ID to use for voice response generation for this number. */
75
+ agentId: z.string().min(1).optional(),
76
+ /** Optional model override for voice responses for this number. */
77
+ responseModel: z.string().optional(),
78
+ /** System prompt for voice responses for this number. */
79
+ responseSystemPrompt: z.string().optional(),
80
+ /** Timeout for response generation in ms for this number. */
81
+ responseTimeoutMs: z.number().int().positive().optional()
82
+ }).strict();
83
+ const VoiceCallServeConfigSchema = z.object({
84
+ /** Port to listen on */
85
+ port: z.number().int().positive().default(3334),
86
+ /** Bind address */
87
+ bind: z.string().default("127.0.0.1"),
88
+ /** Webhook path */
89
+ path: z.string().min(1).default("/voice/webhook")
90
+ }).strict().default({
91
+ port: 3334,
92
+ bind: "127.0.0.1",
93
+ path: "/voice/webhook"
94
+ });
95
+ const VoiceCallTailscaleConfigSchema = z.object({
96
+ /**
97
+ * Tailscale exposure mode:
98
+ * - "off": No Tailscale exposure
99
+ * - "serve": Tailscale serve (private to tailnet)
100
+ * - "funnel": Tailscale funnel (public HTTPS)
101
+ */
102
+ mode: z.enum([
103
+ "off",
104
+ "serve",
105
+ "funnel"
106
+ ]).default("off"),
107
+ /** Path for Tailscale serve/funnel (should usually match serve.path) */
108
+ path: z.string().min(1).default("/voice/webhook")
109
+ }).strict().default({
110
+ mode: "off",
111
+ path: "/voice/webhook"
112
+ });
113
+ const VoiceCallTunnelConfigSchema = z.object({
114
+ /**
115
+ * Tunnel provider:
116
+ * - "none": No tunnel (use publicUrl if set, or manual setup)
117
+ * - "ngrok": Use ngrok for public HTTPS tunnel
118
+ * - "tailscale-serve": Tailscale serve (private to tailnet)
119
+ * - "tailscale-funnel": Tailscale funnel (public HTTPS)
120
+ */
121
+ provider: z.enum([
122
+ "none",
123
+ "ngrok",
124
+ "tailscale-serve",
125
+ "tailscale-funnel"
126
+ ]).default("none"),
127
+ /** ngrok auth token (optional, enables longer sessions and more features) */
128
+ ngrokAuthToken: z.string().min(1).optional(),
129
+ /** ngrok custom domain (paid feature, e.g., "myapp.ngrok.io") */
130
+ ngrokDomain: z.string().min(1).optional(),
131
+ /**
132
+ * Allow ngrok free tier compatibility mode.
133
+ * When true, forwarded headers may be trusted for loopback requests
134
+ * to reconstruct the public ngrok URL used for signing.
135
+ *
136
+ * IMPORTANT: This does NOT bypass signature verification.
137
+ */
138
+ allowNgrokFreeTierLoopbackBypass: z.boolean().default(false)
139
+ }).strict().default({
140
+ provider: "none",
141
+ allowNgrokFreeTierLoopbackBypass: false
142
+ });
143
+ const VoiceCallWebhookSecurityConfigSchema = z.object({
144
+ /**
145
+ * Allowed hostnames for webhook URL reconstruction.
146
+ * Only these hosts are accepted from forwarding headers.
147
+ */
148
+ allowedHosts: z.array(z.string().min(1)).default([]),
149
+ /**
150
+ * Trust X-Forwarded-* headers without a hostname allowlist.
151
+ * WARNING: Only enable if you trust your proxy configuration.
152
+ */
153
+ trustForwardingHeaders: z.boolean().default(false),
154
+ /**
155
+ * Trusted proxy IP addresses. Forwarded headers are only trusted when
156
+ * the remote IP matches one of these addresses.
157
+ */
158
+ trustedProxyIPs: z.array(z.string().min(1)).default([])
159
+ }).strict().default({
160
+ allowedHosts: [],
161
+ trustForwardingHeaders: false,
162
+ trustedProxyIPs: []
163
+ });
164
+ /**
165
+ * Call mode determines how outbound calls behave:
166
+ * - "notify": Deliver message and auto-hangup after delay (one-way notification)
167
+ * - "conversation": Stay open for back-and-forth until explicit end or timeout
168
+ */
169
+ const CallModeSchema = z.enum(["notify", "conversation"]);
170
+ const VoiceCallSessionScopeSchema = z.enum(["per-phone", "per-call"]);
171
+ const OutboundConfigSchema = z.object({
172
+ /** Default call mode for outbound calls */
173
+ defaultMode: CallModeSchema.default("notify"),
174
+ /** Seconds to wait after TTS before auto-hangup in notify mode */
175
+ notifyHangupDelaySec: z.number().int().nonnegative().default(3)
176
+ }).strict().default({
177
+ defaultMode: "notify",
178
+ notifyHangupDelaySec: 3
179
+ });
180
+ const RealtimeToolSchema = z.object({
181
+ type: z.literal("function"),
182
+ name: z.string().min(1),
183
+ description: z.string(),
184
+ parameters: z.object({
185
+ type: z.literal("object"),
186
+ properties: z.record(z.string(), z.unknown()),
187
+ required: z.array(z.string()).optional()
188
+ })
189
+ }).strict();
190
+ const VoiceCallRealtimeProvidersConfigSchema = z.record(z.string(), z.record(z.string(), z.unknown())).default({});
191
+ const VoiceCallRealtimeToolPolicySchema = z.enum(REALTIME_VOICE_AGENT_CONSULT_TOOL_POLICIES);
192
+ const VoiceCallRealtimeFastContextSourceSchema = z.enum(["memory", "sessions"]);
193
+ const VoiceCallRealtimeFastContextConfigSchema = z.object({
194
+ /** Enable bounded memory/session lookup before the full consult agent. */
195
+ enabled: z.boolean().default(false),
196
+ /** Hard deadline for the fast context lookup. */
197
+ timeoutMs: z.number().int().positive().default(800),
198
+ /** Maximum memory/session hits to inject into the realtime tool result. */
199
+ maxResults: z.number().int().positive().default(3),
200
+ /** Indexed sources used by the fast context lookup. */
201
+ sources: z.array(VoiceCallRealtimeFastContextSourceSchema).min(1).default(["memory", "sessions"]),
202
+ /** Fall back to the full agent consult when fast context has no answer. */
203
+ fallbackToConsult: z.boolean().default(false)
204
+ }).strict().default({
205
+ enabled: false,
206
+ timeoutMs: 800,
207
+ maxResults: 3,
208
+ sources: ["memory", "sessions"],
209
+ fallbackToConsult: false
210
+ });
211
+ const VoiceCallStreamingProvidersConfigSchema = z.record(z.string(), z.record(z.string(), z.unknown())).default({});
212
+ const VoiceCallRealtimeConfigSchema = z.object({
213
+ /** Enable realtime voice-to-voice mode. */
214
+ enabled: z.boolean().default(false),
215
+ /** Provider id from registered realtime voice providers. */
216
+ provider: z.string().min(1).optional(),
217
+ /** Optional override for the local WebSocket route path. */
218
+ streamPath: z.string().min(1).optional(),
219
+ /** System instructions passed to the realtime provider. */
220
+ instructions: z.string().default(DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS),
221
+ /** Tool policy for the shared OpenClaw agent consult tool. */
222
+ toolPolicy: VoiceCallRealtimeToolPolicySchema.default("safe-read-only"),
223
+ /** Tool definitions exposed to the realtime provider. */
224
+ tools: z.array(RealtimeToolSchema).default([]),
225
+ /** Low-latency memory/session context for the consult tool. */
226
+ fastContext: VoiceCallRealtimeFastContextConfigSchema,
227
+ /** Provider-owned raw config blobs keyed by provider id. */
228
+ providers: VoiceCallRealtimeProvidersConfigSchema
229
+ }).strict().default({
230
+ enabled: false,
231
+ instructions: DEFAULT_VOICE_CALL_REALTIME_INSTRUCTIONS,
232
+ toolPolicy: "safe-read-only",
233
+ tools: [],
234
+ fastContext: {
235
+ enabled: false,
236
+ timeoutMs: 800,
237
+ maxResults: 3,
238
+ sources: ["memory", "sessions"],
239
+ fallbackToConsult: false
240
+ },
241
+ providers: {}
242
+ });
243
+ const VoiceCallStreamingConfigSchema = z.object({
244
+ /** Enable real-time audio streaming (requires WebSocket support) */
245
+ enabled: z.boolean().default(false),
246
+ /** Provider id from registered realtime transcription providers. */
247
+ provider: z.string().min(1).optional(),
248
+ /** WebSocket path for media stream connections */
249
+ streamPath: z.string().min(1).default("/voice/stream"),
250
+ /** Provider-owned raw config blobs keyed by provider id. */
251
+ providers: VoiceCallStreamingProvidersConfigSchema,
252
+ /**
253
+ * Close unauthenticated media stream sockets if no valid `start` frame arrives in time.
254
+ * Protects against pre-auth idle connection hold attacks.
255
+ */
256
+ preStartTimeoutMs: z.number().int().positive().default(5e3),
257
+ /** Maximum number of concurrently pending (pre-start) media stream sockets. */
258
+ maxPendingConnections: z.number().int().positive().default(32),
259
+ /** Maximum pending media stream sockets per source IP. */
260
+ maxPendingConnectionsPerIp: z.number().int().positive().default(4),
261
+ /** Hard cap for all open media stream sockets (pending + active). */
262
+ maxConnections: z.number().int().positive().default(128)
263
+ }).strict().default({
264
+ enabled: false,
265
+ streamPath: "/voice/stream",
266
+ providers: {},
267
+ preStartTimeoutMs: 5e3,
268
+ maxPendingConnections: 32,
269
+ maxPendingConnectionsPerIp: 4,
270
+ maxConnections: 128
271
+ });
272
+ const VoiceCallConfigSchema = z.object({
273
+ /** Enable voice call functionality */
274
+ enabled: z.boolean().default(false),
275
+ /** Active provider (telnyx, twilio, plivo, or mock) */
276
+ provider: z.enum([
277
+ "telnyx",
278
+ "twilio",
279
+ "plivo",
280
+ "mock"
281
+ ]).optional(),
282
+ /** Telnyx-specific configuration */
283
+ telnyx: TelnyxConfigSchema.optional(),
284
+ /** Twilio-specific configuration */
285
+ twilio: TwilioConfigSchema.optional(),
286
+ /** Plivo-specific configuration */
287
+ plivo: PlivoConfigSchema.optional(),
288
+ /** Phone number to call from (E.164) */
289
+ fromNumber: E164Schema.optional(),
290
+ /** Default phone number to call (E.164) */
291
+ toNumber: E164Schema.optional(),
292
+ /** Inbound call policy */
293
+ inboundPolicy: InboundPolicySchema.default("disabled"),
294
+ /** Allowlist of phone numbers for inbound calls (E.164) */
295
+ allowFrom: z.array(E164Schema).default([]),
296
+ /** Greeting message for inbound calls */
297
+ inboundGreeting: z.string().optional(),
298
+ /** Per-dialed-number overrides for inbound calls. Keys are E.164 numbers. */
299
+ numbers: z.record(E164Schema, VoiceCallNumberRouteConfigSchema).default({}),
300
+ /** Outbound call configuration */
301
+ outbound: OutboundConfigSchema,
302
+ /** Maximum call duration in seconds */
303
+ maxDurationSeconds: z.number().int().positive().default(300),
304
+ /**
305
+ * Maximum age of a call in seconds before it is automatically reaped.
306
+ * Catches calls stuck before answer (for example, local mock calls that
307
+ * never receive provider webhooks). Set to 0 to disable.
308
+ */
309
+ staleCallReaperSeconds: z.number().int().nonnegative().default(120),
310
+ /** Silence timeout for end-of-speech detection (ms) */
311
+ silenceTimeoutMs: z.number().int().positive().default(800),
312
+ /** Timeout for user transcript (ms) */
313
+ transcriptTimeoutMs: z.number().int().positive().default(18e4),
314
+ /** Ring timeout for outbound calls (ms) */
315
+ ringTimeoutMs: z.number().int().positive().default(3e4),
316
+ /** Maximum concurrent calls */
317
+ maxConcurrentCalls: z.number().int().positive().default(1),
318
+ /** Webhook server configuration */
319
+ serve: VoiceCallServeConfigSchema,
320
+ /** @deprecated Prefer tunnel config. */
321
+ tailscale: VoiceCallTailscaleConfigSchema,
322
+ /** Tunnel configuration (unified ngrok/tailscale) */
323
+ tunnel: VoiceCallTunnelConfigSchema,
324
+ /** Webhook signature reconstruction and proxy trust configuration */
325
+ webhookSecurity: VoiceCallWebhookSecurityConfigSchema,
326
+ /** Real-time audio streaming configuration */
327
+ streaming: VoiceCallStreamingConfigSchema,
328
+ /** Realtime voice-to-voice configuration */
329
+ realtime: VoiceCallRealtimeConfigSchema,
330
+ /** Session memory scope for voice conversations. */
331
+ sessionScope: VoiceCallSessionScopeSchema.default("per-phone"),
332
+ /** Public webhook URL override (if set, bypasses tunnel auto-detection) */
333
+ publicUrl: z.string().url().optional(),
334
+ /** Skip webhook signature verification (development only, NOT for production) */
335
+ skipSignatureVerification: z.boolean().default(false),
336
+ /** TTS override (deep-merges with core messages.tts) */
337
+ tts: TtsConfigSchema,
338
+ /** Store path for call logs */
339
+ store: z.string().optional(),
340
+ /** Agent ID to use for voice response generation. Defaults to "main". */
341
+ agentId: z.string().min(1).optional(),
342
+ /** Optional model override for generating voice responses. */
343
+ responseModel: z.string().optional(),
344
+ /** System prompt for voice responses */
345
+ responseSystemPrompt: z.string().optional(),
346
+ /** Timeout for response generation in ms (default 30s) */
347
+ responseTimeoutMs: z.number().int().positive().default(3e4)
348
+ }).strict();
349
+ const TWILIO_AUTH_TOKEN_PATH = "plugins.entries.voice-call.config.twilio.authToken";
350
+ const DEFAULT_VOICE_CALL_CONFIG = VoiceCallConfigSchema.parse({});
351
+ function cloneDefaultVoiceCallConfig() {
352
+ return structuredClone(DEFAULT_VOICE_CALL_CONFIG);
353
+ }
354
+ function normalizeWebhookLikePath(pathname) {
355
+ const trimmed = pathname.trim();
356
+ if (!trimmed) return "/";
357
+ const prefixed = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
358
+ if (prefixed === "/") return prefixed;
359
+ return prefixed.endsWith("/") ? prefixed.slice(0, -1) : prefixed;
360
+ }
361
+ function defaultRealtimeStreamPathForServePath(servePath) {
362
+ const normalized = normalizeWebhookLikePath(servePath);
363
+ if (normalized.endsWith("/webhook")) return `${normalized.slice(0, -8)}/stream/realtime`;
364
+ if (normalized === "/") return "/voice/stream/realtime";
365
+ return `${normalized}/stream/realtime`;
366
+ }
367
+ function normalizeVoiceCallTtsConfig(defaults, overrides) {
368
+ if (!defaults && !overrides) return;
369
+ return TtsConfigSchema.parse(deepMergeDefined(defaults ?? {}, overrides ?? {}));
370
+ }
371
+ function normalizePhoneRouteKey(phone) {
372
+ return phone?.replace(/\D/g, "") ?? "";
373
+ }
374
+ function resolveVoiceCallNumberRouteKey(config, phone) {
375
+ const routes = config.numbers;
376
+ if (!routes) return;
377
+ if (phone && Object.prototype.hasOwnProperty.call(routes, phone)) return phone;
378
+ const normalizedPhone = normalizePhoneRouteKey(phone);
379
+ if (!normalizedPhone) return;
380
+ return Object.keys(routes).find((routeKey) => normalizePhoneRouteKey(routeKey) === normalizedPhone);
381
+ }
382
+ function resolveVoiceCallEffectiveConfig(config, phoneOrRouteKey) {
383
+ const numberRouteKey = resolveVoiceCallNumberRouteKey(config, phoneOrRouteKey);
384
+ if (!numberRouteKey) return { config };
385
+ const route = config.numbers[numberRouteKey];
386
+ if (!route) return { config };
387
+ return {
388
+ numberRouteKey,
389
+ config: {
390
+ ...config,
391
+ ...route,
392
+ tts: normalizeVoiceCallTtsConfig(config.tts, route.tts),
393
+ numbers: config.numbers
394
+ }
395
+ };
396
+ }
397
+ function sanitizeVoiceCallProviderConfigs(value) {
398
+ if (!value) return {};
399
+ return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== void 0));
400
+ }
401
+ function sanitizeVoiceCallNumberRoutes(value) {
402
+ if (!value) return {};
403
+ return Object.fromEntries(Object.entries(value).filter((entry) => entry[1] !== void 0).map(([key, route]) => [key, VoiceCallNumberRouteConfigSchema.parse(route)]));
404
+ }
405
+ function resolveTwilioAuthToken(config) {
406
+ return normalizeResolvedSecretInputString({
407
+ value: config.twilio?.authToken,
408
+ path: TWILIO_AUTH_TOKEN_PATH
409
+ });
410
+ }
411
+ function normalizeVoiceCallConfig(config) {
412
+ const defaults = cloneDefaultVoiceCallConfig();
413
+ const serve = {
414
+ ...defaults.serve,
415
+ ...config.serve
416
+ };
417
+ const streamingProvider = config.streaming?.provider;
418
+ const streamingProviders = sanitizeVoiceCallProviderConfigs(config.streaming?.providers ?? defaults.streaming.providers);
419
+ const realtimeProvider = config.realtime?.provider ?? defaults.realtime.provider;
420
+ const realtimeProviders = sanitizeVoiceCallProviderConfigs(config.realtime?.providers ?? defaults.realtime.providers);
421
+ const realtimeFastContext = {
422
+ ...defaults.realtime.fastContext,
423
+ ...config.realtime?.fastContext,
424
+ sources: config.realtime?.fastContext?.sources ?? defaults.realtime.fastContext.sources
425
+ };
426
+ return {
427
+ ...defaults,
428
+ ...config,
429
+ allowFrom: config.allowFrom ?? defaults.allowFrom,
430
+ numbers: sanitizeVoiceCallNumberRoutes(config.numbers ?? defaults.numbers),
431
+ outbound: {
432
+ ...defaults.outbound,
433
+ ...config.outbound
434
+ },
435
+ serve,
436
+ tailscale: {
437
+ ...defaults.tailscale,
438
+ ...config.tailscale
439
+ },
440
+ tunnel: {
441
+ ...defaults.tunnel,
442
+ ...config.tunnel
443
+ },
444
+ webhookSecurity: {
445
+ ...defaults.webhookSecurity,
446
+ ...config.webhookSecurity,
447
+ allowedHosts: config.webhookSecurity?.allowedHosts ?? defaults.webhookSecurity.allowedHosts,
448
+ trustedProxyIPs: config.webhookSecurity?.trustedProxyIPs ?? defaults.webhookSecurity.trustedProxyIPs
449
+ },
450
+ streaming: {
451
+ ...defaults.streaming,
452
+ ...config.streaming,
453
+ provider: streamingProvider,
454
+ providers: streamingProviders
455
+ },
456
+ realtime: {
457
+ ...defaults.realtime,
458
+ ...config.realtime,
459
+ provider: realtimeProvider,
460
+ streamPath: config.realtime?.streamPath ?? defaultRealtimeStreamPathForServePath(serve.path ?? defaults.serve.path),
461
+ tools: config.realtime?.tools ?? defaults.realtime.tools,
462
+ fastContext: realtimeFastContext,
463
+ providers: realtimeProviders
464
+ },
465
+ tts: normalizeVoiceCallTtsConfig(defaults.tts, config.tts)
466
+ };
467
+ }
468
+ function resolveVoiceCallSessionKey(params) {
469
+ const explicit = params.explicitSessionKey?.trim();
470
+ if (explicit) return explicit;
471
+ if (params.config.sessionScope === "per-call") return `voice:call:${params.callId}`;
472
+ const normalizedPhone = params.phone?.replace(/\D/g, "");
473
+ return normalizedPhone ? `voice:${normalizedPhone}` : `voice:${params.callId}`;
474
+ }
475
+ /**
476
+ * Resolves the configuration by merging environment variables into missing fields.
477
+ * Returns a new configuration object with environment variables applied.
478
+ */
479
+ function resolveVoiceCallConfig(config) {
480
+ const resolved = normalizeVoiceCallConfig(config);
481
+ if (resolved.provider === "telnyx") {
482
+ resolved.telnyx = resolved.telnyx ?? {};
483
+ resolved.telnyx.apiKey = resolved.telnyx.apiKey ?? process.env.TELNYX_API_KEY;
484
+ resolved.telnyx.connectionId = resolved.telnyx.connectionId ?? process.env.TELNYX_CONNECTION_ID;
485
+ resolved.telnyx.publicKey = resolved.telnyx.publicKey ?? process.env.TELNYX_PUBLIC_KEY;
486
+ }
487
+ if (resolved.provider === "twilio") {
488
+ resolved.fromNumber = resolved.fromNumber ?? process.env.TWILIO_FROM_NUMBER;
489
+ resolved.twilio = resolved.twilio ?? {};
490
+ resolved.twilio.accountSid = resolved.twilio.accountSid ?? process.env.TWILIO_ACCOUNT_SID;
491
+ resolved.twilio.authToken = resolved.twilio.authToken ?? process.env.TWILIO_AUTH_TOKEN;
492
+ }
493
+ if (resolved.provider === "plivo") {
494
+ resolved.plivo = resolved.plivo ?? {};
495
+ resolved.plivo.authId = resolved.plivo.authId ?? process.env.PLIVO_AUTH_ID;
496
+ resolved.plivo.authToken = resolved.plivo.authToken ?? process.env.PLIVO_AUTH_TOKEN;
497
+ }
498
+ resolved.tunnel = resolved.tunnel ?? {
499
+ provider: "none",
500
+ allowNgrokFreeTierLoopbackBypass: false
501
+ };
502
+ resolved.tunnel.allowNgrokFreeTierLoopbackBypass = resolved.tunnel.allowNgrokFreeTierLoopbackBypass ?? false;
503
+ resolved.tunnel.ngrokAuthToken = resolved.tunnel.ngrokAuthToken ?? process.env.NGROK_AUTHTOKEN;
504
+ resolved.tunnel.ngrokDomain = resolved.tunnel.ngrokDomain ?? process.env.NGROK_DOMAIN;
505
+ resolved.webhookSecurity = resolved.webhookSecurity ?? {
506
+ allowedHosts: [],
507
+ trustForwardingHeaders: false,
508
+ trustedProxyIPs: []
509
+ };
510
+ resolved.webhookSecurity.allowedHosts = resolved.webhookSecurity.allowedHosts ?? [];
511
+ resolved.webhookSecurity.trustForwardingHeaders = resolved.webhookSecurity.trustForwardingHeaders ?? false;
512
+ resolved.webhookSecurity.trustedProxyIPs = resolved.webhookSecurity.trustedProxyIPs ?? [];
513
+ return normalizeVoiceCallConfig(resolved);
514
+ }
515
+ /**
516
+ * Validate that the configuration has all required fields for the selected provider.
517
+ */
518
+ function validateProviderConfig(config) {
519
+ const errors = [];
520
+ if (!config.enabled) return {
521
+ valid: true,
522
+ errors: []
523
+ };
524
+ if (!config.provider) errors.push("plugins.entries.voice-call.config.provider is required");
525
+ if (!config.fromNumber && config.provider !== "mock") errors.push(config.provider === "twilio" ? "plugins.entries.voice-call.config.fromNumber is required (or set TWILIO_FROM_NUMBER env)" : "plugins.entries.voice-call.config.fromNumber is required");
526
+ if (config.provider === "telnyx") {
527
+ if (!config.telnyx?.apiKey) errors.push("plugins.entries.voice-call.config.telnyx.apiKey is required (or set TELNYX_API_KEY env)");
528
+ if (!config.telnyx?.connectionId) errors.push("plugins.entries.voice-call.config.telnyx.connectionId is required (or set TELNYX_CONNECTION_ID env)");
529
+ if (!config.skipSignatureVerification && !config.telnyx?.publicKey) errors.push("plugins.entries.voice-call.config.telnyx.publicKey is required (or set TELNYX_PUBLIC_KEY env)");
530
+ }
531
+ if (config.provider === "twilio") {
532
+ if (!config.twilio?.accountSid) errors.push("plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)");
533
+ if (!hasConfiguredSecretInput(config.twilio?.authToken)) errors.push("plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)");
534
+ }
535
+ if (config.provider === "plivo") {
536
+ if (!config.plivo?.authId) errors.push("plugins.entries.voice-call.config.plivo.authId is required (or set PLIVO_AUTH_ID env)");
537
+ if (!config.plivo?.authToken) errors.push("plugins.entries.voice-call.config.plivo.authToken is required (or set PLIVO_AUTH_TOKEN env)");
538
+ }
539
+ if (config.realtime.enabled && config.inboundPolicy === "disabled") errors.push("plugins.entries.voice-call.config.inboundPolicy must not be \"disabled\" when realtime.enabled is true");
540
+ if (config.realtime.enabled && config.streaming.enabled) errors.push("plugins.entries.voice-call.config.realtime.enabled and plugins.entries.voice-call.config.streaming.enabled cannot both be true");
541
+ if (config.realtime.enabled && config.provider && config.provider !== "twilio") errors.push("plugins.entries.voice-call.config.provider must be \"twilio\" when realtime.enabled is true");
542
+ return {
543
+ valid: errors.length === 0,
544
+ errors
545
+ };
546
+ }
547
+ //#endregion
548
+ export { resolveVoiceCallEffectiveConfig as a, deepMergeDefined as c, resolveVoiceCallConfig as i, normalizeVoiceCallConfig as n, resolveVoiceCallSessionKey as o, resolveTwilioAuthToken as r, validateProviderConfig as s, VoiceCallConfigSchema as t };