@absolutejs/voice 0.0.22-beta.69 → 0.0.22-beta.70

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
@@ -11,7 +11,7 @@ export { createVoiceToolIdempotencyKey, createVoiceToolRuntime } from './toolRun
11
11
  export { createVoiceToolContract, createVoiceToolContractHTMLHandler, createVoiceToolContractJSONHandler, createVoiceToolContractRoutes, createVoiceToolRuntimeContractDefaults, renderVoiceToolContractHTML, runVoiceToolContractSuite, runVoiceToolContract } from './toolContract';
12
12
  export { createVoiceTurnQualityHTMLHandler, createVoiceTurnQualityJSONHandler, createVoiceTurnQualityRoutes, renderVoiceTurnQualityHTML, summarizeVoiceTurnQuality } from './turnQuality';
13
13
  export { createVoiceOutcomeContractHTMLHandler, createVoiceOutcomeContractJSONHandler, createVoiceOutcomeContractRoutes, renderVoiceOutcomeContractHTML, runVoiceOutcomeContractSuite } from './outcomeContract';
14
- export { applyVoiceTelephonyOutcome, createVoiceTelephonyOutcomePolicy, resolveVoiceTelephonyOutcome, voiceTelephonyOutcomeToRouteResult } from './telephonyOutcome';
14
+ export { applyVoiceTelephonyOutcome, createVoiceTelephonyOutcomePolicy, createVoiceTelephonyWebhookHandler, createVoiceTelephonyWebhookRoutes, parseVoiceTelephonyWebhookEvent, resolveVoiceTelephonyOutcome, voiceTelephonyOutcomeToRouteResult } from './telephonyOutcome';
15
15
  export { createStoredVoiceCallReviewArtifact, createStoredVoiceExternalObjectMap, createStoredVoiceIntegrationEvent, createStoredVoiceOpsTask, createVoiceFileExternalObjectMapStore, createVoiceFileAssistantMemoryStore, createVoiceFileIntegrationEventStore, createVoiceFileReviewStore, createVoiceFileRuntimeStorage, createVoiceFileSessionStore, createVoiceFileTaskStore, createVoiceFileTraceSinkDeliveryStore, createVoiceFileTraceEventStore } from './fileStore';
16
16
  export { createVoiceAssistantMemoryHandle, createVoiceAssistantMemoryRecord, createVoiceMemoryAssistantMemoryStore, resolveVoiceAssistantMemoryNamespace } from './assistantMemory';
17
17
  export { createAnthropicVoiceAssistantModel, createGeminiVoiceAssistantModel, createJSONVoiceAssistantModel, createOpenAIVoiceAssistantModel, resolveVoiceProviderRoutingPolicyPreset, createVoiceProviderRouter } from './modelAdapters';
@@ -57,7 +57,7 @@ export type { VoiceProviderHealthStatus, VoiceProviderHealthSummary, VoiceProvid
57
57
  export type { VoiceProviderCapabilityDefinition, VoiceProviderCapabilityHandlerOptions, VoiceProviderCapabilityHTMLHandlerOptions, VoiceProviderCapabilityKind, VoiceProviderCapabilityOptions, VoiceProviderCapabilityReport, VoiceProviderCapabilityRoutesOptions, VoiceProviderCapabilitySummary } from './providerCapabilities';
58
58
  export type { VoiceTurnQualityHTMLHandlerOptions, VoiceTurnQualityItem, VoiceTurnQualityOptions, VoiceTurnQualityReport, VoiceTurnQualityRoutesOptions, VoiceTurnQualityStatus } from './turnQuality';
59
59
  export type { VoiceOutcomeContractDefinition, VoiceOutcomeContractHTMLHandlerOptions, VoiceOutcomeContractIssue, VoiceOutcomeContractOptions, VoiceOutcomeContractReport, VoiceOutcomeContractRoutesOptions, VoiceOutcomeContractStatus, VoiceOutcomeContractSuiteReport } from './outcomeContract';
60
- export type { VoiceTelephonyOutcomeAction, VoiceTelephonyOutcomeDecision, VoiceTelephonyOutcomePolicy, VoiceTelephonyOutcomeProviderEvent, VoiceTelephonyOutcomeRouteResult, VoiceTelephonyOutcomeStatusDecision } from './telephonyOutcome';
60
+ export type { VoiceTelephonyOutcomeAction, VoiceTelephonyOutcomeDecision, VoiceTelephonyOutcomePolicy, VoiceTelephonyOutcomeProviderEvent, VoiceTelephonyOutcomeRouteResult, VoiceTelephonyOutcomeStatusDecision, VoiceTelephonyWebhookDecision, VoiceTelephonyWebhookHandlerOptions, VoiceTelephonyWebhookParseInput, VoiceTelephonyWebhookProvider, VoiceTelephonyWebhookRoutesOptions } from './telephonyOutcome';
61
61
  export type { VoiceOpsConsoleLink, VoiceOpsConsoleReport, VoiceOpsConsoleRoutesOptions } from './opsConsoleRoutes';
62
62
  export type { VoiceQualityLink, VoiceQualityMetric, VoiceQualityReport, VoiceQualityRoutesOptions, VoiceQualityStatus, VoiceQualityThresholds } from './qualityRoutes';
63
63
  export type { VoiceResilienceIOSimulator, VoiceResilienceLink, VoiceResiliencePageData, VoiceResilienceRoutesOptions, VoiceResilienceSimulationProvider, VoiceRoutingDecisionSummary, VoiceRoutingDecisionSummaryOptions, VoiceRoutingEvent, VoiceRoutingEventKind } from './resilienceRoutes';
package/dist/index.js CHANGED
@@ -10560,6 +10560,7 @@ var createVoiceOutcomeContractRoutes = (options) => {
10560
10560
  return routes;
10561
10561
  };
10562
10562
  // src/telephonyOutcome.ts
10563
+ import { Elysia as Elysia16 } from "elysia";
10563
10564
  var DEFAULT_COMPLETED_STATUSES = [
10564
10565
  "answered",
10565
10566
  "completed",
@@ -10599,7 +10600,55 @@ var DEFAULT_MACHINE_VOICEMAIL_VALUES = [
10599
10600
  "voicemail"
10600
10601
  ];
10601
10602
  var DEFAULT_NO_ANSWER_SIP_CODES = [408, 480, 486, 487, 603];
10603
+ var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
10602
10604
  var normalizeToken = (value) => typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_+/g, "-") : undefined;
10605
+ var firstString = (source, keys) => {
10606
+ for (const key of keys) {
10607
+ const value = source[key];
10608
+ if (typeof value === "string" && value.trim()) {
10609
+ return value.trim();
10610
+ }
10611
+ if (typeof value === "number" && Number.isFinite(value)) {
10612
+ return String(value);
10613
+ }
10614
+ }
10615
+ };
10616
+ var firstNumber = (source, keys) => {
10617
+ for (const key of keys) {
10618
+ const value = source[key];
10619
+ if (typeof value === "number" && Number.isFinite(value)) {
10620
+ return value;
10621
+ }
10622
+ if (typeof value === "string" && value.trim()) {
10623
+ const parsed = Number(value);
10624
+ if (Number.isFinite(parsed)) {
10625
+ return parsed;
10626
+ }
10627
+ }
10628
+ }
10629
+ };
10630
+ var parseMaybeJSON = (value) => {
10631
+ try {
10632
+ return JSON.parse(value);
10633
+ } catch {
10634
+ return;
10635
+ }
10636
+ };
10637
+ var flattenPayload = (value) => {
10638
+ if (!isRecord(value)) {
10639
+ return {};
10640
+ }
10641
+ const data = isRecord(value.data) ? value.data : undefined;
10642
+ const payload = isRecord(value.payload) ? value.payload : undefined;
10643
+ const event = isRecord(value.event) ? value.event : undefined;
10644
+ return {
10645
+ ...value,
10646
+ ...payload,
10647
+ ...event,
10648
+ ...data,
10649
+ ...isRecord(data?.payload) ? data.payload : undefined
10650
+ };
10651
+ };
10603
10652
  var normalizeList = (values, fallback) => new Set((values ?? fallback).map(normalizeToken).filter(Boolean));
10604
10653
  var metadataValue = (metadata, keys) => {
10605
10654
  for (const key of keys) {
@@ -10833,6 +10882,172 @@ var applyVoiceTelephonyOutcome = async (api, decision, result) => {
10833
10882
  break;
10834
10883
  }
10835
10884
  };
10885
+ var parseRequestBody = async (request) => {
10886
+ const contentType = request.headers.get("content-type") ?? "";
10887
+ const text = await request.text();
10888
+ if (!text) {
10889
+ return {};
10890
+ }
10891
+ if (contentType.includes("application/json")) {
10892
+ return parseMaybeJSON(text) ?? {};
10893
+ }
10894
+ if (contentType.includes("application/x-www-form-urlencoded") || contentType.includes("multipart/form-data")) {
10895
+ return Object.fromEntries(new URLSearchParams(text));
10896
+ }
10897
+ return parseMaybeJSON(text) ?? Object.fromEntries(new URLSearchParams(text));
10898
+ };
10899
+ var durationMsFromSeconds = (value) => typeof value === "number" ? value * 1000 : undefined;
10900
+ var parseVoiceTelephonyWebhookEvent = (input) => {
10901
+ const payload = flattenPayload(input.body);
10902
+ const provider = firstString(payload, ["provider", "Provider"]) ?? input.provider;
10903
+ const status = firstString(payload, [
10904
+ "CallStatus",
10905
+ "call_status",
10906
+ "callStatus",
10907
+ "DialCallStatus",
10908
+ "dial_call_status",
10909
+ "status",
10910
+ "event_type",
10911
+ "type"
10912
+ ]);
10913
+ const durationMs = firstNumber(payload, ["durationMs", "duration_ms"]) ?? durationMsFromSeconds(firstNumber(payload, [
10914
+ "CallDuration",
10915
+ "call_duration",
10916
+ "callDuration",
10917
+ "DialCallDuration",
10918
+ "dial_call_duration",
10919
+ "duration"
10920
+ ]));
10921
+ const sipCode = firstNumber(payload, [
10922
+ "SipResponseCode",
10923
+ "sip_response_code",
10924
+ "sipCode",
10925
+ "sip_code",
10926
+ "hangupCauseCode"
10927
+ ]);
10928
+ const from = firstString(payload, ["From", "from", "caller_id", "callerId"]);
10929
+ const to = firstString(payload, ["To", "to", "called_number", "calledNumber"]);
10930
+ const target = firstString(payload, [
10931
+ "transferTarget",
10932
+ "TransferTarget",
10933
+ "target",
10934
+ "queue",
10935
+ "department"
10936
+ ]);
10937
+ return {
10938
+ answeredBy: firstString(payload, [
10939
+ "AnsweredBy",
10940
+ "answered_by",
10941
+ "answeredBy",
10942
+ "machineDetection",
10943
+ "machine_detection"
10944
+ ]),
10945
+ durationMs,
10946
+ from,
10947
+ metadata: payload,
10948
+ provider,
10949
+ reason: firstString(payload, [
10950
+ "Reason",
10951
+ "reason",
10952
+ "HangupCause",
10953
+ "hangup_cause",
10954
+ "hangupCause"
10955
+ ]),
10956
+ sipCode,
10957
+ status,
10958
+ target,
10959
+ to
10960
+ };
10961
+ };
10962
+ var defaultSessionId = (input) => {
10963
+ const payload = flattenPayload(input.body);
10964
+ const metadataSessionId = input.event.metadata?.sessionId;
10965
+ return firstString(input.query, ["sessionId", "session_id"]) ?? firstString(payload, [
10966
+ "sessionId",
10967
+ "session_id",
10968
+ "SessionId",
10969
+ "CallSid",
10970
+ "call_sid",
10971
+ "callSid",
10972
+ "CallUUID",
10973
+ "call_uuid",
10974
+ "callControlId",
10975
+ "call_control_id"
10976
+ ]) ?? (typeof metadataSessionId === "string" ? metadataSessionId : undefined);
10977
+ };
10978
+ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
10979
+ const provider = options.provider ?? "generic";
10980
+ const query = input.query ?? {};
10981
+ const body = await parseRequestBody(input.request);
10982
+ const event = options.parse ? await options.parse({
10983
+ body,
10984
+ headers: input.request.headers,
10985
+ provider,
10986
+ query,
10987
+ request: input.request
10988
+ }) : parseVoiceTelephonyWebhookEvent({
10989
+ body,
10990
+ headers: input.request.headers,
10991
+ provider,
10992
+ query,
10993
+ request: input.request
10994
+ });
10995
+ const sessionId = await (options.resolveSessionId?.({
10996
+ body,
10997
+ event,
10998
+ query,
10999
+ request: input.request
11000
+ }) ?? defaultSessionId({ body, event, query }));
11001
+ const decision = resolveVoiceTelephonyOutcome(event, options.policy);
11002
+ const resultResolver = options.result;
11003
+ const result = typeof resultResolver === "function" ? await resultResolver({
11004
+ decision,
11005
+ event,
11006
+ sessionId
11007
+ }) : resultResolver;
11008
+ const routeResult = voiceTelephonyOutcomeToRouteResult(decision, result);
11009
+ const shouldApply = typeof options.apply === "function" ? options.apply({
11010
+ applied: false,
11011
+ decision,
11012
+ event,
11013
+ routeResult,
11014
+ sessionId
11015
+ }) : options.apply === true;
11016
+ let applied = false;
11017
+ if (shouldApply && decision.action !== "ignore" && options.getSessionHandle) {
11018
+ const api = await options.getSessionHandle({
11019
+ context: options.context,
11020
+ decision,
11021
+ event,
11022
+ request: input.request,
11023
+ sessionId
11024
+ });
11025
+ if (api) {
11026
+ await applyVoiceTelephonyOutcome(api, decision, result);
11027
+ applied = true;
11028
+ }
11029
+ }
11030
+ const webhookDecision = {
11031
+ applied,
11032
+ decision,
11033
+ event,
11034
+ routeResult,
11035
+ sessionId
11036
+ };
11037
+ await options.onDecision?.({
11038
+ ...webhookDecision,
11039
+ context: options.context,
11040
+ request: input.request
11041
+ });
11042
+ return webhookDecision;
11043
+ };
11044
+ var createVoiceTelephonyWebhookRoutes = (options = {}) => {
11045
+ const path = options.path ?? "/api/voice/telephony/webhook";
11046
+ const handler = createVoiceTelephonyWebhookHandler(options);
11047
+ return new Elysia16({
11048
+ name: options.name ?? "absolutejs-voice-telephony-webhooks"
11049
+ }).post(path, async ({ query, request }) => handler({ query, request }));
11050
+ };
10836
11051
  // src/fileStore.ts
10837
11052
  import { mkdir as mkdir2, readFile, readdir, rename, rm, writeFile } from "fs/promises";
10838
11053
  import { join } from "path";
@@ -12926,7 +13141,7 @@ var createVoiceMemoryStore = () => {
12926
13141
  return { get, getOrCreate, list, remove, set };
12927
13142
  };
12928
13143
  // src/opsWebhook.ts
12929
- import { Elysia as Elysia16 } from "elysia";
13144
+ import { Elysia as Elysia17 } from "elysia";
12930
13145
  var toHex5 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
12931
13146
  var signVoiceOpsWebhookBody = async (input) => {
12932
13147
  const encoder = new TextEncoder;
@@ -13056,7 +13271,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
13056
13271
  };
13057
13272
  var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
13058
13273
  const path = options.path ?? "/api/voice-ops/webhook";
13059
- return new Elysia16().post(path, async ({ body, request, set }) => {
13274
+ return new Elysia17().post(path, async ({ body, request, set }) => {
13060
13275
  const bodyText = typeof body === "string" ? body : JSON.stringify(body);
13061
13276
  if (options.signingSecret) {
13062
13277
  const verification = await verifyVoiceOpsWebhookSignature({
@@ -15271,6 +15486,7 @@ export {
15271
15486
  recordVoiceWorkflowContractTrace,
15272
15487
  recordVoiceRuntimeOps,
15273
15488
  pruneVoiceTraceEvents,
15489
+ parseVoiceTelephonyWebhookEvent,
15274
15490
  matchesVoiceOpsTaskAssignmentRule,
15275
15491
  markVoiceOpsTaskSLABreached,
15276
15492
  listVoiceRoutingEvents,
@@ -15320,6 +15536,8 @@ export {
15320
15536
  createVoiceToolContractJSONHandler,
15321
15537
  createVoiceToolContractHTMLHandler,
15322
15538
  createVoiceToolContract,
15539
+ createVoiceTelephonyWebhookRoutes,
15540
+ createVoiceTelephonyWebhookHandler,
15323
15541
  createVoiceTelephonyOutcomePolicy,
15324
15542
  createVoiceTaskUpdatedEvent,
15325
15543
  createVoiceTaskSLABreachedEvent,
@@ -1,3 +1,4 @@
1
+ import { Elysia } from 'elysia';
1
2
  import type { VoiceCallDisposition, VoiceRouteResult, VoiceSessionHandle, VoiceSessionRecord } from './types';
2
3
  export type VoiceTelephonyOutcomeAction = 'complete' | 'escalate' | 'ignore' | 'no-answer' | 'transfer' | 'voicemail';
3
4
  export type VoiceTelephonyOutcomeProviderEvent = {
@@ -43,7 +44,100 @@ export type VoiceTelephonyOutcomePolicy = {
43
44
  voicemailStatuses?: string[];
44
45
  };
45
46
  export type VoiceTelephonyOutcomeRouteResult<TResult = unknown> = VoiceRouteResult<TResult>;
47
+ export type VoiceTelephonyWebhookProvider = 'generic' | 'plivo' | 'telnyx' | 'twilio';
48
+ export type VoiceTelephonyWebhookParseInput = {
49
+ body: unknown;
50
+ headers: Headers;
51
+ provider: VoiceTelephonyWebhookProvider;
52
+ query: Record<string, unknown>;
53
+ request: Request;
54
+ };
55
+ export type VoiceTelephonyWebhookDecision<TResult = unknown> = {
56
+ applied: boolean;
57
+ decision: VoiceTelephonyOutcomeDecision;
58
+ event: VoiceTelephonyOutcomeProviderEvent;
59
+ routeResult: VoiceTelephonyOutcomeRouteResult<TResult>;
60
+ sessionId?: string;
61
+ };
62
+ export type VoiceTelephonyWebhookHandlerOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
63
+ apply?: boolean | ((input: VoiceTelephonyWebhookDecision<TResult>) => boolean);
64
+ context?: TContext;
65
+ getSessionHandle?: (input: {
66
+ context: TContext;
67
+ decision: VoiceTelephonyOutcomeDecision;
68
+ event: VoiceTelephonyOutcomeProviderEvent;
69
+ request: Request;
70
+ sessionId?: string;
71
+ }) => Promise<VoiceSessionHandle<TContext, TSession, TResult> | undefined> | VoiceSessionHandle<TContext, TSession, TResult> | undefined;
72
+ onDecision?: (input: VoiceTelephonyWebhookDecision<TResult> & {
73
+ context: TContext;
74
+ request: Request;
75
+ }) => Promise<void> | void;
76
+ parse?: (input: VoiceTelephonyWebhookParseInput) => Promise<VoiceTelephonyOutcomeProviderEvent> | VoiceTelephonyOutcomeProviderEvent;
77
+ policy?: VoiceTelephonyOutcomePolicy;
78
+ provider?: VoiceTelephonyWebhookProvider;
79
+ resolveSessionId?: (input: {
80
+ body: unknown;
81
+ event: VoiceTelephonyOutcomeProviderEvent;
82
+ query: Record<string, unknown>;
83
+ request: Request;
84
+ }) => Promise<string | undefined> | string | undefined;
85
+ result?: TResult | ((input: {
86
+ decision: VoiceTelephonyOutcomeDecision;
87
+ event: VoiceTelephonyOutcomeProviderEvent;
88
+ sessionId?: string;
89
+ }) => Promise<TResult | undefined> | TResult | undefined);
90
+ };
91
+ export type VoiceTelephonyWebhookRoutesOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = VoiceTelephonyWebhookHandlerOptions<TContext, TSession, TResult> & {
92
+ name?: string;
93
+ path?: string;
94
+ };
46
95
  export declare const createVoiceTelephonyOutcomePolicy: (policy?: VoiceTelephonyOutcomePolicy) => Required<Pick<VoiceTelephonyOutcomePolicy, "completedStatuses" | "escalationStatuses" | "failedAsNoAnswer" | "failedStatuses" | "includeProviderPayload" | "machineDetectionVoicemailValues" | "noAnswerOnZeroDuration" | "noAnswerSipCodes" | "noAnswerStatuses" | "transferStatuses" | "voicemailStatuses">> & VoiceTelephonyOutcomePolicy;
47
96
  export declare const resolveVoiceTelephonyOutcome: (event: VoiceTelephonyOutcomeProviderEvent, policyInput?: VoiceTelephonyOutcomePolicy) => VoiceTelephonyOutcomeDecision;
48
97
  export declare const voiceTelephonyOutcomeToRouteResult: <TResult = unknown>(decision: VoiceTelephonyOutcomeDecision, result?: TResult) => VoiceTelephonyOutcomeRouteResult<TResult>;
49
98
  export declare const applyVoiceTelephonyOutcome: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(api: VoiceSessionHandle<TContext, TSession, TResult>, decision: VoiceTelephonyOutcomeDecision, result?: TResult) => Promise<void>;
99
+ export declare const parseVoiceTelephonyWebhookEvent: (input: VoiceTelephonyWebhookParseInput) => VoiceTelephonyOutcomeProviderEvent;
100
+ export declare const createVoiceTelephonyWebhookHandler: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options?: VoiceTelephonyWebhookHandlerOptions<TContext, TSession, TResult>) => (input: {
101
+ query?: Record<string, unknown>;
102
+ request: Request;
103
+ }) => Promise<VoiceTelephonyWebhookDecision<TResult>>;
104
+ export declare const createVoiceTelephonyWebhookRoutes: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options?: VoiceTelephonyWebhookRoutesOptions<TContext, TSession, TResult>) => Elysia<"", {
105
+ decorator: {};
106
+ store: {};
107
+ derive: {};
108
+ resolve: {};
109
+ }, {
110
+ typebox: {};
111
+ error: {};
112
+ }, {
113
+ schema: {};
114
+ standaloneSchema: {};
115
+ macro: {};
116
+ macroFn: {};
117
+ parser: {};
118
+ response: {};
119
+ }, {
120
+ [x: string]: {
121
+ post: {
122
+ body: unknown;
123
+ params: {};
124
+ query: unknown;
125
+ headers: unknown;
126
+ response: {
127
+ 200: VoiceTelephonyWebhookDecision<TResult>;
128
+ };
129
+ };
130
+ };
131
+ }, {
132
+ derive: {};
133
+ resolve: {};
134
+ schema: {};
135
+ standaloneSchema: {};
136
+ response: {};
137
+ }, {
138
+ derive: {};
139
+ resolve: {};
140
+ schema: {};
141
+ standaloneSchema: {};
142
+ response: {};
143
+ }>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.69",
3
+ "version": "0.0.22-beta.70",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",