@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 +2 -0
- package/dist/index.js +165 -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,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 {};
|