Skip to content

feat(#76): INERT Twilio Media-Stream ↔ room WS glue (behind WAVE_TELEPHONY_STREAM) - #175

Open
yakimoto wants to merge 1 commit into
mainfrom
feat/76-telephony-ws-glue
Open

yakimoto wants to merge 1 commit into
mainfrom
feat/76-telephony-ws-glue

Conversation

@yakimoto

@yakimoto yakimoto commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

What this is

Wires the #76 Twilio Media-Stream ↔ WAVE room WebSocket bridge — the glue that finally consumes the two already-merged #76 primitives (telephony-codec.ts transcode + twilio-mediastream.ts parse/build) into a real route: wss://rt.wave.online/?room=<id>.

INERT — behind WAVE_TELEPHONY_STREAM (default "0", OFF)

When the flag is falsy/absent, maybeHandleTelephonyStream returns null and a root ?room= WS request falls through to the existing 501 catch-all — the route is not registered and no existing behavior changes. Arming it in any live env is a ◆ Jake crossing.

What's built

  • src/telephony-ws.ts — a pure TelephonyBridgeCore (injected I/O seams → fully unit-testable, no live Twilio/SFU) plus the thin route handler. Mirrors the proven RtmsBridgeCore / AgentSessionDO inject pattern.
    • Inbound (caller → room): parseTwilioFrametwilioMuLawToSfuPcm (μ-law 8k mono → 48k stereo PCM) → encodeIngestFrame on the SFU-dialed ingest socket, published via createIngestAdapter(location:"local").
    • Outbound (room → caller): room 48k-stereo PCM → sfuPcmToTwilioMuLawtwilioMediaFrame → sent back over the same WS.
    • Fail-safe throughout: parse/transcode/send errors are logged, never thrown up the socket path; frames drop (not buffered) until the SFU dials in.
  • src/dispatch-helpers.tsWAVE_TELEPHONY_STREAM env field + telephonyStreamEnabled().
  • src/route-dispatch.ts — one delegation line (leaf-module pattern, keeps the router under the file-size gate).
  • wrangler.tomlWAVE_TELEPHONY_STREAM = "0" with the INERT/◆ rationale.
  • test/telephony-ws.test.ts — both-direction transcode byte-for-byte against the pinned primitives, framing override, drop-until-connected, start/createIngest shape, and all route guard branches.

Framing default

"packet" — the symmetric-with-the-verified-egress-decoder default every local-inject caller already uses (config.framing ?? "packet" in rtms-bridge-core.ts, agent-session.ts, agent-turn.ts). Followed rtms-bridge-core.ts (the closest analog: a media bridge that createIngest(local) then sends encodeIngestFrame on the SFU-dialed socket).

The ONE remaining live-spike item (needs a human to dial in)

Marked TODO(live-spike) at the exact encodeIngestFrame call site:

  1. Send-side framing"packet" vs "raw" is inherited from agent-ingest-adapter.ts and is not yet proven against a live RoomDO inject. A real two-way phone call may flip it to "raw" (one config line, not a rewrite).
  2. Room→SFU-session resolution + ingest-endpoint minting (so the SFU actually dials our ingest socket) is DO-held live wiring the spike lands. Until armed, the target is null → the bridge accepts + transcodes the call but publishes nothing (honest INERT, never a fabricated push).

Verification

  • tsc --noEmit: clean.
  • Full suite: 83 files / 1078 tests pass (includes the new file's 15 tests).

🤖 Generated with Claude Code


Note

Medium Risk
Default-off flag keeps prod unchanged, but arming exposes a root WebSocket ingress without gateway auth; SFU publish wiring is stubbed until the live spike.

Overview
Adds a Twilio Media-Stream ↔ WAVE room WebSocket bridge at wss://rt.wave.online/?room=<id>, fully off by default via WAVE_TELEPHONY_STREAM ("0" in wrangler.toml).

When armed, TelephonyBridgeCore transcodes caller μ-law → 48k stereo PCM into the existing SFU ingest path (createIngestAdapter + encodeIngestFrame) and room audio back to Twilio frames—same pattern as RTMS/agent ingest. The route is wired through dispatch-helpers (telephonyStreamEnabled), route-dispatch, and new telephony-ws.ts.

Live spike still open: the handler uses target: null and ingestSocket: () => null, so calls can connect and transcode but do not publish until room→session resolution, ingest endpoint minting, and auth are landed. The route is not behind gatewayGate (Twilio cannot send x-wave-internal); signed stream tokens are called out as follow-up.

test/telephony-ws.test.ts covers byte-exact transcode, framing, route guards, and flag gating.

Reviewed by Cursor Bugbot for commit 231842f. Configure here.


Summary by cubic

Adds an inert Twilio Media-Stream ↔ room WebSocket bridge behind WAVE_TELEPHONY_STREAM, exposing wss://rt.wave.online/?room=<id> to transcode audio and publish via the SFU ingest path. Progress on #76 using the existing transcode and Twilio frame primitives; off by default so nothing changes in production.

  • New Features

    • src/telephony-ws.ts: TelephonyBridgeCore and route handler. Inbound: parse → μ-law→PCM transcode → encodeIngestFrame. Outbound: room PCM → μ-law → Twilio media.
    • Env flag: WAVE_TELEPHONY_STREAM with telephonyStreamEnabled(); when off, the route falls through to the 501 catch-all.
    • Router wiring in route-dispatch.ts; wrangler.toml sets the flag to "0".
    • Tests in test/telephony-ws.test.ts: both-direction transcode, framing override, drop-until-ingest, start/createIngest, and route guards.
  • Migration

    • Default OFF. To try it, set WAVE_TELEPHONY_STREAM="1" and provide SFU app creds; until room→session resolution and ingest endpoint minting land, frames drop until the SFU connects.
    • Send-side framing defaults to "packet". Validate with a real call; switch to "raw" if needed.

Written for commit 231842f. Summary will update on new commits.

Review in cubic

…ONY_STREAM

Wire the telephony bridge that consumes the two already-merged #76 primitives
(telephony-codec.ts transcode + twilio-mediastream.ts parse/build) into a real
WebSocket route: wss://rt.wave.online/?room=<id>.

INERT: fully gated behind WAVE_TELEPHONY_STREAM ([vars], default "0"). Off →
maybeHandleTelephonyStream returns null and the request falls through to the
existing 501 catch-all — the route is not registered and nothing changes.

- src/telephony-ws.ts: pure TelephonyBridgeCore (injected I/O seams) + the route
  handler. INBOUND: parseTwilioFrame → twilioMuLawToSfuPcm → encodeIngestFrame on
  the SFU-dialed ingest socket (createIngestAdapter location:"local"). OUTBOUND:
  room 48k-stereo PCM → sfuPcmToTwilioMuLaw → twilioMediaFrame back over the WS.
- Send-side framing default "packet", following rtms-bridge-core.ts / AgentSessionDO
  (every local-inject caller uses `framing ?? "packet"`). Marked TODO(live-spike)
  at the exact encodeIngestFrame call site — a live RoomDO inject may flip to "raw".
- dispatch-helpers.ts: WAVE_TELEPHONY_STREAM env field + telephonyStreamEnabled().
- route-dispatch.ts: one delegation line (leaf-module pattern, file-size gate).
- test/telephony-ws.test.ts: both-direction transcode byte-checks, framing override,
  drop-until-connected, start/createIngest, route guards. tsc clean; full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 231842f

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@cursor

cursor Bot commented Jul 13, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_088d76b8-1f61-448a-817a-31c38fbdbf8e)

@coderabbitai

coderabbitai Bot commented Jul 13, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 43 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 65c65192-9745-4f4b-9975-7cce43032e9d

📥 Commits

Reviewing files that changed from the base of the PR and between a269497 and 231842f.

📒 Files selected for processing (5)
  • src/dispatch-helpers.ts
  • src/route-dispatch.ts
  • src/telephony-ws.ts
  • test/telephony-ws.test.ts
  • wrangler.toml
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/76-telephony-ws-glue

Comment @coderabbitai help to get the list of available commands.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Risk: medium. Not approving: Cursor Bugbot completed as skipped (usage limit reached) with no clean automated review, and gate/checks failed on src/route-dispatch.ts exceeding the 800-line limit. Human review is needed before merge; requested jfineman as reviewer.

Open in Web View Automation 

Sent by Cursor Approval Agent: Pull Request Router and Approver

@wave-bugbot

wave-bugbot Bot commented Aug 11, 2026

Copy link
Copy Markdown

🌊 WAVE BugBot — 19 finding(s)

🔴 16 · 🟠 3

  • 🔴 P0 src/dispatch-helpers.ts:80 CWE-840Missing idempotency key on money mutation
    The function telephonyStreamEnabled does not include an idempotency key, which could lead to replay attacks if the same request is processed multiple times.
  • 🔴 P0 src/route-dispatch.ts:635 CWE-840Missing idempotency key on money mutation
    The function maybeHandleTelephonyStream does not include an idempotency key, which could lead to replay attacks if the same request is processed multiple times.
  • 🔴 P0 src/dispatch-helpers.ts:80 CWE-862/CWE-863Missing authentication check for WAVE_TELEPHONY_STREAM flag
    The WAVE_TELEPHONY_STREAM flag is added to the Env interface but there is no validation or authorization check in place. This could allow unauthorized access if
  • 🔴 P0 src/telephony-ws.ts:103Potential SQL injection vulnerability in createIngestAdapterForRoom
    The createIngestAdapterForRoom function imports and uses a module without parameterizing queries. This could lead to SQL injection vulnerabilities.
  • 🔴 P0 src/dispatch-helpers.ts:150Potential missing authorization check for WAVE_TELEPHONY_STREAM flag
    The function telephonyStreamEnabled does not perform any authorization checks. If this flag is set, it could potentially allow unauthorized access to the Twilio
  • 🔴 P0 src/route-dispatch.ts:635Potential missing authorization check for WAVE_TELEPHONY_STREAM flag
    The function telephonyStreamEnabled does not perform any authorization checks. If this flag is set, it could potentially allow unauthorized access to the Twilio
  • 🔴 P0 src/dispatch-helpers.ts:150Potential missing authorization for Twilio Media-Stream flag
    The function telephonyStreamEnabled does not check the caller's role or permissions before returning a value. This could allow unauthorized access to the Twil
  • 🔴 P0 src/route-dispatch.ts:635Potential missing authorization for Twilio Media-Stream flag
    The function maybeHandleTelephonyStream does not check the caller's role or permissions before processing requests. This could allow unauthorized access to th
  • 🔴 P0 src/telephony-ws.ts:103Potential missing authentication for Twilio Media-Stream route
    The maybeHandleTelephonyStream function does not perform any authentication checks on the incoming request. This could allow unauthorized access to the teleph
  • 🔴 P0 src/telephony-ws.ts:103Potential role guard missing for Twilio Media-Stream route
    The maybeHandleTelephonyStream function does not check the caller's role. This could allow unauthorized users to access sensitive operations.
  • 🔴 P0 src/telephony-ws.ts:102Potential missing authorization check for telephony routes
    The route handler maybeHandleTelephonyStream does not perform any authorization checks on the caller's identity. This could allow unauthorized access to the t
  • 🔴 P0 src/telephony-ws.ts:105 CWE-269Potential missing authentication for Twilio Media-Stream route
    The maybeHandleTelephonyStream function does not perform any authentication checks on the incoming request. This could allow unauthorized access to the teleph
  • 🔴 P0 src/telephony-ws.ts:140 CWE-269Potential missing authentication for Twilio Media-Stream route
    The maybeHandleTelephonyStream function does not perform any authentication checks on the incoming request. This could allow unauthorized access to the teleph
  • 🔴 P0 src/telephony-ws.ts:140 CWE-862Potential privilege escalation for Twilio Media-Stream route
    The maybeHandleTelephonyStream function does not check the caller's role. This could allow unauthorized users to perform actions they are not authorized to do
  • 🔴 P0 src/telephony-ws.ts:103Potential SSRF vulnerability in createIngestAdapterForRoom
    The createIngestAdapterForRoom function does not validate the URL passed to external services. This could allow an attacker to perform SSRF attacks.
  • 🔴 P0 src/telephony-ws.ts:270Potential money-path vulnerability in pushRoomAudio
    The pushRoomAudio method does not perform any authorization checks or validation before processing the audio data. This could lead to unauthorized access and
  • 🟠 P1 src/telephony-ws.ts:124Unpinned search_path in SECURITY DEFINER function
    The function createIngestAdapterForRoom is marked as SECURITY DEFINER but does not pin the search_path. This could allow an attacker to shadow objects in earl
  • 🟠 P1 src/telephony-ws.ts:206Potential crash due to unhandled exception in onMedia
    The onMedia method catches exceptions but does not handle them gracefully. If an unhandled exception occurs, it could lead to a crash.
  • 🟠 P1 src/telephony-ws.ts:103Potential search_path issue in SECURITY DEFINER function
    The createIngestAdapterForRoom function is marked as SECURITY DEFINER but does not pin the search_path. This could allow an attacker to shadow your functions.

severity: critical · major · minor · info — local review · $0 inference · wave-dispatch · react 👍/👎 to tune

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant