@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.
Files changed (137) hide show
  1. package/README.md +167 -0
  2. package/api.ts +16 -0
  3. package/cli-metadata.ts +10 -0
  4. package/config-api.ts +12 -0
  5. package/dist/api.js +2 -0
  6. package/dist/cli-metadata.js +12 -0
  7. package/dist/config-DAwbG2aw.js +621 -0
  8. package/dist/config-compat-BYfJ5ueI.js +129 -0
  9. package/dist/guarded-json-api-xAIbFPZh.js +591 -0
  10. package/dist/index.js +1341 -0
  11. package/dist/mock-jtSdKDQN.js +135 -0
  12. package/dist/plivo-L-JTeuEc.js +392 -0
  13. package/dist/realtime-handler-5pSItXxX.js +1227 -0
  14. package/dist/realtime-transcription.runtime-CAbQKwCN.js +2 -0
  15. package/dist/realtime-voice.runtime-vCpCAutg.js +2 -0
  16. package/dist/response-generator-B-MjbtsM.js +199 -0
  17. package/dist/runtime-api.js +6 -0
  18. package/dist/runtime-entry-ohPMJR46.js +3435 -0
  19. package/dist/runtime-entry.js +2 -0
  20. package/dist/setup-api.js +37 -0
  21. package/dist/telnyx-BWr9EZ4x.js +278 -0
  22. package/dist/twilio-D9B0zY1k.js +679 -0
  23. package/index.test.ts +1075 -0
  24. package/index.ts +863 -0
  25. package/klaw.plugin.json +30 -133
  26. package/package.json +3 -3
  27. package/runtime-api.ts +20 -0
  28. package/runtime-entry.ts +1 -0
  29. package/setup-api.ts +47 -0
  30. package/src/allowlist.test.ts +18 -0
  31. package/src/allowlist.ts +19 -0
  32. package/src/cli.test.ts +12 -0
  33. package/src/cli.ts +866 -0
  34. package/src/config-compat.test.ts +130 -0
  35. package/src/config-compat.ts +227 -0
  36. package/src/config.test.ts +542 -0
  37. package/src/config.ts +883 -0
  38. package/src/core-bridge.ts +14 -0
  39. package/src/deep-merge.test.ts +40 -0
  40. package/src/deep-merge.ts +23 -0
  41. package/src/gateway-continue-operation.ts +200 -0
  42. package/src/http-headers.test.ts +16 -0
  43. package/src/http-headers.ts +15 -0
  44. package/src/manager/context.ts +50 -0
  45. package/src/manager/events.test.ts +578 -0
  46. package/src/manager/events.ts +332 -0
  47. package/src/manager/lifecycle.ts +53 -0
  48. package/src/manager/lookup.test.ts +52 -0
  49. package/src/manager/lookup.ts +35 -0
  50. package/src/manager/outbound.test.ts +629 -0
  51. package/src/manager/outbound.ts +508 -0
  52. package/src/manager/state.ts +48 -0
  53. package/src/manager/store.ts +107 -0
  54. package/src/manager/timers.test.ts +127 -0
  55. package/src/manager/timers.ts +113 -0
  56. package/src/manager/twiml.test.ts +13 -0
  57. package/src/manager/twiml.ts +17 -0
  58. package/src/manager.closed-loop.test.ts +259 -0
  59. package/src/manager.inbound-allowlist.test.ts +183 -0
  60. package/src/manager.notify.test.ts +390 -0
  61. package/src/manager.restore.test.ts +310 -0
  62. package/src/manager.test-harness.ts +127 -0
  63. package/src/manager.ts +441 -0
  64. package/src/media-stream.test.ts +953 -0
  65. package/src/media-stream.ts +876 -0
  66. package/src/providers/base.ts +99 -0
  67. package/src/providers/mock.test.ts +86 -0
  68. package/src/providers/mock.ts +185 -0
  69. package/src/providers/plivo.test.ts +93 -0
  70. package/src/providers/plivo.ts +601 -0
  71. package/src/providers/shared/call-status.test.ts +24 -0
  72. package/src/providers/shared/call-status.ts +24 -0
  73. package/src/providers/shared/guarded-json-api.test.ts +127 -0
  74. package/src/providers/shared/guarded-json-api.ts +49 -0
  75. package/src/providers/telnyx.test.ts +489 -0
  76. package/src/providers/telnyx.ts +419 -0
  77. package/src/providers/twilio/api.test.ts +184 -0
  78. package/src/providers/twilio/api.ts +100 -0
  79. package/src/providers/twilio/twiml-policy.test.ts +84 -0
  80. package/src/providers/twilio/twiml-policy.ts +87 -0
  81. package/src/providers/twilio/webhook.ts +34 -0
  82. package/src/providers/twilio.test.ts +607 -0
  83. package/src/providers/twilio.ts +861 -0
  84. package/src/providers/twilio.types.ts +17 -0
  85. package/src/realtime-agent-context.test.ts +101 -0
  86. package/src/realtime-agent-context.ts +149 -0
  87. package/src/realtime-defaults.ts +3 -0
  88. package/src/realtime-fast-context.test.ts +74 -0
  89. package/src/realtime-fast-context.ts +27 -0
  90. package/src/realtime-transcription.runtime.ts +4 -0
  91. package/src/realtime-voice.runtime.ts +5 -0
  92. package/src/response-generator.test.ts +385 -0
  93. package/src/response-generator.ts +348 -0
  94. package/src/response-model.test.ts +71 -0
  95. package/src/response-model.ts +23 -0
  96. package/src/runtime.test.ts +625 -0
  97. package/src/runtime.ts +528 -0
  98. package/src/telephony-audio.test.ts +61 -0
  99. package/src/telephony-audio.ts +12 -0
  100. package/src/telephony-tts.test.ts +196 -0
  101. package/src/telephony-tts.ts +235 -0
  102. package/src/test-fixtures.ts +82 -0
  103. package/src/tts-provider-voice.test.ts +34 -0
  104. package/src/tts-provider-voice.ts +21 -0
  105. package/src/tunnel.test.ts +173 -0
  106. package/src/tunnel.ts +314 -0
  107. package/src/types.ts +311 -0
  108. package/src/utils.test.ts +17 -0
  109. package/src/utils.ts +14 -0
  110. package/src/voice-mapping.test.ts +32 -0
  111. package/src/voice-mapping.ts +65 -0
  112. package/src/webhook/realtime-audio-pacer.test.ts +146 -0
  113. package/src/webhook/realtime-audio-pacer.ts +204 -0
  114. package/src/webhook/realtime-handler.test.ts +1450 -0
  115. package/src/webhook/realtime-handler.ts +1382 -0
  116. package/src/webhook/stale-call-reaper.test.ts +89 -0
  117. package/src/webhook/stale-call-reaper.ts +38 -0
  118. package/src/webhook/stream-frame-adapter.test.ts +187 -0
  119. package/src/webhook/stream-frame-adapter.ts +219 -0
  120. package/src/webhook/tailscale.test.ts +216 -0
  121. package/src/webhook/tailscale.ts +129 -0
  122. package/src/webhook-exposure.test.ts +33 -0
  123. package/src/webhook-exposure.ts +84 -0
  124. package/src/webhook-security.test.ts +813 -0
  125. package/src/webhook-security.ts +982 -0
  126. package/src/webhook.hangup-once.lifecycle.test.ts +179 -0
  127. package/src/webhook.test.ts +1615 -0
  128. package/src/webhook.ts +933 -0
  129. package/src/webhook.types.ts +5 -0
  130. package/src/websocket-test-support.ts +72 -0
  131. package/tsconfig.json +16 -0
  132. package/api.js +0 -7
  133. package/cli-metadata.js +0 -7
  134. package/index.js +0 -7
  135. package/runtime-api.js +0 -7
  136. package/runtime-entry.js +0 -7
  137. 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
+ }