Some checks are pending
CI / verify (push) Waiting to run
New /provisionDect slash command manages DECT basestations and handsets for a store via a single card: multi-select checkbox list doubles as the display, MACs render as AA:BB:CC:DD:EE:FF, Add/Remove sit side-by-side per section, and every removal goes through an explicit confirm card. Handsets always auto-pair (no bind-to-basestation input) so they roam. /finalizeStore now idempotently creates the "Store XXXX" DECT network (DBS-210) with the per-store default access code, so new stores are DECT-ready the moment finalize completes. Location-scoped lookup (findDectNetworkInLocation) handles both the finalize idempotency check and the /provisionDect fallback for freshly-created empty networks. Non-critical: a store can still go live if the DECT step fails, and /provisionDect keeps a recovery "create network" prompt for legacy stores. Pure-logic helpers (generateDectAccessCode, MAC normalize/format/ display, dectNetworkName) are unit-tested via node:test. Co-authored-by: Cursor <cursoragent@cursor.com>
221 lines
9.6 KiB
Markdown
221 lines
9.6 KiB
Markdown
# 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)](#remote-agent-siw--google) below).
|
|
|
|
## Local setup
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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/`](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):
|
|
|
|
```bash
|
|
# 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:
|
|
|
|
```bash
|
|
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`](docker/remote-agent/README.md) for build
|
|
details and [`docker/remote-agent/deploy/README.md`](docker/remote-agent/deploy/README.md)
|
|
for the full operator guide (upgrades, troubleshooting, coexistence with
|
|
`sha-remote-agent`).
|
|
|
|
## Bot commands
|
|
|
|
Registered in [src/commands](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.
|
|
- `/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](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:
|
|
|
|
```mermaid
|
|
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](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.
|