@absolutejs/voice 0.0.22-beta.72 → 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
@@ -22,15 +22,15 @@ export { createVoiceQualityRoutes, evaluateVoiceQuality, renderVoiceQualityHTML
22
22
  export { createVoiceResilienceRoutes, createVoiceRoutingDecisionSummary, listVoiceRoutingEvents, renderVoiceResilienceHTML, summarizeVoiceRoutingDecision } from './resilienceRoutes';
23
23
  export { createVoiceSTTProviderRouter, createVoiceTTSProviderRouter } from './providerAdapters';
24
24
  export { buildVoiceTraceReplay, createVoiceMemoryTraceSinkDeliveryStore, createVoiceTraceHTTPSink, createVoiceMemoryTraceEventStore, createVoiceTraceSinkDeliveryId, createVoiceTraceSinkDeliveryRecord, createVoiceTraceSinkStore, createVoiceTraceEvent, createVoiceTraceEventId, deliverVoiceTraceEventsToSinks, evaluateVoiceTrace, exportVoiceTrace, filterVoiceTraceEvents, pruneVoiceTraceEvents, redactVoiceTraceEvent, redactVoiceTraceEvents, redactVoiceTraceText, renderVoiceTraceHTML, renderVoiceTraceMarkdown, resolveVoiceTraceRedactionOptions, selectVoiceTraceEventsForPrune, summarizeVoiceTrace } from './trace';
25
- export { createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore } from './sqliteStore';
26
- export { createVoicePostgresExternalObjectMapStore, createVoicePostgresIntegrationEventStore, createVoicePostgresReviewStore, createVoicePostgresRuntimeStorage, createVoicePostgresSessionStore, createVoicePostgresTaskStore, createVoicePostgresTraceSinkDeliveryStore, createVoicePostgresTraceEventStore } from './postgresStore';
25
+ export { createVoiceSQLiteExternalObjectMapStore, createVoiceSQLiteIntegrationEventStore, createVoiceSQLiteReviewStore, createVoiceSQLiteRuntimeStorage, createVoiceSQLiteSessionStore, createVoiceSQLiteTaskStore, createVoiceSQLiteTelephonyWebhookIdempotencyStore, createVoiceSQLiteTraceSinkDeliveryStore, createVoiceSQLiteTraceEventStore } from './sqliteStore';
26
+ export { createVoicePostgresExternalObjectMapStore, createVoicePostgresIntegrationEventStore, createVoicePostgresReviewStore, createVoicePostgresRuntimeStorage, createVoicePostgresSessionStore, createVoicePostgresTaskStore, createVoicePostgresTelephonyWebhookIdempotencyStore, createVoicePostgresTraceSinkDeliveryStore, createVoicePostgresTraceEventStore } from './postgresStore';
27
27
  export { createVoiceS3ReviewStore } from './s3Store';
28
28
  export { createVoiceMemoryStore } from './memoryStore';
29
29
  export { createVoiceCRMActivitySink, createVoiceHelpdeskTicketSink, createVoiceIntegrationHTTPSink, createVoiceHubSpotTaskSink, createVoiceHubSpotTaskSyncSinks, createVoiceHubSpotTaskUpdateSink, createVoiceLinearIssueSink, createVoiceLinearIssueSyncSinks, createVoiceLinearIssueUpdateSink, createVoiceZendeskTicketSink, createVoiceZendeskTicketSyncSinks, createVoiceZendeskTicketUpdateSink, deliverVoiceIntegrationEventToSinks } from './opsSinks';
30
30
  export { createVoiceOpsWebhookEnvelope, createVoiceOpsWebhookReceiverRoutes, createVoiceOpsWebhookSink, verifyVoiceOpsWebhookSignature } from './opsWebhook';
31
31
  export { applyVoiceHandoffDeliveryResult, createVoiceHandoffDeliveryRecord, createVoiceMemoryHandoffDeliveryStore, createVoiceTwilioRedirectHandoffAdapter, createVoiceWebhookHandoffAdapter, deliverVoiceHandoff, deliverVoiceHandoffDelivery } from './handoff';
32
32
  export { createVoiceHandoffHealthHTMLHandler, createVoiceHandoffHealthJSONHandler, createVoiceHandoffHealthRoutes, renderVoiceHandoffHealthHTML, summarizeVoiceHandoffHealth } from './handoffHealth';
33
- export { createVoiceHandoffDeliveryWorker, createVoiceHandoffDeliveryWorkerLoop, createVoiceIntegrationSinkWorker, createVoiceIntegrationSinkWorkerLoop, createVoiceOpsTaskWorker, createVoiceOpsTaskProcessorWorker, createVoiceOpsTaskProcessorWorkerLoop, createVoiceRedisIdempotencyStore, createVoiceRedisTaskLeaseCoordinator, createVoiceTraceSinkDeliveryWorker, createVoiceTraceSinkDeliveryWorkerLoop, createVoiceWebhookDeliveryWorker, createVoiceWebhookDeliveryWorkerLoop, summarizeVoiceHandoffDeliveries, summarizeVoiceTraceSinkDeliveries, summarizeVoiceOpsTaskQueue, summarizeVoiceIntegrationEvents } from './queue';
33
+ export { createVoiceHandoffDeliveryWorker, createVoiceHandoffDeliveryWorkerLoop, createVoiceIntegrationSinkWorker, createVoiceIntegrationSinkWorkerLoop, createVoiceOpsTaskWorker, createVoiceOpsTaskProcessorWorker, createVoiceOpsTaskProcessorWorkerLoop, createVoiceRedisIdempotencyStore, createVoiceRedisTelephonyWebhookIdempotencyStore, createVoiceRedisTaskLeaseCoordinator, createVoiceTraceSinkDeliveryWorker, createVoiceTraceSinkDeliveryWorkerLoop, createVoiceWebhookDeliveryWorker, createVoiceWebhookDeliveryWorkerLoop, summarizeVoiceHandoffDeliveries, summarizeVoiceTraceSinkDeliveries, summarizeVoiceOpsTaskQueue, summarizeVoiceIntegrationEvents } from './queue';
34
34
  export { assignVoiceOpsTask, applyVoiceOpsTaskAssignmentRule, applyVoiceOpsTaskPolicy, buildVoiceOpsTaskFromReview, buildVoiceOpsTaskFromSLABreach, claimVoiceOpsTask, completeVoiceOpsTask, createVoiceExternalObjectMap, createVoiceExternalObjectMapId, createVoiceCallCompletedEvent, createVoiceTaskSLABreachedEvent, deadLetterVoiceOpsTask, deliverVoiceIntegrationEvent, failVoiceOpsTask, hasVoiceOpsTaskSLABreach, heartbeatVoiceOpsTask, isVoiceOpsTaskOverdue, markVoiceOpsTaskSLABreached, matchesVoiceOpsTaskAssignmentRule, resolveVoiceOpsTaskAgeBucket, createVoiceIntegrationEvent, createVoiceReviewSavedEvent, resolveVoiceOpsTaskAssignment, resolveVoiceOpsTaskPolicy, requeueVoiceOpsTask, createVoiceTaskCreatedEvent, createVoiceTaskUpdatedEvent, listVoiceOpsTasks, reopenVoiceOpsTask, startVoiceOpsTask, summarizeVoiceOpsTaskAnalytics, summarizeVoiceOpsTasks, withVoiceIntegrationEventId, withVoiceOpsTaskId } from './ops';
35
35
  export { createVoiceSession } from './session';
36
36
  export { createVoiceCallReviewFromSession, recordVoiceRuntimeOps } from './runtimeOps';
@@ -76,11 +76,12 @@ export type { StoredVoiceCallReviewArtifact, VoiceCallReviewArtifact, VoiceCallR
76
76
  export type { VoiceFileRuntimeStorage, VoiceFileStoreOptions } from './fileStore';
77
77
  export type { StoredVoiceTraceEvent, VoiceTraceEvaluation, VoiceTraceEvaluationOptions, VoiceTraceEvent, VoiceTraceEventFilter, VoiceTraceEventStore, VoiceTraceEventType, VoiceTraceIssue, VoiceTraceIssueSeverity, VoiceTraceHTTPSinkOptions, VoiceTracePruneFilter, VoiceTracePruneOptions, VoiceTracePruneResult, VoiceTraceRedactionConfig, VoiceTraceRedactionOptions, VoiceTraceRedactionReplacement, VoiceResolvedTraceRedactionOptions, VoiceTraceSink, VoiceTraceSinkDeliveryQueueStatus, VoiceTraceSinkDeliveryRecord, VoiceTraceSinkDeliveryResult, VoiceTraceSinkDeliveryStatus, VoiceTraceSinkDeliveryStore, VoiceTraceSinkFanoutResult, VoiceTraceSinkStoreOptions, VoiceTraceSummary } from './trace';
78
78
  export type { VoicePostgresClient, VoicePostgresRuntimeStorage, VoicePostgresStoreOptions } from './postgresStore';
79
- export type { VoiceOpsTaskLease, VoiceOpsTaskWorker, VoiceOpsTaskWorkerOptions, VoiceHandoffDeliveryQueueSummary, VoiceHandoffDeliveryWorkerLoop, VoiceHandoffDeliveryWorkerLoopOptions, VoiceHandoffDeliveryWorkerOptions, VoiceHandoffDeliveryWorkerResult, VoiceIdempotencyStore, VoiceIntegrationEventQueueSummary, VoiceIntegrationSinkWorkerLoop, VoiceIntegrationSinkWorkerLoopOptions, VoiceIntegrationSinkWorkerOptions, VoiceIntegrationSinkWorkerResult, VoiceRedisIdempotencyClient, VoiceRedisIdempotencyStoreOptions, VoiceRedisTaskLeaseClient, VoiceRedisTaskLeaseCoordinator, VoiceRedisTaskLeaseCoordinatorOptions, VoiceTraceSinkDeliveryQueueSummary, VoiceTraceSinkDeliveryWorkerLoop, VoiceTraceSinkDeliveryWorkerLoopOptions, VoiceTraceSinkDeliveryWorkerOptions, VoiceTraceSinkDeliveryWorkerResult, VoiceOpsTaskClaimFilters, VoiceWebhookDeliveryWorkerLoop, VoiceWebhookDeliveryWorkerLoopOptions, VoiceWebhookDeliveryWorkerOptions, VoiceWebhookDeliveryWorkerResult, VoiceOpsTaskProcessorWorkerLoop, VoiceOpsTaskProcessorWorkerLoopOptions, VoiceOpsTaskProcessorWorkerOptions, VoiceOpsTaskProcessorWorkerResult, VoiceOpsTaskQueueSummary } from './queue';
79
+ export type { VoiceOpsTaskLease, VoiceOpsTaskWorker, VoiceOpsTaskWorkerOptions, VoiceHandoffDeliveryQueueSummary, VoiceHandoffDeliveryWorkerLoop, VoiceHandoffDeliveryWorkerLoopOptions, VoiceHandoffDeliveryWorkerOptions, VoiceHandoffDeliveryWorkerResult, VoiceIdempotencyStore, VoiceIntegrationEventQueueSummary, VoiceIntegrationSinkWorkerLoop, VoiceIntegrationSinkWorkerLoopOptions, VoiceIntegrationSinkWorkerOptions, VoiceIntegrationSinkWorkerResult, VoiceRedisIdempotencyClient, VoiceRedisIdempotencyStoreOptions, VoiceRedisTelephonyWebhookIdempotencyClient, VoiceRedisTelephonyWebhookIdempotencyStoreOptions, VoiceRedisTaskLeaseClient, VoiceRedisTaskLeaseCoordinator, VoiceRedisTaskLeaseCoordinatorOptions, VoiceTraceSinkDeliveryQueueSummary, VoiceTraceSinkDeliveryWorkerLoop, VoiceTraceSinkDeliveryWorkerLoopOptions, VoiceTraceSinkDeliveryWorkerOptions, VoiceTraceSinkDeliveryWorkerResult, VoiceOpsTaskClaimFilters, VoiceWebhookDeliveryWorkerLoop, VoiceWebhookDeliveryWorkerLoopOptions, VoiceWebhookDeliveryWorkerOptions, VoiceWebhookDeliveryWorkerResult, VoiceOpsTaskProcessorWorkerLoop, VoiceOpsTaskProcessorWorkerLoopOptions, VoiceOpsTaskProcessorWorkerOptions, VoiceOpsTaskProcessorWorkerResult, VoiceOpsTaskQueueSummary } from './queue';
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
@@ -12976,6 +12976,12 @@ var createSQLiteTraceSinkDeliveryStoreWithDatabase = (database, tableName) => cr
12976
12976
  getSortAt: (value) => value.createdAt,
12977
12977
  tableName
12978
12978
  });
12979
+ var createSQLiteTelephonyWebhookIdempotencyStoreWithDatabase = (database, tableName) => createSQLiteRecordStore({
12980
+ database,
12981
+ decorate: (_id, value) => value,
12982
+ getSortAt: (value) => value.updatedAt,
12983
+ tableName
12984
+ });
12979
12985
  var createVoiceSQLiteSessionStore = (options) => createSQLiteSessionStoreWithDatabase(openVoiceSQLiteDatabase(options.path), resolveTableName({
12980
12986
  fallback: "sessions",
12981
12987
  options
@@ -13004,6 +13010,10 @@ var createVoiceSQLiteTraceSinkDeliveryStore = (options) => createSQLiteTraceSink
13004
13010
  fallback: "trace_deliveries",
13005
13011
  options
13006
13012
  }));
13013
+ var createVoiceSQLiteTelephonyWebhookIdempotencyStore = (options) => createSQLiteTelephonyWebhookIdempotencyStoreWithDatabase(openVoiceSQLiteDatabase(options.path), resolveTableName({
13014
+ fallback: "telephony_webhook_idempotency",
13015
+ options
13016
+ }));
13007
13017
  var createVoiceSQLiteRuntimeStorage = (options) => {
13008
13018
  const database = openVoiceSQLiteDatabase(options.path);
13009
13019
  return {
@@ -13219,6 +13229,15 @@ var createPostgresTraceSinkDeliveryStoreWithClient = (client, options) => create
13219
13229
  }),
13220
13230
  sql: client
13221
13231
  });
13232
+ var createPostgresTelephonyWebhookIdempotencyStoreWithClient = (client, options) => createPostgresRecordStore({
13233
+ decorate: (_id, value) => value,
13234
+ getSortAt: (value) => value.updatedAt,
13235
+ qualifiedTableName: resolveQualifiedTableName({
13236
+ fallback: "telephony_webhook_idempotency",
13237
+ options
13238
+ }),
13239
+ sql: client
13240
+ });
13222
13241
  var createVoicePostgresSessionStore = (options) => createPostgresSessionStoreWithClient(createVoicePostgresClient(options), options);
13223
13242
  var createVoicePostgresReviewStore = (options) => createPostgresReviewStoreWithClient(createVoicePostgresClient(options), options);
13224
13243
  var createVoicePostgresTaskStore = (options) => createPostgresTaskStoreWithClient(createVoicePostgresClient(options), options);
@@ -13226,6 +13245,7 @@ var createVoicePostgresIntegrationEventStore = (options) => createPostgresEventS
13226
13245
  var createVoicePostgresExternalObjectMapStore = (options) => createPostgresExternalObjectMapStoreWithClient(createVoicePostgresClient(options), options);
13227
13246
  var createVoicePostgresTraceEventStore = (options) => createPostgresTraceEventStoreWithClient(createVoicePostgresClient(options), options);
13228
13247
  var createVoicePostgresTraceSinkDeliveryStore = (options) => createPostgresTraceSinkDeliveryStoreWithClient(createVoicePostgresClient(options), options);
13248
+ var createVoicePostgresTelephonyWebhookIdempotencyStore = (options) => createPostgresTelephonyWebhookIdempotencyStoreWithClient(createVoicePostgresClient(options), options);
13229
13249
  var createVoicePostgresRuntimeStorage = (options) => {
13230
13250
  const client = createVoicePostgresClient(options);
13231
13251
  return {
@@ -13495,6 +13515,7 @@ return 0
13495
13515
  `;
13496
13516
  var getLeaseKey = (prefix, taskId) => `${prefix}:${taskId}`;
13497
13517
  var getIdempotencyKey = (prefix, key) => `${prefix}:${key}`;
13518
+ var getTelephonyWebhookIdempotencyKey = (prefix, key) => `${prefix}:${key}`;
13498
13519
  var parseLeaseValue = (taskId, value, ttlMs) => {
13499
13520
  if (!value || ttlMs <= 0) {
13500
13521
  return null;
@@ -13802,6 +13823,26 @@ var createVoiceRedisIdempotencyStore = (options = {}) => {
13802
13823
  }
13803
13824
  };
13804
13825
  };
13826
+ var createVoiceRedisTelephonyWebhookIdempotencyStore = (options = {}) => {
13827
+ const client = options.client ?? new Bun.RedisClient(options.url);
13828
+ const keyPrefix = options.keyPrefix?.trim() || "voice:telephony-webhook";
13829
+ const defaultTtlSeconds = options.ttlSeconds;
13830
+ return {
13831
+ get: async (key) => {
13832
+ const value = await client.get(getTelephonyWebhookIdempotencyKey(keyPrefix, key));
13833
+ return value ? JSON.parse(value) : undefined;
13834
+ },
13835
+ set: async (key, decision) => {
13836
+ const redisKey = getTelephonyWebhookIdempotencyKey(keyPrefix, key);
13837
+ const value = JSON.stringify(decision);
13838
+ if (typeof defaultTtlSeconds === "number" && defaultTtlSeconds > 0) {
13839
+ await client.set(redisKey, value, "EX", String(Math.ceil(defaultTtlSeconds)));
13840
+ return;
13841
+ }
13842
+ await client.set(redisKey, value);
13843
+ }
13844
+ };
13845
+ };
13805
13846
  var createVoiceWebhookDeliveryWorker = (options) => {
13806
13847
  const allowedStatuses = options.statuses ?? ["pending", "failed"];
13807
13848
  const leaseMs = Math.max(1, options.leaseMs ?? 30000);
@@ -15156,9 +15197,35 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
15156
15197
  };
15157
15198
  // src/telephony/twilio.ts
15158
15199
  import { Buffer as Buffer3 } from "buffer";
15200
+ import { Elysia as Elysia18 } from "elysia";
15159
15201
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
15160
15202
  var VOICE_PCM_SAMPLE_RATE = 16000;
15161
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
+ };
15162
15229
  var normalizeOnTurn2 = (handler) => {
15163
15230
  if (handler.length > 1) {
15164
15231
  const directHandler = handler;
@@ -15535,6 +15602,83 @@ var createTwilioMediaStreamBridge = (socket, options) => {
15535
15602
  }
15536
15603
  };
15537
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
+ };
15538
15682
  // src/telephony/response.ts
15539
15683
  var normalizeWhitespace = (value) => value.replace(/\s+/g, " ").trim();
15540
15684
  var DEFAULT_MAX_WORDS = 12;
@@ -15734,6 +15878,7 @@ export {
15734
15878
  createVoiceSTTProviderRouter,
15735
15879
  createVoiceSQLiteTraceSinkDeliveryStore,
15736
15880
  createVoiceSQLiteTraceEventStore,
15881
+ createVoiceSQLiteTelephonyWebhookIdempotencyStore,
15737
15882
  createVoiceSQLiteTaskStore,
15738
15883
  createVoiceSQLiteSessionStore,
15739
15884
  createVoiceSQLiteRuntimeStorage,
@@ -15744,6 +15889,7 @@ export {
15744
15889
  createVoiceRoutingDecisionSummary,
15745
15890
  createVoiceReviewSavedEvent,
15746
15891
  createVoiceResilienceRoutes,
15892
+ createVoiceRedisTelephonyWebhookIdempotencyStore,
15747
15893
  createVoiceRedisTaskLeaseCoordinator,
15748
15894
  createVoiceRedisIdempotencyStore,
15749
15895
  createVoiceQualityRoutes,
@@ -15756,6 +15902,7 @@ export {
15756
15902
  createVoiceProviderCapabilityHTMLHandler,
15757
15903
  createVoicePostgresTraceSinkDeliveryStore,
15758
15904
  createVoicePostgresTraceEventStore,
15905
+ createVoicePostgresTelephonyWebhookIdempotencyStore,
15759
15906
  createVoicePostgresTaskStore,
15760
15907
  createVoicePostgresSessionStore,
15761
15908
  createVoicePostgresRuntimeStorage,
@@ -15827,6 +15974,7 @@ export {
15827
15974
  createVoiceAgentTool,
15828
15975
  createVoiceAgentSquad,
15829
15976
  createVoiceAgent,
15977
+ createTwilioVoiceRoutes,
15830
15978
  createTwilioVoiceResponse,
15831
15979
  createTwilioMediaStreamBridge,
15832
15980
  createStoredVoiceOpsTask,
@@ -1,6 +1,7 @@
1
1
  import { type StoredVoiceTraceEvent, type VoiceTraceSinkDeliveryRecord, type VoiceTraceSinkDeliveryStore, type VoiceTraceEventStore } from './trace';
2
2
  import type { StoredVoiceIntegrationEvent, StoredVoiceExternalObjectMap, StoredVoiceOpsTask, VoiceExternalObjectMapStore, VoiceIntegrationEventStore, VoiceOpsTaskStore } from './ops';
3
3
  import type { StoredVoiceCallReviewArtifact, VoiceCallReviewStore } from './testing/review';
4
+ import type { VoiceTelephonyWebhookIdempotencyStore } from './telephonyOutcome';
4
5
  import type { VoiceSessionRecord, VoiceSessionStore } from './types';
5
6
  export type VoicePostgresClient = {
6
7
  unsafe: <TRow extends Record<string, unknown> = Record<string, unknown>>(query: string, parameters?: unknown[]) => Promise<TRow[]>;
@@ -28,4 +29,5 @@ export declare const createVoicePostgresIntegrationEventStore: <TEvent extends S
28
29
  export declare const createVoicePostgresExternalObjectMapStore: <TMapping extends StoredVoiceExternalObjectMap = StoredVoiceExternalObjectMap>(options: VoicePostgresStoreOptions) => VoiceExternalObjectMapStore<TMapping>;
29
30
  export declare const createVoicePostgresTraceEventStore: <TEvent extends StoredVoiceTraceEvent = StoredVoiceTraceEvent>(options: VoicePostgresStoreOptions) => VoiceTraceEventStore<TEvent>;
30
31
  export declare const createVoicePostgresTraceSinkDeliveryStore: <TDelivery extends VoiceTraceSinkDeliveryRecord = VoiceTraceSinkDeliveryRecord>(options: VoicePostgresStoreOptions) => VoiceTraceSinkDeliveryStore<TDelivery>;
32
+ export declare const createVoicePostgresTelephonyWebhookIdempotencyStore: <TResult = unknown>(options: VoicePostgresStoreOptions) => VoiceTelephonyWebhookIdempotencyStore<TResult>;
31
33
  export declare const createVoicePostgresRuntimeStorage: <TSession extends VoiceSessionRecord = VoiceSessionRecord, TReview extends StoredVoiceCallReviewArtifact = StoredVoiceCallReviewArtifact, TTask extends StoredVoiceOpsTask = StoredVoiceOpsTask, TEvent extends StoredVoiceIntegrationEvent = StoredVoiceIntegrationEvent, TMapping extends StoredVoiceExternalObjectMap = StoredVoiceExternalObjectMap, TTrace extends StoredVoiceTraceEvent = StoredVoiceTraceEvent, TTraceDelivery extends VoiceTraceSinkDeliveryRecord = VoiceTraceSinkDeliveryRecord>(options: VoicePostgresStoreOptions) => VoicePostgresRuntimeStorage<TSession, TReview, TTask, TEvent, TMapping, TTrace, TTraceDelivery>;
package/dist/queue.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { RedisClient } from 'bun';
2
2
  import type { VoiceIntegrationSink } from './opsSinks';
3
3
  import type { VoiceTraceRedactionConfig, VoiceTraceSink, VoiceTraceSinkDeliveryRecord, VoiceTraceSinkDeliveryStore, VoiceTraceSinkDeliveryQueueStatus } from './trace';
4
+ import type { VoiceTelephonyWebhookIdempotencyStore } from './telephonyOutcome';
4
5
  import type { StoredVoiceHandoffDelivery, VoiceHandoffAdapter, VoiceHandoffDeliveryQueueStatus, VoiceHandoffDeliveryStore, VoiceSessionHandle, VoiceSessionRecord } from './types';
5
6
  import type { VoiceOpsTaskPriority, StoredVoiceOpsTask, StoredVoiceIntegrationEvent, VoiceIntegrationDeliveryStatus, VoiceIntegrationEventStore, VoiceIntegrationWebhookConfig, VoiceOpsTaskKind, VoiceOpsTaskStatus, VoiceOpsTaskStore } from './ops';
6
7
  export type VoiceOpsTaskLease = {
@@ -45,6 +46,13 @@ export type VoiceRedisIdempotencyStoreOptions = {
45
46
  ttlSeconds?: number;
46
47
  url?: string;
47
48
  };
49
+ export type VoiceRedisTelephonyWebhookIdempotencyClient = Pick<RedisClient, 'get' | 'set'>;
50
+ export type VoiceRedisTelephonyWebhookIdempotencyStoreOptions = {
51
+ client?: VoiceRedisTelephonyWebhookIdempotencyClient;
52
+ keyPrefix?: string;
53
+ ttlSeconds?: number;
54
+ url?: string;
55
+ };
48
56
  export type VoiceWebhookDeliveryWorkerOptions<TEvent extends StoredVoiceIntegrationEvent = StoredVoiceIntegrationEvent> = {
49
57
  deadLetters?: VoiceIntegrationEventStore<TEvent>;
50
58
  events: VoiceIntegrationEventStore<TEvent>;
@@ -305,6 +313,7 @@ export declare const summarizeVoiceOpsTaskQueue: <TTask extends StoredVoiceOpsTa
305
313
  }) => Promise<VoiceOpsTaskQueueSummary> | VoiceOpsTaskQueueSummary;
306
314
  export declare const createVoiceRedisTaskLeaseCoordinator: (options?: VoiceRedisTaskLeaseCoordinatorOptions) => VoiceRedisTaskLeaseCoordinator;
307
315
  export declare const createVoiceRedisIdempotencyStore: (options?: VoiceRedisIdempotencyStoreOptions) => VoiceIdempotencyStore;
316
+ export declare const createVoiceRedisTelephonyWebhookIdempotencyStore: <TResult = unknown>(options?: VoiceRedisTelephonyWebhookIdempotencyStoreOptions) => VoiceTelephonyWebhookIdempotencyStore<TResult>;
308
317
  export declare const createVoiceWebhookDeliveryWorker: <TEvent extends StoredVoiceIntegrationEvent = StoredVoiceIntegrationEvent>(options: VoiceWebhookDeliveryWorkerOptions<TEvent>) => {
309
318
  drain: () => Promise<VoiceWebhookDeliveryWorkerResult>;
310
319
  };
@@ -1,6 +1,7 @@
1
1
  import { type StoredVoiceTraceEvent, type VoiceTraceSinkDeliveryRecord, type VoiceTraceSinkDeliveryStore, type VoiceTraceEventStore } from './trace';
2
2
  import type { StoredVoiceIntegrationEvent, StoredVoiceExternalObjectMap, StoredVoiceOpsTask, VoiceExternalObjectMapStore, VoiceIntegrationEventStore, VoiceOpsTaskStore } from './ops';
3
3
  import type { StoredVoiceCallReviewArtifact, VoiceCallReviewStore } from './testing/review';
4
+ import type { VoiceTelephonyWebhookIdempotencyStore } from './telephonyOutcome';
4
5
  import type { VoiceSessionRecord, VoiceSessionStore } from './types';
5
6
  export type VoiceSQLiteStoreOptions = {
6
7
  path: string;
@@ -23,4 +24,5 @@ export declare const createVoiceSQLiteIntegrationEventStore: <TEvent extends Sto
23
24
  export declare const createVoiceSQLiteExternalObjectMapStore: <TMapping extends StoredVoiceExternalObjectMap = StoredVoiceExternalObjectMap>(options: VoiceSQLiteStoreOptions) => VoiceExternalObjectMapStore<TMapping>;
24
25
  export declare const createVoiceSQLiteTraceEventStore: <TEvent extends StoredVoiceTraceEvent = StoredVoiceTraceEvent>(options: VoiceSQLiteStoreOptions) => VoiceTraceEventStore<TEvent>;
25
26
  export declare const createVoiceSQLiteTraceSinkDeliveryStore: <TDelivery extends VoiceTraceSinkDeliveryRecord = VoiceTraceSinkDeliveryRecord>(options: VoiceSQLiteStoreOptions) => VoiceTraceSinkDeliveryStore<TDelivery>;
27
+ export declare const createVoiceSQLiteTelephonyWebhookIdempotencyStore: <TResult = unknown>(options: VoiceSQLiteStoreOptions) => VoiceTelephonyWebhookIdempotencyStore<TResult>;
26
28
  export declare const createVoiceSQLiteRuntimeStorage: <TSession extends VoiceSessionRecord = VoiceSessionRecord, TReview extends StoredVoiceCallReviewArtifact = StoredVoiceCallReviewArtifact, TTask extends StoredVoiceOpsTask = StoredVoiceOpsTask, TEvent extends StoredVoiceIntegrationEvent = StoredVoiceIntegrationEvent, TMapping extends StoredVoiceExternalObjectMap = StoredVoiceExternalObjectMap, TTrace extends StoredVoiceTraceEvent = StoredVoiceTraceEvent, TTraceDelivery extends VoiceTraceSinkDeliveryRecord = VoiceTraceSinkDeliveryRecord>(options: VoiceSQLiteStoreOptions) => VoiceSQLiteRuntimeStorage<TSession, TReview, TTask, TEvent, TMapping, TTrace, TTraceDelivery>;
@@ -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.72",
3
+ "version": "0.0.22-beta.74",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",