fix(voice-call): ignore blank provider environment values (#108816)

* fix(voice-call): ignore blank provider environment values

* refactor(voice-call): reuse shared credential resolver

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
sunlit-deng
2026-07-16 08:39:39 -07:00
committed by GitHub
co-authored by Peter Steinberger
parent d1e2fbd682
commit 9c08675852
2 changed files with 83 additions and 10 deletions
+62
View File
@@ -54,6 +54,8 @@ describe("validateProviderConfig", () => {
delete process.env.TELNYX_PUBLIC_KEY;
delete process.env.PLIVO_AUTH_ID;
delete process.env.PLIVO_AUTH_TOKEN;
delete process.env.NGROK_AUTHTOKEN;
delete process.env.NGROK_DOMAIN;
};
beforeEach(() => {
@@ -100,6 +102,66 @@ describe("validateProviderConfig", () => {
expect(validateProviderConfig(fromEnv)).toEqual({ valid: true, errors: [] });
}
});
it("ignores blank provider and tunnel environment values", () => {
for (const provider of ["twilio", "telnyx", "plivo"] as const) {
clearProviderEnv();
process.env.TWILIO_ACCOUNT_SID = " ";
process.env.TWILIO_AUTH_TOKEN = " ";
process.env.TWILIO_FROM_NUMBER = " ";
process.env.TELNYX_API_KEY = " ";
process.env.TELNYX_CONNECTION_ID = " ";
process.env.TELNYX_PUBLIC_KEY = " ";
process.env.PLIVO_AUTH_ID = " ";
process.env.PLIVO_AUTH_TOKEN = " ";
process.env.NGROK_AUTHTOKEN = " ";
process.env.NGROK_DOMAIN = " ";
const config = resolveVoiceCallConfig({
...createBaseConfig(provider),
fromNumber: undefined,
tunnel: { provider: "ngrok" },
});
const result = validateProviderConfig(config);
expect(result.valid).toBe(false);
expect(config.tunnel.ngrokAuthToken).toBeUndefined();
expect(config.tunnel.ngrokDomain).toBeUndefined();
if (provider === "twilio") {
expect(config.fromNumber).toBeUndefined();
expect(config.twilio?.accountSid).toBeUndefined();
expect(config.twilio?.authToken).toBeUndefined();
expect(result.errors).toContain(
"plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)",
);
expect(result.errors).toContain(
"plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)",
);
expect(result.errors).toContain(
"plugins.entries.voice-call.config.fromNumber is required (or set TWILIO_FROM_NUMBER env)",
);
} else if (provider === "telnyx") {
expect(config.telnyx?.apiKey).toBeUndefined();
expect(config.telnyx?.connectionId).toBeUndefined();
expect(config.telnyx?.publicKey).toBeUndefined();
expect(result.errors).toContain(
"plugins.entries.voice-call.config.telnyx.apiKey is required (or set TELNYX_API_KEY env)",
);
expect(result.errors).toContain(
"plugins.entries.voice-call.config.telnyx.connectionId is required (or set TELNYX_CONNECTION_ID env)",
);
} else {
expect(config.plivo?.authId).toBeUndefined();
expect(config.plivo?.authToken).toBeUndefined();
expect(result.errors).toContain(
"plugins.entries.voice-call.config.plivo.authId is required (or set PLIVO_AUTH_ID env)",
);
expect(result.errors).toContain(
"plugins.entries.voice-call.config.plivo.authToken is required (or set PLIVO_AUTH_TOKEN env)",
);
}
}
});
});
describe("twilio provider", () => {
+21 -10
View File
@@ -12,6 +12,7 @@ import {
canonicalizeMainSessionAlias,
type SessionScope,
} from "openclaw/plugin-sdk/session-store-runtime";
import { resolveSpeechProviderApiKey } from "openclaw/plugin-sdk/speech-core";
import { normalizeWebhookPath } from "openclaw/plugin-sdk/webhook-ingress";
import { z } from "zod";
import { TtsConfigSchema } from "../api.js";
@@ -795,24 +796,32 @@ export function resolveVoiceCallConfig(config: VoiceCallConfigInput): VoiceCallC
// Telnyx
if (resolved.provider === "telnyx") {
resolved.telnyx = resolved.telnyx ?? {};
resolved.telnyx.apiKey = resolved.telnyx.apiKey ?? process.env.TELNYX_API_KEY;
resolved.telnyx.connectionId = resolved.telnyx.connectionId ?? process.env.TELNYX_CONNECTION_ID;
resolved.telnyx.publicKey = resolved.telnyx.publicKey ?? process.env.TELNYX_PUBLIC_KEY;
resolved.telnyx.apiKey =
resolved.telnyx.apiKey ?? resolveSpeechProviderApiKey(process.env.TELNYX_API_KEY);
resolved.telnyx.connectionId =
resolved.telnyx.connectionId ?? resolveSpeechProviderApiKey(process.env.TELNYX_CONNECTION_ID);
resolved.telnyx.publicKey =
resolved.telnyx.publicKey ?? resolveSpeechProviderApiKey(process.env.TELNYX_PUBLIC_KEY);
}
// Twilio
if (resolved.provider === "twilio") {
resolved.fromNumber = resolved.fromNumber ?? process.env.TWILIO_FROM_NUMBER;
resolved.fromNumber =
resolved.fromNumber ?? resolveSpeechProviderApiKey(process.env.TWILIO_FROM_NUMBER);
resolved.twilio = resolved.twilio ?? {};
resolved.twilio.accountSid = resolved.twilio.accountSid ?? process.env.TWILIO_ACCOUNT_SID;
resolved.twilio.authToken = resolved.twilio.authToken ?? process.env.TWILIO_AUTH_TOKEN;
resolved.twilio.accountSid =
resolved.twilio.accountSid ?? resolveSpeechProviderApiKey(process.env.TWILIO_ACCOUNT_SID);
resolved.twilio.authToken =
resolved.twilio.authToken ?? resolveSpeechProviderApiKey(process.env.TWILIO_AUTH_TOKEN);
}
// Plivo
if (resolved.provider === "plivo") {
resolved.plivo = resolved.plivo ?? {};
resolved.plivo.authId = resolved.plivo.authId ?? process.env.PLIVO_AUTH_ID;
resolved.plivo.authToken = resolved.plivo.authToken ?? process.env.PLIVO_AUTH_TOKEN;
resolved.plivo.authId =
resolved.plivo.authId ?? resolveSpeechProviderApiKey(process.env.PLIVO_AUTH_ID);
resolved.plivo.authToken =
resolved.plivo.authToken ?? resolveSpeechProviderApiKey(process.env.PLIVO_AUTH_TOKEN);
}
// Tunnel Config
@@ -822,8 +831,10 @@ export function resolveVoiceCallConfig(config: VoiceCallConfigInput): VoiceCallC
};
resolved.tunnel.allowNgrokFreeTierLoopbackBypass =
resolved.tunnel.allowNgrokFreeTierLoopbackBypass ?? false;
resolved.tunnel.ngrokAuthToken = resolved.tunnel.ngrokAuthToken ?? process.env.NGROK_AUTHTOKEN;
resolved.tunnel.ngrokDomain = resolved.tunnel.ngrokDomain ?? process.env.NGROK_DOMAIN;
resolved.tunnel.ngrokAuthToken =
resolved.tunnel.ngrokAuthToken ?? resolveSpeechProviderApiKey(process.env.NGROK_AUTHTOKEN);
resolved.tunnel.ngrokDomain =
resolved.tunnel.ngrokDomain ?? resolveSpeechProviderApiKey(process.env.NGROK_DOMAIN);
// Webhook Security Config
resolved.webhookSecurity = resolved.webhookSecurity ?? {