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

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, createVoiceTelephonyWebhookHandler, createVoiceTelephonyWebhookRoutes, parseVoiceTelephonyWebhookEvent, resolveVoiceTelephonyOutcome, voiceTelephonyOutcomeToRouteResult } from './telephonyOutcome';
14
+ export { applyVoiceTelephonyOutcome, createMemoryVoiceTelephonyWebhookIdempotencyStore, createVoiceTelephonyOutcomePolicy, createVoiceTelephonyWebhookHandler, createVoiceTelephonyWebhookRoutes, parseVoiceTelephonyWebhookEvent, resolveVoiceTelephonyOutcome, signVoiceTwilioWebhook, verifyVoiceTwilioWebhookSignature, 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, VoiceTelephonyWebhookDecision, VoiceTelephonyWebhookHandlerOptions, VoiceTelephonyWebhookParseInput, VoiceTelephonyWebhookProvider, VoiceTelephonyWebhookRoutesOptions } from './telephonyOutcome';
60
+ export type { VoiceTelephonyOutcomeAction, VoiceTelephonyOutcomeDecision, VoiceTelephonyOutcomePolicy, VoiceTelephonyOutcomeProviderEvent, VoiceTelephonyOutcomeRouteResult, VoiceTelephonyOutcomeStatusDecision, VoiceTelephonyWebhookDecision, VoiceTelephonyWebhookHandlerOptions, VoiceTelephonyWebhookIdempotencyStore, VoiceTelephonyWebhookParseInput, VoiceTelephonyWebhookProvider, VoiceTelephonyWebhookRoutesOptions, VoiceTelephonyWebhookVerificationResult, StoredVoiceTelephonyWebhookDecision } 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
@@ -2990,7 +2990,7 @@ var toVoiceSessionSummary = (session) => ({
2990
2990
  });
2991
2991
 
2992
2992
  // src/session.ts
2993
- import { Buffer } from "buffer";
2993
+ import { Buffer as Buffer2 } from "buffer";
2994
2994
 
2995
2995
  // src/handoff.ts
2996
2996
  var toHex3 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
@@ -3425,7 +3425,7 @@ var createEmptyCurrentTurn = () => ({
3425
3425
  transcripts: []
3426
3426
  });
3427
3427
  var cloneTranscript = (transcript) => ({ ...transcript });
3428
- var encodeBase64 = (chunk) => Buffer.from(chunk).toString("base64");
3428
+ var encodeBase64 = (chunk) => Buffer2.from(chunk).toString("base64");
3429
3429
  var countWords2 = (text) => text.trim().split(/\s+/).filter(Boolean).length;
3430
3430
  var normalizeText2 = (text) => text.trim().replace(/\s+/g, " ");
3431
3431
  var getAudioChunkDurationMs = (chunk) => chunk.byteLength / (DEFAULT_FORMAT.sampleRateHz * DEFAULT_FORMAT.channels * 2) * 1000;
@@ -10601,6 +10601,24 @@ var DEFAULT_MACHINE_VOICEMAIL_VALUES = [
10601
10601
  ];
10602
10602
  var DEFAULT_NO_ANSWER_SIP_CODES = [408, 480, 486, 487, 603];
10603
10603
  var isRecord = (value) => Boolean(value) && typeof value === "object" && !Array.isArray(value);
10604
+
10605
+ class VoiceTelephonyWebhookVerificationError extends Error {
10606
+ result;
10607
+ constructor(result) {
10608
+ super(result.ok ? "telephony webhook verified" : result.reason);
10609
+ this.name = "VoiceTelephonyWebhookVerificationError";
10610
+ this.result = result;
10611
+ }
10612
+ }
10613
+ var createMemoryVoiceTelephonyWebhookIdempotencyStore = () => {
10614
+ const decisions = new Map;
10615
+ return {
10616
+ get: (key) => decisions.get(key),
10617
+ set: (key, decision) => {
10618
+ decisions.set(key, decision);
10619
+ }
10620
+ };
10621
+ };
10604
10622
  var normalizeToken = (value) => typeof value === "string" ? value.trim().toLowerCase().replace(/\s+/g, "-").replace(/_+/g, "-") : undefined;
10605
10623
  var firstString = (source, keys) => {
10606
10624
  for (const key of keys) {
@@ -10649,6 +10667,30 @@ var flattenPayload = (value) => {
10649
10667
  ...isRecord(data?.payload) ? data.payload : undefined
10650
10668
  };
10651
10669
  };
10670
+ var toBase64 = (bytes) => Buffer.from(new Uint8Array(bytes)).toString("base64");
10671
+ var timingSafeEqual = (left, right) => {
10672
+ const encoder = new TextEncoder;
10673
+ const leftBytes = encoder.encode(left);
10674
+ const rightBytes = encoder.encode(right);
10675
+ if (leftBytes.length !== rightBytes.length) {
10676
+ return false;
10677
+ }
10678
+ let diff = 0;
10679
+ for (let index = 0;index < leftBytes.length; index += 1) {
10680
+ diff |= leftBytes[index] ^ rightBytes[index];
10681
+ }
10682
+ return diff === 0;
10683
+ };
10684
+ var signHmacSHA1Base64 = async (secret, payload) => {
10685
+ const encoder = new TextEncoder;
10686
+ const key = await crypto.subtle.importKey("raw", encoder.encode(secret), {
10687
+ hash: "SHA-1",
10688
+ name: "HMAC"
10689
+ }, false, ["sign"]);
10690
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(payload));
10691
+ return toBase64(signature);
10692
+ };
10693
+ 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("");
10652
10694
  var normalizeList = (values, fallback) => new Set((values ?? fallback).map(normalizeToken).filter(Boolean));
10653
10695
  var metadataValue = (metadata, keys) => {
10654
10696
  for (const key of keys) {
@@ -10882,9 +10924,8 @@ var applyVoiceTelephonyOutcome = async (api, decision, result) => {
10882
10924
  break;
10883
10925
  }
10884
10926
  };
10885
- var parseRequestBody = async (request) => {
10886
- const contentType = request.headers.get("content-type") ?? "";
10887
- const text = await request.text();
10927
+ var parseRequestBodyText = (input) => {
10928
+ const { contentType, text } = input;
10888
10929
  if (!text) {
10889
10930
  return {};
10890
10931
  }
@@ -10896,6 +10937,58 @@ var parseRequestBody = async (request) => {
10896
10937
  }
10897
10938
  return parseMaybeJSON(text) ?? Object.fromEntries(new URLSearchParams(text));
10898
10939
  };
10940
+ var readRequestBody = async (request) => {
10941
+ const contentType = request.headers.get("content-type") ?? "";
10942
+ const text = await request.text();
10943
+ return {
10944
+ body: parseRequestBodyText({ contentType, text }),
10945
+ rawBody: text
10946
+ };
10947
+ };
10948
+ var signVoiceTwilioWebhook = async (input) => signHmacSHA1Base64(input.authToken, `${input.url}${sortedParamsForSignature(input.body ?? {})}`);
10949
+ var verifyVoiceTwilioWebhookSignature = async (input) => {
10950
+ if (!input.authToken) {
10951
+ return { ok: false, reason: "missing-secret" };
10952
+ }
10953
+ const signature = input.headers.get("x-twilio-signature");
10954
+ if (!signature) {
10955
+ return { ok: false, reason: "missing-signature" };
10956
+ }
10957
+ const expected = await signVoiceTwilioWebhook({
10958
+ authToken: input.authToken,
10959
+ body: input.body,
10960
+ url: input.url
10961
+ });
10962
+ return timingSafeEqual(signature, expected) ? { ok: true } : { ok: false, reason: "invalid-signature" };
10963
+ };
10964
+ var resolveVerificationUrl = (option, input) => typeof option === "function" ? option(input) : option ?? input.request.url;
10965
+ var verifyVoiceTelephonyWebhook = async (input) => {
10966
+ if (input.options.verify) {
10967
+ return input.options.verify({
10968
+ body: input.body,
10969
+ headers: input.request.headers,
10970
+ provider: input.provider,
10971
+ query: input.query,
10972
+ rawBody: input.rawBody,
10973
+ request: input.request
10974
+ });
10975
+ }
10976
+ if (!input.options.signingSecret) {
10977
+ return input.options.requireVerification ? { ok: false, reason: "missing-secret" } : { ok: true };
10978
+ }
10979
+ if (input.provider !== "twilio") {
10980
+ return { ok: false, reason: "unsupported-provider" };
10981
+ }
10982
+ return verifyVoiceTwilioWebhookSignature({
10983
+ authToken: input.options.signingSecret,
10984
+ body: input.body,
10985
+ headers: input.request.headers,
10986
+ url: resolveVerificationUrl(input.options.verificationUrl, {
10987
+ query: input.query,
10988
+ request: input.request
10989
+ })
10990
+ });
10991
+ };
10899
10992
  var durationMsFromSeconds = (value) => typeof value === "number" ? value * 1000 : undefined;
10900
10993
  var parseVoiceTelephonyWebhookEvent = (input) => {
10901
10994
  const payload = flattenPayload(input.body);
@@ -10975,10 +11068,46 @@ var defaultSessionId = (input) => {
10975
11068
  "call_control_id"
10976
11069
  ]) ?? (typeof metadataSessionId === "string" ? metadataSessionId : undefined);
10977
11070
  };
11071
+ var defaultIdempotencyKey = (input) => {
11072
+ const payload = flattenPayload(input.body);
11073
+ const eventId = firstString(payload, [
11074
+ "id",
11075
+ "event_id",
11076
+ "eventId",
11077
+ "EventSid",
11078
+ "event_sid",
11079
+ "MessageSid",
11080
+ "message_sid",
11081
+ "CallSid",
11082
+ "call_sid",
11083
+ "CallUUID",
11084
+ "call_uuid",
11085
+ "callControlId",
11086
+ "call_control_id"
11087
+ ]);
11088
+ const status = normalizeToken(input.event.status) ?? "unknown";
11089
+ if (eventId) {
11090
+ return `${input.provider}:${eventId}:${status}`;
11091
+ }
11092
+ if (input.sessionId) {
11093
+ return `${input.provider}:${input.sessionId}:${status}`;
11094
+ }
11095
+ };
10978
11096
  var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
10979
11097
  const provider = options.provider ?? "generic";
10980
11098
  const query = input.query ?? {};
10981
- const body = await parseRequestBody(input.request);
11099
+ const { body, rawBody } = await readRequestBody(input.request);
11100
+ const verification = await verifyVoiceTelephonyWebhook({
11101
+ body,
11102
+ options,
11103
+ provider,
11104
+ query,
11105
+ rawBody,
11106
+ request: input.request
11107
+ });
11108
+ if (!verification.ok) {
11109
+ throw new VoiceTelephonyWebhookVerificationError(verification);
11110
+ }
10982
11111
  const event = options.parse ? await options.parse({
10983
11112
  body,
10984
11113
  headers: input.request.headers,
@@ -10998,6 +11127,31 @@ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
10998
11127
  query,
10999
11128
  request: input.request
11000
11129
  }) ?? defaultSessionId({ body, event, query }));
11130
+ const idempotencyEnabled = options.idempotency?.enabled !== false;
11131
+ const idempotencyKey = idempotencyEnabled ? await (options.idempotency?.key?.({
11132
+ body,
11133
+ event,
11134
+ provider,
11135
+ query,
11136
+ request: input.request,
11137
+ sessionId
11138
+ }) ?? defaultIdempotencyKey({ body, event, provider, sessionId })) : undefined;
11139
+ const idempotencyStore = options.idempotency?.store;
11140
+ if (idempotencyKey && idempotencyStore) {
11141
+ const existing = await idempotencyStore.get(idempotencyKey);
11142
+ if (existing) {
11143
+ const duplicateDecision = {
11144
+ ...existing,
11145
+ duplicate: true
11146
+ };
11147
+ await options.onDecision?.({
11148
+ ...duplicateDecision,
11149
+ context: options.context,
11150
+ request: input.request
11151
+ });
11152
+ return duplicateDecision;
11153
+ }
11154
+ }
11001
11155
  const decision = resolveVoiceTelephonyOutcome(event, options.policy);
11002
11156
  const resultResolver = options.result;
11003
11157
  const result = typeof resultResolver === "function" ? await resultResolver({
@@ -11031,9 +11185,18 @@ var createVoiceTelephonyWebhookHandler = (options = {}) => async (input) => {
11031
11185
  applied,
11032
11186
  decision,
11033
11187
  event,
11188
+ idempotencyKey,
11034
11189
  routeResult,
11035
11190
  sessionId
11036
11191
  };
11192
+ if (idempotencyKey && idempotencyStore) {
11193
+ const now = Date.now();
11194
+ await idempotencyStore.set(idempotencyKey, {
11195
+ ...webhookDecision,
11196
+ createdAt: now,
11197
+ updatedAt: now
11198
+ });
11199
+ }
11037
11200
  await options.onDecision?.({
11038
11201
  ...webhookDecision,
11039
11202
  context: options.context,
@@ -11046,7 +11209,21 @@ var createVoiceTelephonyWebhookRoutes = (options = {}) => {
11046
11209
  const handler = createVoiceTelephonyWebhookHandler(options);
11047
11210
  return new Elysia16({
11048
11211
  name: options.name ?? "absolutejs-voice-telephony-webhooks"
11049
- }).post(path, async ({ query, request }) => handler({ query, request }));
11212
+ }).post(path, async ({ query, request }) => {
11213
+ try {
11214
+ return await handler({ query, request });
11215
+ } catch (error) {
11216
+ if (error instanceof VoiceTelephonyWebhookVerificationError) {
11217
+ return new Response(JSON.stringify({ verification: error.result }), {
11218
+ headers: {
11219
+ "content-type": "application/json"
11220
+ },
11221
+ status: 401
11222
+ });
11223
+ }
11224
+ throw error;
11225
+ }
11226
+ });
11050
11227
  };
11051
11228
  // src/fileStore.ts
11052
11229
  import { mkdir as mkdir2, readFile, readdir, rename, rm, writeFile } from "fs/promises";
@@ -13152,7 +13329,7 @@ var signVoiceOpsWebhookBody = async (input) => {
13152
13329
  const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
13153
13330
  return `sha256=${toHex5(new Uint8Array(signature))}`;
13154
13331
  };
13155
- var timingSafeEqual = (left, right) => {
13332
+ var timingSafeEqual2 = (left, right) => {
13156
13333
  const encoder = new TextEncoder;
13157
13334
  const leftBytes = encoder.encode(left);
13158
13335
  const rightBytes = encoder.encode(right);
@@ -13259,7 +13436,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
13259
13436
  secret: input.secret,
13260
13437
  timestamp: input.timestamp
13261
13438
  });
13262
- if (!timingSafeEqual(expected, input.signature)) {
13439
+ if (!timingSafeEqual2(expected, input.signature)) {
13263
13440
  return {
13264
13441
  ok: false,
13265
13442
  reason: "invalid-signature"
@@ -14978,7 +15155,7 @@ var createVoiceSTTRoutingCorrectionHandler = (mode = "generic") => {
14978
15155
  return createPhraseHintCorrectionHandler();
14979
15156
  };
14980
15157
  // src/telephony/twilio.ts
14981
- import { Buffer as Buffer2 } from "buffer";
15158
+ import { Buffer as Buffer3 } from "buffer";
14982
15159
  var TWILIO_MULAW_SAMPLE_RATE = 8000;
14983
15160
  var VOICE_PCM_SAMPLE_RATE = 16000;
14984
15161
  var escapeXml2 = (value) => value.replaceAll("&", "&amp;").replaceAll('"', "&quot;").replaceAll("'", "&apos;").replaceAll("<", "&lt;").replaceAll(">", "&gt;");
@@ -15083,7 +15260,7 @@ var bytesToInt16Array = (bytes) => {
15083
15260
  return output;
15084
15261
  };
15085
15262
  var decodeTwilioMulawBase64 = (payload) => {
15086
- const bytes = Uint8Array.from(Buffer2.from(payload, "base64"));
15263
+ const bytes = Uint8Array.from(Buffer3.from(payload, "base64"));
15087
15264
  const samples = new Int16Array(bytes.length);
15088
15265
  for (let index = 0;index < bytes.length; index += 1) {
15089
15266
  samples[index] = decodeMulawSample(bytes[index] ?? 0);
@@ -15095,7 +15272,7 @@ var encodeTwilioMulawBase64 = (samples) => {
15095
15272
  for (let index = 0;index < samples.length; index += 1) {
15096
15273
  bytes[index] = encodeMulawSample(samples[index] ?? 0);
15097
15274
  }
15098
- return Buffer2.from(bytes).toString("base64");
15275
+ return Buffer3.from(bytes).toString("base64");
15099
15276
  };
15100
15277
  var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
15101
15278
  const narrowband = decodeTwilioMulawBase64(payload);
@@ -15104,7 +15281,7 @@ var transcodeTwilioInboundPayloadToPCM16 = (payload) => {
15104
15281
  };
15105
15282
  var transcodePCMToTwilioOutboundPayload = (chunk, format) => {
15106
15283
  if (format.container === "raw" && format.encoding === "mulaw" && format.channels === 1 && format.sampleRateHz === TWILIO_MULAW_SAMPLE_RATE) {
15107
- return Buffer2.from(chunk).toString("base64");
15284
+ return Buffer3.from(chunk).toString("base64");
15108
15285
  }
15109
15286
  if (format.encoding !== "pcm_s16le") {
15110
15287
  throw new Error(`Unsupported outbound telephony audio format: ${format.container}/${format.encoding}`);
@@ -15145,7 +15322,7 @@ var createTwilioSocketAdapter = (socket, getState) => ({
15145
15322
  return;
15146
15323
  }
15147
15324
  if (message.type === "audio") {
15148
- const payload = transcodePCMToTwilioOutboundPayload(Uint8Array.from(Buffer2.from(message.chunkBase64, "base64")), message.format);
15325
+ const payload = transcodePCMToTwilioOutboundPayload(Uint8Array.from(Buffer3.from(message.chunkBase64, "base64")), message.format);
15149
15326
  state.hasOutboundAudioSinceLastInbound = true;
15150
15327
  state.reviewRecorder?.recordTwilioOutbound({
15151
15328
  bytes: payload.length,
@@ -15415,6 +15592,7 @@ export {
15415
15592
  withVoiceIntegrationEventId,
15416
15593
  voiceTelephonyOutcomeToRouteResult,
15417
15594
  voice,
15595
+ verifyVoiceTwilioWebhookSignature,
15418
15596
  verifyVoiceOpsWebhookSignature,
15419
15597
  validateVoiceWorkflowRouteResult,
15420
15598
  transcodeTwilioInboundPayloadToPCM16,
@@ -15437,6 +15615,7 @@ export {
15437
15615
  summarizeVoiceAssistantHealth,
15438
15616
  summarizeVoiceAppKitStatus,
15439
15617
  startVoiceOpsTask,
15618
+ signVoiceTwilioWebhook,
15440
15619
  shapeTelephonyAssistantText,
15441
15620
  selectVoiceTraceEventsForPrune,
15442
15621
  runVoiceToolContractSuite,
@@ -15657,6 +15836,7 @@ export {
15657
15836
  createRiskyTurnCorrectionHandler,
15658
15837
  createPhraseHintCorrectionHandler,
15659
15838
  createOpenAIVoiceAssistantModel,
15839
+ createMemoryVoiceTelephonyWebhookIdempotencyStore,
15660
15840
  createJSONVoiceAssistantModel,
15661
15841
  createId,
15662
15842
  createGeminiVoiceAssistantModel,
@@ -55,10 +55,26 @@ export type VoiceTelephonyWebhookParseInput = {
55
55
  export type VoiceTelephonyWebhookDecision<TResult = unknown> = {
56
56
  applied: boolean;
57
57
  decision: VoiceTelephonyOutcomeDecision;
58
+ duplicate?: boolean;
58
59
  event: VoiceTelephonyOutcomeProviderEvent;
60
+ idempotencyKey?: string;
59
61
  routeResult: VoiceTelephonyOutcomeRouteResult<TResult>;
60
62
  sessionId?: string;
61
63
  };
64
+ export type StoredVoiceTelephonyWebhookDecision<TResult = unknown> = VoiceTelephonyWebhookDecision<TResult> & {
65
+ createdAt: number;
66
+ updatedAt: number;
67
+ };
68
+ export type VoiceTelephonyWebhookIdempotencyStore<TResult = unknown> = {
69
+ get: (key: string) => Promise<StoredVoiceTelephonyWebhookDecision<TResult> | undefined> | StoredVoiceTelephonyWebhookDecision<TResult> | undefined;
70
+ set: (key: string, decision: StoredVoiceTelephonyWebhookDecision<TResult>) => Promise<void> | void;
71
+ };
72
+ export type VoiceTelephonyWebhookVerificationResult = {
73
+ ok: true;
74
+ } | {
75
+ ok: false;
76
+ reason: 'invalid-signature' | 'missing-secret' | 'missing-signature' | 'unsupported-provider';
77
+ };
62
78
  export type VoiceTelephonyWebhookHandlerOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = {
63
79
  apply?: boolean | ((input: VoiceTelephonyWebhookDecision<TResult>) => boolean);
64
80
  context?: TContext;
@@ -69,6 +85,18 @@ export type VoiceTelephonyWebhookHandlerOptions<TContext = unknown, TSession ext
69
85
  request: Request;
70
86
  sessionId?: string;
71
87
  }) => Promise<VoiceSessionHandle<TContext, TSession, TResult> | undefined> | VoiceSessionHandle<TContext, TSession, TResult> | undefined;
88
+ idempotency?: {
89
+ enabled?: boolean;
90
+ key?: (input: {
91
+ body: unknown;
92
+ event: VoiceTelephonyOutcomeProviderEvent;
93
+ provider: VoiceTelephonyWebhookProvider;
94
+ query: Record<string, unknown>;
95
+ request: Request;
96
+ sessionId?: string;
97
+ }) => Promise<string | undefined> | string | undefined;
98
+ store?: VoiceTelephonyWebhookIdempotencyStore<TResult>;
99
+ };
72
100
  onDecision?: (input: VoiceTelephonyWebhookDecision<TResult> & {
73
101
  context: TContext;
74
102
  request: Request;
@@ -76,6 +104,7 @@ export type VoiceTelephonyWebhookHandlerOptions<TContext = unknown, TSession ext
76
104
  parse?: (input: VoiceTelephonyWebhookParseInput) => Promise<VoiceTelephonyOutcomeProviderEvent> | VoiceTelephonyOutcomeProviderEvent;
77
105
  policy?: VoiceTelephonyOutcomePolicy;
78
106
  provider?: VoiceTelephonyWebhookProvider;
107
+ requireVerification?: boolean;
79
108
  resolveSessionId?: (input: {
80
109
  body: unknown;
81
110
  event: VoiceTelephonyOutcomeProviderEvent;
@@ -87,15 +116,44 @@ export type VoiceTelephonyWebhookHandlerOptions<TContext = unknown, TSession ext
87
116
  event: VoiceTelephonyOutcomeProviderEvent;
88
117
  sessionId?: string;
89
118
  }) => Promise<TResult | undefined> | TResult | undefined);
119
+ signingSecret?: string;
120
+ verificationUrl?: string | ((input: {
121
+ query: Record<string, unknown>;
122
+ request: Request;
123
+ }) => string);
124
+ verify?: (input: {
125
+ body: unknown;
126
+ headers: Headers;
127
+ provider: VoiceTelephonyWebhookProvider;
128
+ query: Record<string, unknown>;
129
+ rawBody: string;
130
+ request: Request;
131
+ }) => Promise<VoiceTelephonyWebhookVerificationResult> | VoiceTelephonyWebhookVerificationResult;
90
132
  };
91
133
  export type VoiceTelephonyWebhookRoutesOptions<TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown> = VoiceTelephonyWebhookHandlerOptions<TContext, TSession, TResult> & {
92
134
  name?: string;
93
135
  path?: string;
94
136
  };
137
+ export declare class VoiceTelephonyWebhookVerificationError extends Error {
138
+ result: VoiceTelephonyWebhookVerificationResult;
139
+ constructor(result: VoiceTelephonyWebhookVerificationResult);
140
+ }
141
+ export declare const createMemoryVoiceTelephonyWebhookIdempotencyStore: <TResult = unknown>() => VoiceTelephonyWebhookIdempotencyStore<TResult>;
95
142
  export declare const createVoiceTelephonyOutcomePolicy: (policy?: VoiceTelephonyOutcomePolicy) => Required<Pick<VoiceTelephonyOutcomePolicy, "completedStatuses" | "escalationStatuses" | "failedAsNoAnswer" | "failedStatuses" | "includeProviderPayload" | "machineDetectionVoicemailValues" | "noAnswerOnZeroDuration" | "noAnswerSipCodes" | "noAnswerStatuses" | "transferStatuses" | "voicemailStatuses">> & VoiceTelephonyOutcomePolicy;
96
143
  export declare const resolveVoiceTelephonyOutcome: (event: VoiceTelephonyOutcomeProviderEvent, policyInput?: VoiceTelephonyOutcomePolicy) => VoiceTelephonyOutcomeDecision;
97
144
  export declare const voiceTelephonyOutcomeToRouteResult: <TResult = unknown>(decision: VoiceTelephonyOutcomeDecision, result?: TResult) => VoiceTelephonyOutcomeRouteResult<TResult>;
98
145
  export declare const applyVoiceTelephonyOutcome: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(api: VoiceSessionHandle<TContext, TSession, TResult>, decision: VoiceTelephonyOutcomeDecision, result?: TResult) => Promise<void>;
146
+ export declare const signVoiceTwilioWebhook: (input: {
147
+ authToken: string;
148
+ body?: unknown;
149
+ url: string;
150
+ }) => Promise<string>;
151
+ export declare const verifyVoiceTwilioWebhookSignature: (input: {
152
+ authToken?: string;
153
+ body?: unknown;
154
+ headers: Headers;
155
+ url: string;
156
+ }) => Promise<VoiceTelephonyWebhookVerificationResult>;
99
157
  export declare const parseVoiceTelephonyWebhookEvent: (input: VoiceTelephonyWebhookParseInput) => VoiceTelephonyOutcomeProviderEvent;
100
158
  export declare const createVoiceTelephonyWebhookHandler: <TContext = unknown, TSession extends VoiceSessionRecord = VoiceSessionRecord, TResult = unknown>(options?: VoiceTelephonyWebhookHandlerOptions<TContext, TSession, TResult>) => (input: {
101
159
  query?: Record<string, unknown>;
@@ -124,7 +182,7 @@ export declare const createVoiceTelephonyWebhookRoutes: <TContext = unknown, TSe
124
182
  query: unknown;
125
183
  headers: unknown;
126
184
  response: {
127
- 200: VoiceTelephonyWebhookDecision<TResult>;
185
+ 200: Response | VoiceTelephonyWebhookDecision<TResult>;
128
186
  };
129
187
  };
130
188
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.70",
3
+ "version": "0.0.22-beta.72",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",