@kodelyth/voice-call 2026.5.42 → 2026.6.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 (111) hide show
  1. package/package.json +16 -4
  2. package/api.ts +0 -16
  3. package/cli-metadata.ts +0 -10
  4. package/config-api.ts +0 -12
  5. package/index.test.ts +0 -1075
  6. package/index.ts +0 -863
  7. package/runtime-api.ts +0 -20
  8. package/runtime-entry.ts +0 -1
  9. package/setup-api.ts +0 -47
  10. package/src/allowlist.test.ts +0 -18
  11. package/src/allowlist.ts +0 -19
  12. package/src/cli.test.ts +0 -12
  13. package/src/cli.ts +0 -866
  14. package/src/config-compat.test.ts +0 -130
  15. package/src/config-compat.ts +0 -227
  16. package/src/config.test.ts +0 -542
  17. package/src/config.ts +0 -883
  18. package/src/core-bridge.ts +0 -14
  19. package/src/deep-merge.test.ts +0 -40
  20. package/src/deep-merge.ts +0 -23
  21. package/src/gateway-continue-operation.ts +0 -200
  22. package/src/http-headers.test.ts +0 -16
  23. package/src/http-headers.ts +0 -15
  24. package/src/manager/context.ts +0 -50
  25. package/src/manager/events.test.ts +0 -578
  26. package/src/manager/events.ts +0 -332
  27. package/src/manager/lifecycle.ts +0 -53
  28. package/src/manager/lookup.test.ts +0 -52
  29. package/src/manager/lookup.ts +0 -35
  30. package/src/manager/outbound.test.ts +0 -629
  31. package/src/manager/outbound.ts +0 -508
  32. package/src/manager/state.ts +0 -48
  33. package/src/manager/store.ts +0 -107
  34. package/src/manager/timers.test.ts +0 -127
  35. package/src/manager/timers.ts +0 -113
  36. package/src/manager/twiml.test.ts +0 -13
  37. package/src/manager/twiml.ts +0 -17
  38. package/src/manager.closed-loop.test.ts +0 -259
  39. package/src/manager.inbound-allowlist.test.ts +0 -183
  40. package/src/manager.notify.test.ts +0 -390
  41. package/src/manager.restore.test.ts +0 -310
  42. package/src/manager.test-harness.ts +0 -127
  43. package/src/manager.ts +0 -441
  44. package/src/media-stream.test.ts +0 -953
  45. package/src/media-stream.ts +0 -876
  46. package/src/providers/base.ts +0 -99
  47. package/src/providers/mock.test.ts +0 -86
  48. package/src/providers/mock.ts +0 -185
  49. package/src/providers/plivo.test.ts +0 -93
  50. package/src/providers/plivo.ts +0 -601
  51. package/src/providers/shared/call-status.test.ts +0 -24
  52. package/src/providers/shared/call-status.ts +0 -24
  53. package/src/providers/shared/guarded-json-api.test.ts +0 -127
  54. package/src/providers/shared/guarded-json-api.ts +0 -49
  55. package/src/providers/telnyx.test.ts +0 -489
  56. package/src/providers/telnyx.ts +0 -419
  57. package/src/providers/twilio/api.test.ts +0 -184
  58. package/src/providers/twilio/api.ts +0 -100
  59. package/src/providers/twilio/twiml-policy.test.ts +0 -84
  60. package/src/providers/twilio/twiml-policy.ts +0 -87
  61. package/src/providers/twilio/webhook.ts +0 -34
  62. package/src/providers/twilio.test.ts +0 -607
  63. package/src/providers/twilio.ts +0 -861
  64. package/src/providers/twilio.types.ts +0 -17
  65. package/src/realtime-agent-context.test.ts +0 -101
  66. package/src/realtime-agent-context.ts +0 -149
  67. package/src/realtime-defaults.ts +0 -3
  68. package/src/realtime-fast-context.test.ts +0 -74
  69. package/src/realtime-fast-context.ts +0 -27
  70. package/src/realtime-transcription.runtime.ts +0 -4
  71. package/src/realtime-voice.runtime.ts +0 -5
  72. package/src/response-generator.test.ts +0 -385
  73. package/src/response-generator.ts +0 -348
  74. package/src/response-model.test.ts +0 -71
  75. package/src/response-model.ts +0 -23
  76. package/src/runtime.test.ts +0 -625
  77. package/src/runtime.ts +0 -528
  78. package/src/telephony-audio.test.ts +0 -61
  79. package/src/telephony-audio.ts +0 -12
  80. package/src/telephony-tts.test.ts +0 -196
  81. package/src/telephony-tts.ts +0 -235
  82. package/src/test-fixtures.ts +0 -82
  83. package/src/tts-provider-voice.test.ts +0 -34
  84. package/src/tts-provider-voice.ts +0 -21
  85. package/src/tunnel.test.ts +0 -173
  86. package/src/tunnel.ts +0 -314
  87. package/src/types.ts +0 -311
  88. package/src/utils.test.ts +0 -17
  89. package/src/utils.ts +0 -14
  90. package/src/voice-mapping.test.ts +0 -32
  91. package/src/voice-mapping.ts +0 -65
  92. package/src/webhook/realtime-audio-pacer.test.ts +0 -146
  93. package/src/webhook/realtime-audio-pacer.ts +0 -204
  94. package/src/webhook/realtime-handler.test.ts +0 -1450
  95. package/src/webhook/realtime-handler.ts +0 -1382
  96. package/src/webhook/stale-call-reaper.test.ts +0 -89
  97. package/src/webhook/stale-call-reaper.ts +0 -38
  98. package/src/webhook/stream-frame-adapter.test.ts +0 -187
  99. package/src/webhook/stream-frame-adapter.ts +0 -219
  100. package/src/webhook/tailscale.test.ts +0 -216
  101. package/src/webhook/tailscale.ts +0 -129
  102. package/src/webhook-exposure.test.ts +0 -33
  103. package/src/webhook-exposure.ts +0 -84
  104. package/src/webhook-security.test.ts +0 -813
  105. package/src/webhook-security.ts +0 -982
  106. package/src/webhook.hangup-once.lifecycle.test.ts +0 -179
  107. package/src/webhook.test.ts +0 -1615
  108. package/src/webhook.ts +0 -933
  109. package/src/webhook.types.ts +0 -5
  110. package/src/websocket-test-support.ts +0 -72
  111. package/tsconfig.json +0 -16
@@ -1,310 +0,0 @@
1
- import { afterEach, describe, expect, it, vi } from "vitest";
2
- import { VoiceCallConfigSchema } from "./config.js";
3
- import { CallManager } from "./manager.js";
4
- import {
5
- createTestStorePath,
6
- FakeProvider,
7
- makePersistedCall,
8
- writeCallsToStore,
9
- } from "./manager.test-harness.js";
10
- import { flushPendingCallRecordWritesForTest, loadActiveCallsFromStore } from "./manager/store.js";
11
-
12
- function requireSingleActiveCall(manager: CallManager) {
13
- const activeCalls = manager.getActiveCalls();
14
- expect(activeCalls).toHaveLength(1);
15
- const activeCall = activeCalls[0];
16
- if (!activeCall) {
17
- throw new Error("expected restored active call");
18
- }
19
- return activeCall;
20
- }
21
-
22
- function requireRecord(value: unknown, label: string): Record<string, unknown> {
23
- if (value === null || typeof value !== "object" || Array.isArray(value)) {
24
- throw new Error(`expected ${label} to be a record`);
25
- }
26
- return value as Record<string, unknown>;
27
- }
28
-
29
- function requireSingleHangupCall(provider: FakeProvider) {
30
- expect(provider.hangupCalls).toHaveLength(1);
31
- return requireRecord(provider.hangupCalls[0], "hangup call");
32
- }
33
-
34
- describe("CallManager verification on restore", () => {
35
- afterEach(() => {
36
- vi.useRealTimers();
37
- vi.restoreAllMocks();
38
- });
39
-
40
- async function initializeManager(params?: {
41
- callOverrides?: Parameters<typeof makePersistedCall>[0];
42
- providerResult?: FakeProvider["getCallStatusResult"];
43
- configureProvider?: (provider: FakeProvider) => void;
44
- configOverrides?: Partial<{ maxDurationSeconds: number }>;
45
- }) {
46
- const storePath = createTestStorePath();
47
- const call = makePersistedCall(params?.callOverrides);
48
- writeCallsToStore(storePath, [call]);
49
-
50
- const provider = new FakeProvider();
51
- if (params?.providerResult) {
52
- provider.getCallStatusResult = params.providerResult;
53
- }
54
- params?.configureProvider?.(provider);
55
-
56
- const config = VoiceCallConfigSchema.parse({
57
- enabled: true,
58
- provider: "plivo",
59
- fromNumber: "+15550000000",
60
- ...params?.configOverrides,
61
- });
62
- const manager = new CallManager(config, storePath);
63
- await manager.initialize(provider, "https://example.com/voice/webhook");
64
-
65
- return { call, manager, provider, storePath };
66
- }
67
-
68
- it("skips stale calls reported terminal by provider", async () => {
69
- const { manager } = await initializeManager({
70
- providerResult: { status: "completed", isTerminal: true },
71
- });
72
-
73
- expect(manager.getActiveCalls()).toHaveLength(0);
74
- });
75
-
76
- it("keeps calls reported active by provider", async () => {
77
- const { call, manager } = await initializeManager({
78
- providerResult: { status: "in-progress", isTerminal: false },
79
- });
80
-
81
- const activeCall = requireSingleActiveCall(manager);
82
- expect(activeCall.callId).toBe(call.callId);
83
- });
84
-
85
- it("keeps calls when provider returns unknown (transient error)", async () => {
86
- const { call, manager } = await initializeManager({
87
- providerResult: { status: "error", isTerminal: false, isUnknown: true },
88
- });
89
-
90
- const activeCall = requireSingleActiveCall(manager);
91
- expect(activeCall.callId).toBe(call.callId);
92
- expect(activeCall.state).toBe(call.state);
93
- });
94
-
95
- it("skips calls older than maxDurationSeconds", async () => {
96
- const { manager, provider, storePath } = await initializeManager({
97
- callOverrides: {
98
- startedAt: Date.now() - 600_000,
99
- answeredAt: Date.now() - 590_000,
100
- },
101
- configOverrides: { maxDurationSeconds: 300 },
102
- });
103
-
104
- expect(manager.getActiveCalls()).toHaveLength(0);
105
- const hangupCall = requireSingleHangupCall(provider);
106
- expect(hangupCall.reason).toBe("timeout");
107
-
108
- await flushPendingCallRecordWritesForTest();
109
- expect(loadActiveCallsFromStore(storePath).activeCalls.size).toBe(0);
110
- });
111
-
112
- it("skips calls without providerCallId", async () => {
113
- const { manager } = await initializeManager({
114
- callOverrides: { providerCallId: undefined, state: "initiated" },
115
- });
116
-
117
- expect(manager.getActiveCalls()).toHaveLength(0);
118
- });
119
-
120
- it("keeps call when getCallStatus throws (verification failure)", async () => {
121
- const { call, manager } = await initializeManager({
122
- configureProvider: (provider) => {
123
- provider.getCallStatus = async () => {
124
- throw new Error("network failure");
125
- };
126
- },
127
- });
128
-
129
- const activeCall = requireSingleActiveCall(manager);
130
- expect(activeCall.callId).toBe(call.callId);
131
- expect(activeCall.state).toBe(call.state);
132
- });
133
-
134
- it("summarizes repeated restored-call verification outcomes", async () => {
135
- const now = Date.now();
136
- const storePath = createTestStorePath();
137
- const calls = [
138
- makePersistedCall({
139
- callId: "missing-provider-a",
140
- providerCallId: undefined,
141
- state: "initiated",
142
- startedAt: now - 10_000,
143
- answeredAt: undefined,
144
- }),
145
- makePersistedCall({
146
- callId: "missing-provider-b",
147
- providerCallId: undefined,
148
- state: "initiated",
149
- startedAt: now - 10_000,
150
- answeredAt: undefined,
151
- }),
152
- makePersistedCall({
153
- callId: "expired-a",
154
- providerCallId: "expired-provider-a",
155
- state: "initiated",
156
- startedAt: now - 600_000,
157
- answeredAt: undefined,
158
- }),
159
- makePersistedCall({
160
- callId: "terminal-a",
161
- providerCallId: "terminal-provider-a",
162
- state: "initiated",
163
- startedAt: now - 20_000,
164
- answeredAt: undefined,
165
- }),
166
- makePersistedCall({
167
- callId: "terminal-b",
168
- providerCallId: "terminal-provider-b",
169
- state: "initiated",
170
- startedAt: now - 20_000,
171
- answeredAt: undefined,
172
- }),
173
- makePersistedCall({
174
- callId: "unknown-a",
175
- providerCallId: "unknown-provider-a",
176
- state: "initiated",
177
- startedAt: now - 20_000,
178
- answeredAt: undefined,
179
- }),
180
- makePersistedCall({
181
- callId: "active-a",
182
- providerCallId: "active-provider-a",
183
- state: "initiated",
184
- startedAt: now - 20_000,
185
- answeredAt: undefined,
186
- }),
187
- makePersistedCall({
188
- callId: "failure-a",
189
- providerCallId: "failure-provider-a",
190
- state: "initiated",
191
- startedAt: now - 20_000,
192
- answeredAt: undefined,
193
- }),
194
- ];
195
- writeCallsToStore(storePath, calls);
196
-
197
- const provider = new FakeProvider();
198
- provider.getCallStatus = async ({ providerCallId }) => {
199
- if (providerCallId.startsWith("terminal-provider")) {
200
- return { status: "completed", isTerminal: true };
201
- }
202
- if (providerCallId.startsWith("unknown-provider")) {
203
- return { status: "unknown", isTerminal: false, isUnknown: true };
204
- }
205
- if (providerCallId.startsWith("active-provider")) {
206
- return { status: "in-progress", isTerminal: false };
207
- }
208
- throw new Error("network failure");
209
- };
210
- const config = VoiceCallConfigSchema.parse({
211
- enabled: true,
212
- provider: "plivo",
213
- fromNumber: "+15550000000",
214
- maxDurationSeconds: 300,
215
- });
216
- const logSpy = vi.spyOn(console, "log").mockImplementation(() => {});
217
- const manager = new CallManager(config, storePath);
218
-
219
- await manager.initialize(provider, "https://example.com/voice/webhook");
220
-
221
- expect(
222
- manager
223
- .getActiveCalls()
224
- .map((call) => call.callId)
225
- .toSorted(),
226
- ).toEqual(["active-a", "failure-a", "unknown-a"]);
227
- const hangupCall = requireSingleHangupCall(provider);
228
- expect(hangupCall.callId).toBe("expired-a");
229
- expect(hangupCall.providerCallId).toBe("expired-provider-a");
230
- expect(hangupCall.reason).toBe("timeout");
231
- expect(logSpy).toHaveBeenCalledWith(
232
- "[voice-call] Skipped 2 restored call(s) with no providerCallId",
233
- );
234
- expect(logSpy).toHaveBeenCalledWith(
235
- "[voice-call] Skipped 1 restored call(s) older than maxDurationSeconds",
236
- );
237
- expect(logSpy).toHaveBeenCalledWith(
238
- "[voice-call] Skipped 2 restored call(s) with provider status: completed",
239
- );
240
- expect(logSpy).toHaveBeenCalledWith(
241
- "[voice-call] Kept 1 restored call(s) confirmed active by provider",
242
- );
243
- expect(logSpy).toHaveBeenCalledWith(
244
- "[voice-call] Kept 1 restored call(s) with unknown provider status (relying on timer)",
245
- );
246
- expect(logSpy).toHaveBeenCalledWith(
247
- "[voice-call] Kept 1 restored call(s) after verification failure (relying on timer)",
248
- );
249
- expect(logSpy.mock.calls.map((call) => String(call[0])).join("\n")).not.toContain("terminal-a");
250
-
251
- logSpy.mockRestore();
252
- });
253
-
254
- it("uses only remaining max duration for restored answered calls", async () => {
255
- vi.useFakeTimers();
256
- const now = new Date("2026-03-17T03:07:00Z");
257
- vi.setSystemTime(now);
258
- const { manager, provider } = await initializeManager({
259
- callOverrides: {
260
- startedAt: now.getTime() - 290_000,
261
- answeredAt: now.getTime() - 290_000,
262
- state: "answered",
263
- },
264
- configOverrides: { maxDurationSeconds: 300 },
265
- });
266
-
267
- expect(manager.getActiveCalls()).toHaveLength(1);
268
- await vi.advanceTimersByTimeAsync(9_000);
269
- expect(manager.getActiveCalls()).toHaveLength(1);
270
- expect(provider.hangupCalls).toHaveLength(0);
271
-
272
- await vi.advanceTimersByTimeAsync(1_100);
273
- expect(manager.getActiveCalls()).toHaveLength(0);
274
- const hangupCall = requireSingleHangupCall(provider);
275
- expect(hangupCall.reason).toBe("timeout");
276
- });
277
-
278
- it("restores dedupe keys from terminal persisted calls so replayed webhooks stay ignored", async () => {
279
- const storePath = createTestStorePath();
280
- const persisted = makePersistedCall({
281
- state: "completed",
282
- endedAt: Date.now() - 5_000,
283
- endReason: "completed",
284
- processedEventIds: ["evt-terminal-init"],
285
- });
286
- writeCallsToStore(storePath, [persisted]);
287
-
288
- const provider = new FakeProvider();
289
- const config = VoiceCallConfigSchema.parse({
290
- enabled: true,
291
- provider: "plivo",
292
- fromNumber: "+15550000000",
293
- });
294
- const manager = new CallManager(config, storePath);
295
- await manager.initialize(provider, "https://example.com/voice/webhook");
296
-
297
- manager.processEvent({
298
- id: "evt-terminal-init",
299
- type: "call.initiated",
300
- callId: String(persisted.providerCallId),
301
- providerCallId: String(persisted.providerCallId),
302
- timestamp: Date.now(),
303
- direction: "outbound",
304
- from: "+15550000000",
305
- to: "+15550000001",
306
- });
307
-
308
- expect(manager.getActiveCalls()).toHaveLength(0);
309
- });
310
- });
@@ -1,127 +0,0 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { VoiceCallConfigSchema } from "./config.js";
5
- import { CallManager } from "./manager.js";
6
- import type { VoiceCallProvider } from "./providers/base.js";
7
- import type {
8
- GetCallStatusInput,
9
- GetCallStatusResult,
10
- HangupCallInput,
11
- InitiateCallInput,
12
- InitiateCallResult,
13
- PlayTtsInput,
14
- ProviderWebhookParseResult,
15
- StartListeningInput,
16
- StopListeningInput,
17
- WebhookContext,
18
- WebhookVerificationResult,
19
- } from "./types.js";
20
-
21
- export class FakeProvider implements VoiceCallProvider {
22
- readonly name: "plivo" | "twilio" | "telnyx";
23
- twilioStreamConnectEnabled = true;
24
- readonly playTtsCalls: PlayTtsInput[] = [];
25
- readonly hangupCalls: HangupCallInput[] = [];
26
- readonly startListeningCalls: StartListeningInput[] = [];
27
- readonly stopListeningCalls: StopListeningInput[] = [];
28
- getCallStatusResult: GetCallStatusResult = { status: "in-progress", isTerminal: false };
29
-
30
- constructor(name: "plivo" | "twilio" | "telnyx" = "plivo") {
31
- this.name = name;
32
- }
33
-
34
- verifyWebhook(_ctx: WebhookContext): WebhookVerificationResult {
35
- return { ok: true };
36
- }
37
-
38
- parseWebhookEvent(_ctx: WebhookContext): ProviderWebhookParseResult {
39
- return { events: [], statusCode: 200 };
40
- }
41
-
42
- async initiateCall(_input: InitiateCallInput): Promise<InitiateCallResult> {
43
- return { providerCallId: "request-uuid", status: "initiated" };
44
- }
45
-
46
- async hangupCall(input: HangupCallInput): Promise<void> {
47
- this.hangupCalls.push(input);
48
- }
49
-
50
- async playTts(input: PlayTtsInput): Promise<void> {
51
- this.playTtsCalls.push(input);
52
- }
53
-
54
- async startListening(input: StartListeningInput): Promise<void> {
55
- this.startListeningCalls.push(input);
56
- }
57
-
58
- async stopListening(input: StopListeningInput): Promise<void> {
59
- this.stopListeningCalls.push(input);
60
- }
61
-
62
- async getCallStatus(_input: GetCallStatusInput): Promise<GetCallStatusResult> {
63
- return this.getCallStatusResult;
64
- }
65
-
66
- isConversationStreamConnectEnabled(): boolean {
67
- return this.name === "twilio" && this.twilioStreamConnectEnabled;
68
- }
69
- }
70
-
71
- export function createTestStorePath(): string {
72
- return fs.mkdtempSync(path.join(os.tmpdir(), "klaw-voice-call-test-"));
73
- }
74
-
75
- export async function createManagerHarness(
76
- configOverrides: Record<string, unknown> = {},
77
- provider = new FakeProvider(),
78
- ): Promise<{
79
- manager: CallManager;
80
- provider: FakeProvider;
81
- }> {
82
- const config = VoiceCallConfigSchema.parse({
83
- enabled: true,
84
- provider: "plivo",
85
- fromNumber: "+15550000000",
86
- ...configOverrides,
87
- });
88
- const manager = new CallManager(config, createTestStorePath());
89
- await manager.initialize(provider, "https://example.com/voice/webhook");
90
- return { manager, provider };
91
- }
92
-
93
- export function markCallAnswered(manager: CallManager, callId: string, eventId: string): void {
94
- manager.processEvent({
95
- id: eventId,
96
- type: "call.answered",
97
- callId,
98
- providerCallId: "request-uuid",
99
- timestamp: Date.now(),
100
- });
101
- }
102
-
103
- export function writeCallsToStore(storePath: string, calls: Record<string, unknown>[]): void {
104
- fs.mkdirSync(storePath, { recursive: true });
105
- const logPath = path.join(storePath, "calls.jsonl");
106
- const lines = calls.map((c) => JSON.stringify(c)).join("\n") + "\n";
107
- fs.writeFileSync(logPath, lines);
108
- }
109
-
110
- export function makePersistedCall(
111
- overrides: Record<string, unknown> = {},
112
- ): Record<string, unknown> {
113
- return {
114
- callId: `call-${Date.now()}-${Math.random().toString(36).slice(2)}`,
115
- providerCallId: `prov-${Date.now()}-${Math.random().toString(36).slice(2)}`,
116
- provider: "plivo",
117
- direction: "outbound",
118
- state: "answered",
119
- from: "+15550000000",
120
- to: "+15550000001",
121
- startedAt: Date.now() - 30_000,
122
- answeredAt: Date.now() - 25_000,
123
- transcript: [],
124
- processedEventIds: [],
125
- ...overrides,
126
- };
127
- }