@absolutejs/voice 0.0.22-beta.24 → 0.0.22-beta.26

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
@@ -8872,6 +8872,169 @@ var createVoiceMemoryStore = () => {
8872
8872
  };
8873
8873
  return { get, getOrCreate, list, remove, set };
8874
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 ({ body, request, set }) => {
9007
+ const bodyText = typeof body === "string" ? body : JSON.stringify(body);
9008
+ if (options.signingSecret) {
9009
+ const verification = await verifyVoiceOpsWebhookSignature({
9010
+ body: bodyText,
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(bodyText);
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
+ parse: "text"
9036
+ });
9037
+ };
8875
9038
  // src/queue.ts
8876
9039
  var releaseLeaseScript = `
8877
9040
  if redis.call("GET", KEYS[1]) == ARGV[1] then
@@ -10837,6 +11000,7 @@ export {
10837
11000
  withVoiceOpsTaskId,
10838
11001
  withVoiceIntegrationEventId,
10839
11002
  voice,
11003
+ verifyVoiceOpsWebhookSignature,
10840
11004
  transcodeTwilioInboundPayloadToPCM16,
10841
11005
  transcodePCMToTwilioOutboundPayload,
10842
11006
  summarizeVoiceTraceSinkDeliveries,
@@ -10943,6 +11107,9 @@ export {
10943
11107
  createVoicePostgresReviewStore,
10944
11108
  createVoicePostgresIntegrationEventStore,
10945
11109
  createVoicePostgresExternalObjectMapStore,
11110
+ createVoiceOpsWebhookSink,
11111
+ createVoiceOpsWebhookReceiverRoutes,
11112
+ createVoiceOpsWebhookEnvelope,
10946
11113
  createVoiceOpsTaskWorker,
10947
11114
  createVoiceOpsTaskProcessorWorkerLoop,
10948
11115
  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.24",
3
+ "version": "0.0.22-beta.26",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",