@absolutejs/voice 0.0.22-beta.73 → 0.0.22-beta.74

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/dist/index.d.ts CHANGED
@@ -80,7 +80,8 @@ export type { VoiceOpsTaskLease, VoiceOpsTaskWorker, VoiceOpsTaskWorkerOptions,
80
80
  export type { VoiceS3ReviewStoreClient, VoiceS3ReviewStoreFile, VoiceS3ReviewStoreOptions } from './s3Store';
81
81
  export type { VoiceSQLiteRuntimeStorage, VoiceSQLiteStoreOptions } from './sqliteStore';
82
82
  export type { StoredVoiceIntegrationEvent, StoredVoiceExternalObjectMap, StoredVoiceOpsTask, VoiceExternalObjectMap, VoiceExternalObjectMapStore, VoiceOpsTaskAgeBucket, VoiceOpsTaskAnalyticsOptions, VoiceOpsTaskAnalyticsSummary, VoiceOpsTaskAssignmentRule, VoiceOpsTaskAssignmentRuleCondition, VoiceOpsTaskAssignmentRules, VoiceOpsTaskAssigneeAnalytics, VoiceOpsDispositionTaskPolicies, VoiceOpsSLABreachPolicy, VoiceIntegrationDeliveryStatus, VoiceIntegrationEvent, VoiceIntegrationEventStore, VoiceIntegrationSinkDelivery, VoiceIntegrationEventType, VoiceIntegrationWebhookConfig, VoiceOpsTask, VoiceOpsTaskHistoryEntry, VoiceOpsTaskKind, VoiceOpsTaskPolicy, VoiceOpsTaskPriority, VoiceOpsTaskStatus, VoiceOpsTaskStore, VoiceOpsTaskSummary, VoiceOpsTaskWorkerAnalytics } from './ops';
83
- export { createTwilioMediaStreamBridge, createTwilioVoiceResponse, decodeTwilioMulawBase64, encodeTwilioMulawBase64, transcodePCMToTwilioOutboundPayload, transcodeTwilioInboundPayloadToPCM16 } from './telephony/twilio';
83
+ export { createTwilioMediaStreamBridge, createTwilioVoiceRoutes, createTwilioVoiceResponse, decodeTwilioMulawBase64, encodeTwilioMulawBase64, transcodePCMToTwilioOutboundPayload, transcodeTwilioInboundPayloadToPCM16 } from './telephony/twilio';
84
+ export type { TwilioInboundMessage, TwilioMediaStreamBridge, TwilioMediaStreamBridgeOptions, TwilioMediaStreamSocket, TwilioOutboundClearMessage, TwilioOutboundMarkMessage, TwilioOutboundMediaMessage, TwilioOutboundMessage, TwilioVoiceRouteParameters, TwilioVoiceResponseOptions, TwilioVoiceRoutesOptions } from './telephony/twilio';
84
85
  export { shapeTelephonyAssistantText } from './telephony/response';
85
86
  export type { TelephonyResponseShapeMode, TelephonyResponseShapeOptions } from './telephony/response';
86
87
  export * from './types';
package/dist/index.js CHANGED
@@ -15197,9 +15197,35 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
15197
15197
  };
15198
15198
  // src/telephony/twilio.ts
15199
15199
  import { Buffer as Buffer3 } from "buffer";
15200
+ import { Elysia as Elysia18 } from "elysia";
15200
15201
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
15201
15202
  var VOICE_PCM_SAMPLE_RATE = 16000;
15202
15203
  var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
15204
+ var resolveRequestOrigin = (request) => {
15205
+ const url = new URL(request.url);
15206
+ const forwardedHost = request.headers.get("x-forwarded-host");
15207
+ const forwardedProto = request.headers.get("x-forwarded-proto");
15208
+ const host = forwardedHost ?? request.headers.get("host") ?? url.host;
15209
+ const protocol = forwardedProto ?? url.protocol.replace(":", "");
15210
+ return `${protocol}://${host}`;
15211
+ };
15212
+ var resolveTwilioStreamUrl = async (options, input) => {
15213
+ if (typeof options.twiml?.streamUrl === "function") {
15214
+ return options.twiml.streamUrl(input);
15215
+ }
15216
+ if (typeof options.twiml?.streamUrl === "string") {
15217
+ return options.twiml.streamUrl;
15218
+ }
15219
+ const origin = resolveRequestOrigin(input.request);
15220
+ const wsOrigin = origin.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
15221
+ return `${wsOrigin}${input.streamPath}`;
15222
+ };
15223
+ var resolveTwilioStreamParameters = async (parameters, input) => {
15224
+ if (typeof parameters === "function") {
15225
+ return parameters(input);
15226
+ }
15227
+ return parameters;
15228
+ };
15203
15229
  var normalizeOnTurn2 = (handler) => {
15204
15230
  if (handler.length > 1) {
15205
15231
  const directHandler = handler;
@@ -15576,6 +15602,83 @@ var createTwilioMediaStreamBridge = (socket, options) => {
15576
15602
  }
15577
15603
  };
15578
15604
  };
15605
+ var createTwilioVoiceRoutes = (options) => {
15606
+ const streamPath = options.streamPath ?? "/api/voice/twilio/stream";
15607
+ const twimlPath = options.twiml?.path ?? "/api/voice/twilio";
15608
+ const webhookPath = options.webhook?.path ?? "/api/voice/twilio/webhook";
15609
+ const bridges = new WeakMap;
15610
+ const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
15611
+ return new Elysia18({
15612
+ name: options.name ?? "absolutejs-voice-twilio"
15613
+ }).get(twimlPath, async ({ query, request }) => {
15614
+ const streamUrl = await resolveTwilioStreamUrl(options, {
15615
+ query,
15616
+ request,
15617
+ streamPath
15618
+ });
15619
+ const parameters = await resolveTwilioStreamParameters(options.twiml?.parameters, {
15620
+ query,
15621
+ request
15622
+ });
15623
+ return new Response(createTwilioVoiceResponse({
15624
+ parameters,
15625
+ streamName: options.twiml?.streamName,
15626
+ streamUrl,
15627
+ track: options.twiml?.track
15628
+ }), {
15629
+ headers: {
15630
+ "content-type": "text/xml; charset=utf-8"
15631
+ }
15632
+ });
15633
+ }).post(twimlPath, async ({ query, request }) => {
15634
+ const streamUrl = await resolveTwilioStreamUrl(options, {
15635
+ query,
15636
+ request,
15637
+ streamPath
15638
+ });
15639
+ const parameters = await resolveTwilioStreamParameters(options.twiml?.parameters, {
15640
+ query,
15641
+ request
15642
+ });
15643
+ return new Response(createTwilioVoiceResponse({
15644
+ parameters,
15645
+ streamName: options.twiml?.streamName,
15646
+ streamUrl,
15647
+ track: options.twiml?.track
15648
+ }), {
15649
+ headers: {
15650
+ "content-type": "text/xml; charset=utf-8"
15651
+ }
15652
+ });
15653
+ }).ws(streamPath, {
15654
+ close: async (ws, _code, reason) => {
15655
+ const bridge = bridges.get(ws);
15656
+ bridges.delete(ws);
15657
+ await bridge?.close(reason);
15658
+ },
15659
+ message: async (ws, raw) => {
15660
+ let bridge = bridges.get(ws);
15661
+ if (!bridge) {
15662
+ bridge = createTwilioMediaStreamBridge({
15663
+ close: (code, reason) => {
15664
+ ws.close(code, reason);
15665
+ },
15666
+ send: (data) => {
15667
+ ws.send(data);
15668
+ }
15669
+ }, options);
15670
+ bridges.set(ws, bridge);
15671
+ }
15672
+ await bridge.handleMessage(raw);
15673
+ }
15674
+ }).use(createVoiceTelephonyWebhookRoutes({
15675
+ ...options.webhook ?? {},
15676
+ context: options.context,
15677
+ path: webhookPath,
15678
+ policy: webhookPolicy,
15679
+ provider: "twilio"
15680
+ }));
15681
+ };
15579
15682
  // src/telephony/response.ts
15580
15683
  var normalizeWhitespace = (value) => value.replace(/\s+/g, " ").trim();
15581
15684
  var DEFAULT_MAX_WORDS = 12;
@@ -15871,6 +15974,7 @@ export {
15871
15974
  createVoiceAgentTool,
15872
15975
  createVoiceAgentSquad,
15873
15976
  createVoiceAgent,
15977
+ createTwilioVoiceRoutes,
15874
15978
  createTwilioVoiceResponse,
15875
15979
  createTwilioMediaStreamBridge,
15876
15980
  createStoredVoiceOpsTask,
@@ -1,3 +1,5 @@
1
+ import { Elysia } from 'elysia';
2
+ import { type VoiceTelephonyOutcomePolicy, type VoiceTelephonyWebhookRoutesOptions } from '../telephonyOutcome';
1
3
  import { type VoiceCallReviewArtifact, type VoiceCallReviewConfig } from '../testing/review';
2
4
  import type { AudioFormat, VoiceLogger, VoicePluginConfig, VoiceSessionRecord, VoiceServerMessage } from '../types';
3
5
  type TwilioMediaPayload = {
@@ -107,10 +109,114 @@ export type TwilioVoiceResponseOptions = {
107
109
  streamUrl: string;
108
110
  track?: 'both_tracks' | 'inbound_track' | 'outbound_track';
109
111
  };
112
+ export type TwilioVoiceRouteParameters = Record<string, string | number | boolean | undefined> | ((input: {
113
+ query: Record<string, unknown>;
114
+ request: Request;
115
+ }) => Promise<Record<string, string | number | boolean | undefined>> | Record<string, string | number | boolean | undefined>);
116
+ export type TwilioVoiceRoutesOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = TwilioMediaStreamBridgeOptions<TContext, TSession, TResult> & {
117
+ name?: string;
118
+ outcomePolicy?: VoiceTelephonyOutcomePolicy;
119
+ streamPath?: string;
120
+ twiml?: {
121
+ parameters?: TwilioVoiceRouteParameters;
122
+ path?: string;
123
+ streamName?: string;
124
+ streamUrl?: string | ((input: {
125
+ query: Record<string, unknown>;
126
+ request: Request;
127
+ streamPath: string;
128
+ }) => Promise<string> | string);
129
+ track?: TwilioVoiceResponseOptions['track'];
130
+ };
131
+ webhook?: Omit<VoiceTelephonyWebhookRoutesOptions<TContext, TSession, TResult>, 'context' | 'path' | 'policy' | 'provider'> & {
132
+ path?: string;
133
+ policy?: VoiceTelephonyOutcomePolicy;
134
+ };
135
+ };
110
136
  export declare const decodeTwilioMulawBase64: (payload: string) => Int16Array<ArrayBuffer>;
111
137
  export declare const encodeTwilioMulawBase64: (samples: Int16Array) => string;
112
138
  export declare const transcodeTwilioInboundPayloadToPCM16: (payload: string) => Uint8Array<ArrayBuffer>;
113
139
  export declare const transcodePCMToTwilioOutboundPayload: (chunk: Uint8Array, format: AudioFormat) => string;
114
140
  export declare const createTwilioVoiceResponse: (options: TwilioVoiceResponseOptions) => string;
115
141
  export declare const createTwilioMediaStreamBridge: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(socket: TwilioMediaStreamSocket, options: TwilioMediaStreamBridgeOptions<TContext, TSession, TResult>) => TwilioMediaStreamBridge;
142
+ export declare const createTwilioVoiceRoutes: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options: TwilioVoiceRoutesOptions<TContext, TSession, TResult>) => Elysia<"", {
143
+ decorator: {};
144
+ store: {};
145
+ derive: {};
146
+ resolve: {};
147
+ }, {
148
+ typebox: {};
149
+ error: {};
150
+ }, {
151
+ schema: {};
152
+ standaloneSchema: {};
153
+ macro: {};
154
+ macroFn: {};
155
+ parser: {};
156
+ response: {};
157
+ }, {
158
+ [x: string]: {
159
+ get: {
160
+ body: unknown;
161
+ params: {};
162
+ query: unknown;
163
+ headers: unknown;
164
+ response: {
165
+ 200: Response;
166
+ };
167
+ };
168
+ };
169
+ } & {
170
+ [x: string]: {
171
+ post: {
172
+ body: unknown;
173
+ params: {};
174
+ query: unknown;
175
+ headers: unknown;
176
+ response: {
177
+ 200: Response;
178
+ };
179
+ };
180
+ };
181
+ } & {
182
+ [x: string]: {
183
+ subscribe: {
184
+ body: unknown;
185
+ params: {};
186
+ query: unknown;
187
+ headers: unknown;
188
+ response: {};
189
+ };
190
+ };
191
+ } & {
192
+ [x: string]: {
193
+ post: {
194
+ body: unknown;
195
+ params: {};
196
+ query: unknown;
197
+ headers: unknown;
198
+ response: {
199
+ 200: Response | import("..").VoiceTelephonyWebhookDecision<TResult>;
200
+ };
201
+ };
202
+ };
203
+ }, {
204
+ derive: {};
205
+ resolve: {};
206
+ schema: {};
207
+ standaloneSchema: {};
208
+ response: {};
209
+ }, {
210
+ derive: {};
211
+ resolve: {};
212
+ schema: {};
213
+ standaloneSchema: {};
214
+ response: {};
215
+ } & {
216
+ derive: {};
217
+ resolve: {};
218
+ schema: {};
219
+ standaloneSchema: {};
220
+ response: {};
221
+ }>;
116
222
  export {};
@@ -4683,7 +4683,7 @@ var createVoiceMemoryStore = () => {
4683
4683
  };
4684
4684
 
4685
4685
  // src/session.ts
4686
- import { Buffer } from "buffer";
4686
+ import { Buffer as Buffer2 } from "buffer";
4687
4687
 
4688
4688
  // src/handoff.ts
4689
4689
  var toHex = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
@@ -5014,7 +5014,7 @@ var createEmptyCurrentTurn = () => ({
5014
5014
  transcripts: []
5015
5015
  });
5016
5016
  var cloneTranscript = (transcript) => ({ ...transcript });
5017
- var encodeBase64 = (chunk) => Buffer.from(chunk).toString("base64");
5017
+ var encodeBase64 = (chunk) => Buffer2.from(chunk).toString("base64");
5018
5018
  var countWords2 = (text) => text.trim().split(/\s+/).filter(Boolean).length;
5019
5019
  var normalizeText2 = (text) => text.trim().replace(/\s+/g, " ");
5020
5020
  var getAudioChunkDurationMs = (chunk) => chunk.byteLength / (DEFAULT_FORMAT.sampleRateHz * DEFAULT_FORMAT.channels * 2) * 1000;
@@ -7866,10 +7866,705 @@ var runVoiceSessionBenchmarkSeries = async (input) => {
7866
7866
  });
7867
7867
  };
7868
7868
  // src/telephony/twilio.ts
7869
- import { Buffer as Buffer2 } from "buffer";
7869
+ import { Buffer as Buffer3 } from "buffer";
7870
+ import { Elysia as Elysia2 } from "elysia";
7871
+
7872
+ // src/telephonyOutcome.ts
7873
+ import { Elysia } from "elysia";
7874
+ var DEFAULT_COMPLETED_STATUSES = [
7875
+ "answered",
7876
+ "completed",
7877
+ "complete",
7878
+ "connected",
7879
+ "in-progress",
7880
+ "live"
7881
+ ];
7882
+ var DEFAULT_NO_ANSWER_STATUSES = [
7883
+ "busy",
7884
+ "canceled",
7885
+ "cancelled",
7886
+ "failed",
7887
+ "no-answer",
7888
+ "no_answer",
7889
+ "not-answered",
7890
+ "ring-no-answer",
7891
+ "timeout",
7892
+ "unanswered"
7893
+ ];
7894
+ var DEFAULT_VOICEMAIL_STATUSES = [
7895
+ "answering-machine",
7896
+ "machine",
7897
+ "voicemail",
7898
+ "voice-mail"
7899
+ ];
7900
+ var DEFAULT_TRANSFER_STATUSES = ["bridged", "forwarded", "transferred"];
7901
+ var DEFAULT_ESCALATION_STATUSES = ["escalated", "human-required", "operator"];
7902
+ var DEFAULT_FAILED_STATUSES = ["busy", "failed", "no-answer"];
7903
+ var DEFAULT_MACHINE_VOICEMAIL_VALUES = [
7904
+ "answering-machine",
7905
+ "fax",
7906
+ "machine",
7907
+ "machine-end-beep",
7908
+ "machine-end-other",
7909
+ "machine-start",
7910
+ "voicemail"
7911
+ ];
7912
+ var DEFAULT_NO_ANSWER_SIP_CODES = [408, 480, 486, 487, 603];
7913
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
7914
+
7915
+ class VoiceTelephonyWebhookVerificationError extends Error {
7916
+ result;
7917
+ constructor(result) {
7918
+ super(result.ok ? "telephony webhook verified" : result.reason);
7919
+ this.name = "VoiceTelephonyWebhookVerificationError";
7920
+ this.result = result;
7921
+ }
7922
+ }
7923
+ var createMemoryVoiceTelephonyWebhookIdempotencyStore = () => {
7924
+ const decisions = new Map;
7925
+ return {
7926
+ get: (key) => decisions.get(key),
7927
+ set: (key, decision) => {
7928
+ decisions.set(key, decision);
7929
+ }
7930
+ };
7931
+ };
7932
+ var normalizeToken = (value) => typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_+/g, "-") : undefined;
7933
+ var firstString = (source, keys) => {
7934
+ for (const key of keys) {
7935
+ const value = source[key];
7936
+ if (typeof value === "string" && value.trim()) {
7937
+ return value.trim();
7938
+ }
7939
+ if (typeof value === "number" && Number.isFinite(value)) {
7940
+ return String(value);
7941
+ }
7942
+ }
7943
+ };
7944
+ var firstNumber = (source, keys) => {
7945
+ for (const key of keys) {
7946
+ const value = source[key];
7947
+ if (typeof value === "number" && Number.isFinite(value)) {
7948
+ return value;
7949
+ }
7950
+ if (typeof value === "string" && value.trim()) {
7951
+ const parsed = Number(value);
7952
+ if (Number.isFinite(parsed)) {
7953
+ return parsed;
7954
+ }
7955
+ }
7956
+ }
7957
+ };
7958
+ var parseMaybeJSON = (value) => {
7959
+ try {
7960
+ return JSON.parse(value);
7961
+ } catch {
7962
+ return;
7963
+ }
7964
+ };
7965
+ var flattenPayload = (value) => {
7966
+ if (!isRecord(value)) {
7967
+ return {};
7968
+ }
7969
+ const data = isRecord(value.data) ? value.data : undefined;
7970
+ const payload = isRecord(value.payload) ? value.payload : undefined;
7971
+ const event = isRecord(value.event) ? value.event : undefined;
7972
+ return {
7973
+ ...value,
7974
+ ...payload,
7975
+ ...event,
7976
+ ...data,
7977
+ ...isRecord(data?.payload) ? data.payload : undefined
7978
+ };
7979
+ };
7980
+ var toBase64 = (bytes) => Buffer.from(new Uint8Array(bytes)).toString("base64");
7981
+ var timingSafeEqual = (left, right) => {
7982
+ const encoder = new TextEncoder;
7983
+ const leftBytes = encoder.encode(left);
7984
+ const rightBytes = encoder.encode(right);
7985
+ if (leftBytes.length !== rightBytes.length) {
7986
+ return false;
7987
+ }
7988
+ let diff = 0;
7989
+ for (let index = 0;index < leftBytes.length; index += 1) {
7990
+ diff |= leftBytes[index] ^ rightBytes[index];
7991
+ }
7992
+ return diff === 0;
7993
+ };
7994
+ var signHmacSHA1Base64 = async (secret, payload) => {
7995
+ const encoder = new TextEncoder;
7996
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
7997
+ hash: "SHA-1",
7998
+ name: "HMAC"
7999
+ }, false, ["sign"]);
8000
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
8001
+ return toBase64(signature);
8002
+ };
8003
+ var sortedParamsForSignature = (body) => Object.entries(flattenPayload(body)).filter(([, value]) => value !== undefined && value !== null).sort(([left], [right]) => left.localeCompare(right)).map(([key, value]) => `${key}${String(value)}`).join("");
8004
+ var normalizeList = (values, fallback) => new Set((values ?? fallback).map(normalizeToken).filter(Boolean));
8005
+ var metadataValue = (metadata, keys) => {
8006
+ for (const key of keys) {
8007
+ const value = metadata?.[key];
8008
+ if (typeof value === "string" && value.trim()) {
8009
+ return value.trim();
8010
+ }
8011
+ }
8012
+ };
8013
+ var resolveTransferTarget = (event, policy) => {
8014
+ if (typeof event.target === "string" && event.target.trim()) {
8015
+ return event.target.trim();
8016
+ }
8017
+ const metadataTarget = metadataValue(event.metadata, [
8018
+ "transferTarget",
8019
+ "target",
8020
+ "queue",
8021
+ "department"
8022
+ ]);
8023
+ if (metadataTarget) {
8024
+ return metadataTarget;
8025
+ }
8026
+ if (typeof policy.transferTarget === "function") {
8027
+ const target = policy.transferTarget(event);
8028
+ return typeof target === "string" && target.trim() ? target.trim() : undefined;
8029
+ }
8030
+ return typeof policy.transferTarget === "string" && policy.transferTarget.trim() ? policy.transferTarget.trim() : undefined;
8031
+ };
8032
+ var mergeMetadata = (event, policy) => ({
8033
+ ...policy.includeProviderPayload ? {
8034
+ answeredBy: event.answeredBy,
8035
+ durationMs: event.durationMs,
8036
+ provider: event.provider,
8037
+ reason: event.reason,
8038
+ sipCode: event.sipCode,
8039
+ status: event.status
8040
+ } : undefined,
8041
+ ...policy.metadata,
8042
+ ...event.metadata
8043
+ });
8044
+ var withDecisionDefaults = (decision, input) => {
8045
+ if (typeof decision === "string") {
8046
+ return buildDecision(decision, input);
8047
+ }
8048
+ return {
8049
+ ...buildDecision(decision.action, input),
8050
+ ...decision,
8051
+ confidence: decision.confidence ?? "high",
8052
+ metadata: {
8053
+ ...mergeMetadata(input.event, input.policy),
8054
+ ...decision.metadata
8055
+ },
8056
+ source: decision.source ?? input.source,
8057
+ target: decision.target ?? (decision.action === "transfer" ? resolveTransferTarget(input.event, input.policy) : undefined)
8058
+ };
8059
+ };
8060
+ var dispositionForAction = (action) => {
8061
+ switch (action) {
8062
+ case "complete":
8063
+ return "completed";
8064
+ case "escalate":
8065
+ return "escalated";
8066
+ case "no-answer":
8067
+ return "no-answer";
8068
+ case "transfer":
8069
+ return "transferred";
8070
+ case "voicemail":
8071
+ return "voicemail";
8072
+ default:
8073
+ return;
8074
+ }
8075
+ };
8076
+ var buildDecision = (action, input) => ({
8077
+ action,
8078
+ confidence: action === "ignore" ? "low" : "high",
8079
+ disposition: dispositionForAction(action),
8080
+ metadata: mergeMetadata(input.event, input.policy),
8081
+ reason: input.event.reason,
8082
+ source: input.source,
8083
+ target: action === "transfer" ? resolveTransferTarget(input.event, input.policy) : undefined
8084
+ });
8085
+ var createVoiceTelephonyOutcomePolicy = (policy = {}) => ({
8086
+ completedStatuses: policy.completedStatuses ?? DEFAULT_COMPLETED_STATUSES,
8087
+ escalationStatuses: policy.escalationStatuses ?? DEFAULT_ESCALATION_STATUSES,
8088
+ failedAsNoAnswer: policy.failedAsNoAnswer ?? true,
8089
+ failedStatuses: policy.failedStatuses ?? DEFAULT_FAILED_STATUSES,
8090
+ includeProviderPayload: policy.includeProviderPayload ?? true,
8091
+ machineDetectionVoicemailValues: policy.machineDetectionVoicemailValues ?? DEFAULT_MACHINE_VOICEMAIL_VALUES,
8092
+ metadata: policy.metadata,
8093
+ minAnsweredDurationMs: policy.minAnsweredDurationMs,
8094
+ noAnswerOnZeroDuration: policy.noAnswerOnZeroDuration ?? true,
8095
+ noAnswerSipCodes: policy.noAnswerSipCodes ?? DEFAULT_NO_ANSWER_SIP_CODES,
8096
+ noAnswerStatuses: policy.noAnswerStatuses ?? DEFAULT_NO_ANSWER_STATUSES,
8097
+ statusMap: policy.statusMap,
8098
+ transferStatuses: policy.transferStatuses ?? DEFAULT_TRANSFER_STATUSES,
8099
+ transferTarget: policy.transferTarget,
8100
+ voicemailStatuses: policy.voicemailStatuses ?? DEFAULT_VOICEMAIL_STATUSES
8101
+ });
8102
+ var resolveVoiceTelephonyOutcome = (event, policyInput = {}) => {
8103
+ const policy = createVoiceTelephonyOutcomePolicy(policyInput);
8104
+ const status = normalizeToken(event.status);
8105
+ const provider = normalizeToken(event.provider);
8106
+ const answeredBy = normalizeToken(event.answeredBy);
8107
+ const target = resolveTransferTarget(event, policy);
8108
+ if (status) {
8109
+ const mapped = policy.statusMap?.[status] ?? (provider ? policy.statusMap?.[`${provider}:${status}`] : undefined);
8110
+ if (mapped) {
8111
+ return withDecisionDefaults(mapped, {
8112
+ event,
8113
+ policy,
8114
+ source: "policy"
8115
+ });
8116
+ }
8117
+ }
8118
+ if (answeredBy && normalizeList(policy.machineDetectionVoicemailValues, []).has(answeredBy)) {
8119
+ return buildDecision("voicemail", { event, policy, source: "answered-by" });
8120
+ }
8121
+ if (typeof event.sipCode === "number" && policy.noAnswerSipCodes.includes(event.sipCode)) {
8122
+ return buildDecision("no-answer", { event, policy, source: "sip" });
8123
+ }
8124
+ if (target && status && normalizeList(policy.transferStatuses, []).has(status)) {
8125
+ return buildDecision("transfer", { event, policy, source: "status" });
8126
+ }
8127
+ if (status && normalizeList(policy.voicemailStatuses, []).has(status)) {
8128
+ return buildDecision("voicemail", { event, policy, source: "status" });
8129
+ }
8130
+ if (status && normalizeList(policy.escalationStatuses, []).has(status)) {
8131
+ return buildDecision("escalate", { event, policy, source: "status" });
8132
+ }
8133
+ if (status && (policy.failedAsNoAnswer ? normalizeList(policy.noAnswerStatuses, []).has(status) || normalizeList(policy.failedStatuses, []).has(status) : normalizeList(policy.noAnswerStatuses, []).has(status))) {
8134
+ return buildDecision("no-answer", { event, policy, source: "status" });
8135
+ }
8136
+ if (policy.noAnswerOnZeroDuration && typeof event.durationMs === "number" && event.durationMs <= 0) {
8137
+ return buildDecision("no-answer", { event, policy, source: "duration" });
8138
+ }
8139
+ if (typeof policy.minAnsweredDurationMs === "number" && typeof event.durationMs === "number" && event.durationMs < policy.minAnsweredDurationMs) {
8140
+ return {
8141
+ ...buildDecision("no-answer", { event, policy, source: "duration" }),
8142
+ confidence: "medium"
8143
+ };
8144
+ }
8145
+ if (status && normalizeList(policy.completedStatuses, []).has(status)) {
8146
+ return buildDecision("complete", { event, policy, source: "status" });
8147
+ }
8148
+ if (target) {
8149
+ return {
8150
+ ...buildDecision("transfer", { event, policy, source: "explicit-target" }),
8151
+ confidence: "medium"
8152
+ };
8153
+ }
8154
+ return buildDecision("ignore", { event, policy, source: "status" });
8155
+ };
8156
+ var voiceTelephonyOutcomeToRouteResult = (decision, result) => {
8157
+ switch (decision.action) {
8158
+ case "complete":
8159
+ return { complete: true, result };
8160
+ case "escalate":
8161
+ return {
8162
+ escalate: {
8163
+ metadata: decision.metadata,
8164
+ reason: decision.reason ?? "telephony-escalation"
8165
+ },
8166
+ result
8167
+ };
8168
+ case "no-answer":
8169
+ return {
8170
+ noAnswer: {
8171
+ metadata: decision.metadata
8172
+ },
8173
+ result
8174
+ };
8175
+ case "transfer":
8176
+ if (!decision.target) {
8177
+ return { result };
8178
+ }
8179
+ return {
8180
+ result,
8181
+ transfer: {
8182
+ metadata: decision.metadata,
8183
+ reason: decision.reason,
8184
+ target: decision.target
8185
+ }
8186
+ };
8187
+ case "voicemail":
8188
+ return {
8189
+ result,
8190
+ voicemail: {
8191
+ metadata: decision.metadata
8192
+ }
8193
+ };
8194
+ default:
8195
+ return { result };
8196
+ }
8197
+ };
8198
+ var applyVoiceTelephonyOutcome = async (api, decision, result) => {
8199
+ switch (decision.action) {
8200
+ case "complete":
8201
+ await api.complete(result);
8202
+ break;
8203
+ case "escalate":
8204
+ await api.escalate({
8205
+ metadata: decision.metadata,
8206
+ reason: decision.reason ?? "telephony-escalation",
8207
+ result
8208
+ });
8209
+ break;
8210
+ case "no-answer":
8211
+ await api.markNoAnswer({
8212
+ metadata: decision.metadata,
8213
+ result
8214
+ });
8215
+ break;
8216
+ case "transfer":
8217
+ if (!decision.target) {
8218
+ return;
8219
+ }
8220
+ await api.transfer({
8221
+ metadata: decision.metadata,
8222
+ reason: decision.reason,
8223
+ result,
8224
+ target: decision.target
8225
+ });
8226
+ break;
8227
+ case "voicemail":
8228
+ await api.markVoicemail({
8229
+ metadata: decision.metadata,
8230
+ result
8231
+ });
8232
+ break;
8233
+ default:
8234
+ break;
8235
+ }
8236
+ };
8237
+ var parseRequestBodyText = (input) => {
8238
+ const { contentType, text } = input;
8239
+ if (!text) {
8240
+ return {};
8241
+ }
8242
+ if (contentType.includes("application/json")) {
8243
+ return parseMaybeJSON(text) ?? {};
8244
+ }
8245
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
8246
+ return Object.fromEntries(new URLSearchParams(text));
8247
+ }
8248
+ return parseMaybeJSON(text) ?? Object.fromEntries(new URLSearchParams(text));
8249
+ };
8250
+ var readRequestBody = async (request) => {
8251
+ const contentType = request.headers.get("content-type") ?? "";
8252
+ const text = await request.text();
8253
+ return {
8254
+ body: parseRequestBodyText({ contentType, text }),
8255
+ rawBody: text
8256
+ };
8257
+ };
8258
+ var signVoiceTwilioWebhook = async (input) => signHmacSHA1Base64(input.authToken, `${input.url}${sortedParamsForSignature(input.body ?? {})}`);
8259
+ var verifyVoiceTwilioWebhookSignature = async (input) => {
8260
+ if (!input.authToken) {
8261
+ return { ok: false, reason: "missing-secret" };
8262
+ }
8263
+ const signature = input.headers.get("x-twilio-signature");
8264
+ if (!signature) {
8265
+ return { ok: false, reason: "missing-signature" };
8266
+ }
8267
+ const expected = await signVoiceTwilioWebhook({
8268
+ authToken: input.authToken,
8269
+ body: input.body,
8270
+ url: input.url
8271
+ });
8272
+ return timingSafeEqual(signature, expected) ? { ok: true } : { ok: false, reason: "invalid-signature" };
8273
+ };
8274
+ var resolveVerificationUrl = (option, input) => typeof option === "function" ? option(input) : option ?? input.request.url;
8275
+ var verifyVoiceTelephonyWebhook = async (input) => {
8276
+ if (input.options.verify) {
8277
+ return input.options.verify({
8278
+ body: input.body,
8279
+ headers: input.request.headers,
8280
+ provider: input.provider,
8281
+ query: input.query,
8282
+ rawBody: input.rawBody,
8283
+ request: input.request
8284
+ });
8285
+ }
8286
+ if (!input.options.signingSecret) {
8287
+ return input.options.requireVerification ? { ok: false, reason: "missing-secret" } : { ok: true };
8288
+ }
8289
+ if (input.provider !== "twilio") {
8290
+ return { ok: false, reason: "unsupported-provider" };
8291
+ }
8292
+ return verifyVoiceTwilioWebhookSignature({
8293
+ authToken: input.options.signingSecret,
8294
+ body: input.body,
8295
+ headers: input.request.headers,
8296
+ url: resolveVerificationUrl(input.options.verificationUrl, {
8297
+ query: input.query,
8298
+ request: input.request
8299
+ })
8300
+ });
8301
+ };
8302
+ var durationMsFromSeconds = (value) => typeof value === "number" ? value * 1000 : undefined;
8303
+ var parseVoiceTelephonyWebhookEvent = (input) => {
8304
+ const payload = flattenPayload(input.body);
8305
+ const provider = firstString(payload, ["provider", "Provider"]) ?? input.provider;
8306
+ const status = firstString(payload, [
8307
+ "CallStatus",
8308
+ "call_status",
8309
+ "callStatus",
8310
+ "DialCallStatus",
8311
+ "dial_call_status",
8312
+ "status",
8313
+ "event_type",
8314
+ "type"
8315
+ ]);
8316
+ const durationMs = firstNumber(payload, ["durationMs", "duration_ms"]) ?? durationMsFromSeconds(firstNumber(payload, [
8317
+ "CallDuration",
8318
+ "call_duration",
8319
+ "callDuration",
8320
+ "DialCallDuration",
8321
+ "dial_call_duration",
8322
+ "duration"
8323
+ ]));
8324
+ const sipCode = firstNumber(payload, [
8325
+ "SipResponseCode",
8326
+ "sip_response_code",
8327
+ "sipCode",
8328
+ "sip_code",
8329
+ "hangupCauseCode"
8330
+ ]);
8331
+ const from = firstString(payload, ["From", "from", "caller_id", "callerId"]);
8332
+ const to = firstString(payload, ["To", "to", "called_number", "calledNumber"]);
8333
+ const target = firstString(payload, [
8334
+ "transferTarget",
8335
+ "TransferTarget",
8336
+ "target",
8337
+ "queue",
8338
+ "department"
8339
+ ]);
8340
+ return {
8341
+ answeredBy: firstString(payload, [
8342
+ "AnsweredBy",
8343
+ "answered_by",
8344
+ "answeredBy",
8345
+ "machineDetection",
8346
+ "machine_detection"
8347
+ ]),
8348
+ durationMs,
8349
+ from,
8350
+ metadata: payload,
8351
+ provider,
8352
+ reason: firstString(payload, [
8353
+ "Reason",
8354
+ "reason",
8355
+ "HangupCause",
8356
+ "hangup_cause",
8357
+ "hangupCause"
8358
+ ]),
8359
+ sipCode,
8360
+ status,
8361
+ target,
8362
+ to
8363
+ };
8364
+ };
8365
+ var defaultSessionId = (input) => {
8366
+ const payload = flattenPayload(input.body);
8367
+ const metadataSessionId = input.event.metadata?.sessionId;
8368
+ return firstString(input.query, ["sessionId", "session_id"]) ?? firstString(payload, [
8369
+ "sessionId",
8370
+ "session_id",
8371
+ "SessionId",
8372
+ "CallSid",
8373
+ "call_sid",
8374
+ "callSid",
8375
+ "CallUUID",
8376
+ "call_uuid",
8377
+ "callControlId",
8378
+ "call_control_id"
8379
+ ]) ?? (typeof metadataSessionId === "string" ? metadataSessionId : undefined);
8380
+ };
8381
+ var defaultIdempotencyKey = (input) => {
8382
+ const payload = flattenPayload(input.body);
8383
+ const eventId = firstString(payload, [
8384
+ "id",
8385
+ "event_id",
8386
+ "eventId",
8387
+ "EventSid",
8388
+ "event_sid",
8389
+ "MessageSid",
8390
+ "message_sid",
8391
+ "CallSid",
8392
+ "call_sid",
8393
+ "CallUUID",
8394
+ "call_uuid",
8395
+ "callControlId",
8396
+ "call_control_id"
8397
+ ]);
8398
+ const status = normalizeToken(input.event.status) ?? "unknown";
8399
+ if (eventId) {
8400
+ return `${input.provider}:${eventId}:${status}`;
8401
+ }
8402
+ if (input.sessionId) {
8403
+ return `${input.provider}:${input.sessionId}:${status}`;
8404
+ }
8405
+ };
8406
+ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
8407
+ const provider = options.provider ?? "generic";
8408
+ const query = input.query ?? {};
8409
+ const { body, rawBody } = await readRequestBody(input.request);
8410
+ const verification = await verifyVoiceTelephonyWebhook({
8411
+ body,
8412
+ options,
8413
+ provider,
8414
+ query,
8415
+ rawBody,
8416
+ request: input.request
8417
+ });
8418
+ if (!verification.ok) {
8419
+ throw new VoiceTelephonyWebhookVerificationError(verification);
8420
+ }
8421
+ const event = options.parse ? await options.parse({
8422
+ body,
8423
+ headers: input.request.headers,
8424
+ provider,
8425
+ query,
8426
+ request: input.request
8427
+ }) : parseVoiceTelephonyWebhookEvent({
8428
+ body,
8429
+ headers: input.request.headers,
8430
+ provider,
8431
+ query,
8432
+ request: input.request
8433
+ });
8434
+ const sessionId = await (options.resolveSessionId?.({
8435
+ body,
8436
+ event,
8437
+ query,
8438
+ request: input.request
8439
+ }) ?? defaultSessionId({ body, event, query }));
8440
+ const idempotencyEnabled = options.idempotency?.enabled !== false;
8441
+ const idempotencyKey = idempotencyEnabled ? await (options.idempotency?.key?.({
8442
+ body,
8443
+ event,
8444
+ provider,
8445
+ query,
8446
+ request: input.request,
8447
+ sessionId
8448
+ }) ?? defaultIdempotencyKey({ body, event, provider, sessionId })) : undefined;
8449
+ const idempotencyStore = options.idempotency?.store;
8450
+ if (idempotencyKey && idempotencyStore) {
8451
+ const existing = await idempotencyStore.get(idempotencyKey);
8452
+ if (existing) {
8453
+ const duplicateDecision = {
8454
+ ...existing,
8455
+ duplicate: true
8456
+ };
8457
+ await options.onDecision?.({
8458
+ ...duplicateDecision,
8459
+ context: options.context,
8460
+ request: input.request
8461
+ });
8462
+ return duplicateDecision;
8463
+ }
8464
+ }
8465
+ const decision = resolveVoiceTelephonyOutcome(event, options.policy);
8466
+ const resultResolver = options.result;
8467
+ const result = typeof resultResolver === "function" ? await resultResolver({
8468
+ decision,
8469
+ event,
8470
+ sessionId
8471
+ }) : resultResolver;
8472
+ const routeResult = voiceTelephonyOutcomeToRouteResult(decision, result);
8473
+ const shouldApply = typeof options.apply === "function" ? options.apply({
8474
+ applied: false,
8475
+ decision,
8476
+ event,
8477
+ routeResult,
8478
+ sessionId
8479
+ }) : options.apply === true;
8480
+ let applied = false;
8481
+ if (shouldApply && decision.action !== "ignore" && options.getSessionHandle) {
8482
+ const api = await options.getSessionHandle({
8483
+ context: options.context,
8484
+ decision,
8485
+ event,
8486
+ request: input.request,
8487
+ sessionId
8488
+ });
8489
+ if (api) {
8490
+ await applyVoiceTelephonyOutcome(api, decision, result);
8491
+ applied = true;
8492
+ }
8493
+ }
8494
+ const webhookDecision = {
8495
+ applied,
8496
+ decision,
8497
+ event,
8498
+ idempotencyKey,
8499
+ routeResult,
8500
+ sessionId
8501
+ };
8502
+ if (idempotencyKey && idempotencyStore) {
8503
+ const now = Date.now();
8504
+ await idempotencyStore.set(idempotencyKey, {
8505
+ ...webhookDecision,
8506
+ createdAt: now,
8507
+ updatedAt: now
8508
+ });
8509
+ }
8510
+ await options.onDecision?.({
8511
+ ...webhookDecision,
8512
+ context: options.context,
8513
+ request: input.request
8514
+ });
8515
+ return webhookDecision;
8516
+ };
8517
+ var createVoiceTelephonyWebhookRoutes = (options = {}) => {
8518
+ const path = options.path ?? "/api/voice/telephony/webhook";
8519
+ const handler = createVoiceTelephonyWebhookHandler(options);
8520
+ return new Elysia({
8521
+ name: options.name ?? "absolutejs-voice-telephony-webhooks"
8522
+ }).post(path, async ({ query, request }) => {
8523
+ try {
8524
+ return await handler({ query, request });
8525
+ } catch (error) {
8526
+ if (error instanceof VoiceTelephonyWebhookVerificationError) {
8527
+ return new Response(JSON.stringify({ verification: error.result }), {
8528
+ headers: {
8529
+ "content-type": "application/json"
8530
+ },
8531
+ status: 401
8532
+ });
8533
+ }
8534
+ throw error;
8535
+ }
8536
+ });
8537
+ };
8538
+
8539
+ // src/telephony/twilio.ts
7870
8540
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
7871
8541
  var VOICE_PCM_SAMPLE_RATE = 16000;
7872
8542
  var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
8543
+ var resolveRequestOrigin = (request) => {
8544
+ const url = new URL(request.url);
8545
+ const forwardedHost = request.headers.get("x-forwarded-host");
8546
+ const forwardedProto = request.headers.get("x-forwarded-proto");
8547
+ const host = forwardedHost ?? request.headers.get("host") ?? url.host;
8548
+ const protocol = forwardedProto ?? url.protocol.replace(":", "");
8549
+ return `${protocol}://${host}`;
8550
+ };
8551
+ var resolveTwilioStreamUrl = async (options, input) => {
8552
+ if (typeof options.twiml?.streamUrl === "function") {
8553
+ return options.twiml.streamUrl(input);
8554
+ }
8555
+ if (typeof options.twiml?.streamUrl === "string") {
8556
+ return options.twiml.streamUrl;
8557
+ }
8558
+ const origin = resolveRequestOrigin(input.request);
8559
+ const wsOrigin = origin.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
8560
+ return `${wsOrigin}${input.streamPath}`;
8561
+ };
8562
+ var resolveTwilioStreamParameters = async (parameters, input) => {
8563
+ if (typeof parameters === "function") {
8564
+ return parameters(input);
8565
+ }
8566
+ return parameters;
8567
+ };
7873
8568
  var normalizeOnTurn = (handler) => {
7874
8569
  if (handler.length > 1) {
7875
8570
  const directHandler = handler;
@@ -7971,7 +8666,7 @@ var bytesToInt16Array = (bytes) => {
7971
8666
  return output;
7972
8667
  };
7973
8668
  var decodeTwilioMulawBase64 = (payload) => {
7974
- const bytes = Uint8Array.from(Buffer2.from(payload, "base64"));
8669
+ const bytes = Uint8Array.from(Buffer3.from(payload, "base64"));
7975
8670
  const samples = new Int16Array(bytes.length);
7976
8671
  for (let index = 0;index < bytes.length; index += 1) {
7977
8672
  samples[index] = decodeMulawSample(bytes[index] ?? 0);
@@ -7983,7 +8678,7 @@ var encodeTwilioMulawBase64 = (samples) => {
7983
8678
  for (let index = 0;index < samples.length; index += 1) {
7984
8679
  bytes[index] = encodeMulawSample(samples[index] ?? 0);
7985
8680
  }
7986
- return Buffer2.from(bytes).toString("base64");
8681
+ return Buffer3.from(bytes).toString("base64");
7987
8682
  };
7988
8683
  var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
7989
8684
  const narrowband = decodeTwilioMulawBase64(payload);
@@ -7992,7 +8687,7 @@ var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
7992
8687
  };
7993
8688
  var transcodePCMToTwilioOutboundPayload = (chunk, format) => {
7994
8689
  if (format.container === "raw" && format.encoding === "mulaw" && format.channels === 1 && format.sampleRateHz === TWILIO_MULAW_SAMPLE_RATE) {
7995
- return Buffer2.from(chunk).toString("base64");
8690
+ return Buffer3.from(chunk).toString("base64");
7996
8691
  }
7997
8692
  if (format.encoding !== "pcm_s16le") {
7998
8693
  throw new Error(`Unsupported outbound telephony audio format: ${format.container}/${format.encoding}`);
@@ -8033,7 +8728,7 @@ var createTwilioSocketAdapter = (socket, getState) => ({
8033
8728
  return;
8034
8729
  }
8035
8730
  if (message.type === "audio") {
8036
- const payload = transcodePCMToTwilioOutboundPayload(Uint8Array.from(Buffer2.from(message.chunkBase64, "base64")), message.format);
8731
+ const payload = transcodePCMToTwilioOutboundPayload(Uint8Array.from(Buffer3.from(message.chunkBase64, "base64")), message.format);
8037
8732
  state.hasOutboundAudioSinceLastInbound = true;
8038
8733
  state.reviewRecorder?.recordTwilioOutbound({
8039
8734
  bytes: payload.length,
@@ -8246,6 +8941,83 @@ var createTwilioMediaStreamBridge = (socket, options) => {
8246
8941
  }
8247
8942
  };
8248
8943
  };
8944
+ var createTwilioVoiceRoutes = (options) => {
8945
+ const streamPath = options.streamPath ?? "/api/voice/twilio/stream";
8946
+ const twimlPath = options.twiml?.path ?? "/api/voice/twilio";
8947
+ const webhookPath = options.webhook?.path ?? "/api/voice/twilio/webhook";
8948
+ const bridges = new WeakMap;
8949
+ const webhookPolicy = options.webhook?.policy ?? options.outcomePolicy ?? createVoiceTelephonyOutcomePolicy();
8950
+ return new Elysia2({
8951
+ name: options.name ?? "absolutejs-voice-twilio"
8952
+ }).get(twimlPath, async ({ query, request }) => {
8953
+ const streamUrl = await resolveTwilioStreamUrl(options, {
8954
+ query,
8955
+ request,
8956
+ streamPath
8957
+ });
8958
+ const parameters = await resolveTwilioStreamParameters(options.twiml?.parameters, {
8959
+ query,
8960
+ request
8961
+ });
8962
+ return new Response(createTwilioVoiceResponse({
8963
+ parameters,
8964
+ streamName: options.twiml?.streamName,
8965
+ streamUrl,
8966
+ track: options.twiml?.track
8967
+ }), {
8968
+ headers: {
8969
+ "content-type": "text/xml; charset=utf-8"
8970
+ }
8971
+ });
8972
+ }).post(twimlPath, async ({ query, request }) => {
8973
+ const streamUrl = await resolveTwilioStreamUrl(options, {
8974
+ query,
8975
+ request,
8976
+ streamPath
8977
+ });
8978
+ const parameters = await resolveTwilioStreamParameters(options.twiml?.parameters, {
8979
+ query,
8980
+ request
8981
+ });
8982
+ return new Response(createTwilioVoiceResponse({
8983
+ parameters,
8984
+ streamName: options.twiml?.streamName,
8985
+ streamUrl,
8986
+ track: options.twiml?.track
8987
+ }), {
8988
+ headers: {
8989
+ "content-type": "text/xml; charset=utf-8"
8990
+ }
8991
+ });
8992
+ }).ws(streamPath, {
8993
+ close: async (ws, _code, reason) => {
8994
+ const bridge = bridges.get(ws);
8995
+ bridges.delete(ws);
8996
+ await bridge?.close(reason);
8997
+ },
8998
+ message: async (ws, raw) => {
8999
+ let bridge = bridges.get(ws);
9000
+ if (!bridge) {
9001
+ bridge = createTwilioMediaStreamBridge({
9002
+ close: (code, reason) => {
9003
+ ws.close(code, reason);
9004
+ },
9005
+ send: (data) => {
9006
+ ws.send(data);
9007
+ }
9008
+ }, options);
9009
+ bridges.set(ws, bridge);
9010
+ }
9011
+ await bridge.handleMessage(raw);
9012
+ }
9013
+ }).use(createVoiceTelephonyWebhookRoutes({
9014
+ ...options.webhook ?? {},
9015
+ context: options.context,
9016
+ path: webhookPath,
9017
+ policy: webhookPolicy,
9018
+ provider: "twilio"
9019
+ }));
9020
+ };
8249
9021
 
8250
9022
  // src/testing/telephony.ts
8251
9023
  var DEFAULT_PCM16_FORMAT = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.73",
3
+ "version": "0.0.22-beta.74",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",