@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 +6 -5
- package/dist/index.js +148 -0
- package/dist/postgresStore.d.ts +2 -0
- package/dist/queue.d.ts +9 -0
- package/dist/sqliteStore.d.ts +2 -0
- package/dist/telephony/twilio.d.ts +106 -0
- package/dist/testing/index.js +779 -7
- package/package.json +1 -1
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("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
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,
|
package/dist/postgresStore.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/sqliteStore.d.ts
CHANGED
|
@@ -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 {};
|
package/dist/testing/index.js
CHANGED
|
@@ -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) =>
|
|
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
|
|
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("&", "&").replaceAll('"', """).replaceAll("'", "'").replaceAll("<", "<").replaceAll(">", ">");
|
|
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(
|
|
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
|
|
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
|
|
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(
|
|
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 = {
|