@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 +2 -0
- package/dist/index.js +167 -0
- package/dist/opsWebhook.d.ts +126 -0
- package/package.json +1 -1
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 {};
|