Two bugs surfaced by the same failed finalize run for store 7311.
1. addPhoneNumbersToLocation was POSTing to /locations/{id}/numbers,
which Webex treats as "provision a NEW number" — that triggered
duplicate PSTN orders against numbers AE had already manually
ordered through Control Hub, and the provider rejected them with
ORDER_CREATION_FAILED. AE's workflow is manual ordering, so this
step now VERIFIES the number's current state in the org via
GET /telephony/config/numbers?phoneNumber=..., classifies the
outcome (already here / pending / wrong location / not in org),
and either succeeds silently or throws with a specific
Control-Hub fix-it instruction. The bot never triggers a PSTN
order now.
2. findDectNetworkInLocation was hitting
GET /telephony/config/locations/{id}/dectNetworks, which Webex
does not implement (returns HTTP 404 "No static resource ..."
unconditionally). That silently broke both the finalize
idempotency pre-check and the /provisionDect fallback, so
/provisionDect kept showing the "create network" card for stores
that already had one, then 409'd on the create attempt. Switched
to the real endpoint GET /telephony/config/dectNetworks with a
locationId filter. Also added translateCreateDectError to
rewrite the deeply-nested 27453 "access code in use" 409 blob
into an actionable "already exists, re-run /provisionDect"
message.
Extracted both error/state translators as pure exported functions
(classifyNumberAssignment, translateCreateDectError) with unit-test
coverage locking down the exact Webex payload shapes.
README: updated /finalizeStore to reflect the manual-order
expectation and specific fix-it instructions on mismatch.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
requestJson merged Content-Type: application/json into a headers object,
then spread `...extra` after `headers` in the fetch options — which let
extra.headers (always passed by webexJson for Authorization) overwrite
the merged object, stripping Content-Type entirely. Undici then
defaulted the string body's Content-Type to text/plain;charset=UTF-8,
and strict Webex endpoints like POST /locations rejected the request
with HTTP 415. Latent since the node-fetch -> native fetch migration.
Reorder to `{ method, ...extra, headers }` so the explicit merged
headers win, and add a test/http.test.js suite (7 tests) that pins the
merge behaviour down so this can't regress silently again.
Also: update the npm test script to `test/**/*.test.js` glob. Node
22.23 no longer accepts a bare `test/` directory as a positional arg
to `--test` (fails with ERR_UNSUPPORTED_DIR_IMPORT).
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
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>
- Delete /buildStore command + flow (dead since phone numbers now have
to be purchased before provisioning); drop the only cascading dead
helper (updateLocationRouteGroup) that was only called by that flow
- Rename /migrateStore -> /finalizeStore. "Migrate" was a leftover from
the legacy-system era; the command finishes what stage started
- Rename buildStoreInfoCard -> storeConfirmationCard (misleading name
since all flows share it); rename btnBuildStore/btnBadInfo ->
btnConfirm/btnCancel to match
- Fix the double-write of storeInfo.extension: siw.js sets a 5XXXX
default and both command handlers were overriding it (stage to 8XXXX,
finalize back to 5XXXX). Drop both overrides; single source of truth
- Add /finalizeStore preflight that runs findWebexLocation in parallel
with findWebexUser; if the location doesn't exist, bail before showing
the confirmation card with an actionable "Run /stageStore first" line
- Add post-run next-step summaries: stage points at
/finalizeStore <n>, finalize confirms the location is live
- Update README (commands, mermaid) and deviceCustomizations header
Co-authored-by: Cursor <cursoragent@cursor.com>
Pure git-mv rename step. Preserves file history so subsequent content
edits show up as modifications rather than delete+add. Follow-up commit
carries the actual content changes.
Co-authored-by: Cursor <cursoragent@cursor.com>
- 30-test suite (node --test, no framework) covering the exact functions
that have regressed in previous rounds:
* parseStoreArg / parseStoreNumber / storeEmail (helpers)
* greetingForBrand (brand fallback)
* formatE911Address / formatSuite (google response reducer)
* parseNextLink (RFC-5988 pagination — now exported)
* runStep success / non-critical / critical / no-bot paths
- test/setup.js stubs required env vars so any src/ module can be
imported cleanly; loaded via --import once per test process
- npm run test wired up; directory form works on Node 20 + 22
- .gitea/workflows/ci.yml runs lint + format:check + test on push/PR
to main, on Node 22 to match the runtime image
- Exclude test/ and .gitea/ from the Docker build context
- Exclude docker/ from bot Prettier scope (remote-agent is its own
sub-project with its own tooling)
Co-authored-by: Cursor <cursoragent@cursor.com>
- Route Twilio Lookup v2 through the on-prem remote agent so all
third-party calls (SIW, Google, Twilio) share the same network path
and future IP allow-lists / corp proxies don't break it silently
- Delete unused GOOGLE_APPLICATION_CREDENTIALS / serviceAccountKeyPath
wiring in config.js, .env.example, and README (never read by any code)
- Extract 150 lines of STORE_DEVICE_CUSTOMIZATIONS out of
src/webex/devices.js into src/webex/deviceCustomizations.js so device
config diffs are self-contained and devices.js stays focused on API
- runStep learns a {critical: true} option that re-throws on failure
instead of the legacy always-swallow behavior; mark
enableLocationForCalling critical in buildStore + stageStore so a
failure there aborts the flow cleanly rather than cascading into
dozens of downstream 404s
Co-authored-by: Cursor <cursoragent@cursor.com>
- Agent hello handshake: agent (v1.2.0) sends {type, version, capabilities}
on connect; bot logs "Agent hello: v1.2.0 (capabilities: insecure, hello)"
and exposes getAgentInfo(). Backward-compatible with older agents.
- proxyRequest tracks method + url per request; all error paths (agent
errors, timeouts, disconnect rejects, send failures) now include
"(for METHOD URL)" so the failing endpoint is unambiguous
- Add requireAgent(bot) preflight; buildStore/stageStore/migrateStore
reject up front when the agent is disconnected instead of failing
mid-flow after partial Webex mutations
- Add parseStoreNumber (^\d{1,5}$) and wire into store-number commands
with proper usage messages; add loose email-shape check to /userInfo
- Fix pre-existing catch(_e) lint warning in remoteAgent.js (bare catch)
Co-authored-by: Cursor <cursoragent@cursor.com>
- validateAddress and getTimeZone now use proxyRequest instead of native
fetch so the request originates from the on-prem IP, satisfying the
API_KEY_IP_ADDRESS_BLOCKED restriction on the Google key
- Both calls carry insecure:true because the agent's network path runs
SSL-inspecting proxies that substitute an internal-CA chain (throws
SELF_SIGNED_CERT_IN_CHAIN otherwise); scoped per-request, consistent
with SIW
- Fix latent bug in getTimeZone: use GET (per Google docs) instead of POST
- Rename README section to "Remote agent (SIW + Google)" and document
the SSL-inspection nuance
Co-authored-by: Cursor <cursoragent@cursor.com>
- 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>