Skip to content

Repository files navigation

CTI Platform

Multi-tenant CTI middleware connecting Asterisk/FreePBX to CRMs — click-to-call, screen pops, automated call logging. Design rationale, event model, and roadmap: cti-architecture.md.

Documentation

Doc What's in it
docs/INSTALL.md Step-by-step installation: infra, env, PBX prep (lab + production FreePBX), onboarding, WebRTC, reverse connector, container deploy, go-live runbook
docs/HOW-TO-USE.md Task-oriented how-tos for every feature (curl/WS examples) + troubleshooting
docs/TESTING.md Manual test & validation scenarios, negative cases, and end-to-end user-flow use-cases
docs/FEATURES.md Feature catalog — how each works and its endpoints
/docs (Swagger UI) Live, interactive API reference with all four auth schemes — served by the running app
docs/postman/ Postman collection: every endpoint with request + example responses
docs/adr/ Architecture Decision Records 0001–0010 (AMI-over-ARI, correlation engine, multi-tenancy, reverse connector, deployment, WebRTC/CRM expansion, …)
docs/ROADMAP.md Roadmap with per-phase status; only Phase 11 (ARI) remains
cti-architecture.md Architecture explainer + sequence diagrams (with an as-built note)

Status

Built and validated through Phase 11 (all roadmap phases complete). What's implemented:

  • Core (P1–2): hand-rolled AMI connector, Linkedid call-state correlation → normalized call.* events, multi-tenant registry (shared-PBX routing, encrypted creds, scoped API keys), generic signed webhooks over durable BullMQ.
  • CRMs (P3–4, P10): Zoho PhoneBridge, Salesforce Open CTI, HubSpot, Microsoft Dynamics 365 — a tenant can enable several; logging fans out to each.
  • Productization (P5): reverse on-prem connector (no inbound firewall holes), recording proxy with signed URLs, agent presence, admin API + dashboard with hot-reload.
  • Hardening (P6–9): 60-test Jest suite + CI, TypeORM migrations, Redis-backed call-state + CoreShowChannels resync, per-tenant originate rate-limit, graceful shutdown, structured JSON logs, Prometheus /metrics, readiness/liveness probes, dead-letter alerting + retry UI, multi-stage Docker image + Caddy TLS/wss reverse proxy, recordings pulled over the reverse tunnel.
  • WebRTC softphone (P10): in-browser audio via self-hosted JsSIP (real two-way audio needs a WebRTC-configured PBX).
  • Advanced telephony (P11): ARI connector (Stasis → same normalized events), in-call coaching (spy/whisper/barge), queue/ACD wallboard, CRM-driven IVR routing.

Truly pending (operational, not code): real Zoho/Salesforce/HubSpot/Dynamics org credentials (see INSTALL §11); a WebRTC-enabled Asterisk for live browser media; and a Stasis/queue-configured PBX for live ARI coaching audio + queue wallboard stats.

Selected internals

  • Reverse connector — a PBX connection with mode: reverse is passive: the customer runs scripts/connector-agent.mjs (dependency-free, Node ≥ 21) with CTI_URL + CONNECTOR_TOKEN; it dials OUT to wss://cti/connector-ws and tunnels the local AMI socket (and, on a second channel, serves recording files). AMI credentials never leave the cloud registry — login happens server-side over the tunnel. Heartbeats (15s ping) reap dead tunnels.
  • Recordingscall.ended carries recordingUrl, a 15-minute signed capability URL served by GET /v1/recordings/:token (basename-only, traversal-proof). Direct connections read RECORDINGS_BASE_DIR; reverse connections pull the file from the on-prem agent over the tunnel — no shared mount needed.
  • Presenceagent.state (RINGING/INUSE/NOT_INUSE from call lifecycle, UNAVAILABLE from AMI DeviceStateChange) broadcast to the tenant's softphone sockets and queryable at GET /v1/agents/state.
  • Admin/admin dashboard (connections, tenants, active calls, queue health, dead-letter retry) over X-Admin-Key endpoints: GET /admin/overview, POST /admin/{pbx-connections,tenants,agents,integrations} (generated keys/tokens returned once), POST /admin/dead-letters/:queue/:jobId/retry, and POST /admin/reload (diff-restarts only changed connections).

Salesforce Open CTI adapter

Salesforce is client-side: we host the softphone page (public/softphone.html), Salesforce embeds it via the Call Center definition (public/callcenter-definition.xml, served at /softphone/callcenter-definition.xml).

  • Agent session: POST /v1/softphone/login {ext} (tenant API key) returns a short-lived HS256 token (SOFTPHONE_JWT_SECRET).
  • Live events: the page connects to ws(s)://host/softphone-ws?token=…; the gateway pushes only that agent's call.* events. On inbound ringing the page calls sforce.opencti.searchAndScreenPop (when embedded) — Salesforce matches the number and pops the record.
  • Click-to-dial: Open CTI's onClickToDial (or the page's dial pad) POSTs /v1/softphone/originate with the agent token → agent-leg-first originate.
  • Call logging: call.ended for Salesforce-enabled tenants flows through the durable salesforce-delivery queue; the processor exchanges the org's connected-app refresh token for an access token and creates a Task (TaskSubtype Call, duration, disposition) owned by the mapped user (Agent.crmRefs.salesforce).
  • Lab testing: node scripts/mock-salesforce.mjs 4200 mimics the OAuth + Task endpoints; the seed points tenant-b at it. In production, import the Call Center XML (Setup → Call Center), replace CTI_BASE_URL, and assign users.

Zoho PhoneBridge adapter

Per-tenant CrmIntegration rows (type zoho) hold the DC/base URLs/client id in config and the client secret, refresh token, and callback token encrypted in secretsEnc. Flow:

  • Events out: normalized call.* events for Zoho-enabled tenants are mirrored into a durable zoho-delivery queue; the processor exchanges the tenant's refresh token for a cached access token and POSTs RINGING (creates the call → Zoho pops the matched contact for the mapped user) then PUTs ANSWERED/ENDED (Zoho logs the activity). Agent ↔ Zoho user mapping lives in Agent.crmRefs.zoho.
  • Click-to-call in: Zoho's dial-icon callback POSTs to /v1/integrations/zoho/:tenantSlug/click-to-call (X-Zoho-Token = per-integration callback token); the Zoho user resolves to an agent extension and a normal agent-leg-first originate follows.
  • Lab testing: node scripts/mock-zoho.mjs 4100 mimics the token + PhoneBridge endpoints; the seed points tenant-a at it. ⚠️ Endpoint paths/payloads follow PhoneBridge v3 shape but must be reconciled with Zoho's partner docs once registration is approved — changes are confined to zoho-client.ts + the mock.

Run against the lab PBX

# 1. Lab Asterisk (AMI on 127.0.0.1:5038)
cd ../Multi-Tenant-Asterisk-PBX && docker compose up -d

# 2. CTI infra (Postgres :5433, Redis :6380)
docker compose up -d

# 3. Build, seed two lab tenants (prints their API keys ONCE), run
npm install && npm run build
npm run seed
npm start

# 4. Example per-tenant webhook consumers
WEBHOOK_SECRET=receiver-a-secret node scripts/webhook-receiver.mjs 4000
WEBHOOK_SECRET=receiver-b-secret node scripts/webhook-receiver.mjs 4001

The seed creates tenant-a (extensions 1XXX, contexts tenant-a-*) and tenant-b (2XXX, tenant-b-*) sharing the single lab Asterisk — the same shape as a hosted multi-tenant PBX. A production FreePBX is one more PbxConnection row with originateChannelTemplate = PJSIP/{ext}.

API

Endpoint Auth Purpose
GET /health none per-PBX-connection status
POST /v1/calls/originate {agentExt, number} X-API-Key (tenant) click-to-call; agent must belong to the key's tenant
GET /v1/calls X-API-Key (tenant) in-flight calls of that tenant
curl -s -X POST http://127.0.0.1:3000/v1/calls/originate \
  -H "X-API-Key: $TENANT_A_KEY" -H 'Content-Type: application/json' \
  -d '{"agentExt": "1001", "number": "1000"}'

Webhooks

Normalized events (call.ringing, call.answered, call.ended) are enqueued in BullMQ (4 attempts, exponential backoff; failed jobs remain in Redis as dead letters) and POSTed to each tenant's webhookUrl:

{ "id": "uuid", "type": "call.ended", "tenantId": "tenant-a", "occurredAt": "", "data": { } }

Headers: X-CTI-Timestamp (epoch ms) and X-CTI-Signature = hex HMAC-SHA256 of `${timestamp}.${rawBody}` with the tenant's webhook secret. Reject skew > 5 min; compare in constant time. Reference consumer: scripts/webhook-receiver.mjs.

Tests & container deployment

npm test                                        # 60 unit + integration tests, no live infra
docker compose -f docker-compose.full.yml up -d --build   # app + pg + redis + Caddy (TLS)
curl -k https://localhost:8443/health                     # HTTPS via Caddy

Observability: GET /metrics (Prometheus), GET /health/live + /health/ready (probes), structured JSON logs (LOG_FORMAT=json). See INSTALL §10–12 for production deployment, secrets/KMS, and the go-live checklist.

Earlier "deferred" items (TypeORM migrations, Redis-backed call state + resync, admin CRUD/hot-reload, recordings over the tunnel) are all implemented — see the Status section. Registry changes now apply via POST /admin/reload (no restart).

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages