fix(voice-call): validate provider credentials from env vars
The `validateProviderConfig()` function now checks both config values AND environment variables when validating provider credentials. This aligns the validation behavior with `resolveProvider()` which already falls back to env vars. Previously, users who set credentials via environment variables would get validation errors even though the credentials would be found at runtime. The error messages correctly suggested env vars as an alternative, but the validation didn't actually check them. Affects all three supported providers: Twilio, Telnyx, and Plivo. Fixes #1709 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
196
extensions/voice-call/src/config.test.ts
Normal file
196
extensions/voice-call/src/config.test.ts
Normal file
@@ -0,0 +1,196 @@
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
import { validateProviderConfig, type VoiceCallConfig } from "./config.js";
|
||||
|
||||
function createBaseConfig(
|
||||
provider: "telnyx" | "twilio" | "plivo" | "mock",
|
||||
): VoiceCallConfig {
|
||||
return {
|
||||
enabled: true,
|
||||
provider,
|
||||
fromNumber: "+15550001234",
|
||||
inboundPolicy: "disabled",
|
||||
allowFrom: [],
|
||||
outbound: { defaultMode: "notify", notifyHangupDelaySec: 3 },
|
||||
maxDurationSeconds: 300,
|
||||
silenceTimeoutMs: 800,
|
||||
transcriptTimeoutMs: 180000,
|
||||
ringTimeoutMs: 30000,
|
||||
maxConcurrentCalls: 1,
|
||||
serve: { port: 3334, bind: "127.0.0.1", path: "/voice/webhook" },
|
||||
tailscale: { mode: "off", path: "/voice/webhook" },
|
||||
tunnel: { provider: "none", allowNgrokFreeTier: true },
|
||||
streaming: {
|
||||
enabled: false,
|
||||
sttProvider: "openai-realtime",
|
||||
sttModel: "gpt-4o-transcribe",
|
||||
silenceDurationMs: 800,
|
||||
vadThreshold: 0.5,
|
||||
streamPath: "/voice/stream",
|
||||
},
|
||||
skipSignatureVerification: false,
|
||||
stt: { provider: "openai", model: "whisper-1" },
|
||||
tts: { provider: "openai", model: "gpt-4o-mini-tts", voice: "coral" },
|
||||
responseModel: "openai/gpt-4o-mini",
|
||||
responseTimeoutMs: 30000,
|
||||
};
|
||||
}
|
||||
|
||||
describe("validateProviderConfig", () => {
|
||||
const originalEnv = { ...process.env };
|
||||
|
||||
beforeEach(() => {
|
||||
// Clear all relevant env vars before each test
|
||||
delete process.env.TWILIO_ACCOUNT_SID;
|
||||
delete process.env.TWILIO_AUTH_TOKEN;
|
||||
delete process.env.TELNYX_API_KEY;
|
||||
delete process.env.TELNYX_CONNECTION_ID;
|
||||
delete process.env.PLIVO_AUTH_ID;
|
||||
delete process.env.PLIVO_AUTH_TOKEN;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original env
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
describe("twilio provider", () => {
|
||||
it("passes validation when credentials are in config", () => {
|
||||
const config = createBaseConfig("twilio");
|
||||
config.twilio = { accountSid: "AC123", authToken: "secret" };
|
||||
|
||||
const result = validateProviderConfig(config);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("passes validation when credentials are in environment variables", () => {
|
||||
process.env.TWILIO_ACCOUNT_SID = "AC123";
|
||||
process.env.TWILIO_AUTH_TOKEN = "secret";
|
||||
const config = createBaseConfig("twilio");
|
||||
|
||||
const result = validateProviderConfig(config);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("passes validation with mixed config and env vars", () => {
|
||||
process.env.TWILIO_AUTH_TOKEN = "secret";
|
||||
const config = createBaseConfig("twilio");
|
||||
config.twilio = { accountSid: "AC123" };
|
||||
|
||||
const result = validateProviderConfig(config);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("fails validation when accountSid is missing everywhere", () => {
|
||||
process.env.TWILIO_AUTH_TOKEN = "secret";
|
||||
const config = createBaseConfig("twilio");
|
||||
|
||||
const result = validateProviderConfig(config);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
"plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)",
|
||||
);
|
||||
});
|
||||
|
||||
it("fails validation when authToken is missing everywhere", () => {
|
||||
process.env.TWILIO_ACCOUNT_SID = "AC123";
|
||||
const config = createBaseConfig("twilio");
|
||||
|
||||
const result = validateProviderConfig(config);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
"plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("telnyx provider", () => {
|
||||
it("passes validation when credentials are in config", () => {
|
||||
const config = createBaseConfig("telnyx");
|
||||
config.telnyx = { apiKey: "KEY123", connectionId: "CONN456" };
|
||||
|
||||
const result = validateProviderConfig(config);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("passes validation when credentials are in environment variables", () => {
|
||||
process.env.TELNYX_API_KEY = "KEY123";
|
||||
process.env.TELNYX_CONNECTION_ID = "CONN456";
|
||||
const config = createBaseConfig("telnyx");
|
||||
|
||||
const result = validateProviderConfig(config);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("fails validation when apiKey is missing everywhere", () => {
|
||||
process.env.TELNYX_CONNECTION_ID = "CONN456";
|
||||
const config = createBaseConfig("telnyx");
|
||||
|
||||
const result = validateProviderConfig(config);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
"plugins.entries.voice-call.config.telnyx.apiKey is required (or set TELNYX_API_KEY env)",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("plivo provider", () => {
|
||||
it("passes validation when credentials are in config", () => {
|
||||
const config = createBaseConfig("plivo");
|
||||
config.plivo = { authId: "MA123", authToken: "secret" };
|
||||
|
||||
const result = validateProviderConfig(config);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("passes validation when credentials are in environment variables", () => {
|
||||
process.env.PLIVO_AUTH_ID = "MA123";
|
||||
process.env.PLIVO_AUTH_TOKEN = "secret";
|
||||
const config = createBaseConfig("plivo");
|
||||
|
||||
const result = validateProviderConfig(config);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
|
||||
it("fails validation when authId is missing everywhere", () => {
|
||||
process.env.PLIVO_AUTH_TOKEN = "secret";
|
||||
const config = createBaseConfig("plivo");
|
||||
|
||||
const result = validateProviderConfig(config);
|
||||
|
||||
expect(result.valid).toBe(false);
|
||||
expect(result.errors).toContain(
|
||||
"plugins.entries.voice-call.config.plivo.authId is required (or set PLIVO_AUTH_ID env)",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("disabled config", () => {
|
||||
it("skips validation when enabled is false", () => {
|
||||
const config = createBaseConfig("twilio");
|
||||
config.enabled = false;
|
||||
|
||||
const result = validateProviderConfig(config);
|
||||
|
||||
expect(result.valid).toBe(true);
|
||||
expect(result.errors).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -352,12 +352,12 @@ export function validateProviderConfig(config: VoiceCallConfig): {
|
||||
}
|
||||
|
||||
if (config.provider === "telnyx") {
|
||||
if (!config.telnyx?.apiKey) {
|
||||
if (!config.telnyx?.apiKey && !process.env.TELNYX_API_KEY) {
|
||||
errors.push(
|
||||
"plugins.entries.voice-call.config.telnyx.apiKey is required (or set TELNYX_API_KEY env)",
|
||||
);
|
||||
}
|
||||
if (!config.telnyx?.connectionId) {
|
||||
if (!config.telnyx?.connectionId && !process.env.TELNYX_CONNECTION_ID) {
|
||||
errors.push(
|
||||
"plugins.entries.voice-call.config.telnyx.connectionId is required (or set TELNYX_CONNECTION_ID env)",
|
||||
);
|
||||
@@ -365,12 +365,12 @@ export function validateProviderConfig(config: VoiceCallConfig): {
|
||||
}
|
||||
|
||||
if (config.provider === "twilio") {
|
||||
if (!config.twilio?.accountSid) {
|
||||
if (!config.twilio?.accountSid && !process.env.TWILIO_ACCOUNT_SID) {
|
||||
errors.push(
|
||||
"plugins.entries.voice-call.config.twilio.accountSid is required (or set TWILIO_ACCOUNT_SID env)",
|
||||
);
|
||||
}
|
||||
if (!config.twilio?.authToken) {
|
||||
if (!config.twilio?.authToken && !process.env.TWILIO_AUTH_TOKEN) {
|
||||
errors.push(
|
||||
"plugins.entries.voice-call.config.twilio.authToken is required (or set TWILIO_AUTH_TOKEN env)",
|
||||
);
|
||||
@@ -378,12 +378,12 @@ export function validateProviderConfig(config: VoiceCallConfig): {
|
||||
}
|
||||
|
||||
if (config.provider === "plivo") {
|
||||
if (!config.plivo?.authId) {
|
||||
if (!config.plivo?.authId && !process.env.PLIVO_AUTH_ID) {
|
||||
errors.push(
|
||||
"plugins.entries.voice-call.config.plivo.authId is required (or set PLIVO_AUTH_ID env)",
|
||||
);
|
||||
}
|
||||
if (!config.plivo?.authToken) {
|
||||
if (!config.plivo?.authToken && !process.env.PLIVO_AUTH_TOKEN) {
|
||||
errors.push(
|
||||
"plugins.entries.voice-call.config.plivo.authToken is required (or set PLIVO_AUTH_TOKEN env)",
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user