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).
264 lines
13 KiB
Text
264 lines
13 KiB
Text
# =============================================================================
|
|
# CollabFinder / CollabSupport Environment Variables
|
|
# Copy this file to .env and fill in your values.
|
|
# =============================================================================
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Server
|
|
# -----------------------------------------------------------------------------
|
|
SERVER_PORT=1800
|
|
|
|
# Logging level: info (default - clean), debug (verbose, includes per-fetch details)
|
|
LOG_LEVEL=info
|
|
|
|
# Verbose Webex framework debug logs. Default off; auto-enabled when LOG_LEVEL=debug.
|
|
# WEBEX_FRAMEWORK_DEBUG=false
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# HTTP API authentication
|
|
# -----------------------------------------------------------------------------
|
|
# Shared secret required to call destructive /:command HTTP endpoints such as
|
|
# /offboarduser, /provision-dect, /provision-vc, /vcmonitor, /bulkavstatuscsv,
|
|
# /bulkavswitchcsv, /devicesbymodel. Without it, those endpoints fail-closed
|
|
# with HTTP 503 — set this to any high-entropy string (e.g. `openssl rand -hex 32`).
|
|
# Callers send the token as `Authorization: Bearer <token>` or `X-API-Token: <token>`.
|
|
HTTP_API_TOKEN=
|
|
|
|
# Optional. When set to "true" / "1" / "yes", the same token is also required
|
|
# for read-only endpoints (/avstatus, /phonestatus, /av/devices/build/…,
|
|
# /phone/devices/build/…, /api/av/*, etc.). Default = false (those endpoints
|
|
# stay open so the bundled dashboards keep working without auth headers).
|
|
HTTP_API_REQUIRE_AUTH=false
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Webex Bot (required)
|
|
# -----------------------------------------------------------------------------
|
|
# Bot token from developer.webex.com. The framework connects to Webex over
|
|
# websockets using this token, so no public ingress / webhook URL is needed.
|
|
WEBEX_BOT_TOKEN=your-bot-token-here
|
|
|
|
# Service App credentials (used by WebexServiceAppAuth for backend Webex API
|
|
# calls — people lookup, room operations, etc.). Separate from the bot token.
|
|
#
|
|
# Required scopes (set when creating the service app at developer.webex.com):
|
|
# - spark-admin:people_read (people lookup)
|
|
# - identity:tokens_read (offboarduser: list a user's authorizations)
|
|
# - identity:tokens_write (offboarduser: revoke a user's authorizations)
|
|
# The authorizing admin must also hold Full / User / Device Admin role for the
|
|
# token-management calls to succeed.
|
|
WEBEX_CLIENT_ID=your-service-app-client-id
|
|
WEBEX_CLIENT_SECRET=your-service-app-client-secret
|
|
|
|
# Path to the rotating service app tokens file (must be writable).
|
|
# IMPORTANT: Use a *relative* path (e.g. ./config/...). The same .env works for both:
|
|
# - Local runs (resolved against your project root cwd)
|
|
# - Docker (resolved against /app inside container; see docker-compose volume mount)
|
|
# Do NOT use an absolute host path here — it will break inside the container.
|
|
WEBEX_TOKENS_PATH=./config/webex-service-tokens.json
|
|
|
|
# Optional override for the Webex API base URL (default https://webexapis.com/v1).
|
|
# WEBEX_BASE_URL=https://webexapis.com/v1
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# /webexhost — Webex Meetings host license helper
|
|
# -----------------------------------------------------------------------------
|
|
# Site to evaluate host status against. Default: aeo2go.webex.com.
|
|
# WEBEX_HOST_SITE_URL=aeo2go.webex.com
|
|
|
|
# License ID auto-assigned by `/webexhost <email>` when the user is missing a
|
|
# host license on the site. Discover the right id by running `/webexhost list`
|
|
# (lists every meeting license on the site with id + remaining seats).
|
|
# Until this is set, `/webexhost <email>` will still report status, but the
|
|
# confirm-assign step refuses with a clear message pointing at /webexhost list.
|
|
WEBEX_HOST_LICENSE_ID=
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Jira (required for /jira* commands)
|
|
# Use JIRA_CLOUD_ID for service accounts / new Atlassian API gateway endpoints:
|
|
# JIRA_CLOUD_ID=<uuid-from-atlassian>
|
|
# (constructs https://api.atlassian.com/ex/jira/<id>/rest/api/3 ...)
|
|
# Otherwise fall back to classic site base:
|
|
# JIRA_BASE_URL=https://your-org.atlassian.net
|
|
# -----------------------------------------------------------------------------
|
|
JIRA_CLOUD_ID=
|
|
JIRA_BASE_URL=https://your-org.atlassian.net
|
|
JIRA_EMAIL=your-email@company.com
|
|
JIRA_API_TOKEN=your-jira-api-token
|
|
JIRA_MAX_RESULTS=30
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Jira Poller (hourly ticket enrichment)
|
|
# -----------------------------------------------------------------------------
|
|
# Cron poller that scans unassigned tickets in the AV / Comm Services /
|
|
# Mobility queue every hour, posts a phone or AV status snapshot as a
|
|
# Jira comment on each store-scoped ticket, labels the ticket
|
|
# `bot-enriched` so it's not re-processed, and posts a summary of newly
|
|
# enriched tickets to a Webex space. Tickets the AI classifier decides
|
|
# are out-of-scope get labeled `bot-skipped` (same anti-retry purpose,
|
|
# no comment posted) so we don't burn AI tokens re-reading them every
|
|
# hour.
|
|
#
|
|
# Required scopes on JIRA_API_TOKEN: read + write on issues in the
|
|
# target projects (comment + edit-labels). The token owner needs "Add
|
|
# Comments" and "Edit Issues" permission — a plain read-only integration
|
|
# token WILL NOT work.
|
|
#
|
|
# JIRA_POLLER_ROOM_ID
|
|
# Webex space roomId to post the per-poll "N new tickets" summary to.
|
|
# Poller stays DISABLED (cron never registered) if unset — safe default
|
|
# for dev instances that share the same Jira credentials.
|
|
#
|
|
# JIRA_POLLER_PRIME_ON_START
|
|
# One-shot backlog-prime toggle. Set to `true` for a SINGLE deploy to
|
|
# bulk-label every ticket currently matching the poller's JQL as
|
|
# `bot-enriched` WITHOUT enriching them or posting a summary. Prevents
|
|
# day-one spam from a queue that already has dozens of open tickets.
|
|
# Flip back to `false` (or remove) before the next restart or the
|
|
# prime pass runs again.
|
|
#
|
|
# JIRA_STORE_FIELD_ID
|
|
# Optional. Numeric custom-field id for the `Store Number` field
|
|
# (e.g. `customfield_10042`). If unset, the poller discovers it at
|
|
# first use via GET /rest/api/3/field. Set explicitly to skip
|
|
# discovery (saves one API call at startup) or when the display
|
|
# name resolves ambiguously in your Jira schema.
|
|
#
|
|
# Note: on tenants where Store Number is an Atlassian Assets object
|
|
# reference (not a plain string), the field value the poller reads
|
|
# will be an opaque object like {"objectId":"81255"}. The AI
|
|
# classifier handles this by extracting the store number from the
|
|
# ticket summary / description text instead ("Store 3860 - ..."), so
|
|
# the field being unreadable is not fatal.
|
|
#
|
|
# JIRA_POLLER_MODEL
|
|
# Optional model override for the AI ticket classifier. Defaults to
|
|
# XAI_MODEL if unset. Classification is a small, deterministic
|
|
# structured task (~50-token JSON output per ticket) that doesn't
|
|
# need the reasoning depth of the summary model — a cheaper/faster
|
|
# model (e.g. `grok-3-mini`) saves noticeable money at scale without
|
|
# hurting classification accuracy on the phone/av/skip taxonomy.
|
|
# -----------------------------------------------------------------------------
|
|
JIRA_POLLER_ROOM_ID=
|
|
JIRA_POLLER_PRIME_ON_START=false
|
|
JIRA_STORE_FIELD_ID=
|
|
JIRA_POLLER_MODEL=
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# xAI / Grok (used for ticket and work order summarization)
|
|
# -----------------------------------------------------------------------------
|
|
XAI_URL=https://api.x.ai/v1/chat/completions
|
|
XAI_API_KEY=your-xai-api-key
|
|
XAI_MODEL=grok-2-latest
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Meraki
|
|
# -----------------------------------------------------------------------------
|
|
MERAKI_API_KEY=your-meraki-api-key
|
|
MERAKI_ORG_ID=your-org-id
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# RED (Digital Signage)
|
|
# Comma-separated list of company IDs (one per company tenant).
|
|
# -----------------------------------------------------------------------------
|
|
RED_BASE_URL=https://api.red.com
|
|
RED_CLIENT_ID=your-red-client-id
|
|
RED_API_KEY=your-red-api-key
|
|
RED_COMPANY_IDS=company-id-1,company-id-2,company-id-3
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# OptiSigns
|
|
# -----------------------------------------------------------------------------
|
|
OPTISIGN_API_KEY=your-optisigns-api-key
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# DigiCert (for VC provisioning)
|
|
# -----------------------------------------------------------------------------
|
|
DIGICERT_API_KEY=your-digicert-key
|
|
DIGICERT_BASE_URL=https://one.digicert.com
|
|
DIGICERT_PROFILE_ID=your-profile-id
|
|
DIGICERT_SEAT_EMAIL=your-email@company.com
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# MDM / Workspace ONE (two instances)
|
|
# -----------------------------------------------------------------------------
|
|
# Standard / Store MDM
|
|
WS1_CLIENT_ID=...
|
|
WS1_CLIENT_SECRET=...
|
|
WS1_TENANT_CODE=...
|
|
|
|
# CORP MDM (used for offboarding / enterprise wipes)
|
|
CORP_WS1_API_BASE=https://...
|
|
CORP_WS1_CLIENT_ID=...
|
|
CORP_WS1_CLIENT_SECRET=...
|
|
CORP_WS1_TENANT_CODE=...
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Atlas
|
|
# -----------------------------------------------------------------------------
|
|
ATLAS_AUTH_KEY=your-atlas-key
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# ServiceChannel
|
|
# OAuth password grant against ServiceChannel's identity endpoint.
|
|
# -----------------------------------------------------------------------------
|
|
SC_BASE_URL=https://api.servicechannel.com/v3
|
|
SC_OAUTH_URL=https://login.servicechannel.com/oauth/token
|
|
SC_CLIENT_ID=your-sc-client-id
|
|
SC_CLIENT_SECRET=your-sc-client-secret
|
|
SC_USERNAME=your-sc-username@company.com
|
|
SC_PASSWORD=your-sc-password
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# VC Provisioning "backdoor" account
|
|
# Used by vcProvisionService for local-device authentication during certificate
|
|
# enrollment. DIGICERT_SEAT_EMAIL above is unrelated.
|
|
# -----------------------------------------------------------------------------
|
|
BACKDOOR_USERNAME=monitor
|
|
BACKDOOR_PASSWORD=...
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Cisco DBS-210 DECT — LOCAL DEV TEST HARNESS
|
|
# Used only by scripts/testDectBase.js when iterating locally against a
|
|
# lab base. NOT read by the bot at runtime — the bot never talks to a
|
|
# DBS-210 directly. Actual production DBS-210 access lives in the
|
|
# DECT_RELAY_* env below and is executed by dect-relay-agent/.
|
|
#
|
|
# Cisco's guidance: use the DECT serviceability password (Control Hub
|
|
# → Calling → Features → DECT Networks → Manage → Manage DECT
|
|
# serviceability password). Our tenant is configured to share a
|
|
# single password across all bases in the fleet.
|
|
# -----------------------------------------------------------------------------
|
|
DECT_TEST_BASE_IP=10.0.0.100
|
|
DECT_TEST_USER=admin
|
|
DECT_TEST_PASSWORD=your-dect-serviceability-password
|
|
# Optional: seconds to wait for base station responses. DBS-210 is slow
|
|
# on syslog/PRT downloads; 30s is a good starting point.
|
|
DECT_TEST_TIMEOUT_MS=30000
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# DECT Relay Hub — WSS endpoint for the data-center relay agent
|
|
# The bot process runs in the public cloud and can't reach 10.x. The
|
|
# relay agent (see dect-relay-agent/) runs INSIDE the DC, dials
|
|
# outbound over WSS to this bot, and executes any DECT command the bot
|
|
# pushes. Feature-gated: without DECT_RELAY_AGENT_TOKEN, the WSS
|
|
# endpoint is not attached and /phonestatus's DECT follow-up + the
|
|
# future /dectstatus command return "relay not connected".
|
|
#
|
|
# Rotate DECT_RELAY_AGENT_TOKEN on both sides at once. Suggested
|
|
# generation: `openssl rand -hex 32`.
|
|
# -----------------------------------------------------------------------------
|
|
DECT_RELAY_AGENT_TOKEN=
|
|
# Optional. Default: /dect-relay/ws. Change only if you also change
|
|
# DECT_RELAY_BOT_URL on the agent side to match.
|
|
# DECT_RELAY_PATH=/dect-relay/ws
|
|
# Optional. Per-base collect() RPC timeout. Corporate proxies can make
|
|
# DBS-210 reads slow; 15s is comfortable, 30s is generous.
|
|
# DECT_COLLECT_TIMEOUT_MS=15000
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Notes
|
|
# -----------------------------------------------------------------------------
|
|
# - config/config.json has been fully removed. All configuration is via env vars.
|
|
# - The rotating Webex service token lives in config/webex-service-tokens.json
|
|
# and is mounted separately when running in Docker.
|
|
# - Add any new integration keys above following the same pattern.
|