Some checks failed
CI / verify (push) Has been cancelled
New /provisionPhone <storeNumber> command manages the store user's wired desk phones via the same card pattern as /provisionDect: multi-select checkbox list that doubles as the display, MACs render as AA:BB:CC:DD:EE:FF, model dropdown for add (defaults to 7841), side-by-side Add / Remove-checked actions, and every removal goes through an explicit confirm card. Adds are idempotent (MACs already registered are skipped) and bulk MAC input is supported. DECT handsets are filtered out of the display so /provisionPhone and /provisionDect coexist cleanly on the same store user without overlapping responsibilities. Refactors: - Extract MAC helpers (normalize/format/display) to src/utils/mac.js so both DECT and wired-phone flows share one implementation. dect.js re-exports for backward compat with existing consumers. - Add parseEmailArg helper; migrate /userInfo to use it. Tests: pure-logic coverage for WIRED_PHONE_MODELS, isSupportedWiredModel, filterWiredPhones (DECT/model filter), and parseEmailArg. Co-authored-by: Cursor <cursoragent@cursor.com>
229 lines
10 KiB
Markdown
229 lines
10 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.
|
|
- `/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](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.
|