@coventia/softphone 0.4.0 → 0.5.0

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.
Files changed (57) hide show
  1. package/dist/{call-C6YpT6uR.d.cts → call-FqcvDg1t.d.cts} +105 -12
  2. package/dist/{call-C6YpT6uR.d.ts → call-FqcvDg1t.d.ts} +105 -12
  3. package/dist/{chunk-EYEDPD7Y.js → chunk-6RR6G35E.js} +92 -17
  4. package/dist/chunk-6RR6G35E.js.map +1 -0
  5. package/dist/{chunk-2ZSOGGPO.js → chunk-FRXHEHQA.js} +15 -2
  6. package/dist/chunk-FRXHEHQA.js.map +1 -0
  7. package/dist/{chunk-UHNLEBBO.js → chunk-I3RSC52R.js} +28 -3
  8. package/dist/chunk-I3RSC52R.js.map +1 -0
  9. package/dist/chunk-M76SAXXM.js +13 -0
  10. package/dist/chunk-M76SAXXM.js.map +1 -0
  11. package/dist/{chunk-7R6LOOAI.js → chunk-N4PLEYFJ.js} +4 -2
  12. package/dist/chunk-N4PLEYFJ.js.map +1 -0
  13. package/dist/element/index.d.ts +142 -6
  14. package/dist/element/index.js +138 -43
  15. package/dist/element/index.js.map +1 -1
  16. package/dist/{engine-DPft99Wj.d.ts → engine-CsNRD7B7.d.ts} +1 -1
  17. package/dist/{engine-CDdaSojx.d.cts → engine-CzOCrfY9.d.cts} +1 -1
  18. package/dist/index.cjs +579 -47
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +5 -5
  21. package/dist/index.d.ts +5 -5
  22. package/dist/index.js +447 -37
  23. package/dist/index.js.map +1 -1
  24. package/dist/react/index.cjs +26 -0
  25. package/dist/react/index.cjs.map +1 -1
  26. package/dist/react/index.d.cts +24 -5
  27. package/dist/react/index.d.ts +24 -5
  28. package/dist/react/index.js +2 -2
  29. package/dist/{softphone-CY5d2WWY.d.cts → softphone-By9sizlV.d.ts} +42 -3
  30. package/dist/{softphone-DXgmTyrX.d.ts → softphone-JZvJdzU3.d.cts} +42 -3
  31. package/dist/{sources-ONYQW733.js → sources-QL2LRZVH.js} +19 -2
  32. package/dist/sources-QL2LRZVH.js.map +1 -0
  33. package/dist/testing/index.cjs +13 -0
  34. package/dist/testing/index.cjs.map +1 -1
  35. package/dist/testing/index.d.cts +2 -2
  36. package/dist/testing/index.d.ts +2 -2
  37. package/dist/testing/index.js +1 -1
  38. package/dist/ui/index.cjs +92 -2
  39. package/dist/ui/index.cjs.map +1 -1
  40. package/dist/ui/index.d.cts +2 -2
  41. package/dist/ui/index.d.ts +2 -2
  42. package/dist/ui/index.js +69 -4
  43. package/dist/ui/index.js.map +1 -1
  44. package/dist/zadarma/index.cjs +111 -14
  45. package/dist/zadarma/index.cjs.map +1 -1
  46. package/dist/zadarma/index.d.cts +5 -6
  47. package/dist/zadarma/index.d.ts +5 -6
  48. package/dist/zadarma/index.js +3 -2
  49. package/dist/zadarma-engine-TIUE7PZ7.js +6 -0
  50. package/dist/{zadarma-engine-L5DEFXUS.js.map → zadarma-engine-TIUE7PZ7.js.map} +1 -1
  51. package/package.json +1 -1
  52. package/dist/chunk-2ZSOGGPO.js.map +0 -1
  53. package/dist/chunk-7R6LOOAI.js.map +0 -1
  54. package/dist/chunk-EYEDPD7Y.js.map +0 -1
  55. package/dist/chunk-UHNLEBBO.js.map +0 -1
  56. package/dist/sources-ONYQW733.js.map +0 -1
  57. package/dist/zadarma-engine-L5DEFXUS.js +0 -5
@@ -86,10 +86,10 @@ interface TokenEndpointOptions {
86
86
  */
87
87
  interface CoventiaBackendConfig {
88
88
  /**
89
- * e.g. `https://api.coventia.es`. **Optional** — falls back to `SoftphoneConfig.apiBase`, which
90
- * exists precisely so this and the default call engine name the same backend once instead of
91
- * twice. Set it here only when contacts/history genuinely live on a DIFFERENT host than calling
92
- * does, which is not the normal case.
89
+ * Coventia's domain, like `SoftphoneConfig.apiBase`: no path, the SDK builds every route.
90
+ * **Optional** — falls back to `SoftphoneConfig.apiBase`, which exists precisely so this and the
91
+ * default call engine name the same backend once instead of twice. Set it here only when
92
+ * contacts/history genuinely live on a DIFFERENT host than calling does, which is not the normal case.
93
93
  */
94
94
  readonly baseUrl?: string | undefined;
95
95
  /**
@@ -104,7 +104,10 @@ interface CoventiaBackendConfig {
104
104
  * That is the CRM today.
105
105
  */
106
106
  readonly organizationId?: string;
107
- /** `/phone` today; `/api/v1/phone` once the partner API ships (SPEC §6.6). */
107
+ /**
108
+ * Where the phone routes sit under the base URL. Internal: leave it unset (`/phone`). The routes are
109
+ * the SDK's to build, which is why `apiBase` is the domain only.
110
+ */
108
111
  readonly basePath?: string;
109
112
  /**
110
113
  * **The one thing a provider customises**: their own contact list, searched on their own backend.
@@ -134,8 +137,11 @@ interface CoventiaBackendConfig {
134
137
  interface SoftphoneConfig {
135
138
  readonly tokenSource: TokenSource;
136
139
  /**
137
- * The Coventia backend, e.g. `https://api.coventia.es`. **The one field to set** if you want the
138
- * default call engine (see `provider` below) and are not overriding `coventia.baseUrl` separately.
140
+ * Coventia's domain — **only the domain**, e.g. `https://backend.example.com`, with no path: the SDK
141
+ * builds every route itself. Not the Partner API URL your backend calls: that one ends in `/api/v1`,
142
+ * which does not belong here (it is ignored, with a console warning). **The one field to set** if you
143
+ * want the default call engine (see `provider` below) and are not overriding `coventia.baseUrl`
144
+ * separately.
139
145
  *
140
146
  * Named once here rather than requiring it on both `coventia` and the engine you construct: they
141
147
  * are two independent subsystems (contacts/history/caller ID vs. SIP credential provisioning) that
@@ -188,8 +194,44 @@ interface CallOptions {
188
194
  * Zadarma registers on-demand, per call, in response to a push (SPEC §3.2); a persistent UA is
189
195
  * ignored by its PBX. Leaking "registered" into the public API would force every future provider —
190
196
  * including our own SIP infrastructure, which registers persistently — to fake it.
197
+ *
198
+ * `standby` means started, and this browser's leader, but ANOTHER softphone of the same extension has
199
+ * the line — a tab in another browser, the desktop app, a partner page (`docs/specs/line-owner.md` §5).
200
+ * Its push socket is closed on purpose, so nothing here registers or rings: `line` says where the calls
201
+ * go, and `takeLine()` — or a `call()` — brings them here. A host that reads `online` as "ready"
202
+ * correctly reads this as not ready.
203
+ */
204
+ type SoftphoneState = 'stopped' | 'starting' | 'online' | 'standby' | 'offline' | 'reconnecting' | 'destroyed';
205
+ /**
206
+ * Where this extension's incoming calls ring, as far as this softphone knows. `here`: it has the line.
207
+ * `elsewhere`: another softphone of the same extension has it, and this one waits in `standby`. `unknown`:
208
+ * nobody has said — no presence source, a backend that predates the line owner, a presence socket that is
209
+ * down — and this softphone listens, exactly as before the line owner existed (§5.1 R7, fail-open).
210
+ */
211
+ type LineState = 'here' | 'elsewhere' | 'unknown';
212
+ /** Who has the line when it is `elsewhere`: the desktop app, or any other softphone (a browser tab). */
213
+ type LineLocation = 'desktop' | 'browser';
214
+ /**
215
+ * The last attempt to bring the line here — `takeLine()`, or a `call()` from `standby`, whoever made it:
216
+ * `claiming` while it is in flight, `refused` when the user is on a call where the line is, `failed` when
217
+ * nothing could decide. `idle` before any attempt, once the line comes here, and after a `stop()`.
191
218
  */
192
- type SoftphoneState = 'stopped' | 'starting' | 'online' | 'offline' | 'reconnecting' | 'destroyed';
219
+ type LineClaim = 'idle' | 'claiming' | 'refused' | 'failed';
220
+ /** The server's word on this softphone's line, as a `PresenceSource` hands it over (`PresenceCallbacks.onLine`). */
221
+ interface LineUpdate {
222
+ readonly state: 'here' | 'elsewhere';
223
+ /** Who has the line when it is `elsewhere`; `null` when it is here, or the server did not say. */
224
+ readonly where: LineLocation | null;
225
+ }
226
+ /** The answer to `PresenceConnection.claimLine()`. */
227
+ interface LineClaimResult {
228
+ readonly ok: boolean;
229
+ /**
230
+ * Why it was refused: `'on_call'` while the user is on a call (the line never moves during one). Any
231
+ * other value means nothing was decided. Absent when `ok`.
232
+ */
233
+ readonly reason?: string;
234
+ }
193
235
  interface ContactsPage {
194
236
  readonly items: readonly Contact[];
195
237
  /**
@@ -349,6 +391,18 @@ interface PresenceConnection {
349
391
  * own source that has no such message simply omits it and gets the grace. Sent just before `close()`.
350
392
  */
351
393
  readonly bye?: () => void;
394
+ /**
395
+ * The line owner (`docs/specs/line-owner.md` §5.2): says which softphone this connection speaks for, so
396
+ * the server can tell it whether it has the line — through `PresenceCallbacks.onLine`, right after this
397
+ * and on every change. Sent as the FIRST message of every connection. Optional: a source without it never
398
+ * takes part, and its softphone listens exactly as before (fail-open), reading its line as `unknown`.
399
+ */
400
+ readonly hello?: (clientId: string) => void;
401
+ /**
402
+ * "Recibir aquí": asks for the line. Answered through `PresenceCallbacks.onClaimResult` — and, when it
403
+ * succeeds, by `onLine` saying `here` first. Optional, like `hello`.
404
+ */
405
+ readonly claimLine?: () => void;
352
406
  /**
353
407
  * Ends this connection deliberately. Never causes the SAME callbacks' `onClose` to fire — that is
354
408
  * reserved for the connection ending on its OWN (a network drop, a server-side close, a rejected
@@ -368,6 +422,13 @@ interface PresenceCallbacks {
368
422
  * identical from here, and both get the same treatment (spec §4.4 — quiet retry, nothing surfaced).
369
423
  */
370
424
  readonly onClose: () => void;
425
+ /**
426
+ * The line, as the server says it — right after `PresenceConnection.hello`, and on every change after
427
+ * that. Optional, so callbacks written before the line owner existed still type-check.
428
+ */
429
+ readonly onLine?: (update: LineUpdate) => void;
430
+ /** The answer to `PresenceConnection.claimLine()`. Optional, like `onLine`. */
431
+ readonly onClaimResult?: (result: LineClaimResult) => void;
371
432
  }
372
433
  /**
373
434
  * Opens one live presence connection. Coventia-backed by default (mints a one-time ticket, opens
@@ -598,6 +659,16 @@ declare const ERROR_CATALOGUE: {
598
659
  readonly message: "The provider does not support this operation.";
599
660
  readonly solution: "Guard the UI on the operation before offering it.";
600
661
  };
662
+ readonly 'line/claim-refused': {
663
+ readonly fatal: false;
664
+ readonly message: "The line was not moved here: the user is on a call on the softphone that has it.";
665
+ readonly solution: "Nothing to fix. The line never moves during a call; take it again once that call has ended. Incoming calls keep ringing where they ring now.";
666
+ };
667
+ readonly 'line/unavailable': {
668
+ readonly fatal: false;
669
+ readonly message: "The line could not be taken: nothing decided where it is.";
670
+ readonly solution: "The presence connection is down, the backend predates the line owner, or it did not answer in time. Retry in a moment; incoming calls keep ringing where they ring now.";
671
+ };
601
672
  readonly 'provider/provision-failed': {
602
673
  readonly fatal: true;
603
674
  readonly message: "The provider could not issue SIP credentials.";
@@ -686,10 +757,25 @@ declare enum SoftphoneEvent {
686
757
  /**
687
758
  * `softphone.presence` changed — the live agent-presence list (Phase B,
688
759
  * `docs/specs/transfer-presence-picker.md`) gained a snapshot or an updated entry. Requires a
689
- * `PresenceSource` (from `coventia`/`apiBase`, or a host override) AND this tab currently being
690
- * `online && isLeader`; never fires at all otherwise, same reasoning as `TransferOutcome`.
760
+ * `PresenceSource` (from `coventia`/`apiBase`, or a host override) AND this tab currently being a
761
+ * started leader — `online`, or waiting in `standby` for the line; never fires at all otherwise,
762
+ * same reasoning as `TransferOutcome`.
691
763
  */
692
- PresenceChanged = "presenceChanged"
764
+ PresenceChanged = "presenceChanged",
765
+ /**
766
+ * `softphone.line` / `lineWhere` changed (`docs/specs/line-owner.md` §5): this softphone got the line,
767
+ * lost it to another softphone of the same extension, or stopped knowing (`unknown` — it then listens,
768
+ * as before the line owner existed). A third axis beside `StateChanged` and `LeaderChanged`: losing the
769
+ * line is what takes the softphone to `standby`, but not at once while a call is in progress — the line
770
+ * can read `elsewhere` while the state is still `online`, until that call ends.
771
+ */
772
+ LineChanged = "lineChanged",
773
+ /**
774
+ * `softphone.lineClaim` changed: an attempt to bring the line here started, was refused, failed, or ended
775
+ * (the line came here, or the softphone stopped). Kept apart from `LineChanged`, which only ever means the
776
+ * line itself moved.
777
+ */
778
+ LineClaimChanged = "lineClaimChanged"
693
779
  }
694
780
  interface SoftphoneEventMap {
695
781
  [SoftphoneEvent.StateChanged]: {
@@ -710,6 +796,13 @@ interface SoftphoneEventMap {
710
796
  [SoftphoneEvent.PresenceChanged]: {
711
797
  readonly agents: readonly PresenceEntry[];
712
798
  };
799
+ [SoftphoneEvent.LineChanged]: {
800
+ readonly line: LineState;
801
+ readonly where: LineLocation | null;
802
+ };
803
+ [SoftphoneEvent.LineClaimChanged]: {
804
+ readonly claim: LineClaim;
805
+ };
713
806
  }
714
807
  declare enum CallEvent {
715
808
  /** Any transition. The catch-all; the specific events below are conveniences. */
@@ -936,4 +1029,4 @@ interface Call extends TypedEmitter<CallEventMap> {
936
1029
  cancelConsult(): Promise<void>;
937
1030
  }
938
1031
 
939
- export { isFatal as A, isTerminal as B, CALL_TRANSITIONS as C, type ContactsSource as D, ERROR_CATALOGUE as E, type FatalErrorCode as F, type HistorySource as G, type HangupReason as H, INITIAL_STATES as I, type AgentsDirectory as J, type TransferOutcomesPage as K, type LogLevel as L, type PresenceSource as M, type NonFatalErrorCode as N, type OutboundNumberRefusal as O, type PresenceEntry as P, type CallHistoryEntry as Q, type RemoteParty as R, type SoftphoneConfig as S, type TokenEndpointOptions as T, type ContactsPage as U, type VoiceToken as V, type Call as a, type CallDirection as b, CallEvent as c, type CallEventMap as d, type CallOptions as e, type CallState as f, type Contact as g, type FatalSoftphoneError as h, type HoldOriginator as i, type NonFatalSoftphoneError as j, type OutboundNumber as k, type OutboundNumberKind as l, type OutboundNumberSettings as m, type OutboundNumberState as n, type OutboundNumbersSource as o, type RingtoneOption as p, type RingtoneSettings as q, type RingtoneSource as r, type SoftphoneError as s, type SoftphoneErrorCode as t, SoftphoneEvent as u, type SoftphoneEventMap as v, type SoftphoneState as w, type TokenSource as x, type TypedEmitter as y, canTransition as z };
1032
+ export { type TokenSource as A, type TypedEmitter as B, CALL_TRANSITIONS as C, canTransition as D, ERROR_CATALOGUE as E, type FatalErrorCode as F, isFatal as G, type HangupReason as H, INITIAL_STATES as I, isTerminal as J, type ContactsSource as K, type LineClaim as L, type HistorySource as M, type NonFatalErrorCode as N, type OutboundNumberRefusal as O, type PresenceEntry as P, type AgentsDirectory as Q, type RemoteParty as R, type SoftphoneConfig as S, type TokenEndpointOptions as T, type TransferOutcomesPage as U, type VoiceToken as V, type PresenceSource as W, type CallHistoryEntry as X, type ContactsPage as Y, type Call as a, type CallDirection as b, CallEvent as c, type CallEventMap as d, type CallOptions as e, type CallState as f, type Contact as g, type FatalSoftphoneError as h, type HoldOriginator as i, type LineLocation as j, type LineState as k, type LogLevel as l, type NonFatalSoftphoneError as m, type OutboundNumber as n, type OutboundNumberKind as o, type OutboundNumberSettings as p, type OutboundNumberState as q, type OutboundNumbersSource as r, type RingtoneOption as s, type RingtoneSettings as t, type RingtoneSource as u, type SoftphoneError as v, type SoftphoneErrorCode as w, SoftphoneEvent as x, type SoftphoneEventMap as y, type SoftphoneState as z };
@@ -86,10 +86,10 @@ interface TokenEndpointOptions {
86
86
  */
87
87
  interface CoventiaBackendConfig {
88
88
  /**
89
- * e.g. `https://api.coventia.es`. **Optional** — falls back to `SoftphoneConfig.apiBase`, which
90
- * exists precisely so this and the default call engine name the same backend once instead of
91
- * twice. Set it here only when contacts/history genuinely live on a DIFFERENT host than calling
92
- * does, which is not the normal case.
89
+ * Coventia's domain, like `SoftphoneConfig.apiBase`: no path, the SDK builds every route.
90
+ * **Optional** — falls back to `SoftphoneConfig.apiBase`, which exists precisely so this and the
91
+ * default call engine name the same backend once instead of twice. Set it here only when
92
+ * contacts/history genuinely live on a DIFFERENT host than calling does, which is not the normal case.
93
93
  */
94
94
  readonly baseUrl?: string | undefined;
95
95
  /**
@@ -104,7 +104,10 @@ interface CoventiaBackendConfig {
104
104
  * That is the CRM today.
105
105
  */
106
106
  readonly organizationId?: string;
107
- /** `/phone` today; `/api/v1/phone` once the partner API ships (SPEC §6.6). */
107
+ /**
108
+ * Where the phone routes sit under the base URL. Internal: leave it unset (`/phone`). The routes are
109
+ * the SDK's to build, which is why `apiBase` is the domain only.
110
+ */
108
111
  readonly basePath?: string;
109
112
  /**
110
113
  * **The one thing a provider customises**: their own contact list, searched on their own backend.
@@ -134,8 +137,11 @@ interface CoventiaBackendConfig {
134
137
  interface SoftphoneConfig {
135
138
  readonly tokenSource: TokenSource;
136
139
  /**
137
- * The Coventia backend, e.g. `https://api.coventia.es`. **The one field to set** if you want the
138
- * default call engine (see `provider` below) and are not overriding `coventia.baseUrl` separately.
140
+ * Coventia's domain — **only the domain**, e.g. `https://backend.example.com`, with no path: the SDK
141
+ * builds every route itself. Not the Partner API URL your backend calls: that one ends in `/api/v1`,
142
+ * which does not belong here (it is ignored, with a console warning). **The one field to set** if you
143
+ * want the default call engine (see `provider` below) and are not overriding `coventia.baseUrl`
144
+ * separately.
139
145
  *
140
146
  * Named once here rather than requiring it on both `coventia` and the engine you construct: they
141
147
  * are two independent subsystems (contacts/history/caller ID vs. SIP credential provisioning) that
@@ -188,8 +194,44 @@ interface CallOptions {
188
194
  * Zadarma registers on-demand, per call, in response to a push (SPEC §3.2); a persistent UA is
189
195
  * ignored by its PBX. Leaking "registered" into the public API would force every future provider —
190
196
  * including our own SIP infrastructure, which registers persistently — to fake it.
197
+ *
198
+ * `standby` means started, and this browser's leader, but ANOTHER softphone of the same extension has
199
+ * the line — a tab in another browser, the desktop app, a partner page (`docs/specs/line-owner.md` §5).
200
+ * Its push socket is closed on purpose, so nothing here registers or rings: `line` says where the calls
201
+ * go, and `takeLine()` — or a `call()` — brings them here. A host that reads `online` as "ready"
202
+ * correctly reads this as not ready.
203
+ */
204
+ type SoftphoneState = 'stopped' | 'starting' | 'online' | 'standby' | 'offline' | 'reconnecting' | 'destroyed';
205
+ /**
206
+ * Where this extension's incoming calls ring, as far as this softphone knows. `here`: it has the line.
207
+ * `elsewhere`: another softphone of the same extension has it, and this one waits in `standby`. `unknown`:
208
+ * nobody has said — no presence source, a backend that predates the line owner, a presence socket that is
209
+ * down — and this softphone listens, exactly as before the line owner existed (§5.1 R7, fail-open).
210
+ */
211
+ type LineState = 'here' | 'elsewhere' | 'unknown';
212
+ /** Who has the line when it is `elsewhere`: the desktop app, or any other softphone (a browser tab). */
213
+ type LineLocation = 'desktop' | 'browser';
214
+ /**
215
+ * The last attempt to bring the line here — `takeLine()`, or a `call()` from `standby`, whoever made it:
216
+ * `claiming` while it is in flight, `refused` when the user is on a call where the line is, `failed` when
217
+ * nothing could decide. `idle` before any attempt, once the line comes here, and after a `stop()`.
191
218
  */
192
- type SoftphoneState = 'stopped' | 'starting' | 'online' | 'offline' | 'reconnecting' | 'destroyed';
219
+ type LineClaim = 'idle' | 'claiming' | 'refused' | 'failed';
220
+ /** The server's word on this softphone's line, as a `PresenceSource` hands it over (`PresenceCallbacks.onLine`). */
221
+ interface LineUpdate {
222
+ readonly state: 'here' | 'elsewhere';
223
+ /** Who has the line when it is `elsewhere`; `null` when it is here, or the server did not say. */
224
+ readonly where: LineLocation | null;
225
+ }
226
+ /** The answer to `PresenceConnection.claimLine()`. */
227
+ interface LineClaimResult {
228
+ readonly ok: boolean;
229
+ /**
230
+ * Why it was refused: `'on_call'` while the user is on a call (the line never moves during one). Any
231
+ * other value means nothing was decided. Absent when `ok`.
232
+ */
233
+ readonly reason?: string;
234
+ }
193
235
  interface ContactsPage {
194
236
  readonly items: readonly Contact[];
195
237
  /**
@@ -349,6 +391,18 @@ interface PresenceConnection {
349
391
  * own source that has no such message simply omits it and gets the grace. Sent just before `close()`.
350
392
  */
351
393
  readonly bye?: () => void;
394
+ /**
395
+ * The line owner (`docs/specs/line-owner.md` §5.2): says which softphone this connection speaks for, so
396
+ * the server can tell it whether it has the line — through `PresenceCallbacks.onLine`, right after this
397
+ * and on every change. Sent as the FIRST message of every connection. Optional: a source without it never
398
+ * takes part, and its softphone listens exactly as before (fail-open), reading its line as `unknown`.
399
+ */
400
+ readonly hello?: (clientId: string) => void;
401
+ /**
402
+ * "Recibir aquí": asks for the line. Answered through `PresenceCallbacks.onClaimResult` — and, when it
403
+ * succeeds, by `onLine` saying `here` first. Optional, like `hello`.
404
+ */
405
+ readonly claimLine?: () => void;
352
406
  /**
353
407
  * Ends this connection deliberately. Never causes the SAME callbacks' `onClose` to fire — that is
354
408
  * reserved for the connection ending on its OWN (a network drop, a server-side close, a rejected
@@ -368,6 +422,13 @@ interface PresenceCallbacks {
368
422
  * identical from here, and both get the same treatment (spec §4.4 — quiet retry, nothing surfaced).
369
423
  */
370
424
  readonly onClose: () => void;
425
+ /**
426
+ * The line, as the server says it — right after `PresenceConnection.hello`, and on every change after
427
+ * that. Optional, so callbacks written before the line owner existed still type-check.
428
+ */
429
+ readonly onLine?: (update: LineUpdate) => void;
430
+ /** The answer to `PresenceConnection.claimLine()`. Optional, like `onLine`. */
431
+ readonly onClaimResult?: (result: LineClaimResult) => void;
371
432
  }
372
433
  /**
373
434
  * Opens one live presence connection. Coventia-backed by default (mints a one-time ticket, opens
@@ -598,6 +659,16 @@ declare const ERROR_CATALOGUE: {
598
659
  readonly message: "The provider does not support this operation.";
599
660
  readonly solution: "Guard the UI on the operation before offering it.";
600
661
  };
662
+ readonly 'line/claim-refused': {
663
+ readonly fatal: false;
664
+ readonly message: "The line was not moved here: the user is on a call on the softphone that has it.";
665
+ readonly solution: "Nothing to fix. The line never moves during a call; take it again once that call has ended. Incoming calls keep ringing where they ring now.";
666
+ };
667
+ readonly 'line/unavailable': {
668
+ readonly fatal: false;
669
+ readonly message: "The line could not be taken: nothing decided where it is.";
670
+ readonly solution: "The presence connection is down, the backend predates the line owner, or it did not answer in time. Retry in a moment; incoming calls keep ringing where they ring now.";
671
+ };
601
672
  readonly 'provider/provision-failed': {
602
673
  readonly fatal: true;
603
674
  readonly message: "The provider could not issue SIP credentials.";
@@ -686,10 +757,25 @@ declare enum SoftphoneEvent {
686
757
  /**
687
758
  * `softphone.presence` changed — the live agent-presence list (Phase B,
688
759
  * `docs/specs/transfer-presence-picker.md`) gained a snapshot or an updated entry. Requires a
689
- * `PresenceSource` (from `coventia`/`apiBase`, or a host override) AND this tab currently being
690
- * `online && isLeader`; never fires at all otherwise, same reasoning as `TransferOutcome`.
760
+ * `PresenceSource` (from `coventia`/`apiBase`, or a host override) AND this tab currently being a
761
+ * started leader — `online`, or waiting in `standby` for the line; never fires at all otherwise,
762
+ * same reasoning as `TransferOutcome`.
691
763
  */
692
- PresenceChanged = "presenceChanged"
764
+ PresenceChanged = "presenceChanged",
765
+ /**
766
+ * `softphone.line` / `lineWhere` changed (`docs/specs/line-owner.md` §5): this softphone got the line,
767
+ * lost it to another softphone of the same extension, or stopped knowing (`unknown` — it then listens,
768
+ * as before the line owner existed). A third axis beside `StateChanged` and `LeaderChanged`: losing the
769
+ * line is what takes the softphone to `standby`, but not at once while a call is in progress — the line
770
+ * can read `elsewhere` while the state is still `online`, until that call ends.
771
+ */
772
+ LineChanged = "lineChanged",
773
+ /**
774
+ * `softphone.lineClaim` changed: an attempt to bring the line here started, was refused, failed, or ended
775
+ * (the line came here, or the softphone stopped). Kept apart from `LineChanged`, which only ever means the
776
+ * line itself moved.
777
+ */
778
+ LineClaimChanged = "lineClaimChanged"
693
779
  }
694
780
  interface SoftphoneEventMap {
695
781
  [SoftphoneEvent.StateChanged]: {
@@ -710,6 +796,13 @@ interface SoftphoneEventMap {
710
796
  [SoftphoneEvent.PresenceChanged]: {
711
797
  readonly agents: readonly PresenceEntry[];
712
798
  };
799
+ [SoftphoneEvent.LineChanged]: {
800
+ readonly line: LineState;
801
+ readonly where: LineLocation | null;
802
+ };
803
+ [SoftphoneEvent.LineClaimChanged]: {
804
+ readonly claim: LineClaim;
805
+ };
713
806
  }
714
807
  declare enum CallEvent {
715
808
  /** Any transition. The catch-all; the specific events below are conveniences. */
@@ -936,4 +1029,4 @@ interface Call extends TypedEmitter<CallEventMap> {
936
1029
  cancelConsult(): Promise<void>;
937
1030
  }
938
1031
 
939
- export { isFatal as A, isTerminal as B, CALL_TRANSITIONS as C, type ContactsSource as D, ERROR_CATALOGUE as E, type FatalErrorCode as F, type HistorySource as G, type HangupReason as H, INITIAL_STATES as I, type AgentsDirectory as J, type TransferOutcomesPage as K, type LogLevel as L, type PresenceSource as M, type NonFatalErrorCode as N, type OutboundNumberRefusal as O, type PresenceEntry as P, type CallHistoryEntry as Q, type RemoteParty as R, type SoftphoneConfig as S, type TokenEndpointOptions as T, type ContactsPage as U, type VoiceToken as V, type Call as a, type CallDirection as b, CallEvent as c, type CallEventMap as d, type CallOptions as e, type CallState as f, type Contact as g, type FatalSoftphoneError as h, type HoldOriginator as i, type NonFatalSoftphoneError as j, type OutboundNumber as k, type OutboundNumberKind as l, type OutboundNumberSettings as m, type OutboundNumberState as n, type OutboundNumbersSource as o, type RingtoneOption as p, type RingtoneSettings as q, type RingtoneSource as r, type SoftphoneError as s, type SoftphoneErrorCode as t, SoftphoneEvent as u, type SoftphoneEventMap as v, type SoftphoneState as w, type TokenSource as x, type TypedEmitter as y, canTransition as z };
1032
+ export { type TokenSource as A, type TypedEmitter as B, CALL_TRANSITIONS as C, canTransition as D, ERROR_CATALOGUE as E, type FatalErrorCode as F, isFatal as G, type HangupReason as H, INITIAL_STATES as I, isTerminal as J, type ContactsSource as K, type LineClaim as L, type HistorySource as M, type NonFatalErrorCode as N, type OutboundNumberRefusal as O, type PresenceEntry as P, type AgentsDirectory as Q, type RemoteParty as R, type SoftphoneConfig as S, type TokenEndpointOptions as T, type TransferOutcomesPage as U, type VoiceToken as V, type PresenceSource as W, type CallHistoryEntry as X, type ContactsPage as Y, type Call as a, type CallDirection as b, CallEvent as c, type CallEventMap as d, type CallOptions as e, type CallState as f, type Contact as g, type FatalSoftphoneError as h, type HoldOriginator as i, type LineLocation as j, type LineState as k, type LogLevel as l, type NonFatalSoftphoneError as m, type OutboundNumber as n, type OutboundNumberKind as o, type OutboundNumberSettings as p, type OutboundNumberState as q, type OutboundNumbersSource as r, type RingtoneOption as s, type RingtoneSettings as t, type RingtoneSource as u, type SoftphoneError as v, type SoftphoneErrorCode as w, SoftphoneEvent as x, type SoftphoneEventMap as y, type SoftphoneState as z };
@@ -1,4 +1,5 @@
1
- import { createError, isSoftphoneError } from './chunk-2ZSOGGPO.js';
1
+ import { normalizeApiBase } from './chunk-M76SAXXM.js';
2
+ import { createError, isSoftphoneError } from './chunk-FRXHEHQA.js';
2
3
  import { cachedTokenSource, invalidateToken } from './chunk-MHOJHG5E.js';
3
4
 
4
5
  // src/transports/sip/media.ts
@@ -254,9 +255,22 @@ var SipCallEngine = class {
254
255
  provider;
255
256
  factory;
256
257
  #state = "stopped";
258
+ /**
259
+ * Which `start()`…`stop()` cycle this is — bumped by `stop()`, which ends one. Everything that awaits
260
+ * and then acts (builds a UA, subscribes to the signal, sets the state) captures it first and gives up
261
+ * if a stop has happened since. Not bumped by `start()`: that would cancel work begun just before it,
262
+ * such as a call placed while the softphone is still starting.
263
+ *
264
+ * Without it a stopped phone came back to life: a provisioning or a signal connect still in flight
265
+ * when `stop()` ran finished afterwards and REGISTERed — and since the PBX routes a call to whichever
266
+ * client registered last, it quietly took calls from the user's other device while its UI said off.
267
+ */
268
+ #run = 0;
257
269
  #ua = null;
258
270
  /** In-flight `#createUA()`. THE concurrency guard — see `#ensureUA`. */
259
271
  #uaInFlight = null;
272
+ /** Calls waiting for the current UA to register (`#callWhenRegistered`), failed by `#teardownUA`. */
273
+ #awaitingRegistration = /* @__PURE__ */ new Set();
260
274
  #domain = "";
261
275
  #activeCall = null;
262
276
  /**
@@ -339,14 +353,25 @@ var SipCallEngine = class {
339
353
  for (const listener of [...this.#errorListeners]) listener(error);
340
354
  }
341
355
  async start() {
356
+ const run = this.#run;
342
357
  this.#setState("starting");
343
358
  if (this.provider.registrationMode === "persistent") {
344
- await this.#ensureUA();
359
+ try {
360
+ await this.#ensureUA();
361
+ } catch (cause) {
362
+ if (run === this.#run) throw cause;
363
+ }
364
+ if (run !== this.#run) return;
345
365
  this.#setState("online");
346
366
  } else {
347
367
  const signal = this.provider.incomingSignal;
348
368
  if (signal) {
349
- await signal.connect();
369
+ try {
370
+ await signal.connect();
371
+ } catch (cause) {
372
+ if (run === this.#run) throw cause;
373
+ }
374
+ if (run !== this.#run) return;
350
375
  this.#signalUnsub = signal.onSignal(() => void this.#onIncomingSignal());
351
376
  this.#signalConnectionUnsub = signal.onConnectionChange?.(
352
377
  (connected) => this.#setState(connected ? "online" : "offline")
@@ -356,6 +381,7 @@ var SipCallEngine = class {
356
381
  }
357
382
  }
358
383
  async stop() {
384
+ this.#run += 1;
359
385
  this.#clearNoInviteTimer();
360
386
  this.#signalUnsub?.();
361
387
  this.#signalUnsub = null;
@@ -374,11 +400,12 @@ var SipCallEngine = class {
374
400
  async #onIncomingSignal() {
375
401
  if (this.now() < this.#rejectGuardUntil) return;
376
402
  if (this.#activeCall) return;
403
+ const run = this.#run;
377
404
  try {
378
405
  await this.#ensureUA();
379
406
  this.#armNoInviteTeardown();
380
407
  } catch {
381
- this.#teardownUA();
408
+ if (run === this.#run) this.#teardownUA();
382
409
  }
383
410
  }
384
411
  #armNoInviteTeardown() {
@@ -440,12 +467,16 @@ var SipCallEngine = class {
440
467
  */
441
468
  async #ensureUA() {
442
469
  if (this.#ua) return this.#ua;
443
- this.#uaInFlight ??= this.#createUA().finally(() => {
444
- this.#uaInFlight = null;
445
- });
470
+ if (!this.#uaInFlight) {
471
+ const inFlight = this.#createUA().finally(() => {
472
+ if (this.#uaInFlight === inFlight) this.#uaInFlight = null;
473
+ });
474
+ this.#uaInFlight = inFlight;
475
+ }
446
476
  return this.#uaInFlight;
447
477
  }
448
478
  async #createUA() {
479
+ const run = this.#run;
449
480
  const factory = await this.#resolveFactory();
450
481
  let credentials;
451
482
  try {
@@ -455,17 +486,28 @@ var SipCallEngine = class {
455
486
  else this.#reportError("provider/provision-failed", cause);
456
487
  throw cause;
457
488
  }
489
+ if (run !== this.#run) throw createError("engine/not-started");
458
490
  this.#domain = domainOf(credentials);
459
491
  const config = this.#uaConfig(credentials, factory);
460
492
  const ua = factory.createUA(config);
461
- ua.on("newRTCSession", ({ session, originator }) => this.#onNewSession(session, originator));
493
+ ua.on("newRTCSession", ({ session, originator }) => {
494
+ if (ua !== this.#ua) {
495
+ if (originator === "remote") session.terminate({ status_code: 486, reason_phrase: "Busy Here" });
496
+ return;
497
+ }
498
+ this.#onNewSession(session, originator);
499
+ });
462
500
  ua.on("disconnected", ({ error }) => {
501
+ if (ua !== this.#ua) return;
463
502
  if (!error) return;
464
503
  this.#setState("reconnecting");
465
504
  this.#reportError("net/transport-failed", error);
466
505
  });
467
- ua.on("connected", () => this.#setState("online"));
506
+ ua.on("connected", () => {
507
+ if (ua === this.#ua) this.#setState("online");
508
+ });
468
509
  ua.on("registrationFailed", (e) => {
510
+ if (run !== this.#run) return;
469
511
  const detail = e;
470
512
  this.#reportError("net/registration-failed", {
471
513
  cause: detail?.cause,
@@ -473,7 +515,7 @@ var SipCallEngine = class {
473
515
  domain: this.#domain,
474
516
  hint: "A bare 401 with no auth challenge usually means the registrar is in the wrong datacenter (SPEC \xA73.1)."
475
517
  });
476
- this.#teardownUA();
518
+ if (ua === this.#ua) this.#teardownUA();
477
519
  void this.#refreshSignal();
478
520
  });
479
521
  this.#ua = ua;
@@ -500,6 +542,7 @@ var SipCallEngine = class {
500
542
  #teardownUA() {
501
543
  this.#uaInFlight = null;
502
544
  if (!this.#ua) return;
545
+ for (const fail of [...this.#awaitingRegistration]) fail();
503
546
  try {
504
547
  this.#ua.terminateSessions({ status_code: 487 });
505
548
  this.#ua.stop();
@@ -523,8 +566,27 @@ var SipCallEngine = class {
523
566
  #callWhenRegistered(ua, target) {
524
567
  const options = { mediaConstraints: buildAudioConstraints(this.#inputDeviceId), pcConfig: PC_CONFIG };
525
568
  if (ua.isRegistered()) return Promise.resolve(ua.call(target, options));
526
- return new Promise((resolve) => {
527
- ua.on("registered", () => resolve(ua.call(target, options)));
569
+ const run = this.#run;
570
+ return new Promise((resolve, reject) => {
571
+ let settled = false;
572
+ const fail = () => {
573
+ if (settled) return;
574
+ settled = true;
575
+ this.#awaitingRegistration.delete(fail);
576
+ reject(createError(run === this.#run ? "call/failed" : "engine/not-started"));
577
+ };
578
+ this.#awaitingRegistration.add(fail);
579
+ ua.on("registered", () => {
580
+ if (settled) return;
581
+ if (ua !== this.#ua) return fail();
582
+ settled = true;
583
+ this.#awaitingRegistration.delete(fail);
584
+ try {
585
+ resolve(ua.call(target, options));
586
+ } catch (cause) {
587
+ reject(cause);
588
+ }
589
+ });
528
590
  });
529
591
  }
530
592
  #wrapCall(session) {
@@ -664,6 +726,12 @@ var ZadarmaPushSignal = class {
664
726
  #retryTimer;
665
727
  #unwatchBackground;
666
728
  #closed = false;
729
+ /**
730
+ * Bumped by `disconnect()`. An open or a rebuild still awaiting its key when the signal is disconnected
731
+ * must build nothing afterwards — and `#closed` alone cannot say so once a later `connect()` (stop, then
732
+ * start again) has reset it: the late socket was built beside the new one, and nothing ever closed it.
733
+ */
734
+ #run = 0;
667
735
  #listeners = /* @__PURE__ */ new Set();
668
736
  #connectionListeners = /* @__PURE__ */ new Set();
669
737
  async connect() {
@@ -673,8 +741,9 @@ var ZadarmaPushSignal = class {
673
741
  await this.#open();
674
742
  }
675
743
  async #open() {
744
+ const run = this.#run;
676
745
  const { key, sip, keyExpiresAt } = await this.deps.fetchKey();
677
- if (this.#closed) return;
746
+ if (run !== this.#run) return;
678
747
  const socket = this.deps.createPushSocket(buildPushUrl(key, sip));
679
748
  this.#socket = socket;
680
749
  this.#attach(socket);
@@ -769,9 +838,10 @@ var ZadarmaPushSignal = class {
769
838
  return;
770
839
  }
771
840
  this.#rebuilding = true;
841
+ const run = this.#run;
772
842
  try {
773
843
  const { key, sip, keyExpiresAt } = await this.deps.fetchKey();
774
- if (this.#closed) return;
844
+ if (run !== this.#run) return;
775
845
  const swapped = await new Promise((resolve) => {
776
846
  const socket = this.deps.createPushSocket(buildPushUrl(key, sip));
777
847
  this.#candidate = socket;
@@ -824,6 +894,7 @@ var ZadarmaPushSignal = class {
824
894
  }
825
895
  disconnect() {
826
896
  this.#closed = true;
897
+ this.#run += 1;
827
898
  this.#stopPings();
828
899
  clearTimeout(this.#refreshTimer);
829
900
  this.#refreshTimer = void 0;
@@ -1061,11 +1132,15 @@ function makeZadarmaBrowserDeps(config) {
1061
1132
  // src/transports/zadarma-engine.ts
1062
1133
  function createZadarmaEngine(config) {
1063
1134
  const provider = new ZadarmaProvider(
1064
- makeZadarmaBrowserDeps({ ...config, tokenSource: cachedTokenSource(config.tokenSource) })
1135
+ makeZadarmaBrowserDeps({
1136
+ ...config,
1137
+ apiBase: normalizeApiBase(config.apiBase),
1138
+ tokenSource: cachedTokenSource(config.tokenSource)
1139
+ })
1065
1140
  );
1066
1141
  return new SipCallEngine(provider, loadJsSipFactory);
1067
1142
  }
1068
1143
 
1069
1144
  export { ZADARMA_QUIRKS, createZadarmaEngine };
1070
- //# sourceMappingURL=chunk-EYEDPD7Y.js.map
1071
- //# sourceMappingURL=chunk-EYEDPD7Y.js.map
1145
+ //# sourceMappingURL=chunk-6RR6G35E.js.map
1146
+ //# sourceMappingURL=chunk-6RR6G35E.js.map