- Refactor monolithic index.js (2646 lines) into src/{webex,integrations,
cards,flows,commands,services} modules; replace node-fetch/form-data
with native fetch/FormData; move all secrets to .env via dotenv
- Add dockerized remote SIW agent (docker/remote-agent/) with cross-arch
buildx packaging (arm64 Mac -> linux/amd64), idempotent install.sh
deploy bundle, and docker-free ZIP inspector for arch verification
- Bot hosts a WebSocket server; agent proxies SIW requests with a
per-request insecure:true flag, replacing the process-wide
NODE_TLS_REJECT_UNAUTHORIZED bypass
- Add ESLint flat config + Prettier, rewrite Dockerfile as non-root
multi-stage node:22-alpine build, README covering setup / deploy /
remote agent workflow
- Fix parseStoreArg to read trigger.prompt correctly (was indexing past
the framework's post-match slice); register /help as regex (string
matcher only compares the first token); switch catch-all to /.+/
(previous /.*/gim was stateful due to the g flag); remove
/fixDisplayNames command and its flow/card
Co-authored-by: Cursor <cursoragent@cursor.com>
|
||
|---|---|---|
| docker/remote-agent | ||
| greetings | ||
| scripts | ||
| src | ||
| .dockerignore | ||
| .env.example | ||
| .gitignore | ||
| .prettierignore | ||
| .prettierrc.json | ||
| Dockerfile | ||
| eslint.config.js | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
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, to run the remote agent (see Remote SIW agent below).
- Optional: a Google service-account JSON key. Only the REST API key is
required today, but if/when you add code that uses
google-auth-library, save the JSON atconfig/google-service-account.jsonand setGOOGLE_APPLICATION_CREDENTIALSin.envto point at it. The file is git-ignored.
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 SIW agent
Store Info Web only accepts connections from inside the corporate network, but the bot runs in the cloud. To bridge the gap, the bot hosts a WebSocket server; a small on-prem agent dials in and proxies HTTP requests back and forth.
The agent itself is intentionally generic — it just proxies whatever
{method, url, headers, auth, body} 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.
Once the agent is connected, the bot logs Remote agent connected and any
/buildStore, /stageStore, /migrateStore command will succeed. If the
agent is not connected, the SIW-dependent commands fail immediately with
No remote SIW agent connected rather than silently timing out.
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:
/buildStore <storeNumber>— full green-field build (create location, calling, greeting, attach user, license cleanup)./stageStore <storeNumber>— pre-migration setup: same as buildStore but without phone-number attachment or licensing cleanup./migrateStore <storeNumber>— cut-over for a staged store: attach the phone number, set caller ID, create the auto-attendant, finalize licensing./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.
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 (build, stage, migrate, ...)
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] -->|/buildStore 1234| Commands[commands/*]
Commands --> SIW[integrations/siw.js]
Commands --> Users[webex/users.js]
Commands --> Card[cards/storeInfoCard.js]
Card -->|confirm| Attachment[attachmentActions.js]
Attachment --> Flow[flows/buildStore.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
.envandconfig/wbxTokens.jsonare git-ignored. Never commit them.NODE_TLS_REJECT_UNAUTHORIZED=0is no longer set globally. If SIW's TLS cert cannot be verified from your host, setALLOW_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 inconfig/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 inconfig.json
After rotating, populate the new values in .env and seed a fresh
config/wbxTokens.json with the new access/refresh token pair.