Auto-detect CSV format from the header:
• meetings-inactive: EMAIL / IS_HOST / DAYS_SINCE_LAST_ACTIVE
(Analyzer → Meetings → Inactive Users)
• users-export: "User ID/Email (Required)" /
"Days since Last Service Accessed"
(Users → Manage users → Export)
The users-export report has no host flag, but we don't need one —
the authoritative host-holder set comes from the live assignee
roster fetched from the Webex API. Format-B rows with blank
"Days since Last Service Accessed" (never-signed-in accounts,
often generic mailroom/store logins) are intentionally skipped so
they aren't silently reclaimed.
Also stopped upper-casing the header so we can preserve the
punctuation-rich column names Users Export uses verbatim.
Reads a Control Hub "Meetings Inactive Users" CSV, filters to
IS_HOST=Y AND DAYS_SINCE_LAST_ACTIVE > --min-days (default 120),
cross-references against the current holders of --host-license-id
(so no per-user /people lookup), then PATCHes /v1/licenses/users to
atomically remove the host license and either (a) add a specific
free-tier license (--free-license-id) or (b) add attendee-only
siteUrl on --site (--free-attendee).
Dry-run by default; enumerates every license on the site so the
operator can pick the free tier. Bounded concurrency with 429/503
retry, optional --offset/--limit for staged rollouts, per-user
outcome CSV via --report, and full audit trail via the existing
webex:reclaim:audit log scope.
Whitelisted in .gitignore so it stays version-controlled alongside
the other tracked operational scripts.
The previous packager (scripts/packageDectRelayAgent.js) shipped a
source-only bundle and expected the DC host to build the image with
`docker compose up --build`. That fails hard in corporate DCs with
TLS-intercepted egress: Alpine's apk fetch of dl-cdn.alpinelinux.org
can't verify the intercepted certificate ("apk: TLS: server
certificate not trusted"), and npm install would fail the same way
if apk had succeeded.
New approach: build the image ONCE on the dev machine (where TLS
works), save it as a gzipped tarball, and ship a ZIP whose install
step is `docker load` + `docker compose up -d`. Zero network calls
inside the DC container, ever.
Bundling (dev-machine):
- dect-relay-agent/bundle.sh: build → docker save → gzip → zip.
Auto-derives version from package.json, records git sha + dirty
flag + build date into image labels. Cross-arch friendly
(--platform=linux/amd64 by default; --platform linux/arm64 for
ARM DCs). Output: dect-relay-agent-bundle-<YYYYMMDD-HHMMSS>.zip
at repo root (typically 40-60MB).
- dect-relay-agent/Dockerfile: multi-stage node:20-alpine build.
No apk add. No runtime npm install. Non-root `node` user (uid
1000). Node handles SIGTERM natively via index.js handlers, so
no tini/dumb-init needed. Designed to build from the REPO ROOT
(not the agent folder) because the agent imports shared modules
from ../integrations/cisco-dect and ../utils.
- dect-relay-agent/Dockerfile.dockerignore: per-Dockerfile ignore
(BuildKit ≥ 23.0) with a whitelist that keeps the build context
to ~50KB. Older Docker daemons fall through to the repo-root
.dockerignore, which already excludes secrets — nothing sensitive
can leak either way.
- package.json: `npm run package:relay` now invokes bundle.sh.
Runtime (DC-host):
- dect-relay-agent/docker-compose.yml: pins IMAGE_TAG from .env
(install.sh writes it there — never falls back to :latest), reads
the rest of the config via env_file, restart: unless-stopped,
host networking (needed to reach 10.x/8 without userland proxy
translation, and the agent doesn't listen on anything). Hardened:
read_only: true rootfs with a 16MB /tmp tmpfs, cap_drop: ALL,
no-new-privileges, log rotation at 10MB × 5 files.
- dect-relay-agent/install.sh: preflight (docker + compose present,
daemon reachable, bundle files intact), docker load, pin loaded
tag into .env, validate .env has the three required values not
still set to placeholder strings, docker compose up -d, tail last
40 log lines. Idempotent — safe to re-run on upgrades.
Cleanup:
- scripts/packageDectRelayAgent.js: deleted (superseded).
- .gitignore: drops the scripts/* + !packageDectRelayAgent.js dance
since we no longer need to whitelist that one file; add pattern
for the datestamped bundle zips + staging dirs at repo root.
- dect-relay-agent/README.md: replaces the deploy section with the
new dev-machine-build → DC-host-load workflow, plus a
troubleshooting section keyed on the exact error messages seen
during the failed in-DC build (TLS cert not trusted, docker perm
denied, DIGEST_401).
Verified: all 113 existing tests still pass. Docker build itself
requires a Docker daemon (dev machine) so can't be exercised in
this sandbox — the bash scripts pass `bash -n` syntax checks.
Adds a one-command packager (`npm run package:relay`) that produces a
self-contained zip ready to transfer into the data center and start
with `docker compose up -d --build`. Three commands on the DC host:
unzip, edit .env, docker compose up.
Why a packager instead of `docker build` in the repo:
The agent's index.js imports the shared cisco-dect + httpDigestAuth
modules via `../integrations/...` paths, so a naive
`docker build dect-relay-agent/` would fail because those files live
outside the build context. The packager copies them into a
`workspace/` tree inside the bundle so the Dockerfile sees them as
local paths without any source rewriting.
Docker artifacts (in dect-relay-agent/):
- Dockerfile: multi-stage node:20-alpine build (~55MB final image),
non-root `dect` user (UID/GID 1500), tini as PID 1 for clean
SIGTERM propagation to node's graceful-shutdown path,
`npm install --omit=dev --ignore-scripts` in the deps stage.
- docker-compose.yml: restart:unless-stopped, JSON log rotation
(10MB × 5 files), pgrep-based health check. No `ports:` block
because the agent is outbound-only (dials the bot).
- .dockerignore: defensive — the bundle already excludes cruft, but
this hardens against a stray manual build.
Packager (scripts/packageDectRelayAgent.js):
- Assembles agent code + shared modules + deploy artifacts into a
timestamped staging dir (.package-relay-tmp/, git-ignored).
- Generates a bundle README with three-command deploy instructions,
ongoing-ops table, no-internet-DC fallback (docker save/load), and
troubleshooting for the most common failure modes.
- Generates BUNDLE_INFO.txt with build metadata (git sha + dirty
flag + timestamp + size) so the DC operator can trace deployed
bundles back to source.
- Emits `dist/dect-relay-agent-bundle-<YYYYMMDD-HHMMSS>.zip` (30KB).
- Cleans staging in a finally block so failed runs don't leak.
Bundle layout (matches Dockerfile expectations):
dect-relay-agent-bundle-<version>/
Dockerfile, docker-compose.yml, .dockerignore
.env.example, README.md, BUNDLE_INFO.txt
workspace/dect-relay-agent/{package.json, index.js}
workspace/integrations/cisco-dect/{client,probes,statusXml}.js
workspace/utils/httpDigestAuth.js
Wiring:
- package.json: new `package:relay` and `test` npm scripts.
- .gitignore: `scripts/` changed to `scripts/*` so `!scripts/
packageDectRelayAgent.js` can re-include just the packager
(git forbids re-including files under a fully-excluded directory,
hence the glob form).
- dect-relay-agent/README.md: rewrites deployment section to show
the Docker path as the recommended production route, with the
node-directly path kept for local dev.
Verified end-to-end: `npm run package:relay` produces a valid zip
that unpacks to the expected layout in <2s. All 113 existing tests
still pass.