Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Bug report
description: Something the adapter got wrong — a bad translation, an unexpected failure, or a crash.
description: Something the toolkit got wrong — a bad translation, an unexpected failure, or a crash.
title: "[Bug]: "
labels: ["bug"]
body:
Expand All @@ -22,7 +22,7 @@ body:
attributes:
label: What happened vs. what you expected
description: >
Include the adapter's output. When it can't do something, it's designed to say so and stop
Include the toolkit's output. When it can't do something, it's designed to say so and stop
rather than fail silently — paste that message if you got one.
validations:
required: true
Expand All @@ -38,9 +38,9 @@ body:
id: environment
attributes:
label: Environment
description: Adapter version/commit, Node version, and whether you're hitting real Bandwidth or a test setup.
description: Toolkit version/commit, Node version, and whether you're hitting real Bandwidth or a test setup.
value: |
- Adapter version/commit:
- Toolkit version/commit:
- Node version:
- Target: real Bandwidth / test setup
validations:
Expand Down
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ contact_links:
For guaranteed, SLA-backed support on a production voice migration, talk to your Bandwidth
account team — this repo is best-effort and community-supported.
- name: How to contribute
url: https://github.com/Bandwidth/bw-voice-adapter/blob/main/CONTRIBUTING.md
url: https://github.com/Bandwidth/migration-toolkit/blob/main/CONTRIBUTING.md
about: Read the contributing guide before opening an issue or PR.
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/feature_request.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
name: Feature or support request
description: Ask for new verb/attribute support, a capability, or a change to how the adapter behaves.
description: Ask for new verb/attribute support, a capability, or a change to how the toolkit behaves.
title: "[Request]: "
labels: ["enhancement"]
body:
Expand All @@ -18,7 +18,7 @@ body:
- type: textarea
id: proposal
attributes:
label: What would you like the adapter to do?
label: What would you like the toolkit to do?
description: >
If it's about a specific TwiML verb/attribute or REST call, name it. Translation support is driven by
`src/matrix/twilio-voice.json`, so pointing at the exact verb/attribute helps.
Expand Down
28 changes: 14 additions & 14 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,25 @@
# AGENTS.md — Driving a Twilio→Bandwidth cutover with this adapter
# AGENTS.md — Driving a Twilio→Bandwidth cutover with this toolkit

This adapter translates a **live call flow** (Twilio TwiML ⇄ Bandwidth BXML) so a
This toolkit's **Real-Time Translator** translates a live call flow (Twilio TwiML ⇄ Bandwidth BXML) so a
customer's existing Twilio voice app runs over Bandwidth with a single URL change.
It does **not** provision anything on a Bandwidth account.

## The paired model

Use two tools together:

- **This adapter** — translates the call flow and proxies live calls.
- **This toolkit's Real-Time Translator** — translates the call flow and proxies live calls.
- **The `band` CLI** — executes account-side actions on the user's Bandwidth
account (numbers, applications, service activation). `band` is agent-native:
JSON output by default, `--plain` for stable parsing, `--wait` for async ops,
`--if-not-exists` for idempotency.

An agent runs `band` to provision, then configures and runs this adapter.
An agent runs `band` to provision, then configures and runs the translator.

## What an agent can and cannot do

**Can do unattended:** compatibility-check analysis, provisioning on an *existing*
Bandwidth account via `band`, adapter configuration, and the readiness check.
Bandwidth account via `band`, translator configuration, and the readiness check.

**🧍 Human required** (flagged inline below): creating a brand-new Bandwidth
account, enabling the account "HTTP Voice" feature, providing a public HTTPS
Expand Down Expand Up @@ -57,19 +57,19 @@ email/SMS OTP first. If `band app create` errors that the account lacks the "HTT
Voice" feature, a human must request it from Bandwidth support — the CLI cannot
enable it.

### Phase 3 — Configure the adapter
### Phase 3 — Configure the translator
Set these env vars (the server reads **these exact names** — note the checked-in
`.env` uses different, non-functional names):

| Var | Who sets it |
|---|---|
| `ADAPTER_ACCOUNT_SID`, `ADAPTER_AUTH_TOKEN` | agent (adapter's own Twilio-compat creds) |
| `TRANSLATOR_ACCOUNT_SID`, `TRANSLATOR_AUTH_TOKEN` | agent (translator's own Twilio-compat creds) |
| `CUSTOMER_VOICE_URL` | agent (the customer's unchanged Twilio app URL) |
| `WEBHOOK_USER`, `WEBHOOK_PASSWORD` | agent — Basic-auth creds Bandwidth presents on inbound `/bw/*` webhooks; **must match the `CallbackCreds` set on the BW Voice Application in Phase 2** |
| `PUBLIC_BASE_URL` | 🧍 **Human required** (public HTTPS host/tunnel) |
| `BW_ACCOUNT_ID`, `BW_CLIENT_ID`, `BW_CLIENT_SECRET`, `BW_APPLICATION_ID` | from Phase 2 |

> Non-loopback deploys: the listen host defaults to `127.0.0.1`; set `HOST=0.0.0.0` (or a specific interface) to expose the adapter behind your `PUBLIC_BASE_URL`.
> Non-loopback deploys: the listen host defaults to `127.0.0.1`; set `HOST=0.0.0.0` (or a specific interface) to expose the translator behind your `PUBLIC_BASE_URL`.

### Phase 4 — Deploy
🧍 **Human required:** provide a public HTTPS host (or tunnel) for
Expand Down Expand Up @@ -108,7 +108,7 @@ Translation is a fixed rulebook (`src/matrix/twilio-voice.json`), not a guess.
- `Connect` — the `Stream` noun maps to `StartStream` via the Media Streams
bridge; `ConversationRelay` and `VirtualAgent` are unsupported (separate
IoV).
- `Stream` — Twilio's WS message schema is emulated by the adapter's stream
- `Stream` — Twilio's WS message schema is emulated by the translator's stream
bridge; live Bandwidth-side binding requires fixture capture.
- `Conference` — basic named conferences work, but `waitUrl` hold music has
no Bandwidth equivalent, `beep` is only partially supported, and
Expand All @@ -118,10 +118,10 @@ Translation is a fixed rulebook (`src/matrix/twilio-voice.json`), not a guess.
- **Unsupported (no BXML equivalent — a business decision to drop/redesign):**
`Enqueue`, `Leave`, `Queue`, `Client`, `Pay`. Bandwidth has no queue primitive
(`Enqueue`/`Leave`/`Queue` fail loudly), no WebRTC client endpoint
(`Client`), and PCI payment capture (`Pay`) is out of scope for the adapter.
(`Client`), and PCI payment capture (`Pay`) is out of scope for the translator.
- **Dynamic SDK-built TwiML:** if the customer app generates TwiML at runtime,
there is no static markup to transpile. Run the adapter with
`ADAPTER_CAPTURE_DIR=<dir>` and place a few test calls; each customer TwiML
there is no static markup to transpile. Run the translator with
`TRANSLATOR_CAPTURE_DIR=<dir>` and place a few test calls; each customer TwiML
response is written there verbatim (content-addressed, deduped). Then
`npm run bxml-generator -- <dir> <out>` produces standalone BXML for the paths those
calls exercised. Capture only covers exercised paths — branches you never dial
Expand All @@ -130,12 +130,12 @@ Translation is a fixed rulebook (`src/matrix/twilio-voice.json`), not a guess.
## Errors

Operational failures return a Twilio-shaped JSON body: `{ code, message, more_info, status }`.
Adapter-specific codes use a private range and are documented here:
Translator-specific codes use a private range and are documented here:

| code | status | meaning |
|---|---|---|
| 90001 | 400 | Missing required request parameter |
| 90002 | 500 | Internal adapter error (detail is in server logs, not the response) |
| 90002 | 500 | Internal translator error (detail is in server logs, not the response) |
| 90003 | 4xx | Malformed request rejected before handling |
| 90004 | 400 | Request parameter present but failed validation (e.g. an unsafe identifier) |

Expand Down
10 changes: 5 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ The Migration Toolkit is best-effort and community-supported — not a fully sup
Open a GitHub issue with enough for someone to reproduce it without a back-and-forth:

- **What you were trying to do** — the TwiML/verb or REST call involved.
- **What happened vs. what you expected** — include the adapter's output. When it can't do something, it's designed to say so and stop rather than fail silently, so paste that message if you got one.
- **What happened vs. what you expected** — include the toolkit's output. When it can't do something, it's designed to say so and stop rather than fail silently, so paste that message if you got one.
- **A minimal repro** — the smallest TwiML snippet or request that triggers it. Scrub any real numbers, tokens, or customer data first.
- **Environment** — adapter version/commit, Node version, and whether you're hitting real Bandwidth or a test setup.
- **Environment** — toolkit version/commit, Node version, and whether you're hitting real Bandwidth or a test setup.

> ⚠️ **Never** paste live credentials, real customer phone numbers, recordings, or any PII into an issue or PR. Redact before you post.

Expand All @@ -34,13 +34,13 @@ Open a GitHub issue with enough for someone to reproduce it without a back-and-f

## What makes a change easy to accept

- It keeps the adapter **honest**: correct translations, and a loud, clear failure when something isn't supported — never a silently broken call.
- It keeps the toolkit **honest**: correct translations, and a loud, clear failure when something isn't supported — never a silently broken call.
- It's **tested** and the suite passes.
- It's **scoped** — one focused change per PR beats a sprawling one.
- It updates the **docs** (README capability tables especially) when it changes what the adapter can do.
- It updates the **docs** (README capability tables especially) when it changes what the toolkit can do.

## What to expect on review

Review may take a while, and a maintainer might ask you to carry a change further than you expected — more tests, a doc update, a tweak to the matrix. That bar is what keeps the adapter trustworthy on live calls.
Review may take a while, and a maintainer might ask you to carry a change further than you expected — more tests, a doc update, a tweak to the matrix. That bar is what keeps the toolkit trustworthy on live calls.

Thanks for contributing. Every solid issue and PR makes this more useful for the next person moving a voice app without rewriting it.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ npm run doctor # readiness check — see AGENTS.md Phase 5

| Env var | Meaning |
|---|---|
| `ADAPTER_ACCOUNT_SID` / `ADAPTER_AUTH_TOKEN` | What the customer's Twilio SDK + webhook-signature validation use |
| `TRANSLATOR_ACCOUNT_SID` / `TRANSLATOR_AUTH_TOKEN` | What the customer's Twilio SDK + webhook-signature validation use |
| `WEBHOOK_USER` / `WEBHOOK_PASSWORD` | Basic-auth creds Bandwidth presents on inbound `/bw/*` webhooks; set the same as your Voice app's `CallbackCreds` |
| `HOST` | Listen interface (default `127.0.0.1`); set `0.0.0.0` for containers/exposed deployments |
| `EGRESS_ALLOW_PRIVATE` | Set `1` to allow outbound fetches to private/loopback ranges (local dev only) |
Expand All @@ -96,8 +96,8 @@ npm run doctor # readiness check — see AGENTS.md Phase 5
| `CUSTOMER_VOICE_URL` | The customer's Twilio voice webhook (inbound calls) |
| `BW_ACCOUNT_ID` / `BW_CLIENT_ID` / `BW_CLIENT_SECRET` / `BW_APPLICATION_ID` | Bandwidth credentials (OAuth2 client-credentials), provisioned via `band` — see [`AGENTS.md`](AGENTS.md) |
| `BW_ENVIRONMENT` | Optional — `test` targets BW's test hosts; defaults to `prod` |
| `ADAPTER_CAPTURE_DIR` | Optional — dir to persist each customer TwiML response (verbatim, content-addressed) so the BXML Generator can turn the paths a test call exercised into standalone BXML. Essential for SDK-built apps with no static TwiML to transpile. Off by default |
| `ADAPTER_LOG=1` | Optional — enable request logging |
| `TRANSLATOR_CAPTURE_DIR` | Optional — dir to persist each customer TwiML response (verbatim, content-addressed) so the BXML Generator can turn the paths a test call exercised into standalone BXML. Essential for SDK-built apps with no static TwiML to transpile. Off by default |
| `TRANSLATOR_LOG=1` | Optional — enable request logging |

Run `npm run doctor` (or `GET /readyz?deep=1` once the server is up) to confirm
this env is set and the Bandwidth OAuth2 token exchange works before routing
Expand Down
12 changes: 6 additions & 6 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
# Security

The adapter authenticates in both directions and constrains where it will send outbound requests.
The translator authenticates in both directions and constrains where it will send outbound requests.

- **Outbound to Bandwidth** — OAuth2 client credentials (`BW_CLIENT_ID`/`BW_CLIENT_SECRET`).
- **Inbound webhooks (`/bw/*`)** — HTTP Basic auth. Set `WEBHOOK_USER`/`WEBHOOK_PASSWORD`, and configure the **same** credentials as `CallbackCreds` on your Bandwidth Voice-V2 application. The adapter also stamps them onto the BXML callback verbs it emits, so Bandwidth presents them on continuation callbacks. Requests to `/bw/*` without valid credentials are rejected.
- **REST facade (`/2010-04-01/*`)** — HTTP Basic auth using `ADAPTER_ACCOUNT_SID`/`ADAPTER_AUTH_TOKEN` (the Twilio-compat credential). This is a distinct secret from the webhook credential above.
- **Outbound fetches** — the adapter only fetches customer callback URLs over http/https and refuses loopback/link-local/private destinations. For local development against `localhost`, set `EGRESS_ALLOW_PRIVATE=1`.
- **Inbound webhooks (`/bw/*`)** — HTTP Basic auth. Set `WEBHOOK_USER`/`WEBHOOK_PASSWORD`, and configure the **same** credentials as `CallbackCreds` on your Bandwidth Voice-V2 application. The translator also stamps them onto the BXML callback verbs it emits, so Bandwidth presents them on continuation callbacks. Requests to `/bw/*` without valid credentials are rejected.
- **REST facade (`/2010-04-01/*`)** — HTTP Basic auth using `TRANSLATOR_ACCOUNT_SID`/`TRANSLATOR_AUTH_TOKEN` (the Twilio-compat credential). This is a distinct secret from the webhook credential above.
- **Outbound fetches** — the translator only fetches customer callback URLs over http/https and refuses loopback/link-local/private destinations. For local development against `localhost`, set `EGRESS_ALLOW_PRIVATE=1`.
For fixed, known customer hosts you may set `EGRESS_ALLOW_HOSTS` (comma-separated) to switch outbound fetches to a strict default-deny allowlist — the tightest posture ("full remediation"). Listed hosts are trusted and bypass the range check, so this also lets you allow `localhost` explicitly in development.
Note: the range check above resolves the hostname and validates the resulting IPs before the fetch, but it does not pin the connection to those IPs — the fetch itself re-resolves the hostname. That means it's defense-in-depth (the inbound Basic auth on `/bw/*` is the primary control) and does not defend against DNS rebinding, where a low-TTL hostname answers with a public IP at check time and an internal one at connect time. Deployments serving untrusted tenants should use `EGRESS_ALLOW_HOSTS` or pin the resolved IP at connect time.

## Deployment

- Serve over HTTPS. Basic-auth credentials are only as safe as the transport.
- The listen host defaults to `127.0.0.1`. To expose the adapter (containers, production), set `HOST=0.0.0.0` deliberately.
- Do not expose the adapter to the internet without the webhook credentials configured.
- The listen host defaults to `127.0.0.1`. To expose the translator (containers, production), set `HOST=0.0.0.0` deliberately.
- Do not expose the translator to the internet without the webhook credentials configured.

## Reporting

Expand Down
10 changes: 5 additions & 5 deletions docs/demo.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ cd examples/sample-twilio-app && npm install && npm start
Terminal B — the translator:

```bash
ADAPTER_ACCOUNT_SID=AC123 ADAPTER_AUTH_TOKEN=demo \
TRANSLATOR_ACCOUNT_SID=AC123 TRANSLATOR_AUTH_TOKEN=demo \
PUBLIC_BASE_URL=http://localhost:3000 \
CUSTOMER_VOICE_URL=http://localhost:4000/voice \
BW_ACCOUNT_ID=x BW_CLIENT_ID=x BW_CLIENT_SECRET=x BW_APPLICATION_ID=x \
Expand Down Expand Up @@ -58,7 +58,7 @@ Expected: BXML containing `<Transfer>` to the sales number.

Provisioning checklist: BW test account with Voice API enabled, a sub-account/
site, 2–3 voice-enabled numbers, a Voice application pointed at the public
adapter URL (`/bw/initiate`), and a public HTTPS tunnel (ngrok) or small host.
translator URL (`/bw/initiate`), and a public HTTPS tunnel (ngrok) or small host.
(A SIP peer is only needed on the legacy platform; the default Universal
Platform path uses a VCP instead — see `AGENTS.md` Phase 2.) Call the BW number,
walk the IVR by ear. Then exercise outbound via the REST facade with the real
Expand All @@ -73,7 +73,7 @@ P0 exit criteria for the live milestone:
## 4. Callbacks

These layer on top of the loop above and can be shown with no credentials.
Number provisioning is not part of this adapter — it's handled by the `band`
Number provisioning is not part of this translator — it's handled by the `band`
CLI; see the Phase 2 runbook in [`AGENTS.md`](../AGENTS.md).

Add a stand-in for the customer's callback receiver:
Expand Down Expand Up @@ -115,13 +115,13 @@ client.calls.create({
```

Hang up the call; when Bandwidth posts the disconnect to `/bw/disconnect`, the
adapter fires a signed Twilio `completed` callback (`CallStatus=completed`,
translator fires a signed Twilio `completed` callback (`CallStatus=completed`,
`CallDuration`, `CallSid`) to that URL. No-telephony proof:
`npx vitest run test/server-status-callback.test.ts`.

### 4c. Number provisioning — now via `band`

Number search/order/activate used to be served through this adapter's own
Number search/order/activate used to be served through this translator's own
REST facade; that surface has been removed in favor of the `band` CLI, which
is the account-side tool of record. To demo provisioning, run the Phase 2
commands from [`AGENTS.md`](../AGENTS.md) (`band number search`, `band number
Expand Down
2 changes: 1 addition & 1 deletion examples/complex-twilio-app/server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// "Acme Health" contact center — a realistic, feature-heavy Twilio voice app.
// Unmodified Twilio SDK. Exercises the full spread of TwiML the adapter cares
// Unmodified Twilio SDK. Exercises the full spread of TwiML the translator cares
// about: the parts that migrate clean AND the parts that must fail loudly.
const express = require("express");
const { twiml } = require("twilio");
Expand Down
4 changes: 2 additions & 2 deletions examples/full-pv-app/server.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// "Full PV" demo — a Twilio voice app that exercises EVERYTHING Bandwidth
// Programmable Voice supports (verified against dev.bandwidth.com via Context7).
// Deliberately stays inside the supported surface, so the whole app translates
// clean through the adapter: no blockers, only expected heads-up notes.
// clean through the translator: no blockers, only expected heads-up notes.
//
// Supported BW PV surface covered here (Twilio verb -> BXML verb):
// Say (+SSML, voice) -> SpeakSentence
Expand Down Expand Up @@ -62,7 +62,7 @@ app.post("/voices", (req, res) => {
const vr = new twiml.VoiceResponse();
vr.say({ voice: "bridget" }, "This is Bridget speaking.");
// Real SSML via the Twilio SDK builders (produces actual child elements,
// which the adapter now preserves through to BXML SpeakSentence).
// which the translator now preserves through to BXML SpeakSentence).
const s = vr.say({ voice: "julie" });
s.addText("And this is Julie, with ");
s.emphasis({ level: "strong" }, "real S S M L emphasis");
Expand Down
2 changes: 1 addition & 1 deletion examples/sample-twilio-app/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ app.use(express.urlencoded({ extended: false }));
app.post("/voice", (req, res) => {
const vr = new twiml.VoiceResponse();
const gather = vr.gather({ numDigits: 1, action: "/menu", method: "POST" });
gather.say("Welcome to the Bandwidth adapter demo. Press 1 for sales. Press 2 to leave a message.");
gather.say("Welcome to the Bandwidth translator demo. Press 1 for sales. Press 2 to leave a message.");
vr.say("We did not receive input. Goodbye.");
res.type("text/xml").send(vr.toString());
});
Expand Down
Loading