@coventia/softphone 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{call-BTYlT2SU.d.cts → call-VKq1I8yP.d.cts} +158 -5
- package/dist/{call-BTYlT2SU.d.ts → call-VKq1I8yP.d.ts} +158 -5
- package/dist/{chunk-N2N2EQQJ.js → chunk-7R6LOOAI.js} +4 -2
- package/dist/chunk-7R6LOOAI.js.map +1 -0
- package/dist/{chunk-5BGEHMPP.js → chunk-EYEDPD7Y.js} +146 -20
- package/dist/chunk-EYEDPD7Y.js.map +1 -0
- package/dist/{chunk-3V2L6UTK.js → chunk-LMJXX4YM.js} +17 -3
- package/dist/chunk-LMJXX4YM.js.map +1 -0
- package/dist/element/index.d.ts +162 -7
- package/dist/element/index.js +866 -421
- package/dist/element/index.js.map +1 -1
- package/dist/{engine-D9nbeY08.d.cts → engine-BlQBo2Ys.d.cts} +1 -1
- package/dist/{engine-Doa6Brkm.d.ts → engine-C0BOkIcW.d.ts} +1 -1
- package/dist/index.cjs +586 -28
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +335 -8
- package/dist/index.js.map +1 -1
- package/dist/react/index.cjs +15 -0
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +14 -3
- package/dist/react/index.d.ts +14 -3
- package/dist/react/index.js +2 -2
- package/dist/{softphone-DhuhcKCy.d.cts → softphone-Cyq3hiIM.d.ts} +17 -2
- package/dist/{softphone-DHmhotbf.d.ts → softphone-DlGV12Xp.d.cts} +17 -2
- package/dist/softphone.css +477 -35
- package/dist/{sources-N4BOEDRL.js → sources-IRKNQPWW.js} +108 -5
- package/dist/sources-IRKNQPWW.js.map +1 -0
- package/dist/testing/index.cjs +41 -5
- package/dist/testing/index.cjs.map +1 -1
- package/dist/testing/index.d.cts +8 -2
- package/dist/testing/index.d.ts +8 -2
- package/dist/testing/index.js +41 -5
- package/dist/testing/index.js.map +1 -1
- package/dist/ui/index.cjs +846 -410
- package/dist/ui/index.cjs.map +1 -1
- package/dist/ui/index.d.cts +17 -6
- package/dist/ui/index.d.ts +17 -6
- package/dist/ui/index.js +878 -456
- package/dist/ui/index.js.map +1 -1
- package/dist/zadarma/index.cjs +144 -18
- package/dist/zadarma/index.cjs.map +1 -1
- package/dist/zadarma/index.d.cts +2 -2
- package/dist/zadarma/index.d.ts +2 -2
- package/dist/zadarma/index.js +1 -1
- package/dist/zadarma-engine-L5DEFXUS.js +5 -0
- package/dist/{zadarma-engine-DN2SZSQ3.js.map → zadarma-engine-L5DEFXUS.js.map} +1 -1
- package/package.json +1 -1
- package/dist/chunk-3V2L6UTK.js.map +0 -1
- package/dist/chunk-5BGEHMPP.js.map +0 -1
- package/dist/chunk-N2N2EQQJ.js.map +0 -1
- package/dist/sources-N4BOEDRL.js.map +0 -1
- package/dist/zadarma-engine-DN2SZSQ3.js +0 -5
|
@@ -259,6 +259,14 @@ var SipCallEngine = class {
|
|
|
259
259
|
#uaInFlight = null;
|
|
260
260
|
#domain = "";
|
|
261
261
|
#activeCall = null;
|
|
262
|
+
/**
|
|
263
|
+
* The consult leg from `startConsult` (SPEC §5.6), if one is live on this UA right now.
|
|
264
|
+
*
|
|
265
|
+
* Tracked separately from `#activeCall` so `#maybeTeardownUA` can tell "both legs are actually gone"
|
|
266
|
+
* apart from "the main leg ended, but a consult is still up" — see that method's own comment for the
|
|
267
|
+
* bug this exists to prevent.
|
|
268
|
+
*/
|
|
269
|
+
#consultCall = null;
|
|
262
270
|
/** The chosen microphone, or `null` for the browser default. Read fresh by every new leg's
|
|
263
271
|
* `mediaConstraints`, and pushed live to `#activeCall` by `setInputDevice`. */
|
|
264
272
|
#inputDeviceId = null;
|
|
@@ -377,9 +385,7 @@ var SipCallEngine = class {
|
|
|
377
385
|
const ms = this.provider.quirks?.noInviteTeardownMs;
|
|
378
386
|
if (!ms) return;
|
|
379
387
|
this.#clearNoInviteTimer();
|
|
380
|
-
this.#noInviteTimer = setTimeout(() =>
|
|
381
|
-
if (!this.#activeCall) this.#teardownUA();
|
|
382
|
-
}, ms);
|
|
388
|
+
this.#noInviteTimer = setTimeout(() => this.#maybeTeardownUA(), ms);
|
|
383
389
|
}
|
|
384
390
|
/**
|
|
385
391
|
* Rebuild the incoming-signal channel with fresh credentials, after a registration failure.
|
|
@@ -501,6 +507,7 @@ var SipCallEngine = class {
|
|
|
501
507
|
}
|
|
502
508
|
this.#ua = null;
|
|
503
509
|
this.#activeCall = null;
|
|
510
|
+
this.#consultCall = null;
|
|
504
511
|
}
|
|
505
512
|
/** Place a call once the UA is registered (its 200 OK for REGISTER has arrived). */
|
|
506
513
|
/**
|
|
@@ -543,7 +550,7 @@ var SipCallEngine = class {
|
|
|
543
550
|
const code = missed ? "call/missed" : CALL_END_CODES[event.reason];
|
|
544
551
|
if (code) this.#reportError(code, event.cause);
|
|
545
552
|
if (this.#activeCall === call) this.#activeCall = null;
|
|
546
|
-
|
|
553
|
+
this.#maybeTeardownUA();
|
|
547
554
|
}
|
|
548
555
|
});
|
|
549
556
|
return call;
|
|
@@ -554,12 +561,38 @@ var SipCallEngine = class {
|
|
|
554
561
|
mediaConstraints: buildAudioConstraints(this.#inputDeviceId),
|
|
555
562
|
pcConfig: PC_CONFIG
|
|
556
563
|
});
|
|
557
|
-
|
|
564
|
+
const consultCall = new SipEngineCall(consultSession, {
|
|
558
565
|
id: nextCallId(),
|
|
559
566
|
buildTarget: (x) => this.#buildTarget(x),
|
|
560
567
|
placeConsult: () => Promise.reject(new Error("cannot consult from a consult leg")),
|
|
561
568
|
iceGatheringCapMs: this.provider.quirks?.iceGatheringCapMs ?? DEFAULT_ICE_CAP_MS
|
|
562
569
|
});
|
|
570
|
+
this.#consultCall = consultCall;
|
|
571
|
+
consultCall.onEvent((event) => {
|
|
572
|
+
if (event.type === "ended") {
|
|
573
|
+
if (this.#consultCall === consultCall) this.#consultCall = null;
|
|
574
|
+
this.#maybeTeardownUA();
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
return consultCall;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* On-demand registration (SPEC §3.3): the UA is disposable once NOTHING is left on it — but "the
|
|
581
|
+
* main leg ended" (or "no INVITE followed this push") is not the same fact as "nothing is left."
|
|
582
|
+
* `completeTransfer()` (SPEC §5.6) ends the main leg via a REFER…Replaces BYE *while the consult leg
|
|
583
|
+
* is still what the PBX needs to bridge the caller onto* — tearing the UA down (and with it, EVERY
|
|
584
|
+
* session on it, via `terminateSessions`) the instant the main leg's `ended` fires killed that
|
|
585
|
+
* consult leg out from under the transfer before the bridge could use it, and the caller was simply
|
|
586
|
+
* gone. Two real call sites hit this independently (found live, 2026-08-04): the main leg's own
|
|
587
|
+
* `ended` handler, and `#armNoInviteTeardown`'s 5s timer — the latter re-arms on EVERY push,
|
|
588
|
+
* including unrelated "answered" pushes Zadarma sends around other account activity, so it can fire
|
|
589
|
+
* minutes after a transfer with no `#activeCall` and no idea a consult leg is mid-handoff. Both now
|
|
590
|
+
* route through here rather than checking `#activeCall` alone.
|
|
591
|
+
*/
|
|
592
|
+
#maybeTeardownUA() {
|
|
593
|
+
if (this.provider.registrationMode !== "on-demand") return;
|
|
594
|
+
if (this.#activeCall || this.#consultCall) return;
|
|
595
|
+
this.#teardownUA();
|
|
563
596
|
}
|
|
564
597
|
#startRejectGuard() {
|
|
565
598
|
const ms = this.provider.quirks?.rejectGuardMs;
|
|
@@ -594,6 +627,8 @@ async function loadJsSipFactory() {
|
|
|
594
627
|
// src/transports/providers/zadarma.ts
|
|
595
628
|
var PING = { localMs: 3e4, globalMs: 6e4 };
|
|
596
629
|
var KEY_REFRESH_BUFFER_MS = 2 * 60 * 60 * 1e3;
|
|
630
|
+
var REBUILD_CONNECT_TIMEOUT_MS = 1e4;
|
|
631
|
+
var REBUILD_RETRY_MS = 3e4;
|
|
597
632
|
var ZADARMA_QUIRKS = {
|
|
598
633
|
// Zadarma's registrar REFUSES to register a WebRTC client with any other User-Agent — a clean 401
|
|
599
634
|
// after a valid digest challenge, indistinguishable from a wrong password (§3.1). The old CRM
|
|
@@ -621,6 +656,12 @@ var ZadarmaPushSignal = class {
|
|
|
621
656
|
#connected = false;
|
|
622
657
|
#pingTimers = [];
|
|
623
658
|
#refreshTimer;
|
|
659
|
+
/** The replacement socket of a rebuild in flight, not yet current. */
|
|
660
|
+
#candidate = null;
|
|
661
|
+
/** Abandons the rebuild in flight (closing its candidate). */
|
|
662
|
+
#abortRebuild = null;
|
|
663
|
+
#rebuilding = false;
|
|
664
|
+
#retryTimer;
|
|
624
665
|
#unwatchBackground;
|
|
625
666
|
#closed = false;
|
|
626
667
|
#listeners = /* @__PURE__ */ new Set();
|
|
@@ -636,20 +677,35 @@ var ZadarmaPushSignal = class {
|
|
|
636
677
|
if (this.#closed) return;
|
|
637
678
|
const socket = this.deps.createPushSocket(buildPushUrl(key, sip));
|
|
638
679
|
this.#socket = socket;
|
|
680
|
+
this.#attach(socket);
|
|
681
|
+
this.#armRefresh(keyExpiresAt);
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Wires one socket's events. `this.#socket` is read when an event FIRES, not when it is wired: a socket that
|
|
685
|
+
* has been replaced must stop driving connectivity (our own `close()` of it emits `disconnect`, which would
|
|
686
|
+
* otherwise flip the state to offline right after a perfectly healthy swap). `update` is the exception — it is
|
|
687
|
+
* forwarded from any socket not yet closed, because a push seen twice is harmless (the engine ignores a
|
|
688
|
+
* repeated signal mid-call and reuses a UA that already exists) while one seen by neither is a missed call.
|
|
689
|
+
*
|
|
690
|
+
* With `hooks`, the socket is a rebuild's candidate: its first `connect` or failure goes to the rebuild instead
|
|
691
|
+
* of changing the connection state (it is not current yet).
|
|
692
|
+
*/
|
|
693
|
+
#attach(socket, hooks) {
|
|
639
694
|
socket.on("connect", () => {
|
|
695
|
+
if (hooks && socket === this.#candidate) return hooks.onConnect();
|
|
696
|
+
if (socket !== this.#socket) return;
|
|
640
697
|
this.#setConnected(true);
|
|
641
698
|
this.#startPings(socket);
|
|
642
699
|
});
|
|
643
700
|
socket.on("update", () => this.#emit());
|
|
644
|
-
|
|
701
|
+
const lost = () => {
|
|
702
|
+
if (hooks && socket === this.#candidate) return hooks.onFailure();
|
|
703
|
+
if (socket !== this.#socket) return;
|
|
645
704
|
this.#stopPings();
|
|
646
705
|
this.#setConnected(false);
|
|
647
|
-
}
|
|
648
|
-
socket.on("
|
|
649
|
-
|
|
650
|
-
this.#setConnected(false);
|
|
651
|
-
});
|
|
652
|
-
this.#armRefresh(keyExpiresAt);
|
|
706
|
+
};
|
|
707
|
+
socket.on("disconnect", lost);
|
|
708
|
+
socket.on("connect_error", lost);
|
|
653
709
|
}
|
|
654
710
|
/**
|
|
655
711
|
* Presence pings — **the reason an idle tab keeps receiving calls**.
|
|
@@ -688,22 +744,92 @@ var ZadarmaPushSignal = class {
|
|
|
688
744
|
if (!Number.isFinite(ms) || ms <= 0) return;
|
|
689
745
|
this.#refreshTimer = setTimeout(() => void this.#reopen(), ms);
|
|
690
746
|
}
|
|
747
|
+
/**
|
|
748
|
+
* Replaces the push socket with a fresh one — MAKE-BEFORE-BREAK (presence-client-reporting.md §4.1, T8).
|
|
749
|
+
*
|
|
750
|
+
* Why it must happen at all: Zadarma's gateway decides the extension is alive from the raw Engine.IO ping, which
|
|
751
|
+
* a hidden tab's throttled timers silently stop sending, so while the tab is hidden the socket is rebuilt every
|
|
752
|
+
* 20 s (`push-socket-reliability.md` §5.3), and once before the key expires. Why it must not leave a gap: the
|
|
753
|
+
* old close-then-reopen reported `connected = false` for the ~0.5 s in between, which took the softphone
|
|
754
|
+
* `offline`, tore down the presence socket and left Zadarma with no socket to push an incoming call to.
|
|
755
|
+
*
|
|
756
|
+
* Now the OLD socket stays up and stays current until the NEW one has actually connected; only then do we swap,
|
|
757
|
+
* move the pings and close the old one. A healthy rebuild never changes `connected`. A failed one — the key fetch
|
|
758
|
+
* throws, the socket errors, or it has not connected within `REBUILD_CONNECT_TIMEOUT_MS` — keeps the old socket
|
|
759
|
+
* and retries, instead of leaving the tab disconnected. Zadarma does not evict the older of two sockets for one
|
|
760
|
+
* sip (multi-tab-leader-election.md §1), so the overlap is safe.
|
|
761
|
+
*/
|
|
691
762
|
async #reopen() {
|
|
692
|
-
if (this.#closed) return;
|
|
693
|
-
this.#
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
763
|
+
if (this.#closed || this.#rebuilding) return;
|
|
764
|
+
if (!this.#socket) {
|
|
765
|
+
try {
|
|
766
|
+
await this.#open();
|
|
767
|
+
} catch {
|
|
768
|
+
}
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
this.#rebuilding = true;
|
|
697
772
|
try {
|
|
698
|
-
await this
|
|
773
|
+
const { key, sip, keyExpiresAt } = await this.deps.fetchKey();
|
|
774
|
+
if (this.#closed) return;
|
|
775
|
+
const swapped = await new Promise((resolve) => {
|
|
776
|
+
const socket = this.deps.createPushSocket(buildPushUrl(key, sip));
|
|
777
|
+
this.#candidate = socket;
|
|
778
|
+
let settled = false;
|
|
779
|
+
const finish = (ok) => {
|
|
780
|
+
if (settled) return;
|
|
781
|
+
settled = true;
|
|
782
|
+
clearTimeout(timer);
|
|
783
|
+
this.#abortRebuild = null;
|
|
784
|
+
if (ok && socket.connected) {
|
|
785
|
+
this.#swapIn(socket, keyExpiresAt);
|
|
786
|
+
} else {
|
|
787
|
+
this.#candidate = null;
|
|
788
|
+
socket.close();
|
|
789
|
+
}
|
|
790
|
+
resolve(ok && socket.connected);
|
|
791
|
+
};
|
|
792
|
+
const timer = setTimeout(() => finish(false), REBUILD_CONNECT_TIMEOUT_MS);
|
|
793
|
+
this.#abortRebuild = () => finish(false);
|
|
794
|
+
this.#attach(socket, { onConnect: () => finish(true), onFailure: () => finish(false) });
|
|
795
|
+
});
|
|
796
|
+
if (!swapped) this.#armRetry();
|
|
699
797
|
} catch {
|
|
798
|
+
this.#armRetry();
|
|
799
|
+
} finally {
|
|
800
|
+
this.#rebuilding = false;
|
|
700
801
|
}
|
|
701
802
|
}
|
|
803
|
+
/**
|
|
804
|
+
* The moment of the swap — synchronous, called from the candidate's own `connect` event so nothing can run
|
|
805
|
+
* between "the new socket is up" and "it is the current one". Order matters: `#socket` moves first, so the old
|
|
806
|
+
* socket's `disconnect` (our own `close()` of it, just below) is ignored by `#attach`.
|
|
807
|
+
*/
|
|
808
|
+
#swapIn(socket, keyExpiresAt) {
|
|
809
|
+
const previous = this.#socket;
|
|
810
|
+
this.#socket = socket;
|
|
811
|
+
this.#candidate = null;
|
|
812
|
+
clearTimeout(this.#retryTimer);
|
|
813
|
+
this.#retryTimer = void 0;
|
|
814
|
+
this.#stopPings();
|
|
815
|
+
this.#startPings(socket);
|
|
816
|
+
this.#setConnected(true);
|
|
817
|
+
previous?.close();
|
|
818
|
+
this.#armRefresh(keyExpiresAt);
|
|
819
|
+
}
|
|
820
|
+
#armRetry() {
|
|
821
|
+
if (this.#closed) return;
|
|
822
|
+
clearTimeout(this.#retryTimer);
|
|
823
|
+
this.#retryTimer = setTimeout(() => void this.#reopen(), REBUILD_RETRY_MS);
|
|
824
|
+
}
|
|
702
825
|
disconnect() {
|
|
703
826
|
this.#closed = true;
|
|
704
827
|
this.#stopPings();
|
|
705
828
|
clearTimeout(this.#refreshTimer);
|
|
706
829
|
this.#refreshTimer = void 0;
|
|
830
|
+
clearTimeout(this.#retryTimer);
|
|
831
|
+
this.#retryTimer = void 0;
|
|
832
|
+
this.#abortRebuild?.();
|
|
707
833
|
this.#unwatchBackground?.();
|
|
708
834
|
this.#unwatchBackground = void 0;
|
|
709
835
|
this.#socket?.close();
|
|
@@ -941,5 +1067,5 @@ function createZadarmaEngine(config) {
|
|
|
941
1067
|
}
|
|
942
1068
|
|
|
943
1069
|
export { ZADARMA_QUIRKS, createZadarmaEngine };
|
|
944
|
-
//# sourceMappingURL=chunk-
|
|
945
|
-
//# sourceMappingURL=chunk-
|
|
1070
|
+
//# sourceMappingURL=chunk-EYEDPD7Y.js.map
|
|
1071
|
+
//# sourceMappingURL=chunk-EYEDPD7Y.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/transports/sip/media.ts","../src/transports/sip/ice.ts","../src/transports/sip/session.ts","../src/transports/sip/ua.ts","../src/transports/sip/real-factory.ts","../src/transports/providers/zadarma.ts","../src/transports/providers/zadarma-browser.ts","../src/transports/zadarma-engine.ts"],"names":["track"],"mappings":"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","file":"chunk-EYEDPD7Y.js","sourcesContent":["import type { JsSipSession, JsSipIceEvent } from './jssip.js';\r\n\r\n/**\r\n * What a leg asks the browser for. Audio only — `video: false` is explicit rather than omitted,\r\n * matching the legacy hook exactly.\r\n *\r\n * A FUNCTION, not the static constant this used to be: device selection (SPEC §7 Phase 2) means the\r\n * constraint depends on whichever input device is currently selected, which can change between calls\r\n * — a static object could never reflect that. `deviceId: null` (the default, no device chosen yet)\r\n * falls back to bare `audio: true`, letting the browser pick its own default exactly as before.\r\n */\r\nexport function buildAudioConstraints(deviceId: string | null): MediaStreamConstraints {\r\n return { audio: deviceId ? { deviceId: { exact: deviceId } } : true, video: false };\r\n}\r\n\r\n/**\r\n * The two most non-obvious, most load-bearing, most deletable-LOOKING pieces of the engine\r\n * (SPEC §3.1). Everything here is generic WebRTC — nothing Zadarma-specific — but it is exactly the\r\n * code a rewrite quietly drops, so the reasoning travels with it.\r\n *\r\n * No browser global is touched at module scope (SPEC §10.2): every function reaches for\r\n * `window`/`navigator` only when called, in a browser.\r\n */\r\n\r\n/**\r\n * A **safety net** on ICE gathering: send the INVITE anyway if gathering has not finished in `capMs`.\r\n *\r\n * JsSIP already releases the INVITE the moment gathering completes — it calls its own `ready()` both\r\n * on the end-of-candidates signal and on `icegatheringstatechange === 'complete'`. So the normal path\r\n * needs nothing from us. This exists only for the pathological one: against an unreachable STUN or\r\n * TURN server, Chrome can sit in `gathering` for ~40 s, and a phone that takes 40 s to ring is broken.\r\n *\r\n * **It deliberately does NOT settle on the first `typ srflx` candidate**, which is what it used to do.\r\n * One reflexive candidate is not \"enough for NAT traversal\" — it is enough only if it happens to be\r\n * the right one, and cutting gathering short there loses:\r\n *\r\n * - **the other address family.** A first srflx over IPv6 against an IPv4-only peer is useless; the\r\n * IPv4 one arrives moments later and never makes it into the offer.\r\n * - **the other interfaces.** On a multi-homed machine (Wi-Fi + VPN + Ethernet) the first srflx may\r\n * be the one that cannot reach the media server.\r\n * - **every relay candidate.** TURN needs an allocation round-trip, so `typ relay` always arrives\r\n * AFTER srflx. Settling early meant a configured TURN server could never appear in the SDP — the\r\n * relay would be paid for and never offered, and the call would fail exactly as if it were absent.\r\n *\r\n * This is SIP, not trickle ICE: whatever is in the offer is all the peer will ever see. So the cost of\r\n * settling early is permanent for that call, while the cost of waiting is a few hundred milliseconds.\r\n *\r\n * @returns a disposer that cancels the pending timer (call it on teardown).\r\n */\r\nexport function capIceGathering(session: JsSipSession, capMs: number): () => void {\r\n let settled = false;\r\n let timer: ReturnType<typeof setTimeout> | undefined;\r\n\r\n const settle = (ready: () => void): void => {\r\n if (settled) return;\r\n settled = true;\r\n if (timer) clearTimeout(timer);\r\n try {\r\n ready();\r\n } catch {\r\n /* offer already sent */\r\n }\r\n };\r\n\r\n session.on('icecandidate', ({ ready }: JsSipIceEvent) => {\r\n // Armed on the first candidate of any kind; JsSIP wins the race whenever gathering completes\r\n // normally, which is the overwhelmingly common case.\r\n if (!timer) timer = setTimeout(() => settle(ready), capMs);\r\n });\r\n\r\n return () => {\r\n if (timer) clearTimeout(timer);\r\n };\r\n}\r\n","/**\r\n * ICE configuration — how the browser's media finds its way out of a NAT.\r\n *\r\n * **SDK infrastructure, not host configuration.** A partner integrating a softphone should not have\r\n * to learn what a STUN server is, and every host would otherwise solve the same problem identically.\r\n * So there is a working default here and nothing to configure.\r\n *\r\n * With no ICE servers at all, a browser offers only HOST candidates — its own LAN addresses — and the\r\n * far end has to reach `192.168.x.x` directly. That works only where the NAT happens to cooperate,\r\n * and fails on symmetric NAT and most corporate networks. Firefox says `ICE failed, add a STUN\r\n * server`; Chrome's version is worse, connecting a call that carries no audio.\r\n *\r\n * **STUN only, deliberately — there is no TURN here and it could not be added here.** STUN just\r\n * reports the browser's own public address: stateless, unauthenticated, free. TURN *relays the media*,\r\n * so it costs bandwidth and therefore needs credentials — and a credential compiled into a client\r\n * bundle is a public one, usable by anyone who opens devtools. A relay would have to be issued per\r\n * user and short-lived by a backend that knows who is asking (coturn's `use-auth-secret` scheme is\r\n * the usual shape). That is real infrastructure, and until a network actually demands it we do not\r\n * run any: the legacy CRM softphone shipped exactly this one STUN server and placed real calls for\r\n * years.\r\n */\r\n\r\n/**\r\n * Google's public STUN. No registration, and the de facto default for the WebRTC ecosystem — it is\r\n * also, verbatim, what the legacy `useSipUA.js` used, which is the strongest argument available that\r\n * it is sufficient for Zadarma.\r\n *\r\n * Two caveats worth a decision rather than a shrug: it carries **no SLA**, and every user's IP is\r\n * observed by Google. Both are fixed the same way — point this at a STUN server we run.\r\n */\r\nexport const DEFAULT_ICE_SERVERS: readonly RTCIceServer[] = [\r\n { urls: 'stun:stun.l.google.com:19302' },\r\n];\r\n\r\n/** The `pcConfig` JsSIP hands to `new RTCPeerConnection`, for every leg in either direction. */\r\nexport const PC_CONFIG: RTCConfiguration = { iceServers: [...DEFAULT_ICE_SERVERS] };\r\n","import type { EngineCall, EngineCallEvent } from '../../core/engine.js';\r\nimport type { HangupReason } from '../../core/call.js';\r\nimport type { CallDirection } from '../../core/types.js';\r\nimport type { JsSipEndEvent, JsSipSession } from './jssip.js';\r\nimport { buildAudioConstraints, capIceGathering } from './media.js';\r\nimport { PC_CONFIG } from './ice.js';\r\n\r\n/** Map JsSIP end/failed causes onto the SDK's HangupReason. Generic SIP, no provider specifics. */\r\nfunction mapEndReason(e: JsSipEndEvent): HangupReason {\r\n if (e.originator === 'local') return 'local';\r\n if (e.originator === 'remote') return 'remote';\r\n return 'failed';\r\n}\r\n/**\r\n * A JsSIP cause onto the SDK's HangupReason.\r\n *\r\n * The cases below are `JsSIP.C.causes`, and the status codes in the comments are its own\r\n * `SIP_ERROR_CAUSES` table — this must be read against that list rather than guessed at, because\r\n * `JsSipEndEvent.cause` is a bare `string`, so a case that never matches costs nothing at compile\r\n * time and silently falls to `failed`. That is exactly how `Unavailable` went unhandled: it is what\r\n * a call that rings out actually returns, and it was reaching `default`.\r\n *\r\n * `failed` is a real outcome, not a fallthrough — see CALL_END_CODES in `ua.ts`. Anything unmapped\r\n * still tells the user the call did not connect.\r\n */\r\nfunction mapFailReason(e: JsSipEndEvent): HangupReason {\r\n switch (e.cause) {\r\n // 486 Busy Here, 600 Busy Everywhere. Also what most endpoints send for a MANUAL DECLINE — SIP\r\n // does not distinguish the two, and neither can we, which is why the copy covers both.\r\n case 'Busy':\r\n return 'busy';\r\n /*\r\n `Rejected` is 403 / 603 Decline — an explicit refusal, sent by the endpoints that bother to.\r\n `Redirected` is 3xx: not literally a rejection, but it ends the same way — not connected, and\r\n nothing to retry.\r\n\r\n (Both comments sit ABOVE the group rather than between the labels. A comment between two `case`\r\n lines makes the first one non-empty to `no-fallthrough`, which then demands a `break` that\r\n would be unreachable.)\r\n */\r\n case 'Rejected':\r\n case 'Redirected':\r\n return 'rejected';\r\n /*\r\n `Unavailable` is 480 Temporarily Unavailable / 410 / 408 Request Timeout / 430 — THE ring-out\r\n case, and the one that was missing. `No Answer` and `Expires` are local: our own no-answer\r\n timer, and a session that expired.\r\n */\r\n case 'Unavailable':\r\n case 'No Answer':\r\n case 'Expires':\r\n return 'no-answer';\r\n case 'Canceled':\r\n /*\r\n BOTH directions produce this cause, meaning opposite things, and only the originator tells\r\n them apart: `remote` is the caller giving up while our phone rang (a missed call — `ua.ts`\r\n turns this one into `call/missed`), `local` is the user abandoning their own outbound call\r\n before it was picked up. This used to return `remote` for both, which told the host that the\r\n far end ended a call the user had just hung up on themselves.\r\n */\r\n return e.originator === 'local' ? 'local' : 'remote';\r\n // Everything else is a genuine failure worth naming: Not Found (404/604), Address Incomplete,\r\n // Incompatible SDP, Authentication Error, WebRTC Error, RTP Timeout, Connection Error…\r\n default:\r\n return 'failed';\r\n }\r\n}\r\n\r\nfunction remoteNumberOf(session: JsSipSession): string {\r\n const id = session.remote_identity;\r\n return id?.uri?.user ?? id?.uri?.toString() ?? '';\r\n}\r\n\r\nexport interface SipEngineCallDeps {\r\n readonly id: string;\r\n /** Turn an extension or bare number into a full SIP URI (the engine knows the domain). */\r\n readonly buildTarget: (target: string) => string;\r\n /** Place a consult leg on the same UA (attended transfer). */\r\n readonly placeConsult: (target: string) => Promise<SipEngineCall>;\r\n readonly iceGatheringCapMs?: number;\r\n /** ICE servers for the answer's peer connection — same set the engine uses outbound. */\r\n readonly pcConfig?: RTCConfiguration;\r\n /**\r\n * Called when THIS call is rejected (486), so the engine can start its provider re-ring guard\r\n * (SPEC §3.4). Tied to the reject ACTION, not to `ended` — a normal hangup must not start it.\r\n */\r\n readonly onReject?: () => void;\r\n /**\r\n * The currently-selected input device, read fresh at ANSWER time — an inbound call's `getUserMedia`\r\n * happens here, not at construction, so a device chosen after the call started ringing still takes\r\n * effect the moment it is picked up. `undefined`/`null` means no device chosen — the browser default.\r\n */\r\n readonly getInputDeviceId?: () => string | null;\r\n}\r\n\r\n/**\r\n * One JsSIP call, as an `EngineCall`. It translates JsSIP's events into the SDK's raw\r\n * `EngineCallEvent`s — and `Call` (core) turns those into the public state machine. Nothing here\r\n * knows the state machine, and nothing here is Zadarma-specific.\r\n */\r\nexport class SipEngineCall implements EngineCall {\r\n readonly id: string;\r\n readonly direction: CallDirection;\r\n readonly remoteNumber: string;\r\n\r\n readonly #session: JsSipSession;\r\n readonly #deps: SipEngineCallDeps;\r\n readonly #listeners = new Set<(event: EngineCallEvent) => void>();\r\n\r\n #ended = false;\r\n #cancelIceCap: (() => void) | null = null;\r\n /**\r\n * The stream behind a LIVE device swap (`swapInputDevice`), if one has happened. Distinct from\r\n * JsSIP's own original capture: that one is `_localMediaStreamLocallyGenerated` and JsSIP stops it\r\n * itself on close (see `#end`'s comment). This one is ours — obtained with our own `getUserMedia`\r\n * call, attached via `replaceTrack`, and never known to JsSIP at all — so nothing else will ever\r\n * stop it unless we do, here.\r\n */\r\n #swappedStream: MediaStream | null = null;\r\n\r\n constructor(session: JsSipSession, deps: SipEngineCallDeps) {\r\n this.#session = session;\r\n this.#deps = deps;\r\n this.id = deps.id;\r\n this.direction = session.direction === 'incoming' ? 'inbound' : 'outbound';\r\n this.remoteNumber = remoteNumberOf(session);\r\n this.#wire();\r\n }\r\n\r\n /** @internal — the underlying session, for `completeTransfer`'s REFER…Replaces. */\r\n get session(): JsSipSession {\r\n return this.#session;\r\n }\r\n\r\n onEvent(listener: (event: EngineCallEvent) => void): () => void {\r\n this.#listeners.add(listener);\r\n return () => this.#listeners.delete(listener);\r\n }\r\n\r\n // ── JsSIP events → raw engine signals ─────────────────────────────────────\r\n #wire(): void {\r\n const s = this.#session;\r\n\r\n /*\r\n * `progress` is OUTBOUND-only.\r\n *\r\n * JsSIP fires it in both directions, but it means opposite things: on an outbound call it is the\r\n * remote's `180`/`183` — the state we call `early`, possibly with early media. On an INBOUND call\r\n * it fires when *we* send `180 Ringing`, which is not a state change at all: the call is already\r\n * `ringing`, and the machine has no `ringing → early` edge because there is no such thing.\r\n *\r\n * Wiring it for both is what produced `illegal call transition ringing → early` on every real\r\n * inbound call. It was harmless — the transition was refused and the call rang and answered fine —\r\n * but it logged an error on a healthy path, and an error that fires when nothing is wrong is how\r\n * people learn to ignore the console.\r\n */\r\n if (this.direction === 'outbound') {\r\n s.on('progress', () => this.#fire({ type: 'progress', response: 'ringing' }));\r\n }\r\n\r\n /*\r\n * The remote audio. `ontrack` has to be wired on BOTH paths, because JsSIP creates the peer\r\n * connection at a different moment in each direction:\r\n *\r\n * INBOUND — created inside `answer()`, long after this runs. The event is the only way to see it.\r\n * OUTBOUND — created SYNCHRONOUSLY inside `ua.call()`: JsSIP's `connect()` calls\r\n * `_createRTCConnection()` before it returns, so `peerconnection` has already fired\r\n * by the time this object exists. Listening alone means `ontrack` is never set and\r\n * the caller hears silence — while the callee hears them perfectly, since the\r\n * outbound media path is unaffected.\r\n *\r\n * The legacy hook did exactly this: `if (session.connection) session.connection.ontrack = …`\r\n * right after `ua.call()`, plus the event for the answer path.\r\n */\r\n const wireRemoteTrack = (pc: RTCPeerConnection): void => {\r\n pc.ontrack = ({ streams }) => {\r\n if (streams[0]) this.#fire({ type: 'stream', stream: streams[0] });\r\n };\r\n };\r\n s.on('peerconnection', ({ peerconnection }) => wireRemoteTrack(peerconnection));\r\n if (s.connection) wireRemoteTrack(s.connection);\r\n\r\n // Outbound only: release the INVITE without waiting for full ICE gathering (SPEC §3.1).\r\n if (this.direction === 'outbound' && this.#deps.iceGatheringCapMs) {\r\n this.#cancelIceCap = capIceGathering(s, this.#deps.iceGatheringCapMs);\r\n }\r\n\r\n s.on('accepted', () => this.#onAccepted());\r\n s.on('ended', (e) => this.#end(mapEndReason(e), e.cause));\r\n s.on('failed', (e) => this.#end(mapFailReason(e), e.cause));\r\n s.on('hold', ({ originator }) => this.#fire({ type: 'held', originator }));\r\n s.on('unhold', ({ originator }) => this.#fire({ type: 'unheld', originator }));\r\n }\r\n\r\n /**\r\n * The call is live. Nothing to swap: JsSIP opened the real microphone when the session was created,\r\n * so the far end has had our actual audio from the first packet.\r\n *\r\n * There used to be a silent-mic dance here — place the call with a synthetic AudioContext track,\r\n * swap the real mic in on accept — so a Bluetooth headset stayed in A2DP while the phone rang. It\r\n * cost more than it bought: with no active capture, Firefox restricts ICE gathering and the offer\r\n * goes out with no candidates at all, which no STUN or TURN server can fix.\r\n */\r\n #onAccepted(): void {\r\n this.#fire({ type: 'accepted' });\r\n }\r\n\r\n // ── Commands ───────────────────────────────────────────────────────────────\r\n async answer(): Promise<void> {\r\n const deviceId = this.#deps.getInputDeviceId?.() ?? null;\r\n this.#session.answer({\r\n mediaConstraints: buildAudioConstraints(deviceId),\r\n pcConfig: this.#deps.pcConfig ?? PC_CONFIG,\r\n });\r\n }\r\n\r\n /**\r\n * Live-swap the outgoing microphone track — `CallEngine.setInputDevice` on a call that is already\r\n * connected. Never touches the FIRST call's capture: that one is JsSIP's own `getUserMedia`, opened\r\n * inside `answer()`/`ua.call()` (see `#onAccepted`'s comment on why that stays that way). This is\r\n * only for a device chosen AFTER the call is already live.\r\n *\r\n * `RTCRtpSender.replaceTrack` rather than a fresh `answer()`/re-INVITE: it needs no SDP\r\n * renegotiation and the far end never sees an interruption — exactly the mechanism real softphones\r\n * use for \"switch mic mid-call.\"\r\n */\r\n async swapInputDevice(deviceId: string | null): Promise<void> {\r\n const pc = this.#session.connection;\r\n if (!pc || this.#ended) return; // not connected yet, or already over — nothing to swap\r\n const sender = pc.getSenders().find((s) => s.track?.kind === 'audio');\r\n if (!sender) return;\r\n\r\n let stream: MediaStream;\r\n try {\r\n stream = await navigator.mediaDevices.getUserMedia(buildAudioConstraints(deviceId));\r\n } catch {\r\n // The chosen device vanished between selection and this call (unplugged, permission revoked,\r\n // a stale id) — nothing useful to do beyond leaving the current track exactly as it was.\r\n return;\r\n }\r\n if (this.#ended) {\r\n // The call ended while getUserMedia was in flight — don't attach a track to a dead sender,\r\n // and don't leak the capture we just opened.\r\n for (const track of stream.getTracks()) track.stop();\r\n return;\r\n }\r\n const [track] = stream.getAudioTracks();\r\n if (!track) return;\r\n\r\n await sender.replaceTrack(track);\r\n // Stop whatever we swapped in LAST time — never the original JsSIP capture, which this never\r\n // touches and which JsSIP stops itself on close.\r\n for (const oldTrack of this.#swappedStream?.getTracks() ?? []) oldTrack.stop();\r\n this.#swappedStream = stream;\r\n }\r\n\r\n async reject(): Promise<void> {\r\n this.#deps.onReject?.();\r\n this.#session.terminate({ status_code: 486, reason_phrase: 'Busy Here' });\r\n }\r\n\r\n async hangup(): Promise<void> {\r\n this.#session.terminate();\r\n }\r\n\r\n mute(muted: boolean): void {\r\n if (muted) this.#session.mute({ audio: true });\r\n else this.#session.unmute({ audio: true });\r\n }\r\n\r\n async hold(): Promise<void> {\r\n // JsSIP writes `a=sendonly` and fires 'hold' — which #wire translates to a `held` signal. One\r\n // path, so a locally-initiated hold and a remote one travel identically (SPEC §5.6).\r\n this.#session.hold();\r\n }\r\n\r\n async unhold(): Promise<void> {\r\n this.#session.unhold();\r\n }\r\n\r\n sendDigits(digits: string): void {\r\n this.#session.sendDTMF(digits);\r\n }\r\n\r\n async transfer(target: string): Promise<void> {\r\n // Blind transfer. JsSIP ends the session on a successful REFER; that surfaces as `ended`\r\n // (reason 'transferred' is set by the core when it sees the transfer intent — here it is a\r\n // normal local end).\r\n this.#session.refer(this.#deps.buildTarget(target));\r\n }\r\n\r\n async startConsult(target: string): Promise<EngineCall> {\r\n return this.#deps.placeConsult(target);\r\n }\r\n\r\n async completeTransfer(consult: EngineCall): Promise<void> {\r\n const consultSession = (consult as SipEngineCall).session;\r\n this.#session.refer(consultSession.remote_identity.uri.toString(), { replaces: consultSession });\r\n }\r\n\r\n // ── internals ──────────────────────────────────────────────────────────────\r\n #end(reason: HangupReason, cause?: string): void {\r\n if (this.#ended) return;\r\n this.#ended = true;\r\n this.#cancelIceCap?.();\r\n // The ORIGINAL microphone is NOT stopped here: JsSIP opened it (no `mediaStream` was passed), so\r\n // it owns it and stops the tracks itself on close. See `_localMediaStreamLocallyGenerated` in\r\n // JsSIP. A SWAPPED-IN one (`swapInputDevice`) is a different story — JsSIP never learned about\r\n // it, so it is ours to stop, or the capture (and the browser's mic indicator) outlives the call.\r\n for (const track of this.#swappedStream?.getTracks() ?? []) track.stop();\r\n this.#swappedStream = null;\r\n this.#fire({ type: 'ended', reason, cause });\r\n this.#listeners.clear();\r\n }\r\n\r\n #fire(event: EngineCallEvent): void {\r\n if (this.#ended && event.type !== 'ended') return;\r\n for (const listener of [...this.#listeners]) listener(event);\r\n }\r\n}\r\n","import type { CallEngine, EngineCall } from '../../core/engine.js';\r\nimport type { CallOptions, SoftphoneState } from '../../core/types.js';\r\nimport type { SipCredentials, SipProvider } from '../providers/provider.js';\r\nimport type { JsSipFactory, JsSipSession, JsSipUA, JsSipUAConfig } from './jssip.js';\r\nimport { SipEngineCall } from './session.js';\r\nimport { buildAudioConstraints } from './media.js';\r\nimport { PC_CONFIG } from './ice.js';\r\nimport { createError, isSoftphoneError } from '../../core/errors.js';\r\nimport type { NonFatalErrorCode, SoftphoneError, SoftphoneErrorCode } from '../../core/errors.js';\r\nimport type { HangupReason } from '../../core/call.js';\r\n\r\nconst DEFAULT_ICE_CAP_MS = 3000;\r\nconst REGISTER_EXPIRES = 580;\r\n/** Floor between signal rebuilds, so a persistently failing registrar cannot become a hot loop. */\r\nconst SIGNAL_REFRESH_DEBOUNCE_MS = 30_000;\r\n/** Same window, same reason: a retrying registrar must not become a stream of identical errors. */\r\nconst ERROR_DEBOUNCE_MS = 30_000;\r\n\r\n/**\r\n * Call outcomes worth telling the user about. Reasons NOT here (`local`, `remote`, `transferred`)\r\n * are ordinary endings — reporting them would turn every completed call into an error.\r\n *\r\n * `failed` MUST stay mapped. It is `mapFailReason`'s catch-all, so leaving it out means every cause\r\n * nobody thought to enumerate ends the call in silence — the user watches the dialpad come back with\r\n * no idea whether it rang, and the least-understood failures are the ones explained least.\r\n */\r\nconst CALL_END_CODES: Partial<Record<HangupReason, NonFatalErrorCode>> = {\r\n busy: 'call/busy',\r\n 'no-answer': 'call/no-answer',\r\n rejected: 'call/rejected',\r\n failed: 'call/failed',\r\n};\r\n\r\n/** `sip:100@pbx.example.com` → `pbx.example.com`. */\r\nfunction domainOf(credentials: SipCredentials): string {\r\n const at = credentials.uri.lastIndexOf('@');\r\n return at === -1 ? '' : credentials.uri.slice(at + 1);\r\n}\r\n\r\nlet callSeq = 0;\r\nconst nextCallId = (): string => `sip-${++callSeq}`;\r\n\r\n/**\r\n * The generic JsSIP call engine. A `CallEngine` driven by a `SipProvider` for credentials and\r\n * registration policy (SPEC §5.4).\r\n *\r\n * NOTHING here is Zadarma-specific — `grep -ri zadarma` over this directory must return nothing. The\r\n * push socket, the JSONP key exchange and the quirk-timer *values* all live in the provider (S4);\r\n * this engine only reads `provider.registrationMode`, `provider.incomingSignal` and\r\n * `provider.quirks` as data.\r\n *\r\n * Registration policy:\r\n * - `persistent` — provision + register once at start(); the standing UA both receives INVITEs\r\n * and places calls. The normal SIP model; our own infrastructure, later.\r\n * - `on-demand` — register only in response to `incomingSignal` (or to place a call), tear down\r\n * after. Zadarma's PBX ignores a standing registration (SPEC §3.2). The\r\n * no-INVITE teardown and reject-guard timers come from `provider.quirks`.\r\n */\r\nexport class SipCallEngine implements CallEngine {\r\n #state: SoftphoneState = 'stopped';\r\n #ua: JsSipUA | null = null;\r\n /** In-flight `#createUA()`. THE concurrency guard — see `#ensureUA`. */\r\n #uaInFlight: Promise<JsSipUA> | null = null;\r\n #domain = '';\r\n #activeCall: SipEngineCall | null = null;\r\n /**\r\n * The consult leg from `startConsult` (SPEC §5.6), if one is live on this UA right now.\r\n *\r\n * Tracked separately from `#activeCall` so `#maybeTeardownUA` can tell \"both legs are actually gone\"\r\n * apart from \"the main leg ended, but a consult is still up\" — see that method's own comment for the\r\n * bug this exists to prevent.\r\n */\r\n #consultCall: SipEngineCall | null = null;\r\n /** The chosen microphone, or `null` for the browser default. Read fresh by every new leg's\r\n * `mediaConstraints`, and pushed live to `#activeCall` by `setInputDevice`. */\r\n #inputDeviceId: string | null = null;\r\n\r\n readonly #incomingListeners = new Set<(call: EngineCall) => void>();\r\n readonly #stateListeners = new Set<(state: SoftphoneState) => void>();\r\n readonly #errorListeners = new Set<(error: SoftphoneError) => void>();\r\n #signalUnsub: (() => void) | null = null;\r\n #signalConnectionUnsub: (() => void) | null = null;\r\n #noInviteTimer: ReturnType<typeof setTimeout> | undefined;\r\n #rejectGuardUntil = 0;\r\n #signalRefreshAfter = 0;\r\n readonly #errorSentAfter = new Map<SoftphoneErrorCode, number>();\r\n\r\n private readonly now: () => number;\r\n #resolvedFactory: JsSipFactory | null = null;\r\n\r\n constructor(\r\n private readonly provider: SipProvider,\r\n // A factory OR an async loader. The loader form bridges sync construction with the dynamic\r\n // `import('jssip')` (SPEC §10.2) — tests still pass a plain FakeFactory.\r\n private readonly factory: JsSipFactory | (() => JsSipFactory | Promise<JsSipFactory>),\r\n options: { now?: () => number } = {},\r\n ) {\r\n this.now = options.now ?? Date.now;\r\n }\r\n\r\n async #resolveFactory(): Promise<JsSipFactory> {\r\n if (this.#resolvedFactory) return this.#resolvedFactory;\r\n this.#resolvedFactory =\r\n typeof this.factory === 'function' ? await this.factory() : this.factory;\r\n return this.#resolvedFactory;\r\n }\r\n\r\n get state(): SoftphoneState {\r\n return this.#state;\r\n }\r\n\r\n onIncoming(listener: (call: EngineCall) => void): () => void {\r\n this.#incomingListeners.add(listener);\r\n return () => this.#incomingListeners.delete(listener);\r\n }\r\n onStateChange(listener: (state: SoftphoneState) => void): () => void {\r\n this.#stateListeners.add(listener);\r\n return () => this.#stateListeners.delete(listener);\r\n }\r\n onError(listener: (error: SoftphoneError) => void): () => void {\r\n this.#errorListeners.add(listener);\r\n return () => this.#errorListeners.delete(listener);\r\n }\r\n\r\n /**\r\n * Switch the microphone: stored for every FUTURE leg's `mediaConstraints`, and — if a call is live\r\n * right now — pushed to it immediately via `SipEngineCall.swapInputDevice` (SPEC §7 Phase 2, device\r\n * management). `null` resets to the browser's own default device.\r\n */\r\n async setInputDevice(deviceId: string | null): Promise<void> {\r\n this.#inputDeviceId = deviceId;\r\n await this.#activeCall?.swapInputDevice(deviceId);\r\n }\r\n\r\n /**\r\n * Report a failure, at most once per code per `ERROR_DEBOUNCE_MS`.\r\n *\r\n * A failing registrar retries, and one emission per attempt would flood a host's logging and make\r\n * whatever the widget shows flicker. Debounced per CODE rather than globally, so a transport\r\n * failure is never swallowed because a registration failure happened to precede it.\r\n */\r\n #reportError(code: SoftphoneErrorCode, cause?: unknown): void {\r\n this.#emitError(createError(code, cause));\r\n }\r\n\r\n /**\r\n * Emit, debouncing FATAL codes only.\r\n *\r\n * A failing registrar retries, and one emission per attempt would flood a host's logging and make\r\n * whatever the widget shows flicker — so the same fatal code is reported at most once per window,\r\n * per CODE, so a transport failure is never swallowed because a registration failure preceded it.\r\n *\r\n * Non-fatal codes are never debounced: they describe one call each, and two busy tones in a row are\r\n * two facts, not a repeated one.\r\n */\r\n #emitError(error: SoftphoneError): void {\r\n if (error.fatal) {\r\n const now = this.now();\r\n if (now < (this.#errorSentAfter.get(error.code) ?? 0)) return;\r\n this.#errorSentAfter.set(error.code, now + ERROR_DEBOUNCE_MS);\r\n }\r\n for (const listener of [...this.#errorListeners]) listener(error);\r\n }\r\n\r\n async start(): Promise<void> {\r\n this.#setState('starting');\r\n if (this.provider.registrationMode === 'persistent') {\r\n await this.#ensureUA();\r\n this.#setState('online');\r\n } else {\r\n // On-demand: the push socket keeps us \"reachable\" without a standing registration. We only\r\n // register when it signals a call (or when we place one).\r\n const signal = this.provider.incomingSignal;\r\n if (signal) {\r\n await signal.connect();\r\n this.#signalUnsub = signal.onSignal(() => void this.#onIncomingSignal());\r\n /*\r\n * The signal channel IS our reachability, so it drives the reported state.\r\n *\r\n * Without this, `online` meant \"start() was called\" and nothing could ever contradict it —\r\n * the only other state transitions come from a UA that, in on-demand mode, exists solely for\r\n * the duration of a call. A dead push socket therefore looked identical to a healthy one, and\r\n * the phone silently stopped ringing while the widget said it was online.\r\n */\r\n this.#signalConnectionUnsub =\r\n signal.onConnectionChange?.((connected) =>\r\n this.#setState(connected ? 'online' : 'offline'),\r\n ) ?? null;\r\n }\r\n // Optimistic only when the signal cannot report: an existing provider without\r\n // `onConnectionChange` keeps the old behaviour rather than being stuck offline.\r\n this.#setState(signal?.onConnectionChange ? (signal.connected ? 'online' : 'offline') : 'online');\r\n }\r\n }\r\n\r\n async stop(): Promise<void> {\r\n this.#clearNoInviteTimer();\r\n this.#signalUnsub?.();\r\n this.#signalUnsub = null;\r\n this.#signalConnectionUnsub?.();\r\n this.#signalConnectionUnsub = null;\r\n this.provider.incomingSignal?.disconnect();\r\n this.#teardownUA();\r\n this.#setState('stopped');\r\n }\r\n\r\n async placeCall(destination: string, _options?: CallOptions): Promise<EngineCall> {\r\n const ua = await this.#ensureUA();\r\n const session = await this.#callWhenRegistered(ua, this.#buildTarget(destination));\r\n return this.#wrapCall(session);\r\n }\r\n\r\n // ── incoming (on-demand: push → register → INVITE) ────────────────────────\r\n async #onIncomingSignal(): Promise<void> {\r\n // Guard: after a reject, the provider may RE-RING us (Zadarma's return_timeout). Swallow those\r\n // for the quirk window (SPEC §3.4). Persistent providers set no guard, so this is a no-op there.\r\n if (this.now() < this.#rejectGuardUntil) return;\r\n if (this.#activeCall) return; // already on a call\r\n try {\r\n await this.#ensureUA();\r\n // The push is NOT an INVITE (SPEC §3.3): it fires for transfers, retries, hangups. If no INVITE\r\n // arrives within the quirk window, tear the fresh UA down so it doesn't block the next call.\r\n this.#armNoInviteTeardown();\r\n } catch {\r\n this.#teardownUA();\r\n }\r\n }\r\n\r\n #armNoInviteTeardown(): void {\r\n const ms = this.provider.quirks?.noInviteTeardownMs;\r\n if (!ms) return;\r\n this.#clearNoInviteTimer();\r\n // `#onIncomingSignal` (SPEC §3.3) re-arms this on EVERY push, including the generic \"answered\"\r\n // pushes Zadarma sends around unrelated account activity — one landed 300ms after a real transfer\r\n // completed here (live capture, 2026-08-04) and, checking only `#activeCall`, blew away the still-\r\n // live consult leg 5s later with the exact same premature-teardown bug `#maybeTeardownUA` exists to\r\n // prevent (§ its own comment). Same guard, same reason: a consult leg is still \"something on this\r\n // UA,\" even with no `#activeCall`.\r\n this.#noInviteTimer = setTimeout(() => this.#maybeTeardownUA(), ms);\r\n }\r\n /**\r\n * Rebuild the incoming-signal channel with fresh credentials, after a registration failure.\r\n *\r\n * Registration fails when the credentials behind it have gone stale — and for Zadarma those come\r\n * from the same WebRTC key the push socket is holding. Tearing down the UA alone leaves that stale\r\n * key in place, so every subsequent attempt fails the same way and the phone is dead until the page\r\n * is reloaded. Reconnecting re-fetches the key. The legacy hook did exactly this from both of its\r\n * `registrationFailed` handlers.\r\n *\r\n * Debounced, which the legacy was not: a provider failing every attempt would otherwise become a\r\n * tight loop against our backend and theirs.\r\n */\r\n async #refreshSignal(): Promise<void> {\r\n const signal = this.provider.incomingSignal;\r\n if (!signal) return;\r\n if (this.now() < this.#signalRefreshAfter) return;\r\n this.#signalRefreshAfter = this.now() + SIGNAL_REFRESH_DEBOUNCE_MS;\r\n try {\r\n signal.disconnect();\r\n await signal.connect();\r\n } catch {\r\n // Still disconnected, and the signal reports that — the engine surfaces it as `offline`\r\n // rather than claiming a reachability it does not have.\r\n }\r\n }\r\n\r\n #clearNoInviteTimer(): void {\r\n if (this.#noInviteTimer) clearTimeout(this.#noInviteTimer);\r\n this.#noInviteTimer = undefined;\r\n }\r\n\r\n #onNewSession(session: JsSipSession, originator: 'local' | 'remote'): void {\r\n if (originator !== 'remote') return; // our own outbound legs are wired at placeCall\r\n this.#clearNoInviteTimer();\r\n\r\n if (this.#activeCall || this.now() < this.#rejectGuardUntil) {\r\n session.terminate({ status_code: 486, reason_phrase: 'Busy Here' });\r\n return;\r\n }\r\n const call = this.#wrapCall(session);\r\n for (const listener of [...this.#incomingListeners]) listener(call);\r\n }\r\n\r\n // ── UA lifecycle ───────────────────────────────────────────────────────────\r\n /**\r\n * The single UA, created at most once — even under concurrent callers.\r\n *\r\n * **The `#ua` check alone is not a guard**, because everything after it awaits: two callers both\r\n * see `null`, both `provision()`, both build a UA, and both REGISTER the same extension. Zadarma\r\n * rejects the loser, `registrationFailed` tears down, and the survivor may be the one that was\r\n * torn down. The visible symptom is two `get_webphone_data.php` calls at startup and a phone that\r\n * never registers.\r\n *\r\n * Concurrency is the normal case here, not an edge: the push socket can signal twice in a\r\n * heartbeat (SPEC §3.3 — a push is not an INVITE; it fires for transfers, retries and hangups),\r\n * and `placeCall` can land on top of one. So the in-flight promise IS the guard.\r\n */\r\n async #ensureUA(): Promise<JsSipUA> {\r\n if (this.#ua) return this.#ua;\r\n this.#uaInFlight ??= this.#createUA().finally(() => {\r\n this.#uaInFlight = null;\r\n });\r\n return this.#uaInFlight;\r\n }\r\n\r\n async #createUA(): Promise<JsSipUA> {\r\n const factory = await this.#resolveFactory();\r\n\r\n /*\r\n * Report what actually went wrong, not the layer that caught it.\r\n *\r\n * A rejected voice token, a token endpoint that is down, and a carrier returning no credential\r\n * are three problems with three different fixes; the lower layers already raise the precise\r\n * `auth/*` code. Only supply the generic one when nothing more specific is known.\r\n *\r\n * Inline rather than a helper method: an extra async frame is an extra microtask, and the\r\n * no-INVITE teardown is armed the moment this resolves (QUIRK 1).\r\n */\r\n let credentials: SipCredentials;\r\n try {\r\n credentials = await this.provider.provision();\r\n } catch (cause) {\r\n if (isSoftphoneError(cause)) this.#emitError(cause);\r\n else this.#reportError('provider/provision-failed', cause);\r\n throw cause;\r\n }\r\n this.#domain = domainOf(credentials);\r\n const config = this.#uaConfig(credentials, factory);\r\n const ua = factory.createUA(config);\r\n\r\n ua.on('newRTCSession', ({ session, originator }) => this.#onNewSession(session, originator));\r\n ua.on('disconnected', ({ error }) => {\r\n if (!error) return; // a clean close (our own teardown) is not a failure\r\n this.#setState('reconnecting');\r\n this.#reportError('net/transport-failed', error);\r\n });\r\n ua.on('connected', () => this.#setState('online'));\r\n ua.on('registrationFailed', (e) => {\r\n /*\r\n * On Zadarma the usual cause is geo-routing (SPEC §3.1): a registrar in the wrong datacenter\r\n * answers a bare 401 with no auth challenge. The registrar `domain` is what\r\n * `get_webphone_data.php` handed back, so it is the one detail that makes this diagnosable —\r\n * it rides along as the error's `cause` rather than only reaching a console nobody is reading.\r\n */\r\n const detail = e as { cause?: string; response?: { status_code?: number } } | undefined;\r\n this.#reportError('net/registration-failed', {\r\n cause: detail?.cause,\r\n status: detail?.response?.status_code,\r\n domain: this.#domain,\r\n hint: 'A bare 401 with no auth challenge usually means the registrar is in the wrong datacenter (SPEC §3.1).',\r\n });\r\n this.#teardownUA();\r\n void this.#refreshSignal();\r\n });\r\n\r\n this.#ua = ua;\r\n ua.start();\r\n return ua;\r\n }\r\n\r\n #uaConfig(credentials: SipCredentials, factory: JsSipFactory): JsSipUAConfig {\r\n return {\r\n sockets: [factory.createWebSocket(credentials.wsUrl)],\r\n uri: credentials.uri,\r\n authorization_user: credentials.authUser,\r\n password: credentials.password,\r\n register: true,\r\n register_expires: REGISTER_EXPIRES,\r\n connection_recovery_min_interval: 2,\r\n connection_recovery_max_interval: 30,\r\n // Generic — NOT a provider brand string. If a provider ever requires a specific UA header it\r\n // belongs in the provider, not hard-coded here.\r\n // NOT ours to choose: Zadarma refuses to register any client whose User-Agent it does not\r\n // recognise (see ProviderQuirks.userAgent). The provider decides; the engine just sends it.\r\n user_agent: this.provider.quirks?.userAgent ?? 'Coventia Softphone',\r\n };\r\n }\r\n\r\n #teardownUA(): void {\r\n this.#uaInFlight = null;\r\n if (!this.#ua) return;\r\n try {\r\n this.#ua.terminateSessions({ status_code: 487 });\r\n this.#ua.stop();\r\n } catch {\r\n /* already stopped */\r\n }\r\n this.#ua = null;\r\n this.#activeCall = null;\r\n this.#consultCall = null;\r\n }\r\n\r\n /** Place a call once the UA is registered (its 200 OK for REGISTER has arrived). */\r\n /**\r\n * **No `mediaStream`** — JsSIP calls `getUserMedia` itself, which is what opens a real capture.\r\n *\r\n * That is load-bearing, not incidental. Firefox restricts ICE gathering on a page with no active\r\n * capture: it discards every address but the default route's (\"Ignoring addr from interface other\r\n * than …\" in about:webrtc) and can end up gathering NOTHING, so the INVITE goes out with\r\n * `c=IN IP4 0.0.0.0` and no candidates at all. No STUN or TURN server can rescue an offer that\r\n * contains no candidates. The legacy hook passed `mediaConstraints` and nothing else, for what\r\n * turns out to be a very good reason.\r\n */\r\n #callWhenRegistered(ua: JsSipUA, target: string): Promise<JsSipSession> {\r\n const options = { mediaConstraints: buildAudioConstraints(this.#inputDeviceId), pcConfig: PC_CONFIG };\r\n if (ua.isRegistered()) return Promise.resolve(ua.call(target, options));\r\n return new Promise<JsSipSession>((resolve) => {\r\n ua.on('registered', () => resolve(ua.call(target, options)));\r\n });\r\n }\r\n\r\n #wrapCall(session: JsSipSession): SipEngineCall {\r\n const call = new SipEngineCall(session, {\r\n id: nextCallId(),\r\n buildTarget: (t) => this.#buildTarget(t),\r\n placeConsult: (t) => this.#placeConsult(t),\r\n // The guard is tied to the reject ACTION, not to `ended` — a normal hangup must not arm it.\r\n onReject: () => this.#startRejectGuard(),\r\n iceGatheringCapMs: this.provider.quirks?.iceGatheringCapMs ?? DEFAULT_ICE_CAP_MS,\r\n // Inbound answers open their own peer connection, so they need the same ICE servers the\r\n // outbound leg got. Missing here, an ANSWERED call is the one that fails — the half nobody\r\n // tests until a real person picks up.\r\n pcConfig: PC_CONFIG,\r\n // Read fresh at answer() time, not captured now — a device chosen while the phone is still\r\n // ringing must still apply once it is picked up.\r\n getInputDeviceId: () => this.#inputDeviceId,\r\n });\r\n\r\n this.#activeCall = call;\r\n call.onEvent((event) => {\r\n if (event.type === 'ended') {\r\n /*\r\n The outcome of a call, as a code the widget can show: \"Sin respuesta\", \"Llamada rechazada\".\r\n A normal hangup by either side is not an error.\r\n\r\n A MISSED call is the one outcome `reason` cannot carry on its own. The caller CANCELling\r\n while our phone rings is an ordinary `remote` ending, so it would pass in silence — and it\r\n is the case the user is least able to reconstruct, because they never saw the call. JsSIP\r\n sends the same `Canceled` cause when the user abandons their OWN outbound call, so the\r\n direction is what separates a call that was missed from one that was given up on.\r\n */\r\n const missed = call.direction === 'inbound' && event.cause === 'Canceled';\r\n const code = missed ? 'call/missed' : CALL_END_CODES[event.reason];\r\n // The raw cause rides along so a `call/failed` in a host's logs says which failure it was.\r\n if (code) this.#reportError(code, event.cause);\r\n if (this.#activeCall === call) this.#activeCall = null;\r\n this.#maybeTeardownUA();\r\n }\r\n });\r\n return call;\r\n }\r\n\r\n async #placeConsult(target: string): Promise<SipEngineCall> {\r\n const ua = await this.#ensureUA();\r\n const consultSession = ua.call(this.#buildTarget(target), {\r\n mediaConstraints: buildAudioConstraints(this.#inputDeviceId),\r\n pcConfig: PC_CONFIG,\r\n });\r\n const consultCall = new SipEngineCall(consultSession, {\r\n id: nextCallId(),\r\n buildTarget: (x) => this.#buildTarget(x),\r\n placeConsult: () => Promise.reject(new Error('cannot consult from a consult leg')),\r\n iceGatheringCapMs: this.provider.quirks?.iceGatheringCapMs ?? DEFAULT_ICE_CAP_MS,\r\n });\r\n this.#consultCall = consultCall;\r\n consultCall.onEvent((event) => {\r\n if (event.type === 'ended') {\r\n if (this.#consultCall === consultCall) this.#consultCall = null;\r\n this.#maybeTeardownUA();\r\n }\r\n });\r\n return consultCall;\r\n }\r\n\r\n /**\r\n * On-demand registration (SPEC §3.3): the UA is disposable once NOTHING is left on it — but \"the\r\n * main leg ended\" (or \"no INVITE followed this push\") is not the same fact as \"nothing is left.\"\r\n * `completeTransfer()` (SPEC §5.6) ends the main leg via a REFER…Replaces BYE *while the consult leg\r\n * is still what the PBX needs to bridge the caller onto* — tearing the UA down (and with it, EVERY\r\n * session on it, via `terminateSessions`) the instant the main leg's `ended` fires killed that\r\n * consult leg out from under the transfer before the bridge could use it, and the caller was simply\r\n * gone. Two real call sites hit this independently (found live, 2026-08-04): the main leg's own\r\n * `ended` handler, and `#armNoInviteTeardown`'s 5s timer — the latter re-arms on EVERY push,\r\n * including unrelated \"answered\" pushes Zadarma sends around other account activity, so it can fire\r\n * minutes after a transfer with no `#activeCall` and no idea a consult leg is mid-handoff. Both now\r\n * route through here rather than checking `#activeCall` alone.\r\n */\r\n #maybeTeardownUA(): void {\r\n if (this.provider.registrationMode !== 'on-demand') return;\r\n if (this.#activeCall || this.#consultCall) return;\r\n this.#teardownUA();\r\n }\r\n\r\n #startRejectGuard(): void {\r\n const ms = this.provider.quirks?.rejectGuardMs;\r\n if (ms) this.#rejectGuardUntil = this.now() + ms;\r\n }\r\n\r\n #buildTarget(target: string): string {\r\n return target.includes('@') ? target : `sip:${target}@${this.#domain}`;\r\n }\r\n\r\n #setState(state: SoftphoneState): void {\r\n if (state === this.#state) return;\r\n this.#state = state;\r\n for (const listener of [...this.#stateListeners]) listener(state);\r\n }\r\n}\r\n","import type { JsSipFactory, JsSipUA, JsSipUAConfig } from './jssip.js';\r\n\r\n/**\r\n * The REAL JsSipFactory — the only place `jssip` is loaded, via a **dynamic import** so that merely\r\n * importing `@coventia/softphone` never pulls it. jssip is CJS and touches browser globals; a static\r\n * import would break `import` under SSR (SPEC §10.2) and drag ~66 KB into every bundle.\r\n *\r\n * Because the import is async but `JsSipFactory.createUA` is sync, we load jssip once up front via\r\n * `loadJsSipFactory()` and hand back a fully-synchronous factory.\r\n */\r\n\r\ninterface JsSipModule {\r\n UA: new (config: JsSipUAConfig) => JsSipUA;\r\n WebSocketInterface: new (url: string) => unknown;\r\n}\r\n\r\nlet cached: JsSipFactory | null = null;\r\n\r\nexport async function loadJsSipFactory(): Promise<JsSipFactory> {\r\n if (cached) return cached;\r\n const mod = (await import('jssip')) as unknown as { default?: JsSipModule } & Partial<JsSipModule>;\r\n const JsSIP = (mod.default ?? mod) as JsSipModule;\r\n if (!JsSIP?.UA || !JsSIP?.WebSocketInterface) {\r\n throw new Error('jssip: unexpected module shape (no UA / WebSocketInterface)');\r\n }\r\n cached = {\r\n createWebSocket: (wsUrl) => new JsSIP.WebSocketInterface(wsUrl),\r\n createUA: (config) => new JsSIP.UA(config),\r\n };\r\n return cached;\r\n}\r\n","import type {\r\n IncomingSignal,\r\n ProviderQuirks,\r\n RegistrationMode,\r\n SipCredentials,\r\n SipProvider,\r\n} from './provider.js';\r\n\r\n/**\r\n * The Zadarma provider — the FIRST (and, for now, only) `SipProvider`. Everything Zadarma-specific\r\n * that the generic SIP engine (`transports/sip/`) must not know lives here, expressed as data the\r\n * engine reads (SPEC §5.4).\r\n *\r\n * Network I/O is INJECTED (`ZadarmaDeps`) for two reasons: the JSONP exchange and the push socket\r\n * are browser-only, and the quirk tests (SPEC §3, the S4 safety net) must drive this without a\r\n * browser, a network, or a real call.\r\n */\r\n\r\n/**\r\n * The presence heartbeat Zadarma's push gateway expects, and how long before key expiry we rebuild\r\n * the socket. Both are Zadarma's numbers, carried over from the CRM hook that ran them in production.\r\n */\r\nconst PING = { localMs: 30_000, globalMs: 60_000 } as const;\r\nconst KEY_REFRESH_BUFFER_MS = 2 * 60 * 60 * 1000;\r\n\r\n/**\r\n * A rebuild is make-before-break (presence-client-reporting.md §4.1, T8): the replacement socket gets this long\r\n * to connect before it is abandoned and the old one kept. Generous — a rebuild measured ~0.5 s live — but\r\n * bounded, so one stuck handshake cannot wedge every later rebuild behind the overlap guard.\r\n */\r\nconst REBUILD_CONNECT_TIMEOUT_MS = 10_000;\r\n/** After a FAILED rebuild, when to try again. While the tab is hidden the next 20 s background tick also\r\n * retries; this is what keeps a visible tab (whose only other trigger is the 70 h key refresh) from waiting. */\r\nconst REBUILD_RETRY_MS = 30_000;\r\n\r\n/** The Zadarma quirk-timer values (SPEC §3). The engine reads these; it never hard-codes them. */\r\nexport const ZADARMA_QUIRKS: ProviderQuirks = {\r\n // Zadarma's registrar REFUSES to register a WebRTC client with any other User-Agent — a clean 401\r\n // after a valid digest challenge, indistinguishable from a wrong password (§3.1). The old CRM\r\n // hook carried this string; the port dropped it, and registration failed for exactly that reason.\r\n userAgent: 'Zadarma webRTC',\r\n // Zadarma re-rings a declined call for ~30 s (return_timeout). Swallow those (§3.4).\r\n rejectGuardMs: 35_000,\r\n // A push is not an INVITE (§3.3). If none arrives, tear the fresh UA down.\r\n noInviteTeardownMs: 5_000,\r\n // A ceiling on ICE gathering, not a target: JsSIP sends as soon as gathering completes. 3 s leaves\r\n // room for a TURN allocation (whose relay candidates arrive last) while still bounding the ~40 s\r\n // Chrome can spend against an unreachable server. See `capIceGathering`.\r\n iceGatheringCapMs: 3_000,\r\n};\r\n\r\nexport interface ZadarmaKeyResponse {\r\n readonly key: string;\r\n readonly sip: string;\r\n readonly keyExpiresAt?: string;\r\n}\r\n\r\n/** The minimal push-socket surface we consume (Zadarma's Socket.IO v2 gateway). */\r\nexport interface ZadarmaPushSocket {\r\n on(event: 'connect' | 'disconnect' | 'connect_error' | 'update', cb: () => void): void;\r\n /**\r\n * Send to the gateway. Needed for the presence pings — see `PING`.\r\n *\r\n * Its absence is why the extension went offline after a few idle minutes: the socket cannot be\r\n * kept alive by a client that has no way to say anything on it.\r\n */\r\n emit(event: string, payload?: unknown): void;\r\n close(): void;\r\n readonly connected: boolean;\r\n}\r\n\r\nexport interface ZadarmaDeps {\r\n /**\r\n * Fetch `{ key, sip }` from the Coventia backend (authenticated with the voice token upstream).\r\n * The key is a scoped, refreshable credential — NEVER a stored SIP password (SPEC §6.4, §6.5).\r\n */\r\n fetchKey(): Promise<ZadarmaKeyResponse>;\r\n\r\n /**\r\n * Exchange the key for ephemeral SIP credentials. MUST run in the browser: Zadarma geo-routes the\r\n * registrar `domain` (and the matching `pass`) to the caller's region (SPEC §3.1). The returned\r\n * password is ephemeral and must never be persisted.\r\n */\r\n exchangeWebphoneData(key: string, sip: string): Promise<{ pass: string; domain: string }>;\r\n\r\n /** Open the Zadarma push socket for a `(key, sip)`. */\r\n createPushSocket(url: string): ZadarmaPushSocket;\r\n\r\n /**\r\n * OPTIONAL: proactively rebuild the push socket, via `cb`, while the tab is hidden.\r\n *\r\n * The obvious fix — ping more often — does not work: `push-socket-reliability.md` §5.2 confirmed\r\n * empirically that Zadarma's gateway is keyed to the raw Engine.IO ping specifically, and that ping\r\n * dies under background-tab timer throttling no matter how much other traffic the socket carries.\r\n * Rather than trying to keep a fragile internal timer alive, `cb` tears the connection down and\r\n * rebuilds it on a schedule safely inside the failure window (§5.3) — a fresh socket always gets\r\n * its own fresh, reliably-successful first ping cycle.\r\n *\r\n * Returns a teardown function. Omitted (tests, SSR, no `Worker`) and `ZadarmaPushSignal` simply\r\n * does not self-heal in the background — exactly today's behaviour.\r\n */\r\n watchBackgroundReconnect?(cb: () => void): () => void;\r\n}\r\n\r\n/** `wss://ws.zadarma.com/webrtc_pushes?key=…&integrationType=site&sipId=…` */\r\nexport function buildPushUrl(key: string, sip: string): string {\r\n const params = new URLSearchParams({ key, integrationType: 'site', sipId: sip });\r\n return `wss://ws.zadarma.com/webrtc_pushes?${params.toString()}`;\r\n}\r\n\r\n/**\r\n * The push socket as an `IncomingSignal`. Its `connect()` fetches a key and opens the socket; a\r\n * Zadarma `update` becomes an out-of-band incoming signal the engine reacts to (SPEC §3.2–3.3).\r\n *\r\n * The engine deliberately does NOT ring on this signal — it rings on the SIP INVITE. This only tells\r\n * the engine to register a UA so Zadarma will *send* the INVITE.\r\n */\r\nexport class ZadarmaPushSignal implements IncomingSignal {\r\n #socket: ZadarmaPushSocket | null = null;\r\n #connected = false;\r\n #pingTimers: Array<ReturnType<typeof setInterval>> = [];\r\n #refreshTimer: ReturnType<typeof setTimeout> | undefined;\r\n /** The replacement socket of a rebuild in flight, not yet current. */\r\n #candidate: ZadarmaPushSocket | null = null;\r\n /** Abandons the rebuild in flight (closing its candidate). */\r\n #abortRebuild: (() => void) | null = null;\r\n #rebuilding = false;\r\n #retryTimer: ReturnType<typeof setTimeout> | undefined;\r\n #unwatchBackground: (() => void) | undefined;\r\n #closed = false;\r\n readonly #listeners = new Set<() => void>();\r\n readonly #connectionListeners = new Set<(connected: boolean) => void>();\r\n\r\n constructor(private readonly deps: ZadarmaDeps) {}\r\n\r\n async connect(): Promise<void> {\r\n if (this.#socket) return;\r\n this.#closed = false;\r\n // Armed once per signal lifetime, not per reopen — `#reopen()` itself is the callback.\r\n this.#unwatchBackground ??= this.deps.watchBackgroundReconnect?.(() => void this.#reopen());\r\n await this.#open();\r\n }\r\n\r\n async #open(): Promise<void> {\r\n const { key, sip, keyExpiresAt } = await this.deps.fetchKey();\r\n if (this.#closed) return;\r\n\r\n const socket = this.deps.createPushSocket(buildPushUrl(key, sip));\r\n this.#socket = socket;\r\n this.#attach(socket);\r\n this.#armRefresh(keyExpiresAt);\r\n }\r\n\r\n /**\r\n * Wires one socket's events. `this.#socket` is read when an event FIRES, not when it is wired: a socket that\r\n * has been replaced must stop driving connectivity (our own `close()` of it emits `disconnect`, which would\r\n * otherwise flip the state to offline right after a perfectly healthy swap). `update` is the exception — it is\r\n * forwarded from any socket not yet closed, because a push seen twice is harmless (the engine ignores a\r\n * repeated signal mid-call and reuses a UA that already exists) while one seen by neither is a missed call.\r\n *\r\n * With `hooks`, the socket is a rebuild's candidate: its first `connect` or failure goes to the rebuild instead\r\n * of changing the connection state (it is not current yet).\r\n */\r\n #attach(socket: ZadarmaPushSocket, hooks?: { onConnect(): void; onFailure(): void }): void {\r\n socket.on('connect', () => {\r\n if (hooks && socket === this.#candidate) return hooks.onConnect();\r\n if (socket !== this.#socket) return;\r\n this.#setConnected(true);\r\n this.#startPings(socket);\r\n });\r\n socket.on('update', () => this.#emit());\r\n // Both mean the same thing to us: no INVITE can arrive right now.\r\n const lost = (): void => {\r\n if (hooks && socket === this.#candidate) return hooks.onFailure();\r\n if (socket !== this.#socket) return;\r\n this.#stopPings();\r\n this.#setConnected(false);\r\n };\r\n socket.on('disconnect', lost);\r\n socket.on('connect_error', lost);\r\n }\r\n\r\n /**\r\n * Presence pings — **the reason an idle tab keeps receiving calls**.\r\n *\r\n * Zadarma's gateway decides whether the extension is online, and it decides from these. Without\r\n * them the socket goes quiet, Zadarma marks the extension offline within minutes, and INVITEs stop\r\n * being routed — with no error anywhere, because nothing failed. The only visible symptom is that\r\n * the phone silently stops ringing until the page is reloaded.\r\n *\r\n * The names and intervals are Zadarma's, taken from the CRM hook that ran this in production for\r\n * years. They are not ours to tune.\r\n */\r\n #startPings(socket: ZadarmaPushSocket): void {\r\n this.#stopPings();\r\n this.#pingTimers = [\r\n setInterval(() => socket.emit('localPing', {}), PING.localMs),\r\n setInterval(() => socket.emit('globalPing', {}), PING.globalMs),\r\n ];\r\n }\r\n\r\n #stopPings(): void {\r\n for (const t of this.#pingTimers) clearInterval(t);\r\n this.#pingTimers = [];\r\n }\r\n\r\n /**\r\n * Rebuild the socket before the WebRTC key expires.\r\n *\r\n * The key is baked into the socket URL, so socket.io's own reconnection re-presents whatever it was\r\n * given at construction. A tab left open past the key's 72 h lifetime would reconnect forever with\r\n * a dead credential. Absolute, from the backend's `key_expires_at`, so it targets the real expiry\r\n * rather than restarting a countdown on every reconnect.\r\n */\r\n #armRefresh(keyExpiresAt: string | undefined): void {\r\n clearTimeout(this.#refreshTimer);\r\n if (!keyExpiresAt) return;\r\n const ms = new Date(keyExpiresAt).getTime() - Date.now() - KEY_REFRESH_BUFFER_MS;\r\n if (!Number.isFinite(ms) || ms <= 0) return;\r\n this.#refreshTimer = setTimeout(() => void this.#reopen(), ms);\r\n }\r\n\r\n /**\r\n * Replaces the push socket with a fresh one — MAKE-BEFORE-BREAK (presence-client-reporting.md §4.1, T8).\r\n *\r\n * Why it must happen at all: Zadarma's gateway decides the extension is alive from the raw Engine.IO ping, which\r\n * a hidden tab's throttled timers silently stop sending, so while the tab is hidden the socket is rebuilt every\r\n * 20 s (`push-socket-reliability.md` §5.3), and once before the key expires. Why it must not leave a gap: the\r\n * old close-then-reopen reported `connected = false` for the ~0.5 s in between, which took the softphone\r\n * `offline`, tore down the presence socket and left Zadarma with no socket to push an incoming call to.\r\n *\r\n * Now the OLD socket stays up and stays current until the NEW one has actually connected; only then do we swap,\r\n * move the pings and close the old one. A healthy rebuild never changes `connected`. A failed one — the key fetch\r\n * throws, the socket errors, or it has not connected within `REBUILD_CONNECT_TIMEOUT_MS` — keeps the old socket\r\n * and retries, instead of leaving the tab disconnected. Zadarma does not evict the older of two sockets for one\r\n * sip (multi-tab-leader-election.md §1), so the overlap is safe.\r\n */\r\n async #reopen(): Promise<void> {\r\n if (this.#closed || this.#rebuilding) return; // one rebuild at a time: a slow one must not stack with the next tick\r\n\r\n // Nothing to be gapless about — never opened, or an earlier failure left no socket: a plain open.\r\n if (!this.#socket) {\r\n try {\r\n await this.#open();\r\n } catch {\r\n // Still no socket, and `connected` says so: the engine can surface it instead of the phone quietly never\r\n // ringing again.\r\n }\r\n return;\r\n }\r\n\r\n this.#rebuilding = true;\r\n try {\r\n const { key, sip, keyExpiresAt } = await this.deps.fetchKey();\r\n if (this.#closed) return;\r\n const swapped = await new Promise<boolean>((resolve) => {\r\n const socket = this.deps.createPushSocket(buildPushUrl(key, sip));\r\n this.#candidate = socket;\r\n let settled = false;\r\n const finish = (ok: boolean): void => {\r\n if (settled) return;\r\n settled = true;\r\n clearTimeout(timer);\r\n this.#abortRebuild = null;\r\n // A candidate that connected and dropped again in the same breath is not worth swapping in.\r\n if (ok && socket.connected) {\r\n this.#swapIn(socket, keyExpiresAt);\r\n } else {\r\n this.#candidate = null;\r\n socket.close();\r\n }\r\n resolve(ok && socket.connected);\r\n };\r\n const timer = setTimeout(() => finish(false), REBUILD_CONNECT_TIMEOUT_MS);\r\n this.#abortRebuild = () => finish(false);\r\n this.#attach(socket, { onConnect: () => finish(true), onFailure: () => finish(false) });\r\n });\r\n if (!swapped) this.#armRetry();\r\n } catch {\r\n this.#armRetry(); // the key fetch failed: the old socket is untouched\r\n } finally {\r\n this.#rebuilding = false;\r\n }\r\n }\r\n\r\n /**\r\n * The moment of the swap — synchronous, called from the candidate's own `connect` event so nothing can run\r\n * between \"the new socket is up\" and \"it is the current one\". Order matters: `#socket` moves first, so the old\r\n * socket's `disconnect` (our own `close()` of it, just below) is ignored by `#attach`.\r\n */\r\n #swapIn(socket: ZadarmaPushSocket, keyExpiresAt: string | undefined): void {\r\n const previous = this.#socket;\r\n this.#socket = socket;\r\n this.#candidate = null;\r\n clearTimeout(this.#retryTimer);\r\n this.#retryTimer = undefined;\r\n this.#stopPings(); // the old socket's pings end here...\r\n this.#startPings(socket); // ...and the new one's begin\r\n this.#setConnected(true); // a no-op when the old socket was healthy — the whole point\r\n previous?.close();\r\n this.#armRefresh(keyExpiresAt);\r\n }\r\n\r\n #armRetry(): void {\r\n if (this.#closed) return;\r\n clearTimeout(this.#retryTimer);\r\n this.#retryTimer = setTimeout(() => void this.#reopen(), REBUILD_RETRY_MS);\r\n }\r\n\r\n disconnect(): void {\r\n this.#closed = true;\r\n this.#stopPings();\r\n clearTimeout(this.#refreshTimer);\r\n this.#refreshTimer = undefined;\r\n clearTimeout(this.#retryTimer);\r\n this.#retryTimer = undefined;\r\n this.#abortRebuild?.(); // a rebuild in flight closes its candidate and swaps nothing in\r\n this.#unwatchBackground?.();\r\n this.#unwatchBackground = undefined;\r\n this.#socket?.close();\r\n this.#socket = null;\r\n this.#setConnected(false);\r\n }\r\n\r\n onSignal(listener: () => void): () => void {\r\n this.#listeners.add(listener);\r\n return () => this.#listeners.delete(listener);\r\n }\r\n\r\n onConnectionChange(listener: (connected: boolean) => void): () => void {\r\n this.#connectionListeners.add(listener);\r\n return () => this.#connectionListeners.delete(listener);\r\n }\r\n\r\n get connected(): boolean {\r\n return this.#connected;\r\n }\r\n\r\n #setConnected(connected: boolean): void {\r\n if (this.#connected === connected) return;\r\n this.#connected = connected;\r\n for (const listener of [...this.#connectionListeners]) listener(connected);\r\n }\r\n\r\n #emit(): void {\r\n for (const listener of [...this.#listeners]) listener();\r\n }\r\n}\r\n\r\nexport class ZadarmaProvider implements SipProvider {\r\n readonly name = 'zadarma';\r\n // Zadarma's PBX ignores a standing registration — it routes INVITEs only to a UA that registered\r\n // in response to a push (SPEC §3.2). Hence on-demand.\r\n readonly registrationMode: RegistrationMode = 'on-demand';\r\n readonly quirks: ProviderQuirks = ZADARMA_QUIRKS;\r\n readonly incomingSignal: IncomingSignal;\r\n\r\n constructor(private readonly deps: ZadarmaDeps) {\r\n this.incomingSignal = new ZadarmaPushSignal(deps);\r\n }\r\n\r\n /**\r\n * Per-registration credentialing: fetch a scoped key, then exchange it in the browser for\r\n * ephemeral SIP creds. The key fetch is cheap and backend-cached; doing it here (rather than\r\n * sharing state with the push signal) keeps the two paths independent.\r\n */\r\n async provision(): Promise<SipCredentials> {\r\n const { key, sip } = await this.deps.fetchKey();\r\n const { pass, domain } = await this.deps.exchangeWebphoneData(key, sip);\r\n return {\r\n uri: `sip:${sip}@${domain}`,\r\n authUser: sip,\r\n password: pass,\r\n wsUrl: `wss://${domain}:4443`,\r\n };\r\n }\r\n}\r\n","import type { TokenSource } from '../../core/types.js';\r\nimport { invalidateToken } from '../../core/token-cache.js';\r\nimport { createError } from '../../core/errors.js';\r\nimport type { ZadarmaDeps, ZadarmaKeyResponse, ZadarmaPushSocket } from './zadarma.js';\r\n\r\n/** The key fetch was refused. Distinct so only THIS failure triggers a token re-mint. */\r\nclass UnauthorizedKeyError extends Error {}\r\n\r\n/**\r\n * The REAL, browser-only Zadarma network deps: the backend key fetch, the JSONP credential exchange,\r\n * and the Socket.IO v2 push socket. Isolated here (not in zadarma.ts) so the provider stays\r\n * unit-testable and so these browser globals never enter the SSR import path — `socket.io-client` is\r\n * dynamically imported, and every function touches `window`/`document`/`fetch` only when called\r\n * (SPEC §10.2).\r\n *\r\n * Ported from the legacy `useSipUA.js` / `useZadarmaWebrtcLogin.js`, with the geo-routing rationale\r\n * intact (SPEC §3.1).\r\n */\r\n\r\nexport interface ZadarmaBrowserConfig {\r\n /** Coventia backend base URL, e.g. `https://api.coventia.es`. */\r\n readonly apiBase: string;\r\n /** Provides the auth token for the backend key fetch. */\r\n readonly tokenSource: TokenSource;\r\n /** Selected organization id, appended to the key request. */\r\n readonly organizationId?: string | undefined;\r\n}\r\n\r\n/**\r\n * Fetch `{ key, sip }` from the Coventia backend.\r\n *\r\n * Today this targets the existing `/phone/webrtc/key`, authenticated with the CRM's own session.\r\n * Phase 2 moves it to `/api/v1/phone/webrtc/key`, authenticated with a voice token and CORS-gated —\r\n * which is what lets a PARTNER's browser call it from their origin (SPEC §6.6, hop ③). Only the URL\r\n * and the credential change; this function's shape does not, which is the point.\r\n *\r\n * The key is a scoped, refreshable credential — never a stored SIP password (SPEC §6.4).\r\n */\r\nexport function makeFetchKey(config: ZadarmaBrowserConfig): () => Promise<ZadarmaKeyResponse> {\r\n const attempt = async (): Promise<ZadarmaKeyResponse> => {\r\n // The host's own endpoint failing is a DIFFERENT failure from us refusing their token, and the\r\n // fix differs too — \"your broker is down\" vs \"mint a fresh one\". Flattening both into one code\r\n // would send an integrator to the wrong place.\r\n let token: string;\r\n try {\r\n ({ token } = await config.tokenSource.getToken());\r\n } catch (cause) {\r\n throw createError('auth/token-fetch-failed', cause);\r\n }\r\n const qs = config.organizationId\r\n ? `?organization_id=${encodeURIComponent(config.organizationId)}`\r\n : '';\r\n const res = await fetch(`${config.apiBase}/phone/webrtc/key${qs}`, {\r\n headers: { Authorization: token },\r\n });\r\n if (!res.ok) {\r\n const body = (await res.json().catch(() => null)) as { message?: string } | null;\r\n const message = body?.message ?? `webrtc/key HTTP ${res.status}`;\r\n throw res.status === 401 ? new UnauthorizedKeyError(message) : new Error(message);\r\n }\r\n const data = (await res.json()) as {\r\n key: string;\r\n sip: string;\r\n key_expires_at?: string;\r\n };\r\n return {\r\n key: data.key,\r\n sip: data.sip,\r\n ...(data.key_expires_at ? { keyExpiresAt: data.key_expires_at } : {}),\r\n };\r\n };\r\n\r\n /*\r\n * Retry once on 401 with a fresh token — same reasoning as `createGet`, and it matters more here:\r\n * this runs per registration, so a token revoked mid-session would otherwise block every call\r\n * until its TTL elapsed.\r\n */\r\n return async () => {\r\n try {\r\n return await attempt();\r\n } catch (error) {\r\n if (!(error instanceof UnauthorizedKeyError)) throw error;\r\n invalidateToken(config.tokenSource);\r\n try {\r\n return await attempt();\r\n } catch (retryError) {\r\n // Refused twice, the second time with a freshly minted token — so the credential itself is\r\n // rejected, not merely stale.\r\n if (retryError instanceof UnauthorizedKeyError) {\r\n throw createError('auth/token-rejected', retryError);\r\n }\r\n throw retryError;\r\n }\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * Exchange the key for `{ pass, domain }` via Zadarma's `get_webphone_data.php`.\r\n *\r\n * **MUST run in the browser** (SPEC §3.1) — this is the load-bearing constraint and it has not\r\n * changed: Zadarma geo-routes the registrar `domain`, and the `pass` that matches it, to the\r\n * datacenter nearest the *caller's* IP. Called server-side it pins every user to the backend's\r\n * region and REGISTER is refused.\r\n *\r\n * **`fetch`, not a `<script>` tag.** The endpoint is JSONP-shaped — it always answers\r\n * `callbackName({...})`, with no raw-JSON mode — but it now sends `Access-Control-Allow-Origin: *`,\r\n * so the response is readable and the wrapper is just text to strip. Verified against the live\r\n * endpoint before switching.\r\n *\r\n * The old `<script>` injection worked, but cost three things:\r\n * - **A console warning on every call.** The response is served as `application/json`, and a\r\n * classic script with a non-JS MIME type warns in Firefox (\"loaded despite its MIME type…\").\r\n * It executes, so it was harmless — but it is noise in a console that should stay readable, and\r\n * nothing on our side could silence it.\r\n * - **A CSP demand we should not make.** A `<script src>` to a third-party origin needs\r\n * `script-src https://api.zadarma.com` — permission to EXECUTE their code. `fetch` needs\r\n * `connect-src`, which is permission to TALK to them. For a reseller embedding this SDK that is\r\n * a materially smaller ask (SPEC §7, Phase 3: we publish the CSP directives).\r\n * - **A global.** A `window[cbName]` per exchange, plus a `<script>` in `<head>` to clean up.\r\n *\r\n * What we keep is real error handling: an HTTP status, a network failure and a timeout are now\r\n * distinguishable, where a `<script>` could only ever report \"onerror\".\r\n */\r\nexport async function exchangeWebphoneData(\r\n key: string,\r\n sip: string,\r\n timeoutMs = 10_000,\r\n): Promise<{ pass: string; domain: string }> {\r\n const params = new URLSearchParams({\r\n // Still sent: the endpoint wraps its response either way, and naming the callback keeps the\r\n // wrapper predictable rather than relying on their default.\r\n jsonpCallback: 'cv',\r\n integrationType: 'site',\r\n key,\r\n sipId: sip,\r\n });\r\n\r\n let res: Response;\r\n try {\r\n res = await fetch(\r\n `https://api.zadarma.com/sys/webrtc/get_webphone_data.php?${params.toString()}`,\r\n { signal: AbortSignal.timeout(timeoutMs) },\r\n );\r\n } catch (cause) {\r\n const timedOut = cause instanceof DOMException && cause.name === 'TimeoutError';\r\n throw new Error(timedOut ? 'get_webphone_data timed out' : 'get_webphone_data network error', {\r\n cause,\r\n });\r\n }\r\n if (!res.ok) throw new Error(`get_webphone_data HTTP ${res.status}`);\r\n\r\n const data = parseJsonp(await res.text());\r\n if (!data || data.error || !data.pass || !data.domain) {\r\n throw new Error(data?.error?.content ?? 'get_webphone_data: missing SIP credentials');\r\n }\r\n return { pass: data.pass, domain: data.domain };\r\n}\r\n\r\ninterface WebphoneData {\r\n error?: { content?: string };\r\n pass?: string;\r\n domain?: string;\r\n}\r\n\r\n/**\r\n * `cv({...})` → the object.\r\n *\r\n * Sliced between the FIRST `(` and the LAST `)` rather than matched against the callback name we\r\n * sent: the endpoint ignores an empty `jsonpCallback` and falls back to its own literal\r\n * `jsonpCallback(...)`, so trusting the name we asked for would break on a response we can see it\r\n * actually returns.\r\n */\r\nfunction parseJsonp(text: string): WebphoneData | null {\r\n const open = text.indexOf('(');\r\n const close = text.lastIndexOf(')');\r\n const body = open !== -1 && close > open ? text.slice(open + 1, close) : text;\r\n try {\r\n return JSON.parse(body) as WebphoneData;\r\n } catch {\r\n return null;\r\n }\r\n}\r\n\r\n/**\r\n * Open the Zadarma push socket (Socket.IO **v2** — their gateway is v2, hence the pinned dependency).\r\n * `socket.io-client` is dynamically imported so it never enters the SSR bundle.\r\n *\r\n * Note: because the ZadarmaPushSocket surface is synchronous but the socket.io import is async, the\r\n * returned wrapper connects lazily and buffers `on()` registrations until the client is ready.\r\n */\r\nexport function createPushSocket(url: string): ZadarmaPushSocket {\r\n const handlers = new Map<string, Array<() => void>>();\r\n let client: {\r\n on(e: string, cb: () => void): void;\r\n emit(e: string, payload?: unknown): void;\r\n close(): void;\r\n connected?: boolean;\r\n } | null = null;\r\n let closed = false;\r\n\r\n void (async () => {\r\n // A LITERAL specifier, and it must stay one. This was `const s = 'socket.io-client-v2'` behind an\r\n // indirection to dodge TS7016 on an untyped module — which no bundler can statically analyse, so\r\n // the browser got a bare specifier, the push socket never connected, `start()` rejected, and the\r\n // softphone never reached `online`. The types now come from `src/socket-io.d.ts`, which is what\r\n // should have solved it in the first place (`jssip` was always a literal and always worked).\r\n const mod = await import('socket.io-client-v2');\r\n if (closed) return;\r\n // v2's connector arrives as `default` under ESM interop and as `io` when it does not.\r\n const connect = mod.default ?? mod.io;\r\n if (!connect) throw new Error('socket.io-client-v2: no connect export');\r\n const c = connect(url, {\r\n transports: ['websocket'],\r\n reconnection: true,\r\n // The push socket drops on a schedule under background-tab timer throttling, NOT from network\r\n // flakiness — every reconnect observed succeeds on the first attempt (push-socket-reliability.md\r\n // §2.3, §5.1). There is nothing to back off from on that first attempt, so waiting the old 10s\r\n // before retrying was pure, uncontested exposure — an inbound call landing in that window is\r\n // lost, unqueued (Zadarma's gateway speaks EIO3, no offline redelivery). `reconnectionDelayMax`\r\n // stays as the ceiling for a genuinely down gateway, where backing off still matters.\r\n reconnectionDelay: 300,\r\n reconnectionDelayMax: 30_000,\r\n });\r\n client = c;\r\n for (const [event, cbs] of handlers) for (const cb of cbs) c!.on(event, cb);\r\n })();\r\n\r\n return {\r\n on(event, cb) {\r\n const list = handlers.get(event) ?? [];\r\n list.push(cb);\r\n handlers.set(event, list);\r\n client?.on(event, cb);\r\n },\r\n emit(event, payload) {\r\n // Dropped rather than queued when the client is not up yet: every caller is a periodic ping,\r\n // so the next one is seconds away and a replayed backlog would be worse than a missed beat.\r\n client?.emit(event, payload);\r\n },\r\n close() {\r\n closed = true;\r\n client?.close();\r\n },\r\n get connected() {\r\n return client?.connected ?? false;\r\n },\r\n };\r\n}\r\n\r\n/** How long the tab can stay hidden before we proactively rebuild the socket. See `watchBackgroundReconnect`. */\r\nconst BACKGROUND_RECONNECT_MS = 20_000;\r\n\r\n/**\r\n * The Worker's entire job: an alarm clock, nothing else. It owns no socket state and knows nothing\r\n * about Zadarma — Chrome does not apply background-tab timer throttling to Worker threads, only to\r\n * the page's own JS context, so ticking here stays reliable exactly where `setInterval` on the main\r\n * thread was proven not to be (`push-socket-reliability.md` §5.2, §5.3). `start`/`stop` rather than\r\n * a fire-and-forget interval so the SAME worker can be reused across every hidden/visible cycle in a\r\n * session instead of being spun up and torn down each time.\r\n */\r\nconst HEARTBEAT_WORKER_SOURCE = `\r\n let timer;\r\n self.onmessage = (e) => {\r\n if (e.data.type === 'start') timer = setInterval(() => self.postMessage('tick'), e.data.intervalMs);\r\n else if (e.data.type === 'stop') { clearInterval(timer); timer = undefined; }\r\n };\r\n`;\r\n\r\n/**\r\n * Proactively rebuilds the push socket, via `cb`, on a schedule while the tab is hidden — the real\r\n * implementation of the optional `ZadarmaDeps` seam (`zadarma.ts`).\r\n *\r\n * **Constructed lazily, only when called** (SPEC §10.3 — no browser globals at module scope): safe\r\n * to import on a server, and a host that never calls it (tests, an environment with no `Worker`)\r\n * pays nothing.\r\n */\r\nexport function watchBackgroundReconnect(cb: () => void): () => void {\r\n if (typeof document === 'undefined' || typeof Worker === 'undefined') return () => {};\r\n\r\n let worker: Worker | null = null;\r\n let blobUrl: string | null = null;\r\n\r\n const ensureWorker = (): Worker => {\r\n if (!worker) {\r\n blobUrl = URL.createObjectURL(new Blob([HEARTBEAT_WORKER_SOURCE], { type: 'application/javascript' }));\r\n worker = new Worker(blobUrl);\r\n worker.onmessage = (e: MessageEvent) => {\r\n if (e.data === 'tick') cb();\r\n };\r\n }\r\n return worker;\r\n };\r\n\r\n const onVisibilityChange = (): void => {\r\n if (document.visibilityState === 'hidden') {\r\n ensureWorker().postMessage({ type: 'start', intervalMs: BACKGROUND_RECONNECT_MS });\r\n } else {\r\n worker?.postMessage({ type: 'stop' });\r\n }\r\n };\r\n\r\n document.addEventListener('visibilitychange', onVisibilityChange);\r\n // A signal can `connect()` while the tab is already hidden (e.g. a backgrounded pre-loaded tab).\r\n if (document.visibilityState === 'hidden') onVisibilityChange();\r\n\r\n return () => {\r\n document.removeEventListener('visibilitychange', onVisibilityChange);\r\n worker?.postMessage({ type: 'stop' });\r\n worker?.terminate();\r\n if (blobUrl) URL.revokeObjectURL(blobUrl);\r\n };\r\n}\r\n\r\n/** Assemble the real browser deps for a `ZadarmaProvider`. */\r\nexport function makeZadarmaBrowserDeps(config: ZadarmaBrowserConfig): ZadarmaDeps {\r\n return {\r\n fetchKey: makeFetchKey(config),\r\n exchangeWebphoneData: (key, sip) => exchangeWebphoneData(key, sip),\r\n createPushSocket,\r\n watchBackgroundReconnect,\r\n };\r\n}\r\n","import { SipCallEngine } from './sip/ua.js';\r\nimport { loadJsSipFactory } from './sip/real-factory.js';\r\nimport { ZadarmaProvider } from './providers/zadarma.js';\r\nimport { makeZadarmaBrowserDeps, type ZadarmaBrowserConfig } from './providers/zadarma-browser.js';\r\nimport { cachedTokenSource } from '../core/token-cache.js';\r\nimport type { CallEngine } from '../core/engine.js';\r\n\r\n/**\r\n * Assemble a production Zadarma call engine: the real browser deps (backend key fetch + JSONP\r\n * exchange + push socket) → `ZadarmaProvider` → `SipCallEngine`, with jssip loaded lazily via a\r\n * dynamic import (SPEC §10.2 — importing the package stays SSR-safe).\r\n *\r\n * This is the real thing the app and the e2e smoke test use. It is intentionally NOT the automatic\r\n * default of `createSoftphone` yet — the app wires it explicitly in S9, and the voice-token endpoint\r\n * migration is Phase 2 (SPEC §6).\r\n */\r\nexport function createZadarmaEngine(config: ZadarmaBrowserConfig): CallEngine {\r\n // Cached here as well as in `createSoftphone`: a host passes the same `TokenSource` object to both,\r\n // and `cachedTokenSource` is keyed on that object — so the two share ONE token rather than minting\r\n // one each. Without this the WebRTC-key fetch, which runs per registration, would be uncached.\r\n const provider = new ZadarmaProvider(\r\n makeZadarmaBrowserDeps({ ...config, tokenSource: cachedTokenSource(config.tokenSource) }),\r\n );\r\n return new SipCallEngine(provider, loadJsSipFactory);\r\n}\r\n"]}
|
|
@@ -51,6 +51,20 @@ function useActiveCall() {
|
|
|
51
51
|
useCallback(() => phone.activeCall, [phone])
|
|
52
52
|
);
|
|
53
53
|
}
|
|
54
|
+
function usePresence(active) {
|
|
55
|
+
const phone = useSoftphone();
|
|
56
|
+
return useSyncExternalStore(
|
|
57
|
+
useCallback(
|
|
58
|
+
(cb) => {
|
|
59
|
+
if (!active) return () => {
|
|
60
|
+
};
|
|
61
|
+
return phone.on("presenceChanged" /* PresenceChanged */, () => cb());
|
|
62
|
+
},
|
|
63
|
+
[phone, active]
|
|
64
|
+
),
|
|
65
|
+
useCallback(() => phone.presence, [phone])
|
|
66
|
+
);
|
|
67
|
+
}
|
|
54
68
|
var CALL_EVENTS = [
|
|
55
69
|
"stateChanged" /* StateChanged */,
|
|
56
70
|
"muted" /* Muted */,
|
|
@@ -124,6 +138,6 @@ function useDevices() {
|
|
|
124
138
|
return useSyncExternalStore(subscribe, getSnapshot);
|
|
125
139
|
}
|
|
126
140
|
|
|
127
|
-
export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, useSoftphone, useSoftphoneState };
|
|
128
|
-
//# sourceMappingURL=chunk-
|
|
129
|
-
//# sourceMappingURL=chunk-
|
|
141
|
+
export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, usePresence, useSoftphone, useSoftphoneState };
|
|
142
|
+
//# sourceMappingURL=chunk-LMJXX4YM.js.map
|
|
143
|
+
//# sourceMappingURL=chunk-LMJXX4YM.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/react/context.tsx","../src/react/hooks.ts"],"names":[],"mappings":"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","file":"chunk-LMJXX4YM.js","sourcesContent":["import { createContext, useContext, type ReactNode } from 'react';\r\nimport type { Softphone } from '../core/softphone.js';\r\n\r\n/**\r\n * The provider takes an INSTANCE the host constructed — not config props.\r\n *\r\n * This is LiveKit's hard-won lesson (SPEC §5.1): they are deprecating their config-props\r\n * `<LiveKitRoom>` in favour of a provider that accepts an instance. Holding the `Softphone` above the\r\n * React tree lets the app own the connection, survive route changes and unmounts without dropping a\r\n * call, and unit-test the engine with no React at all. Config-props would rebuild the engine on\r\n * every prop change — catastrophic for something holding a live `RTCPeerConnection`.\r\n */\r\nconst SoftphoneContext = createContext<Softphone | null>(null);\r\n\r\nexport interface SoftphoneProviderProps {\r\n readonly softphone: Softphone;\r\n readonly children: ReactNode;\r\n}\r\n\r\nexport function SoftphoneProvider({ softphone, children }: SoftphoneProviderProps) {\r\n return <SoftphoneContext.Provider value={softphone}>{children}</SoftphoneContext.Provider>;\r\n}\r\n\r\n/** The `Softphone` instance from context. Stable — does not re-render on state changes. */\r\nexport function useSoftphone(): Softphone {\r\n const softphone = useContext(SoftphoneContext);\r\n if (!softphone) {\r\n throw new Error('useSoftphone() must be used within a <SoftphoneProvider>.');\r\n }\r\n return softphone;\r\n}\r\n","// Built-in, no shim: the peer floor is React 18 (SPEC §2.7), which ships useSyncExternalStore.\r\nimport { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport { useSoftphone } from './context.js';\r\nimport { SoftphoneEvent, CallEvent } from '../core/events.js';\r\nimport type { Call, CallState } from '../core/call.js';\r\nimport type { PresenceEntry, RemoteParty, SoftphoneState } from '../core/types.js';\r\nimport type { AudioDevices } from '../core/softphone.js';\r\nimport type { FatalSoftphoneError } from '../core/errors.js';\r\n\r\n/** Reactive softphone lifecycle state (`stopped` → `online` → …). */\r\n/**\r\n * The reason the phone cannot currently be used, or `null`.\r\n *\r\n * Reads `softphone.lastError`, which is STATE rather than only an event — the widget is usually\r\n * opened *after* something broke, and a listener mounted at that point would have missed the event\r\n * entirely and rendered a dialpad with no explanation.\r\n *\r\n * Re-reads on both channels because both can change it: `Error` sets it, and `StateChanged` clears it\r\n * on reaching `online`. Subscribing to only the first would leave a stale gate on screen after the\r\n * phone recovered.\r\n */\r\nexport function useLastError(): FatalSoftphoneError | null {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n const offError = phone.on(SoftphoneEvent.Error, () => cb());\r\n const offState = phone.on(SoftphoneEvent.StateChanged, () => cb());\r\n return () => {\r\n offError();\r\n offState();\r\n };\r\n },\r\n [phone],\r\n ),\r\n useCallback(() => phone.lastError, [phone]),\r\n );\r\n}\r\n\r\nexport function useSoftphoneState(): SoftphoneState {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.StateChanged, () => cb()), [phone]),\r\n useCallback(() => phone.state, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * Whether THIS tab is the one allowed to be reachable (`multi-tab-leader-election.md`). `false` by\r\n * default and while waiting for another open tab to release it — show your own \"active in another\r\n * tab\" state for that duration; the `/ui` widget already does.\r\n */\r\nexport function useIsLeader(): boolean {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.LeaderChanged, () => cb()), [phone]),\r\n useCallback(() => phone.isLeader, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * The current call, or null. Re-renders when it changes — inbound, outbound, or ended.\r\n *\r\n * `phone.activeCall` returns a stable reference for a given call, so it is a safe snapshot.\r\n */\r\nexport function useActiveCall(): Call | null {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback((cb: () => void) => phone.on(SoftphoneEvent.ActiveCallChanged, () => cb()), [phone]),\r\n useCallback(() => phone.activeCall, [phone]),\r\n );\r\n}\r\n\r\n/**\r\n * The live agent-presence list (`phone.presence`, Phase B) — reactive on `SoftphoneEvent.\r\n * PresenceChanged`. `active` gates the SUBSCRIPTION only, not the underlying connection: the socket\r\n * itself stays governed entirely by `online && isLeader` inside the softphone (spec's own\r\n * always-connected decision), so passing `false` here does not close it — it only stops this\r\n * particular component from re-rendering on every update while it has no reason to care, e.g. the\r\n * transfer step being closed (spec §4.5: \"subscribe when the step opens... unsubscribe when it\r\n * closes\"). `phone.presence` is still read fresh on every call, so flipping `active` back to `true`\r\n * never shows a stale snapshot.\r\n */\r\nexport function usePresence(active: boolean): readonly PresenceEntry[] {\r\n const phone = useSoftphone();\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n if (!active) return () => {};\r\n return phone.on(SoftphoneEvent.PresenceChanged, () => cb());\r\n },\r\n [phone, active],\r\n ),\r\n useCallback(() => phone.presence, [phone]),\r\n );\r\n}\r\n\r\n/** An immutable snapshot of a call's reactive fields. */\r\nexport interface CallView {\r\n readonly state: CallState;\r\n readonly isMuted: boolean;\r\n readonly isOnHold: boolean;\r\n readonly remote: RemoteParty;\r\n readonly remoteStream: MediaStream | null;\r\n readonly answeredAt: Date | null;\r\n}\r\n\r\nconst CALL_EVENTS: readonly CallEvent[] = [\r\n CallEvent.StateChanged,\r\n CallEvent.Muted,\r\n CallEvent.Held,\r\n CallEvent.Unheld,\r\n CallEvent.RemotePartyUpdated,\r\n CallEvent.RemoteStream,\r\n CallEvent.Ended,\r\n];\r\n\r\n/**\r\n * Reactive view of a specific call. Re-renders only when one of the call's own events fires — no\r\n * polling, no re-render storms.\r\n *\r\n * The snapshot is CACHED and invalidated only on an event, so `useSyncExternalStore` gets a stable\r\n * reference between changes (returning a fresh object every render would infinite-loop).\r\n */\r\nexport function useCall(call: Call | null): CallView | null {\r\n const cache = useRef<{ call: Call; view: CallView } | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (cb: () => void) => {\r\n if (!call) return () => {};\r\n const unsubs = CALL_EVENTS.map((event) =>\r\n call.on(event, () => {\r\n cache.current = null; // invalidate; getSnapshot rebuilds on the next read\r\n cb();\r\n }),\r\n );\r\n return () => {\r\n for (const off of unsubs) off();\r\n };\r\n },\r\n [call],\r\n );\r\n\r\n const getSnapshot = useCallback((): CallView | null => {\r\n if (!call) return null;\r\n if (cache.current?.call === call) return cache.current.view;\r\n const view: CallView = {\r\n state: call.state,\r\n isMuted: call.isMuted,\r\n isOnHold: call.isOnHold,\r\n remote: call.remote,\r\n remoteStream: call.remoteStream,\r\n answeredAt: call.answeredAt,\r\n };\r\n cache.current = { call, view };\r\n return view;\r\n }, [call]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n\r\n/**\r\n * The audio device surface (`phone.audio`) — reactive. Re-renders on `SoftphoneEvent.DeviceChange`: a\r\n * device plugged/unplugged, or a selection changed (SPEC §7 Phase 2).\r\n *\r\n * The snapshot is CACHED and invalidated only on that event, same reasoning as `useCall`'s own\r\n * `CallView`: `phone.audio` is ONE stable object whose fields mutate in place, so returning it\r\n * directly would never change by reference and `useSyncExternalStore` would never re-render. A fresh\r\n * object, rebuilt only when something actually changed, is what makes this reactive without polling.\r\n */\r\nexport function useDevices(): AudioDevices {\r\n const phone = useSoftphone();\r\n const cache = useRef<{ audio: AudioDevices; view: AudioDevices } | null>(null);\r\n\r\n const subscribe = useCallback(\r\n (cb: () => void) =>\r\n phone.on(SoftphoneEvent.DeviceChange, () => {\r\n cache.current = null; // invalidate; getSnapshot rebuilds on the next read\r\n cb();\r\n }),\r\n [phone],\r\n );\r\n\r\n const getSnapshot = useCallback((): AudioDevices => {\r\n const audio = phone.audio;\r\n if (cache.current?.audio === audio) return cache.current.view;\r\n const view: AudioDevices = {\r\n inputs: audio.inputs,\r\n outputs: audio.outputs,\r\n currentInputId: audio.currentInputId,\r\n currentOutputId: audio.currentOutputId,\r\n canSelectOutput: audio.canSelectOutput,\r\n setInput: (deviceId) => audio.setInput(deviceId),\r\n setOutput: (deviceId) => audio.setOutput(deviceId),\r\n setRingtoneOutput: (deviceId) => audio.setRingtoneOutput(deviceId),\r\n refresh: () => audio.refresh(),\r\n };\r\n cache.current = { audio, view };\r\n return view;\r\n }, [phone]);\r\n\r\n return useSyncExternalStore(subscribe, getSnapshot);\r\n}\r\n"]}
|