Webex Call Provisioning tool
Find a file
jmcqueen 261d11f29d
Some checks are pending
CI / verify (push) Waiting to run
Harden /finalizeStore against pending-order and DECT-404 noise
A finalize run against a store whose phone number still had a pending
carrier order surfaced three unrelated-looking 400s (add-number,
caller ID = LOCATION_NUMBER, auto attendant "number already used")
that all traced back to the number not being attached, plus a
spurious "DECT network pre-check failed" warn when the location had
never had a DECT network before.

Fixes:
- addPhoneNumbersToLocation now translates the raw
  NUMBER_HAS_PENDING_ORDERS payload into an actionable operator
  message ("wait for the carrier order, then re-run /finalizeStore").
  Original error preserved as .cause. Extracted as pure
  translateAddNumberError so it can be unit-tested.
- finalizeStore marks the phone-number-add step { critical: true } so
  finalize aborts immediately on that failure instead of cascading
  three downstream errors that bury the root cause.
- findDectNetworkInLocation treats HTTP 404 as "no networks yet"
  (Webex returns 404 for that state, not an empty list), so the
  finalize pre-check stops warning on the normal first-time path.
  The remaining warn now includes status code for anything that does
  reach it.

Tests: 4 new cases in test/locations.test.js locking down the
translator behavior against the exact Webex payload shape.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-09 12:35:00 -04:00
.gitea/workflows Add node:test suite and Gitea Actions CI 2026-07-06 16:07:02 -04:00
docker/remote-agent Harden agent bridge + command inputs 2026-07-06 15:48:54 -04:00
greetings Modernize bot to Node.js 22 with modular architecture and remote SIW agent 2026-07-06 14:48:51 -04:00
scripts Modernize bot to Node.js 22 with modular architecture and remote SIW agent 2026-07-06 14:48:51 -04:00
src Harden /finalizeStore against pending-order and DECT-404 noise 2026-07-09 12:35:00 -04:00
test Harden /finalizeStore against pending-order and DECT-404 noise 2026-07-09 12:35:00 -04:00
.dockerignore Add node:test suite and Gitea Actions CI 2026-07-06 16:07:02 -04:00
.env.example Consistency + cleanup pass 2026-07-06 15:56:01 -04:00
.gitignore Modernize bot to Node.js 22 with modular architecture and remote SIW agent 2026-07-06 14:48:51 -04:00
.prettierignore Add node:test suite and Gitea Actions CI 2026-07-06 16:07:02 -04:00
.prettierrc.json Modernize bot to Node.js 22 with modular architecture and remote SIW agent 2026-07-06 14:48:51 -04:00
Dockerfile Modernize bot to Node.js 22 with modular architecture and remote SIW agent 2026-07-06 14:48:51 -04:00
eslint.config.js Modernize bot to Node.js 22 with modular architecture and remote SIW agent 2026-07-06 14:48:51 -04:00
package-lock.json Modernize bot to Node.js 22 with modular architecture and remote SIW agent 2026-07-06 14:48:51 -04:00
package.json Fix requestJson dropping Content-Type on POSTs (HTTP 415) 2026-07-09 12:07:15 -04:00
README.md Add /provisionPhone for wired desk phones (7841/7821) 2026-07-07 12:39:06 -04:00

wbxStoreProvision

Webex Calling provisioning bot for AEO retail stores. Runs as a Webex bot and exposes slash commands that create Webex locations, attach phone numbers, upload greetings, build auto-attendants, and clean up user licensing.

Requirements

  • Node.js 20+ (Docker image uses node:22-alpine).
  • A Webex bot token, a Webex integration (service account) with the scopes currently used by admin API calls, a Twilio lookup account, an SIW basic-auth user, and a Google API key with Address Validation + Time Zone enabled.
  • An on-prem host that can reach Store Info Web, Google APIs, and Twilio Lookup from an IP-whitelisted subnet, to run the remote agent (see Remote agent (SIW + Google) below).

Local setup

cp .env.example .env
# Edit .env with real values
npm install
npm start

The token-refresh cron reads and writes config/wbxTokens.json (path configurable via WEBEX_TOKEN_STORE). Seed this file once with a valid access_token, refresh_token, and expiresOn; the process will keep it up to date from then on.

Scripts

npm script What it does
npm start Run the bot.
npm run dev Run with node --watch for local iteration.
npm run lint ESLint (flat config) across the tree.
npm run format Prettier write.
npm run generate-911-csv Rebuild buildingFile.csv + locationFile.csv from SIW.
npm run fix-store-phones Normalize licensing + default meeting site for store users.

Docker

docker build -t wbxcallprov .
docker run --rm --env-file .env \
    -p 8080:8080 \
    -v "$(pwd)/config:/app/config" \
    wbxcallprov

The volume mount preserves config/wbxTokens.json across restarts so the refresh cron doesn't lose state. The -p 8080:8080 publishes the WebSocket port so the remote SIW agent can connect back to the bot.

Remote agent (SIW + Google)

Some services can only be reached from inside the corporate network:

  • Store Info Web — only accepts connections from internal IPs, and presents an internal-CA TLS cert Node's default trust store doesn't know about.
  • Google Maps / Address Validation — the API key is IP-restricted, so requests direct from the cloud bot IP get API_KEY_IP_ADDRESS_BLOCKED (403).

To bridge the gap, the bot hosts a WebSocket server; a small on-prem agent dials in and proxies HTTP requests back and forth. Both SIW and Google calls go over this bridge so they originate from the on-prem IP.

The agent itself is intentionally generic — it just proxies whatever {method, url, headers, auth, body, insecure?} payload arrives — and lives in docker/remote-agent/ as a self-contained Docker bundle you can build here and ship to the on-prem host. The insecure: true flag is scoped per request. Both SIW and Google set it, because the corporate network the agent lives on runs SSL-inspecting proxies that intercept outbound HTTPS with an internal-CA chain — without the flag Node throws SELF_SIGNED_CERT_IN_CHAIN even for Google's public certs. Trust is delegated to that proxy by policy, so the scoped bypass is consistent across all proxied traffic.

Once the agent is connected, the bot logs Remote agent connected and /stageStore and /finalizeStore will succeed. If the agent is not connected, those commands fail immediately with a preflight message rather than silently timing out mid-flow.

Deploying the agent

The docker/remote-agent/ folder produces a fully offline-installable ZIP (image tarball + install.sh + docker-compose.yml). The workflow is the same as netanalyzer's bundle — same layout, same operator playbook — and the two bundles use distinct image tags and container names (wbxprov-remote-agent vs sha-remote-agent) so a single on-prem host can run both agents side by side.

Build the bundle (on your workstation, or in CI):

# Default: linux/amd64 (typical Rocky/RHEL/Ubuntu server)
npm run agent:package

# Or explicitly, with a different target arch:
./docker/remote-agent/package.sh --platform linux/arm64

That produces docker/remote-agent/dist/wbxprov-remote-agent-<version>.zip. Transfer it to the on-prem host, then:

unzip wbxprov-remote-agent-<version>.zip
cd wbxprov-remote-agent-<version>
./install.sh                        # first run: seeds .env and stops
vi .env                             # set WS_URL + WS_TOKEN
./install.sh                        # second run: starts the container
docker compose logs -f              # tail the agent's connection status

Set WS_URL to the bot's public WebSocket endpoint (e.g. wss://your-wbxstoreprovision-host:8080) and WS_TOKEN to the same value you configured for WS_TOKEN in the bot's .env. See docker/remote-agent/README.md for build details and docker/remote-agent/deploy/README.md for the full operator guide (upgrades, troubleshooting, coexistence with sha-remote-agent).

Bot commands

Registered in src/commands:

  • /stageStore <storeNumber> — creates the Webex location, enables calling, configures the schedule, greeting, music-on-hold, voicemail, voice portal, and attaches the Webex Calling license to the store user. Everything except the phone number, which has to be purchased separately in Control Hub.
  • /finalizeStore <storeNumber> — cut-over for a previously staged store: attaches the purchased phone number, sets caller ID, creates the Store XXXX DECT network (DBS-210) with the per-store default access code, creates the auto-attendant, and finalizes user licensing. Preflights that the store was actually staged first, and is idempotent on the DECT network (skips if one already exists).
  • /provisionDect <storeNumber> — manage DECT phones for a store: card for adding basestations by MAC and adding/removing handsets. Every removal goes through an explicit confirmation card. New stores have their DECT network created by /finalizeStore; this command still offers a recovery "create network" prompt for legacy stores or if finalize's DECT step was skipped. All Webex API — no remote agent required.
  • /provisionPhone <storeNumber> — manage wired desk phones (Cisco 7841 / 7821) owned by the store user (resolved via ae<5-digit>@ae.com). The card shows the store user's current wired phones (DECT handsets are filtered out — those live in /provisionDect), with an add form (MAC + model) and a checkbox list for removal. Adds are idempotent (MACs already registered are skipped), and every removal goes through an explicit confirmation card. All Webex API — no remote agent required.
  • /storeinfo <storeNumber> — show current Webex info for the store user (ae<5-digit>@ae.com).
  • /userinfo <email> — show current Webex info for any user by email.
  • /help — bot's own help output.

Typical provisioning workflow: /stageStore 499 → purchase a phone number in Control Hub → /finalizeStore 499.

Card confirmations post attachmentAction events, dispatched in src/commands/attachmentActions.js.

Architecture

src/
  index.js               bot bootstrap, cron, command wiring (~70 lines)
  config.js              dotenv loading + validation
  constants.js           org-scoped IDs (route groups, licenses, greetings, ...)
  logger.js              small level-aware logger
  http.js                fetch wrapper (rate limit + optional SIW TLS agent)
  webex/                 all Webex API calls, one module per resource family
  integrations/          SIW / Twilio / Google
  cards/                 Adaptive Card builders
  flows/                 multi-step provisioning (stage, finalize)
  commands/              framework.hears handlers + attachmentAction dispatch
scripts/                 one-off maintenance scripts (911 CSV, phone fix-up)
greetings/               WAV files uploaded as location announcements
config/wbxTokens.json    persistent service-account token cache (git-ignored)

Data flow for a store provisioning:

flowchart LR
    User[Webex user] -->|/stageStore 1234| Commands[commands/*]
    Commands --> SIW[integrations/siw.js]
    Commands --> Users[webex/users.js]
    Commands --> Card[cards/storeConfirmationCard.js]
    Card -->|confirm| Attachment[attachmentActions.js]
    Attachment --> Flow[flows/stageStore.js]
    Flow --> Locations[webex/locations.js]
    Flow --> Devices[webex/devices.js]
    Flow --> Announce[webex/announcements.js]
    Flow --> License[webex/licensing.js]
    subgraph background
      Cron[node-cron every 60s] --> Auth[webex/auth.js]
      Auth --> Tokens[(config/wbxTokens.json)]
    end

Security notes

  • .env and config/wbxTokens.json are git-ignored. Never commit them.
  • NODE_TLS_REJECT_UNAUTHORIZED=0 is no longer set globally. If SIW's TLS cert cannot be verified from your host, set ALLOW_INSECURE_SIW_TLS=true; the insecure dispatcher is then scoped to SIW requests only.
  • Concurrent Webex token refreshes are collapsed into a single in-flight refresh in src/webex/auth.js.

One-time secret rotation

The pre-refactor config.json and getMeeting.js stored real secrets in plain text on disk. Regardless of git history, treat the following as exposed and rotate them at their source:

  • Webex bot token (WEBEX_BOT_TOKEN)
  • Webex integration client secret + service-account access / refresh tokens (WEBEX_SVC_CLIENT_SECRET, plus everything in config/wbxTokens.json)
  • Twilio auth token (TWILIO_AUTH_TOKEN)
  • SIW basic-auth password (SIW_PASSWORD)
  • Google API key (GOOGLE_API_KEY) and any service-account private key that was previously in config.json

After rotating, populate the new values in .env and seed a fresh config/wbxTokens.json with the new access/refresh token pair.