@kodelyth/voice-call 2026.5.39 → 2026.5.42
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.
- package/README.md +167 -0
- package/api.ts +16 -0
- package/cli-metadata.ts +10 -0
- package/config-api.ts +12 -0
- package/dist/api.js +2 -0
- package/dist/cli-metadata.js +12 -0
- package/dist/config-DAwbG2aw.js +621 -0
- package/dist/config-compat-BYfJ5ueI.js +129 -0
- package/dist/guarded-json-api-xAIbFPZh.js +591 -0
- package/dist/index.js +1341 -0
- package/dist/mock-jtSdKDQN.js +135 -0
- package/dist/plivo-L-JTeuEc.js +392 -0
- package/dist/realtime-handler-5pSItXxX.js +1227 -0
- package/dist/realtime-transcription.runtime-CAbQKwCN.js +2 -0
- package/dist/realtime-voice.runtime-vCpCAutg.js +2 -0
- package/dist/response-generator-B-MjbtsM.js +199 -0
- package/dist/runtime-api.js +6 -0
- package/dist/runtime-entry-ohPMJR46.js +3435 -0
- package/dist/runtime-entry.js +2 -0
- package/dist/setup-api.js +37 -0
- package/dist/telnyx-BWr9EZ4x.js +278 -0
- package/dist/twilio-D9B0zY1k.js +679 -0
- package/index.test.ts +1075 -0
- package/index.ts +863 -0
- package/klaw.plugin.json +30 -133
- package/package.json +3 -3
- package/runtime-api.ts +20 -0
- package/runtime-entry.ts +1 -0
- package/setup-api.ts +47 -0
- package/src/allowlist.test.ts +18 -0
- package/src/allowlist.ts +19 -0
- package/src/cli.test.ts +12 -0
- package/src/cli.ts +866 -0
- package/src/config-compat.test.ts +130 -0
- package/src/config-compat.ts +227 -0
- package/src/config.test.ts +542 -0
- package/src/config.ts +883 -0
- package/src/core-bridge.ts +14 -0
- package/src/deep-merge.test.ts +40 -0
- package/src/deep-merge.ts +23 -0
- package/src/gateway-continue-operation.ts +200 -0
- package/src/http-headers.test.ts +16 -0
- package/src/http-headers.ts +15 -0
- package/src/manager/context.ts +50 -0
- package/src/manager/events.test.ts +578 -0
- package/src/manager/events.ts +332 -0
- package/src/manager/lifecycle.ts +53 -0
- package/src/manager/lookup.test.ts +52 -0
- package/src/manager/lookup.ts +35 -0
- package/src/manager/outbound.test.ts +629 -0
- package/src/manager/outbound.ts +508 -0
- package/src/manager/state.ts +48 -0
- package/src/manager/store.ts +107 -0
- package/src/manager/timers.test.ts +127 -0
- package/src/manager/timers.ts +113 -0
- package/src/manager/twiml.test.ts +13 -0
- package/src/manager/twiml.ts +17 -0
- package/src/manager.closed-loop.test.ts +259 -0
- package/src/manager.inbound-allowlist.test.ts +183 -0
- package/src/manager.notify.test.ts +390 -0
- package/src/manager.restore.test.ts +310 -0
- package/src/manager.test-harness.ts +127 -0
- package/src/manager.ts +441 -0
- package/src/media-stream.test.ts +953 -0
- package/src/media-stream.ts +876 -0
- package/src/providers/base.ts +99 -0
- package/src/providers/mock.test.ts +86 -0
- package/src/providers/mock.ts +185 -0
- package/src/providers/plivo.test.ts +93 -0
- package/src/providers/plivo.ts +601 -0
- package/src/providers/shared/call-status.test.ts +24 -0
- package/src/providers/shared/call-status.ts +24 -0
- package/src/providers/shared/guarded-json-api.test.ts +127 -0
- package/src/providers/shared/guarded-json-api.ts +49 -0
- package/src/providers/telnyx.test.ts +489 -0
- package/src/providers/telnyx.ts +419 -0
- package/src/providers/twilio/api.test.ts +184 -0
- package/src/providers/twilio/api.ts +100 -0
- package/src/providers/twilio/twiml-policy.test.ts +84 -0
- package/src/providers/twilio/twiml-policy.ts +87 -0
- package/src/providers/twilio/webhook.ts +34 -0
- package/src/providers/twilio.test.ts +607 -0
- package/src/providers/twilio.ts +861 -0
- package/src/providers/twilio.types.ts +17 -0
- package/src/realtime-agent-context.test.ts +101 -0
- package/src/realtime-agent-context.ts +149 -0
- package/src/realtime-defaults.ts +3 -0
- package/src/realtime-fast-context.test.ts +74 -0
- package/src/realtime-fast-context.ts +27 -0
- package/src/realtime-transcription.runtime.ts +4 -0
- package/src/realtime-voice.runtime.ts +5 -0
- package/src/response-generator.test.ts +385 -0
- package/src/response-generator.ts +348 -0
- package/src/response-model.test.ts +71 -0
- package/src/response-model.ts +23 -0
- package/src/runtime.test.ts +625 -0
- package/src/runtime.ts +528 -0
- package/src/telephony-audio.test.ts +61 -0
- package/src/telephony-audio.ts +12 -0
- package/src/telephony-tts.test.ts +196 -0
- package/src/telephony-tts.ts +235 -0
- package/src/test-fixtures.ts +82 -0
- package/src/tts-provider-voice.test.ts +34 -0
- package/src/tts-provider-voice.ts +21 -0
- package/src/tunnel.test.ts +173 -0
- package/src/tunnel.ts +314 -0
- package/src/types.ts +311 -0
- package/src/utils.test.ts +17 -0
- package/src/utils.ts +14 -0
- package/src/voice-mapping.test.ts +32 -0
- package/src/voice-mapping.ts +65 -0
- package/src/webhook/realtime-audio-pacer.test.ts +146 -0
- package/src/webhook/realtime-audio-pacer.ts +204 -0
- package/src/webhook/realtime-handler.test.ts +1450 -0
- package/src/webhook/realtime-handler.ts +1382 -0
- package/src/webhook/stale-call-reaper.test.ts +89 -0
- package/src/webhook/stale-call-reaper.ts +38 -0
- package/src/webhook/stream-frame-adapter.test.ts +187 -0
- package/src/webhook/stream-frame-adapter.ts +219 -0
- package/src/webhook/tailscale.test.ts +216 -0
- package/src/webhook/tailscale.ts +129 -0
- package/src/webhook-exposure.test.ts +33 -0
- package/src/webhook-exposure.ts +84 -0
- package/src/webhook-security.test.ts +813 -0
- package/src/webhook-security.ts +982 -0
- package/src/webhook.hangup-once.lifecycle.test.ts +179 -0
- package/src/webhook.test.ts +1615 -0
- package/src/webhook.ts +933 -0
- package/src/webhook.types.ts +5 -0
- package/src/websocket-test-support.ts +72 -0
- package/tsconfig.json +16 -0
- package/api.js +0 -7
- package/cli-metadata.js +0 -7
- package/index.js +0 -7
- package/runtime-api.js +0 -7
- package/runtime-entry.js +0 -7
- package/setup-api.js +0 -7
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { WebhookContext } from "../../types.js";
|
|
3
|
+
import { decideTwimlResponse, readTwimlRequestView } from "./twiml-policy.js";
|
|
4
|
+
|
|
5
|
+
function createContext(rawBody: string, query?: WebhookContext["query"]): WebhookContext {
|
|
6
|
+
return {
|
|
7
|
+
headers: {},
|
|
8
|
+
rawBody,
|
|
9
|
+
url: "https://example.ngrok.app/voice/twilio",
|
|
10
|
+
method: "POST",
|
|
11
|
+
query,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("twiml policy", () => {
|
|
16
|
+
it("returns stored twiml decision for initial notify callback", () => {
|
|
17
|
+
const view = readTwimlRequestView(
|
|
18
|
+
createContext("CallStatus=initiated&Direction=outbound-api&CallSid=CA123", {
|
|
19
|
+
callId: "call-1",
|
|
20
|
+
}),
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const decision = decideTwimlResponse({
|
|
24
|
+
...view,
|
|
25
|
+
hasStoredTwiml: true,
|
|
26
|
+
isNotifyCall: true,
|
|
27
|
+
hasActiveStreams: false,
|
|
28
|
+
canStream: true,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(decision.kind).toBe("stored");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("returns queue for inbound when another stream is active", () => {
|
|
35
|
+
const view = readTwimlRequestView(
|
|
36
|
+
createContext("CallStatus=ringing&Direction=inbound&CallSid=CA456"),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
const decision = decideTwimlResponse({
|
|
40
|
+
...view,
|
|
41
|
+
hasStoredTwiml: false,
|
|
42
|
+
isNotifyCall: false,
|
|
43
|
+
hasActiveStreams: true,
|
|
44
|
+
canStream: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
expect(decision.kind).toBe("queue");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns stream + activation for inbound call when available", () => {
|
|
51
|
+
const view = readTwimlRequestView(
|
|
52
|
+
createContext("CallStatus=ringing&Direction=inbound&CallSid=CA789"),
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const decision = decideTwimlResponse({
|
|
56
|
+
...view,
|
|
57
|
+
hasStoredTwiml: false,
|
|
58
|
+
isNotifyCall: false,
|
|
59
|
+
hasActiveStreams: false,
|
|
60
|
+
canStream: true,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
expect(decision.kind).toBe("stream");
|
|
64
|
+
expect(decision.activateStreamCallSid).toBe("CA789");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns empty for status callbacks", () => {
|
|
68
|
+
const view = readTwimlRequestView(
|
|
69
|
+
createContext("CallStatus=completed&Direction=inbound&CallSid=CA123", {
|
|
70
|
+
type: "status",
|
|
71
|
+
}),
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
const decision = decideTwimlResponse({
|
|
75
|
+
...view,
|
|
76
|
+
hasStoredTwiml: false,
|
|
77
|
+
isNotifyCall: false,
|
|
78
|
+
hasActiveStreams: false,
|
|
79
|
+
canStream: true,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
expect(decision.kind).toBe("empty");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { normalizeOptionalString } from "klaw/plugin-sdk/string-coerce-runtime";
|
|
2
|
+
import type { WebhookContext } from "../../types.js";
|
|
3
|
+
|
|
4
|
+
type TwimlRequestView = {
|
|
5
|
+
callStatus: string | null;
|
|
6
|
+
direction: string | null;
|
|
7
|
+
isStatusCallback: boolean;
|
|
8
|
+
callSid?: string;
|
|
9
|
+
callIdFromQuery?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type TwimlPolicyInput = TwimlRequestView & {
|
|
13
|
+
hasStoredTwiml: boolean;
|
|
14
|
+
isNotifyCall: boolean;
|
|
15
|
+
hasActiveStreams: boolean;
|
|
16
|
+
canStream: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type TwimlDecision =
|
|
20
|
+
| {
|
|
21
|
+
kind: "empty" | "pause" | "queue";
|
|
22
|
+
consumeStoredTwimlCallId?: string;
|
|
23
|
+
activateStreamCallSid?: string;
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
kind: "stored";
|
|
27
|
+
consumeStoredTwimlCallId: string;
|
|
28
|
+
activateStreamCallSid?: string;
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
kind: "stream";
|
|
32
|
+
consumeStoredTwimlCallId?: string;
|
|
33
|
+
activateStreamCallSid?: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
function isOutboundDirection(direction: string | null): boolean {
|
|
37
|
+
return direction?.startsWith("outbound") ?? false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function readTwimlRequestView(ctx: WebhookContext): TwimlRequestView {
|
|
41
|
+
const params = new URLSearchParams(ctx.rawBody);
|
|
42
|
+
const type = normalizeOptionalString(ctx.query?.type);
|
|
43
|
+
const callIdFromQuery = normalizeOptionalString(ctx.query?.callId);
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
callStatus: params.get("CallStatus"),
|
|
47
|
+
direction: params.get("Direction"),
|
|
48
|
+
isStatusCallback: type === "status",
|
|
49
|
+
callSid: params.get("CallSid") || undefined,
|
|
50
|
+
callIdFromQuery,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function decideTwimlResponse(input: TwimlPolicyInput): TwimlDecision {
|
|
55
|
+
if (input.callIdFromQuery && !input.isStatusCallback) {
|
|
56
|
+
if (input.hasStoredTwiml) {
|
|
57
|
+
return { kind: "stored", consumeStoredTwimlCallId: input.callIdFromQuery };
|
|
58
|
+
}
|
|
59
|
+
if (input.isNotifyCall) {
|
|
60
|
+
return { kind: "empty" };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (isOutboundDirection(input.direction)) {
|
|
64
|
+
return input.canStream ? { kind: "stream" } : { kind: "pause" };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (input.isStatusCallback) {
|
|
69
|
+
return { kind: "empty" };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (input.direction === "inbound") {
|
|
73
|
+
if (input.hasActiveStreams) {
|
|
74
|
+
return { kind: "queue" };
|
|
75
|
+
}
|
|
76
|
+
if (input.canStream && input.callSid) {
|
|
77
|
+
return { kind: "stream", activateStreamCallSid: input.callSid };
|
|
78
|
+
}
|
|
79
|
+
return { kind: "pause" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (input.callStatus !== "in-progress") {
|
|
83
|
+
return { kind: "empty" };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return input.canStream ? { kind: "stream" } : { kind: "pause" };
|
|
87
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { WebhookContext, WebhookVerificationResult } from "../../types.js";
|
|
2
|
+
import { verifyTwilioWebhook } from "../../webhook-security.js";
|
|
3
|
+
import type { TwilioProviderOptions } from "../twilio.types.js";
|
|
4
|
+
|
|
5
|
+
export function verifyTwilioProviderWebhook(params: {
|
|
6
|
+
ctx: WebhookContext;
|
|
7
|
+
authToken: string;
|
|
8
|
+
currentPublicUrl?: string | null;
|
|
9
|
+
options: TwilioProviderOptions;
|
|
10
|
+
}): WebhookVerificationResult {
|
|
11
|
+
const result = verifyTwilioWebhook(params.ctx, params.authToken, {
|
|
12
|
+
publicUrl: params.currentPublicUrl || undefined,
|
|
13
|
+
allowNgrokFreeTierLoopbackBypass: params.options.allowNgrokFreeTierLoopbackBypass ?? false,
|
|
14
|
+
skipVerification: params.options.skipVerification,
|
|
15
|
+
allowedHosts: params.options.webhookSecurity?.allowedHosts,
|
|
16
|
+
trustForwardingHeaders: params.options.webhookSecurity?.trustForwardingHeaders,
|
|
17
|
+
trustedProxyIPs: params.options.webhookSecurity?.trustedProxyIPs,
|
|
18
|
+
remoteIP: params.ctx.remoteAddress,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (!result.ok) {
|
|
22
|
+
console.warn(`[twilio] Webhook verification failed: ${result.reason}`);
|
|
23
|
+
if (result.verificationUrl) {
|
|
24
|
+
console.warn(`[twilio] Verification URL: ${result.verificationUrl}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
ok: result.ok,
|
|
30
|
+
reason: result.reason,
|
|
31
|
+
isReplay: result.isReplay,
|
|
32
|
+
verifiedRequestKey: result.verifiedRequestKey,
|
|
33
|
+
};
|
|
34
|
+
}
|