From f9e0c5b4278af62960ea70aa519492b9924e4dd8 Mon Sep 17 00:00:00 2001
From: Kush
Date: Thu, 20 Aug 2026 13:51:26 -0400
Subject: [PATCH] chore: replace "adapter" with "translator" throughout
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Finishes the Migration Toolkit rebrand (#21), which renamed the product in
prose but left "adapter" in env vars, code identifiers, filenames, and
runtime strings. No backward-compatible aliases.
Three referents, three words:
- The running proxy is the **translator** — env vars (ADAPTER_* ->
TRANSLATOR_*), user-facing strings, matrix notes, OpenAPI spec, docs,
comments, and test fixture hostnames.
- The `src/server` types are the **server** — AdapterConfig/Deps/Error and
adapterErrors become ServerConfig/Deps/Error and serverErrors. Naming them
Translator* would collide with the narrower `src/translator` module.
- Whole-product framing is the **toolkit** — AGENTS.md's paired model,
CONTRIBUTING.md, and the issue templates cover all three tools, not just
the translator.
Also: openapi/twilio-compat-adapter.yml -> twilio-compat.yml,
scripts/bench-adapter-overhead.ts -> bench-translator-overhead.ts, and the
stale bw-voice-adapter repo URL in the issue-template config.
BREAKING: TRANSLATOR_ACCOUNT_SID, TRANSLATOR_AUTH_TOKEN,
TRANSLATOR_CAPTURE_DIR, and TRANSLATOR_LOG replace their ADAPTER_*
equivalents. Existing deployments must update their environment.
---
.github/ISSUE_TEMPLATE/bug_report.yml | 8 ++---
.github/ISSUE_TEMPLATE/config.yml | 2 +-
.github/ISSUE_TEMPLATE/feature_request.yml | 4 +--
AGENTS.md | 28 +++++++--------
CONTRIBUTING.md | 10 +++---
README.md | 6 ++--
SECURITY.md | 12 +++----
docs/demo.md | 10 +++---
examples/complex-twilio-app/server.js | 2 +-
examples/full-pv-app/server.js | 4 +--
examples/sample-twilio-app/server.js | 2 +-
...o-compat-adapter.yml => twilio-compat.yml} | 32 ++++++++---------
package.json | 2 +-
scripts/bench-translate.ts | 8 ++---
...erhead.ts => bench-translator-overhead.ts} | 36 +++++++++----------
scripts/capture-server.mjs | 2 +-
scripts/emit-bxml.ts | 4 +--
scripts/latency-report.ts | 14 ++++----
scripts/translate-stdin.ts | 4 +--
scripts/validate-bxml.sh | 2 +-
src/matrix/twilio-voice.json | 14 ++++----
src/server/app.ts | 34 +++++++++---------
src/server/capture.ts | 2 +-
src/server/errors.ts | 18 +++++-----
src/server/index.ts | 8 ++---
src/server/readiness.ts | 8 ++---
src/translator/translate.ts | 22 ++++++------
src/twilio/call-resource.ts | 2 +-
src/twilio/egress.ts | 2 +-
test/bw-client.test.ts | 4 +--
test/fixtures/bandwidth/webhooks.json | 2 +-
test/recording-callback.test.ts | 14 ++++----
test/server-bw-fixture.test.ts | 4 +--
test/server-capture.test.ts | 2 +-
test/server-egress.test.ts | 2 +-
test/server-errors.test.ts | 12 +++----
test/server-inbound-auth.test.ts | 2 +-
test/server-inbound.test.ts | 4 +--
test/server-readyz.test.ts | 10 +++---
test/server-rest-get.test.ts | 2 +-
test/server-rest-modify.test.ts | 8 ++---
test/server-rest-recordings.test.ts | 4 +--
test/server-rest.test.ts | 6 ++--
test/server-status-callback.test.ts | 2 +-
test/translate-callback-auth.test.ts | 2 +-
test/translate-core.test.ts | 4 +--
test/translate-dial.test.ts | 4 +--
test/translate-gather-record.test.ts | 6 ++--
test/translate-latency.test.ts | 2 +-
test/translate-loop.test.ts | 2 +-
test/translate-new-verbs.test.ts | 8 ++---
test/translate-stream-conference.test.ts | 2 +-
test/webhook-auth-encoding.test.ts | 2 +-
test/webhook-id-validation.test.ts | 4 +--
web/README.md | 2 +-
web/index.html | 2 +-
web/view-model.ts | 4 +--
57 files changed, 212 insertions(+), 212 deletions(-)
rename openapi/{twilio-compat-adapter.yml => twilio-compat.yml} (94%)
rename scripts/{bench-adapter-overhead.ts => bench-translator-overhead.ts} (82%)
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
index dd454ec..cf076f8 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -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:
@@ -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
@@ -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:
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 3c34b66..10dcf8a 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -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.
diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml
index c893d1a..7022939 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.yml
+++ b/.github/ISSUE_TEMPLATE/feature_request.yml
@@ -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:
@@ -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.
diff --git a/AGENTS.md b/AGENTS.md
index a3ebcf7..2d7448b 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -1,6 +1,6 @@
-# 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.
@@ -8,18 +8,18 @@ It does **not** provision anything on a Bandwidth account.
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
@@ -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
@@ -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
@@ -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=` and place a few test calls; each customer TwiML
+ there is no static markup to transpile. Run the translator with
+ `TRANSLATOR_CAPTURE_DIR=` and place a few test calls; each customer TwiML
response is written there verbatim (content-addressed, deduped). Then
`npm run bxml-generator -- ` produces standalone BXML for the paths those
calls exercised. Capture only covers exercised paths — branches you never dial
@@ -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) |
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index cc21b96..ad5b95d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -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.
@@ -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.
diff --git a/README.md b/README.md
index cdc651d..3ff6db6 100644
--- a/README.md
+++ b/README.md
@@ -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) |
@@ -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
diff --git a/SECURITY.md b/SECURITY.md
index ee604d0..f494a99 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -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
diff --git a/docs/demo.md b/docs/demo.md
index d96ea5f..d73566b 100644
--- a/docs/demo.md
+++ b/docs/demo.md
@@ -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 \
@@ -58,7 +58,7 @@ Expected: BXML containing `` 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
@@ -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:
@@ -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
diff --git a/examples/complex-twilio-app/server.js b/examples/complex-twilio-app/server.js
index 3e789a7..99093e1 100644
--- a/examples/complex-twilio-app/server.js
+++ b/examples/complex-twilio-app/server.js
@@ -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");
diff --git a/examples/full-pv-app/server.js b/examples/full-pv-app/server.js
index 0ec5aec..7161c49 100644
--- a/examples/full-pv-app/server.js
+++ b/examples/full-pv-app/server.js
@@ -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
@@ -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");
diff --git a/examples/sample-twilio-app/server.js b/examples/sample-twilio-app/server.js
index 0501724..e6bfedc 100644
--- a/examples/sample-twilio-app/server.js
+++ b/examples/sample-twilio-app/server.js
@@ -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());
});
diff --git a/openapi/twilio-compat-adapter.yml b/openapi/twilio-compat.yml
similarity index 94%
rename from openapi/twilio-compat-adapter.yml
rename to openapi/twilio-compat.yml
index 5b8f4cf..67109da 100644
--- a/openapi/twilio-compat-adapter.yml
+++ b/openapi/twilio-compat.yml
@@ -5,12 +5,12 @@ info:
**Twilio-compatibility facade for Bandwidth Programmable Voice.**
This API lets an existing Twilio voice application run on Bandwidth's network
- by pointing it at the adapter — no application rewrite. Requests and responses
+ by pointing it at the translator — no application rewrite. Requests and responses
intentionally mirror Twilio's `2010-04-01` REST shape (and webhook payloads),
and are translated to the Bandwidth Voice API in real time.
> NOTE: These paths are deliberately **Twilio-shaped** and therefore diverge
- > from Bandwidth API Standards v3. That is by design — the value of the adapter
+ > from Bandwidth API Standards v3. That is by design — the value of the translator
> is byte-level Twilio compatibility. Treat this spec as a compatibility
> reference, not a standards-compliant Bandwidth API.
@@ -18,19 +18,19 @@ info:
- **Calls, Recordings** — proven against real Twilio↔Bandwidth calls.
- Number provisioning (search, order, release) is not part of this adapter — that's
+ Number provisioning (search, order, release) is not part of this translator — that's
handled by the Bandwidth `band` CLI (https://dev.bandwidth.com/tools/cli/).
## Authentication
- The caller authenticates to the adapter with HTTP Basic, using the Twilio
+ The caller authenticates to the translator with HTTP Basic, using the Twilio
`AccountSid` as the username and the `AuthToken` as the password — exactly as a
- Twilio SDK client would. The adapter authenticates to Bandwidth separately with
- OAuth2 client credentials; that token never leaves the adapter.
+ Twilio SDK client would. The translator authenticates to Bandwidth separately with
+ OAuth2 client credentials; that token never leaves the translator.
## Callbacks
- The adapter POSTs callbacks to your application in Twilio's shape, signed with the
+ The translator POSTs callbacks to your application in Twilio's shape, signed with the
`X-Twilio-Signature` header so your existing signature validation passes:
- **StatusCallback** — set on `createCall`; POSTed when the call ends, with
@@ -39,7 +39,7 @@ info:
TwiML verb; POSTed when a recording is ready, with `RecordingSid`,
`RecordingStatus`, `RecordingDuration`, and a `RecordingUrl` that points back at
this facade.
- title: Twilio Compatibility Adapter (Voice)
+ title: Real-Time Translator (Voice)
version: 0.2.0
contact:
name: Bandwidth Support
@@ -48,12 +48,12 @@ info:
termsOfService: https://www.bandwidth.com/legal/terms-of-use-bandwidthcom-web-sites/
servers:
- - url: https://{adapterHost}/2010-04-01
- description: The deployed adapter (Twilio-shaped base path)
+ - url: https://{translatorHost}/2010-04-01
+ description: The deployed translator (Twilio-shaped base path)
variables:
- adapterHost:
- default: adapter.example.com
- description: Public host of the running adapter
+ translatorHost:
+ default: translator.example.com
+ description: Public host of the running translator
security:
- basicAuth: []
@@ -71,7 +71,7 @@ paths:
summary: Originate a call
description: |-
Equivalent to Twilio `calls.create()`. Translated to a Bandwidth outbound call
- whose answer URL points back at the adapter, so the customer's TwiML drives the call.
+ whose answer URL points back at the translator, so the customer's TwiML drives the call.
operationId: createCall
parameters:
- $ref: '#/components/parameters/accountSidPathParam'
@@ -86,7 +86,7 @@ paths:
To: { type: string, example: '+15552223333', description: Destination number. Missing → error 21201. }
From: { type: string, example: '+15550001111', description: Caller ID. Missing → error 21213. }
Url: { type: string, format: uri, description: Customer TwiML URL for the call. Missing → error 21205. }
- StatusCallback: { type: string, format: uri, description: URL the adapter POSTs a completion callback to when the call ends. }
+ StatusCallback: { type: string, format: uri, description: URL the translator POSTs a completion callback to when the call ends. }
StatusCallbackMethod: { type: string, enum: [POST], default: POST, description: HTTP method for the status callback. }
responses:
'201':
@@ -396,7 +396,7 @@ components:
type: object
description: |-
Twilio-shaped error body. Codes `20003`, `20404`, `21201`, `21205`, and `21213` mirror
- real Twilio errors. Code `21220` is an adapter-specific condition (unsupported
+ real Twilio errors. Code `21220` is a translator-specific condition (unsupported
recording stop) shaped like a Twilio error.
properties:
code: { type: integer, example: 21205 }
diff --git a/package.json b/package.json
index 9a9e4fa..6f68ab9 100644
--- a/package.json
+++ b/package.json
@@ -11,7 +11,7 @@
"compatibility-check": "tsx src/compatibility-check/cli.ts",
"bxml-generator": "tsx src/bxml-generator/cli.ts",
"bench": "tsx scripts/bench-translate.ts",
- "bench:overhead": "tsx scripts/bench-adapter-overhead.ts",
+ "bench:overhead": "tsx scripts/bench-translator-overhead.ts",
"latency:report": "tsx scripts/latency-report.ts",
"playground:build": "tsx scripts/build-playground.ts"
},
diff --git a/scripts/bench-translate.ts b/scripts/bench-translate.ts
index 4ec1c87..0a399f8 100644
--- a/scripts/bench-translate.ts
+++ b/scripts/bench-translate.ts
@@ -1,4 +1,4 @@
-// Micro-benchmark for the TwiML→BXML translation tax — the latency the adapter
+// Micro-benchmark for the TwiML→BXML translation tax — the latency the translator
// itself adds per IVR turn, with the customer-webhook network hop factored out.
//
// The live path (src/server/app.ts `fetchAndTranslate`) is `postToCustomer()`
@@ -12,9 +12,9 @@
import { translateTwiml } from "../src/translator/translate.js";
const rewriteUrl = (u: string) =>
- `https://adapter.example/bw/continue?next=${encodeURIComponent(u)}`;
+ `https://translator.example/bw/continue?next=${encodeURIComponent(u)}`;
-// Representative TwiML spanning the verb surface the adapter translates, from a
+// Representative TwiML spanning the verb surface the translator translates, from a
// trivial greeting to a composite document exercising several verbs at once.
const CORPUS: { name: string; twiml: string }[] = [
{
@@ -109,6 +109,6 @@ console.log(
`${pad("ALL", 16)}${padL("", 7)}${padL(fmt(overallMean), 11)}${padL(fmt(pct(allDurations, 0.5)), 11)}${padL(fmt(pct(allDurations, 0.99)), 11)}${padL(fmt(allDurations[allDurations.length - 1]), 11)}`,
);
console.log(
- `\nThis is the adapter's per-turn translation tax (CPU only). The customer ` +
+ `\nThis is the translator's per-turn translation tax (CPU only). The customer ` +
`webhook round-trip is separate and not measured here.`,
);
diff --git a/scripts/bench-adapter-overhead.ts b/scripts/bench-translator-overhead.ts
similarity index 82%
rename from scripts/bench-adapter-overhead.ts
rename to scripts/bench-translator-overhead.ts
index 087f77c..b92e6a2 100644
--- a/scripts/bench-adapter-overhead.ts
+++ b/scripts/bench-translator-overhead.ts
@@ -1,18 +1,18 @@
-// Differential latency harness: the cost of putting the adapter in the path,
+// Differential latency harness: the cost of putting the translator in the path,
// vs. talking to the customer app directly (the "raw BXML" baseline).
//
// Both measurements hit the SAME local stub customer app over real localhost
-// HTTP, so the customer-app cost cancels out and what's left is the adapter's
+// HTTP, so the customer-app cost cancels out and what's left is the translator's
// own tax — its extra hop + the TwiML→BXML translation:
//
// A = POST straight to the stub, get a document back. (native-equivalent baseline)
-// B = POST to the adapter's /bw/initiate, which internally (adapter in the path)
+// B = POST to the translator's /bw/initiate, which internally (translator in the path)
// hits that same stub and translates the result.
-// B − A = what inserting the adapter costs per IVR turn.
+// B − A = what inserting the translator costs per IVR turn.
//
// Loopback only — this measures the component we own (one extra hop + CPU),
// not internet RTT. A real-call number (geographic distance to BW's edge) needs
-// the live setup in docs/demo.md §3 with ADAPTER_LOG=1 (`fetchMs`).
+// the live setup in docs/demo.md §3 with TRANSLATOR_LOG=1 (`fetchMs`).
//
// npm run bench:overhead # default 1000 iterations per doc
// npm run bench:overhead -- 5000 # custom iteration count
@@ -21,7 +21,7 @@ import type { AddressInfo } from "node:net";
import { buildApp } from "../src/server/app.js";
import type { BwClient } from "../src/bw/client.js";
-// Representative IVR turns the adapter translates per call.
+// Representative IVR turns the translator translates per call.
const CORPUS: Record = {
"say-simple": `Thanks for calling. Goodbye.`,
"gather-menu": `For sales press 1. For support press 2./welcome`,
@@ -51,7 +51,7 @@ const stub = createServer((req, res) => {
});
});
-// ─── the adapter under test (its /bw/initiate path never touches bwClient) ───
+// ─── the translator under test (its /bw/initiate path never touches bwClient) ───
const webhookUser = "bench-user";
const webhookPassword = "bench-pass";
const app = buildApp(
@@ -69,9 +69,9 @@ const app = buildApp(
await new Promise((resolve) => stub.listen(0, "127.0.0.1", resolve));
await app.listen({ port: 0, host: "127.0.0.1" });
const stubPort = (stub.address() as AddressInfo).port;
-const adapterPort = (app.server.address() as AddressInfo).port;
+const translatorPort = (app.server.address() as AddressInfo).port;
const stubBase = `http://127.0.0.1:${stubPort}`;
-const adapterBase = `http://127.0.0.1:${adapterPort}`;
+const translatorBase = `http://127.0.0.1:${translatorPort}`;
const formBody = "CallSid=CAbench&CallStatus=ringing&From=%2B15550001111&To=%2B15552223333";
@@ -86,10 +86,10 @@ async function timeDirect(doc: string): Promise {
return performance.now() - t;
}
-async function timeAdapter(doc: string, i: number): Promise {
+async function timeTranslator(doc: string, i: number): Promise {
const voiceUrl = encodeURIComponent(`${stubBase}/${doc}`);
const t = performance.now();
- const r = await fetch(`${adapterBase}/bw/initiate?voiceUrl=${voiceUrl}`, {
+ const r = await fetch(`${translatorBase}/bw/initiate?voiceUrl=${voiceUrl}`, {
method: "POST",
headers: {
"content-type": "application/json",
@@ -109,8 +109,8 @@ async function timeAdapter(doc: string, i: number): Promise {
}
console.log(
- `[bench-adapter-overhead] ${iterations} iterations/doc (after ${WARMUP} warmup)\n` +
- `A = direct-to-customer (baseline) B = through-adapter overhead = B − A\n`,
+ `[bench-translator-overhead] ${iterations} iterations/doc (after ${WARMUP} warmup)\n` +
+ `A = direct-to-customer (baseline) B = through-translator overhead = B − A\n`,
);
const pad = (s: string, n: number) => s.padEnd(n);
@@ -124,7 +124,7 @@ const allOverhead: number[] = [];
for (const doc of Object.keys(CORPUS)) {
for (let i = 0; i < WARMUP; i++) {
await timeDirect(doc);
- await timeAdapter(doc, -i - 1);
+ await timeTranslator(doc, -i - 1);
}
const aTimes: number[] = [];
@@ -133,7 +133,7 @@ for (const doc of Object.keys(CORPUS)) {
for (let i = 0; i < iterations; i++) {
// Interleave A and B each iteration so machine jitter hits both equally.
const a = await timeDirect(doc);
- const b = await timeAdapter(doc, i);
+ const b = await timeTranslator(doc, i);
aTimes.push(a);
bTimes.push(b);
overhead.push(b - a);
@@ -151,13 +151,13 @@ for (const doc of Object.keys(CORPUS)) {
const oS = [...allOverhead].sort((x, y) => x - y);
console.log("─".repeat(75));
console.log(
- `\nAdapter overhead (B − A) across all turns: ` +
+ `\nTranslator overhead (B − A) across all turns: ` +
`mean ${fmt(mean(allOverhead))}, p50 ${fmt(pct(oS, 0.5))}, p99 ${fmt(pct(oS, 0.99))}`,
);
console.log(
`That is one extra localhost hop + translation — the cost of inserting the\n` +
- `adapter. In production the extra hop carries real network distance to BW's\n` +
- `voice edge; measure that on a live call (docs/demo.md §3, ADAPTER_LOG=1).`,
+ `translator. In production the extra hop carries real network distance to BW's\n` +
+ `voice edge; measure that on a live call (docs/demo.md §3, TRANSLATOR_LOG=1).`,
);
await app.close();
diff --git a/scripts/capture-server.mjs b/scripts/capture-server.mjs
index bf6f40e..f75bfa3 100644
--- a/scripts/capture-server.mjs
+++ b/scripts/capture-server.mjs
@@ -1,6 +1,6 @@
// Dev-only Twilio webhook capture server. Logs each request (method, path,
// query, the X-Twilio-Signature header, and the form body) to capture/ as JSON,
-// and returns TwiML so the call flow proceeds. Not part of the shipped adapter.
+// and returns TwiML so the call flow proceeds. Not part of the shipped translator.
import { createServer } from "node:http";
import { writeFileSync, mkdirSync } from "node:fs";
import { join } from "node:path";
diff --git a/scripts/emit-bxml.ts b/scripts/emit-bxml.ts
index 2d42f37..d6a848d 100644
--- a/scripts/emit-bxml.ts
+++ b/scripts/emit-bxml.ts
@@ -1,8 +1,8 @@
-// Emits the adapter's BXML output for representative TwiML inputs, one doc per
+// Emits the translator's BXML output for representative TwiML inputs, one doc per
// line, so an external validator (band bxml) can check each. Dev tool.
import { translateTwiml } from "../src/translator/translate.js";
-const rw = (u: string) => `https://adapter.test/bw/continue?next=${encodeURIComponent(u)}`;
+const rw = (u: string) => `https://translator.test/bw/continue?next=${encodeURIComponent(u)}`;
const SAMPLES: Record = {
ivr: `Welcome. Press 1 for sales.No input. Goodbye.`,
diff --git a/scripts/latency-report.ts b/scripts/latency-report.ts
index 23df599..f2742f8 100644
--- a/scripts/latency-report.ts
+++ b/scripts/latency-report.ts
@@ -1,4 +1,4 @@
-// Turns a live adapter's ADAPTER_LOG=1 output into a latency report: the
+// Turns a live translator's TRANSLATOR_LOG=1 output into a latency report: the
// per-turn split between the customer webhook round-trip (fetchMs, network —
// not ours) and the TwiML→BXML translation (translateMs, CPU — ours), as
// p50/p99/max, same shape as the benches.
@@ -8,11 +8,11 @@
// distance to Bandwidth's voice edge.
//
// Live pipe (logs stream through to your terminal, report prints on Ctrl-C):
-// ADAPTER_LOG=1 npm start 2>&1 | tsx scripts/latency-report.ts
+// TRANSLATOR_LOG=1 npm start 2>&1 | tsx scripts/latency-report.ts
//
// Or analyze a captured log file:
-// ADAPTER_LOG=1 npm start > adapter.log 2>&1 # ...place call, then Ctrl-C
-// npm run latency:report -- adapter.log
+// TRANSLATOR_LOG=1 npm start > translator.log 2>&1 # ...place call, then Ctrl-C
+// npm run latency:report -- translator.log
import { createInterface } from "node:readline";
import { createReadStream } from "node:fs";
@@ -31,7 +31,7 @@ function consume(line: string) {
try {
rec = JSON.parse(line);
} catch {
- return; // not a JSON log line (e.g. the "adapter listening on :3000" banner)
+ return; // not a JSON log line (e.g. the "translator listening on :3000" banner)
}
if (rec.msg === "fetchAndTranslate timing") {
if (typeof rec.fetchMs === "number") fetchMs.push(rec.fetchMs);
@@ -50,7 +50,7 @@ function statRow(label: string, xs: number[]): string {
function report() {
if (!fetchMs.length && !translateMs.length && !responseMs.length) {
process.stderr.write(
- "\n[latency-report] No timing lines found. Did you run with ADAPTER_LOG=1 " +
+ "\n[latency-report] No timing lines found. Did you run with TRANSLATOR_LOG=1 " +
"and exercise /bw/initiate or /bw/continue?\n",
);
return;
@@ -60,7 +60,7 @@ function report() {
out.write(`${"".padEnd(16)}${"n".padStart(7)}${"mean".padStart(11)}${"p50".padStart(11)}${"p99".padStart(11)}${"max".padStart(11)}\n`);
out.write("─".repeat(67) + "\n");
out.write(statRow("fetchMs", fetchMs) + " ← customer webhook RTT (network)\n");
- out.write(statRow("translateMs", translateMs) + " ← TwiML→BXML (the adapter's tax)\n");
+ out.write(statRow("translateMs", translateMs) + " ← TwiML→BXML (the translator's tax)\n");
if (fetchMs.length && translateMs.length) {
const totMean = mean(fetchMs) + mean(translateMs);
diff --git a/scripts/translate-stdin.ts b/scripts/translate-stdin.ts
index fc6127a..0b83340 100644
--- a/scripts/translate-stdin.ts
+++ b/scripts/translate-stdin.ts
@@ -1,4 +1,4 @@
-// Reads TwiML on stdin, prints the adapter's BXML + findings as JSON.
+// Reads TwiML on stdin, prints the translator's BXML + findings as JSON.
// Lets us see runtime translation behavior for any endpoint's actual output.
import { translateTwiml } from "../src/translator/translate.js";
@@ -6,7 +6,7 @@ let data = "";
process.stdin.on("data", (c) => (data += c));
process.stdin.on("end", () => {
const r = translateTwiml(data, {
- rewriteUrl: (u) => `https://adapter.example/bw/continue?next=${encodeURIComponent(u)}`,
+ rewriteUrl: (u) => `https://translator.example/bw/continue?next=${encodeURIComponent(u)}`,
});
console.log(
JSON.stringify(
diff --git a/scripts/validate-bxml.sh b/scripts/validate-bxml.sh
index ecd95f8..949db4b 100755
--- a/scripts/validate-bxml.sh
+++ b/scripts/validate-bxml.sh
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
-# Conformance check: validate the adapter's BXML output against the official
+# Conformance check: validate the translator's BXML output against the official
# Bandwidth CLI (`band`). Two levels:
# 1. well-formedness — `band bxml raw` returns non-zero on malformed XML
# 2. (manual) schema — compare our verb/attribute names against the canonical
diff --git a/src/matrix/twilio-voice.json b/src/matrix/twilio-voice.json
index 113ff05..f3809c9 100644
--- a/src/matrix/twilio-voice.json
+++ b/src/matrix/twilio-voice.json
@@ -69,15 +69,15 @@
"Record": {
"bxml": "Record",
"status": "supported",
- "notes": "Recording callback payloads differ; adapter normalizes.",
+ "notes": "Recording callback payloads differ; translator normalizes.",
"docsUrl": "https://dev.bandwidth.com/docs/voice/bxml/record",
"attributes": {
"action": { "bxml": "recordCompleteUrl", "status": "supported" },
- "recordingStatusCallback": { "bxml": "recordingAvailableUrl", "status": "supported", "notes": "Async 'recording ready' webhook. Adapter receives the BW recordingAvailableUrl event, reshapes it into Twilio recordingStatusCallback params, and forwards it; RecordingUrl points back at the adapter's recording facade. recordingStatusCallbackMethod=GET is not honored (forwarded via POST)." },
+ "recordingStatusCallback": { "bxml": "recordingAvailableUrl", "status": "supported", "notes": "Async 'recording ready' webhook. Translator receives the BW recordingAvailableUrl event, reshapes it into Twilio recordingStatusCallback params, and forwards it; RecordingUrl points back at the translator's recording facade. recordingStatusCallbackMethod=GET is not honored (forwarded via POST)." },
"maxLength": { "bxml": "maxDuration", "status": "supported" },
"finishOnKey": { "bxml": "terminatingDigits", "status": "supported" },
"transcribe": { "bxml": "transcribe", "status": "partial", "notes": "Engine and callback shape differ." },
- "playBeep": { "bxml": null, "status": "partial", "notes": "No direct equivalent; adapter can prepend a beep PlayAudio later." }
+ "playBeep": { "bxml": null, "status": "partial", "notes": "No direct equivalent; translator can prepend a beep PlayAudio later." }
}
},
"Dial": {
@@ -124,13 +124,13 @@
"Pay": {
"bxml": null,
"status": "unsupported",
- "notes": "PCI payment capture is out of scope for the adapter.",
+ "notes": "PCI payment capture is out of scope for the translator.",
"attributes": {}
},
"Refer": {
"bxml": "Refer",
"status": "partial",
- "notes": "Twilio maps to BW . action maps to referCompleteUrl (rewritten through the adapter so the Refer-complete callback returns translated BXML); method maps to referCompleteMethod. Bandwidth honors Refer only on inbound SIP URI calls — a PSTN call leg cannot be REFER'd — so this is partial by platform constraint, not a translation gap. A Refer without a child fails loudly (BW Refer accepts only a SIP URI).",
+ "notes": "Twilio maps to BW . action maps to referCompleteUrl (rewritten through the translator so the Refer-complete callback returns translated BXML); method maps to referCompleteMethod. Bandwidth honors Refer only on inbound SIP URI calls — a PSTN call leg cannot be REFER'd — so this is partial by platform constraint, not a translation gap. A Refer without a child fails loudly (BW Refer accepts only a SIP URI).",
"docsUrl": "https://dev.bandwidth.com/docs/voice/programmable-voice/bxml/refer/",
"attributes": {
"action": { "bxml": "referCompleteUrl", "status": "supported" },
@@ -147,7 +147,7 @@
"Stream": {
"bxml": "StartStream",
"status": "partial",
- "notes": "Twilio WS message schema is emulated by the adapter's stream bridge; live Bandwidth-side binding requires fixture capture.",
+ "notes": "Twilio WS message schema is emulated by the translator's stream bridge; live Bandwidth-side binding requires fixture capture.",
"docsUrl": "https://dev.bandwidth.com/docs/voice/bxml/startStream",
"attributes": {}
},
@@ -189,7 +189,7 @@
"Client": {
"bxml": null,
"status": "unsupported",
- "notes": "Twilio Client (WebRTC) endpoints have no adapter mapping in P0.",
+ "notes": "Twilio Client (WebRTC) endpoints have no translator mapping in P0.",
"attributes": {}
}
}
diff --git a/src/server/app.ts b/src/server/app.ts
index b6cf86e..3974d95 100644
--- a/src/server/app.ts
+++ b/src/server/app.ts
@@ -12,7 +12,7 @@ import {
import { EgressBlockedError } from "../twilio/egress-guard.js";
import { toCallSid, toRecordingSid } from "../twilio/call-sid.js";
import { createdCallResource, bwStateToTwilioStatus, twilioErrors } from "../twilio/call-resource.js";
-import { adapterErrors } from "./errors.js";
+import { serverErrors } from "./errors.js";
import {
recordingList,
recordingResource,
@@ -25,7 +25,7 @@ import { checkReadiness } from "./readiness.js";
import { safeEqual } from "./safe-equal.js";
import { captureTwiml } from "./capture.js";
-export interface AdapterConfig {
+export interface ServerConfig {
accountSid: string;
authToken: string;
publicBaseUrl: string;
@@ -41,7 +41,7 @@ export interface AdapterConfig {
captureDir?: string;
}
-export interface AdapterDeps {
+export interface ServerDeps {
fetchImpl: typeof fetch;
bwClient: BwClient;
}
@@ -69,22 +69,22 @@ interface BwRecordingEvent {
startTime?: string;
}
-export function buildApp(config: AdapterConfig, deps: AdapterDeps): FastifyInstance {
- // Logging off by default; set ADAPTER_LOG=1 to enable request/error logs.
- const app = Fastify({ logger: process.env.ADAPTER_LOG === "1" });
+export function buildApp(config: ServerConfig, deps: ServerDeps): FastifyInstance {
+ // Logging off by default; set TRANSLATOR_LOG=1 to enable request/error logs.
+ const app = Fastify({ logger: process.env.TRANSLATOR_LOG === "1" });
app.register(formbody);
app.setErrorHandler((err, _req, reply) => {
// Full detail (incl. upstream bodies) goes to logs only, never the response.
- app.log.error({ err }, "unhandled adapter error");
+ app.log.error({ err }, "unhandled translator error");
const sc = (err as { statusCode?: number }).statusCode;
// Preserve a client-error status Fastify already classified (e.g. malformed
// body → 400); everything else is a neutral internal 500. We do NOT relabel
// arbitrary throws as "Bandwidth" failures or forward err.message.
if (sc && sc >= 400 && sc < 500) {
- return reply.code(sc).send({ ...adapterErrors.internal(), status: sc, code: 90003, message: "Invalid request" });
+ return reply.code(sc).send({ ...serverErrors.internal(), status: sc, code: 90003, message: "Invalid request" });
}
- return reply.code(500).send(adapterErrors.internal());
+ return reply.code(500).send(serverErrors.internal());
});
const expectedWebhookAuth =
@@ -125,7 +125,7 @@ export function buildApp(config: AdapterConfig, deps: AdapterDeps): FastifyInsta
{
name: "SpeakSentence",
children: [
- `This application uses a Twilio feature not yet supported by the adapter: ${verbs.join(", ")}. The call will now end.`,
+ `This application uses a Twilio feature not yet supported by the translator: ${verbs.join(", ")}. The call will now end.`,
],
},
{ name: "Hangup" },
@@ -138,7 +138,7 @@ export function buildApp(config: AdapterConfig, deps: AdapterDeps): FastifyInsta
reply: FastifyReply,
) {
// Split the per-turn latency into the customer webhook round-trip (network,
- // not ours) and the TwiML→BXML translation tax (CPU, ours). With ADAPTER_LOG=1
+ // not ours) and the TwiML→BXML translation tax (CPU, ours). With TRANSLATOR_LOG=1
// each turn logs both; see `npm run bench` for the translation tax in isolation.
const fetchStart = performance.now();
let twiml: string;
@@ -193,7 +193,7 @@ export function buildApp(config: AdapterConfig, deps: AdapterDeps): FastifyInsta
app.post("/bw/initiate", async (req, reply) => {
const event = req.body as BwEvent;
- if (!event || !isSafeBwId(event.callId)) return reply.code(400).send(adapterErrors.invalidParam("callId"));
+ if (!event || !isSafeBwId(event.callId)) return reply.code(400).send(serverErrors.invalidParam("callId"));
const query = req.query as { voiceUrl?: string };
const existing = store.get(event.callId);
const voiceUrl = query.voiceUrl ?? existing?.voiceUrl ?? config.voiceUrl;
@@ -211,9 +211,9 @@ export function buildApp(config: AdapterConfig, deps: AdapterDeps): FastifyInsta
app.post("/bw/continue", async (req, reply) => {
const event = req.body as BwEvent;
- if (!event || !isSafeBwId(event.callId)) return reply.code(400).send(adapterErrors.invalidParam("callId"));
+ if (!event || !isSafeBwId(event.callId)) return reply.code(400).send(serverErrors.invalidParam("callId"));
const query = req.query as { next?: string };
- if (!query.next) return reply.code(400).send(adapterErrors.missingParam("next"));
+ if (!query.next) return reply.code(400).send(serverErrors.missingParam("next"));
const record =
store.get(event.callId) ??
({
@@ -233,7 +233,7 @@ export function buildApp(config: AdapterConfig, deps: AdapterDeps): FastifyInsta
app.post("/bw/disconnect", async (req, reply) => {
const event = req.body as BwEvent;
- if (!event || !isSafeBwId(event.callId)) return reply.code(400).send(adapterErrors.invalidParam("callId"));
+ if (!event || !isSafeBwId(event.callId)) return reply.code(400).send(serverErrors.invalidParam("callId"));
const record = store.get(event.callId);
if (record) {
// Bandwidth bills (and Twilio reports) from answer to end; fall back to 0
@@ -270,9 +270,9 @@ export function buildApp(config: AdapterConfig, deps: AdapterDeps): FastifyInsta
// recordingStatusCallback payload and forward it to the customer's callback URL.
app.post("/bw/recording-status", async (req, reply) => {
const event = req.body as BwRecordingEvent;
- if (!event || !isSafeBwId(event.callId)) return reply.code(400).send(adapterErrors.invalidParam("callId"));
+ if (!event || !isSafeBwId(event.callId)) return reply.code(400).send(serverErrors.invalidParam("callId"));
if (event.recordingId !== undefined && !isSafeBwId(event.recordingId))
- return reply.code(400).send(adapterErrors.invalidParam("recordingId"));
+ return reply.code(400).send(serverErrors.invalidParam("recordingId"));
const cb = (req.query as { cb?: string }).cb;
const record = store.get(event.callId);
if (cb && record && event.recordingId) {
diff --git a/src/server/capture.ts b/src/server/capture.ts
index 2010bc5..01edfa5 100644
--- a/src/server/capture.ts
+++ b/src/server/capture.ts
@@ -3,7 +3,7 @@ import { createHash } from "node:crypto";
import { join } from "node:path";
/**
- * Persist a raw TwiML response the adapter fetched from the customer app, so a
+ * Persist a raw TwiML response the translator fetched from the customer app, so a
* later `npm run bxml-generator` over the capture dir can turn the paths a test call
* actually exercised into standalone BXML.
*
diff --git a/src/server/errors.ts b/src/server/errors.ts
index 994af0d..fd01278 100644
--- a/src/server/errors.ts
+++ b/src/server/errors.ts
@@ -1,30 +1,30 @@
// src/server/errors.ts
/** Structured error body — same shape as twilioErrors in src/twilio/call-resource.ts. */
-export interface AdapterError {
+export interface ServerError {
code: number;
message: string;
more_info: string;
status: number;
}
-// Adapter-private code range. Deliberately NOT in Twilio's registry — reusing a
+// Translator-private code range. Deliberately NOT in Twilio's registry — reusing a
// real Twilio code (e.g. 21610 = STOP/unsubscribed) with a twilio.com link would
-// misdiagnose an adapter failure.
+// misdiagnose a translator failure.
const DOCS = "https://github.com/Bandwidth/migration-toolkit/blob/main/AGENTS.md#errors";
-/** Operational errors the adapter itself raises (distinct from Twilio-API-compat errors). */
-export const adapterErrors = {
+/** Operational errors the translator itself raises (distinct from Twilio-API-compat errors). */
+export const serverErrors = {
/** A required request parameter was absent. */
- missingParam(name: string): AdapterError {
+ missingParam(name: string): ServerError {
return { code: 90001, message: `Missing required parameter: ${name}`, more_info: DOCS, status: 400 };
},
/** An unhandled internal failure. Neutral by design — no upstream detail leaked. */
- internal(): AdapterError {
- return { code: 90002, message: "Internal adapter error", more_info: DOCS, status: 500 };
+ internal(): ServerError {
+ return { code: 90002, message: "Internal translator error", more_info: DOCS, status: 500 };
},
/** A request parameter was present but failed validation. */
- invalidParam(name: string): AdapterError {
+ invalidParam(name: string): ServerError {
return { code: 90004, message: `Invalid parameter: ${name}`, more_info: DOCS, status: 400 };
},
} as const;
diff --git a/src/server/index.ts b/src/server/index.ts
index 87f916f..8afdea7 100644
--- a/src/server/index.ts
+++ b/src/server/index.ts
@@ -11,15 +11,15 @@ const bwEnv = process.env.BW_ENVIRONMENT === "test" ? "test" : "prod";
const app = buildApp(
{
- accountSid: env("ADAPTER_ACCOUNT_SID"),
- authToken: env("ADAPTER_AUTH_TOKEN"),
+ accountSid: env("TRANSLATOR_ACCOUNT_SID"),
+ authToken: env("TRANSLATOR_AUTH_TOKEN"),
publicBaseUrl: env("PUBLIC_BASE_URL"),
voiceUrl: env("CUSTOMER_VOICE_URL"),
webhookUser: env("WEBHOOK_USER"),
webhookPassword: env("WEBHOOK_PASSWORD"),
allowPrivateEgress: process.env.EGRESS_ALLOW_PRIVATE === "1",
egressAllowHosts: process.env.EGRESS_ALLOW_HOSTS?.split(",").map((s) => s.trim()).filter(Boolean),
- captureDir: process.env.ADAPTER_CAPTURE_DIR,
+ captureDir: process.env.TRANSLATOR_CAPTURE_DIR,
},
{
fetchImpl: fetch,
@@ -35,4 +35,4 @@ const app = buildApp(
const port = Number(process.env.PORT ?? 3000);
const host = process.env.HOST ?? "127.0.0.1";
-app.listen({ port, host }).then(() => console.log(`adapter listening on ${host}:${port}`));
+app.listen({ port, host }).then(() => console.log(`translator listening on ${host}:${port}`));
diff --git a/src/server/readiness.ts b/src/server/readiness.ts
index b04c5f9..88bc3aa 100644
--- a/src/server/readiness.ts
+++ b/src/server/readiness.ts
@@ -1,7 +1,7 @@
-/** Env vars the adapter server requires to start and serve calls. */
+/** Env vars the translator server requires to start and serve calls. */
export const REQUIRED_ENV = [
- "ADAPTER_ACCOUNT_SID",
- "ADAPTER_AUTH_TOKEN",
+ "TRANSLATOR_ACCOUNT_SID",
+ "TRANSLATOR_AUTH_TOKEN",
"PUBLIC_BASE_URL",
"CUSTOMER_VOICE_URL",
"BW_ACCOUNT_ID",
@@ -20,7 +20,7 @@ export interface ReadinessReport {
}
/**
- * Report whether the adapter is configured to serve calls. Pure over its
+ * Report whether the translator is configured to serve calls. Pure over its
* inputs: pass `env` (usually process.env) and an optional `probeToken` that
* attempts a live Bandwidth OAuth2 token exchange.
*/
diff --git a/src/translator/translate.ts b/src/translator/translate.ts
index 15e3668..9849484 100644
--- a/src/translator/translate.ts
+++ b/src/translator/translate.ts
@@ -165,7 +165,7 @@ function mapVoice(twilioVoice: string): string | null {
return null; // unrecognized — drop rather than break the call
}
-// Attrs that carry a rewritten adapter callback URL (Bandwidth will hit these
+// Attrs that carry a rewritten translator callback URL (Bandwidth will hit these
// endpoints directly, so they need Basic-auth creds matching the app's CallbackCreds).
const CALLBACK_URL_ATTRS = [
"gatherUrl",
@@ -177,7 +177,7 @@ const CALLBACK_URL_ATTRS = [
] as const;
/** Walks the built element tree and stamps username/password onto any element
- * carrying a rewritten adapter callback URL, so Bandwidth Basic-auths the
+ * carrying a rewritten translator callback URL, so Bandwidth Basic-auths the
* continuation request instead of hitting it unauthenticated. */
function stampCallbackAuth(els: XmlEl[], auth: { username: string; password: string }): void {
for (const el of els) {
@@ -217,7 +217,7 @@ function unsupported(node: TwimlNode, findings: Finding[], detail?: string): nul
findings.push({
severity: "error",
verb: node.name,
- message: detail ?? m?.notes ?? `TwiML <${node.name}> has no Bandwidth equivalent in the adapter.`,
+ message: detail ?? m?.notes ?? `TwiML <${node.name}> has no Bandwidth equivalent in the translator.`,
docsUrl: m?.docsUrl,
});
return null;
@@ -388,7 +388,7 @@ function translateGather(
else
warn(
"Gather",
- "Gather without an action attribute re-requests the current document URL on Twilio; set an explicit action for identical behavior through the adapter.",
+ "Gather without an action attribute re-requests the current document URL on Twilio; set an explicit action for identical behavior through the translator.",
findings,
);
const children: XmlEl[] = [];
@@ -410,7 +410,7 @@ function translateRecord(
};
if (node.attrs.action) attrs.recordCompleteUrl = rewrite(node.attrs.action, "record");
// Twilio's async "recording is ready" webhook (recordingStatusCallback) maps to
- // Bandwidth's recordingAvailableUrl. The adapter receives the BW event, reshapes
+ // Bandwidth's recordingAvailableUrl. The translator receives the BW event, reshapes
// it into Twilio recording-callback params, and forwards it to the customer.
if (node.attrs.recordingStatusCallback)
attrs.recordingAvailableUrl = rewrite(node.attrs.recordingStatusCallback, "recordingStatus");
@@ -420,7 +420,7 @@ function translateRecord(
)
warn(
"Record",
- "recordingStatusCallbackMethod=GET is not honored; the adapter forwards recording events via POST.",
+ "recordingStatusCallbackMethod=GET is not honored; the translator forwards recording events via POST.",
findings,
);
if (node.attrs.transcribe === "true")
@@ -487,7 +487,7 @@ function translateDial(
if (child.name === "Number") targets.push({ name: "PhoneNumber", children: [child.text] });
else if (child.name === "Sip") targets.push({ name: "SipUri", children: [child.text] });
else
- return unsupported(child, findings, `Dial noun <${child.name}> is not supported by the adapter.`);
+ return unsupported(child, findings, `Dial noun <${child.name}> is not supported by the translator.`);
}
if (targets.length === 0 && node.text) targets.push({ name: "PhoneNumber", children: [node.text] });
if (targets.length === 0) return unsupported(node, findings, "Dial with no target.");
@@ -497,7 +497,7 @@ function translateDial(
"Child-call status propagation is not fully replicated in P0; validate call-progress behavior.",
findings,
);
- // Twilio Dial attributes the adapter cannot map to BXML Transfer. Surfacing
+ // Twilio Dial attributes the translator cannot map to BXML Transfer. Surfacing
// each one explicitly (rather than dropping it silently) is the product's
// no-silent-degradation contract — the customer learns exactly what won't carry over.
for (const [attr, message] of Object.entries(UNSUPPORTED_DIAL_ATTRS)) {
@@ -571,7 +571,7 @@ function translateConnect(
return unsupported(
node,
findings,
- `Connect noun <${node.children[0]?.name ?? "?"}> is not supported (ConversationRelay/VirtualAgent are out of adapter scope).`,
+ `Connect noun <${node.children[0]?.name ?? "?"}> is not supported (ConversationRelay/VirtualAgent are out of translator scope).`,
);
return streamToStartStream(stream, "bidirectional", findings, rewrite);
}
@@ -602,7 +602,7 @@ function translateStop(node: TwimlNode, findings: Finding[]): XmlEl[] | null {
return unsupported(
node,
findings,
- `Stop noun <${node.children[0]?.name ?? "?"}> is not supported by the adapter.`,
+ `Stop noun <${node.children[0]?.name ?? "?"}> is not supported by the translator.`,
);
}
@@ -645,7 +645,7 @@ function translateStart(
return unsupported(
node,
findings,
- `Start noun <${node.children[0]?.name ?? "?"}> is not supported by the adapter.`,
+ `Start noun <${node.children[0]?.name ?? "?"}> is not supported by the translator.`,
);
warn(
diff --git a/src/twilio/call-resource.ts b/src/twilio/call-resource.ts
index bbdc15a..f9340f3 100644
--- a/src/twilio/call-resource.ts
+++ b/src/twilio/call-resource.ts
@@ -117,7 +117,7 @@ export const twilioErrors = {
};
},
/**
- * 400 for an unsupported recording-control Status. Adapter-specific (not a
+ * 400 for an unsupported recording-control Status. Translator-specific (not a
* fixture-verified Twilio code): Bandwidth's recording REST does pause/resume
* only — there is no REST stop (StopRecording is a BXML verb).
*/
diff --git a/src/twilio/egress.ts b/src/twilio/egress.ts
index c427e6e..9fc7658 100644
--- a/src/twilio/egress.ts
+++ b/src/twilio/egress.ts
@@ -80,7 +80,7 @@ export function statusParams(
/**
* Twilio recording status callback params. This is a leaner payload than the
* voice webhooks (no geo block) — it carries the recording identity plus the
- * call it belongs to. RecordingUrl points back at the adapter's own recording
+ * call it belongs to. RecordingUrl points back at the translator's own recording
* facade so the customer's existing fetch-by-URL code resolves through us.
*/
export function recordingStatusParams(
diff --git a/test/bw-client.test.ts b/test/bw-client.test.ts
index c678fae..bc06493 100644
--- a/test/bw-client.test.ts
+++ b/test/bw-client.test.ts
@@ -33,7 +33,7 @@ describe("createBwClient (OAuth2 Bearer against the Voice API)", () => {
const { callId } = await client.createCall({
to: "+15552223333",
from: "+15550001111",
- answerUrl: "https://adapter.test/bw/initiate",
+ answerUrl: "https://translator.test/bw/initiate",
});
expect(callId).toBe("c-1");
@@ -59,7 +59,7 @@ describe("createBwClient (OAuth2 Bearer against the Voice API)", () => {
const fetchImpl = fetchWithToken({ callId: "c-2" }, 201);
const client = createBwClient({ ...base, environment: "test", fetchImpl });
- await client.createCall({ to: "+1", from: "+2", answerUrl: "https://adapter.test/x" });
+ await client.createCall({ to: "+1", from: "+2", answerUrl: "https://translator.test/x" });
const calls = (fetchImpl as unknown as ReturnType).mock.calls;
expect(calls.some(([u]) => String(u) === "https://test.api.bandwidth.com/api/v1/oauth2/token")).toBe(true);
diff --git a/test/fixtures/bandwidth/webhooks.json b/test/fixtures/bandwidth/webhooks.json
index 60d07d2..b144cc8 100644
--- a/test/fixtures/bandwidth/webhooks.json
+++ b/test/fixtures/bandwidth/webhooks.json
@@ -2,7 +2,7 @@
"_meta": {
"captured": "2026-06-10",
"source": "live Bandwidth voice webhook (app callInitiatedCallbackUrl) via cloudflared tunnel on a real provisioned number",
- "note": "Content-Type application/json (NOT form-encoded). No signature header \u2014 BW voice webhooks are not HMAC-signed like Twilio's. Confirms adapter /bw/initiate field names: eventType, callId, from, to, direction.",
+ "note": "Content-Type application/json (NOT form-encoded). No signature header \u2014 BW voice webhooks are not HMAC-signed like Twilio's. Confirms translator /bw/initiate field names: eventType, callId, from, to, direction.",
"redacted": [
"accountId",
"applicationId",
diff --git a/test/recording-callback.test.ts b/test/recording-callback.test.ts
index 04720ac..e8c7b99 100644
--- a/test/recording-callback.test.ts
+++ b/test/recording-callback.test.ts
@@ -6,8 +6,8 @@ import type { CreateCallOpts } from "../src/bw/client.js";
// Kind-aware rewriter mirroring the one in src/server/app.ts.
const rw = (url: string, kind: UrlKind) =>
kind === "recordingStatus"
- ? `https://adapter.test/bw/recording-status?cb=${encodeURIComponent(url)}`
- : `https://adapter.test/bw/continue?next=${encodeURIComponent(url)}`;
+ ? `https://translator.test/bw/recording-status?cb=${encodeURIComponent(url)}`
+ : `https://translator.test/bw/continue?next=${encodeURIComponent(url)}`;
describe("Record recordingStatusCallback → recordingAvailableUrl", () => {
it("maps the async recording callback onto Bandwidth's recordingAvailableUrl", () => {
@@ -15,9 +15,9 @@ describe("Record recordingStatusCallback → recordingAvailableUrl", () => {
``,
{ rewriteUrl: rw },
);
- expect(r.bxml).toContain(`recordCompleteUrl="https://adapter.test/bw/continue?next=%2Fdone"`);
+ expect(r.bxml).toContain(`recordCompleteUrl="https://translator.test/bw/continue?next=%2Fdone"`);
expect(r.bxml).toContain(
- `recordingAvailableUrl="https://adapter.test/bw/recording-status?cb=%2Frec-ready"`,
+ `recordingAvailableUrl="https://translator.test/bw/recording-status?cb=%2Frec-ready"`,
);
expect(r.hasErrors).toBe(false);
});
@@ -34,7 +34,7 @@ describe("Record recordingStatusCallback → recordingAvailableUrl", () => {
const config = {
accountSid: "AC123",
authToken: "tok",
- publicBaseUrl: "https://adapter.test",
+ publicBaseUrl: "https://translator.test",
voiceUrl: "https://customer.test/voice",
allowPrivateEgress: true,
webhookUser: "u",
@@ -94,9 +94,9 @@ describe("POST /bw/recording-status (recording-available egress)", () => {
expect(body.get("RecordingStatus")).toBe("completed");
expect(body.get("RecordingDuration")).toBe("12");
expect(body.get("CallSid")).toMatch(/^CA[0-9a-f]{32}$/);
- // RecordingUrl points back at the adapter's own facade, not Bandwidth.
+ // RecordingUrl points back at the translator's own facade, not Bandwidth.
expect(body.get("RecordingUrl")).toContain(
- "https://adapter.test/2010-04-01/Accounts/AC123/Recordings/RE",
+ "https://translator.test/2010-04-01/Accounts/AC123/Recordings/RE",
);
expect((init.headers as Record)["X-Twilio-Signature"]).toBeTruthy();
});
diff --git a/test/server-bw-fixture.test.ts b/test/server-bw-fixture.test.ts
index 3692678..d2913ed 100644
--- a/test/server-bw-fixture.test.ts
+++ b/test/server-bw-fixture.test.ts
@@ -4,7 +4,7 @@ import { join } from "node:path";
import { buildApp } from "../src/server/app.js";
// The real Bandwidth initiate webhook, captured live (test/fixtures/bandwidth/
-// webhooks.json). Carries extra fields beyond what the adapter reads
+// webhooks.json). Carries extra fields beyond what the translator reads
// (privacy, applicationId, accountId, startTime, eventTime, callUrl,
// callerDisplayName). This proves those extras don't break the inbound path.
const bwFixture = JSON.parse(
@@ -21,7 +21,7 @@ describe("inbound path against the real captured BW initiate payload", () => {
{
accountSid: "ACtest",
authToken: "tok",
- publicBaseUrl: "https://adapter.test",
+ publicBaseUrl: "https://translator.test",
voiceUrl: "https://customer.test/voice",
allowPrivateEgress: true,
webhookUser: "u",
diff --git a/test/server-capture.test.ts b/test/server-capture.test.ts
index 522dbbd..92210c8 100644
--- a/test/server-capture.test.ts
+++ b/test/server-capture.test.ts
@@ -10,7 +10,7 @@ function baseConfig(captureDir?: string) {
return {
accountSid: "AC123",
authToken: "tok",
- publicBaseUrl: "https://adapter.test",
+ publicBaseUrl: "https://translator.test",
voiceUrl: "https://customer.test/voice",
allowPrivateEgress: true,
webhookUser: "u",
diff --git a/test/server-egress.test.ts b/test/server-egress.test.ts
index 5e555bc..a84980f 100644
--- a/test/server-egress.test.ts
+++ b/test/server-egress.test.ts
@@ -1,7 +1,7 @@
import { describe, it, expect, vi } from "vitest";
import { buildApp } from "../src/server/app.js";
-const base = { accountSid: "AC123", authToken: "tok", publicBaseUrl: "https://adapter.test", voiceUrl: "http://127.0.0.1:4000/voice", webhookUser: "u", webhookPassword: "p" };
+const base = { accountSid: "AC123", authToken: "tok", publicBaseUrl: "https://translator.test", voiceUrl: "http://127.0.0.1:4000/voice", webhookUser: "u", webhookPassword: "p" };
const bwClient = { createCall: vi.fn(), modifyCall: vi.fn(), getCall: vi.fn(), listRecordings: vi.fn(), getRecording: vi.fn(), getRecordingMedia: vi.fn(), updateRecording: vi.fn() };
const authHeader = "Basic " + Buffer.from("u:p").toString("base64");
diff --git a/test/server-errors.test.ts b/test/server-errors.test.ts
index c9f9a27..44df6c1 100644
--- a/test/server-errors.test.ts
+++ b/test/server-errors.test.ts
@@ -1,11 +1,11 @@
// test/server-errors.test.ts
import { describe, it, expect } from "vitest";
-import { buildApp, type AdapterConfig, type AdapterDeps } from "../src/server/app.js";
+import { buildApp, type ServerConfig, type ServerDeps } from "../src/server/app.js";
-const config: AdapterConfig = {
+const config: ServerConfig = {
accountSid: "AC123",
authToken: "tok",
- publicBaseUrl: "https://adapter.test",
+ publicBaseUrl: "https://translator.test",
voiceUrl: "https://customer.test/voice",
webhookUser: "u",
webhookPassword: "p",
@@ -13,9 +13,9 @@ const config: AdapterConfig = {
const auth = "Basic " + Buffer.from("AC123:tok").toString("base64");
const webhookAuth = "Basic " + Buffer.from("u:p").toString("base64");
-describe("structured adapter errors", () => {
+describe("structured translator errors", () => {
it("returns the Twilio-shaped body for a missing parameter", async () => {
- const app = buildApp(config, { fetchImpl: fetch, bwClient: {} as AdapterDeps["bwClient"] });
+ const app = buildApp(config, { fetchImpl: fetch, bwClient: {} as ServerDeps["bwClient"] });
// /bw/continue with no ?next= is a missing-param error (with valid webhook auth)
const res = await app.inject({
method: "POST",
@@ -33,7 +33,7 @@ describe("structured adapter errors", () => {
// createCall must succeed so the store gets seeded and we learn the real SID.
createCall: async () => ({ callId: "bw-call-1" }),
getCall: async () => { throw new Error("BW 503 SECRET-UPSTREAM-BODY"); },
- } as unknown as AdapterDeps["bwClient"];
+ } as unknown as ServerDeps["bwClient"];
const app = buildApp(config, { fetchImpl: fetch, bwClient });
const created = await app.inject({
diff --git a/test/server-inbound-auth.test.ts b/test/server-inbound-auth.test.ts
index f385276..71ef1c6 100644
--- a/test/server-inbound-auth.test.ts
+++ b/test/server-inbound-auth.test.ts
@@ -4,7 +4,7 @@ import { buildApp } from "../src/server/app.js";
const config = {
accountSid: "AC123",
authToken: "tok",
- publicBaseUrl: "https://adapter.test",
+ publicBaseUrl: "https://translator.test",
voiceUrl: "https://customer.test/voice",
webhookUser: "bw-user",
webhookPassword: "bw-pass",
diff --git a/test/server-inbound.test.ts b/test/server-inbound.test.ts
index 223f6e0..b48b1bf 100644
--- a/test/server-inbound.test.ts
+++ b/test/server-inbound.test.ts
@@ -4,7 +4,7 @@ import { buildApp } from "../src/server/app.js";
const config = {
accountSid: "AC123",
authToken: "tok",
- publicBaseUrl: "https://adapter.test",
+ publicBaseUrl: "https://translator.test",
voiceUrl: "https://customer.test/voice",
allowPrivateEgress: true,
webhookUser: "u",
@@ -59,7 +59,7 @@ describe("POST /bw/initiate", () => {
payload: { eventType: "initiate", callId: "c-2", from: "+1", to: "+2", direction: "inbound" },
});
expect(res.body).toContain(
- `gatherUrl="https://adapter.test/bw/continue?next=${encodeURIComponent("https://customer.test/menu")}"`,
+ `gatherUrl="https://translator.test/bw/continue?next=${encodeURIComponent("https://customer.test/menu")}"`,
);
});
diff --git a/test/server-readyz.test.ts b/test/server-readyz.test.ts
index cc8052e..d4b1780 100644
--- a/test/server-readyz.test.ts
+++ b/test/server-readyz.test.ts
@@ -1,21 +1,21 @@
// test/server-readyz.test.ts
import { describe, it, expect, afterEach, vi } from "vitest";
-import { buildApp, type AdapterConfig, type AdapterDeps } from "../src/server/app.js";
+import { buildApp, type ServerConfig, type ServerDeps } from "../src/server/app.js";
import { REQUIRED_ENV } from "../src/server/readiness.js";
-const config: AdapterConfig = {
+const config: ServerConfig = {
accountSid: "AC123",
authToken: "tok",
- publicBaseUrl: "https://adapter.test",
+ publicBaseUrl: "https://translator.test",
voiceUrl: "https://customer.test/voice",
webhookUser: "u",
webhookPassword: "p",
};
-const deps = (over: Partial = {}): AdapterDeps => ({
+const deps = (over: Partial = {}): ServerDeps => ({
fetchImpl: fetch,
// minimal bwClient stub; /readyz never calls it (no outbound work at all)
- bwClient: {} as AdapterDeps["bwClient"],
+ bwClient: {} as ServerDeps["bwClient"],
...over,
});
diff --git a/test/server-rest-get.test.ts b/test/server-rest-get.test.ts
index 4546a9d..cbd6949 100644
--- a/test/server-rest-get.test.ts
+++ b/test/server-rest-get.test.ts
@@ -5,7 +5,7 @@ import type { CreateCallOpts, GetCallResult } from "../src/bw/client.js";
const config = {
accountSid: "AC123",
authToken: "tok",
- publicBaseUrl: "https://adapter.test",
+ publicBaseUrl: "https://translator.test",
voiceUrl: "https://customer.test/voice",
webhookUser: "u",
webhookPassword: "p",
diff --git a/test/server-rest-modify.test.ts b/test/server-rest-modify.test.ts
index 4c52fcf..e0b20ba 100644
--- a/test/server-rest-modify.test.ts
+++ b/test/server-rest-modify.test.ts
@@ -5,7 +5,7 @@ import type { CreateCallOpts, ModifyCallOpts } from "../src/bw/client.js";
const config = {
accountSid: "AC123",
authToken: "tok",
- publicBaseUrl: "https://adapter.test",
+ publicBaseUrl: "https://translator.test",
voiceUrl: "https://customer.test/voice",
webhookUser: "u",
webhookPassword: "p",
@@ -26,7 +26,7 @@ function makeApp() {
return { app, bwClient };
}
-// Put a call in the adapter's store and return its Twilio CallSid.
+// Put a call in the translator's store and return its Twilio CallSid.
async function createCall(app: ReturnType["app"]): Promise {
const res = await app.inject({
method: "POST",
@@ -54,7 +54,7 @@ describe("POST /2010-04-01/Accounts/:sid/Calls/:callSid.json (modify live call)"
expect(res.json().sid).toBe(sid);
});
- it("Url redirects the call through the adapter to the new customer TwiML", async () => {
+ it("Url redirects the call through the translator to the new customer TwiML", async () => {
const { app, bwClient } = makeApp();
const sid = await createCall(app);
@@ -68,7 +68,7 @@ describe("POST /2010-04-01/Accounts/:sid/Calls/:callSid.json (modify live call)"
expect(res.statusCode).toBe(200);
expect(bwClient.modifyCall).toHaveBeenCalledWith("c-out-1", {
state: "active",
- redirectUrl: `https://adapter.test/bw/initiate?voiceUrl=${encodeURIComponent("https://customer.test/step2")}`,
+ redirectUrl: `https://translator.test/bw/initiate?voiceUrl=${encodeURIComponent("https://customer.test/step2")}`,
redirectMethod: "POST",
});
});
diff --git a/test/server-rest-recordings.test.ts b/test/server-rest-recordings.test.ts
index 215e80d..5502a65 100644
--- a/test/server-rest-recordings.test.ts
+++ b/test/server-rest-recordings.test.ts
@@ -6,7 +6,7 @@ import type { CreateCallOpts, BwRecording } from "../src/bw/client.js";
const config = {
accountSid: "AC123",
authToken: "tok",
- publicBaseUrl: "https://adapter.test",
+ publicBaseUrl: "https://translator.test",
voiceUrl: "https://customer.test/voice",
webhookUser: "u",
webhookPassword: "p",
@@ -108,7 +108,7 @@ describe("GET /2010-04-01/Accounts/:sid/Calls/:callSid/Recordings.json", () => {
});
describe("GET /2010-04-01/Accounts/:sid/Recordings/:recordingSid", () => {
- // List first so the adapter learns recordingSid -> (callId, recordingId).
+ // List first so the translator learns recordingSid -> (callId, recordingId).
async function listThenRecordingSid(app: ReturnType["app"]) {
const callSid = await createCall(app);
const list = await app.inject({
diff --git a/test/server-rest.test.ts b/test/server-rest.test.ts
index d34c811..a123e46 100644
--- a/test/server-rest.test.ts
+++ b/test/server-rest.test.ts
@@ -5,7 +5,7 @@ import type { CreateCallOpts } from "../src/bw/client.js";
const config = {
accountSid: "AC123",
authToken: "tok",
- publicBaseUrl: "https://adapter.test",
+ publicBaseUrl: "https://translator.test",
voiceUrl: "https://customer.test/voice",
webhookUser: "u",
webhookPassword: "p",
@@ -27,7 +27,7 @@ describe("POST /2010-04-01/Accounts/:sid/Calls.json", () => {
}
const auth = "Basic " + Buffer.from("AC123:tok").toString("base64");
- it("creates a BW call with adapter answerUrl and returns Twilio-shaped JSON", async () => {
+ it("creates a BW call with translator answerUrl and returns Twilio-shaped JSON", async () => {
const { app, bwClient } = makeApp();
const res = await app.inject({
method: "POST",
@@ -51,7 +51,7 @@ describe("POST /2010-04-01/Accounts/:sid/Calls.json", () => {
const callArgs = bwClient.createCall.mock.calls[0][0];
expect(callArgs.to).toBe("+15552223333");
expect(callArgs.answerUrl).toBe(
- `https://adapter.test/bw/initiate?voiceUrl=${encodeURIComponent("https://customer.test/outbound")}`,
+ `https://translator.test/bw/initiate?voiceUrl=${encodeURIComponent("https://customer.test/outbound")}`,
);
});
diff --git a/test/server-status-callback.test.ts b/test/server-status-callback.test.ts
index 425bb55..711d399 100644
--- a/test/server-status-callback.test.ts
+++ b/test/server-status-callback.test.ts
@@ -5,7 +5,7 @@ import type { CreateCallOpts } from "../src/bw/client.js";
const config = {
accountSid: "AC123",
authToken: "tok",
- publicBaseUrl: "https://adapter.test",
+ publicBaseUrl: "https://translator.test",
voiceUrl: "https://customer.test/voice",
allowPrivateEgress: true,
webhookUser: "u",
diff --git a/test/translate-callback-auth.test.ts b/test/translate-callback-auth.test.ts
index 899382a..b161f32 100644
--- a/test/translate-callback-auth.test.ts
+++ b/test/translate-callback-auth.test.ts
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
import { translateTwiml } from "../src/translator/translate.js";
const auth = { username: "bw-user", password: "bw-pass" };
-const rewriteUrl = (u: string) => `https://adapter.test/bw/continue?next=${encodeURIComponent(u)}`;
+const rewriteUrl = (u: string) => `https://translator.test/bw/continue?next=${encodeURIComponent(u)}`;
describe("callback auth stamping", () => {
it("adds username/password to a Gather that has a rewritten action", () => {
diff --git a/test/translate-core.test.ts b/test/translate-core.test.ts
index 2a41369..9b14e15 100644
--- a/test/translate-core.test.ts
+++ b/test/translate-core.test.ts
@@ -56,10 +56,10 @@ describe("core verb translation", () => {
});
it("Redirect text URL → redirectUrl attr, rewritten via option", () => {
const r = translateTwiml(`/next`, {
- rewriteUrl: (url) => `https://adapter.test/bw/continue?next=${encodeURIComponent(url)}`,
+ rewriteUrl: (url) => `https://translator.test/bw/continue?next=${encodeURIComponent(url)}`,
});
expect(r.bxml).toContain(
- ``,
+ ``,
);
});
it("unknown verb yields error finding and no output element", () => {
diff --git a/test/translate-dial.test.ts b/test/translate-dial.test.ts
index 622cecb..1dd6fac 100644
--- a/test/translate-dial.test.ts
+++ b/test/translate-dial.test.ts
@@ -58,11 +58,11 @@ describe("Connect/Stream", () => {
``,
{
rewriteUrl: (u, k) =>
- k === "stream" ? `wss://adapter.test/streams?dest=${encodeURIComponent(u)}` : u,
+ k === "stream" ? `wss://translator.test/streams?dest=${encodeURIComponent(u)}` : u,
},
);
expect(r.bxml).toContain(
- ` f.severity === "warning" && f.verb === "Stream")).toBe(true);
});
diff --git a/test/translate-gather-record.test.ts b/test/translate-gather-record.test.ts
index 4baaeca..90a946b 100644
--- a/test/translate-gather-record.test.ts
+++ b/test/translate-gather-record.test.ts
@@ -1,7 +1,7 @@
import { describe, it, expect } from "vitest";
import { translateTwiml } from "../src/translator/translate.js";
-const rw = (url: string) => `https://adapter.test/bw/continue?next=${encodeURIComponent(url)}`;
+const rw = (url: string) => `https://translator.test/bw/continue?next=${encodeURIComponent(url)}`;
describe("Gather", () => {
it("maps attributes and nested prompts", () => {
@@ -12,7 +12,7 @@ describe("Gather", () => {
expect(r.bxml).toContain(`maxDigits="1"`);
expect(r.bxml).toContain(`firstDigitTimeout="7"`);
expect(r.bxml).toContain(`terminatingDigits="#"`);
- expect(r.bxml).toContain(`gatherUrl="https://adapter.test/bw/continue?next=%2Fmenu"`);
+ expect(r.bxml).toContain(`gatherUrl="https://translator.test/bw/continue?next=%2Fmenu"`);
expect(r.bxml).toContain(`Press 1`);
expect(r.hasErrors).toBe(false);
});
@@ -44,6 +44,6 @@ describe("Record", () => {
);
expect(r.bxml).toContain(`maxDuration="30"`);
expect(r.bxml).toContain(`terminatingDigits="#"`);
- expect(r.bxml).toContain(`recordCompleteUrl="https://adapter.test/bw/continue?next=%2Fdone"`);
+ expect(r.bxml).toContain(`recordCompleteUrl="https://translator.test/bw/continue?next=%2Fdone"`);
});
});
diff --git a/test/translate-latency.test.ts b/test/translate-latency.test.ts
index 6ae6174..d60300a 100644
--- a/test/translate-latency.test.ts
+++ b/test/translate-latency.test.ts
@@ -11,7 +11,7 @@ import { translateTwiml } from "../src/translator/translate.js";
describe("latency: TwiML→BXML translation tax", () => {
it("p99 stays under 5 ms per turn across the verb surface", () => {
const rewriteUrl = (u: string) =>
- `https://adapter.example/bw/continue?next=${encodeURIComponent(u)}`;
+ `https://translator.example/bw/continue?next=${encodeURIComponent(u)}`;
const corpus = [
`Thanks for calling. Goodbye.`,
`For sales press 1. For support press 2./welcome`,
diff --git a/test/translate-loop.test.ts b/test/translate-loop.test.ts
index c0f4c53..03f5c95 100644
--- a/test/translate-loop.test.ts
+++ b/test/translate-loop.test.ts
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
import { translateTwiml } from "../src/translator/translate.js";
// Twilio's loop="N" repeats a /. BXML has no loop attribute, so the
-// adapter expands a finite count into repeated verbs. loop="0" (infinite) and
+// translator expands a finite count into repeated verbs. loop="0" (infinite) and
// invalid counts can't be expressed inline, so they fall back to a single play
// plus a warning (no silent degradation).
describe("Say loop", () => {
diff --git a/test/translate-new-verbs.test.ts b/test/translate-new-verbs.test.ts
index 63e38af..4f2de60 100644
--- a/test/translate-new-verbs.test.ts
+++ b/test/translate-new-verbs.test.ts
@@ -147,7 +147,7 @@ describe("Start > Transcription → StartTranscription", () => {
});
it("Start with unsupported noun produces an error", () => {
- // Siprec is a real Twilio noun the adapter does not map (Stream and
+ // Siprec is a real Twilio noun the translator does not map (Stream and
// Transcription under ARE supported).
const r = translateTwiml(``);
expect(r.hasErrors).toBe(true);
@@ -224,15 +224,15 @@ describe("SpeakSentence voice mapping – expanded BW voice allowlist", () => {
// ─── Refer → Refer (SIP REFER) ───────────────────────────────────────────────
describe("Refer → Refer", () => {
- const rewrite = { rewriteUrl: (u: string) => `https://adapter.test/bw/continue?next=${encodeURIComponent(u)}` };
+ const rewrite = { rewriteUrl: (u: string) => `https://translator.test/bw/continue?next=${encodeURIComponent(u)}` };
- it("maps to BW with referCompleteUrl rewritten through the adapter", () => {
+ it("maps to BW with referCompleteUrl rewritten through the translator", () => {
const r = translateTwiml(
`sip:alice@atlanta.example.com`,
rewrite,
);
expect(r.bxml).toContain(`sip:alice@atlanta.example.com`);
- expect(r.bxml).toContain(`referCompleteUrl="https://adapter.test/bw/continue?next=`);
+ expect(r.bxml).toContain(`referCompleteUrl="https://translator.test/bw/continue?next=`);
expect(r.bxml).toContain(`referCompleteMethod="POST"`);
expect(r.hasErrors).toBe(false);
});
diff --git a/test/translate-stream-conference.test.ts b/test/translate-stream-conference.test.ts
index b4e09b0..586dfbf 100644
--- a/test/translate-stream-conference.test.ts
+++ b/test/translate-stream-conference.test.ts
@@ -2,7 +2,7 @@ import { describe, it, expect } from "vitest";
import { translateTwiml } from "../src/translator/translate.js";
const rw = (u: string) =>
- u.startsWith("wss") ? `wss://adapter.test/streams?dest=${encodeURIComponent(u)}` : u;
+ u.startsWith("wss") ? `wss://translator.test/streams?dest=${encodeURIComponent(u)}` : u;
describe("Stream lifecycle", () => {
it("Connect>Stream → StartStream mode=bidirectional with name and tracks", () => {
diff --git a/test/webhook-auth-encoding.test.ts b/test/webhook-auth-encoding.test.ts
index cba9ec8..db161b3 100644
--- a/test/webhook-auth-encoding.test.ts
+++ b/test/webhook-auth-encoding.test.ts
@@ -8,7 +8,7 @@ import { buildApp } from "../src/server/app.js";
const config = {
accountSid: "AC123",
authToken: "tok",
- publicBaseUrl: "https://adapter.test",
+ publicBaseUrl: "https://translator.test",
voiceUrl: "https://customer.test/voice",
webhookUser: "bw-user",
webhookPassword: "bw-pass",
diff --git a/test/webhook-id-validation.test.ts b/test/webhook-id-validation.test.ts
index 48680ce..ea70b95 100644
--- a/test/webhook-id-validation.test.ts
+++ b/test/webhook-id-validation.test.ts
@@ -8,7 +8,7 @@ import { buildApp } from "../src/server/app.js";
const config = {
accountSid: "AC123",
authToken: "tok",
- publicBaseUrl: "https://adapter.test",
+ publicBaseUrl: "https://translator.test",
voiceUrl: "https://customer.test/voice",
webhookUser: "u",
webhookPassword: "p",
@@ -67,7 +67,7 @@ describe("/bw/* ingress id validation", () => {
});
describe("/bw/* ingress id validation — more cases", () => {
- const cfg = { accountSid: "AC123", authToken: "tok", publicBaseUrl: "https://adapter.test", voiceUrl: "https://customer.test/voice", webhookUser: "u", webhookPassword: "p", allowPrivateEgress: true };
+ const cfg = { accountSid: "AC123", authToken: "tok", publicBaseUrl: "https://translator.test", voiceUrl: "https://customer.test/voice", webhookUser: "u", webhookPassword: "p", allowPrivateEgress: true };
const authHdr = "Basic " + Buffer.from("u:p").toString("base64");
function app() {
const fetchImpl = vi.fn(async () => new Response("", { status: 200 })) as unknown as typeof fetch;
diff --git a/web/README.md b/web/README.md
index 4cdcaff..63eb490 100644
--- a/web/README.md
+++ b/web/README.md
@@ -6,7 +6,7 @@ No server, no install, no network. It works offline on a plane.
## Why it's trustworthy
-The page runs the **real translation engine** (`src/translator`, `src/compatibility-check`) and the real compatibility matrix (`src/matrix/twilio-voice.json`) in the browser — the exact same code the live adapter uses. It does not reimplement any translation or scoring logic. Because the same matrix drives the adapter and this page, the verdict shown here provably matches what happens in production. That invariant is the point; don't break it by hand-coding results into the UI.
+The page runs the **real translation engine** (`src/translator`, `src/compatibility-check`) and the real compatibility matrix (`src/matrix/twilio-voice.json`) in the browser — the exact same code the live translator uses. It does not reimplement any translation or scoring logic. Because the same matrix drives the translator and this page, the verdict shown here provably matches what happens in production. That invariant is the point; don't break it by hand-coding results into the UI.
## Build & run
diff --git a/web/index.html b/web/index.html
index ecf0d5c..632bbd4 100644
--- a/web/index.html
+++ b/web/index.html
@@ -25,7 +25,7 @@
Twilio → Bandwidth Migration Compatibility C
- Analyzes emitted TwiML — exactly what the adapter sees at runtime.
+ Analyzes emitted TwiML — exactly what the translator sees at runtime.
diff --git a/web/view-model.ts b/web/view-model.ts
index 509e51b..557c626 100644
--- a/web/view-model.ts
+++ b/web/view-model.ts
@@ -4,8 +4,8 @@
// calls the real engine (analyzeSource / complexityScore / translateTwiml) and
// the real compatibility matrix, then shapes the result for rendering. It does
// NOT reimplement any translation, scoring, or compatibility rules — that is the
-// honesty invariant: because the same matrix drives the live adapter and this
-// page, the verdict shown here provably matches what the adapter does in
+// honesty invariant: because the same matrix drives the live translator and this
+// page, the verdict shown here provably matches what the translator does in
// production. Keep this file DOM-free so it stays unit-testable.
import { analyzeSource } from "../src/compatibility-check/analyze.js";