@kuralle-syrinx/server-websocket 4.4.0 → 4.5.0

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.
Files changed (132) hide show
  1. package/dist/backchannel-cue-fixtures.d.ts +4 -0
  2. package/dist/backchannel-cue-fixtures.d.ts.map +1 -0
  3. package/dist/backchannel-cue-fixtures.js +25 -0
  4. package/dist/backchannel-cue-fixtures.js.map +1 -0
  5. package/dist/background-audio.d.ts +59 -0
  6. package/dist/background-audio.d.ts.map +1 -0
  7. package/dist/background-audio.js +248 -0
  8. package/dist/background-audio.js.map +1 -0
  9. package/dist/browser-opus.d.ts +12 -0
  10. package/dist/browser-opus.d.ts.map +1 -0
  11. package/dist/browser-opus.js +48 -0
  12. package/dist/browser-opus.js.map +1 -0
  13. package/dist/carrier-commands.d.ts +124 -0
  14. package/dist/carrier-commands.d.ts.map +1 -0
  15. package/dist/carrier-commands.js +193 -0
  16. package/dist/carrier-commands.js.map +1 -0
  17. package/dist/edge-telnyx.d.ts +37 -0
  18. package/dist/edge-telnyx.d.ts.map +1 -0
  19. package/dist/edge-telnyx.js +320 -0
  20. package/dist/edge-telnyx.js.map +1 -0
  21. package/dist/edge-twilio.d.ts +31 -0
  22. package/dist/edge-twilio.d.ts.map +1 -0
  23. package/dist/edge-twilio.js +286 -0
  24. package/dist/edge-twilio.js.map +1 -0
  25. package/dist/edge.d.ts +71 -0
  26. package/dist/edge.d.ts.map +1 -0
  27. package/dist/edge.js +560 -0
  28. package/dist/edge.js.map +1 -0
  29. package/dist/inbound-audio.d.ts +13 -0
  30. package/dist/inbound-audio.d.ts.map +1 -0
  31. package/dist/inbound-audio.js +66 -0
  32. package/dist/inbound-audio.js.map +1 -0
  33. package/dist/index.d.ts +81 -0
  34. package/dist/index.d.ts.map +1 -0
  35. package/dist/index.js +718 -0
  36. package/dist/index.js.map +1 -0
  37. package/dist/json-message.d.ts +7 -0
  38. package/dist/json-message.d.ts.map +1 -0
  39. package/dist/json-message.js +41 -0
  40. package/dist/json-message.js.map +1 -0
  41. package/dist/outbound-playout-pipeline.d.ts +53 -0
  42. package/dist/outbound-playout-pipeline.d.ts.map +1 -0
  43. package/dist/outbound-playout-pipeline.js +148 -0
  44. package/dist/outbound-playout-pipeline.js.map +1 -0
  45. package/dist/paced-playout.d.ts +42 -0
  46. package/dist/paced-playout.d.ts.map +1 -0
  47. package/dist/paced-playout.js +151 -0
  48. package/dist/paced-playout.js.map +1 -0
  49. package/dist/playout-progress.d.ts +26 -0
  50. package/dist/playout-progress.d.ts.map +1 -0
  51. package/dist/playout-progress.js +64 -0
  52. package/dist/playout-progress.js.map +1 -0
  53. package/dist/session-store.d.ts +39 -0
  54. package/dist/session-store.d.ts.map +1 -0
  55. package/dist/session-store.js +80 -0
  56. package/dist/session-store.js.map +1 -0
  57. package/dist/smartpbx.d.ts +47 -0
  58. package/dist/smartpbx.d.ts.map +1 -0
  59. package/dist/smartpbx.js +437 -0
  60. package/dist/smartpbx.js.map +1 -0
  61. package/dist/telnyx-codec.d.ts +41 -0
  62. package/dist/telnyx-codec.d.ts.map +1 -0
  63. package/dist/telnyx-codec.js +114 -0
  64. package/dist/telnyx-codec.js.map +1 -0
  65. package/dist/telnyx.d.ts +52 -0
  66. package/dist/telnyx.d.ts.map +1 -0
  67. package/dist/telnyx.js +550 -0
  68. package/dist/telnyx.js.map +1 -0
  69. package/dist/test-helpers.d.ts +20 -0
  70. package/dist/test-helpers.d.ts.map +1 -0
  71. package/dist/test-helpers.js +242 -0
  72. package/dist/test-helpers.js.map +1 -0
  73. package/dist/transport-helpers.d.ts +12 -0
  74. package/dist/transport-helpers.d.ts.map +1 -0
  75. package/dist/transport-helpers.js +90 -0
  76. package/dist/transport-helpers.js.map +1 -0
  77. package/dist/transport-host.d.ts +56 -0
  78. package/dist/transport-host.d.ts.map +1 -0
  79. package/dist/transport-host.js +110 -0
  80. package/dist/transport-host.js.map +1 -0
  81. package/dist/turn-metrics.d.ts +79 -0
  82. package/dist/turn-metrics.d.ts.map +1 -0
  83. package/dist/turn-metrics.js +208 -0
  84. package/dist/turn-metrics.js.map +1 -0
  85. package/dist/twilio-auth.d.ts +12 -0
  86. package/dist/twilio-auth.d.ts.map +1 -0
  87. package/dist/twilio-auth.js +44 -0
  88. package/dist/twilio-auth.js.map +1 -0
  89. package/dist/twilio.d.ts +56 -0
  90. package/dist/twilio.d.ts.map +1 -0
  91. package/dist/twilio.js +511 -0
  92. package/dist/twilio.js.map +1 -0
  93. package/dist/websocket-close.d.ts +12 -0
  94. package/dist/websocket-close.d.ts.map +1 -0
  95. package/dist/websocket-close.js +58 -0
  96. package/dist/websocket-close.js.map +1 -0
  97. package/dist/websocket-lifecycle.d.ts +29 -0
  98. package/dist/websocket-lifecycle.d.ts.map +1 -0
  99. package/dist/websocket-lifecycle.js +87 -0
  100. package/dist/websocket-lifecycle.js.map +1 -0
  101. package/dist/websocket-upgrade.d.ts +9 -0
  102. package/dist/websocket-upgrade.d.ts.map +1 -0
  103. package/dist/websocket-upgrade.js +91 -0
  104. package/dist/websocket-upgrade.js.map +1 -0
  105. package/dist/wire-carrier-control.d.ts +27 -0
  106. package/dist/wire-carrier-control.d.ts.map +1 -0
  107. package/dist/wire-carrier-control.js +116 -0
  108. package/dist/wire-carrier-control.js.map +1 -0
  109. package/package.json +32 -11
  110. package/src/admission-control.test.ts +270 -0
  111. package/src/background-audio.test.ts +224 -0
  112. package/src/browser-opus.test.ts +41 -0
  113. package/src/browser-pacing.test.ts +440 -0
  114. package/src/carrier-commands.test.ts +198 -0
  115. package/src/edge-telnyx.test.ts +333 -0
  116. package/src/edge-twilio.test.ts +293 -0
  117. package/src/edge.test.ts +727 -0
  118. package/src/graceful-drain.test.ts +306 -0
  119. package/src/inbound-audio.test.ts +58 -0
  120. package/src/index.test.ts +2078 -0
  121. package/src/outbound-playout-pipeline.test.ts +314 -0
  122. package/src/paced-playout.test.ts +247 -0
  123. package/src/playout-progress.test.ts +56 -0
  124. package/src/session-store.test.ts +274 -0
  125. package/src/smartpbx.test.ts +986 -0
  126. package/src/telnyx.test.ts +1633 -0
  127. package/src/transport-host.test.ts +51 -0
  128. package/src/turn-metrics.test.ts +503 -0
  129. package/src/twilio-auth.test.ts +36 -0
  130. package/src/twilio.test.ts +1274 -0
  131. package/src/websocket-close.test.ts +63 -0
  132. package/src/websocket-lifecycle.test.ts +49 -0
@@ -0,0 +1,79 @@
1
+ import { type PipelineBus, type TurnEndOwner, type TurnEndReason } from "@kuralle-syrinx/core";
2
+ export interface TurnTimestampState {
3
+ speechEndMs: number;
4
+ sttFinalMs: number;
5
+ eosMs: number;
6
+ vadStopHangoverMs: number;
7
+ textReadyMs: number;
8
+ firstAudioByteMs: number;
9
+ firstAudioPlayedMs: number;
10
+ lastAudioPlayedMs: number;
11
+ /** Who decided the turn ended — captured from the completing eos packet. */
12
+ endpointingOwner?: TurnEndOwner;
13
+ /** Why the turn ended — captured from the completing eos packet. */
14
+ endpointingReason?: TurnEndReason;
15
+ /** Set when the STT force-finalize watchdog fired for this turn. */
16
+ forceFinalized?: boolean;
17
+ }
18
+ export interface BrowserMetricsMessage {
19
+ readonly type: "metrics";
20
+ readonly turnId: string;
21
+ readonly correlationId: string;
22
+ readonly speechEndMs?: number;
23
+ readonly textReadyMs?: number;
24
+ readonly firstAudioByteMs?: number;
25
+ readonly firstAudioPlayedMs?: number;
26
+ readonly lastAudioPlayedMs?: number;
27
+ readonly sttMs?: number;
28
+ readonly llmTTFTMs?: number;
29
+ readonly ttsTTFBMs?: number;
30
+ readonly e2eMs?: number;
31
+ readonly eouBudgetMs?: {
32
+ readonly vadStopHangoverMs?: number;
33
+ readonly sttFinalDelayMs?: number;
34
+ readonly endpointDelayMs?: number;
35
+ readonly totalMs?: number;
36
+ };
37
+ /** Which owner decided the turn ended. Omitted when the backend did not say. */
38
+ readonly endpointingOwner?: TurnEndOwner;
39
+ /** Why the turn ended. Omitted when the backend did not say. */
40
+ readonly endpointingReason?: TurnEndReason;
41
+ }
42
+ /**
43
+ * The field set `buildBrowserMetricsMessage` populates for an audio-originated turn
44
+ * (`vad.speech_ended` + `stt.result` + `llm.delta` + `tts.audio` all measured) —
45
+ * everything EXCEPT `firstAudioPlayedMs`/`lastAudioPlayedMs`, which depend on a
46
+ * playout-completion signal whose *source* legitimately differs per transport
47
+ * (Node's browser websocket path paces server-side; the Workers/DO edge path is
48
+ * client-paced). Both runtimes call this same builder and must emit this same core
49
+ * set; the parity tests in both packages assert against this constant so a runtime
50
+ * that silently drops the `metrics` message, or diverges in shape, fails loudly.
51
+ */
52
+ export declare const CORE_METRICS_FIELDS: readonly ["type", "turnId", "correlationId", "speechEndMs", "textReadyMs", "firstAudioByteMs", "sttMs", "llmTTFTMs", "ttsTTFBMs", "e2eMs", "eouBudgetMs"];
53
+ export declare function buildBrowserMetricsMessage(turnId: string, timestamps: TurnTimestampState): BrowserMetricsMessage;
54
+ export interface TurnMetricsTrackerOptions {
55
+ /**
56
+ * Additionally finalize (emit + clear) a turn's metrics on `tts.end` when no
57
+ * `tts.playout_progress` completion has arrived yet. The Node browser websocket
58
+ * path always gets a `tts.playout_progress` completion shortly after `tts.end`
59
+ * from its own server-side pacer (playout-progress.ts) — turning this on there
60
+ * would race a premature, playout-less emission ahead of the real one, so it
61
+ * defaults off and Node leaves it unset. The Workers/DO edge path is
62
+ * client-paced (the browser reports playout over the wire); a client that never
63
+ * reports it — a non-browser client, or a text-only smoke probe — would
64
+ * otherwise leave a turn's metrics pending forever, so edge opts in. If a
65
+ * richer client-reported completion arrives after this floor already fired,
66
+ * it is a no-op: the turn was already finalized and cleared.
67
+ */
68
+ readonly finalizeOnTtsEnd?: boolean;
69
+ }
70
+ export declare class TurnMetricsTracker {
71
+ private readonly bus;
72
+ private readonly onEmit;
73
+ private readonly options;
74
+ private readonly turns;
75
+ constructor(bus: PipelineBus, onEmit: (message: BrowserMetricsMessage) => void, persistedTurns?: Map<string, TurnTimestampState>, options?: TurnMetricsTrackerOptions);
76
+ wire(disposers: Array<() => void>): void;
77
+ private turnState;
78
+ }
79
+ //# sourceMappingURL=turn-metrics.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn-metrics.d.ts","sourceRoot":"","sources":["../src/turn-metrics.ts"],"names":[],"mappings":"AAEA,OAAO,EAKL,KAAK,WAAW,EAMhB,KAAK,YAAY,EACjB,KAAK,aAAa,EAEnB,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,kBAAkB;IACjC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,iBAAiB,EAAE,MAAM,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,EAAE,MAAM,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,YAAY,CAAC;IAChC,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,aAAa,CAAC;IAClC,oEAAoE;IACpE,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE;QACrB,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;QACpC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;QAClC,QAAQ,CAAC,eAAe,CAAC,EAAE,MAAM,CAAC;QAClC,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;KAC3B,CAAC;IACF,gFAAgF;IAChF,QAAQ,CAAC,gBAAgB,CAAC,EAAE,YAAY,CAAC;IACzC,gEAAgE;IAChE,QAAQ,CAAC,iBAAiB,CAAC,EAAE,aAAa,CAAC;CAC5C;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,mBAAmB,2JAYtB,CAAC;AAOX,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,kBAAkB,GAC7B,qBAAqB,CAsDvB;AAeD,MAAM,WAAW,yBAAyB;IACxC;;;;;;;;;;;;OAYG;IACH,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CACrC;AAED,qBAAa,kBAAkB;IAI3B,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM;IAEvB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAN1B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;gBAGrC,GAAG,EAAE,WAAW,EAChB,MAAM,EAAE,CAAC,OAAO,EAAE,qBAAqB,KAAK,IAAI,EACjE,cAAc,CAAC,EAAE,GAAG,CAAC,MAAM,EAAE,kBAAkB,CAAC,EAC/B,OAAO,GAAE,yBAA8B;IAK1D,IAAI,CAAC,SAAS,EAAE,KAAK,CAAC,MAAM,IAAI,CAAC,GAAG,IAAI;IAgGxC,OAAO,CAAC,SAAS;CAQlB"}
@@ -0,0 +1,208 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * The field set `buildBrowserMetricsMessage` populates for an audio-originated turn
4
+ * (`vad.speech_ended` + `stt.result` + `llm.delta` + `tts.audio` all measured) —
5
+ * everything EXCEPT `firstAudioPlayedMs`/`lastAudioPlayedMs`, which depend on a
6
+ * playout-completion signal whose *source* legitimately differs per transport
7
+ * (Node's browser websocket path paces server-side; the Workers/DO edge path is
8
+ * client-paced). Both runtimes call this same builder and must emit this same core
9
+ * set; the parity tests in both packages assert against this constant so a runtime
10
+ * that silently drops the `metrics` message, or diverges in shape, fails loudly.
11
+ */
12
+ export const CORE_METRICS_FIELDS = [
13
+ "type",
14
+ "turnId",
15
+ "correlationId",
16
+ "speechEndMs",
17
+ "textReadyMs",
18
+ "firstAudioByteMs",
19
+ "sttMs",
20
+ "llmTTFTMs",
21
+ "ttsTTFBMs",
22
+ "e2eMs",
23
+ "eouBudgetMs",
24
+ ];
25
+ function positiveDelta(endMs, startMs) {
26
+ if (endMs <= 0 || startMs <= 0 || endMs < startMs)
27
+ return undefined;
28
+ return endMs - startMs;
29
+ }
30
+ export function buildBrowserMetricsMessage(turnId, timestamps) {
31
+ const sttMs = positiveDelta(timestamps.sttFinalMs, timestamps.speechEndMs);
32
+ const llmTTFTMs = positiveDelta(timestamps.textReadyMs, timestamps.sttFinalMs);
33
+ const ttsTTFBMs = positiveDelta(timestamps.firstAudioByteMs, timestamps.textReadyMs);
34
+ const e2eFromPlayout = positiveDelta(timestamps.firstAudioPlayedMs, timestamps.speechEndMs);
35
+ const e2eFromByte = positiveDelta(timestamps.firstAudioByteMs, timestamps.speechEndMs);
36
+ const sttFinalDelayMs = positiveDelta(timestamps.sttFinalMs, timestamps.speechEndMs);
37
+ const endpointDelayMs = positiveDelta(timestamps.eosMs, timestamps.sttFinalMs);
38
+ const vadStopHangoverMs = timestamps.vadStopHangoverMs > 0 ? timestamps.vadStopHangoverMs : undefined;
39
+ const eouTotalMs = (vadStopHangoverMs ?? 0) +
40
+ (positiveDelta(timestamps.eosMs, timestamps.speechEndMs) ?? sttFinalDelayMs ?? 0) ||
41
+ undefined;
42
+ const eouBudgetMs = vadStopHangoverMs !== undefined ||
43
+ sttFinalDelayMs !== undefined ||
44
+ endpointDelayMs !== undefined ||
45
+ eouTotalMs !== undefined
46
+ ? {
47
+ ...(vadStopHangoverMs !== undefined ? { vadStopHangoverMs } : {}),
48
+ ...(sttFinalDelayMs !== undefined ? { sttFinalDelayMs } : {}),
49
+ ...(endpointDelayMs !== undefined ? { endpointDelayMs } : {}),
50
+ ...(eouTotalMs !== undefined ? { totalMs: eouTotalMs } : {}),
51
+ }
52
+ : undefined;
53
+ // The endpointing decision is a fact about the turn, not a timing measurement, so
54
+ // it is emitted whenever it is known — never coerced to a zero. A force-finalize
55
+ // watchdog firing overrides the emitter's reason: the STT plugin that produced the
56
+ // completing eos cannot know the watchdog fired, but this mark (stt.force_finalized)
57
+ // arrives independently on the bus, so the truth wins here regardless of emitter.
58
+ const endpointingOwner = timestamps.endpointingOwner;
59
+ const endpointingReason = timestamps.forceFinalized === true ? "force_finalized" : timestamps.endpointingReason;
60
+ return {
61
+ type: "metrics",
62
+ turnId,
63
+ correlationId: turnId,
64
+ ...(timestamps.speechEndMs > 0 ? { speechEndMs: timestamps.speechEndMs } : {}),
65
+ ...(timestamps.textReadyMs > 0 ? { textReadyMs: timestamps.textReadyMs } : {}),
66
+ ...(timestamps.firstAudioByteMs > 0 ? { firstAudioByteMs: timestamps.firstAudioByteMs } : {}),
67
+ ...(timestamps.firstAudioPlayedMs > 0 ? { firstAudioPlayedMs: timestamps.firstAudioPlayedMs } : {}),
68
+ ...(timestamps.lastAudioPlayedMs > 0 ? { lastAudioPlayedMs: timestamps.lastAudioPlayedMs } : {}),
69
+ ...(sttMs !== undefined ? { sttMs } : {}),
70
+ ...(llmTTFTMs !== undefined ? { llmTTFTMs } : {}),
71
+ ...(ttsTTFBMs !== undefined ? { ttsTTFBMs } : {}),
72
+ ...(e2eFromPlayout !== undefined ? { e2eMs: e2eFromPlayout } : e2eFromByte !== undefined ? { e2eMs: e2eFromByte } : {}),
73
+ ...(eouBudgetMs !== undefined ? { eouBudgetMs } : {}),
74
+ ...(endpointingOwner !== undefined ? { endpointingOwner } : {}),
75
+ ...(endpointingReason !== undefined ? { endpointingReason } : {}),
76
+ };
77
+ }
78
+ function emptyTurnState() {
79
+ return {
80
+ speechEndMs: 0,
81
+ sttFinalMs: 0,
82
+ eosMs: 0,
83
+ vadStopHangoverMs: 0,
84
+ textReadyMs: 0,
85
+ firstAudioByteMs: 0,
86
+ firstAudioPlayedMs: 0,
87
+ lastAudioPlayedMs: 0,
88
+ };
89
+ }
90
+ export class TurnMetricsTracker {
91
+ bus;
92
+ onEmit;
93
+ options;
94
+ turns;
95
+ constructor(bus, onEmit, persistedTurns, options = {}) {
96
+ this.bus = bus;
97
+ this.onEmit = onEmit;
98
+ this.options = options;
99
+ this.turns = persistedTurns ?? new Map();
100
+ }
101
+ wire(disposers) {
102
+ disposers.push(this.bus.on("vad.speech_ended", (pkt) => {
103
+ const ended = pkt;
104
+ const state = this.turnState(ended.contextId);
105
+ if (state.speechEndMs === 0)
106
+ state.speechEndMs = ended.timestampMs;
107
+ }), this.bus.on("stt.result", (pkt) => {
108
+ const result = pkt;
109
+ const state = this.turnState(result.contextId);
110
+ if (state.sttFinalMs === 0)
111
+ state.sttFinalMs = result.timestampMs;
112
+ }), this.bus.on("metric.conversation", (pkt) => {
113
+ const metric = pkt;
114
+ if (metric.name === "vad.stop_hangover_ms") {
115
+ const hangoverMs = Number(metric.value);
116
+ if (Number.isNaN(hangoverMs))
117
+ return;
118
+ const state = this.turnState(metric.contextId);
119
+ if (state.vadStopHangoverMs === 0)
120
+ state.vadStopHangoverMs = hangoverMs;
121
+ return;
122
+ }
123
+ if (metric.name === "stt.force_finalized") {
124
+ this.turnState(metric.contextId).forceFinalized = true;
125
+ }
126
+ }), this.bus.on("eos.turn_complete", (pkt) => {
127
+ const eos = pkt;
128
+ const state = this.turnState(eos.contextId);
129
+ if (state.eosMs === 0)
130
+ state.eosMs = eos.timestampMs;
131
+ // First eos wins (mirrors eosMs): the completing eos is the first, and any
132
+ // duplicate is dropped downstream. Owner/reason are facts, not measurements.
133
+ if (state.endpointingOwner === undefined && eos.endpointingOwner !== undefined) {
134
+ state.endpointingOwner = eos.endpointingOwner;
135
+ }
136
+ if (state.endpointingReason === undefined && eos.endpointingReason !== undefined) {
137
+ state.endpointingReason = eos.endpointingReason;
138
+ }
139
+ }), this.bus.on("llm.delta", (pkt) => {
140
+ const delta = pkt;
141
+ if (delta.text.length === 0)
142
+ return;
143
+ const state = this.turnState(delta.contextId);
144
+ if (state.textReadyMs === 0)
145
+ state.textReadyMs = delta.timestampMs;
146
+ }), this.bus.on("tts.audio", (pkt) => {
147
+ const audio = pkt;
148
+ const state = this.turnState(audio.contextId);
149
+ if (state.firstAudioByteMs === 0)
150
+ state.firstAudioByteMs = audio.timestampMs;
151
+ }), this.bus.on("tts.playout_started", (pkt) => {
152
+ const started = pkt;
153
+ const state = this.turns.get(started.contextId);
154
+ if (!state)
155
+ return;
156
+ if (state.firstAudioPlayedMs === 0) {
157
+ state.firstAudioPlayedMs = started.timestampMs;
158
+ }
159
+ }), this.bus.on("tts.playout_progress", (pkt) => {
160
+ const progress = pkt;
161
+ const state = this.turns.get(progress.contextId);
162
+ if (!state)
163
+ return;
164
+ // The edge transport never emits `tts.playout_started` — the browser reports
165
+ // its own playout clock and that is forwarded here (edge.ts). So take the
166
+ // first progress report as the moment audio started playing. On the Node path
167
+ // `playout_started` has already set this, and the `=== 0` guard leaves it be.
168
+ if (state.firstAudioPlayedMs === 0)
169
+ state.firstAudioPlayedMs = progress.timestampMs;
170
+ if (progress.complete) {
171
+ state.lastAudioPlayedMs = progress.timestampMs;
172
+ this.onEmit(buildBrowserMetricsMessage(progress.contextId, state));
173
+ this.turns.delete(progress.contextId);
174
+ }
175
+ }), this.bus.on("tts.end", (pkt) => {
176
+ if (!this.options.finalizeOnTtsEnd)
177
+ return;
178
+ const end = pkt;
179
+ const state = this.turns.get(end.contextId);
180
+ if (!state)
181
+ return;
182
+ // `tts.end` means synthesis finished, which is EARLIER than playback finishing.
183
+ // So if this client is pacing playout, firing here would emit a poorer message
184
+ // and delete the turn before the real completion arrived — costing
185
+ // firstAudioPlayedMs, lastAudioPlayedMs, and downgrading e2eMs from
186
+ // "to first audio played" to "to first byte" on this runtime only. That is the
187
+ // exact runtime drift this tracker exists to prevent.
188
+ //
189
+ // A non-zero firstAudioPlayedMs means playout has been reported, so wait.
190
+ // The floor is only for clients that never report at all.
191
+ if (state.firstAudioPlayedMs !== 0)
192
+ return;
193
+ this.onEmit(buildBrowserMetricsMessage(end.contextId, state));
194
+ this.turns.delete(end.contextId);
195
+ }), this.bus.on("interrupt.tts", (pkt) => {
196
+ this.turns.delete(pkt.contextId);
197
+ }));
198
+ }
199
+ turnState(contextId) {
200
+ let state = this.turns.get(contextId);
201
+ if (!state) {
202
+ state = emptyTurnState();
203
+ this.turns.set(contextId, state);
204
+ }
205
+ return state;
206
+ }
207
+ }
208
+ //# sourceMappingURL=turn-metrics.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"turn-metrics.js","sourceRoot":"","sources":["../src/turn-metrics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Validate a Twilio request signature. `params` are the POST form fields (empty
3
+ * for a bare GET / WS upgrade). Constant-time compares the computed signature
4
+ * against the provided one. Returns false on any mismatch or malformed input.
5
+ */
6
+ export declare function validateTwilioSignature(args: {
7
+ readonly authToken: string;
8
+ readonly signature: string | null | undefined;
9
+ readonly url: string;
10
+ readonly params?: Record<string, string>;
11
+ }): Promise<boolean>;
12
+ //# sourceMappingURL=twilio-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twilio-auth.d.ts","sourceRoot":"","sources":["../src/twilio-auth.ts"],"names":[],"mappings":"AAUA;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,IAAI,EAAE;IAClD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC9C,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC1C,GAAG,OAAO,CAAC,OAAO,CAAC,CAoBnB"}
@@ -0,0 +1,44 @@
1
+ // SPDX-License-Identifier: MIT
2
+ //
3
+ // Twilio request-signature validation (X-Twilio-Signature). Runtime-neutral —
4
+ // uses Web Crypto (HMAC-SHA1), so it works on Node and Cloudflare Workers. Twilio
5
+ // signs each webhook: signature = base64(HMAC-SHA1(authToken, fullUrl + concat of
6
+ // POST params sorted by key, each as key+value)). Reference:
7
+ // https://www.twilio.com/docs/usage/security#validating-requests
8
+ //
9
+ // Plug into the transport `authorize` hook, or call directly in a webhook handler.
10
+ /**
11
+ * Validate a Twilio request signature. `params` are the POST form fields (empty
12
+ * for a bare GET / WS upgrade). Constant-time compares the computed signature
13
+ * against the provided one. Returns false on any mismatch or malformed input.
14
+ */
15
+ export async function validateTwilioSignature(args) {
16
+ const { authToken, signature, url } = args;
17
+ if (!authToken || !signature)
18
+ return false;
19
+ const params = args.params ?? {};
20
+ let data = url;
21
+ for (const key of Object.keys(params).sort()) {
22
+ data += key + params[key];
23
+ }
24
+ const key = await crypto.subtle.importKey("raw", new TextEncoder().encode(authToken), { name: "HMAC", hash: "SHA-1" }, false, ["sign"]);
25
+ const mac = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(data));
26
+ const expected = base64FromBytes(new Uint8Array(mac));
27
+ return timingSafeEqual(expected, signature);
28
+ }
29
+ function base64FromBytes(bytes) {
30
+ let binary = "";
31
+ for (const b of bytes)
32
+ binary += String.fromCharCode(b);
33
+ // btoa exists on Workers and modern Node globals.
34
+ return btoa(binary);
35
+ }
36
+ function timingSafeEqual(a, b) {
37
+ if (a.length !== b.length)
38
+ return false;
39
+ let diff = 0;
40
+ for (let i = 0; i < a.length; i++)
41
+ diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
42
+ return diff === 0;
43
+ }
44
+ //# sourceMappingURL=twilio-auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twilio-auth.js","sourceRoot":"","sources":["../src/twilio-auth.ts"],"names":[],"mappings":"AAAA,+BAA+B;AAC/B,EAAE;AACF,8EAA8E;AAC9E,kFAAkF;AAClF,kFAAkF;AAClF,6DAA6D;AAC7D,iEAAiE;AACjE,EAAE;AACF,mFAAmF;AAEnF;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,uBAAuB,CAAC,IAK7C;IACC,MAAM,EAAE,SAAS,EAAE,SAAS,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;IAC3C,IAAI,CAAC,SAAS,IAAI,CAAC,SAAS;QAAE,OAAO,KAAK,CAAC;IAE3C,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;IACjC,IAAI,IAAI,GAAG,GAAG,CAAC;IACf,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;QAC7C,IAAI,IAAI,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;IAC5B,CAAC;IAED,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CACvC,KAAK,EACL,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,SAAS,CAAC,EACnC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,EAC/B,KAAK,EACL,CAAC,MAAM,CAAC,CACT,CAAC;IACF,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC;IAClF,MAAM,QAAQ,GAAG,eAAe,CAAC,IAAI,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;IACtD,OAAO,eAAe,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAC9C,CAAC;AAED,SAAS,eAAe,CAAC,KAAiB;IACxC,IAAI,MAAM,GAAG,EAAE,CAAC;IAChB,KAAK,MAAM,CAAC,IAAI,KAAK;QAAE,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IACxD,kDAAkD;IAClD,OAAO,IAAI,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC;AAED,SAAS,eAAe,CAAC,CAAS,EAAE,CAAS;IAC3C,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IAC7E,OAAO,IAAI,KAAK,CAAC,CAAC;AACpB,CAAC"}
@@ -0,0 +1,56 @@
1
+ import type { IncomingMessage } from "node:http";
2
+ import { type Server as HttpServer } from "node:http";
3
+ import { WebSocketServer } from "ws";
4
+ import { type VoiceAgentSession } from "@kuralle-syrinx/core";
5
+ import { type GracefulCloseOptions } from "./transport-host.js";
6
+ import { type BackgroundAudioConfig } from "./background-audio.js";
7
+ import type { FetchLike, TwilioRestCredentials, WarmTransferSummarizer } from "./carrier-commands.js";
8
+ export interface TwilioMediaStreamServerOptions {
9
+ readonly server?: HttpServer;
10
+ readonly port?: number;
11
+ readonly host?: string;
12
+ readonly path?: string;
13
+ readonly createSession: (request: IncomingMessage) => VoiceAgentSession | Promise<VoiceAgentSession>;
14
+ readonly contextId?: (start: TwilioStartPayload) => string;
15
+ readonly inputSampleRateHz?: number;
16
+ readonly outputSampleRateHz?: number;
17
+ readonly twilioSampleRateHz?: number;
18
+ readonly outboundFrameDurationMs?: number;
19
+ readonly maxQueuedOutputAudioMs?: number;
20
+ /** Ambient/thinking bed mixed (ducked) under assistant speech (see BackgroundAudioConfig). */
21
+ readonly backgroundAudio?: BackgroundAudioConfig;
22
+ readonly heartbeatIntervalMs?: number;
23
+ readonly startupTimeoutMs?: number;
24
+ readonly maxSessionDurationMs?: number;
25
+ readonly maxBufferedAmountBytes?: number;
26
+ readonly maxInboundMessageBytes?: number;
27
+ readonly maxConcurrentSessions?: number;
28
+ readonly maxConcurrentSessionsScope?: "path" | "server";
29
+ readonly onTransportMetric?: (name: string) => void;
30
+ /**
31
+ * Injected REST credentials for dtmf.send / call.transfer dispatch.
32
+ * Workers: pass secrets from `env`. Never read from process.env here.
33
+ * Live dispatch unverified without credentials.
34
+ */
35
+ readonly restCredentials?: TwilioRestCredentials;
36
+ readonly restFetch?: FetchLike;
37
+ readonly transferRedirectUrl?: string;
38
+ readonly warmTransferSummarizer?: WarmTransferSummarizer;
39
+ }
40
+ export interface TwilioMediaStreamServer {
41
+ readonly httpServer: HttpServer;
42
+ readonly wsServer: WebSocketServer;
43
+ address(): ReturnType<HttpServer["address"]>;
44
+ close(opts?: GracefulCloseOptions): Promise<void>;
45
+ }
46
+ export interface TwilioStartPayload {
47
+ readonly streamSid?: string;
48
+ readonly callSid?: string;
49
+ readonly mediaFormat?: {
50
+ readonly encoding?: string;
51
+ readonly sampleRate?: number | string;
52
+ readonly channels?: number | string;
53
+ };
54
+ }
55
+ export declare function createTwilioMediaStreamServer(options: TwilioMediaStreamServerOptions): Promise<TwilioMediaStreamServer>;
56
+ //# sourceMappingURL=twilio.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"twilio.d.ts","sourceRoot":"","sources":["../src/twilio.ts"],"names":[],"mappings":"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"}