@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
@@ -46,6 +46,31 @@ function useIsLeader() {
46
46
  react.useCallback(() => phone.isLeader, [phone])
47
47
  );
48
48
  }
49
+ function useLine() {
50
+ const phone = useSoftphone();
51
+ const cache = react.useRef(null);
52
+ return react.useSyncExternalStore(
53
+ react.useCallback(
54
+ (cb) => {
55
+ const offLine = phone.on("lineChanged" /* LineChanged */, () => cb());
56
+ const offClaim = phone.on("lineClaimChanged" /* LineClaimChanged */, () => cb());
57
+ return () => {
58
+ offLine();
59
+ offClaim();
60
+ };
61
+ },
62
+ [phone]
63
+ ),
64
+ react.useCallback(() => {
65
+ const { line, lineWhere: where, lineClaim: claim } = phone;
66
+ const cached = cache.current;
67
+ if (cached?.line === line && cached.where === where && cached.claim === claim) return cached;
68
+ const view = { line, where, claim };
69
+ cache.current = view;
70
+ return view;
71
+ }, [phone])
72
+ );
73
+ }
49
74
  function useActiveCall() {
50
75
  const phone = useSoftphone();
51
76
  return react.useSyncExternalStore(
@@ -148,6 +173,7 @@ exports.useCall = useCall;
148
173
  exports.useDevices = useDevices;
149
174
  exports.useIsLeader = useIsLeader;
150
175
  exports.useLastError = useLastError;
176
+ exports.useLine = useLine;
151
177
  exports.usePresence = usePresence;
152
178
  exports.useSoftphone = useSoftphone;
153
179
  exports.useSoftphoneState = useSoftphoneState;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/react/context.tsx","../../src/react/hooks.ts"],"names":["createContext","useContext","useSyncExternalStore","useCallback","useRef"],"mappings":"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","file":"index.cjs","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 currentRingtoneOutputId: audio.currentRingtoneOutputId,\r\n canSelectOutput: audio.canSelectOutput,\r\n canSelectRingtoneOutput: audio.canSelectRingtoneOutput,\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"]}
1
+ {"version":3,"sources":["../../src/react/context.tsx","../../src/react/hooks.ts"],"names":["createContext","useContext","useSyncExternalStore","useCallback","useRef"],"mappings":"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","file":"index.cjs","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 { LineClaim, LineLocation, LineState, 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 * Where this extension's calls ring, who has them when it is not here, and how the last attempt to bring them\r\n * here went — see `useLine`.\r\n */\r\nexport interface LineView {\r\n readonly line: LineState;\r\n readonly where: LineLocation | null;\r\n readonly claim: LineClaim;\r\n}\r\n\r\n/**\r\n * Where this extension's incoming calls ring (`phone.line` / `phone.lineWhere` / `phone.lineClaim`,\r\n * `docs/specs/line-owner.md` §5) — reactive on `SoftphoneEvent.LineChanged` and `LineClaimChanged`.\r\n * `elsewhere` is what puts the softphone in `standby`; show your own \"your phone is active elsewhere\" state\r\n * with `takeLine()` behind it, or let the `/ui` widget's do it.\r\n *\r\n * The snapshot is the SAME object until a field changes — compared by value, not rebuilt per read, since\r\n * `useSyncExternalStore` re-renders forever on a fresh object every time.\r\n */\r\nexport function useLine(): LineView {\r\n const phone = useSoftphone();\r\n const cache = useRef<LineView | null>(null);\r\n return useSyncExternalStore(\r\n useCallback(\r\n (cb: () => void) => {\r\n const offLine = phone.on(SoftphoneEvent.LineChanged, () => cb());\r\n const offClaim = phone.on(SoftphoneEvent.LineClaimChanged, () => cb());\r\n return () => {\r\n offLine();\r\n offClaim();\r\n };\r\n },\r\n [phone],\r\n ),\r\n useCallback((): LineView => {\r\n const { line, lineWhere: where, lineClaim: claim } = phone;\r\n const cached = cache.current;\r\n if (cached?.line === line && cached.where === where && cached.claim === claim) return cached;\r\n const view: LineView = { line, where, claim };\r\n cache.current = view;\r\n return view;\r\n }, [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 the softphone — open while it is started and this tab leads (spec's\r\n * own 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 currentRingtoneOutputId: audio.currentRingtoneOutputId,\r\n canSelectOutput: audio.canSelectOutput,\r\n canSelectRingtoneOutput: audio.canSelectRingtoneOutput,\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"]}
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { S as Softphone, A as AudioDevices } from '../softphone-CY5d2WWY.cjs';
4
- import { f as CallState, R as RemoteParty, a as Call, h as FatalSoftphoneError, P as PresenceEntry, w as SoftphoneState } from '../call-C6YpT6uR.cjs';
3
+ import { S as Softphone, A as AudioDevices } from '../softphone-JZvJdzU3.cjs';
4
+ import { f as CallState, R as RemoteParty, k as LineState, j as LineLocation, L as LineClaim, a as Call, h as FatalSoftphoneError, P as PresenceEntry, z as SoftphoneState } from '../call-FqcvDg1t.cjs';
5
5
 
6
6
  interface SoftphoneProviderProps {
7
7
  readonly softphone: Softphone;
@@ -31,6 +31,25 @@ declare function useSoftphoneState(): SoftphoneState;
31
31
  * tab" state for that duration; the `/ui` widget already does.
32
32
  */
33
33
  declare function useIsLeader(): boolean;
34
+ /**
35
+ * Where this extension's calls ring, who has them when it is not here, and how the last attempt to bring them
36
+ * here went — see `useLine`.
37
+ */
38
+ interface LineView {
39
+ readonly line: LineState;
40
+ readonly where: LineLocation | null;
41
+ readonly claim: LineClaim;
42
+ }
43
+ /**
44
+ * Where this extension's incoming calls ring (`phone.line` / `phone.lineWhere` / `phone.lineClaim`,
45
+ * `docs/specs/line-owner.md` §5) — reactive on `SoftphoneEvent.LineChanged` and `LineClaimChanged`.
46
+ * `elsewhere` is what puts the softphone in `standby`; show your own "your phone is active elsewhere" state
47
+ * with `takeLine()` behind it, or let the `/ui` widget's do it.
48
+ *
49
+ * The snapshot is the SAME object until a field changes — compared by value, not rebuilt per read, since
50
+ * `useSyncExternalStore` re-renders forever on a fresh object every time.
51
+ */
52
+ declare function useLine(): LineView;
34
53
  /**
35
54
  * The current call, or null. Re-renders when it changes — inbound, outbound, or ended.
36
55
  *
@@ -40,8 +59,8 @@ declare function useActiveCall(): Call | null;
40
59
  /**
41
60
  * The live agent-presence list (`phone.presence`, Phase B) — reactive on `SoftphoneEvent.
42
61
  * PresenceChanged`. `active` gates the SUBSCRIPTION only, not the underlying connection: the socket
43
- * itself stays governed entirely by `online && isLeader` inside the softphone (spec's own
44
- * always-connected decision), so passing `false` here does not close it — it only stops this
62
+ * itself stays governed entirely by the softphone — open while it is started and this tab leads (spec's
63
+ * own always-connected decision) — so passing `false` here does not close it — it only stops this
45
64
  * particular component from re-rendering on every update while it has no reason to care, e.g. the
46
65
  * transfer step being closed (spec §4.5: "subscribe when the step opens... unsubscribe when it
47
66
  * closes"). `phone.presence` is still read fresh on every call, so flipping `active` back to `true`
@@ -76,4 +95,4 @@ declare function useCall(call: Call | null): CallView | null;
76
95
  */
77
96
  declare function useDevices(): AudioDevices;
78
97
 
79
- export { type CallView, SoftphoneProvider, type SoftphoneProviderProps, useActiveCall, useCall, useDevices, useIsLeader, useLastError, usePresence, useSoftphone, useSoftphoneState };
98
+ export { type CallView, type LineView, SoftphoneProvider, type SoftphoneProviderProps, useActiveCall, useCall, useDevices, useIsLeader, useLastError, useLine, usePresence, useSoftphone, useSoftphoneState };
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode } from 'react';
3
- import { S as Softphone, A as AudioDevices } from '../softphone-DXgmTyrX.js';
4
- import { f as CallState, R as RemoteParty, a as Call, h as FatalSoftphoneError, P as PresenceEntry, w as SoftphoneState } from '../call-C6YpT6uR.js';
3
+ import { S as Softphone, A as AudioDevices } from '../softphone-By9sizlV.js';
4
+ import { f as CallState, R as RemoteParty, k as LineState, j as LineLocation, L as LineClaim, a as Call, h as FatalSoftphoneError, P as PresenceEntry, z as SoftphoneState } from '../call-FqcvDg1t.js';
5
5
 
6
6
  interface SoftphoneProviderProps {
7
7
  readonly softphone: Softphone;
@@ -31,6 +31,25 @@ declare function useSoftphoneState(): SoftphoneState;
31
31
  * tab" state for that duration; the `/ui` widget already does.
32
32
  */
33
33
  declare function useIsLeader(): boolean;
34
+ /**
35
+ * Where this extension's calls ring, who has them when it is not here, and how the last attempt to bring them
36
+ * here went — see `useLine`.
37
+ */
38
+ interface LineView {
39
+ readonly line: LineState;
40
+ readonly where: LineLocation | null;
41
+ readonly claim: LineClaim;
42
+ }
43
+ /**
44
+ * Where this extension's incoming calls ring (`phone.line` / `phone.lineWhere` / `phone.lineClaim`,
45
+ * `docs/specs/line-owner.md` §5) — reactive on `SoftphoneEvent.LineChanged` and `LineClaimChanged`.
46
+ * `elsewhere` is what puts the softphone in `standby`; show your own "your phone is active elsewhere" state
47
+ * with `takeLine()` behind it, or let the `/ui` widget's do it.
48
+ *
49
+ * The snapshot is the SAME object until a field changes — compared by value, not rebuilt per read, since
50
+ * `useSyncExternalStore` re-renders forever on a fresh object every time.
51
+ */
52
+ declare function useLine(): LineView;
34
53
  /**
35
54
  * The current call, or null. Re-renders when it changes — inbound, outbound, or ended.
36
55
  *
@@ -40,8 +59,8 @@ declare function useActiveCall(): Call | null;
40
59
  /**
41
60
  * The live agent-presence list (`phone.presence`, Phase B) — reactive on `SoftphoneEvent.
42
61
  * PresenceChanged`. `active` gates the SUBSCRIPTION only, not the underlying connection: the socket
43
- * itself stays governed entirely by `online && isLeader` inside the softphone (spec's own
44
- * always-connected decision), so passing `false` here does not close it — it only stops this
62
+ * itself stays governed entirely by the softphone — open while it is started and this tab leads (spec's
63
+ * own always-connected decision) — so passing `false` here does not close it — it only stops this
45
64
  * particular component from re-rendering on every update while it has no reason to care, e.g. the
46
65
  * transfer step being closed (spec §4.5: "subscribe when the step opens... unsubscribe when it
47
66
  * closes"). `phone.presence` is still read fresh on every call, so flipping `active` back to `true`
@@ -76,4 +95,4 @@ declare function useCall(call: Call | null): CallView | null;
76
95
  */
77
96
  declare function useDevices(): AudioDevices;
78
97
 
79
- export { type CallView, SoftphoneProvider, type SoftphoneProviderProps, useActiveCall, useCall, useDevices, useIsLeader, useLastError, usePresence, useSoftphone, useSoftphoneState };
98
+ export { type CallView, type LineView, SoftphoneProvider, type SoftphoneProviderProps, useActiveCall, useCall, useDevices, useIsLeader, useLastError, useLine, usePresence, useSoftphone, useSoftphoneState };
@@ -1,4 +1,4 @@
1
- export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, usePresence, useSoftphone, useSoftphoneState } from '../chunk-UHNLEBBO.js';
2
- import '../chunk-7R6LOOAI.js';
1
+ export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, useLine, usePresence, useSoftphone, useSoftphoneState } from '../chunk-I3RSC52R.js';
2
+ import '../chunk-N4PLEYFJ.js';
3
3
  //# sourceMappingURL=index.js.map
4
4
  //# sourceMappingURL=index.js.map
@@ -1,4 +1,4 @@
1
- import { D as ContactsSource, G as HistorySource, g as Contact, J as AgentsDirectory, K as TransferOutcomesPage, M as PresenceSource, r as RingtoneSource, o as OutboundNumbersSource, y as TypedEmitter, v as SoftphoneEventMap, w as SoftphoneState, h as FatalSoftphoneError, e as CallOptions, a as Call, P as PresenceEntry } from './call-C6YpT6uR.cjs';
1
+ import { K as ContactsSource, M as HistorySource, g as Contact, Q as AgentsDirectory, U as TransferOutcomesPage, W as PresenceSource, u as RingtoneSource, r as OutboundNumbersSource, B as TypedEmitter, y as SoftphoneEventMap, z as SoftphoneState, h as FatalSoftphoneError, k as LineState, j as LineLocation, L as LineClaim, e as CallOptions, a as Call, P as PresenceEntry } from './call-FqcvDg1t.js';
2
2
 
3
3
  /** Contacts, history, caller ID, the agents directory, transfer outcomes, presence and ringtone
4
4
  * settings, all from one config. */
@@ -116,6 +116,39 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
116
116
  * "active in another tab" state for that duration (the `/ui` widget already does).
117
117
  */
118
118
  readonly isLeader: boolean;
119
+ /**
120
+ * Where this extension's incoming calls ring (`docs/specs/line-owner.md` §5) — the axis ACROSS browsers,
121
+ * devices and the desktop app that `isLeader` cannot see. Only one softphone of an extension listens:
122
+ * the first one started keeps the line, and the others wait in `standby` until it is free or the user
123
+ * takes it (`takeLine()`).
124
+ *
125
+ * `unknown` until the backend has said, and whenever it cannot (no presence source, a backend that
126
+ * predates the line owner, a presence socket that is down): the softphone then listens, exactly as
127
+ * before this existed — it never leaves nobody listening. Changes fire `SoftphoneEvent.LineChanged`.
128
+ */
129
+ readonly line: LineState;
130
+ /**
131
+ * Who has the line while it is `elsewhere` — the desktop app, or another softphone (a browser tab) —
132
+ * or `null`: the line is here, unknown, or the backend did not say.
133
+ */
134
+ readonly lineWhere: LineLocation | null;
135
+ /**
136
+ * How the last attempt to bring the line here went, whoever made it (`takeLine()`, or a `call()` from
137
+ * `standby`) — what the waiting screen shows under its title, so a refused click-to-call explains itself
138
+ * just like a refused "Recibir aquí". Changes fire `SoftphoneEvent.LineClaimChanged`.
139
+ */
140
+ readonly lineClaim: LineClaim;
141
+ /**
142
+ * "Recibir aquí": takes the line from whichever softphone has it, so incoming calls ring here. Resolves
143
+ * once the line IS here (at once when it already is); the softphone then leaves `standby` and starts
144
+ * listening on its own.
145
+ *
146
+ * Rejects with a non-fatal `SoftphoneError`: `line/claim-refused` while the user is on a call on the
147
+ * softphone that has the line (it never moves during a call — retry once it ends), or `line/unavailable`
148
+ * when nothing could decide (no presence connection, a backend without the line owner, no answer in
149
+ * time). Neither is emitted on `SoftphoneEvent.Error` — this promise is the answer.
150
+ */
151
+ takeLine(): Promise<void>;
119
152
  /**
120
153
  * Start.
121
154
  *
@@ -128,6 +161,11 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
128
161
  stop(): Promise<void>;
129
162
  /** Idempotent. After this the instance is dead — construct a new one. */
130
163
  destroy(): Promise<void>;
164
+ /**
165
+ * Dial. From `standby` this takes the line first (`takeLine()`), then dials — a call the host starts is
166
+ * the user asking for the phone here — and rejects with that method's `line/*` error if the line cannot
167
+ * be taken.
168
+ */
131
169
  call(destination: string, options?: CallOptions): Promise<Call>;
132
170
  /** The live call, or null. The consult leg of an attended transfer is reachable from it. */
133
171
  readonly activeCall: Call | null;
@@ -142,8 +180,9 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
142
180
  /**
143
181
  * The live agent-presence list (Phase B, `docs/specs/transfer-presence-picker.md`) — every org
144
182
  * member with a phone extension, whatever their state. Empty when there is no `PresenceSource`
145
- * (no Coventia backend, or a host that omitted one), or while this tab is not `online && isLeader`
146
- * — never an error, just nothing to show (spec §4.4's own success criterion). Updates fire
183
+ * (no Coventia backend, or a host that omitted one), or while this tab is not a started leader —
184
+ * `online`, or waiting in `standby` for the line — or its connection is down: never an error, just
185
+ * nothing to show (spec §4.4's own success criterion). Updates fire
147
186
  * `SoftphoneEvent.PresenceChanged`; `usePresence()` is the reactive read.
148
187
  */
149
188
  readonly presence: readonly PresenceEntry[];
@@ -1,4 +1,4 @@
1
- import { D as ContactsSource, G as HistorySource, g as Contact, J as AgentsDirectory, K as TransferOutcomesPage, M as PresenceSource, r as RingtoneSource, o as OutboundNumbersSource, y as TypedEmitter, v as SoftphoneEventMap, w as SoftphoneState, h as FatalSoftphoneError, e as CallOptions, a as Call, P as PresenceEntry } from './call-C6YpT6uR.js';
1
+ import { K as ContactsSource, M as HistorySource, g as Contact, Q as AgentsDirectory, U as TransferOutcomesPage, W as PresenceSource, u as RingtoneSource, r as OutboundNumbersSource, B as TypedEmitter, y as SoftphoneEventMap, z as SoftphoneState, h as FatalSoftphoneError, k as LineState, j as LineLocation, L as LineClaim, e as CallOptions, a as Call, P as PresenceEntry } from './call-FqcvDg1t.cjs';
2
2
 
3
3
  /** Contacts, history, caller ID, the agents directory, transfer outcomes, presence and ringtone
4
4
  * settings, all from one config. */
@@ -116,6 +116,39 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
116
116
  * "active in another tab" state for that duration (the `/ui` widget already does).
117
117
  */
118
118
  readonly isLeader: boolean;
119
+ /**
120
+ * Where this extension's incoming calls ring (`docs/specs/line-owner.md` §5) — the axis ACROSS browsers,
121
+ * devices and the desktop app that `isLeader` cannot see. Only one softphone of an extension listens:
122
+ * the first one started keeps the line, and the others wait in `standby` until it is free or the user
123
+ * takes it (`takeLine()`).
124
+ *
125
+ * `unknown` until the backend has said, and whenever it cannot (no presence source, a backend that
126
+ * predates the line owner, a presence socket that is down): the softphone then listens, exactly as
127
+ * before this existed — it never leaves nobody listening. Changes fire `SoftphoneEvent.LineChanged`.
128
+ */
129
+ readonly line: LineState;
130
+ /**
131
+ * Who has the line while it is `elsewhere` — the desktop app, or another softphone (a browser tab) —
132
+ * or `null`: the line is here, unknown, or the backend did not say.
133
+ */
134
+ readonly lineWhere: LineLocation | null;
135
+ /**
136
+ * How the last attempt to bring the line here went, whoever made it (`takeLine()`, or a `call()` from
137
+ * `standby`) — what the waiting screen shows under its title, so a refused click-to-call explains itself
138
+ * just like a refused "Recibir aquí". Changes fire `SoftphoneEvent.LineClaimChanged`.
139
+ */
140
+ readonly lineClaim: LineClaim;
141
+ /**
142
+ * "Recibir aquí": takes the line from whichever softphone has it, so incoming calls ring here. Resolves
143
+ * once the line IS here (at once when it already is); the softphone then leaves `standby` and starts
144
+ * listening on its own.
145
+ *
146
+ * Rejects with a non-fatal `SoftphoneError`: `line/claim-refused` while the user is on a call on the
147
+ * softphone that has the line (it never moves during a call — retry once it ends), or `line/unavailable`
148
+ * when nothing could decide (no presence connection, a backend without the line owner, no answer in
149
+ * time). Neither is emitted on `SoftphoneEvent.Error` — this promise is the answer.
150
+ */
151
+ takeLine(): Promise<void>;
119
152
  /**
120
153
  * Start.
121
154
  *
@@ -128,6 +161,11 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
128
161
  stop(): Promise<void>;
129
162
  /** Idempotent. After this the instance is dead — construct a new one. */
130
163
  destroy(): Promise<void>;
164
+ /**
165
+ * Dial. From `standby` this takes the line first (`takeLine()`), then dials — a call the host starts is
166
+ * the user asking for the phone here — and rejects with that method's `line/*` error if the line cannot
167
+ * be taken.
168
+ */
131
169
  call(destination: string, options?: CallOptions): Promise<Call>;
132
170
  /** The live call, or null. The consult leg of an attended transfer is reachable from it. */
133
171
  readonly activeCall: Call | null;
@@ -142,8 +180,9 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
142
180
  /**
143
181
  * The live agent-presence list (Phase B, `docs/specs/transfer-presence-picker.md`) — every org
144
182
  * member with a phone extension, whatever their state. Empty when there is no `PresenceSource`
145
- * (no Coventia backend, or a host that omitted one), or while this tab is not `online && isLeader`
146
- * — never an error, just nothing to show (spec §4.4's own success criterion). Updates fire
183
+ * (no Coventia backend, or a host that omitted one), or while this tab is not a started leader —
184
+ * `online`, or waiting in `standby` for the line — or its connection is down: never an error, just
185
+ * nothing to show (spec §4.4's own success criterion). Updates fire
147
186
  * `SoftphoneEvent.PresenceChanged`; `usePresence()` is the reactive read.
148
187
  */
149
188
  readonly presence: readonly PresenceEntry[];
@@ -152,6 +152,7 @@ var toPresenceEntry = (row) => ({
152
152
  name: row.name ?? null,
153
153
  state: toPresenceState(row.state)
154
154
  });
155
+ var toLineLocation = (raw) => raw === "desktop" || raw === "browser" ? raw : null;
155
156
  var toWebSocketUrl = (httpUrl) => httpUrl.replace(/^http/, "ws").replace(/\/+$/, "");
156
157
  function coventiaSources(config) {
157
158
  const get = createGet(config);
@@ -280,6 +281,16 @@ function coventiaSources(config) {
280
281
  if (ws.readyState !== WS.OPEN) return;
281
282
  ws.send(JSON.stringify({ type: "bye" }));
282
283
  },
284
+ // `line: true` is what makes the server treat this socket as taking part in the line owner;
285
+ // without it (or with an id it rejects) the `hello` is ignored, as an older SDK's would be.
286
+ hello: (clientId) => {
287
+ if (ws.readyState !== WS.OPEN) return;
288
+ ws.send(JSON.stringify({ type: "hello", clientId, line: true }));
289
+ },
290
+ claimLine: () => {
291
+ if (ws.readyState !== WS.OPEN) return;
292
+ ws.send(JSON.stringify({ type: "line-claim" }));
293
+ },
283
294
  close: () => {
284
295
  ws.onclose = null;
285
296
  ws.onerror = null;
@@ -299,6 +310,12 @@ function coventiaSources(config) {
299
310
  callbacks.onSnapshot((msg.agents ?? []).map(toPresenceEntry));
300
311
  } else if (msg.type === "delta" && msg.agent) {
301
312
  callbacks.onDelta(toPresenceEntry(msg.agent));
313
+ } else if (msg.type === "line" && (msg.state === "here" || msg.state === "elsewhere")) {
314
+ callbacks.onLine?.({ state: msg.state, where: msg.state === "here" ? null : toLineLocation(msg.where) });
315
+ } else if (msg.type === "line-claim-result") {
316
+ callbacks.onClaimResult?.(
317
+ msg.ok === true ? { ok: true } : { ok: false, reason: typeof msg.reason === "string" ? msg.reason : "unavailable" }
318
+ );
302
319
  }
303
320
  };
304
321
  ws.onclose = () => {
@@ -444,5 +461,5 @@ function coventiaSources(config) {
444
461
  }
445
462
 
446
463
  export { coventiaSources, resolverFromContacts };
447
- //# sourceMappingURL=sources-ONYQW733.js.map
448
- //# sourceMappingURL=sources-ONYQW733.js.map
464
+ //# sourceMappingURL=sources-QL2LRZVH.js.map
465
+ //# sourceMappingURL=sources-QL2LRZVH.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/sources.ts","../src/coventia/api.ts","../src/coventia/sources.ts"],"names":["resolve"],"mappings":"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","file":"sources-QL2LRZVH.js","sourcesContent":["/**\r\n * **Internal.** Not exported from any entry point.\r\n *\r\n * The rules that are about **phone numbers and names** rather than about any backend: universal,\r\n * easy to get subtly wrong, and worth getting right once.\r\n *\r\n * These were public while a host had to build their own sources. They no longer do — a provider\r\n * supplies `coventia.contacts` and everything else (caller ID, history names) is derived from it in\r\n * `coventia/sources.ts`, the only caller left. Exported API is a contract owed forever, so it should\r\n * not outlive its consumers.\r\n */\r\nimport type { Contact } from './core/types.js';\r\nimport type { CallHistoryEntry, HistorySource } from './ui/types.js';\r\n\r\n/**\r\n * A number reduced to the digits telephony actually matches on — `'+34 600 111 222'` → `'34600111222'`.\r\n *\r\n * Comparing numbers as raw strings is the bug this exists to prevent: the same line arrives as\r\n * `+34600111222` from a SIP header, `34600111222` from a database, and `+34 600 111 222` from a human,\r\n * and all three are one contact.\r\n */\r\nexport function toDigits(number: string): string {\r\n return String(number ?? '').replace(/\\D/g, '');\r\n}\r\n\r\n/**\r\n * Decide whether a resolved string is really a **name** — the \"don't echo the bare number\" rule.\r\n *\r\n * A contact saved as its own number (\"+34 600 111 222\") resolves to a \"name\" that is just the number\r\n * reformatted. Showing it puts `+34600111222` where a name belongs, or worse, a *differently*\r\n * formatted copy of the number the widget is already displaying an inch away.\r\n *\r\n * This is the SDK deciding what it is willing to call a name, so it holds for every host — not only\r\n * for a backend that happens to be careful. Returns `null` when there is no name worth showing, which\r\n * is the signal to keep displaying the number.\r\n */\r\nexport function normalizeResolvedName(raw: string | null | undefined, number: string): string | null {\r\n if (!raw) return null;\r\n const trimmed = String(raw).trim();\r\n if (!trimmed) return null;\r\n const digits = toDigits(number);\r\n if (!digits) return trimmed;\r\n return toDigits(trimmed) === digits ? null : trimmed;\r\n}\r\n\r\n/**\r\n * Wrap a `HistorySource` so its entries get caller names, resolved once per distinct number.\r\n *\r\n * A call log is the same handful of numbers over and over, so the naive \"resolve each row\" costs a\r\n * request per row and hammers the backend with duplicates. This dedupes **by digits** (see\r\n * `toDigits` — `+34600111222` and `34600111222` are one lookup), caches across pages, and remembers\r\n * misses so an unknown number is not re-asked forever.\r\n *\r\n * A failed lookup leaves the entry's `name` null and is NOT cached, so a later page retries: the\r\n * number is still callable, and an unreachable name service must not empty the tab.\r\n *\r\n * const history = withNameResolution(myHistorySource, myResolveContact);\r\n */\r\nexport function withNameResolution(\r\n source: HistorySource,\r\n resolve: (number: string, signal?: AbortSignal) => Promise<Contact | null>,\r\n): HistorySource {\r\n const cache = new Map<string, string | null>();\r\n\r\n return async (params) => {\r\n const entries = await source(params);\r\n\r\n // Only entries that still NEED a name. A source that already knows one (its own join, its own\r\n // cache) must not cost a lookup — the numbers it named are not our business.\r\n const wanted = [\r\n ...new Set(entries.filter((e) => e.name === null).map((e) => toDigits(e.number))),\r\n ].filter((d) => d !== '' && !cache.has(d));\r\n await Promise.all(\r\n wanted.map(async (digits) => {\r\n try {\r\n const contact = await resolve(digits);\r\n cache.set(digits, normalizeResolvedName(contact?.name ?? null, digits));\r\n } catch {\r\n // Deliberately not cached — a transient failure must not become a permanent blank.\r\n }\r\n }),\r\n );\r\n\r\n return entries.map((e): CallHistoryEntry => {\r\n // Never overwrite a name the source already knew.\r\n if (e.name !== null) return e;\r\n const resolved = cache.get(toDigits(e.number)) ?? null;\r\n return resolved === null ? e : { ...e, name: resolved };\r\n });\r\n };\r\n}\r\n","import { invalidateToken } from '../core/token-cache.js';\r\nimport type { TokenSource } from '../core/types.js';\r\n\r\n/**\r\n * A failed backend call. `status` is the HTTP status, or 0 when the request never completed. `body` is the parsed\r\n * response, when there was one — some refusals carry structured detail (e.g. `retryAfterSeconds`).\r\n */\r\nexport class ApiError extends Error {\r\n readonly status: number;\r\n readonly body: unknown;\r\n\r\n constructor(status: number, message: string, body: unknown = null) {\r\n super(message);\r\n this.name = 'ApiError';\r\n this.status = status;\r\n this.body = body;\r\n }\r\n}\r\n\r\nexport type QueryParams = Readonly<Record<string, string | number | undefined>>;\r\n\r\n/**\r\n * A minimal authenticated GET against Coventia's phone service.\r\n *\r\n * Lives under `coventia/` rather than `core/`: the core ships no HTTP at all, so a partner on their\r\n * own backend never loads this.\r\n */\r\ninterface RequestConfig {\r\n readonly baseUrl: string;\r\n readonly tokenSource: TokenSource;\r\n readonly fetch?: typeof globalThis.fetch;\r\n}\r\n\r\n/**\r\n * Shared by `createGet` and `createPost`: one attempt against the backend, with the 401-retry-once\r\n * policy both need (see `createGet`'s own note on why). `method` is the only thing that differs\r\n * between the two — this backend's phone routes take every parameter as a query string, GET or POST\r\n * alike (the presence ticket mint is a POST with no body, `organization_id` on the query, same as any\r\n * GET here), so there is nothing else for a request body to carry.\r\n */\r\nfunction createRequest(config: RequestConfig, method: 'GET' | 'POST') {\r\n // Bound, not passed by reference: an unbound `fetch` throws \"Illegal invocation\" in some browsers.\r\n const doFetch: typeof globalThis.fetch = config.fetch\r\n ? config.fetch\r\n : (...args) => globalThis.fetch(...args);\r\n const base = config.baseUrl.replace(/\\/+$/, '');\r\n\r\n /**\r\n * One attempt. Split out so a 401 can be retried exactly once with a fresh token — see the\r\n * returned function's own note.\r\n */\r\n async function attempt<T>(path: string, params: QueryParams, signal?: AbortSignal): Promise<T> {\r\n const { token } = await config.tokenSource.getToken(signal);\r\n const qs = new URLSearchParams();\r\n for (const [k, v] of Object.entries(params)) {\r\n if (v !== undefined && v !== '') qs.set(k, String(v));\r\n }\r\n const query = qs.toString();\r\n\r\n const res = await doFetch(`${base}${path}${query ? `?${query}` : ''}`, {\r\n method,\r\n // The RAW token, no `Bearer ` prefix — see `VoiceToken.token` (SPEC §6.1).\r\n headers: { Authorization: token },\r\n ...(signal ? { signal } : {}),\r\n });\r\n\r\n const body: unknown = await res.json().catch(() => null);\r\n const message = (body as { message?: string } | null)?.message ?? `${path} — HTTP ${res.status}`;\r\n\r\n if (!res.ok) throw new ApiError(res.status, message, body);\r\n // This backend answers 200 with `{ success: false }` on handled failures, so the status alone is\r\n // not enough to know a call worked.\r\n if ((body as { success?: boolean } | null)?.success === false) {\r\n throw new ApiError(res.status, message, body);\r\n }\r\n return body as T;\r\n }\r\n\r\n /**\r\n * Retry once on 401, with a freshly minted token.\r\n *\r\n * A cached token can be rejected before its nominal expiry — revoked on logout, or invalidated when\r\n * the extension is re-provisioned. Without this the cache would keep handing back the dead one for\r\n * up to its full TTL, so `revokeVoiceTokensForUser` would not actually stop a running softphone.\r\n *\r\n * Exactly one retry: a second 401 means the credential itself is refused, not stale, and retrying\r\n * would be a loop against the host's token endpoint.\r\n */\r\n return async function request<T>(path: string, params: QueryParams = {}, signal?: AbortSignal): Promise<T> {\r\n try {\r\n return await attempt<T>(path, params, signal);\r\n } catch (error) {\r\n if (!(error instanceof ApiError) || error.status !== 401) throw error;\r\n invalidateToken(config.tokenSource);\r\n return attempt<T>(path, params, signal);\r\n }\r\n };\r\n}\r\n\r\n/**\r\n * A minimal authenticated GET against Coventia's phone service.\r\n *\r\n * Lives under `coventia/` rather than `core/`: the core ships no HTTP at all, so a partner on their\r\n * own backend never loads this.\r\n */\r\nexport function createGet(config: RequestConfig) {\r\n return createRequest(config, 'GET');\r\n}\r\n\r\n/** The POST counterpart — today, only the presence ticket mint (`POST /presence/ticket`) uses it. */\r\nexport function createPost(config: RequestConfig) {\r\n return createRequest(config, 'POST');\r\n}\r\n","import { normalizeResolvedName, toDigits, withNameResolution } from '../sources.js';\r\nimport { ApiError, createGet, createPost } from './api.js';\r\nimport { outboundNumberRefused } from '../core/outbound-numbers.js';\r\nimport type {\r\n Contact,\r\n LineLocation,\r\n OutboundNumberKind,\r\n OutboundNumberSettings,\r\n OutboundNumberState,\r\n OutboundNumbersSource,\r\n PresenceCallbacks,\r\n PresenceConnection,\r\n PresenceEntry,\r\n PresenceSource,\r\n PresenceState,\r\n RingtoneSettings,\r\n RingtoneSource,\r\n TokenSource,\r\n TransferOutcomeEntry,\r\n TransferOutcomesPage,\r\n TransferOutcomeSource,\r\n} from '../core/types.js';\r\nimport type {\r\n AgentsDirectory,\r\n CallDetail,\r\n CallHandler,\r\n CallHistoryEntry,\r\n CallOutcome,\r\n ContactsPage,\r\n ContactsSource,\r\n HistorySource,\r\n} from '../ui/types.js';\r\n\r\nexport interface CoventiaSourcesConfig {\r\n /** Coventia's domain, already normalised by `createSoftphone` (no path, no trailing slash). */\r\n readonly baseUrl: string;\r\n readonly tokenSource: TokenSource;\r\n /**\r\n * Only for session-authenticated hosts. A voice token carries the organization in its claim and\r\n * the backend pins it from there, so a partner omits this and cannot point the SDK at a tenant\r\n * their token does not cover. See `CoventiaBackendConfig.organizationId`.\r\n */\r\n readonly organizationId?: string;\r\n /**\r\n * Where the phone routes live. `/phone` today; `/api/v1/phone` once the partner API ships (§6.6),\r\n * at which point only this string changes.\r\n */\r\n readonly basePath?: string;\r\n /**\r\n * **The one thing a provider implements.** Their contact list, searched on their own backend.\r\n *\r\n * Supplying this is enough for everything: it drives the Contactos tab, and caller ID is DERIVED\r\n * from it by searching for the incoming number (see `resolverFromContacts`). One source means the\r\n * Contactos tab, a live call and a history row can never disagree about who a number belongs to —\r\n * which they did when the list and the lookup were separate overrides.\r\n *\r\n * The matching happens on their backend, where the CRM is: we send the number as `search` and take\r\n * an exact digit match from the results.\r\n */\r\n readonly contacts?: ContactsSource;\r\n\r\n /**\r\n * Caller ID, only if searching the list is not how their CRM answers \"who is this number\".\r\n *\r\n * Rarely needed — `contacts` already derives it. Worth setting when a dedicated lookup is cheaper\r\n * than a search, or when the number lives on a record that search does not return.\r\n */\r\n readonly resolveContact?: (number: string) => Promise<Contact | null>;\r\n /** Overrides polling `/transfers/outcomes` — see `CoventiaBackendConfig.transferOutcomes`. */\r\n readonly transferOutcomes?: TransferOutcomeSource;\r\n /** Overrides the presence WebSocket — see `CoventiaBackendConfig.presence`. */\r\n readonly presence?: PresenceSource;\r\n readonly fetch?: typeof globalThis.fetch;\r\n /** Overridable for tests — a fake `WebSocket` constructor. Defaults to `globalThis.WebSocket`. */\r\n readonly WebSocket?: typeof globalThis.WebSocket;\r\n}\r\n\r\nexport interface CoventiaSources {\r\n /** The Contactos tab. A partner may replace this with their own; everything else cannot be. */\r\n readonly contacts: ContactsSource;\r\n /** The Historial tab. **Coventia's, always** — we store the calls and the recordings. */\r\n readonly history: HistorySource;\r\n /** Caller ID for an incoming number, for `SoftphoneConfig.resolveContact`. */\r\n readonly resolveContact: (number: string) => Promise<Contact | null>;\r\n /** The Historial tab's \"filter by person\" directory. Always Coventia's own org membership. */\r\n readonly listAgents: () => Promise<AgentsDirectory>;\r\n /** Phase A (transfer outcome relay) — polled by the transfer watch, never by UI directly. */\r\n readonly transferOutcomes: TransferOutcomeSource;\r\n /** Phase B (presence-gated transfer picker) — opened by `presence-socket.ts`, never by UI directly. */\r\n readonly presence: PresenceSource;\r\n /** The caller's ringtone settings (custom-ringtone.md §4.3–4.4). */\r\n readonly ringtones: RingtoneSource;\r\n /** The caller's outbound number (outbound-number-per-agent.md §5.3–§5.5). */\r\n readonly outboundNumbers: OutboundNumbersSource;\r\n}\r\n\r\n/** What both `/outbound-numbers` agent routes return (backend `routes/outbound-numbers.js`). */\r\ninterface OutboundNumbersResponse {\r\n readonly numbers?: ReadonlyArray<{ readonly id: string; readonly number: string; readonly label?: string | null; readonly kind?: string }>;\r\n readonly orgDefault?: string | null;\r\n readonly mine?: string | null;\r\n readonly effective?: string | null;\r\n readonly extension?: string | null;\r\n readonly state?: string;\r\n readonly changeAvailableInSeconds?: number;\r\n}\r\n\r\n/** What every `/ringtones` route returns (backend `routes/ringtones.js`). */\r\ninterface RingtonesResponse {\r\n readonly catalogue?: ReadonlyArray<{\r\n readonly id: string;\r\n readonly name: string;\r\n readonly path: string | null;\r\n readonly loop?: { readonly start: number; readonly end: number } | null;\r\n }>;\r\n readonly orgDefault?: string | null;\r\n readonly user?: string | null;\r\n readonly effective?: string;\r\n}\r\n\r\n/** What `/agents` returns. */\r\ninterface AgentRow {\r\n readonly user_id: string;\r\n readonly name: string | null;\r\n}\r\n\r\n/** What `/webrtc/contacts` returns. **The only place `wa_id` is named in the whole SDK.** */\r\ninterface WhatsAppContactRow {\r\n readonly wa_id: string;\r\n readonly name: string | null;\r\n}\r\n\r\n/**\r\n * `wa_id` → `Contact`.\r\n *\r\n * `id` IS the `wa_id`, and that leaks nothing: the backend serves callable contacts only\r\n * (`wa_id: /^[0-9]+$/`), so the value is literally the phone digits and carries nothing `phone` does\r\n * not. What the boundary removes is the SHAPE — no `custom_username`/`username` coalescing, no\r\n * WhatsApp field names past this function.\r\n */\r\nconst toContact = (row: WhatsAppContactRow): Contact => ({\r\n id: row.wa_id,\r\n name: row.name,\r\n phone: `+${row.wa_id}`,\r\n});\r\n\r\n/** One hop in `handled_by_chain`, as Mongo stores it — no `name` (see `HandledByChainRow`'s own note). */\r\ninterface HandledByChainRow {\r\n readonly ext?: string;\r\n readonly user_id?: string | null;\r\n}\r\n\r\n/** The same hop, enriched by `GET /calls/:id` with a resolved display name. */\r\ninterface EnrichedHandledByChainRow extends HandledByChainRow {\r\n readonly name?: string | null;\r\n}\r\n\r\n/** A row from `/calls`. The backend spreads raw Mongo docs, so every read below is defensive. */\r\ninterface CallRow {\r\n readonly _id?: string;\r\n /** The PBX's vocabulary: `incoming` | `outgoing`. NOT the SDK's `inbound`/`outbound`. */\r\n readonly direction?: string;\r\n readonly caller_id?: string;\r\n readonly called_did?: string;\r\n readonly destination?: string;\r\n /** Outbound only: the org's number the call showed (from NOTIFY_OUT_START). */\r\n readonly from_number?: string;\r\n readonly disposition?: string;\r\n readonly duration?: number;\r\n readonly created_at?: string | number;\r\n readonly recording_count?: number;\r\n readonly handled_by_chain?: readonly HandledByChainRow[];\r\n /** The transfer route as bare extensions — populated even when `handled_by_chain` isn't (older\r\n * docs, written before the chain existed). */\r\n readonly extensions?: readonly string[];\r\n readonly internal?: string;\r\n readonly last_internal?: string;\r\n}\r\n\r\nconst toDirection = (raw: string | undefined): 'inbound' | 'outbound' =>\r\n raw === 'outgoing' ? 'outbound' : 'inbound';\r\n\r\n/** Which field holds the other party depends on the direction. */\r\nconst otherParty = (row: CallRow): string =>\r\n toDirection(row.direction) === 'outbound'\r\n ? (row.called_did ?? row.destination ?? '')\r\n : (row.caller_id ?? '');\r\n\r\n/**\r\n * Who handled the call, in order. Prefers the real chain (`ext` + resolved `user_id`); falls back to\r\n * bare extensions ONLY when there is no chain at all — a doc written before `handled_by_chain` existed.\r\n * Unifies both shapes into one `CallHandler[]` so the UI never has to know which source it came from:\r\n * a bare extension is just a handler with `userId: null, name: null`.\r\n */\r\nfunction toHandledBy(row: CallRow, chain: readonly EnrichedHandledByChainRow[] = row.handled_by_chain ?? []): readonly CallHandler[] {\r\n if (chain.length > 0) {\r\n return chain\r\n .filter((h): h is EnrichedHandledByChainRow & { ext: string } => Boolean(h?.ext))\r\n .map((h) => ({ extension: h.ext, userId: h.user_id ?? null, name: h.name ?? null }));\r\n }\r\n const bare = row.extensions?.length ? row.extensions : [row.internal, row.last_internal];\r\n return [...new Set(bare.filter((e): e is string => Boolean(e)))].map((extension) => ({\r\n extension,\r\n userId: null,\r\n name: null,\r\n }));\r\n}\r\n\r\n/**\r\n * \"Not answered\" is ANY non-empty disposition other than `answered` — `no answer`, `busy`, `cancel`,\r\n * `failed`, `congestion`, and whatever Zadarma adds next.\r\n *\r\n * Open-ended rather than an allow-list, so an unrecognised value surfaces under \"Perdidas\" instead of\r\n * vanishing. **An EMPTY disposition counts as ANSWERED**, so a call of unknown status is not wrongly\r\n * flagged. Both halves are load-bearing.\r\n */\r\nconst isMissed = (row: CallRow): boolean => {\r\n const d = String(row.disposition ?? '').trim().toLowerCase();\r\n return d !== '' && d !== 'answered';\r\n};\r\n\r\n/**\r\n * The PBX's own vocabulary translated to the SDK's, for a call `isMissed`. Never returns Zadarma's\r\n * raw string — an unrecognised or future value falls into `'unavailable'` rather than leaking through\r\n * or being dropped, same open-ended reasoning `isMissed` itself documents.\r\n */\r\nconst toOutcome = (row: CallRow): CallOutcome => {\r\n switch (String(row.disposition ?? '').trim().toLowerCase()) {\r\n case 'no answer':\r\n return 'no-answer';\r\n case 'busy':\r\n return 'busy';\r\n case 'cancel':\r\n return 'cancelled';\r\n case 'failed':\r\n return 'failed';\r\n default:\r\n return 'unavailable';\r\n }\r\n};\r\n\r\n/**\r\n * Caller ID, derived from a contacts source: ask it for the number and take an exact match.\r\n *\r\n * This is why a provider only has to implement ONE function. Their search endpoint already knows how\r\n * to match a phone number — it is the same question the Contactos tab asks, with a different query —\r\n * and doing it on their backend is where the CRM actually lives.\r\n *\r\n * **Exact digit match only.** A search for a number can return partial or fuzzy hits, and naming a\r\n * caller after a near-miss is worse than showing the bare number: the user trusts the name.\r\n * `toDigits` on both sides so `+34 600 111 222` and `34600111222` compare equal.\r\n */\r\nexport function resolverFromContacts(\r\n contacts: ContactsSource,\r\n): (number: string) => Promise<Contact | null> {\r\n return async (number) => {\r\n const digits = toDigits(number);\r\n if (!digits) return null;\r\n // The number as the PBX reported it — their backend has the most to work with that way. `limit`\r\n // is small: we want one exact hit, not a page of near misses.\r\n const page = await contacts({ search: number, skip: 0, limit: 10 });\r\n return page.items.find((c) => toDigits(c.phone) === digits) ?? null;\r\n };\r\n}\r\n\r\nconst toDate = (raw: string | number | undefined): Date => {\r\n if (raw === undefined) return new Date(0);\r\n const d = new Date(raw);\r\n return Number.isNaN(d.getTime()) ? new Date(0) : d;\r\n};\r\n\r\n/** What `GET /transfers/outcomes` returns per entry — `transferOutcomeStore.js`'s own wire shape. */\r\ninterface TransferOutcomeRow {\r\n readonly id?: string;\r\n readonly target?: string;\r\n /** The backend's wire value; `'connected'` or anything else (`'not_connected'`) maps below. */\r\n readonly status?: string;\r\n readonly at?: string;\r\n}\r\n\r\n/** `not_connected` → `'not-connected'` (spec §4 D2's kebab-case, matching `HangupReason`) — the ONE\r\n * place that translation happens; open-ended rather than an allow-list, so an unrecognised status\r\n * fails safe as `'not-connected'` (the agent gets told, never silently nothing) rather than throwing. */\r\nconst toTransferOutcome = (row: TransferOutcomeRow): TransferOutcomeEntry => ({\r\n id: row.id ?? '',\r\n target: row.target ?? '',\r\n status: row.status === 'connected' ? 'connected' : 'not-connected',\r\n at: toDate(row.at),\r\n});\r\n\r\n/** What the presence WebSocket's `snapshot`/`delta` messages carry per agent — `presenceSocket.js`'s\r\n * own wire shape (`docs/specs/transfer-presence-picker.md` §4.3, plan §2). */\r\ninterface PresenceAgentRow {\r\n readonly userId?: string;\r\n readonly extension?: string;\r\n readonly name?: string | null;\r\n readonly state?: string;\r\n}\r\n\r\n/** Open-ended rather than an allow-list, matching `toTransferOutcome`'s own reasoning for an\r\n * unrecognised value: fails safe as `'desconectado'` (never wrongly reachable) rather than throwing. */\r\nconst toPresenceState = (raw: string | undefined): PresenceState =>\r\n raw === 'disponible' || raw === 'en_llamada' ? raw : 'desconectado';\r\n\r\nconst toPresenceEntry = (row: PresenceAgentRow): PresenceEntry => ({\r\n userId: row.userId ?? '',\r\n extension: row.extension ?? '',\r\n name: row.name ?? null,\r\n state: toPresenceState(row.state),\r\n});\r\n\r\n/** A `line` message's `where` (`docs/specs/line-owner.md` §5.2). Anything unrecognised is \"somewhere we cannot\r\n * name\" (`null`), which the waiting screen words generically — never guessed to be the desktop app. */\r\nconst toLineLocation = (raw: string | null | undefined): LineLocation | null =>\r\n raw === 'desktop' || raw === 'browser' ? raw : null;\r\n\r\n/** `POST /presence/ticket`'s response (`presenceTicketStore.js`'s own wire shape). */\r\ninterface PresenceTicketResponse {\r\n readonly success?: boolean;\r\n readonly ticket?: string;\r\n readonly expires_in?: number;\r\n}\r\n\r\n/** `https://api.coventia.es` → `wss://api.coventia.es`; `http://` → `ws://`. Scheme swapped, and any\r\n * trailing slash trimmed — same normalisation `createGet`/`createPost` apply to `baseUrl` internally,\r\n * needed again here since this function works from the raw config value, not their trimmed copy. */\r\nconst toWebSocketUrl = (httpUrl: string): string => httpUrl.replace(/^http/, 'ws').replace(/\\/+$/, '');\r\n\r\n/**\r\n * Coventia's phone service, as the sources the widget asks for.\r\n *\r\n * **This is the anti-corruption layer.** The backend speaks WhatsApp's data model; the SDK speaks\r\n * `Contact` and `CallHistoryEntry`. The translation happens here and nowhere else — enforced by\r\n * structure, since `core/` cannot reach `coventia/` and a partner never imports it.\r\n */\r\nexport function coventiaSources(config: CoventiaSourcesConfig): CoventiaSources {\r\n const get = createGet(config);\r\n const post = createPost(config);\r\n const base = config.basePath ?? '/phone';\r\n // Absent for a voice token, which already names the organization. Spread into every query below,\r\n // so omitting it removes the parameter entirely rather than sending `organization_id=undefined`.\r\n const org = config.organizationId ? { organization_id: config.organizationId } : {};\r\n\r\n const coventiaContacts: ContactsSource = async ({ search, skip, limit }): Promise<ContactsPage> => {\r\n const body = await get<{ contacts?: readonly WhatsAppContactRow[]; total?: number }>(\r\n `${base}/webrtc/contacts`,\r\n { ...org, search, skip, limit },\r\n );\r\n return { items: (body.contacts ?? []).map(toContact), total: body.total ?? 0 };\r\n };\r\n\r\n const coventiaResolve = async (number: string): Promise<Contact | null> => {\r\n const digits = toDigits(number);\r\n if (!digits) return null;\r\n const body = await get<{ name?: string | null }>(`${base}/webrtc/caller-name`, {\r\n ...org,\r\n number: digits,\r\n });\r\n // Re-applied on top of the backend's own rule: a contact saved as its own number resolves to a\r\n // \"name\" that is just the number reformatted, and showing that puts +34600111222 where a name\r\n // belongs — next to the number the widget is already displaying.\r\n const name = normalizeResolvedName(body.name ?? null, digits);\r\n return name === null ? null : { id: digits, name, phone: `+${digits}` };\r\n };\r\n\r\n const rawHistory: HistorySource = async ({\r\n skip,\r\n limit,\r\n missed,\r\n userId,\r\n }): Promise<readonly CallHistoryEntry[]> => {\r\n const body = await get<{ calls?: readonly CallRow[] }>(`${base}/calls`, {\r\n ...org,\r\n skip,\r\n limit,\r\n // Omitted rather than `'false'` — the backend only special-cases `missed=true`, so leaving it\r\n // out entirely for \"Todas\" is equivalent and one less thing for a param dump to explain.\r\n missed: missed ? 'true' : undefined,\r\n user_id: userId,\r\n });\r\n return (body.calls ?? []).map((row): CallHistoryEntry => {\r\n const id = row._id ?? '';\r\n const recorded = (row.recording_count ?? 0) > 0;\r\n const missedRow = isMissed(row);\r\n const handledBy = toHandledBy(row);\r\n return {\r\n id,\r\n direction: toDirection(row.direction),\r\n number: otherParty(row),\r\n // Filled by `withNameResolution` at the mount — deduped by digits and cached across pages.\r\n name: null,\r\n at: toDate(row.created_at),\r\n durationSec: row.duration ?? 0,\r\n missed: missedRow,\r\n ...(missedRow ? { outcome: toOutcome(row) } : {}),\r\n // The DID is only ever the ORG's own number, which only an inbound call dials — an outbound\r\n // row's `called_did` is the OTHER party (already `number` above); showing it again as \"DID\"\r\n // would just repeat the row under a wrong label.\r\n ...(toDirection(row.direction) === 'inbound' && row.called_did ? { did: row.called_did } : {}),\r\n // And the other way round: which of the org's numbers an outbound call showed.\r\n ...(toDirection(row.direction) === 'outbound' && row.from_number ? { from: row.from_number } : {}),\r\n recorded,\r\n handledBy,\r\n // The detail page's one enrichment fetch — signed recording URLs, plus this SAME chain with\r\n // names resolved. Skipped when there is nothing to enrich: minting signed URLs and resolving\r\n // names for a page of 30 calls nobody opens is pure waste.\r\n ...(recorded || handledBy.length > 0\r\n ? {\r\n loadDetail: async (): Promise<CallDetail> => {\r\n // Unlike `/calls` (rows spread flat into `calls: []`), `/calls/:id` wraps its one\r\n // result in `call` — `recordings.js`'s `res.json({ success, call: {...} })`.\r\n const detail = await get<{\r\n call?: {\r\n recording_urls?: readonly string[];\r\n handled_by_chain?: readonly EnrichedHandledByChainRow[];\r\n };\r\n }>(`${base}/calls/${encodeURIComponent(id)}`, org);\r\n return {\r\n recordingUrls: detail.call?.recording_urls ?? [],\r\n // `row`'s own bare-extension fields (read inside `toHandledBy` when the chain\r\n // passed here is empty) still work as the fallback — only the CHAIN comes from the\r\n // enriched response; the rest of `row` is unchanged.\r\n handledBy: toHandledBy(row, detail.call?.handled_by_chain ?? []),\r\n };\r\n },\r\n }\r\n : {}),\r\n };\r\n });\r\n };\r\n\r\n const coventiaListAgents = async (): Promise<AgentsDirectory> => {\r\n const body = await get<{ me?: AgentRow | null; people?: readonly AgentRow[] }>(\r\n `${base}/agents`,\r\n org,\r\n );\r\n const toAgent = (row: AgentRow) => ({ userId: row.user_id, name: row.name });\r\n return {\r\n me: body.me ? toAgent(body.me) : null,\r\n people: (body.people ?? []).map(toAgent),\r\n };\r\n };\r\n\r\n /**\r\n * Phase A (transfer outcome relay). `since` omitted on the caller's very first poll — that call\r\n * exists only to learn the starting `cursor`, per spec §5.3; this source is a thin, honest wrapper\r\n * around the endpoint and does not itself implement that \"discard the first page\" rule, which\r\n * belongs to the transfer watch (plan T5) that calls it.\r\n */\r\n const coventiaTransferOutcomes: TransferOutcomeSource = async ({ since }): Promise<TransferOutcomesPage> => {\r\n const body = await get<{ cursor?: string; outcomes?: readonly TransferOutcomeRow[] }>(\r\n `${base}/transfers/outcomes`,\r\n { ...org, since },\r\n );\r\n return {\r\n cursor: body.cursor ?? '0',\r\n outcomes: (body.outcomes ?? []).map(toTransferOutcome),\r\n };\r\n };\r\n\r\n /**\r\n * Phase B (presence-gated transfer picker). Mints a fresh, one-time ticket (never the real voice\r\n * token — spec §4.2's \"decided 2026-09-24: where the token travels\") and opens the WebSocket with\r\n * it. Called fresh on every (re)connect by `presence-socket.ts`, which owns retry/backoff — this\r\n * function is a single, honest attempt, the same division of labour `coventiaTransferOutcomes` has\r\n * with the transfer watch that calls IT.\r\n *\r\n * The same socket carries the line owner (`docs/specs/line-owner.md` §5.2): `hello` / `line-claim` out,\r\n * `line` / `line-claim-result` in. Unknown message types are ignored both ways, so an older backend\r\n * simply never answers, and the softphone listens as it always did.\r\n */\r\n const coventiaPresence: PresenceSource = (callbacks: PresenceCallbacks): Promise<PresenceConnection> => {\r\n const WS = config.WebSocket ?? globalThis.WebSocket;\r\n return new Promise<PresenceConnection>((resolve, reject) => {\r\n void (async () => {\r\n let ticket: string;\r\n try {\r\n const body = await post<PresenceTicketResponse>(`${base}/presence/ticket`, org);\r\n if (!body.ticket) throw new Error('presence ticket mint returned no ticket');\r\n ticket = body.ticket;\r\n } catch (error) {\r\n reject(error instanceof Error ? error : new Error(String(error)));\r\n return;\r\n }\r\n\r\n const wsUrl = `${toWebSocketUrl(config.baseUrl)}${base}/presence?ticket=${encodeURIComponent(ticket)}`;\r\n const ws = new WS(wsUrl);\r\n let settled = false;\r\n\r\n ws.onopen = () => {\r\n if (settled) return;\r\n settled = true;\r\n resolve({\r\n reportCallState: (onCall) => {\r\n if (ws.readyState !== WS.OPEN) return;\r\n ws.send(JSON.stringify({ type: 'call-state', onCall }));\r\n },\r\n bye: () => {\r\n if (ws.readyState !== WS.OPEN) return;\r\n ws.send(JSON.stringify({ type: 'bye' }));\r\n },\r\n // `line: true` is what makes the server treat this socket as taking part in the line owner;\r\n // without it (or with an id it rejects) the `hello` is ignored, as an older SDK's would be.\r\n hello: (clientId) => {\r\n if (ws.readyState !== WS.OPEN) return;\r\n ws.send(JSON.stringify({ type: 'hello', clientId, line: true }));\r\n },\r\n claimLine: () => {\r\n if (ws.readyState !== WS.OPEN) return;\r\n ws.send(JSON.stringify({ type: 'line-claim' }));\r\n },\r\n close: () => {\r\n // Detached first — a deliberate close must never be reported to the caller as a drop\r\n // (`PresenceConnection.close`'s own contract).\r\n ws.onclose = null;\r\n ws.onerror = null;\r\n ws.close();\r\n },\r\n });\r\n };\r\n\r\n ws.onmessage = (event: MessageEvent) => {\r\n let data: unknown;\r\n try {\r\n data = JSON.parse(String(event.data));\r\n } catch {\r\n return; // not JSON — ignore rather than tear down the connection over one stray frame\r\n }\r\n const msg = data as {\r\n type?: string;\r\n agents?: readonly PresenceAgentRow[];\r\n agent?: PresenceAgentRow;\r\n state?: string;\r\n where?: string | null;\r\n ok?: boolean;\r\n reason?: string;\r\n };\r\n if (msg.type === 'snapshot') {\r\n callbacks.onSnapshot((msg.agents ?? []).map(toPresenceEntry));\r\n } else if (msg.type === 'delta' && msg.agent) {\r\n callbacks.onDelta(toPresenceEntry(msg.agent));\r\n } else if (msg.type === 'line' && (msg.state === 'here' || msg.state === 'elsewhere')) {\r\n callbacks.onLine?.({ state: msg.state, where: msg.state === 'here' ? null : toLineLocation(msg.where) });\r\n } else if (msg.type === 'line-claim-result') {\r\n // Strict, like the server's own `onCall === true`: anything short of a real `true` took nothing.\r\n callbacks.onClaimResult?.(\r\n msg.ok === true\r\n ? { ok: true }\r\n : { ok: false, reason: typeof msg.reason === 'string' ? msg.reason : 'unavailable' },\r\n );\r\n }\r\n };\r\n\r\n ws.onclose = () => {\r\n if (!settled) {\r\n // Never actually opened (ticket refused, network refusal, ...) — indistinguishable from a\r\n // drop to `presence-socket.ts`, which is exactly why this rejects rather than calling\r\n // `onClose`: there is no `PresenceConnection` yet for that callback to be about.\r\n settled = true;\r\n reject(new Error('presence socket closed before opening'));\r\n return;\r\n }\r\n callbacks.onClose();\r\n };\r\n\r\n // A real WebSocket always follows `error` with `close` — nothing extra to do here, and a\r\n // browser gives an `error` event no further detail worth reporting anyway.\r\n ws.onerror = () => {};\r\n })();\r\n });\r\n };\r\n\r\n /*\r\n * Ringtone settings. The backend answers with a `path` from the API root rather than a URL (behind its\r\n * proxy it cannot know its own public host — custom-ringtone-plan.md §2), so the file URL is built here\r\n * exactly the way every request URL is: `baseUrl` + path. The UI only ever sees absolute URLs.\r\n */\r\n const fileBase = config.baseUrl.replace(/\\/+$/, '');\r\n /** Loop points only when they are usable numbers in order — anything else means \"loop the whole file\". */\r\n const toLoop = (l: { start: number; end: number } | null | undefined) =>\r\n l && Number.isFinite(l.start) && Number.isFinite(l.end) && l.start >= 0 && l.end > l.start\r\n ? { start: l.start, end: l.end }\r\n : null;\r\n const toRingtoneSettings = (body: RingtonesResponse): RingtoneSettings => {\r\n const catalogue = (body.catalogue ?? []).map((r) => ({\r\n id: r.id,\r\n name: r.name,\r\n url: r.path ? `${fileBase}${r.path}` : null,\r\n loop: r.path ? toLoop(r.loop) : null,\r\n }));\r\n // Defensive only — the backend always resolves `effective` to a catalogue id. If a malformed answer\r\n // ever named something else, the built-in tone (the entry with no file) is the safe reading.\r\n const builtIn = catalogue.find((r) => r.url === null)?.id ?? 'clasico';\r\n const effective = catalogue.some((r) => r.id === body.effective) ? (body.effective as string) : builtIn;\r\n return { catalogue, orgDefault: body.orgDefault ?? null, mine: body.user ?? null, effective };\r\n };\r\n\r\n /*\r\n * The last settings this user had, remembered in localStorage (custom-ringtone.md §4.4, added 2026-10-15):\r\n * at startup the softphone can start loading the right file — usually straight from the browser's HTTP\r\n * cache — before the backend has answered, which closes the \"first second after start\" window in which a\r\n * call would otherwise ring the built-in tone. Fresh settings from the backend always replace them.\r\n *\r\n * Keyed by backend, organization and user (the token's identity), so two people sharing a browser — or one\r\n * person in two organizations — never see each other's choice. Never throws: storage may be blocked.\r\n */\r\n const CACHE_PREFIX = 'coventia-softphone:ringtones:';\r\n const cacheKey = async (prefix = CACHE_PREFIX): Promise<string | null> => {\r\n try {\r\n const { identity } = await config.tokenSource.getToken();\r\n return `${prefix}${fileBase}|${config.organizationId ?? '-'}|${identity}`;\r\n } catch {\r\n return null;\r\n }\r\n };\r\n const remember = async (settings: RingtoneSettings): Promise<RingtoneSettings> => {\r\n const key = await cacheKey();\r\n try {\r\n if (key) globalThis.localStorage?.setItem(key, JSON.stringify(settings));\r\n } catch {\r\n /* blocked or full — nothing to remember, nothing lost */\r\n }\r\n return settings;\r\n };\r\n const recall = async (): Promise<RingtoneSettings | null> => {\r\n const key = await cacheKey();\r\n try {\r\n const raw = key ? globalThis.localStorage?.getItem(key) : null;\r\n const s = raw ? (JSON.parse(raw) as RingtoneSettings) : null;\r\n // Only a well-formed record whose effective ringtone is in its own catalogue; anything else is ignored.\r\n const ok =\r\n s &&\r\n Array.isArray(s.catalogue) &&\r\n s.catalogue.every((r) => typeof r?.id === 'string' && typeof r.name === 'string' && (r.url === null || typeof r.url === 'string')) &&\r\n s.catalogue.some((r) => r.id === s.effective);\r\n return ok ? { ...s, catalogue: s.catalogue.map((r) => ({ ...r, loop: toLoop(r.loop) })) } : null;\r\n } catch {\r\n return null;\r\n }\r\n };\r\n\r\n const ringtones: RingtoneSource = {\r\n get: async (signal) =>\r\n remember(toRingtoneSettings(await get<RingtonesResponse>(`${base}/ringtones`, org, signal))),\r\n // `null` clears: the empty id is dropped from the query by the request helper, and the backend reads a\r\n // missing `ringtone_id` as \"clear\".\r\n setMine: async (id) =>\r\n remember(\r\n toRingtoneSettings(await post<RingtonesResponse>(`${base}/ringtones/user`, { ...org, ringtone_id: id ?? '' })),\r\n ),\r\n cached: recall,\r\n };\r\n\r\n /*\r\n * The agent's outbound number (outbound-number-per-agent.md §5.3–§5.5). Remembered per backend, organization and\r\n * user exactly like the ringtone settings, so the dialpad can show the number in use before the backend answers.\r\n */\r\n const KINDS: ReadonlySet<string> = new Set<OutboundNumberKind>(['landline', 'mobile', 'commercial', 'other']);\r\n const STATES: ReadonlySet<string> = new Set<OutboundNumberState>(['applied', 'pending', 'failed', 'unassigned']);\r\n const toOutboundSettings = (body: OutboundNumbersResponse): OutboundNumberSettings => {\r\n const numbers = (body.numbers ?? []).map((n) => ({\r\n id: n.id,\r\n number: n.number,\r\n label: n.label ?? null,\r\n kind: KINDS.has(n.kind ?? '') ? (n.kind as OutboundNumberKind) : 'other',\r\n }));\r\n const known = (id: string | null | undefined) => (id && numbers.some((n) => n.id === id) ? id : null);\r\n return {\r\n numbers,\r\n orgDefault: body.orgDefault ?? null,\r\n mine: known(body.mine),\r\n // Defensive: a malformed answer naming a number that is not in the list reads as the org default.\r\n effective: known(body.effective) ?? known(body.orgDefault),\r\n extension: body.extension ?? null,\r\n state: STATES.has(body.state ?? '') ? (body.state as OutboundNumberState) : 'applied',\r\n changeAvailableInSeconds: Math.max(0, Math.ceil(Number(body.changeAvailableInSeconds) || 0)),\r\n };\r\n };\r\n const OUTBOUND_PREFIX = 'coventia-softphone:outbound-number:';\r\n const rememberOutbound = async (settings: OutboundNumberSettings): Promise<OutboundNumberSettings> => {\r\n const key = await cacheKey(OUTBOUND_PREFIX);\r\n try {\r\n if (key) globalThis.localStorage?.setItem(key, JSON.stringify(settings));\r\n } catch {\r\n /* blocked or full — nothing to remember, nothing lost */\r\n }\r\n return settings;\r\n };\r\n const recallOutbound = async (): Promise<OutboundNumberSettings | null> => {\r\n const key = await cacheKey(OUTBOUND_PREFIX);\r\n try {\r\n const raw = key ? globalThis.localStorage?.getItem(key) : null;\r\n const s = raw ? (JSON.parse(raw) as OutboundNumberSettings) : null;\r\n const ok =\r\n s &&\r\n Array.isArray(s.numbers) &&\r\n s.numbers.every((n) => typeof n?.id === 'string' && typeof n.number === 'string') &&\r\n (s.effective === null || s.numbers.some((n) => n.id === s.effective));\r\n // A remembered countdown is meaningless later: only the backend knows the current limits.\r\n return ok ? { ...s, changeAvailableInSeconds: 0 } : null;\r\n } catch {\r\n return null;\r\n }\r\n };\r\n const outboundNumbers: OutboundNumbersSource = {\r\n get: async (signal) =>\r\n rememberOutbound(toOutboundSettings(await get<OutboundNumbersResponse>(`${base}/outbound-numbers`, org, signal))),\r\n // `null` = back to the org default: the empty id is dropped from the query, which the backend reads that way.\r\n setMine: async (id) => {\r\n try {\r\n return rememberOutbound(\r\n toOutboundSettings(await post<OutboundNumbersResponse>(`${base}/outbound-numbers/user`, { ...org, did_id: id ?? '' })),\r\n );\r\n } catch (error) {\r\n const body = error instanceof ApiError ? (error.body as { code?: string; reason?: string; retryAfterSeconds?: number } | null) : null;\r\n if (error instanceof ApiError && error.status === 429 && body?.code === 'rate_limited') {\r\n throw outboundNumberRefused({\r\n reason: body.reason === 'user' ? 'user' : 'platform',\r\n retryAfterSeconds: Math.max(1, Math.ceil(Number(body.retryAfterSeconds) || 1)),\r\n });\r\n }\r\n throw error;\r\n }\r\n },\r\n cached: recallOutbound,\r\n };\r\n\r\n const contacts = config.contacts ?? coventiaContacts;\r\n\r\n /*\r\n * Caller ID follows the CONTACT LIST, not a separate setting.\r\n *\r\n * A provider who supplies `contacts` gets a resolver derived from it, so the tab, a live call and a\r\n * history row always name the same number the same way. Supplying only the list used to leave\r\n * caller ID on ours — the same person appearing twice under two names inside one widget.\r\n */\r\n const resolve =\r\n config.resolveContact ??\r\n (config.contacts ? resolverFromContacts(config.contacts) : coventiaResolve);\r\n\r\n // Names, resolved once per distinct number and cached across pages — a call log is the same handful\r\n // of numbers over and over. Wrapped HERE rather than left to the caller: the raw rows carry\r\n // `name: null`, so a caller who forgets this gets a history of bare numbers and no error.\r\n const history = withNameResolution(rawHistory, resolve);\r\n\r\n const transferOutcomes = config.transferOutcomes ?? coventiaTransferOutcomes;\r\n const presence = config.presence ?? coventiaPresence;\r\n\r\n return {\r\n contacts,\r\n history,\r\n resolveContact: resolve,\r\n listAgents: coventiaListAgents,\r\n transferOutcomes,\r\n presence,\r\n ringtones,\r\n outboundNumbers,\r\n };\r\n}\r\n"]}
@@ -134,6 +134,19 @@ var ERROR_CATALOGUE = {
134
134
  message: "The provider does not support this operation.",
135
135
  solution: "Guard the UI on the operation before offering it."
136
136
  },
137
+ // ── Line owner (docs/specs/line-owner.md §5) ────────────────────────────────
138
+ // What `takeLine()` rejects with — and `call()` from `standby`, which takes the line first. Never
139
+ // emitted on `SoftphoneEvent.Error`: the promise IS the answer, and nothing about the phone is broken.
140
+ "line/claim-refused": {
141
+ fatal: false,
142
+ message: "The line was not moved here: the user is on a call on the softphone that has it.",
143
+ 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."
144
+ },
145
+ "line/unavailable": {
146
+ fatal: false,
147
+ message: "The line could not be taken: nothing decided where it is.",
148
+ 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."
149
+ },
137
150
  // ── Provider ────────────────────────────────────────────────────────────────
138
151
  "provider/provision-failed": {
139
152
  fatal: true,