The bot runs in the public cloud and can't reach the 10.x/8 network
where DBS-210 bases live. This phase adds a data-center-resident relay
agent that dials outbound over WSS to the bot, and lets /phonestatus
post a follow-up message with per-base health after its main output
has already shipped.
Bot side (services/):
- dectRelayHub.js: WebSocket upgrade handler on /dect-relay/ws with
bearer-token auth (constant-time compare, header + Sec-WebSocket-
Protocol fallback for header-stripping proxies). Promise-based RPC
API with per-call timeouts, mid-flight-disconnect rejection, and
clean replacement of a stale agent socket when a newer one connects.
- dectDiscovery.js: pure filter that turns a phoneService result into
a list of reachable bases. Enforces the "must be on 10.0.0.0/8"
guardrail per requirements, dedups by IP + MAC, prefers Meraki-live
IP over Webex-cached IP.
- dectCollectorService.js: fan-out layer over the hub. collectAll()
runs one RPC per base in parallel with per-base error isolation —
one bad base never fails the batch.
Phone-status integration:
- Renderer gets a dectFollowUpBaseCount opt that emits an italic
"diagnostics loading for N base(s)..." hint inside the DECT section
of the main message.
- New exported renderDectDiagnosticsMarkdown() renders the follow-up
message: healthy/warning icon per base, uptime + firmware summary,
structured Power Loss reboot line, and per-base failure hints (e.g.
"relay accepted the request but the base did not respond in time").
- commands/phoneStatus.js discovers reachable bases synchronously
(pure), sends the main message, then fires collectAll() and posts
the follow-up as a separate message. Failures logged, never thrown
back to the user.
- Chat only: HTTP callers keep their single-message contract.
Agent side (dect-relay-agent/):
- Standalone Node process with its own package.json (only ws, axios,
dotenv). Reuses the shared integrations/cisco-dect/{client,probes,
statusXml}.js modules from the parent workspace so there's no code
duplication.
- Auto-reconnect with exponential backoff + jitter.
- Dispatches collect / reboot / force-reboot / reboot-chain /
force-reboot-chain / factory-reset / reconfigure-tree.
- DECT admin credentials live ONLY on the agent (never on the bot).
Shared bearer token gates the WSS handshake.
- README.md covers install, config, wire protocol, and safety model.
Env / infra:
- .env.example: adds DECT_RELAY_AGENT_TOKEN + optional DECT_RELAY_PATH
and DECT_COLLECT_TIMEOUT_MS. Reframes DECT_TEST_* as the local-dev
test harness rather than the production path.
- index.js: captures the http.Server from app.listen() and attaches
the relay hub when DECT_RELAY_AGENT_TOKEN is set; graceful shutdown
now closes the hub so in-flight RPCs get rejected cleanly.
- Adds "ws" to bot dependencies.
Tests (99 -> 113):
- tests/dectDiscovery.test.js: 13 cases covering the 10.x guardrail,
MAC normalization, IP source preference, dedup, and warning shape.
- tests/dectRelayHub.test.js: 14 integration cases using a real
ws pair on an ephemeral 127.0.0.1 port — auth (missing / wrong /
correct via header / correct via protocol fallback), hello frame,
RPC round-trip with correlation, agent error surfacing, concurrent
out-of-order replies, timeout, mid-flight disconnect, replacement
of a stale socket, and execAction routing.
- tests/renderers.test.js: 8 new cases for the DECT-follow-up loading
hint (plural / singular / off) and the diagnostics renderer (empty,
healthy, warning, power-loss dedup, active RTP, error hint, footer).
|
||
|---|---|---|
| .. | ||
| .env.example | ||
| index.js | ||
| package.json | ||
| README.md | ||
DECT Relay Agent
Bridges the CollabSupport bot (public cloud) to Cisco DBS-210 DECT base stations on the private 10.0.0.0/8 corporate network.
Why it exists
The bot process runs in the public cloud and can't reach 10.x. This agent runs inside the data center, dials outbound over WSS to the bot, and executes any DECT command (collect status, reboot, factory-reset, etc.) the bot pushes to it.
Only one agent is expected to run at a time. If a second agent connects, the bot assumes it's a legitimate restart, closes the old socket, and adopts the new one.
Prerequisites
- Node.js ≥ 20
- Route from the agent host to
10.0.0.0/8on TCP 443 - Route from the agent host to the bot's public HTTPS endpoint
- The DECT serviceability password (Control Hub → Calling → Features → DECT Networks → Manage → Manage DECT serviceability password)
Install
cd dect-relay-agent
npm install
ws, axios, and dotenv are the only runtime dependencies. The agent imports the shared integrations/cisco-dect/ modules from the parent repo via relative paths, so the parent workspace must be present on disk.
Configure
cp .env.example .env
$EDITOR .env
Required values:
| Var | Meaning |
|---|---|
DECT_RELAY_BOT_URL |
Full WSS URL to the bot's DECT relay endpoint (wss://your-bot-host/dect-relay/ws) |
DECT_RELAY_AGENT_TOKEN |
Shared bearer token — MUST match the bot's DECT_RELAY_AGENT_TOKEN exactly |
DECT_ADMIN_USER |
Usually admin |
DECT_ADMIN_PASSWORD |
Fleet-wide serviceability password |
Generate a fresh token: openssl rand -hex 32. Rotate on both sides at once — the bot compares tokens with timingSafeEqual and will reject any drift with a 401 on the WSS upgrade.
Run
npm start
You should see:
[startup] dect-relay-agent v0.1.0 — hostname=..., bot=wss://...
[connect] Dialing wss://.../dect-relay/ws
[connect] Connected — sending hello
And on the bot side:
[dect:relay-hub] Agent connected from ...
[dect:relay-hub] Agent hello: version=0.1.0 host=... caps=collect,reboot,...
Wire protocol
All frames are JSON, one per WebSocket message.
Agent → Bot on connect:
{ "type": "hello",
"agentVersion": "0.1.0",
"hostname": "dc-dect-relay-01",
"capabilities": ["collect","reboot","force-reboot","reboot-chain",
"force-reboot-chain","factory-reset","reconfigure-tree"] }
Bot → Agent (command):
{ "id": "cmd_<uuid>", "type": "collect", "baseIp": "10.4.11.87" }
{ "id": "cmd_<uuid>", "type": "reboot", "baseIp": "10.4.11.87" }
Agent → Bot (reply):
{ "id": "cmd_<uuid>", "ok": true, "elapsedMs": 812,
"result": { "parsed": { ... }, "verdict": { "healthy": true, ... } } }
{ "id": "cmd_<uuid>", "ok": false,
"error": { "code": "DIGEST_401", "message": "Base rejected credentials" } }
Heartbeat (both directions, every 30s):
{ "type": "ping", "at": 1720000000000 }
{ "type": "pong", "at": 1720000000000 }
The bot terminates the socket if no pong arrives within 90s; the agent auto-reconnects with exponential backoff (1s / 2s / 4s / … capped at 30s + 0-1000ms jitter).
Safety guarantees
- DECT admin credentials NEVER leave this agent. The bot only knows the WSS bearer token.
- All mutating actions (reboot, factory-reset, reconfigure-tree) are only executed when the bot explicitly issues the corresponding command frame. The agent has no autonomous logic.
- The agent enforces no policy — the bot decides who can reboot what. See the bot's audit log for the full record of actions taken (
igmp:auditstyle scopes in daily log files). - The agent quarantines mutating actions from probes via the exact same safety model as the CLI tool (
integrations/cisco-dect/probes.js— GET-triggered actions are only reachable via explicittrigger*helpers, never via a generic path fetcher).
Deploying as a container
A Dockerfile isn't included yet — production deployment shape is TBD. Minimum viable:
FROM node:20-alpine
WORKDIR /app
# The agent imports from ../integrations/cisco-dect/, so copy the
# whole workspace (or at least these two paths).
COPY package.json package-lock.json ./
COPY dect-relay-agent ./dect-relay-agent
COPY integrations/cisco-dect ./integrations/cisco-dect
COPY utils/httpDigestAuth.js ./utils/httpDigestAuth.js
RUN cd dect-relay-agent && npm ci --omit=dev
CMD ["node", "dect-relay-agent/index.js"]
Set the env vars from .env.example via your orchestrator's secret store, not baked into the image.