@absolutejs/voice 0.0.22-beta.484 → 0.0.22-beta.486

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
@@ -80,6 +80,10 @@ export type { CreateVoiceCostAccountantOptions, VoiceCostAccountant, VoiceCostBr
80
80
  export { describeVoiceAssistantMode, resolveVoiceAssistantMode, } from "./assistantMode";
81
81
  export type { VoiceAssistantMode, VoiceAssistantModality, VoiceAssistantModeDescriptor, VoiceSemanticVADConfig, } from "./assistantMode";
82
82
  export { createPunctuationSemanticTurnDetector, createRegexSemanticTurnDetector, } from "./semanticTurn";
83
+ export { VOICE_WEBHOOK_SIGNATURE_HEADER, VOICE_WEBHOOK_TIMESTAMP_HEADER, extractVoiceWebhookSignatureFromHeaders, signVoiceWebhookBody, verifyVoiceWebhookSignature, } from "./webhookVerification";
84
+ export { aggregateVoiceTurnLatencySpans, buildOTELSpanId, buildOTELTraceId, buildVoiceOTELPayload, createVoiceOTELHTTPExporter, } from "./otelExporter";
85
+ export type { VoiceOTELAttribute, VoiceOTELExporter, VoiceOTELExporterOptions, VoiceOTELPayload, VoiceOTELResourceSpans, VoiceOTELSpan, VoiceTurnLatencySpanSet, VoiceTurnLatencySpanStage, } from "./otelExporter";
86
+ export type { VoiceWebhookVerificationInput, VoiceWebhookVerificationReason, VoiceWebhookVerificationResult, } from "./webhookVerification";
83
87
  export type { CreatePunctuationSemanticTurnDetectorOptions, CreateRegexSemanticTurnDetectorOptions, VoiceSemanticTurnDetector, VoiceSemanticTurnInput, VoiceSemanticTurnVerdict, } from "./semanticTurn";
84
88
  export { createMonologueAMDDetector } from "./amdDetector";
85
89
  export type { MonologueAMDDetectorOptions, VoiceAMDDetector, VoiceAMDDetectorInput, VoiceAMDVerdict, } from "./amdDetector";
package/dist/index.js CHANGED
@@ -35404,6 +35404,214 @@ var createRegexSemanticTurnDetector = (options) => {
35404
35404
  }
35405
35405
  };
35406
35406
  };
35407
+ // src/webhookVerification.ts
35408
+ var VOICE_WEBHOOK_SIGNATURE_HEADER = "x-absolutejs-signature";
35409
+ var VOICE_WEBHOOK_TIMESTAMP_HEADER = "x-absolutejs-timestamp";
35410
+ var toHex6 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
35411
+ var timingSafeEqual3 = (left, right) => {
35412
+ if (left.length !== right.length) {
35413
+ return false;
35414
+ }
35415
+ let result = 0;
35416
+ for (let index = 0;index < left.length; index += 1) {
35417
+ result |= left.charCodeAt(index) ^ right.charCodeAt(index);
35418
+ }
35419
+ return result === 0;
35420
+ };
35421
+ var computeSignature = async (input) => {
35422
+ const encoder2 = new TextEncoder;
35423
+ const key = await crypto.subtle.importKey("raw", encoder2.encode(input.secret), { hash: "SHA-256", name: "HMAC" }, false, ["sign"]);
35424
+ const payload = encoder2.encode(`${input.timestamp}.${input.body}`);
35425
+ const signature = await crypto.subtle.sign("HMAC", key, payload);
35426
+ return `sha256=${toHex6(new Uint8Array(signature))}`;
35427
+ };
35428
+ var signVoiceWebhookBody = async (input) => computeSignature(input);
35429
+ var verifyVoiceWebhookSignature = async (input) => {
35430
+ if (!input.secret) {
35431
+ return { ok: false, reason: "missing-secret" };
35432
+ }
35433
+ if (!input.signature) {
35434
+ return { ok: false, reason: "missing-signature" };
35435
+ }
35436
+ if (!input.signature.startsWith("sha256=")) {
35437
+ return { ok: false, reason: "unsupported-algorithm" };
35438
+ }
35439
+ if (!input.timestamp) {
35440
+ return { ok: false, reason: "missing-timestamp" };
35441
+ }
35442
+ const timestampMs = Number(input.timestamp);
35443
+ const toleranceMs = Math.max(0, input.toleranceMs ?? 5 * 60 * 1000);
35444
+ if (!Number.isFinite(timestampMs) || toleranceMs > 0 && Math.abs((input.now ?? Date.now()) - timestampMs) > toleranceMs) {
35445
+ return { ok: false, reason: "stale-timestamp" };
35446
+ }
35447
+ const expected = await computeSignature({
35448
+ body: input.body,
35449
+ secret: input.secret,
35450
+ timestamp: input.timestamp
35451
+ });
35452
+ if (!timingSafeEqual3(expected, input.signature)) {
35453
+ return { ok: false, reason: "signature-mismatch" };
35454
+ }
35455
+ return { ok: true };
35456
+ };
35457
+ var extractVoiceWebhookSignatureFromHeaders = (headers) => {
35458
+ const get = (name) => {
35459
+ if (headers instanceof Headers) {
35460
+ return headers.get(name);
35461
+ }
35462
+ const lowerTarget = name.toLowerCase();
35463
+ for (const [key, value] of Object.entries(headers)) {
35464
+ if (key.toLowerCase() === lowerTarget) {
35465
+ if (Array.isArray(value)) {
35466
+ return value[0] ?? null;
35467
+ }
35468
+ return value ?? null;
35469
+ }
35470
+ }
35471
+ return null;
35472
+ };
35473
+ return {
35474
+ signature: get(VOICE_WEBHOOK_SIGNATURE_HEADER),
35475
+ timestamp: get(VOICE_WEBHOOK_TIMESTAMP_HEADER)
35476
+ };
35477
+ };
35478
+ // src/otelExporter.ts
35479
+ var SCOPE_NAME = "@absolutejs/voice";
35480
+ var SPAN_KIND_INTERNAL = 1;
35481
+ var STATUS_OK = 1;
35482
+ var HEX_CHARS = "0123456789abcdef";
35483
+ var hashToHex = (input, length) => {
35484
+ let hash = 0xcbf29ce4_84222325n;
35485
+ const prime = 0x100000001_b3n;
35486
+ for (let index = 0;index < input.length; index += 1) {
35487
+ hash ^= BigInt(input.charCodeAt(index));
35488
+ hash = hash * prime & 0xffffffffffffffffn;
35489
+ }
35490
+ let hex = hash.toString(16).padStart(16, "0");
35491
+ while (hex.length < length) {
35492
+ let next = 0n;
35493
+ for (const ch of hex) {
35494
+ next = next * 31n + BigInt(HEX_CHARS.indexOf(ch)) & 0xffffffffffffffffn;
35495
+ }
35496
+ hex += next.toString(16).padStart(16, "0");
35497
+ }
35498
+ return hex.slice(0, length);
35499
+ };
35500
+ var buildOTELTraceId = (sessionId) => hashToHex(`voice-trace:${sessionId}`, 32);
35501
+ var buildOTELSpanId = (sessionId, suffix) => hashToHex(`voice-span:${sessionId}:${suffix}`, 16);
35502
+ var toUnixNano = (ms) => `${Math.trunc(ms * 1e6)}`;
35503
+ var stringAttr = (key, value) => ({
35504
+ key,
35505
+ value: { stringValue: value }
35506
+ });
35507
+ var aggregateVoiceTurnLatencySpans = (events) => {
35508
+ const byTurn = new Map;
35509
+ for (const event of events) {
35510
+ if (event.type !== "turn_latency.stage" || !event.turnId) {
35511
+ continue;
35512
+ }
35513
+ const stage = typeof event.payload?.stage === "string" ? event.payload.stage : undefined;
35514
+ if (!stage) {
35515
+ continue;
35516
+ }
35517
+ const key = `${event.sessionId}::${event.turnId}`;
35518
+ const existing = byTurn.get(key);
35519
+ if (!existing) {
35520
+ byTurn.set(key, {
35521
+ endedAt: event.at,
35522
+ scenarioId: event.scenarioId,
35523
+ sessionId: event.sessionId,
35524
+ stages: [{ at: event.at, stage }],
35525
+ startedAt: event.at,
35526
+ turnId: event.turnId
35527
+ });
35528
+ continue;
35529
+ }
35530
+ existing.stages.push({ at: event.at, stage });
35531
+ existing.startedAt = Math.min(existing.startedAt, event.at);
35532
+ existing.endedAt = Math.max(existing.endedAt, event.at);
35533
+ }
35534
+ return Array.from(byTurn.values()).sort((left, right) => left.startedAt - right.startedAt);
35535
+ };
35536
+ var buildVoiceOTELPayload = (spanSets, options = {}) => {
35537
+ const resourceAttributes = [
35538
+ stringAttr("service.name", options.serviceName ?? "absolutejs-voice")
35539
+ ];
35540
+ for (const [key, value] of Object.entries(options.resourceAttributes ?? {})) {
35541
+ resourceAttributes.push(stringAttr(key, value));
35542
+ }
35543
+ const spans = [];
35544
+ for (const set of spanSets) {
35545
+ const traceId = buildOTELTraceId(set.sessionId);
35546
+ const parentSpanId = buildOTELSpanId(set.sessionId, `turn:${set.turnId}`);
35547
+ spans.push({
35548
+ attributes: [
35549
+ stringAttr("voice.session_id", set.sessionId),
35550
+ stringAttr("voice.turn_id", set.turnId),
35551
+ ...set.scenarioId ? [stringAttr("voice.scenario_id", set.scenarioId)] : []
35552
+ ],
35553
+ endTimeUnixNano: toUnixNano(set.endedAt),
35554
+ kind: SPAN_KIND_INTERNAL,
35555
+ name: "voice.turn",
35556
+ spanId: parentSpanId,
35557
+ startTimeUnixNano: toUnixNano(set.startedAt),
35558
+ status: { code: STATUS_OK },
35559
+ traceId
35560
+ });
35561
+ for (let index = 0;index < set.stages.length; index += 1) {
35562
+ const stage = set.stages[index];
35563
+ const next = set.stages[index + 1];
35564
+ const endsAt = next ? next.at : set.endedAt;
35565
+ spans.push({
35566
+ attributes: [
35567
+ stringAttr("voice.session_id", set.sessionId),
35568
+ stringAttr("voice.turn_id", set.turnId),
35569
+ stringAttr("voice.stage", stage.stage)
35570
+ ],
35571
+ endTimeUnixNano: toUnixNano(endsAt),
35572
+ kind: SPAN_KIND_INTERNAL,
35573
+ name: `voice.turn.stage.${stage.stage}`,
35574
+ parentSpanId,
35575
+ spanId: buildOTELSpanId(set.sessionId, `${set.turnId}:${stage.stage}:${index}`),
35576
+ startTimeUnixNano: toUnixNano(stage.at),
35577
+ status: { code: STATUS_OK },
35578
+ traceId
35579
+ });
35580
+ }
35581
+ }
35582
+ return {
35583
+ resourceSpans: [
35584
+ {
35585
+ resource: { attributes: resourceAttributes },
35586
+ scopeSpans: [{ scope: { name: SCOPE_NAME }, spans }]
35587
+ }
35588
+ ]
35589
+ };
35590
+ };
35591
+ var createVoiceOTELHTTPExporter = (options) => {
35592
+ const fetchImpl = options.fetch ?? globalThis.fetch.bind(globalThis);
35593
+ return {
35594
+ export: async (events) => {
35595
+ const spanSets = aggregateVoiceTurnLatencySpans(events);
35596
+ if (spanSets.length === 0) {
35597
+ return { ok: true };
35598
+ }
35599
+ const payload = buildVoiceOTELPayload(spanSets, {
35600
+ resourceAttributes: options.resourceAttributes,
35601
+ serviceName: options.serviceName
35602
+ });
35603
+ const response = await fetchImpl(options.url, {
35604
+ body: JSON.stringify(payload),
35605
+ headers: {
35606
+ "content-type": "application/json",
35607
+ ...options.headers
35608
+ },
35609
+ method: "POST"
35610
+ });
35611
+ return { ok: response.ok, status: response.status };
35612
+ }
35613
+ };
35614
+ };
35407
35615
  // src/amdDetector.ts
35408
35616
  var createMonologueAMDDetector = (options = {}) => {
35409
35617
  const minMonologueMs = options.minMonologueMs ?? 8000;
@@ -41951,7 +42159,7 @@ var createVoiceMemoryStore = () => {
41951
42159
  };
41952
42160
  // src/opsWebhook.ts
41953
42161
  import { Elysia as Elysia66 } from "elysia";
41954
- var toHex6 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
42162
+ var toHex7 = (bytes) => Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
41955
42163
  var signVoiceOpsWebhookBody = async (input) => {
41956
42164
  const encoder2 = new TextEncoder;
41957
42165
  const key = await crypto.subtle.importKey("raw", encoder2.encode(input.secret), {
@@ -41959,9 +42167,9 @@ var signVoiceOpsWebhookBody = async (input) => {
41959
42167
  name: "HMAC"
41960
42168
  }, false, ["sign"]);
41961
42169
  const signature = await crypto.subtle.sign("HMAC", key, encoder2.encode(`${input.timestamp}.${input.body}`));
41962
- return `sha256=${toHex6(new Uint8Array(signature))}`;
42170
+ return `sha256=${toHex7(new Uint8Array(signature))}`;
41963
42171
  };
41964
- var timingSafeEqual3 = (left, right) => {
42172
+ var timingSafeEqual4 = (left, right) => {
41965
42173
  const encoder2 = new TextEncoder;
41966
42174
  const leftBytes = encoder2.encode(left);
41967
42175
  const rightBytes = encoder2.encode(right);
@@ -42068,7 +42276,7 @@ var verifyVoiceOpsWebhookSignature = async (input) => {
42068
42276
  secret: input.secret,
42069
42277
  timestamp: input.timestamp
42070
42278
  });
42071
- if (!timingSafeEqual3(expected, input.signature)) {
42279
+ if (!timingSafeEqual4(expected, input.signature)) {
42072
42280
  return {
42073
42281
  ok: false,
42074
42282
  reason: "invalid-signature"
@@ -45977,6 +46185,7 @@ export {
45977
46185
  voiceGuardrailPolicyPresets,
45978
46186
  voiceComplianceRedactionDefaults,
45979
46187
  voice,
46188
+ verifyVoiceWebhookSignature,
45980
46189
  verifyVoiceTwilioWebhookSignature,
45981
46190
  verifyVoiceTelnyxWebhookSignature,
45982
46191
  verifyVoicePlivoWebhookSignature,
@@ -46020,6 +46229,7 @@ export {
46020
46229
  summarizeVoiceAssistantRuns,
46021
46230
  summarizeVoiceAssistantHealth,
46022
46231
  startVoiceOpsTask,
46232
+ signVoiceWebhookBody,
46023
46233
  signVoiceTwilioWebhook,
46024
46234
  signVoicePlivoWebhook,
46025
46235
  shapeTelephonyAssistantText,
@@ -46219,6 +46429,7 @@ export {
46219
46429
  filterVoiceAuditEvents,
46220
46430
  fetchVoiceProofTarget,
46221
46431
  failVoiceOpsTask,
46432
+ extractVoiceWebhookSignatureFromHeaders,
46222
46433
  extractVoiceMediaPipelineIssueEntries,
46223
46434
  exportVoiceTrace,
46224
46435
  exportVoiceAuditTrail,
@@ -46466,6 +46677,7 @@ export {
46466
46677
  createVoiceObservabilityExportSchema,
46467
46678
  createVoiceObservabilityExportRoutes,
46468
46679
  createVoiceObservabilityExportReplayRoutes,
46680
+ createVoiceOTELHTTPExporter,
46469
46681
  createVoiceMonitorWebhookNotifier,
46470
46682
  createVoiceMonitorSession,
46471
46683
  createVoiceMonitorRuntimeBinding,
@@ -46684,6 +46896,7 @@ export {
46684
46896
  buildVoiceObservabilityExportDeliveryHistory,
46685
46897
  buildVoiceObservabilityExport,
46686
46898
  buildVoiceObservabilityArtifactIndex,
46899
+ buildVoiceOTELPayload,
46687
46900
  buildVoiceMultilingualProofReadinessCheck,
46688
46901
  buildVoiceMonitorRunReport,
46689
46902
  buildVoiceMonitorPlan,
@@ -46714,6 +46927,8 @@ export {
46714
46927
  buildVoiceAuditTrailReport,
46715
46928
  buildVoiceAuditExport,
46716
46929
  buildVoiceAuditDeliveryReport,
46930
+ buildOTELTraceId,
46931
+ buildOTELSpanId,
46717
46932
  buildEmptyVoiceProofTrendReport,
46718
46933
  assignVoiceOpsTask,
46719
46934
  assertVoiceToolContractEvidence,
@@ -46764,7 +46979,10 @@ export {
46764
46979
  appendVoiceRealCallProfileRecoveryEvidence,
46765
46980
  appendVoiceProviderRouterTraceEvent,
46766
46981
  appendVoiceIOProviderRouterTraceEvent,
46982
+ aggregateVoiceTurnLatencySpans,
46767
46983
  acknowledgeVoiceMonitorIssue,
46984
+ VOICE_WEBHOOK_TIMESTAMP_HEADER,
46985
+ VOICE_WEBHOOK_SIGNATURE_HEADER,
46768
46986
  VOICE_LIVE_OPS_ACTIONS,
46769
46987
  TURN_PROFILE_DEFAULTS,
46770
46988
  DEFAULT_VOICE_REDACTION_PATTERNS,
@@ -0,0 +1,83 @@
1
+ import type { StoredVoiceTraceEvent } from "./trace";
2
+ export type VoiceOTELAttribute = {
3
+ key: string;
4
+ value: {
5
+ boolValue: boolean;
6
+ };
7
+ } | {
8
+ key: string;
9
+ value: {
10
+ doubleValue: number;
11
+ };
12
+ } | {
13
+ key: string;
14
+ value: {
15
+ intValue: string;
16
+ };
17
+ } | {
18
+ key: string;
19
+ value: {
20
+ stringValue: string;
21
+ };
22
+ };
23
+ export type VoiceOTELSpan = {
24
+ attributes: VoiceOTELAttribute[];
25
+ endTimeUnixNano: string;
26
+ kind: number;
27
+ name: string;
28
+ parentSpanId?: string;
29
+ spanId: string;
30
+ startTimeUnixNano: string;
31
+ status: {
32
+ code: number;
33
+ };
34
+ traceId: string;
35
+ };
36
+ export type VoiceOTELResourceSpans = {
37
+ resource: {
38
+ attributes: VoiceOTELAttribute[];
39
+ };
40
+ scopeSpans: ReadonlyArray<{
41
+ scope: {
42
+ name: string;
43
+ version?: string;
44
+ };
45
+ spans: VoiceOTELSpan[];
46
+ }>;
47
+ };
48
+ export type VoiceOTELPayload = {
49
+ resourceSpans: VoiceOTELResourceSpans[];
50
+ };
51
+ export type VoiceTurnLatencySpanStage = {
52
+ at: number;
53
+ stage: string;
54
+ };
55
+ export type VoiceTurnLatencySpanSet = {
56
+ endedAt: number;
57
+ scenarioId?: string;
58
+ sessionId: string;
59
+ stages: VoiceTurnLatencySpanStage[];
60
+ startedAt: number;
61
+ turnId: string;
62
+ };
63
+ export declare const buildOTELTraceId: (sessionId: string) => string;
64
+ export declare const buildOTELSpanId: (sessionId: string, suffix: string) => string;
65
+ export declare const aggregateVoiceTurnLatencySpans: (events: StoredVoiceTraceEvent[]) => VoiceTurnLatencySpanSet[];
66
+ export type VoiceOTELExporterOptions = {
67
+ fetch?: typeof fetch;
68
+ headers?: Record<string, string>;
69
+ resourceAttributes?: Record<string, string>;
70
+ serviceName?: string;
71
+ url: string;
72
+ };
73
+ export declare const buildVoiceOTELPayload: (spanSets: VoiceTurnLatencySpanSet[], options?: {
74
+ resourceAttributes?: Record<string, string>;
75
+ serviceName?: string;
76
+ }) => VoiceOTELPayload;
77
+ export type VoiceOTELExporter = {
78
+ export: (events: StoredVoiceTraceEvent[]) => Promise<{
79
+ ok: boolean;
80
+ status?: number;
81
+ }>;
82
+ };
83
+ export declare const createVoiceOTELHTTPExporter: (options: VoiceOTELExporterOptions) => VoiceOTELExporter;
@@ -0,0 +1,27 @@
1
+ export type VoiceWebhookVerificationReason = "missing-secret" | "missing-signature" | "missing-timestamp" | "signature-mismatch" | "stale-timestamp" | "unsupported-algorithm";
2
+ export type VoiceWebhookVerificationResult = {
3
+ ok: true;
4
+ } | {
5
+ ok: false;
6
+ reason: VoiceWebhookVerificationReason;
7
+ };
8
+ export type VoiceWebhookVerificationInput = {
9
+ body: string;
10
+ now?: number;
11
+ secret?: string;
12
+ signature?: string | null;
13
+ timestamp?: string | null;
14
+ toleranceMs?: number;
15
+ };
16
+ export declare const VOICE_WEBHOOK_SIGNATURE_HEADER = "x-absolutejs-signature";
17
+ export declare const VOICE_WEBHOOK_TIMESTAMP_HEADER = "x-absolutejs-timestamp";
18
+ export declare const signVoiceWebhookBody: (input: {
19
+ body: string;
20
+ secret: string;
21
+ timestamp: string;
22
+ }) => Promise<string>;
23
+ export declare const verifyVoiceWebhookSignature: (input: VoiceWebhookVerificationInput) => Promise<VoiceWebhookVerificationResult>;
24
+ export declare const extractVoiceWebhookSignatureFromHeaders: (headers: Headers | Record<string, string | string[] | undefined>) => {
25
+ signature: string | null;
26
+ timestamp: string | null;
27
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@absolutejs/voice",
3
- "version": "0.0.22-beta.484",
3
+ "version": "0.0.22-beta.486",
4
4
  "description": "Voice primitives and Elysia plugin for AbsoluteJS",
5
5
  "repository": {
6
6
  "type": "git",