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).
126 lines
4.5 KiB
Markdown
126 lines
4.5 KiB
Markdown
# 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/8` on 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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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
|
|
|
|
```bash
|
|
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:**
|
|
```json
|
|
{ "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):**
|
|
```json
|
|
{ "id": "cmd_<uuid>", "type": "collect", "baseIp": "10.4.11.87" }
|
|
{ "id": "cmd_<uuid>", "type": "reboot", "baseIp": "10.4.11.87" }
|
|
```
|
|
|
|
**Agent → Bot (reply):**
|
|
```json
|
|
{ "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):**
|
|
```json
|
|
{ "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:audit` style 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 explicit `trigger*` helpers, never via a generic path fetcher).
|
|
|
|
## Deploying as a container
|
|
|
|
A `Dockerfile` isn't included yet — production deployment shape is TBD. Minimum viable:
|
|
|
|
```Dockerfile
|
|
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.
|