Your CRM's click-to-call fails on most mobile numbers, so your reps dial from their phones and nothing gets logged. This intercepts that button, places the call through Microsoft Teams or your PBX, and lands a complete activity on the contact — the rep's notes, the call duration, the recording, an AI transcript and summary — without the rep typing anything.
Built and running at eTop Technology, a managed service provider. Published as a recipe with working code, not a product: there is no installer, no support, and you will have to adapt the CRM-specific parts. Read the recipe first — it explains the eight steps, the reasoning behind the non-obvious ones, and the traps that cost us real time.
Browser extension Edge worker Outbound only
───────────────── ─────────── ─────────────
content script ──────► validate Entra token ──────► CRM partner API
intercepts click write CRM activity phone system (CDR,
reads the number queue enrichment recording, origination)
resolves contact id speech-to-text + summary
service worker
holds the token
queues failed dials
notes panel
No inbound webhooks. Three authenticated routes. Vendor credentials live only in worker secrets.
| Path | What it is |
|---|---|
apps/extension/ |
MV3 extension — interception, auth, notes panel |
apps/worker/src/ |
Cloudflare Worker — token validation, CRM writes, enrichment |
apps/worker/migrations/ |
D1 schema |
docs/ARCHITECTURE.md |
How the pieces fit and why |
docs/SECURITY.md |
Auth layers, attack surface, request hardening |
docs/ADMIN_SETUP.md |
Entra app, secrets, provisioning a rep |
RECIPE.md |
The build guide — start here |
GlassHive (CRM) · Microsoft Entra ID · Microsoft Teams · NetSapiens/OIT (PBX) · Cloudflare Workers, D1, Queues, Workers AI.
Steps 1–3 of the recipe are the only CRM-specific parts. The phone system is optional — without one you use a Teams deep link and accept one confirmation prompt.
npm install
npm run check # static checks + tests, no networkEverything is configured through placeholders. scripts/check.mjs will tell you what
is still unset; the CI workflow skips the deploy dry-run until they are filled in.
apps/extension/config.js— tenant id, client id, your service URLapps/worker/wrangler.toml— the same ids, your D1 database id, your routewrangler secret put—GLASSHIVE_API_KEY,OIT_API_KEYpwsh scripts/provision-entra.ps1— creates the app registration, group, and consent
Then npm run migrate and npm run deploy. See docs/ADMIN_SETUP.md for the long form.
Before you ship: generate your own extension signing key and pin the id. The Entra
redirect URI contains the extension id, so an unpinned id changes on every install and
sign-in breaks. scripts/package-extension.ps1 builds a signed CRX and update manifest
once you have one.
Bring-up probe scripts, our rollout and rep-onboarding docs, vendor correspondence, and
build output — all specific to one deployment or one vendor conversation. The captured
CDR in tests/oit.test.mjs keeps the real field names (which is the part that matters,
since the published schema doesn't match live deployments) with invented values.
MIT licensed, which is the short way of saying: use it, change it, ship it, sell what you
build with it, no permission needed and no attribution required. It is shared because the
problem is common to everyone running a sales team on a CRM, and nobody should have to
rediscover the traps in RECIPE.md one at a time.
Written by BJ Pote at eTop Technology. If you build on it, break it in a new way, or get it working against a CRM that isn't GlassHive, open an issue — that's the part worth having back.