One Genesys Cloud Embeddable Framework (PEF) session, many CRMs at the same time.
Chrome / Edge PEF Hub (Tauri 2, ~5-10 MB)
┌──────────────────────────┐ wss://127.0.0.1:9101 ┌──────────────────────────────────┐
│ Fake CRM tab │◄──────────────────────►│ listener fake-crm ┐ │
│ extension (SW holds WS) │ │ ├─ router ─┐ │
├──────────────────────────┤ wss://127.0.0.1:9102 │ │ state │ │
│ Mystery CRM tab │◄──────────────────────►│ listener mystery-crm┘ queue │ │
│ extension (SW holds WS) │ │ │ │
└──────────────────────────┘ │ PEF iframe (1 session, 1 phone)◄┘ │
└──────────────────────────────────┘
- Multi-port: one WSS listener per CRM. A port only accepts its own
crmId(hello handshake), from loopback, from allowed origins, with a valid pairing token. - Refresh-proof: the WebSocket lives in the extension service worker, not in the page. A CRM refresh only re-attaches the content script (local Port) and requests a snapshot from the hub.
- Stateful hub, stateless adapters: interaction ownership, offline queue with TTL, contact-search fan-out/merge all live in the hub (Rust).
- SDK-generated extensions: CRM-specific code is one file (
adapters/<crm>/adapter.ts). Change the SDK, rebuild, every extension is regenerated.
| Path | What |
|---|---|
packages/protocol |
Wire protocol v1 (TS). Mirrored in apps/hub/src-tauri/src/protocol.rs |
packages/sdk-core |
HubClient: hello/pairing, heartbeat, reconnect with backoff + jitter |
packages/sdk-extension |
MV3 runtime: service worker, isolated bridge, MAIN-world adapter runtime, badge + in-page pill |
adapters/fake-crm, adapters/mystery-crm |
Generated extensions (WXT). Only crm.config.ts + adapter.ts are CRM-specific |
tools/adapter-template, tools/new-adapter.mjs |
Generator for new CRM extensions |
apps/hub |
Tauri 2 hub: WSS listeners, router, PEF host page, status UI, tray |
pef/framework.js |
Reference framework.js message contract to merge into yours |
config/hub.config.json |
Ports, routing rules, certificate paths, PEF URL |
certs/test |
Shareable test certificates (see below) |
scripts/certs |
Generate / trust / remove certificates |
- Node 24 LTS (
nvm use 24),npm i -g pnpm - Rust: https://rustup.rs (stable)
- Visual Studio Build Tools, workload Desktop development with C++
- WebView2 runtime (already present with an updated Edge)
pnpm install
# 1. Trust the TEST CA (current user, Windows asks for confirmation)
powershell -ExecutionPolicy Bypass -File scripts\certs\install-ca.ps1
# 2. framework.js for ?crm=framework-local-secure (https://localhost/framework.js, port 443)
# Set your OAuth client ID in pef/framework.js, or in pef/client-id.local (gitignored)
pnpm pef:serve
# 3. Hub (Vite on https://localhost:1420 + Tauri window, listeners 9101/9102)
pnpm hub:dev
# 4. Dev CRM pages (stand-ins for the real CRMs, window.pefHubCrm + tel: links + event log)
pnpm crm:dev:fake # https://localhost:5173
pnpm crm:dev:mystery # https://localhost:4200
# 5. Extensions (each opens a Chrome instance with the extension loaded)
pnpm ext:dev:fake # matches https://localhost:5173/*
pnpm ext:dev:mystery # matches https://localhost:4200/*Dev notes:
- The PEF login runs in a popup (
dedicatedLoginWindow: true): login.mypurecloud.ie refuses to be framed under the hub. The hub only allows popups on the region domain derived frompef.url(e.g.*.mypurecloud.ie). - OAuth client: grant type Code Authorization (PKCE), redirect URI
https://apps.mypurecloud.ie/crm/authWindow.html(the PEF uses PKCE for the popup login). - Antivirus HTTPS scanning (e.g. Avast Web Shield) intercepts loopback TLS too. It works as long as the browser trusts the antivirus root, but check it on customer PCs.
Production builds:
pnpm hub:build # MSI + NSIS in apps/hub/src-tauri/target/release/bundle
pnpm ext:zip # one zip per CRM in adapters/<crm>/.output- In the Embeddable Framework integration, add the hub origins as CRM domain / allowed origin:
https://tauri.localhost(installed app) andhttps://localhost:1420(dev). - Put the same iframe URL you use today in
config/hub.config.json→pef.url(region.iepreset). - Merge
pef/framework.js(message contract) into your framework.js. Keep your clientIds and settings.
certs/test contains a name-constrained CA: it can only sign certificates for localhost, 127.0.0.1 and ::1.
Even though the test CA key is shared, it cannot be abused to impersonate any other website.
| File | Purpose |
|---|---|
ca.crt |
Trust on agent PCs (Trusted Root) |
localhost-fullchain.crt |
Hub tls.certPath (leaf + CA) |
localhost.key |
Hub tls.keyPath |
ca.key |
Only for re-issuing the leaf. Test only: everyone with this repo has it |
Leaf: SAN localhost, 127.0.0.1, ::1, EKU serverAuth, valid 397 days. CA: 5 years.
- Generate the customer's own CA + leaf (Git Bash):
or use a certificate issued by the customer's PKI (PEM, SAN must include
bash scripts/certs/gen-certs.sh certs/customer "ACME Contact Center"127.0.0.1andlocalhost). - Point the hub to it. Either edit
config/hub.config.json, or (no rebuild) drop ahub.config.jsonin%APPDATA%\com.genesys.ps.pefhub\with:"tls": { "certPath": "C:\\ProgramData\\PEFHub\\localhost-fullchain.crt", "keyPath": "C:\\ProgramData\\PEFHub\\localhost.key" }
- Trust the customer CA on agent PCs (Intune / GPO, machine store, silent):
scripts\certs\install-ca.ps1 -CaPath C:\path\ca.crt -Machine
- Remove the test CA:
scripts\certs\uninstall-ca.ps1(add-Machineif installed per machine).
Config lookup order: %PEF_HUB_CONFIG% → %APPDATA%\com.genesys.ps.pefhub\hub.config.json → bundled config/hub.config.json.
pnpm adapter:new servicenow 9103 "https://*.service-now.com/*" "ServiceNow"
pnpm installThis copies the template, sets the port, and adds the listener to config/hub.config.json.
Then implement adapters/servicenow/adapter.ts (detect, screenPop, searchContacts, logCall, bindClickToDial).
The generic adapter already works with any page exposing window.pefHubCrm (see packages/sdk-extension/src/generic-adapter.ts).
| Badge / pill | Meaning |
|---|---|
green ON |
Hub reached, paired, agent signed in to Genesys Cloud |
amber ! |
Hub reached, PEF not ready (not signed in) |
red OFF (blinking pill) |
Hub unreachable: app not running, port closed, certificate not trusted |
grey ? |
Hub reached, no CRM page detected |
Click the toolbar icon or the pill to retry immediately. Background retry: backoff 1s → 2s → 5s → 10s, plus a chrome.alarms wake-up every 30s.
screenPop: interaction attributecrmTarget(configurable) →queueRules[queueName]→ last focused CRM (fallback: "focused") or all ("broadcast"). The chosen CRM becomes the interaction owner.processCallLog/openCallLog/interactionUpdate: to the owner.clickToDial: any CRM → PEF. The next outbound interaction within 30s is owned by that CRM.contactSearch: fan-out to one session per CRM, merge withattributes.pefHubCrm, timeoutsearchTimeoutMs.- Owner offline: event queued for
queueTtlSecs, delivered on reconnect. Refreshed tabs get a snapshot.
- Listeners bind
127.0.0.1only and drop non-loopback peers. - Replace
"allowedOrigins": ["chrome-extension://*"]with exact extension IDs (chrome-extension://<id>). Fix IDs by setting akeyin each extension manifest or by publishing them. - The pairing token inside an extension is readable: treat it as an extra check, not a secret. Production: per-install token delivered via Native Messaging or MSI.
- Distribute extensions with
ExtensionInstallForcelist(Chrome / Edge policy) and a self-hostedupdate_url. - Chrome Local Network Access: validate on the customer's Chrome version; allow via enterprise policy if prompted.
- VDI / multi-session hosts: one user per port set. Plan per-session port offsets before rollout.
- Microphone permission: WebView2 shows its own prompt the first time. Next step: auto-grant for the PEF origin via WebView2
PermissionRequested. - PEF agent state is derived from
userActionSubscription(login/logout) and interaction events. Validate the category names on your org. - Autostart at login: add
tauri-plugin-autostart.