@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.
Files changed (54) hide show
  1. package/dist/{call-BTYlT2SU.d.cts → call-VKq1I8yP.d.cts} +158 -5
  2. package/dist/{call-BTYlT2SU.d.ts → call-VKq1I8yP.d.ts} +158 -5
  3. package/dist/{chunk-N2N2EQQJ.js → chunk-7R6LOOAI.js} +4 -2
  4. package/dist/chunk-7R6LOOAI.js.map +1 -0
  5. package/dist/{chunk-5BGEHMPP.js → chunk-EYEDPD7Y.js} +146 -20
  6. package/dist/chunk-EYEDPD7Y.js.map +1 -0
  7. package/dist/{chunk-3V2L6UTK.js → chunk-LMJXX4YM.js} +17 -3
  8. package/dist/chunk-LMJXX4YM.js.map +1 -0
  9. package/dist/element/index.d.ts +162 -7
  10. package/dist/element/index.js +866 -421
  11. package/dist/element/index.js.map +1 -1
  12. package/dist/{engine-D9nbeY08.d.cts → engine-BlQBo2Ys.d.cts} +1 -1
  13. package/dist/{engine-Doa6Brkm.d.ts → engine-C0BOkIcW.d.ts} +1 -1
  14. package/dist/index.cjs +586 -28
  15. package/dist/index.cjs.map +1 -1
  16. package/dist/index.d.cts +5 -5
  17. package/dist/index.d.ts +5 -5
  18. package/dist/index.js +335 -8
  19. package/dist/index.js.map +1 -1
  20. package/dist/react/index.cjs +15 -0
  21. package/dist/react/index.cjs.map +1 -1
  22. package/dist/react/index.d.cts +14 -3
  23. package/dist/react/index.d.ts +14 -3
  24. package/dist/react/index.js +2 -2
  25. package/dist/{softphone-DhuhcKCy.d.cts → softphone-Cyq3hiIM.d.ts} +17 -2
  26. package/dist/{softphone-DHmhotbf.d.ts → softphone-DlGV12Xp.d.cts} +17 -2
  27. package/dist/softphone.css +477 -35
  28. package/dist/{sources-N4BOEDRL.js → sources-IRKNQPWW.js} +108 -5
  29. package/dist/sources-IRKNQPWW.js.map +1 -0
  30. package/dist/testing/index.cjs +41 -5
  31. package/dist/testing/index.cjs.map +1 -1
  32. package/dist/testing/index.d.cts +8 -2
  33. package/dist/testing/index.d.ts +8 -2
  34. package/dist/testing/index.js +41 -5
  35. package/dist/testing/index.js.map +1 -1
  36. package/dist/ui/index.cjs +846 -410
  37. package/dist/ui/index.cjs.map +1 -1
  38. package/dist/ui/index.d.cts +17 -6
  39. package/dist/ui/index.d.ts +17 -6
  40. package/dist/ui/index.js +878 -456
  41. package/dist/ui/index.js.map +1 -1
  42. package/dist/zadarma/index.cjs +144 -18
  43. package/dist/zadarma/index.cjs.map +1 -1
  44. package/dist/zadarma/index.d.cts +2 -2
  45. package/dist/zadarma/index.d.ts +2 -2
  46. package/dist/zadarma/index.js +1 -1
  47. package/dist/zadarma-engine-L5DEFXUS.js +5 -0
  48. package/dist/{zadarma-engine-DN2SZSQ3.js.map → zadarma-engine-L5DEFXUS.js.map} +1 -1
  49. package/package.json +1 -1
  50. package/dist/chunk-3V2L6UTK.js.map +0 -1
  51. package/dist/chunk-5BGEHMPP.js.map +0 -1
  52. package/dist/chunk-N2N2EQQJ.js.map +0 -1
  53. package/dist/sources-N4BOEDRL.js.map +0 -1
  54. package/dist/zadarma-engine-DN2SZSQ3.js +0 -5
@@ -117,6 +117,19 @@ interface CoventiaBackendConfig {
117
117
  * (SPEC §6.6).
118
118
  */
119
119
  readonly contacts?: ContactsSource;
120
+ /**
121
+ * Transfer outcomes, overriding Coventia's own polling — for a `MockProvider`-style test double, or
122
+ * a future non-Zadarma provider that computes outcomes differently. Not a per-partner customisation
123
+ * point the way `contacts` is: unlike a contact list, a transfer outcome is not a partner's own
124
+ * data, so there is normally no reason to set this (`docs/specs/transfer-outcome-relay.md`, Phase A).
125
+ */
126
+ readonly transferOutcomes?: TransferOutcomeSource;
127
+ /**
128
+ * Live agent presence, overriding Coventia's own WebSocket — for a `MockProvider`-style test
129
+ * double, or a host with its own presence transport. Not a per-partner customisation point the way
130
+ * `contacts` is (`docs/specs/transfer-presence-picker.md`, Phase B).
131
+ */
132
+ readonly presence?: PresenceSource;
120
133
  }
121
134
  interface SoftphoneConfig {
122
135
  readonly tokenSource: TokenSource;
@@ -261,6 +274,104 @@ interface AgentsDirectory {
261
274
  readonly me: PhoneAgent | null;
262
275
  readonly people: readonly PhoneAgent[];
263
276
  }
277
+ /**
278
+ * `'connected'` | `'not-connected'` — whether an attended transfer's second ring, after `Completar`,
279
+ * actually reached the target. Kebab-case to match `HangupReason` (`'no-answer'`, `'transferred'`);
280
+ * the backend's own wire value stays `not_connected` — this is the one place that gets translated.
281
+ */
282
+ type TransferOutcomeStatus = 'connected' | 'not-connected';
283
+ /** One resolved transfer, as `GET /phone/transfers/outcomes` reports it. */
284
+ interface TransferOutcomeEntry {
285
+ /** Opaque, strictly increasing per organization, never re-issued. The cursor for the NEXT poll. */
286
+ readonly id: string;
287
+ /** The extension the call was transferred to. */
288
+ readonly target: string;
289
+ readonly status: TransferOutcomeStatus;
290
+ readonly at: Date;
291
+ }
292
+ interface TransferOutcomesPage {
293
+ /** The newest `id` currently retained, or `'0'` when there are none — pass back as `since`. */
294
+ readonly cursor: string;
295
+ readonly outcomes: readonly TransferOutcomeEntry[];
296
+ }
297
+ /**
298
+ * Poll for transfer outcomes. `since` omitted ⇒ everything currently retained — used for the FIRST
299
+ * poll after starting a watch, which exists only to learn the current cursor (spec §5.3's
300
+ * no-mis-attribution rule: that first poll's own outcomes are discarded, only `cursor` is kept).
301
+ */
302
+ type TransferOutcomeSource = (params: {
303
+ readonly since?: string;
304
+ }) => Promise<TransferOutcomesPage>;
305
+ /**
306
+ * `SoftphoneEvent.TransferOutcome`'s payload. Unlike `TransferOutcomeEntry`, this never carries `id`
307
+ * (a polling-internal concern) and adds `caller` — the backend never stores a caller number (spec
308
+ * §5.3: "no caller number, nothing about other users"), so this is filled in locally, from the `Call`
309
+ * that was transferred, at the moment an outcome is matched to it.
310
+ */
311
+ interface TransferOutcomeEvent {
312
+ readonly status: TransferOutcomeStatus;
313
+ readonly target: string;
314
+ /** The original caller's number, for "Llamar de vuelta" — null if it was never known. */
315
+ readonly caller: string | null;
316
+ readonly at: Date;
317
+ }
318
+ /**
319
+ * `'disponible'` | `'en_llamada'` | `'desconectado'` — Spanish because this is what the picker shows,
320
+ * not an internal code (spec §4.1). `derivePresence` (the backend's own reducer, mirrored nowhere in
321
+ * the SDK — the SDK only ever receives an already-derived state, never `onCall`/`onlineFresh` apart).
322
+ */
323
+ type PresenceState = 'disponible' | 'en_llamada' | 'desconectado';
324
+ /** One org member with a phone extension, as the picker lists them. */
325
+ interface PresenceEntry {
326
+ readonly userId: string;
327
+ readonly extension: string;
328
+ /** `null` when the backend has no name to offer — the picker falls back to the extension. */
329
+ readonly name: string | null;
330
+ readonly state: PresenceState;
331
+ }
332
+ /** What a live presence connection hands back once open. */
333
+ interface PresenceConnection {
334
+ /**
335
+ * Report this softphone's own on-call-ness, so a colleague transferring TO this agent sees it
336
+ * without a round trip through the webhook path (spec §4.1's `onCall` OR-combine). Calling this
337
+ * before the connection is genuinely open is never required of an implementation — a caller (see
338
+ * `presence-socket.ts`) that reports early is expected to wait for the connection to exist first.
339
+ */
340
+ readonly reportCallState: (onCall: boolean) => void;
341
+ /**
342
+ * Says this phone is going away ON PURPOSE (the user turned it off, the app is quitting), so the backend
343
+ * drops it at once instead of waiting out the close grace it gives every ordinary close. Optional: a host's
344
+ * own source that has no such message simply omits it and gets the grace. Sent just before `close()`.
345
+ */
346
+ readonly bye?: () => void;
347
+ /**
348
+ * Ends this connection deliberately. Never causes the SAME callbacks' `onClose` to fire — that is
349
+ * reserved for the connection ending on its OWN (a network drop, a server-side close, a rejected
350
+ * ticket), which is the only thing a caller should ever treat as "reconnect now".
351
+ */
352
+ readonly close: () => void;
353
+ }
354
+ interface PresenceCallbacks {
355
+ /** Once, right after connecting — the full current roster (spec §4.3). */
356
+ readonly onSnapshot: (agents: readonly PresenceEntry[]) => void;
357
+ /** Zero or more times after that — one agent whose state just changed. */
358
+ readonly onDelta: (agent: PresenceEntry) => void;
359
+ /**
360
+ * The connection ended on its own. Never called after the CALLER's own `close()` — see
361
+ * `PresenceConnection.close`'s own note. The only signal a watch layer needs to know it must
362
+ * reconnect, deliberately carrying no reason: a dropped connection and a rejected ticket look
363
+ * identical from here, and both get the same treatment (spec §4.4 — quiet retry, nothing surfaced).
364
+ */
365
+ readonly onClose: () => void;
366
+ }
367
+ /**
368
+ * Opens one live presence connection. Coventia-backed by default (mints a one-time ticket, opens
369
+ * `wss://…/phone/presence`); a host may override it entirely, or omit it — omitted, or while the
370
+ * connection is down, the picker simply has no live data to show (spec §4.4, success criterion #5:
371
+ * no error surfaces). Unlike `TransferOutcomeSource` (poll, get a page, done), this is push-based and
372
+ * long-lived — the returned promise resolves once, when the connection opens, not once per update.
373
+ */
374
+ type PresenceSource = (callbacks: PresenceCallbacks) => Promise<PresenceConnection>;
264
375
 
265
376
  /**
266
377
  * The error taxonomy.
@@ -456,7 +567,23 @@ declare enum SoftphoneEvent {
456
567
  * `StateChanged`: `state` is reachability, this is whether this tab is even allowed to be
457
568
  * reachable at all while another tab holding the same extension stays open.
458
569
  */
459
- LeaderChanged = "leaderChanged"
570
+ LeaderChanged = "leaderChanged",
571
+ /**
572
+ * `Completar` completed a transfer, and the target's SECOND ring either connected or didn't —
573
+ * `docs/specs/transfer-outcome-relay.md` (Phase A). Fires for BOTH outcomes so a host can react to
574
+ * either; only `'not-connected'` is worth surfacing to the agent (spec §5.5) — roughly a minute
575
+ * after the transfer for a target who never answers, seconds for a decline or the caller hanging up
576
+ * first. Requires a `TransferOutcomeSource` (from `coventia`/`apiBase`, or a host override); never
577
+ * fires at all when none is configured, since there is then nothing to poll.
578
+ */
579
+ TransferOutcome = "transferOutcome",
580
+ /**
581
+ * `softphone.presence` changed — the live agent-presence list (Phase B,
582
+ * `docs/specs/transfer-presence-picker.md`) gained a snapshot or an updated entry. Requires a
583
+ * `PresenceSource` (from `coventia`/`apiBase`, or a host override) AND this tab currently being
584
+ * `online && isLeader`; never fires at all otherwise, same reasoning as `TransferOutcome`.
585
+ */
586
+ PresenceChanged = "presenceChanged"
460
587
  }
461
588
  interface SoftphoneEventMap {
462
589
  [SoftphoneEvent.StateChanged]: {
@@ -473,6 +600,10 @@ interface SoftphoneEventMap {
473
600
  [SoftphoneEvent.LeaderChanged]: {
474
601
  readonly isLeader: boolean;
475
602
  };
603
+ [SoftphoneEvent.TransferOutcome]: TransferOutcomeEvent;
604
+ [SoftphoneEvent.PresenceChanged]: {
605
+ readonly agents: readonly PresenceEntry[];
606
+ };
476
607
  }
477
608
  declare enum CallEvent {
478
609
  /** Any transition. The catch-all; the specific events below are conveniences. */
@@ -663,18 +794,40 @@ interface Call extends TypedEmitter<CallEventMap> {
663
794
  hold(): Promise<void>;
664
795
  unhold(): Promise<void>;
665
796
  sendDigits(digits: string): void;
666
- /** Blind transfer (SIP REFER). Ends this call. */
797
+ /**
798
+ * Blind transfer (SIP REFER). Ends this call — but **whether the transfer itself succeeded or
799
+ * failed is unknowable from here**: against Zadarma, a REFER's outcome is not reported back to the
800
+ * browser at all (confirmed on real calls — `docs/specs/transfer-availability.md` §2), so a REFER to
801
+ * an unreachable target ends this call exactly the same way a successful one does. Prefer
802
+ * `startConsult`/`completeTransfer` (below) when the caller must not be put at risk — that path
803
+ * correctly reports busy/no-answer/failed because the consult leg is an ordinary outbound call, not
804
+ * a REFER.
805
+ */
667
806
  transfer(target: string): Promise<void>;
668
807
  /**
669
808
  * Attended transfer, part 1: call the target while holding this call.
670
809
  *
671
- * Returns the consult leg — itself a Call, with its own state machine and events.
810
+ * Returns the consult leg — itself a Call, with its own state machine and events. Unlike blind
811
+ * `transfer()`, a consult leg that goes unanswered reports a real reason (busy/no-answer/rejected/
812
+ * failed) and never puts this call at risk — see `docs/specs/transfer-availability.md` §5.
672
813
  */
673
814
  startConsult(target: string): Promise<Call>;
674
- /** Attended transfer, part 2: bridge the two, ending both. (REFER … Replaces) */
815
+ /**
816
+ * Attended transfer, part 2: bridge the two, ending both. (REFER … Replaces)
817
+ *
818
+ * **Confirmed non-functional against Zadarma, on real calls, three times
819
+ * (`docs/specs/transfer-availability.md` §12.3).** Zadarma accepts the REFER…Replaces but never
820
+ * actually bridges the two legs — the consult leg is silently orphaned and the caller is lost, with
821
+ * no error thrown here to signal it. The pattern that DOES work (the same one `/ui`'s own
822
+ * `ConsultScreen` uses): hang up the consult leg, then call `transfer(target)` on THIS call, to the
823
+ * same already-confirmed-answering target — a blind transfer to a target already known to be there
824
+ * works, even though `completeTransfer()` itself does not. Left in the type for hosts against a
825
+ * different (non-Zadarma) SIP provider where REFER…Replaces may work as specified; do not reach for
826
+ * it against Zadarma.
827
+ */
675
828
  completeTransfer(): Promise<void>;
676
829
  /** Abandon the consult and resume this call. */
677
830
  cancelConsult(): Promise<void>;
678
831
  }
679
832
 
680
- export { type AgentsDirectory as A, CALL_TRANSITIONS as C, ERROR_CATALOGUE as E, type FatalErrorCode as F, type HangupReason as H, INITIAL_STATES as I, type LogLevel as L, type NonFatalErrorCode as N, type RemoteParty as R, type SoftphoneConfig as S, type TokenEndpointOptions as T, 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 SoftphoneError as k, type SoftphoneErrorCode as l, SoftphoneEvent as m, type SoftphoneEventMap as n, type SoftphoneState as o, type TokenSource as p, type TypedEmitter as q, canTransition as r, isFatal as s, isTerminal as t, type ContactsSource as u, type HistorySource as v, type CallHistoryEntry as w, type ContactsPage as x };
833
+ export { type AgentsDirectory as A, CALL_TRANSITIONS as C, ERROR_CATALOGUE as E, type FatalErrorCode as F, type HangupReason as H, INITIAL_STATES as I, type LogLevel as L, type NonFatalErrorCode as N, type PresenceEntry as P, type RemoteParty as R, type SoftphoneConfig as S, type TokenEndpointOptions as T, 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 SoftphoneError as k, type SoftphoneErrorCode as l, SoftphoneEvent as m, type SoftphoneEventMap as n, type SoftphoneState as o, type TokenSource as p, type TypedEmitter as q, canTransition as r, isFatal as s, isTerminal as t, type ContactsSource as u, type HistorySource as v, type TransferOutcomesPage as w, type PresenceSource as x, type CallHistoryEntry as y, type ContactsPage as z };
@@ -117,6 +117,19 @@ interface CoventiaBackendConfig {
117
117
  * (SPEC §6.6).
118
118
  */
119
119
  readonly contacts?: ContactsSource;
120
+ /**
121
+ * Transfer outcomes, overriding Coventia's own polling — for a `MockProvider`-style test double, or
122
+ * a future non-Zadarma provider that computes outcomes differently. Not a per-partner customisation
123
+ * point the way `contacts` is: unlike a contact list, a transfer outcome is not a partner's own
124
+ * data, so there is normally no reason to set this (`docs/specs/transfer-outcome-relay.md`, Phase A).
125
+ */
126
+ readonly transferOutcomes?: TransferOutcomeSource;
127
+ /**
128
+ * Live agent presence, overriding Coventia's own WebSocket — for a `MockProvider`-style test
129
+ * double, or a host with its own presence transport. Not a per-partner customisation point the way
130
+ * `contacts` is (`docs/specs/transfer-presence-picker.md`, Phase B).
131
+ */
132
+ readonly presence?: PresenceSource;
120
133
  }
121
134
  interface SoftphoneConfig {
122
135
  readonly tokenSource: TokenSource;
@@ -261,6 +274,104 @@ interface AgentsDirectory {
261
274
  readonly me: PhoneAgent | null;
262
275
  readonly people: readonly PhoneAgent[];
263
276
  }
277
+ /**
278
+ * `'connected'` | `'not-connected'` — whether an attended transfer's second ring, after `Completar`,
279
+ * actually reached the target. Kebab-case to match `HangupReason` (`'no-answer'`, `'transferred'`);
280
+ * the backend's own wire value stays `not_connected` — this is the one place that gets translated.
281
+ */
282
+ type TransferOutcomeStatus = 'connected' | 'not-connected';
283
+ /** One resolved transfer, as `GET /phone/transfers/outcomes` reports it. */
284
+ interface TransferOutcomeEntry {
285
+ /** Opaque, strictly increasing per organization, never re-issued. The cursor for the NEXT poll. */
286
+ readonly id: string;
287
+ /** The extension the call was transferred to. */
288
+ readonly target: string;
289
+ readonly status: TransferOutcomeStatus;
290
+ readonly at: Date;
291
+ }
292
+ interface TransferOutcomesPage {
293
+ /** The newest `id` currently retained, or `'0'` when there are none — pass back as `since`. */
294
+ readonly cursor: string;
295
+ readonly outcomes: readonly TransferOutcomeEntry[];
296
+ }
297
+ /**
298
+ * Poll for transfer outcomes. `since` omitted ⇒ everything currently retained — used for the FIRST
299
+ * poll after starting a watch, which exists only to learn the current cursor (spec §5.3's
300
+ * no-mis-attribution rule: that first poll's own outcomes are discarded, only `cursor` is kept).
301
+ */
302
+ type TransferOutcomeSource = (params: {
303
+ readonly since?: string;
304
+ }) => Promise<TransferOutcomesPage>;
305
+ /**
306
+ * `SoftphoneEvent.TransferOutcome`'s payload. Unlike `TransferOutcomeEntry`, this never carries `id`
307
+ * (a polling-internal concern) and adds `caller` — the backend never stores a caller number (spec
308
+ * §5.3: "no caller number, nothing about other users"), so this is filled in locally, from the `Call`
309
+ * that was transferred, at the moment an outcome is matched to it.
310
+ */
311
+ interface TransferOutcomeEvent {
312
+ readonly status: TransferOutcomeStatus;
313
+ readonly target: string;
314
+ /** The original caller's number, for "Llamar de vuelta" — null if it was never known. */
315
+ readonly caller: string | null;
316
+ readonly at: Date;
317
+ }
318
+ /**
319
+ * `'disponible'` | `'en_llamada'` | `'desconectado'` — Spanish because this is what the picker shows,
320
+ * not an internal code (spec §4.1). `derivePresence` (the backend's own reducer, mirrored nowhere in
321
+ * the SDK — the SDK only ever receives an already-derived state, never `onCall`/`onlineFresh` apart).
322
+ */
323
+ type PresenceState = 'disponible' | 'en_llamada' | 'desconectado';
324
+ /** One org member with a phone extension, as the picker lists them. */
325
+ interface PresenceEntry {
326
+ readonly userId: string;
327
+ readonly extension: string;
328
+ /** `null` when the backend has no name to offer — the picker falls back to the extension. */
329
+ readonly name: string | null;
330
+ readonly state: PresenceState;
331
+ }
332
+ /** What a live presence connection hands back once open. */
333
+ interface PresenceConnection {
334
+ /**
335
+ * Report this softphone's own on-call-ness, so a colleague transferring TO this agent sees it
336
+ * without a round trip through the webhook path (spec §4.1's `onCall` OR-combine). Calling this
337
+ * before the connection is genuinely open is never required of an implementation — a caller (see
338
+ * `presence-socket.ts`) that reports early is expected to wait for the connection to exist first.
339
+ */
340
+ readonly reportCallState: (onCall: boolean) => void;
341
+ /**
342
+ * Says this phone is going away ON PURPOSE (the user turned it off, the app is quitting), so the backend
343
+ * drops it at once instead of waiting out the close grace it gives every ordinary close. Optional: a host's
344
+ * own source that has no such message simply omits it and gets the grace. Sent just before `close()`.
345
+ */
346
+ readonly bye?: () => void;
347
+ /**
348
+ * Ends this connection deliberately. Never causes the SAME callbacks' `onClose` to fire — that is
349
+ * reserved for the connection ending on its OWN (a network drop, a server-side close, a rejected
350
+ * ticket), which is the only thing a caller should ever treat as "reconnect now".
351
+ */
352
+ readonly close: () => void;
353
+ }
354
+ interface PresenceCallbacks {
355
+ /** Once, right after connecting — the full current roster (spec §4.3). */
356
+ readonly onSnapshot: (agents: readonly PresenceEntry[]) => void;
357
+ /** Zero or more times after that — one agent whose state just changed. */
358
+ readonly onDelta: (agent: PresenceEntry) => void;
359
+ /**
360
+ * The connection ended on its own. Never called after the CALLER's own `close()` — see
361
+ * `PresenceConnection.close`'s own note. The only signal a watch layer needs to know it must
362
+ * reconnect, deliberately carrying no reason: a dropped connection and a rejected ticket look
363
+ * identical from here, and both get the same treatment (spec §4.4 — quiet retry, nothing surfaced).
364
+ */
365
+ readonly onClose: () => void;
366
+ }
367
+ /**
368
+ * Opens one live presence connection. Coventia-backed by default (mints a one-time ticket, opens
369
+ * `wss://…/phone/presence`); a host may override it entirely, or omit it — omitted, or while the
370
+ * connection is down, the picker simply has no live data to show (spec §4.4, success criterion #5:
371
+ * no error surfaces). Unlike `TransferOutcomeSource` (poll, get a page, done), this is push-based and
372
+ * long-lived — the returned promise resolves once, when the connection opens, not once per update.
373
+ */
374
+ type PresenceSource = (callbacks: PresenceCallbacks) => Promise<PresenceConnection>;
264
375
 
265
376
  /**
266
377
  * The error taxonomy.
@@ -456,7 +567,23 @@ declare enum SoftphoneEvent {
456
567
  * `StateChanged`: `state` is reachability, this is whether this tab is even allowed to be
457
568
  * reachable at all while another tab holding the same extension stays open.
458
569
  */
459
- LeaderChanged = "leaderChanged"
570
+ LeaderChanged = "leaderChanged",
571
+ /**
572
+ * `Completar` completed a transfer, and the target's SECOND ring either connected or didn't —
573
+ * `docs/specs/transfer-outcome-relay.md` (Phase A). Fires for BOTH outcomes so a host can react to
574
+ * either; only `'not-connected'` is worth surfacing to the agent (spec §5.5) — roughly a minute
575
+ * after the transfer for a target who never answers, seconds for a decline or the caller hanging up
576
+ * first. Requires a `TransferOutcomeSource` (from `coventia`/`apiBase`, or a host override); never
577
+ * fires at all when none is configured, since there is then nothing to poll.
578
+ */
579
+ TransferOutcome = "transferOutcome",
580
+ /**
581
+ * `softphone.presence` changed — the live agent-presence list (Phase B,
582
+ * `docs/specs/transfer-presence-picker.md`) gained a snapshot or an updated entry. Requires a
583
+ * `PresenceSource` (from `coventia`/`apiBase`, or a host override) AND this tab currently being
584
+ * `online && isLeader`; never fires at all otherwise, same reasoning as `TransferOutcome`.
585
+ */
586
+ PresenceChanged = "presenceChanged"
460
587
  }
461
588
  interface SoftphoneEventMap {
462
589
  [SoftphoneEvent.StateChanged]: {
@@ -473,6 +600,10 @@ interface SoftphoneEventMap {
473
600
  [SoftphoneEvent.LeaderChanged]: {
474
601
  readonly isLeader: boolean;
475
602
  };
603
+ [SoftphoneEvent.TransferOutcome]: TransferOutcomeEvent;
604
+ [SoftphoneEvent.PresenceChanged]: {
605
+ readonly agents: readonly PresenceEntry[];
606
+ };
476
607
  }
477
608
  declare enum CallEvent {
478
609
  /** Any transition. The catch-all; the specific events below are conveniences. */
@@ -663,18 +794,40 @@ interface Call extends TypedEmitter<CallEventMap> {
663
794
  hold(): Promise<void>;
664
795
  unhold(): Promise<void>;
665
796
  sendDigits(digits: string): void;
666
- /** Blind transfer (SIP REFER). Ends this call. */
797
+ /**
798
+ * Blind transfer (SIP REFER). Ends this call — but **whether the transfer itself succeeded or
799
+ * failed is unknowable from here**: against Zadarma, a REFER's outcome is not reported back to the
800
+ * browser at all (confirmed on real calls — `docs/specs/transfer-availability.md` §2), so a REFER to
801
+ * an unreachable target ends this call exactly the same way a successful one does. Prefer
802
+ * `startConsult`/`completeTransfer` (below) when the caller must not be put at risk — that path
803
+ * correctly reports busy/no-answer/failed because the consult leg is an ordinary outbound call, not
804
+ * a REFER.
805
+ */
667
806
  transfer(target: string): Promise<void>;
668
807
  /**
669
808
  * Attended transfer, part 1: call the target while holding this call.
670
809
  *
671
- * Returns the consult leg — itself a Call, with its own state machine and events.
810
+ * Returns the consult leg — itself a Call, with its own state machine and events. Unlike blind
811
+ * `transfer()`, a consult leg that goes unanswered reports a real reason (busy/no-answer/rejected/
812
+ * failed) and never puts this call at risk — see `docs/specs/transfer-availability.md` §5.
672
813
  */
673
814
  startConsult(target: string): Promise<Call>;
674
- /** Attended transfer, part 2: bridge the two, ending both. (REFER … Replaces) */
815
+ /**
816
+ * Attended transfer, part 2: bridge the two, ending both. (REFER … Replaces)
817
+ *
818
+ * **Confirmed non-functional against Zadarma, on real calls, three times
819
+ * (`docs/specs/transfer-availability.md` §12.3).** Zadarma accepts the REFER…Replaces but never
820
+ * actually bridges the two legs — the consult leg is silently orphaned and the caller is lost, with
821
+ * no error thrown here to signal it. The pattern that DOES work (the same one `/ui`'s own
822
+ * `ConsultScreen` uses): hang up the consult leg, then call `transfer(target)` on THIS call, to the
823
+ * same already-confirmed-answering target — a blind transfer to a target already known to be there
824
+ * works, even though `completeTransfer()` itself does not. Left in the type for hosts against a
825
+ * different (non-Zadarma) SIP provider where REFER…Replaces may work as specified; do not reach for
826
+ * it against Zadarma.
827
+ */
675
828
  completeTransfer(): Promise<void>;
676
829
  /** Abandon the consult and resume this call. */
677
830
  cancelConsult(): Promise<void>;
678
831
  }
679
832
 
680
- export { type AgentsDirectory as A, CALL_TRANSITIONS as C, ERROR_CATALOGUE as E, type FatalErrorCode as F, type HangupReason as H, INITIAL_STATES as I, type LogLevel as L, type NonFatalErrorCode as N, type RemoteParty as R, type SoftphoneConfig as S, type TokenEndpointOptions as T, 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 SoftphoneError as k, type SoftphoneErrorCode as l, SoftphoneEvent as m, type SoftphoneEventMap as n, type SoftphoneState as o, type TokenSource as p, type TypedEmitter as q, canTransition as r, isFatal as s, isTerminal as t, type ContactsSource as u, type HistorySource as v, type CallHistoryEntry as w, type ContactsPage as x };
833
+ export { type AgentsDirectory as A, CALL_TRANSITIONS as C, ERROR_CATALOGUE as E, type FatalErrorCode as F, type HangupReason as H, INITIAL_STATES as I, type LogLevel as L, type NonFatalErrorCode as N, type PresenceEntry as P, type RemoteParty as R, type SoftphoneConfig as S, type TokenEndpointOptions as T, 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 SoftphoneError as k, type SoftphoneErrorCode as l, SoftphoneEvent as m, type SoftphoneEventMap as n, type SoftphoneState as o, type TokenSource as p, type TypedEmitter as q, canTransition as r, isFatal as s, isTerminal as t, type ContactsSource as u, type HistorySource as v, type TransferOutcomesPage as w, type PresenceSource as x, type CallHistoryEntry as y, type ContactsPage as z };
@@ -7,6 +7,8 @@ var SoftphoneEvent = /* @__PURE__ */ ((SoftphoneEvent2) => {
7
7
  SoftphoneEvent2["DeviceChange"] = "deviceChange";
8
8
  SoftphoneEvent2["Error"] = "error";
9
9
  SoftphoneEvent2["LeaderChanged"] = "leaderChanged";
10
+ SoftphoneEvent2["TransferOutcome"] = "transferOutcome";
11
+ SoftphoneEvent2["PresenceChanged"] = "presenceChanged";
10
12
  return SoftphoneEvent2;
11
13
  })(SoftphoneEvent || {});
12
14
  var CallEvent = /* @__PURE__ */ ((CallEvent2) => {
@@ -25,5 +27,5 @@ var CallEvent = /* @__PURE__ */ ((CallEvent2) => {
25
27
  })(CallEvent || {});
26
28
 
27
29
  export { CallEvent, SoftphoneEvent };
28
- //# sourceMappingURL=chunk-N2N2EQQJ.js.map
29
- //# sourceMappingURL=chunk-N2N2EQQJ.js.map
30
+ //# sourceMappingURL=chunk-7R6LOOAI.js.map
31
+ //# sourceMappingURL=chunk-7R6LOOAI.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/events.ts"],"names":["SoftphoneEvent","CallEvent"],"mappings":";AAcO,IAAK,cAAA,qBAAAA,eAAAA,KAAL;AAEL,EAAAA,gBAAA,cAAA,CAAA,GAAe,cAAA;AAEf,EAAAA,gBAAA,cAAA,CAAA,GAAe,cAAA;AAMf,EAAAA,gBAAA,mBAAA,CAAA,GAAoB,mBAAA;AAQpB,EAAAA,gBAAA,iBAAA,CAAA,GAAkB,iBAAA;AAOlB,EAAAA,gBAAA,cAAA,CAAA,GAAe,cAAA;AAEf,EAAAA,gBAAA,OAAA,CAAA,GAAQ,OAAA;AAMR,EAAAA,gBAAA,eAAA,CAAA,GAAgB,eAAA;AAShB,EAAAA,gBAAA,iBAAA,CAAA,GAAkB,iBAAA;AAOlB,EAAAA,gBAAA,iBAAA,CAAA,GAAkB,iBAAA;AAjDR,EAAA,OAAAA,eAAAA;AAAA,CAAA,EAAA,cAAA,IAAA,EAAA;AAgEL,IAAK,SAAA,qBAAAC,UAAAA,KAAL;AAEL,EAAAA,WAAA,cAAA,CAAA,GAAe,cAAA;AAEf,EAAAA,WAAA,UAAA,CAAA,GAAW,UAAA;AAEX,EAAAA,WAAA,OAAA,CAAA,GAAQ,OAAA;AACR,EAAAA,WAAA,OAAA,CAAA,GAAQ,OAAA;AAER,EAAAA,WAAA,MAAA,CAAA,GAAO,MAAA;AACP,EAAAA,WAAA,QAAA,CAAA,GAAS,QAAA;AAET,EAAAA,WAAA,cAAA,CAAA,GAAe,cAAA;AACf,EAAAA,WAAA,aAAA,CAAA,GAAc,aAAA;AACd,EAAAA,WAAA,YAAA,CAAA,GAAa,YAAA;AAEb,EAAAA,WAAA,oBAAA,CAAA,GAAqB,oBAAA;AAMrB,EAAAA,WAAA,cAAA,CAAA,GAAe,cAAA;AAtBL,EAAA,OAAAA,UAAAA;AAAA,CAAA,EAAA,SAAA,IAAA,EAAA","file":"chunk-7R6LOOAI.js","sourcesContent":["import type { Call, CallState, HangupReason, HoldOriginator } from './call.js';\r\nimport type { SoftphoneError } from './errors.js';\r\nimport type { PresenceEntry, SoftphoneState, TransferOutcomeEvent } from './types.js';\r\n\r\n/**\r\n * Typed events.\r\n *\r\n * Enums rather than magic strings: autocomplete, and rename-safety. Daily's magic strings are the\r\n * counter-example — a typo in `'participant-joined'` is a silent no-op forever.\r\n *\r\n * Each map narrows the listener payload PER EVENT, which is the thing that would have been painful\r\n * to express in JSDoc and is a large part of why this package is TypeScript (SPEC §2.2).\r\n */\r\n\r\nexport enum SoftphoneEvent {\r\n /** Lifecycle. `online` = reachable, NOT SIP-registered — see SoftphoneState. */\r\n StateChanged = 'stateChanged',\r\n /** An INVITE landed. The Call is `ringing`; answer or reject it. */\r\n IncomingCall = 'incomingCall',\r\n /**\r\n * The active call changed — a call began (inbound OR outbound) or ended. Fires for both\r\n * directions, unlike IncomingCall. This is what a UI binds to in order to track \"the current call\"\r\n * without threading the Call through every component.\r\n */\r\n ActiveCallChanged = 'activeCallChanged',\r\n /**\r\n * The browser refused to play audio (autoplay policy).\r\n *\r\n * This is the single most under-documented failure in the industry and it is how calls get MISSED:\r\n * a blocked ringtone rings silently. Surface a button and call `startAudio()` from its click\r\n * handler. Only Twilio and LiveKit document it properly.\r\n */\r\n PlaybackBlocked = 'playbackBlocked',\r\n /**\r\n * `softphone.audio` changed: a device was plugged/unplugged (`inputs`/`outputs`), or the selected\r\n * input/output changed (`currentInputId`/`currentOutputId`). One event for all of it — re-read\r\n * `softphone.audio` rather than inspecting the payload, which carries nothing (`useDevices` does\r\n * exactly this).\r\n */\r\n DeviceChange = 'deviceChange',\r\n /** Fatal or non-fatal — narrow on `error.fatal`. */\r\n Error = 'error',\r\n /**\r\n * This tab's leadership changed (`multi-tab-leader-election.md`) — a DIFFERENT axis from\r\n * `StateChanged`: `state` is reachability, this is whether this tab is even allowed to be\r\n * reachable at all while another tab holding the same extension stays open.\r\n */\r\n LeaderChanged = 'leaderChanged',\r\n /**\r\n * `Completar` completed a transfer, and the target's SECOND ring either connected or didn't —\r\n * `docs/specs/transfer-outcome-relay.md` (Phase A). Fires for BOTH outcomes so a host can react to\r\n * either; only `'not-connected'` is worth surfacing to the agent (spec §5.5) — roughly a minute\r\n * after the transfer for a target who never answers, seconds for a decline or the caller hanging up\r\n * first. Requires a `TransferOutcomeSource` (from `coventia`/`apiBase`, or a host override); never\r\n * fires at all when none is configured, since there is then nothing to poll.\r\n */\r\n TransferOutcome = 'transferOutcome',\r\n /**\r\n * `softphone.presence` changed — the live agent-presence list (Phase B,\r\n * `docs/specs/transfer-presence-picker.md`) gained a snapshot or an updated entry. Requires a\r\n * `PresenceSource` (from `coventia`/`apiBase`, or a host override) AND this tab currently being\r\n * `online && isLeader`; never fires at all otherwise, same reasoning as `TransferOutcome`.\r\n */\r\n PresenceChanged = 'presenceChanged',\r\n}\r\n\r\nexport interface SoftphoneEventMap {\r\n [SoftphoneEvent.StateChanged]: { readonly state: SoftphoneState; readonly previous: SoftphoneState };\r\n [SoftphoneEvent.IncomingCall]: Call;\r\n [SoftphoneEvent.ActiveCallChanged]: { readonly call: Call | null };\r\n [SoftphoneEvent.PlaybackBlocked]: void;\r\n [SoftphoneEvent.DeviceChange]: void;\r\n [SoftphoneEvent.Error]: SoftphoneError;\r\n [SoftphoneEvent.LeaderChanged]: { readonly isLeader: boolean };\r\n [SoftphoneEvent.TransferOutcome]: TransferOutcomeEvent;\r\n [SoftphoneEvent.PresenceChanged]: { readonly agents: readonly PresenceEntry[] };\r\n}\r\n\r\nexport enum CallEvent {\r\n /** Any transition. The catch-all; the specific events below are conveniences. */\r\n StateChanged = 'stateChanged',\r\n /** Media is flowing. */\r\n Accepted = 'accepted',\r\n /** Terminal. Always fires exactly once, whatever the reason. */\r\n Ended = 'ended',\r\n Muted = 'muted',\r\n /** `originator: 'remote'` means the OTHER party held US — new capability; today's softphone is blind to it. */\r\n Held = 'held',\r\n Unheld = 'unheld',\r\n /** Media lost; an ICE restart is in flight. The call is NOT over — show a banner. */\r\n Reconnecting = 'reconnecting',\r\n Reconnected = 'reconnected',\r\n DigitsSent = 'digitsSent',\r\n /** The contact name resolved after the call was already ringing. */\r\n RemotePartyUpdated = 'remotePartyUpdated',\r\n /**\r\n * The remote audio stream is available. Attach it to an `<audio>` element and play it — this is\r\n * how the caller is actually HEARD. Core cannot do this itself (it must not touch `document` —\r\n * SPEC §10.2); the UI layer or the host owns the audio element.\r\n */\r\n RemoteStream = 'remoteStream',\r\n}\r\n\r\nexport interface CallEventMap {\r\n [CallEvent.StateChanged]: { readonly state: CallState; readonly previous: CallState };\r\n [CallEvent.Accepted]: void;\r\n [CallEvent.Ended]: { readonly reason: HangupReason; readonly error?: SoftphoneError };\r\n [CallEvent.Muted]: { readonly muted: boolean };\r\n [CallEvent.Held]: { readonly originator: HoldOriginator };\r\n [CallEvent.Unheld]: { readonly originator: HoldOriginator };\r\n [CallEvent.Reconnecting]: void;\r\n [CallEvent.Reconnected]: void;\r\n [CallEvent.DigitsSent]: { readonly digits: string };\r\n [CallEvent.RemotePartyUpdated]: void;\r\n [CallEvent.RemoteStream]: { readonly stream: MediaStream };\r\n}\r\n\r\n/**\r\n * `on()` returns an unsubscribe function.\r\n *\r\n * Deliberate: it makes the cleanup impossible to get wrong in a React effect\r\n * (`useEffect(() => call.on(...), [call])`) and it avoids Vonage's design, where `on()` returns an\r\n * opaque `symbol` you must retain in order to ever call `off()`.\r\n */\r\nexport interface TypedEmitter<M> {\r\n on<E extends keyof M>(event: E, listener: (payload: M[E]) => void): () => void;\r\n once<E extends keyof M>(event: E, listener: (payload: M[E]) => void): () => void;\r\n off<E extends keyof M>(event: E, listener: (payload: M[E]) => void): void;\r\n}\r\n"]}