@absolutejs/voice 0.0.22-beta.23 → 0.0.22-beta.25

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
@@ -13,6 +13,7 @@ export { createVoicePostgresExternalObjectMapStore, createVoicePostgresIntegrati
13
13
  export { createVoiceS3ReviewStore } from './s3Store';
14
14
  export { createVoiceMemoryStore } from './memoryStore';
15
15
  export { createVoiceCRMActivitySink, createVoiceHelpdeskTicketSink, createVoiceIntegrationHTTPSink, createVoiceHubSpotTaskSink, createVoiceHubSpotTaskSyncSinks, createVoiceHubSpotTaskUpdateSink, createVoiceLinearIssueSink, createVoiceLinearIssueSyncSinks, createVoiceLinearIssueUpdateSink, createVoiceZendeskTicketSink, createVoiceZendeskTicketSyncSinks, createVoiceZendeskTicketUpdateSink, deliverVoiceIntegrationEventToSinks } from './opsSinks';
16
+ export { createVoiceOpsWebhookEnvelope, createVoiceOpsWebhookReceiverRoutes, createVoiceOpsWebhookSink, verifyVoiceOpsWebhookSignature } from './opsWebhook';
16
17
  export { createVoiceIntegrationSinkWorker, createVoiceIntegrationSinkWorkerLoop, createVoiceOpsTaskWorker, createVoiceOpsTaskProcessorWorker, createVoiceOpsTaskProcessorWorkerLoop, createVoiceRedisIdempotencyStore, createVoiceRedisTaskLeaseCoordinator, createVoiceTraceSinkDeliveryWorker, createVoiceTraceSinkDeliveryWorkerLoop, createVoiceWebhookDeliveryWorker, createVoiceWebhookDeliveryWorkerLoop, summarizeVoiceTraceSinkDeliveries, summarizeVoiceOpsTaskQueue, summarizeVoiceIntegrationEvents } from './queue';
17
18
  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';
18
19
  export { createVoiceSession } from './session';
@@ -38,6 +39,7 @@ export type { VoiceOpsRuntime, VoiceOpsRuntimeConfig, VoiceOpsRuntimeSummary, Vo
38
39
  export type { VoiceOpsPresetName, VoiceOpsPresetOverrides, VoiceResolvedOpsPreset } from './opsPresets';
39
40
  export type { VoiceOutcomeRecipe, VoiceOutcomeRecipeName, VoiceOutcomeRecipeOptions } from './outcomeRecipes';
40
41
  export type { VoiceCRMActivitySinkOptions, VoiceHubSpotTaskSinkOptions, VoiceHubSpotTaskUpdateSinkOptions, VoiceHelpdeskTicketSinkOptions, VoiceIntegrationHTTPSinkOptions, VoiceIntegrationSink, VoiceIntegrationSinkDeliveryResult, VoiceLinearIssueSinkOptions, VoiceLinearIssueUpdateSinkOptions, VoiceZendeskTicketSinkOptions, VoiceZendeskTicketUpdateSinkOptions } from './opsSinks';
42
+ export type { VoiceOpsWebhookEnvelope, VoiceOpsWebhookEntity, VoiceOpsWebhookLinkResolver, VoiceOpsWebhookReceiverRoutesOptions, VoiceOpsWebhookSinkOptions, VoiceOpsWebhookVerificationResult } from './opsWebhook';
41
43
  export type { StoredVoiceCallReviewArtifact, VoiceCallReviewArtifact, VoiceCallReviewConfig, VoiceCallReviewPostCallSummary, VoiceCallReviewRecorder, VoiceCallReviewRecorderOptions, VoiceCallReviewStore, VoiceCallReviewSummary, VoiceCallReviewTimelineEvent } from './testing/review';
42
44
  export type { VoiceFileRuntimeStorage, VoiceFileStoreOptions } from './fileStore';
43
45
  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';
package/dist/index.js CHANGED
@@ -7167,12 +7167,14 @@ var summarizeVoiceSessions = async (options = {}) => {
7167
7167
  const providerErrors = {};
7168
7168
  const providers = new Set;
7169
7169
  let latestOutcome;
7170
+ let errorCount = 0;
7170
7171
  for (const event of sorted) {
7171
7172
  const provider = getString3(event.payload.provider);
7172
7173
  if (provider) {
7173
7174
  providers.add(provider);
7174
7175
  }
7175
7176
  if (event.type === "session.error" && (event.payload.providerStatus === "error" || typeof event.payload.error === "string")) {
7177
+ errorCount += 1;
7176
7178
  increment2(providerErrors, provider ?? "unknown");
7177
7179
  }
7178
7180
  const outcome = getString3(event.payload.outcome);
@@ -7182,14 +7184,14 @@ var summarizeVoiceSessions = async (options = {}) => {
7182
7184
  }
7183
7185
  const item = {
7184
7186
  endedAt: summary.endedAt,
7185
- errorCount: summary.errorCount,
7187
+ errorCount,
7186
7188
  eventCount: summary.eventCount,
7187
7189
  latestOutcome,
7188
7190
  providerErrors,
7189
7191
  providers: [...providers].sort(),
7190
7192
  sessionId,
7191
7193
  startedAt: summary.startedAt,
7192
- status: summary.failed ? "failed" : "healthy",
7194
+ status: errorCount > 0 ? "failed" : "healthy",
7193
7195
  transcriptCount: summary.transcriptCount,
7194
7196
  turnCount: summary.turnCount
7195
7197
  };
@@ -8870,6 +8872,167 @@ var createVoiceMemoryStore = () => {
8870
8872
  };
8871
8873
  return { get, getOrCreate, list, remove, set };
8872
8874
  };
8875
+ // src/opsWebhook.ts
8876
+ import { Elysia as Elysia5 } from "elysia";
8877
+ var toHex4 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
8878
+ var signVoiceOpsWebhookBody = async (input) => {
8879
+ const encoder = new TextEncoder;
8880
+ const key = await crypto.subtle.importKey("raw", encoder.encode(input.secret), {
8881
+ hash: "SHA-256",
8882
+ name: "HMAC"
8883
+ }, false, ["sign"]);
8884
+ const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(`${input.timestamp}.${input.body}`));
8885
+ return `sha256=${toHex4(new Uint8Array(signature))}`;
8886
+ };
8887
+ var timingSafeEqual = (left, right) => {
8888
+ const encoder = new TextEncoder;
8889
+ const leftBytes = encoder.encode(left);
8890
+ const rightBytes = encoder.encode(right);
8891
+ if (leftBytes.length !== rightBytes.length) {
8892
+ return false;
8893
+ }
8894
+ let diff = 0;
8895
+ for (let index = 0;index < leftBytes.length; index += 1) {
8896
+ diff |= leftBytes[index] ^ rightBytes[index];
8897
+ }
8898
+ return diff === 0;
8899
+ };
8900
+ var resolveWebhookLink = async (resolver, event) => {
8901
+ if (typeof resolver === "function") {
8902
+ return resolver({
8903
+ event
8904
+ });
8905
+ }
8906
+ return resolver;
8907
+ };
8908
+ var joinBaseUrl = (baseUrl, path) => `${baseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
8909
+ var asString = (value) => typeof value === "string" && value.length > 0 ? value : undefined;
8910
+ var buildVoiceOpsWebhookEntity = (event) => ({
8911
+ disposition: asString(event.payload.disposition),
8912
+ outcome: asString(event.payload.outcome),
8913
+ priority: asString(event.payload.priority),
8914
+ queue: asString(event.payload.queue),
8915
+ reviewId: asString(event.payload.reviewId),
8916
+ scenarioId: asString(event.payload.scenarioId),
8917
+ sessionId: asString(event.payload.sessionId),
8918
+ status: asString(event.payload.status),
8919
+ target: asString(event.payload.target),
8920
+ taskId: asString(event.payload.taskId)
8921
+ });
8922
+ var createVoiceOpsWebhookEnvelope = async (input) => {
8923
+ const entity = buildVoiceOpsWebhookEntity(input.event);
8924
+ const replayHref = await resolveWebhookLink(input.replayHref, input.event) ?? (input.baseUrl && entity.sessionId ? joinBaseUrl(input.baseUrl, `/api/voice-sessions/${encodeURIComponent(entity.sessionId)}/replay`) : undefined);
8925
+ const links = {
8926
+ event: await resolveWebhookLink(input.eventHref, input.event),
8927
+ replay: replayHref,
8928
+ review: await resolveWebhookLink(input.reviewHref, input.event),
8929
+ task: await resolveWebhookLink(input.taskHref, input.event)
8930
+ };
8931
+ return {
8932
+ entity,
8933
+ event: {
8934
+ createdAt: input.event.createdAt,
8935
+ id: input.event.id,
8936
+ payload: input.event.payload,
8937
+ type: input.event.type
8938
+ },
8939
+ links: links.event || links.replay || links.review || links.task ? links : undefined,
8940
+ schemaVersion: 1,
8941
+ source: "absolutejs-voice"
8942
+ };
8943
+ };
8944
+ var createVoiceOpsWebhookSink = (options) => createVoiceIntegrationHTTPSink({
8945
+ ...options,
8946
+ body: ({ event }) => createVoiceOpsWebhookEnvelope({
8947
+ baseUrl: options.baseUrl,
8948
+ event,
8949
+ eventHref: options.eventHref,
8950
+ replayHref: options.replayHref,
8951
+ reviewHref: options.reviewHref,
8952
+ taskHref: options.taskHref
8953
+ }),
8954
+ kind: options.kind ?? "ops-webhook"
8955
+ });
8956
+ var verifyVoiceOpsWebhookSignature = async (input) => {
8957
+ if (!input.secret) {
8958
+ return {
8959
+ ok: false,
8960
+ reason: "missing-secret"
8961
+ };
8962
+ }
8963
+ if (!input.signature) {
8964
+ return {
8965
+ ok: false,
8966
+ reason: "missing-signature"
8967
+ };
8968
+ }
8969
+ if (!input.signature.startsWith("sha256=")) {
8970
+ return {
8971
+ ok: false,
8972
+ reason: "unsupported-algorithm"
8973
+ };
8974
+ }
8975
+ if (!input.timestamp) {
8976
+ return {
8977
+ ok: false,
8978
+ reason: "missing-timestamp"
8979
+ };
8980
+ }
8981
+ const timestampMs = Number(input.timestamp);
8982
+ const toleranceMs = Math.max(0, input.toleranceMs ?? 5 * 60 * 1000);
8983
+ if (!Number.isFinite(timestampMs) || toleranceMs > 0 && Math.abs((input.now ?? Date.now()) - timestampMs) > toleranceMs) {
8984
+ return {
8985
+ ok: false,
8986
+ reason: "stale-timestamp"
8987
+ };
8988
+ }
8989
+ const expected = await signVoiceOpsWebhookBody({
8990
+ body: input.body,
8991
+ secret: input.secret,
8992
+ timestamp: input.timestamp
8993
+ });
8994
+ if (!timingSafeEqual(expected, input.signature)) {
8995
+ return {
8996
+ ok: false,
8997
+ reason: "invalid-signature"
8998
+ };
8999
+ }
9000
+ return {
9001
+ ok: true
9002
+ };
9003
+ };
9004
+ var createVoiceOpsWebhookReceiverRoutes = (options = {}) => {
9005
+ const path = options.path ?? "/api/voice-ops/webhook";
9006
+ return new Elysia5().post(path, async ({ request, set }) => {
9007
+ const body = await request.text();
9008
+ if (options.signingSecret) {
9009
+ const verification = await verifyVoiceOpsWebhookSignature({
9010
+ body,
9011
+ secret: options.signingSecret,
9012
+ signature: request.headers.get("x-absolutejs-signature"),
9013
+ timestamp: request.headers.get("x-absolutejs-timestamp"),
9014
+ toleranceMs: options.toleranceMs
9015
+ });
9016
+ if (!verification.ok) {
9017
+ set.status = 401;
9018
+ return {
9019
+ ok: false,
9020
+ reason: verification.reason
9021
+ };
9022
+ }
9023
+ }
9024
+ const envelope = JSON.parse(body);
9025
+ await options.onEnvelope?.({
9026
+ envelope,
9027
+ request
9028
+ });
9029
+ return {
9030
+ eventId: envelope.event?.id,
9031
+ ok: true,
9032
+ type: envelope.event?.type
9033
+ };
9034
+ });
9035
+ };
8873
9036
  // src/queue.ts
8874
9037
  var releaseLeaseScript = `
8875
9038
  if redis.call("GET", KEYS[1]) == ARGV[1] then
@@ -10835,6 +10998,7 @@ export {
10835
10998
  withVoiceOpsTaskId,
10836
10999
  withVoiceIntegrationEventId,
10837
11000
  voice,
11001
+ verifyVoiceOpsWebhookSignature,
10838
11002
  transcodeTwilioInboundPayloadToPCM16,
10839
11003
  transcodePCMToTwilioOutboundPayload,
10840
11004
  summarizeVoiceTraceSinkDeliveries,
@@ -10941,6 +11105,9 @@ export {
10941
11105
  createVoicePostgresReviewStore,
10942
11106
  createVoicePostgresIntegrationEventStore,
10943
11107
  createVoicePostgresExternalObjectMapStore,
11108
+ createVoiceOpsWebhookSink,
11109
+ createVoiceOpsWebhookReceiverRoutes,
11110
+ createVoiceOpsWebhookEnvelope,
10944
11111
  createVoiceOpsTaskWorker,
10945
11112
  createVoiceOpsTaskProcessorWorkerLoop,
10946
11113
  createVoiceOpsTaskProcessorWorker,
@@ -0,0 +1,126 @@
1
+ import { Elysia } from 'elysia';
2
+ import type { StoredVoiceIntegrationEvent, VoiceIntegrationEventType } from './ops';
3
+ import type { VoiceIntegrationHTTPSinkOptions, VoiceIntegrationSink } from './opsSinks';
4
+ type MaybePromise<T> = T | Promise<T>;
5
+ export type VoiceOpsWebhookLinkResolver = string | ((input: {
6
+ event: StoredVoiceIntegrationEvent;
7
+ }) => MaybePromise<string | undefined>);
8
+ export type VoiceOpsWebhookEntity = {
9
+ disposition?: string;
10
+ outcome?: string;
11
+ priority?: string;
12
+ queue?: string;
13
+ reviewId?: string;
14
+ scenarioId?: string;
15
+ sessionId?: string;
16
+ status?: string;
17
+ target?: string;
18
+ taskId?: string;
19
+ };
20
+ export type VoiceOpsWebhookEnvelope = {
21
+ entity: VoiceOpsWebhookEntity;
22
+ event: {
23
+ createdAt: number;
24
+ id: string;
25
+ payload: Record<string, unknown>;
26
+ type: VoiceIntegrationEventType;
27
+ };
28
+ links?: {
29
+ event?: string;
30
+ replay?: string;
31
+ review?: string;
32
+ task?: string;
33
+ };
34
+ schemaVersion: 1;
35
+ source: 'absolutejs-voice';
36
+ };
37
+ export type VoiceOpsWebhookSinkOptions = Omit<VoiceIntegrationHTTPSinkOptions<VoiceOpsWebhookEnvelope>, 'body'> & {
38
+ baseUrl?: string;
39
+ eventHref?: VoiceOpsWebhookLinkResolver;
40
+ replayHref?: VoiceOpsWebhookLinkResolver;
41
+ reviewHref?: VoiceOpsWebhookLinkResolver;
42
+ taskHref?: VoiceOpsWebhookLinkResolver;
43
+ };
44
+ export type VoiceOpsWebhookVerificationResult = {
45
+ ok: true;
46
+ } | {
47
+ ok: false;
48
+ reason: 'invalid-signature' | 'missing-secret' | 'missing-signature' | 'missing-timestamp' | 'stale-timestamp' | 'unsupported-algorithm';
49
+ };
50
+ export type VoiceOpsWebhookReceiverRoutesOptions = {
51
+ onEnvelope?: (input: {
52
+ envelope: VoiceOpsWebhookEnvelope;
53
+ request: Request;
54
+ }) => MaybePromise<void>;
55
+ path?: string;
56
+ signingSecret?: string;
57
+ toleranceMs?: number;
58
+ };
59
+ export declare const createVoiceOpsWebhookEnvelope: (input: {
60
+ baseUrl?: string;
61
+ event: StoredVoiceIntegrationEvent;
62
+ eventHref?: VoiceOpsWebhookLinkResolver;
63
+ replayHref?: VoiceOpsWebhookLinkResolver;
64
+ reviewHref?: VoiceOpsWebhookLinkResolver;
65
+ taskHref?: VoiceOpsWebhookLinkResolver;
66
+ }) => Promise<VoiceOpsWebhookEnvelope>;
67
+ export declare const createVoiceOpsWebhookSink: (options: VoiceOpsWebhookSinkOptions) => VoiceIntegrationSink;
68
+ export declare const verifyVoiceOpsWebhookSignature: (input: {
69
+ body: string;
70
+ now?: number;
71
+ secret?: string;
72
+ signature?: string | null;
73
+ timestamp?: string | null;
74
+ toleranceMs?: number;
75
+ }) => Promise<VoiceOpsWebhookVerificationResult>;
76
+ export declare const createVoiceOpsWebhookReceiverRoutes: (options?: VoiceOpsWebhookReceiverRoutesOptions) => Elysia<"", {
77
+ decorator: {};
78
+ store: {};
79
+ derive: {};
80
+ resolve: {};
81
+ }, {
82
+ typebox: {};
83
+ error: {};
84
+ }, {
85
+ schema: {};
86
+ standaloneSchema: {};
87
+ macro: {};
88
+ macroFn: {};
89
+ parser: {};
90
+ response: {};
91
+ }, {
92
+ [x: string]: {
93
+ post: {
94
+ body: unknown;
95
+ params: {};
96
+ query: unknown;
97
+ headers: unknown;
98
+ response: {
99
+ 200: {
100
+ ok: boolean;
101
+ reason: "invalid-signature" | "missing-secret" | "missing-signature" | "missing-timestamp" | "stale-timestamp" | "unsupported-algorithm";
102
+ eventId?: undefined;
103
+ type?: undefined;
104
+ } | {
105
+ eventId: string;
106
+ ok: boolean;
107
+ type: VoiceIntegrationEventType;
108
+ reason?: undefined;
109
+ };
110
+ };
111
+ };
112
+ };
113
+ }, {
114
+ derive: {};
115
+ resolve: {};
116
+ schema: {};
117
+ standaloneSchema: {};
118
+ response: {};
119
+ }, {
120
+ derive: {};
121
+ resolve: {};
122
+ schema: {};
123
+ standaloneSchema: {};
124
+ response: {};
125
+ }>;
126
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.23",
3
+ "version": "0.0.22-beta.25",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",