@cloudflare/voice-telnyx 0.0.0 → 0.0.2

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser.js","names":[],"sources":["../src/audio/utils.ts","../src/providers/call-bridge.ts","../src/phone-client.ts","../src/transport/phone-transport.ts","../src/helpers/transport-config.ts"],"sourcesContent":["/**\n * Convert Float32 audio samples (-1.0..1.0) to Int16 PCM (-32768..32767).\n * Clamps values outside the -1..1 range.\n */\nexport function float32ToInt16(float32: Float32Array): Int16Array {\n const int16 = new Int16Array(float32.length);\n for (let i = 0; i < float32.length; i++) {\n const clamped = Math.max(-1, Math.min(1, float32[i]));\n int16[i] = clamped < 0 ? clamped * 32768 : clamped * 32767;\n }\n return int16;\n}\n\n/**\n * Compute the Root Mean Square (RMS) of audio samples.\n * Returns 0 for empty input.\n */\nexport function computeRMS(samples: Float32Array): number {\n if (samples.length === 0) return 0;\n let sum = 0;\n for (let i = 0; i < samples.length; i++) {\n sum += samples[i] * samples[i];\n }\n return Math.sqrt(sum / samples.length);\n}\n\n/**\n * AudioWorklet processor source code for capturing PCM from a MediaStream.\n *\n * This processor collects Float32 audio frames and posts them to the\n * main thread via the MessagePort. It runs in the AudioWorklet thread.\n *\n * Expected AudioContext sampleRate: 16000 (browser resamples from source).\n */\nexport const PCM_CAPTURE_PROCESSOR_SOURCE = /* js */ `\nclass PcmCaptureProcessor extends AudioWorkletProcessor {\n process(inputs) {\n const input = inputs[0];\n if (input && input[0] && input[0].length > 0) {\n // Post a copy of the Float32 channel data to the main thread\n this.port.postMessage(new Float32Array(input[0]));\n }\n return true;\n }\n}\nregisterProcessor(\"pcm-capture-processor\", PcmCaptureProcessor);\n`;\n\n/**\n * AudioWorklet processor source code for playing back PCM into a MediaStream.\n *\n * Receives Float32 audio frames from the main thread via MessagePort\n * and writes them to the output buffer. Buffers frames to handle timing\n * differences between the main thread and the audio thread.\n *\n * Expected AudioContext sampleRate: 48000 (matching WebRTC).\n */\nexport const PCM_PLAYBACK_PROCESSOR_SOURCE = /* js */ `\nclass PcmPlaybackProcessor extends AudioWorkletProcessor {\n constructor() {\n super();\n this._buffer = [];\n this._maxBufferFrames = 50; // ~1s at 48kHz/128 samples per frame\n this.port.onmessage = (e) => {\n if (e.data === 'clear') {\n this._buffer = [];\n return;\n }\n this._buffer.push(e.data);\n // Evict oldest frames if buffer grows too large\n while (this._buffer.length > this._maxBufferFrames) {\n this._buffer.shift();\n }\n };\n }\n\n process(inputs, outputs) {\n const output = outputs[0];\n if (!output || !output[0]) return true;\n\n const channel = output[0];\n let written = 0;\n\n while (written < channel.length && this._buffer.length > 0) {\n const frame = this._buffer[0];\n const available = frame.length;\n const needed = channel.length - written;\n\n if (available <= needed) {\n channel.set(frame, written);\n written += available;\n this._buffer.shift();\n } else {\n channel.set(frame.subarray(0, needed), written);\n this._buffer[0] = frame.subarray(needed);\n written += needed;\n }\n }\n\n // Fill remainder with silence\n for (let i = written; i < channel.length; i++) {\n channel[i] = 0;\n }\n\n return true;\n }\n}\nregisterProcessor(\"pcm-playback-processor\", PcmPlaybackProcessor);\n`;\n","import type { VoiceAudioInput } from \"@cloudflare/voice/client\";\nimport { TelnyxRTC } from \"@telnyx/webrtc\";\nimport {\n float32ToInt16,\n computeRMS,\n PCM_CAPTURE_PROCESSOR_SOURCE,\n PCM_PLAYBACK_PROCESSOR_SOURCE\n} from \"../audio/utils.js\";\n\n/**\n * Configuration for the TelnyxCallBridge.\n *\n * Uses JWT authentication (browser-side). The JWT is generated\n * server-side from a Telnyx API key + credential connection.\n */\nexport interface TelnyxCallBridgeConfig {\n /** JWT token from the Telnyx telephony credentials API. */\n loginToken: string;\n /** Automatically answer inbound calls. @default false */\n autoAnswer?: boolean;\n /** Enable debug logging. @default false */\n debug?: boolean;\n}\n\ninterface TelnyxCallLike {\n state?: string;\n answer?: () => void;\n hangup?: () => void;\n dtmf?: (digits: string) => void;\n remoteStream?: MediaStream | null;\n peer?: { instance?: RTCPeerConnection };\n}\n\ninterface TelnyxNotificationLike {\n type?: string;\n call?: TelnyxCallLike;\n}\n\ninterface TelnyxRTCWithCalls {\n newCall: (options: {\n destinationNumber: string;\n callerNumber?: string;\n }) => TelnyxCallLike;\n}\n\ninterface AudioInboundRtpStats extends RTCStats {\n kind?: string;\n bytesReceived?: number;\n packetsReceived?: number;\n packetsLost?: number;\n packetsDiscarded?: number;\n totalSamplesReceived?: number;\n jitterBufferEmittedCount?: number;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null;\n}\n\nfunction isTelnyxNotification(value: unknown): value is TelnyxNotificationLike {\n return isRecord(value);\n}\n\nfunction hasNewCall(\n client: TelnyxRTC\n): client is TelnyxRTC & TelnyxRTCWithCalls {\n return \"newCall\" in client && typeof client.newCall === \"function\";\n}\n\nfunction getPeerConnection(\n call: TelnyxCallLike\n): RTCPeerConnection | undefined {\n return call.peer?.instance;\n}\n\nfunction getRemoteStream(call: TelnyxCallLike): MediaStream | null {\n return call.remoteStream ?? null;\n}\n\n/**\n * Bridges Telnyx phone calls into the Cloudflare voice pipeline.\n *\n * Implements `VoiceAudioInput` from @cloudflare/voice — extracts PCM\n * audio from inbound phone calls and feeds it to the AI pipeline.\n * Also provides `playAudio()` for injecting response audio back\n * into the phone call.\n *\n * Usage:\n * ```typescript\n * const bridge = new TelnyxCallBridge({ loginToken: jwt });\n * const voiceClient = new VoiceClient({\n * agent: \"my-agent\",\n * audioInput: bridge,\n * });\n * ```\n */\nexport class TelnyxCallBridge implements VoiceAudioInput {\n // VoiceAudioInput callbacks — set by VoiceClient before start()\n onAudioLevel: ((rms: number) => void) | null = null;\n onAudioData?: ((pcm: ArrayBuffer) => void) | null = null;\n\n private readonly config: TelnyxCallBridgeConfig;\n private _connected = false;\n private _activeCall: TelnyxCallLike | null = null;\n private client: TelnyxRTC | null = null;\n private captureContext: AudioContext | null = null;\n private captureSource: MediaStreamAudioSourceNode | null = null;\n private captureWorklet: AudioWorkletNode | null = null;\n private captureBlobUrl: string | null = null;\n private captureAudioEl: HTMLAudioElement | null = null;\n private statsInterval: ReturnType<typeof setInterval> | null = null;\n private playbackContext: AudioContext | null = null;\n private playbackWorklet: AudioWorkletNode | null = null;\n private playbackBlobUrl: string | null = null;\n private startPromise: Promise<void> | null = null;\n private finishStart: (() => void) | null = null;\n private startAttempt = 0;\n private mediaSetupAttempt = 0;\n\n constructor(config: TelnyxCallBridgeConfig) {\n this.config = config;\n }\n\n /** Whether the Telnyx client is connected to the platform. */\n get connected(): boolean {\n return this._connected;\n }\n\n /** The currently active Telnyx call, or null. */\n get activeCall(): unknown | null {\n return this._activeCall;\n }\n\n /** Connect to Telnyx and start listening for calls. */\n async start(): Promise<void> {\n if (this._connected) return;\n if (this.startPromise) return this.startPromise;\n\n const attempt = ++this.startAttempt;\n const client = new TelnyxRTC({\n login_token: this.config.loginToken,\n debug: this.config.debug\n });\n this.client = client;\n\n this.startPromise = new Promise<void>((resolve, reject) => {\n this.finishStart = resolve;\n client.on(\"telnyx.ready\", () => {\n if (this.startAttempt !== attempt || this.client !== client) {\n resolve();\n return;\n }\n this._connected = true;\n resolve();\n });\n\n client.on(\"telnyx.error\", (error: unknown) => {\n if (this.startAttempt !== attempt || this.client !== client) {\n resolve();\n return;\n }\n reject(error);\n });\n\n client.on(\"telnyx.notification\", (notification: unknown) => {\n if (this.startAttempt !== attempt || this.client !== client) return;\n this.handleNotification(notification);\n });\n\n client.connect();\n }).finally(() => {\n if (this.startAttempt === attempt) {\n this.startPromise = null;\n this.finishStart = null;\n }\n });\n\n return this.startPromise;\n }\n\n /** Answer the current inbound call. */\n answer(): void {\n if (!this._activeCall) throw new Error(\"No active call\");\n this._activeCall.answer?.();\n }\n\n /** End the active call. */\n hangup(): void {\n if (!this._activeCall) return;\n this._activeCall.hangup?.();\n }\n\n /**\n * Initiate an outbound PSTN call.\n * @param destination Phone number or SIP URI to call.\n * @param callerNumber The caller ID number to present.\n * @returns The Telnyx Call object.\n */\n dial(destination: string, callerNumber?: string): unknown {\n if (!this.client) throw new Error(\"Not connected — call start() first\");\n if (!hasNewCall(this.client)) {\n throw new Error(\"Telnyx client does not expose newCall()\");\n }\n const call = this.client.newCall({\n destinationNumber: destination,\n callerNumber\n });\n this._activeCall = call;\n return call;\n }\n\n /** Send DTMF digits on the active call. */\n sendDTMF(digits: string): void {\n if (!this._activeCall) throw new Error(\"No active call\");\n this._activeCall.dtmf?.(digits);\n }\n\n /**\n * Clear any buffered audio in the playback pipeline.\n * Used during interrupt detection to stop stale audio from playing.\n */\n clearPlaybackBuffer(): void {\n this.playbackWorklet?.port.postMessage(\"clear\");\n }\n\n /**\n * Inject PCM audio into the active phone call (agent → caller).\n * Accepts 16kHz mono Int16 PCM. Upsamples to 48kHz for WebRTC.\n * No-op if no active call.\n */\n playAudio(pcm: ArrayBuffer): void {\n if (!this.playbackWorklet) return;\n const int16 = new Int16Array(pcm);\n\n // Upsample 16kHz → 48kHz (3x) via linear interpolation\n const upsampleRatio = 3;\n const float32 = new Float32Array(int16.length * upsampleRatio);\n for (let i = 0; i < int16.length; i++) {\n const current = int16[i] / 32768;\n const next = i < int16.length - 1 ? int16[i + 1] / 32768 : current;\n const base = i * upsampleRatio;\n for (let j = 0; j < upsampleRatio; j++) {\n float32[base + j] = current + (next - current) * (j / upsampleRatio);\n }\n }\n\n this.playbackWorklet.port.postMessage(float32);\n }\n\n /** Disconnect from Telnyx and clean up all resources. */\n stop(): void {\n this.startAttempt++;\n this.mediaSetupAttempt++;\n this.finishStart?.();\n this.finishStart = null;\n this.startPromise = null;\n this.stopAudioCapture();\n this.stopAudioPlayback();\n this._activeCall = null;\n this._connected = false;\n if (this.client) {\n this.client.disconnect();\n this.client = null;\n }\n }\n\n private handleNotification(notification: unknown): void {\n if (!isTelnyxNotification(notification)) return;\n\n console.log(\n \"[TelnyxCallBridge] notification:\",\n notification.type,\n \"call:\",\n !!notification.call,\n \"state:\",\n notification.call?.state\n );\n if (notification.type !== \"callUpdate\" || !notification.call) return;\n\n const call = notification.call;\n console.log(\"[TelnyxCallBridge] call state:\", call.state);\n\n switch (call.state) {\n case \"ringing\":\n this._activeCall = call;\n if (this.config.autoAnswer) {\n console.log(\"[TelnyxCallBridge] auto-answering call\");\n call.answer?.();\n }\n break;\n\n case \"active\":\n this._activeCall = call;\n console.log(\n \"[TelnyxCallBridge] call active — starting audio capture + playback\"\n );\n this.mediaSetupAttempt++;\n this.stopAudioCapture();\n this.stopAudioPlayback();\n this.startAudioCapture(call, this.mediaSetupAttempt).catch((err) =>\n console.error(\"[TelnyxCallBridge] startAudioCapture failed:\", err)\n );\n this.startAudioPlayback(call, this.mediaSetupAttempt).catch((err) =>\n console.error(\"[TelnyxCallBridge] startAudioPlayback failed:\", err)\n );\n break;\n\n case \"hangup\":\n case \"destroy\":\n case \"purge\":\n this.mediaSetupAttempt++;\n this.stopAudioCapture();\n this.stopAudioPlayback();\n this._activeCall = null;\n break;\n }\n }\n\n private isCurrentMediaSetup(setupAttempt: number): boolean {\n return this.mediaSetupAttempt === setupAttempt;\n }\n\n private cleanupCaptureResources(resources: {\n audioEl?: HTMLAudioElement | null;\n context?: AudioContext | null;\n source?: MediaStreamAudioSourceNode | null;\n worklet?: AudioWorkletNode | null;\n blobUrl?: string | null;\n }): void {\n resources.worklet?.disconnect();\n resources.source?.disconnect();\n resources.context?.close();\n if (resources.blobUrl) URL.revokeObjectURL(resources.blobUrl);\n if (resources.audioEl) {\n resources.audioEl.pause();\n resources.audioEl.srcObject = null;\n resources.audioEl.remove();\n }\n }\n\n private cleanupPlaybackResources(resources: {\n context?: AudioContext | null;\n worklet?: AudioWorkletNode | null;\n blobUrl?: string | null;\n }): void {\n resources.worklet?.disconnect();\n resources.context?.close();\n if (resources.blobUrl) URL.revokeObjectURL(resources.blobUrl);\n }\n\n private async startAudioCapture(\n call: TelnyxCallLike,\n setupAttempt: number\n ): Promise<void> {\n // Get the remote audio track from the peer connection receiver.\n const pc = getPeerConnection(call);\n let track: MediaStreamTrack | null = null;\n\n if (pc) {\n const receivers = pc.getReceivers();\n const audioReceiver = receivers.find(\n (r: RTCRtpReceiver) => r.track?.kind === \"audio\"\n );\n track = audioReceiver?.track ?? null;\n }\n\n // Fall back to call.remoteStream\n if (!track) {\n const stream = getRemoteStream(call);\n track = stream?.getAudioTracks()?.[0] ?? null;\n }\n\n if (!track || track.readyState !== \"live\") {\n console.warn(\n \"[TelnyxCallBridge] No live audio track — audio capture skipped\"\n );\n return;\n }\n\n // Ensure the track is enabled\n track.enabled = true;\n\n const remoteStream = new MediaStream([track]);\n\n // Attach the remote stream to an <audio> element to force the browser's\n // WebRTC audio decoder to start processing incoming RTP packets. Without\n // a media element consumer, the decoder may never run despite packets\n // arriving at the transport level (totalSamplesReceived stays 0).\n const captureAudioEl = document.createElement(\"audio\");\n captureAudioEl.srcObject = remoteStream;\n captureAudioEl.autoplay = true;\n captureAudioEl.volume = 0; // silent — audio goes to AI pipeline, not speakers\n document.body.appendChild(captureAudioEl);\n try {\n await captureAudioEl.play();\n } catch (e) {\n console.warn(\"[TelnyxCallBridge] audio element play() failed:\", e);\n }\n\n // Wait for track to unmute (media won't flow until DTLS completes)\n if (track.muted) {\n console.log(\"[TelnyxCallBridge] track muted — waiting for unmute...\");\n await new Promise<void>((resolve) => {\n const onUnmute = () => {\n track.removeEventListener(\"unmute\", onUnmute);\n console.log(\"[TelnyxCallBridge] track unmuted\");\n resolve();\n };\n track.addEventListener(\"unmute\", onUnmute);\n setTimeout(() => {\n track.removeEventListener(\"unmute\", onUnmute);\n resolve();\n }, 5000);\n });\n }\n if (!this.isCurrentMediaSetup(setupAttempt)) {\n this.cleanupCaptureResources({ audioEl: captureAudioEl });\n return;\n }\n\n // Set up AudioContext for capture at 48kHz (matching WebRTC)\n const captureContext = new AudioContext({ sampleRate: 48000 });\n if (captureContext.state === \"suspended\") {\n await captureContext.resume();\n }\n if (!this.isCurrentMediaSetup(setupAttempt)) {\n this.cleanupCaptureResources({\n audioEl: captureAudioEl,\n context: captureContext\n });\n return;\n }\n\n const blob = new Blob([PCM_CAPTURE_PROCESSOR_SOURCE], {\n type: \"application/javascript\"\n });\n const captureBlobUrl = URL.createObjectURL(blob);\n await captureContext.audioWorklet.addModule(captureBlobUrl);\n if (!this.isCurrentMediaSetup(setupAttempt)) {\n this.cleanupCaptureResources({\n audioEl: captureAudioEl,\n context: captureContext,\n blobUrl: captureBlobUrl\n });\n return;\n }\n\n const captureSource = captureContext.createMediaStreamSource(remoteStream);\n const captureWorklet = new AudioWorkletNode(\n captureContext,\n \"pcm-capture-processor\"\n );\n\n const downsampleRatio = 3; // 48kHz → 16kHz\n let captureCount = 0;\n captureWorklet.port.onmessage = (event: MessageEvent) => {\n if (!(event.data instanceof Float32Array)) return;\n const raw = event.data;\n\n // Downsample 48kHz → 16kHz via linear interpolation\n const outLen = Math.floor(raw.length / downsampleRatio);\n const float32 = new Float32Array(outLen);\n for (let i = 0; i < outLen; i++) {\n const srcIdx = i * downsampleRatio;\n const idx0 = Math.floor(srcIdx);\n const idx1 = Math.min(idx0 + 1, raw.length - 1);\n const frac = srcIdx - idx0;\n float32[i] = raw[idx0] * (1 - frac) + raw[idx1] * frac;\n }\n\n const rms = computeRMS(float32);\n captureCount++;\n if (captureCount <= 5 || captureCount % 200 === 0) {\n console.log(\n `[TelnyxCallBridge] capture #${captureCount} rms=${rms.toFixed(4)} samples=${float32.length}`\n );\n }\n this.onAudioLevel?.(rms);\n const int16 = float32ToInt16(float32);\n this.onAudioData?.(int16.buffer as ArrayBuffer);\n };\n\n this.captureAudioEl = captureAudioEl;\n this.captureContext = captureContext;\n this.captureBlobUrl = captureBlobUrl;\n this.captureSource = captureSource;\n this.captureWorklet = captureWorklet;\n captureSource.connect(captureWorklet);\n captureWorklet.connect(captureContext.destination);\n\n // Start background stats monitoring to track decoder state\n if (pc) {\n this.monitorInboundStats(pc);\n }\n }\n\n private monitorInboundStats(pc: RTCPeerConnection): void {\n let count = 0;\n this.statsInterval = setInterval(async () => {\n count++;\n if (count > 10 || pc.connectionState === \"closed\") {\n if (this.statsInterval) {\n clearInterval(this.statsInterval);\n this.statsInterval = null;\n }\n return;\n }\n try {\n const stats = await pc.getStats();\n for (const [, report] of stats) {\n const inbound = report as AudioInboundRtpStats;\n if (inbound.type === \"inbound-rtp\" && inbound.kind === \"audio\") {\n console.log(\n \"[TelnyxCallBridge] inbound-rtp:\",\n `bytesRx=${inbound.bytesReceived}`,\n `pktsRx=${inbound.packetsReceived}`,\n `pktsLost=${inbound.packetsLost}`,\n `pktsDiscard=${inbound.packetsDiscarded ?? \"n/a\"}`,\n `samplesRx=${inbound.totalSamplesReceived}`,\n `jbEmit=${inbound.jitterBufferEmittedCount}`\n );\n }\n }\n } catch {\n if (this.statsInterval) {\n clearInterval(this.statsInterval);\n this.statsInterval = null;\n }\n }\n }, 2000);\n }\n\n private stopAudioCapture(): void {\n if (this.statsInterval) {\n clearInterval(this.statsInterval);\n this.statsInterval = null;\n }\n if (this.captureWorklet) {\n this.captureWorklet.disconnect();\n this.captureWorklet = null;\n }\n if (this.captureSource) {\n this.captureSource.disconnect();\n this.captureSource = null;\n }\n if (this.captureContext) {\n this.captureContext.close();\n this.captureContext = null;\n }\n if (this.captureBlobUrl) {\n URL.revokeObjectURL(this.captureBlobUrl);\n this.captureBlobUrl = null;\n }\n if (this.captureAudioEl) {\n this.captureAudioEl.pause();\n this.captureAudioEl.srcObject = null;\n this.captureAudioEl.remove();\n this.captureAudioEl = null;\n }\n }\n\n private async startAudioPlayback(\n call: TelnyxCallLike,\n setupAttempt: number\n ): Promise<void> {\n const peerConnection = getPeerConnection(call);\n if (!peerConnection) return;\n\n const playbackContext = new AudioContext({ sampleRate: 48000 });\n if (playbackContext.state === \"suspended\") {\n await playbackContext.resume();\n }\n if (!this.isCurrentMediaSetup(setupAttempt)) {\n this.cleanupPlaybackResources({ context: playbackContext });\n return;\n }\n\n const blob = new Blob([PCM_PLAYBACK_PROCESSOR_SOURCE], {\n type: \"application/javascript\"\n });\n const playbackBlobUrl = URL.createObjectURL(blob);\n await playbackContext.audioWorklet.addModule(playbackBlobUrl);\n if (!this.isCurrentMediaSetup(setupAttempt)) {\n this.cleanupPlaybackResources({\n context: playbackContext,\n blobUrl: playbackBlobUrl\n });\n return;\n }\n\n const playbackWorklet = new AudioWorkletNode(\n playbackContext,\n \"pcm-playback-processor\"\n );\n\n const destination = playbackContext.createMediaStreamDestination();\n this.playbackContext = playbackContext;\n this.playbackBlobUrl = playbackBlobUrl;\n this.playbackWorklet = playbackWorklet;\n playbackWorklet.connect(destination);\n\n const audioTrack = destination.stream.getAudioTracks()[0];\n if (audioTrack) {\n const sender = peerConnection\n .getSenders()\n .find((s: RTCRtpSender) => s.track?.kind === \"audio\");\n if (sender) {\n await sender.replaceTrack(audioTrack);\n }\n }\n }\n\n private stopAudioPlayback(): void {\n if (this.playbackWorklet) {\n this.playbackWorklet.disconnect();\n this.playbackWorklet = null;\n }\n if (this.playbackContext) {\n this.playbackContext.close();\n this.playbackContext = null;\n }\n if (this.playbackBlobUrl) {\n URL.revokeObjectURL(this.playbackBlobUrl);\n this.playbackBlobUrl = null;\n }\n }\n}\n","/**\n * Standalone voice client for PSTN phone calls via TelnyxCallBridge.\n *\n * Speaks the Cloudflare voice protocol directly over any VoiceTransport,\n * routing audio through a TelnyxCallBridge instead of browser speakers.\n * Provides the same detection, transcript, and event features as\n * VoiceClient from @cloudflare/voice/client — ported for phone use.\n *\n * **Why not VoiceClient?**\n * VoiceClient plays received audio through the browser's AudioContext.\n * For phone calls, audio must go to the bridge (WebRTC → PSTN), not\n * speakers. VoiceClient has no hook to redirect this. TelnyxPhoneClient\n * gives full control over both audio directions.\n *\n * For lowest latency, configure the server to send 16kHz mono PCM16 audio.\n * Non-PCM formats such as MP3 are decoded in the browser before playback.\n *\n * @example\n * ```typescript\n * import { WebSocketVoiceTransport } from \"@cloudflare/voice/client\";\n * import { TelnyxPhoneClient, createTelnyxVoiceConfig } from \"@cloudflare/voice-telnyx/browser\";\n *\n * const telnyx = await createTelnyxVoiceConfig({\n * jwtEndpoint: \"/api/telnyx-token\",\n * autoAnswer: true,\n * });\n *\n * const client = new TelnyxPhoneClient({\n * transport: new WebSocketVoiceTransport({ agent: \"my-voice-agent\" }),\n * bridge: telnyx.bridge,\n * });\n *\n * client.addEventListener(\"statuschange\", (s) => console.log(\"status:\", s));\n * client.connect();\n * client.addEventListener(\"connectionchange\", async (connected) => {\n * if (connected) await client.startCall();\n * });\n * ```\n */\n\nimport type {\n VoiceTransport,\n VoiceStatus,\n VoiceAudioFormat,\n VoiceRole,\n TranscriptMessage,\n VoicePipelineMetrics\n} from \"@cloudflare/voice/client\";\nimport type { TelnyxCallBridge } from \"./providers/call-bridge.js\";\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface TelnyxPhoneClientConfig {\n /** The transport for server communication (e.g. WebSocketVoiceTransport). */\n transport: VoiceTransport;\n /** The call bridge for PSTN audio I/O. */\n bridge: TelnyxCallBridge;\n /**\n * Preferred audio format sent to the server in `start_call`.\n * Must be `\"pcm16\"` for TelnyxCallBridge compatibility.\n * @default \"pcm16\"\n */\n preferredFormat?: VoiceAudioFormat;\n /** RMS threshold below which audio is considered silence. @default 0.04 */\n silenceThreshold?: number;\n /** How long silence must last before sending end_of_speech (ms). @default 500 */\n silenceDurationMs?: number;\n /** RMS threshold for detecting user speech during agent playback. @default 0.05 */\n interruptThreshold?: number;\n /** Consecutive high-RMS chunks needed to trigger an interrupt. @default 2 */\n interruptChunks?: number;\n /** Maximum transcript messages to keep in memory. @default 200 */\n maxTranscriptMessages?: number;\n}\n\nexport interface TelnyxPhoneClientEventMap {\n statuschange: VoiceStatus;\n transcriptchange: TranscriptMessage[];\n interimtranscript: string | null;\n metricschange: VoicePipelineMetrics | null;\n audiolevelchange: number;\n connectionchange: boolean;\n error: string | null;\n mutechange: boolean;\n custommessage: unknown;\n}\n\nexport type TelnyxPhoneClientEvent = keyof TelnyxPhoneClientEventMap;\n\n// ─── Implementation ─────────────────────────────────────────────────────────\n\nexport class TelnyxPhoneClient {\n // ── Public state (read via getters) ────────────────────────────────────\n private _status: VoiceStatus = \"idle\";\n private _transcript: TranscriptMessage[] = [];\n private _metrics: VoicePipelineMetrics | null = null;\n private _audioLevel = 0;\n private _isMuted = false;\n private _connected = false;\n private _error: string | null = null;\n private _interimTranscript: string | null = null;\n private _lastCustomMessage: unknown = null;\n private _audioFormat: VoiceAudioFormat | null = null;\n private _serverProtocolVersion: number | null = null;\n\n // ── Internal state ─────────────────────────────────────────────────────\n private inCall = false;\n private isPlaying = false;\n private isSpeaking = false;\n private silenceTimer: ReturnType<typeof setTimeout> | null = null;\n private interruptChunkCount = 0;\n private warnedFormat = false;\n private listeners = new Map<string, Set<Function>>();\n\n // ── Config ─────────────────────────────────────────────────────────────\n private transport: VoiceTransport;\n private bridge: TelnyxCallBridge;\n private preferredFormat: VoiceAudioFormat;\n private silenceThreshold: number;\n private silenceDurationMs: number;\n private interruptThreshold: number;\n private interruptChunks: number;\n private maxTranscriptMessages: number;\n\n constructor(config: TelnyxPhoneClientConfig) {\n this.transport = config.transport;\n this.bridge = config.bridge;\n this.preferredFormat = config.preferredFormat ?? \"pcm16\";\n this.silenceThreshold = config.silenceThreshold ?? 0.04;\n this.silenceDurationMs = config.silenceDurationMs ?? 500;\n this.interruptThreshold = config.interruptThreshold ?? 0.05;\n this.interruptChunks = config.interruptChunks ?? 2;\n this.maxTranscriptMessages = config.maxTranscriptMessages ?? 200;\n }\n\n // ─── Getters ──────────────────────────────────────────────────────────\n\n get status(): VoiceStatus {\n return this._status;\n }\n get transcript(): TranscriptMessage[] {\n return this._transcript;\n }\n get metrics(): VoicePipelineMetrics | null {\n return this._metrics;\n }\n get audioLevel(): number {\n return this._audioLevel;\n }\n get isMuted(): boolean {\n return this._isMuted;\n }\n get connected(): boolean {\n return this._connected;\n }\n get error(): string | null {\n return this._error;\n }\n get interimTranscript(): string | null {\n return this._interimTranscript;\n }\n get lastCustomMessage(): unknown {\n return this._lastCustomMessage;\n }\n get audioFormat(): VoiceAudioFormat | null {\n return this._audioFormat;\n }\n get serverProtocolVersion(): number | null {\n return this._serverProtocolVersion;\n }\n\n // ─── Events ───────────────────────────────────────────────────────────\n\n addEventListener<K extends TelnyxPhoneClientEvent>(\n event: K,\n listener: (data: TelnyxPhoneClientEventMap[K]) => void\n ): void {\n let set = this.listeners.get(event);\n if (!set) {\n set = new Set();\n this.listeners.set(event, set);\n }\n set.add(listener);\n }\n\n removeEventListener<K extends TelnyxPhoneClientEvent>(\n event: K,\n listener: (data: TelnyxPhoneClientEventMap[K]) => void\n ): void {\n this.listeners.get(event)?.delete(listener);\n }\n\n private emit<K extends TelnyxPhoneClientEvent>(\n event: K,\n data: TelnyxPhoneClientEventMap[K]\n ): void {\n const set = this.listeners.get(event);\n if (set) for (const fn of set) (fn as Function)(data);\n }\n\n // ─── Lifecycle ────────────────────────────────────────────────────────\n\n /** Open the transport connection and send the protocol handshake. */\n connect(): void {\n this.transport.onopen = () => {\n this._connected = true;\n this._error = null;\n this.transport.sendJSON({ type: \"hello\", protocol_version: 1 });\n this.emit(\"connectionchange\", true);\n this.emit(\"error\", null);\n // If we were already in a call when reconnecting, re-send start_call\n if (this.inCall) this.transport.sendJSON(this.createStartCallMessage());\n };\n\n this.transport.onclose = () => {\n this._connected = false;\n this.emit(\"connectionchange\", false);\n };\n\n this.transport.onerror = () => {\n this._error = \"Connection lost. Reconnecting...\";\n this.emit(\"error\", this._error);\n };\n\n this.transport.onmessage = (data) => {\n if (typeof data === \"string\") {\n this.handleJSON(data);\n } else if (data instanceof ArrayBuffer) {\n this.handleAudio(data);\n } else if (data instanceof Blob) {\n data.arrayBuffer().then((buf) => this.handleAudio(buf));\n }\n };\n\n this.transport.connect();\n }\n\n /** End any active call, then close the transport. */\n disconnect(): void {\n this.endCall();\n this.transport.disconnect();\n this._connected = false;\n this.emit(\"connectionchange\", false);\n }\n\n /**\n * Start a voice call. Wires up the bridge audio callbacks,\n * starts the bridge, and sends `start_call` to the server.\n *\n * The bridge's `start()` is called here — do not call it separately.\n */\n async startCall(): Promise<void> {\n if (!this.transport.connected) {\n this._error = \"Cannot start call: not connected. Call connect() first.\";\n this.emit(\"error\", this._error);\n return;\n }\n\n this.inCall = true;\n this._error = null;\n this._metrics = null;\n this.emit(\"error\", null);\n this.emit(\"metricschange\", null);\n\n this.transport.sendJSON(this.createStartCallMessage());\n\n // Wire bridge → server audio pipeline\n this.bridge.onAudioLevel = (rms) => this.processAudioLevel(rms);\n this.bridge.onAudioData = (pcm) => {\n if (this.transport.connected && !this._isMuted) {\n this.transport.sendBinary(pcm);\n }\n };\n\n await this.bridge.start();\n }\n\n /**\n * End the voice call. Detaches audio callbacks from the bridge\n * and sends `end_call` to the server.\n *\n * Does NOT stop the bridge or hang up the phone — call\n * `bridge.stop()` or `cleanup()` separately for that.\n */\n endCall(): void {\n this.inCall = false;\n\n if (this.transport.connected) {\n this.transport.sendJSON({ type: \"end_call\" });\n }\n\n // Detach bridge callbacks but don't stop the bridge itself —\n // the phone call may need to continue independently.\n this.bridge.onAudioLevel = null;\n if (this.bridge.onAudioData !== undefined) {\n this.bridge.onAudioData = null;\n }\n\n this.isPlaying = false;\n this.resetDetection();\n this._status = \"idle\";\n this.emit(\"statuschange\", \"idle\");\n }\n\n /** Toggle mute. When muted, audio is not sent to the server. */\n toggleMute(): void {\n this._isMuted = !this._isMuted;\n\n if (this._isMuted) {\n this._audioLevel = 0;\n this.emit(\"audiolevelchange\", 0);\n }\n\n // If muted while speaking, end the speech boundary\n if (this._isMuted && this.isSpeaking) {\n this.isSpeaking = false;\n if (this.silenceTimer) {\n clearTimeout(this.silenceTimer);\n this.silenceTimer = null;\n }\n if (this.transport.connected) {\n this.transport.sendJSON({ type: \"end_of_speech\" });\n }\n }\n\n this.emit(\"mutechange\", this._isMuted);\n }\n\n /** Send a text message to the agent (bypasses STT, goes to onTurn). */\n sendText(text: string): void {\n if (this.transport.connected) {\n this.transport.sendJSON({ type: \"text_message\", text });\n }\n }\n\n /** Send arbitrary JSON to the agent (app-level messages). */\n sendJSON(data: Record<string, unknown>): void {\n if (this.transport.connected) this.transport.sendJSON(data);\n }\n\n private createStartCallMessage(): Record<string, unknown> {\n const startMsg: Record<string, unknown> = { type: \"start_call\" };\n if (this.preferredFormat) startMsg.preferred_format = this.preferredFormat;\n return startMsg;\n }\n\n // ─── Server Message Handling ──────────────────────────────────────────\n\n private handleJSON(raw: string): void {\n let msg: Record<string, unknown>;\n try {\n msg = JSON.parse(raw);\n } catch {\n return;\n }\n\n switch (msg.type) {\n case \"welcome\":\n this._serverProtocolVersion = msg.protocol_version as number;\n if (msg.protocol_version !== 1) {\n console.warn(\n `[TelnyxPhoneClient] Protocol version mismatch: ` +\n `client=1, server=${msg.protocol_version}`\n );\n }\n break;\n\n case \"audio_config\":\n this._audioFormat = msg.format as VoiceAudioFormat;\n this.warnedFormat = false;\n break;\n\n case \"status\":\n this._status = msg.status as VoiceStatus;\n // Track server-side playback state for interrupt detection\n this.isPlaying = msg.status === \"speaking\";\n if (msg.status === \"listening\" || msg.status === \"idle\") {\n this._error = null;\n this.emit(\"error\", null);\n }\n this.emit(\"statuschange\", this._status);\n break;\n\n case \"transcript_interim\":\n this._interimTranscript = msg.text as string;\n this.emit(\"interimtranscript\", this._interimTranscript);\n break;\n\n case \"transcript\": {\n this._interimTranscript = null;\n this.emit(\"interimtranscript\", null);\n // User transcript during playback → clear playback buffer\n if ((msg.role as string) === \"user\" && this.isPlaying) {\n this.isPlaying = false;\n this.bridge.clearPlaybackBuffer();\n }\n this._transcript = [\n ...this._transcript,\n {\n role: msg.role as VoiceRole,\n text: msg.text as string,\n timestamp: Date.now()\n }\n ];\n this.trimTranscript();\n this.emit(\"transcriptchange\", this._transcript);\n break;\n }\n\n case \"transcript_start\":\n this._transcript = [\n ...this._transcript,\n { role: \"assistant\" as VoiceRole, text: \"\", timestamp: Date.now() }\n ];\n this.trimTranscript();\n this.emit(\"transcriptchange\", this._transcript);\n break;\n\n case \"transcript_delta\": {\n if (this._transcript.length === 0) break;\n const updated = [...this._transcript];\n const last = updated[updated.length - 1];\n if (last.role === \"assistant\") {\n updated[updated.length - 1] = {\n ...last,\n text: last.text + (msg.text as string)\n };\n this._transcript = updated;\n this.emit(\"transcriptchange\", this._transcript);\n }\n break;\n }\n\n case \"transcript_end\": {\n if (this._transcript.length === 0) break;\n const updated = [...this._transcript];\n const last = updated[updated.length - 1];\n if (last.role === \"assistant\") {\n updated[updated.length - 1] = {\n ...last,\n text: msg.text as string\n };\n this._transcript = updated;\n this.emit(\"transcriptchange\", this._transcript);\n }\n break;\n }\n\n case \"metrics\":\n this._metrics = {\n llm_ms: msg.llm_ms as number,\n tts_ms: msg.tts_ms as number,\n first_audio_ms: msg.first_audio_ms as number,\n total_ms: msg.total_ms as number\n };\n this.emit(\"metricschange\", this._metrics);\n break;\n\n case \"error\":\n this._error = msg.message as string;\n this.emit(\"error\", this._error);\n break;\n\n default:\n this._lastCustomMessage = msg;\n this.emit(\"custommessage\", msg);\n break;\n }\n }\n\n private handleAudio(audio: ArrayBuffer): void {\n if (this._audioFormat === \"pcm16\" || this._audioFormat === null) {\n this.bridge.playAudio(audio);\n } else {\n // Non-PCM format (mp3, wav, opus) — decode to PCM16 via AudioContext\n this.decodeAndPlay(audio);\n }\n }\n\n private decodeContext: AudioContext | null = null;\n\n private async decodeAndPlay(audio: ArrayBuffer): Promise<void> {\n try {\n if (!this.decodeContext) {\n this.decodeContext = new AudioContext({ sampleRate: 16000 });\n }\n const decoded = await this.decodeContext.decodeAudioData(audio.slice(0));\n const float32 = decoded.getChannelData(0);\n // Convert float32 → Int16 PCM\n const int16 = new Int16Array(float32.length);\n for (let i = 0; i < float32.length; i++) {\n const s = Math.max(-1, Math.min(1, float32[i]));\n int16[i] = s < 0 ? s * 0x8000 : s * 0x7fff;\n }\n this.bridge.playAudio(int16.buffer as ArrayBuffer);\n } catch (err) {\n if (!this.warnedFormat) {\n this.warnedFormat = true;\n console.warn(\n `[TelnyxPhoneClient] Failed to decode \"${this._audioFormat}\" audio:`,\n err\n );\n }\n }\n }\n\n // ─── Silence & Interrupt Detection ────────────────────────────────────\n //\n // Ported from VoiceClient (@cloudflare/voice/client).\n // Same thresholds, same logic, same protocol messages.\n\n private processAudioLevel(rms: number): void {\n if (this._isMuted) return;\n\n this._audioLevel = rms;\n this.emit(\"audiolevelchange\", rms);\n\n // ── Interrupt detection ──\n // If the agent is speaking and the caller talks over it,\n // send an interrupt to stop the agent response.\n if (this.isPlaying && rms > this.interruptThreshold) {\n this.interruptChunkCount++;\n if (this.interruptChunkCount >= this.interruptChunks) {\n this.isPlaying = false;\n this.interruptChunkCount = 0;\n this.bridge.clearPlaybackBuffer();\n if (this.transport.connected) {\n this.transport.sendJSON({ type: \"interrupt\" });\n }\n }\n } else {\n this.interruptChunkCount = 0;\n }\n\n // ── Speech boundary detection ──\n // Detect when the user starts/stops speaking and signal the server.\n if (rms > this.silenceThreshold) {\n if (!this.isSpeaking) {\n this.isSpeaking = true;\n if (this.transport.connected) {\n this.transport.sendJSON({ type: \"start_of_speech\" });\n }\n }\n if (this.silenceTimer) {\n clearTimeout(this.silenceTimer);\n this.silenceTimer = null;\n }\n } else if (this.isSpeaking) {\n if (!this.silenceTimer) {\n this.silenceTimer = setTimeout(() => {\n this.isSpeaking = false;\n this.silenceTimer = null;\n if (this.transport.connected) {\n this.transport.sendJSON({ type: \"end_of_speech\" });\n }\n }, this.silenceDurationMs);\n }\n }\n }\n\n private resetDetection(): void {\n if (this.silenceTimer) {\n clearTimeout(this.silenceTimer);\n this.silenceTimer = null;\n }\n this.isSpeaking = false;\n this.interruptChunkCount = 0;\n this._audioLevel = 0;\n this.emit(\"audiolevelchange\", 0);\n }\n\n private trimTranscript(): void {\n if (this._transcript.length > this.maxTranscriptMessages) {\n this._transcript = this._transcript.slice(-this.maxTranscriptMessages);\n }\n }\n}\n","/**\n * Voice transport wrapper that intercepts server audio and routes it\n * to a TelnyxCallBridge for PSTN playback.\n *\n * Wraps any VoiceTransport from @cloudflare/voice/client, forwarding\n * all messages to VoiceClient while also feeding binary audio into the\n * phone bridge. VoiceClient still receives everything — it manages\n * status, interrupts, and transcript state normally.\n *\n * **Important:** The server-side agent should use `audioFormat: \"pcm16\"`\n * in VoiceAgentOptions. TelnyxCallBridge.playAudio() expects 16kHz mono\n * Int16 LE PCM, which matches pcm16. Other formats (mp3, opus, wav)\n * require decoding before the bridge can play them, and this transport\n * does not decode — it will log a warning and skip bridge routing.\n *\n * @example\n * ```typescript\n * import { WebSocketVoiceTransport, VoiceClient } from \"@cloudflare/voice/client\";\n * import { TelnyxPhoneTransport, createTelnyxVoiceConfig } from \"@cloudflare/voice-telnyx/browser\";\n *\n * const telnyx = await createTelnyxVoiceConfig({\n * jwtEndpoint: \"/api/telnyx-token\",\n * autoAnswer: true,\n * });\n *\n * const transport = new TelnyxPhoneTransport({\n * inner: new WebSocketVoiceTransport({ agent: \"my-voice-agent\" }),\n * bridge: telnyx.bridge,\n * });\n *\n * const voiceClient = new VoiceClient({\n * agent: \"my-voice-agent\",\n * audioInput: telnyx.audioInput,\n * transport,\n * preferredFormat: \"pcm16\",\n * });\n *\n * voiceClient.connect();\n * voiceClient.addEventListener(\"connectionchange\", async (connected) => {\n * if (connected) await voiceClient.startCall();\n * });\n * ```\n */\n\nimport type { VoiceTransport } from \"@cloudflare/voice/client\";\nimport type { TelnyxCallBridge } from \"../providers/call-bridge.js\";\n\nexport interface TelnyxPhoneTransportConfig {\n /**\n * The underlying transport to wrap. Typically a `WebSocketVoiceTransport`\n * from @cloudflare/voice/client.\n */\n inner: VoiceTransport;\n /** The call bridge to route audio into. */\n bridge: TelnyxCallBridge;\n /**\n * Optional callback for every binary audio frame received from the server.\n * Called with the raw ArrayBuffer regardless of format.\n */\n onServerAudio?: (audio: ArrayBuffer) => void;\n}\n\nexport class TelnyxPhoneTransport implements VoiceTransport {\n private inner: VoiceTransport;\n private bridge: TelnyxCallBridge;\n private audioFormat: string | null = null;\n private warnedFormat = false;\n private userAudioCallback?: (audio: ArrayBuffer) => void;\n\n // VoiceTransport callbacks — set by VoiceClient before connect()\n onopen: (() => void) | null = null;\n onclose: (() => void) | null = null;\n onerror: ((error?: unknown) => void) | null = null;\n onmessage: ((data: string | ArrayBuffer | Blob) => void) | null = null;\n\n constructor(config: TelnyxPhoneTransportConfig) {\n this.inner = config.inner;\n this.bridge = config.bridge;\n this.userAudioCallback = config.onServerAudio;\n }\n\n get connected(): boolean {\n return this.inner.connected;\n }\n\n sendJSON(data: Record<string, unknown>): void {\n this.inner.sendJSON(data);\n }\n\n sendBinary(data: ArrayBuffer): void {\n this.inner.sendBinary(data);\n }\n\n connect(): void {\n // Proxy inner transport callbacks through our properties so\n // VoiceClient (which set them before connect) receives events.\n this.inner.onopen = () => this.onopen?.();\n this.inner.onclose = () => this.onclose?.();\n this.inner.onerror = (err) => this.onerror?.(err);\n this.inner.onmessage = (data) => {\n this.intercept(data);\n // Always forward to VoiceClient — it needs all messages for\n // status management, interrupt detection, transcript, etc.\n this.onmessage?.(data);\n };\n this.inner.connect();\n }\n\n disconnect(): void {\n this.inner.disconnect();\n }\n\n // ─── Private ──────────────────────────────────────────────────────────\n\n private intercept(data: string | ArrayBuffer | Blob): void {\n if (typeof data === \"string\") {\n this.trackAudioConfig(data);\n } else if (data instanceof ArrayBuffer) {\n this.routeAudio(data);\n } else if (data instanceof Blob) {\n data.arrayBuffer().then((buf) => this.routeAudio(buf));\n }\n }\n\n /** Parse audio_config messages to know what format the server is sending. */\n private trackAudioConfig(json: string): void {\n try {\n const msg = JSON.parse(json) as { type?: string; format?: string };\n if (msg.type === \"audio_config\" && msg.format) {\n this.audioFormat = msg.format;\n this.warnedFormat = false;\n }\n } catch {\n /* not JSON — ignore */\n }\n }\n\n /** Fork audio to the bridge (pcm16 only) and optional user callback. */\n private routeAudio(audio: ArrayBuffer): void {\n this.userAudioCallback?.(audio);\n\n if (this.audioFormat === \"pcm16\" || this.audioFormat === null) {\n this.bridge.playAudio(audio);\n } else if (this.audioFormat && !this.warnedFormat) {\n this.warnedFormat = true;\n console.warn(\n `[TelnyxPhoneTransport] Server audio format is \"${this.audioFormat}\". ` +\n `TelnyxCallBridge expects pcm16 (16kHz mono Int16 LE). ` +\n `Set audioFormat: \"pcm16\" in your server-side VoiceAgentOptions.`\n );\n }\n }\n}\n","/**\n * Factory helper that wires up JWT auth + TelnyxCallBridge\n * into a ready-to-use VoiceClient configuration.\n *\n * Usage:\n * ```typescript\n * import { createTelnyxVoiceConfig } from \"@cloudflare/voice-telnyx/browser\";\n * import { VoiceClient } from \"@cloudflare/voice/client\";\n *\n * const telnyx = await createTelnyxVoiceConfig({\n * jwtEndpoint: \"/api/telnyx-token\",\n * autoAnswer: true,\n * });\n *\n * const voiceClient = new VoiceClient({\n * agent: \"my-agent\",\n * audioInput: telnyx.audioInput,\n * });\n *\n * // Inject agent audio back into the phone call:\n * voiceClient.on(\"audio\", (pcm) => telnyx.bridge.playAudio(pcm));\n *\n * // On disconnect, clean up server-side credential:\n * await telnyx.cleanup();\n * ```\n */\n\nimport {\n TelnyxCallBridge,\n type TelnyxCallBridgeConfig\n} from \"../providers/call-bridge.js\";\n\nexport interface TelnyxVoiceConfigOptions {\n /** URL of the JWT endpoint (the TelnyxJWTEndpoint handler). */\n jwtEndpoint: string;\n /** Automatically answer inbound calls. @default false */\n autoAnswer?: boolean;\n /** Enable Telnyx SDK debug logging. @default false */\n debug?: boolean;\n}\n\nexport interface TelnyxVoiceSetup {\n /** The TelnyxCallBridge instance — use for playAudio(), dial(), hangup(), etc. */\n bridge: TelnyxCallBridge;\n /** Pass this to VoiceClientOptions.audioInput. Same as `bridge`. */\n audioInput: TelnyxCallBridge;\n /** The server-side credential ID (for manual cleanup if needed). */\n credentialId: string;\n /** The SIP username (e.g. \"genCredXYZ123\") — call this to reach the agent. */\n sipUsername: string;\n /** Stop the bridge and revoke the server-side credential. */\n cleanup: () => Promise<void>;\n}\n\n/**\n * Fetch a JWT from the server, create a TelnyxCallBridge, and return\n * everything needed to configure a VoiceClient for phone calls.\n */\nexport async function createTelnyxVoiceConfig(\n options: TelnyxVoiceConfigOptions\n): Promise<TelnyxVoiceSetup> {\n // Fetch JWT from the server-side endpoint\n const response = await fetch(options.jwtEndpoint, {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" }\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch JWT: ${response.status}`);\n }\n\n const body = (await response.json()) as {\n token?: string;\n credentialId?: string;\n sipUsername?: string;\n };\n\n if (!body.token) {\n throw new Error(\"JWT response missing token\");\n }\n\n const credentialId = body.credentialId ?? \"\";\n const sipUsername = body.sipUsername ?? \"\";\n\n // Create the bridge with the fetched token\n const bridgeConfig: TelnyxCallBridgeConfig = {\n loginToken: body.token,\n autoAnswer: options.autoAnswer,\n debug: options.debug\n };\n\n const bridge = new TelnyxCallBridge(bridgeConfig);\n\n // Cleanup function: stop the bridge + revoke the server-side credential\n const cleanup = async (): Promise<void> => {\n bridge.stop();\n if (credentialId) {\n const response = await fetch(options.jwtEndpoint, {\n method: \"DELETE\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ credentialId })\n });\n if (!response.ok) {\n throw new Error(\n `Failed to revoke Telnyx credential: ${response.status}`\n );\n }\n }\n };\n\n return {\n bridge,\n audioInput: bridge,\n credentialId,\n sipUsername,\n cleanup\n };\n}\n"],"mappings":"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"}
@@ -0,0 +1,11 @@
1
+ //#region src/client.ts
2
+ var TelnyxClient = class {
3
+ constructor(config) {
4
+ this.apiKey = config.apiKey;
5
+ this.baseUrl = config.baseUrl ?? "https://api.telnyx.com/v2";
6
+ }
7
+ };
8
+ //#endregion
9
+ export { TelnyxClient as t };
10
+
11
+ //# sourceMappingURL=client-C14kiRwB.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-C14kiRwB.js","names":[],"sources":["../src/client.ts"],"sourcesContent":["/**\n * Shared Telnyx client configuration.\n * All providers use this for authentication and connection setup.\n */\n\nexport interface TelnyxClientConfig {\n /** Telnyx API key (from portal or API key management) */\n apiKey: string;\n /** Optional: Override base URL for Telnyx API */\n baseUrl?: string;\n}\n\nexport class TelnyxClient {\n readonly apiKey: string;\n readonly baseUrl: string;\n\n constructor(config: TelnyxClientConfig) {\n this.apiKey = config.apiKey;\n this.baseUrl = config.baseUrl ?? \"https://api.telnyx.com/v2\";\n }\n}\n"],"mappings":";AAYA,IAAa,eAAb,MAA0B;CAIxB,YAAY,QAA4B;EACtC,KAAK,SAAS,OAAO;EACrB,KAAK,UAAU,OAAO,WAAW;CACnC;AACF"}
@@ -0,0 +1,19 @@
1
+ //#region src/client.d.ts
2
+ /**
3
+ * Shared Telnyx client configuration.
4
+ * All providers use this for authentication and connection setup.
5
+ */
6
+ interface TelnyxClientConfig {
7
+ /** Telnyx API key (from portal or API key management) */
8
+ apiKey: string;
9
+ /** Optional: Override base URL for Telnyx API */
10
+ baseUrl?: string;
11
+ }
12
+ declare class TelnyxClient {
13
+ readonly apiKey: string;
14
+ readonly baseUrl: string;
15
+ constructor(config: TelnyxClientConfig);
16
+ }
17
+ //#endregion
18
+ export { TelnyxClientConfig as n, TelnyxClient as t };
19
+ //# sourceMappingURL=client-tnkkrw_G.d.ts.map
@@ -0,0 +1,97 @@
1
+ import {
2
+ n as TelnyxClientConfig,
3
+ t as TelnyxClient
4
+ } from "./client-tnkkrw_G.js";
5
+ import {
6
+ n as TelnyxSTTConfig,
7
+ r as TelnyxSTTSessionOptions,
8
+ t as TelnyxSTT
9
+ } from "./stt-HkxzilR4.js";
10
+ import { n as TelnyxTTSConfig, t as TelnyxTTS } from "./tts-D73UUcew.js";
11
+
12
+ //#region src/server/jwt-endpoint.d.ts
13
+ /**
14
+ * Server-side JWT endpoint for Telnyx WebRTC authentication.
15
+ *
16
+ * Wraps the Telnyx telephony credentials API so that the browser
17
+ * can obtain a JWT without ever seeing the API key. Mount the
18
+ * `handleRequest` method in a Cloudflare Worker route.
19
+ *
20
+ * This helper is intentionally closed by default: configure an `authorize`
21
+ * callback (recommended) before mounting it in a public Worker route. For
22
+ * local demos only, set `allowUnauthenticated: true` explicitly.
23
+ */
24
+ interface TelnyxJWTEndpointConfig {
25
+ /** Telnyx API key (server-side secret — never send to the browser). */
26
+ apiKey: string;
27
+ /** The credential connection ID that new telephony credentials are created under. */
28
+ credentialConnectionId: string;
29
+ /** Override the Telnyx API base URL. @default "https://api.telnyx.com/v2" */
30
+ baseUrl?: string;
31
+ /**
32
+ * Authorize a request before creating or deleting credentials.
33
+ * Use this to check your app session, signed token, or other auth state.
34
+ */
35
+ authorize?: (request: Request) => boolean | Promise<boolean>;
36
+ /**
37
+ * Allowed browser origins for CORS. If omitted, no CORS origin is emitted.
38
+ * Use exact origins such as `https://example.com`.
39
+ */
40
+ allowedOrigins?: string[];
41
+ /**
42
+ * Explicit opt-in for unauthenticated token creation. Only use for local demos.
43
+ * @default false
44
+ */
45
+ allowUnauthenticated?: boolean;
46
+ }
47
+ declare class TelnyxJWTEndpoint {
48
+ private readonly apiKey;
49
+ private readonly credentialConnectionId;
50
+ private readonly baseUrl;
51
+ private readonly authorize?;
52
+ private readonly allowedOrigins;
53
+ private readonly allowUnauthenticated;
54
+ constructor(config: TelnyxJWTEndpointConfig);
55
+ /**
56
+ * Create a telephony credential and generate a JWT token.
57
+ * This calls two Telnyx APIs in sequence:
58
+ * 1. POST /v2/telephony_credentials — creates a credential under the connection
59
+ * 2. POST /v2/telephony_credentials/:id/token — generates a short-lived JWT
60
+ *
61
+ * @returns The JWT token string and the credential ID (for later revocation).
62
+ */
63
+ createToken(): Promise<{
64
+ token: string;
65
+ credentialId: string;
66
+ sipUsername: string;
67
+ }>;
68
+ /**
69
+ * Delete a telephony credential, invalidating its JWT.
70
+ * Call this when a session ends to clean up server-side resources.
71
+ */
72
+ revokeCredential(credentialId: string): Promise<void>;
73
+ /**
74
+ * HTTP request handler for Cloudflare Workers (or any Request/Response runtime).
75
+ *
76
+ * - `POST` → creates a credential + JWT, returns `{ token, credentialId }`
77
+ * - `DELETE` → revokes a credential (body: `{ credentialId }`)
78
+ * - `OPTIONS` → CORS preflight
79
+ */
80
+ handleRequest(request: Request): Promise<Response>;
81
+ private authorizeRequest;
82
+ private telephonyCredentialUrl;
83
+ private corsHeaders;
84
+ }
85
+ //#endregion
86
+ export {
87
+ TelnyxClient,
88
+ type TelnyxClientConfig,
89
+ TelnyxJWTEndpoint,
90
+ type TelnyxJWTEndpointConfig,
91
+ TelnyxSTT,
92
+ type TelnyxSTTConfig,
93
+ type TelnyxSTTSessionOptions,
94
+ TelnyxTTS,
95
+ type TelnyxTTSConfig
96
+ };
97
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,180 @@
1
+ import { t as TelnyxClient } from "./client-C14kiRwB.js";
2
+ import { t as TelnyxSTT } from "./stt-DPden1_S.js";
3
+ import { t as TelnyxTTS } from "./tts-BNsM0WUr.js";
4
+ //#region src/server/jwt-endpoint.ts
5
+ var TelnyxJWTEndpoint = class {
6
+ constructor(config) {
7
+ this.apiKey = config.apiKey;
8
+ this.credentialConnectionId = config.credentialConnectionId;
9
+ this.baseUrl = config.baseUrl ?? "https://api.telnyx.com/v2";
10
+ this.authorize = config.authorize;
11
+ this.allowedOrigins = config.allowedOrigins ?? [];
12
+ this.allowUnauthenticated = config.allowUnauthenticated ?? false;
13
+ }
14
+ /**
15
+ * Create a telephony credential and generate a JWT token.
16
+ * This calls two Telnyx APIs in sequence:
17
+ * 1. POST /v2/telephony_credentials — creates a credential under the connection
18
+ * 2. POST /v2/telephony_credentials/:id/token — generates a short-lived JWT
19
+ *
20
+ * @returns The JWT token string and the credential ID (for later revocation).
21
+ */
22
+ async createToken() {
23
+ const credResponse = await fetch(`${this.baseUrl}/telephony_credentials`, {
24
+ method: "POST",
25
+ headers: {
26
+ Authorization: `Bearer ${this.apiKey}`,
27
+ "Content-Type": "application/json"
28
+ },
29
+ body: JSON.stringify({ connection_id: this.credentialConnectionId })
30
+ });
31
+ if (!credResponse.ok) throw new Error(`Failed to create telephony credential: ${credResponse.status}`);
32
+ const credBody = await credResponse.json();
33
+ const credentialId = credBody.data.id;
34
+ const sipUsername = credBody.data.sip_username;
35
+ try {
36
+ const tokenResponse = await fetch(`${this.telephonyCredentialUrl(credentialId)}/token`, {
37
+ method: "POST",
38
+ headers: {
39
+ Authorization: `Bearer ${this.apiKey}`,
40
+ "Content-Type": "application/json"
41
+ }
42
+ });
43
+ if (!tokenResponse.ok) throw new Error(`Failed to generate JWT: ${tokenResponse.status}`);
44
+ const tokenText = await tokenResponse.text();
45
+ let token;
46
+ try {
47
+ const parsed = JSON.parse(tokenText);
48
+ token = typeof parsed === "string" ? parsed : typeof parsed === "object" && parsed !== null && "data" in parsed && typeof parsed.data === "string" ? parsed.data : tokenText;
49
+ } catch {
50
+ token = tokenText;
51
+ }
52
+ return {
53
+ token,
54
+ credentialId,
55
+ sipUsername
56
+ };
57
+ } catch (error) {
58
+ try {
59
+ await this.revokeCredential(credentialId);
60
+ } catch (revokeError) {
61
+ console.warn(`Failed to revoke Telnyx credential ${credentialId} after token creation failed:`, revokeError);
62
+ }
63
+ throw error;
64
+ }
65
+ }
66
+ /**
67
+ * Delete a telephony credential, invalidating its JWT.
68
+ * Call this when a session ends to clean up server-side resources.
69
+ */
70
+ async revokeCredential(credentialId) {
71
+ const response = await fetch(this.telephonyCredentialUrl(credentialId), {
72
+ method: "DELETE",
73
+ headers: {
74
+ Authorization: `Bearer ${this.apiKey}`,
75
+ "Content-Type": "application/json"
76
+ }
77
+ });
78
+ if (!response.ok) throw new Error(`Failed to revoke credential: ${response.status}`);
79
+ }
80
+ /**
81
+ * HTTP request handler for Cloudflare Workers (or any Request/Response runtime).
82
+ *
83
+ * - `POST` → creates a credential + JWT, returns `{ token, credentialId }`
84
+ * - `DELETE` → revokes a credential (body: `{ credentialId }`)
85
+ * - `OPTIONS` → CORS preflight
86
+ */
87
+ async handleRequest(request) {
88
+ const corsHeaders = this.corsHeaders(request);
89
+ if (request.method === "OPTIONS") return new Response(null, {
90
+ status: 204,
91
+ headers: {
92
+ ...corsHeaders,
93
+ "Access-Control-Allow-Methods": "POST, DELETE, OPTIONS",
94
+ "Access-Control-Allow-Headers": "Content-Type, Authorization"
95
+ }
96
+ });
97
+ const authResponse = await this.authorizeRequest(request, corsHeaders);
98
+ if (authResponse) return authResponse;
99
+ if (request.method === "POST") try {
100
+ const result = await this.createToken();
101
+ return Response.json(result, { headers: {
102
+ ...corsHeaders,
103
+ "Content-Type": "application/json"
104
+ } });
105
+ } catch (err) {
106
+ return Response.json({ error: err.message }, {
107
+ status: 500,
108
+ headers: {
109
+ ...corsHeaders,
110
+ "Content-Type": "application/json"
111
+ }
112
+ });
113
+ }
114
+ if (request.method === "DELETE") try {
115
+ const body = await request.json();
116
+ if (!body.credentialId) return Response.json({ error: "Missing credentialId in request body" }, {
117
+ status: 400,
118
+ headers: {
119
+ ...corsHeaders,
120
+ "Content-Type": "application/json"
121
+ }
122
+ });
123
+ await this.revokeCredential(body.credentialId);
124
+ return Response.json({ ok: true }, { headers: {
125
+ ...corsHeaders,
126
+ "Content-Type": "application/json"
127
+ } });
128
+ } catch (err) {
129
+ return Response.json({ error: err.message }, {
130
+ status: 500,
131
+ headers: {
132
+ ...corsHeaders,
133
+ "Content-Type": "application/json"
134
+ }
135
+ });
136
+ }
137
+ return Response.json({ error: "Method not allowed" }, {
138
+ status: 405,
139
+ headers: {
140
+ ...corsHeaders,
141
+ "Content-Type": "application/json"
142
+ }
143
+ });
144
+ }
145
+ async authorizeRequest(request, headers) {
146
+ if (this.authorize) {
147
+ if (!await this.authorize(request)) return Response.json({ error: "Forbidden" }, {
148
+ status: 403,
149
+ headers: {
150
+ ...headers,
151
+ "Content-Type": "application/json"
152
+ }
153
+ });
154
+ return null;
155
+ }
156
+ if (this.allowUnauthenticated) return null;
157
+ return Response.json({ error: "TelnyxJWTEndpoint requires an authorize callback. Set allowUnauthenticated: true only for local demos." }, {
158
+ status: 401,
159
+ headers: {
160
+ ...headers,
161
+ "Content-Type": "application/json"
162
+ }
163
+ });
164
+ }
165
+ telephonyCredentialUrl(credentialId) {
166
+ return `${this.baseUrl}/telephony_credentials/${encodeURIComponent(credentialId)}`;
167
+ }
168
+ corsHeaders(request) {
169
+ const origin = request.headers.get("Origin");
170
+ if (!origin || !this.allowedOrigins.includes(origin)) return {};
171
+ return {
172
+ "Access-Control-Allow-Origin": origin,
173
+ Vary: "Origin"
174
+ };
175
+ }
176
+ };
177
+ //#endregion
178
+ export { TelnyxClient, TelnyxJWTEndpoint, TelnyxSTT, TelnyxTTS };
179
+
180
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/server/jwt-endpoint.ts"],"sourcesContent":["/**\n * Server-side JWT endpoint for Telnyx WebRTC authentication.\n *\n * Wraps the Telnyx telephony credentials API so that the browser\n * can obtain a JWT without ever seeing the API key. Mount the\n * `handleRequest` method in a Cloudflare Worker route.\n *\n * This helper is intentionally closed by default: configure an `authorize`\n * callback (recommended) before mounting it in a public Worker route. For\n * local demos only, set `allowUnauthenticated: true` explicitly.\n */\n\nexport interface TelnyxJWTEndpointConfig {\n /** Telnyx API key (server-side secret — never send to the browser). */\n apiKey: string;\n /** The credential connection ID that new telephony credentials are created under. */\n credentialConnectionId: string;\n /** Override the Telnyx API base URL. @default \"https://api.telnyx.com/v2\" */\n baseUrl?: string;\n /**\n * Authorize a request before creating or deleting credentials.\n * Use this to check your app session, signed token, or other auth state.\n */\n authorize?: (request: Request) => boolean | Promise<boolean>;\n /**\n * Allowed browser origins for CORS. If omitted, no CORS origin is emitted.\n * Use exact origins such as `https://example.com`.\n */\n allowedOrigins?: string[];\n /**\n * Explicit opt-in for unauthenticated token creation. Only use for local demos.\n * @default false\n */\n allowUnauthenticated?: boolean;\n}\n\nexport class TelnyxJWTEndpoint {\n private readonly apiKey: string;\n private readonly credentialConnectionId: string;\n private readonly baseUrl: string;\n private readonly authorize?: (request: Request) => boolean | Promise<boolean>;\n private readonly allowedOrigins: string[];\n private readonly allowUnauthenticated: boolean;\n\n constructor(config: TelnyxJWTEndpointConfig) {\n this.apiKey = config.apiKey;\n this.credentialConnectionId = config.credentialConnectionId;\n this.baseUrl = config.baseUrl ?? \"https://api.telnyx.com/v2\";\n this.authorize = config.authorize;\n this.allowedOrigins = config.allowedOrigins ?? [];\n this.allowUnauthenticated = config.allowUnauthenticated ?? false;\n }\n\n /**\n * Create a telephony credential and generate a JWT token.\n * This calls two Telnyx APIs in sequence:\n * 1. POST /v2/telephony_credentials — creates a credential under the connection\n * 2. POST /v2/telephony_credentials/:id/token — generates a short-lived JWT\n *\n * @returns The JWT token string and the credential ID (for later revocation).\n */\n async createToken(): Promise<{\n token: string;\n credentialId: string;\n sipUsername: string;\n }> {\n // Step 1: Create telephony credential\n const credResponse = await fetch(`${this.baseUrl}/telephony_credentials`, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\"\n },\n body: JSON.stringify({\n connection_id: this.credentialConnectionId\n })\n });\n\n if (!credResponse.ok) {\n throw new Error(\n `Failed to create telephony credential: ${credResponse.status}`\n );\n }\n\n const credBody = (await credResponse.json()) as {\n data: { id: string; sip_username: string };\n };\n const credentialId = credBody.data.id;\n const sipUsername = credBody.data.sip_username;\n\n try {\n // Step 2: Generate JWT from the credential\n const tokenResponse = await fetch(\n `${this.telephonyCredentialUrl(credentialId)}/token`,\n {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\"\n }\n }\n );\n\n if (!tokenResponse.ok) {\n throw new Error(`Failed to generate JWT: ${tokenResponse.status}`);\n }\n\n // The Telnyx token endpoint may return a raw JWT string or a JSON\n // wrapper like { data: \"eyJ...\" }. Handle both.\n const tokenText = await tokenResponse.text();\n let token: string;\n try {\n const parsed: unknown = JSON.parse(tokenText);\n token =\n typeof parsed === \"string\"\n ? parsed\n : typeof parsed === \"object\" &&\n parsed !== null &&\n \"data\" in parsed &&\n typeof parsed.data === \"string\"\n ? parsed.data\n : tokenText;\n } catch {\n // Raw JWT string (not JSON-wrapped)\n token = tokenText;\n }\n\n return { token, credentialId, sipUsername };\n } catch (error) {\n try {\n await this.revokeCredential(credentialId);\n } catch (revokeError) {\n console.warn(\n `Failed to revoke Telnyx credential ${credentialId} after token creation failed:`,\n revokeError\n );\n }\n throw error;\n }\n }\n\n /**\n * Delete a telephony credential, invalidating its JWT.\n * Call this when a session ends to clean up server-side resources.\n */\n async revokeCredential(credentialId: string): Promise<void> {\n const response = await fetch(this.telephonyCredentialUrl(credentialId), {\n method: \"DELETE\",\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n \"Content-Type\": \"application/json\"\n }\n });\n\n if (!response.ok) {\n throw new Error(`Failed to revoke credential: ${response.status}`);\n }\n }\n\n /**\n * HTTP request handler for Cloudflare Workers (or any Request/Response runtime).\n *\n * - `POST` → creates a credential + JWT, returns `{ token, credentialId }`\n * - `DELETE` → revokes a credential (body: `{ credentialId }`)\n * - `OPTIONS` → CORS preflight\n */\n async handleRequest(request: Request): Promise<Response> {\n const corsHeaders = this.corsHeaders(request);\n\n if (request.method === \"OPTIONS\") {\n return new Response(null, {\n status: 204,\n headers: {\n ...corsHeaders,\n \"Access-Control-Allow-Methods\": \"POST, DELETE, OPTIONS\",\n \"Access-Control-Allow-Headers\": \"Content-Type, Authorization\"\n }\n });\n }\n\n const authResponse = await this.authorizeRequest(request, corsHeaders);\n if (authResponse) return authResponse;\n\n if (request.method === \"POST\") {\n try {\n const result = await this.createToken();\n return Response.json(result, {\n headers: { ...corsHeaders, \"Content-Type\": \"application/json\" }\n });\n } catch (err) {\n return Response.json(\n { error: (err as Error).message },\n {\n status: 500,\n headers: { ...corsHeaders, \"Content-Type\": \"application/json\" }\n }\n );\n }\n }\n\n if (request.method === \"DELETE\") {\n try {\n const body = (await request.json()) as {\n credentialId?: string;\n };\n if (!body.credentialId) {\n return Response.json(\n { error: \"Missing credentialId in request body\" },\n {\n status: 400,\n headers: { ...corsHeaders, \"Content-Type\": \"application/json\" }\n }\n );\n }\n await this.revokeCredential(body.credentialId);\n return Response.json(\n { ok: true },\n { headers: { ...corsHeaders, \"Content-Type\": \"application/json\" } }\n );\n } catch (err) {\n return Response.json(\n { error: (err as Error).message },\n {\n status: 500,\n headers: { ...corsHeaders, \"Content-Type\": \"application/json\" }\n }\n );\n }\n }\n\n return Response.json(\n { error: \"Method not allowed\" },\n {\n status: 405,\n headers: { ...corsHeaders, \"Content-Type\": \"application/json\" }\n }\n );\n }\n\n private async authorizeRequest(\n request: Request,\n headers: HeadersInit\n ): Promise<Response | null> {\n if (this.authorize) {\n const ok = await this.authorize(request);\n if (!ok) {\n return Response.json(\n { error: \"Forbidden\" },\n {\n status: 403,\n headers: { ...headers, \"Content-Type\": \"application/json\" }\n }\n );\n }\n return null;\n }\n\n if (this.allowUnauthenticated) return null;\n\n return Response.json(\n {\n error:\n \"TelnyxJWTEndpoint requires an authorize callback. Set allowUnauthenticated: true only for local demos.\"\n },\n {\n status: 401,\n headers: { ...headers, \"Content-Type\": \"application/json\" }\n }\n );\n }\n\n private telephonyCredentialUrl(credentialId: string): string {\n return `${this.baseUrl}/telephony_credentials/${encodeURIComponent(\n credentialId\n )}`;\n }\n\n private corsHeaders(request: Request): Record<string, string> {\n const origin = request.headers.get(\"Origin\");\n if (!origin || !this.allowedOrigins.includes(origin)) return {};\n\n return {\n \"Access-Control-Allow-Origin\": origin,\n Vary: \"Origin\"\n };\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,138 @@
1
+ import { t as TelnyxClient } from "./client-C14kiRwB.js";
2
+ //#region src/providers/stt.ts
3
+ const DEFAULT_STT_URL = "wss://api.telnyx.com/v2/speech-to-text/transcription";
4
+ /**
5
+ * Build a 44-byte WAV header for streaming PCM16 audio.
6
+ * The data-size field is set to 0x7FFFFFFF since the total length is unknown.
7
+ */
8
+ function wavHeader(sampleRate, channels) {
9
+ const bitsPerSample = 16;
10
+ const byteRate = sampleRate * channels * (bitsPerSample / 8);
11
+ const blockAlign = channels * (bitsPerSample / 8);
12
+ const buf = /* @__PURE__ */ new ArrayBuffer(44);
13
+ const v = new DataView(buf);
14
+ v.setUint32(0, 1380533830);
15
+ v.setUint32(4, 2147483647, true);
16
+ v.setUint32(8, 1463899717);
17
+ v.setUint32(12, 1718449184);
18
+ v.setUint32(16, 16, true);
19
+ v.setUint16(20, 1, true);
20
+ v.setUint16(22, channels, true);
21
+ v.setUint32(24, sampleRate, true);
22
+ v.setUint32(28, byteRate, true);
23
+ v.setUint16(32, blockAlign, true);
24
+ v.setUint16(34, bitsPerSample, true);
25
+ v.setUint32(36, 1684108385);
26
+ v.setUint32(40, 2147483647, true);
27
+ return buf;
28
+ }
29
+ var TelnyxSTT = class {
30
+ constructor(config) {
31
+ this.client = new TelnyxClient(config);
32
+ this.engine = config.engine ?? "Telnyx";
33
+ this.language = config.language ?? "en";
34
+ this.inputFormat = config.inputFormat ?? "wav";
35
+ this.transcriptionModel = config.transcriptionModel;
36
+ this.interimResults = config.interimResults ?? true;
37
+ this.sttUrl = config.sttWsUrl ?? DEFAULT_STT_URL;
38
+ }
39
+ createSession(options) {
40
+ const language = options?.language ?? this.language;
41
+ return new TelnyxSTTSession({
42
+ apiKey: this.client.apiKey,
43
+ sttUrl: this.sttUrl,
44
+ engine: this.engine,
45
+ inputFormat: this.inputFormat,
46
+ transcriptionModel: this.transcriptionModel,
47
+ interimResults: this.interimResults,
48
+ language,
49
+ onInterim: options?.onInterim,
50
+ onUtterance: options?.onUtterance
51
+ });
52
+ }
53
+ };
54
+ var TelnyxSTTSession = class {
55
+ constructor(params) {
56
+ this.ws = null;
57
+ this.pendingChunks = [];
58
+ this.closed = false;
59
+ this.sentWavHeader = false;
60
+ this.onInterim = params.onInterim;
61
+ this.onUtterance = params.onUtterance;
62
+ this.inputFormat = params.inputFormat;
63
+ const url = new URL(params.sttUrl);
64
+ url.searchParams.set("transcription_engine", params.engine);
65
+ url.searchParams.set("input_format", params.inputFormat);
66
+ url.searchParams.set("language", params.language);
67
+ url.searchParams.set("interim_results", String(params.interimResults));
68
+ if (params.transcriptionModel) url.searchParams.set("transcription_model", params.transcriptionModel);
69
+ url.searchParams.set("token", params.apiKey);
70
+ this.connect(url.toString(), params.apiKey);
71
+ }
72
+ async connect(wsUrl, apiKey) {
73
+ try {
74
+ const ws = (await fetch(wsUrl.replace("wss://", "https://"), { headers: {
75
+ Upgrade: "websocket",
76
+ Authorization: `Bearer ${apiKey}`
77
+ } })).webSocket;
78
+ if (!ws) throw new Error("STT WebSocket requires the Cloudflare Workers runtime. The fetch-upgrade did not return a WebSocket pair.");
79
+ if (this.closed) {
80
+ try {
81
+ ws.accept();
82
+ ws.close();
83
+ } catch {}
84
+ return;
85
+ }
86
+ ws.addEventListener("message", (event) => {
87
+ this.handleMessage(event);
88
+ });
89
+ ws.addEventListener("error", (event) => {
90
+ console.error("[TelnyxSTT] WebSocket error:", event);
91
+ this.closed = true;
92
+ });
93
+ ws.addEventListener("close", () => {
94
+ this.closed = true;
95
+ });
96
+ ws.accept();
97
+ if (this.closed) return;
98
+ this.ws = ws;
99
+ if (this.inputFormat === "wav" && !this.sentWavHeader) {
100
+ ws.send(wavHeader(16e3, 1));
101
+ this.sentWavHeader = true;
102
+ }
103
+ for (const chunk of this.pendingChunks) ws.send(chunk);
104
+ this.pendingChunks = [];
105
+ } catch (error) {
106
+ if (error instanceof Error && error.message.includes("Cloudflare Workers")) console.error(`[TelnyxSTT] ${error.message}`);
107
+ else console.error("[TelnyxSTT] WebSocket connection failed:", error);
108
+ this.closed = true;
109
+ }
110
+ }
111
+ feed(chunk) {
112
+ if (this.closed) return;
113
+ if (this.ws) this.ws.send(chunk);
114
+ else this.pendingChunks.push(chunk);
115
+ }
116
+ close() {
117
+ if (this.closed) return;
118
+ this.closed = true;
119
+ this.pendingChunks = [];
120
+ this.ws?.close();
121
+ }
122
+ handleMessage(event) {
123
+ if (this.closed) return;
124
+ let data;
125
+ try {
126
+ data = JSON.parse(event.data);
127
+ } catch {
128
+ return;
129
+ }
130
+ if (typeof data.transcript !== "string" || data.transcript === "") return;
131
+ if (data.is_final) this.onUtterance?.(data.transcript);
132
+ else this.onInterim?.(data.transcript);
133
+ }
134
+ };
135
+ //#endregion
136
+ export { TelnyxSTT as t };
137
+
138
+ //# sourceMappingURL=stt-DPden1_S.js.map