@absolutejs/voice 0.0.22-beta.24 → 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
@@ -8872,6 +8872,167 @@ 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 ({ 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
+ };
8875
9036
  // src/queue.ts
8876
9037
  var releaseLeaseScript = `
8877
9038
  if redis.call("GET", KEYS[1]) == ARGV[1] then
@@ -10837,6 +10998,7 @@ export {
10837
10998
  withVoiceOpsTaskId,
10838
10999
  withVoiceIntegrationEventId,
10839
11000
  voice,
11001
+ verifyVoiceOpsWebhookSignature,
10840
11002
  transcodeTwilioInboundPayloadToPCM16,
10841
11003
  transcodePCMToTwilioOutboundPayload,
10842
11004
  summarizeVoiceTraceSinkDeliveries,
@@ -10943,6 +11105,9 @@ export {
10943
11105
  createVoicePostgresReviewStore,
10944
11106
  createVoicePostgresIntegrationEventStore,
10945
11107
  createVoicePostgresExternalObjectMapStore,
11108
+ createVoiceOpsWebhookSink,
11109
+ createVoiceOpsWebhookReceiverRoutes,
11110
+ createVoiceOpsWebhookEnvelope,
10946
11111
  createVoiceOpsTaskWorker,
10947
11112
  createVoiceOpsTaskProcessorWorkerLoop,
10948
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.24",
3
+ "version": "0.0.22-beta.25",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",