@coventia/softphone 0.4.0 → 0.5.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 (60) hide show
  1. package/dist/{call-C6YpT6uR.d.cts → call-iGvr2ZnO.d.cts} +130 -13
  2. package/dist/{call-C6YpT6uR.d.ts → call-iGvr2ZnO.d.ts} +130 -13
  3. package/dist/chunk-3UTUWGVG.js +1891 -0
  4. package/dist/chunk-3UTUWGVG.js.map +1 -0
  5. package/dist/chunk-DZRSSZWB.js +39 -0
  6. package/dist/chunk-DZRSSZWB.js.map +1 -0
  7. package/dist/{chunk-2ZSOGGPO.js → chunk-FRXHEHQA.js} +15 -2
  8. package/dist/chunk-FRXHEHQA.js.map +1 -0
  9. package/dist/{chunk-UHNLEBBO.js → chunk-I3RSC52R.js} +28 -3
  10. package/dist/chunk-I3RSC52R.js.map +1 -0
  11. package/dist/chunk-M76SAXXM.js +13 -0
  12. package/dist/chunk-M76SAXXM.js.map +1 -0
  13. package/dist/{chunk-7R6LOOAI.js → chunk-N4PLEYFJ.js} +4 -2
  14. package/dist/chunk-N4PLEYFJ.js.map +1 -0
  15. package/dist/element/index.d.ts +167 -7
  16. package/dist/element/index.js +138 -43
  17. package/dist/element/index.js.map +1 -1
  18. package/dist/{engine-DPft99Wj.d.ts → engine-BKoT_Pj7.d.cts} +10 -1
  19. package/dist/{engine-CDdaSojx.d.cts → engine-CeGh5Yai.d.ts} +10 -1
  20. package/dist/index.cjs +1596 -145
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +5 -5
  23. package/dist/index.d.ts +5 -5
  24. package/dist/index.js +618 -42
  25. package/dist/index.js.map +1 -1
  26. package/dist/react/index.cjs +26 -0
  27. package/dist/react/index.cjs.map +1 -1
  28. package/dist/react/index.d.cts +24 -5
  29. package/dist/react/index.d.ts +24 -5
  30. package/dist/react/index.js +2 -2
  31. package/dist/{softphone-CY5d2WWY.d.cts → softphone-D0ao5W4z.d.ts} +42 -3
  32. package/dist/{softphone-DXgmTyrX.d.ts → softphone-DRoS0jJR.d.cts} +42 -3
  33. package/dist/{sources-ONYQW733.js → sources-LGFJG7W2.js} +27 -38
  34. package/dist/sources-LGFJG7W2.js.map +1 -0
  35. package/dist/testing/index.cjs +13 -0
  36. package/dist/testing/index.cjs.map +1 -1
  37. package/dist/testing/index.d.cts +2 -2
  38. package/dist/testing/index.d.ts +2 -2
  39. package/dist/testing/index.js +1 -1
  40. package/dist/ui/index.cjs +92 -2
  41. package/dist/ui/index.cjs.map +1 -1
  42. package/dist/ui/index.d.cts +2 -2
  43. package/dist/ui/index.d.ts +2 -2
  44. package/dist/ui/index.js +69 -4
  45. package/dist/ui/index.js.map +1 -1
  46. package/dist/zadarma/index.cjs +950 -104
  47. package/dist/zadarma/index.cjs.map +1 -1
  48. package/dist/zadarma/index.d.cts +40 -14
  49. package/dist/zadarma/index.d.ts +40 -14
  50. package/dist/zadarma/index.js +4 -2
  51. package/dist/zadarma-engine-VYIXLBQG.js +7 -0
  52. package/dist/{zadarma-engine-L5DEFXUS.js.map → zadarma-engine-VYIXLBQG.js.map} +1 -1
  53. package/package.json +1 -1
  54. package/dist/chunk-2ZSOGGPO.js.map +0 -1
  55. package/dist/chunk-7R6LOOAI.js.map +0 -1
  56. package/dist/chunk-EYEDPD7Y.js +0 -1071
  57. package/dist/chunk-EYEDPD7Y.js.map +0 -1
  58. package/dist/chunk-UHNLEBBO.js.map +0 -1
  59. package/dist/sources-ONYQW733.js.map +0 -1
  60. package/dist/zadarma-engine-L5DEFXUS.js +0 -5
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/errors.ts"],"names":[],"mappings":"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","file":"chunk-FRXHEHQA.js","sourcesContent":["/**\r\n * The error taxonomy.\r\n *\r\n * Two things make this worth more than `onError(Error)`:\r\n *\r\n * 1. **Fatal vs non-fatal, as a discriminated union.** A fatal error means the call (or the engine)\r\n * is over and the host must react. A non-fatal one is a warning the host may surface or ignore.\r\n * Collapsing them into one channel — which is what almost every SDK does — forces every\r\n * integrator to re-derive the distinction from string matching. Daily's split is the model.\r\n *\r\n * 2. **A coded catalogue with a `solution` per code.** Twilio and Telnyx both ship this and\r\n * integrators love it, because the support conversation becomes \"send me the code\" instead of\r\n * \"send me your logs\".\r\n *\r\n * The catalogue below is the SINGLE source of truth: `fatal` is derived from it at the type level,\r\n * so a code cannot be fatal in the table and non-fatal in a type, and adding a code without deciding\r\n * its severity is a compile error.\r\n */\r\n\r\ninterface ErrorSpec {\r\n readonly fatal: boolean;\r\n /** What went wrong, for a developer. */\r\n readonly message: string;\r\n /** What to do about it. Shown in logs and in our docs. */\r\n readonly solution: string;\r\n}\r\n\r\nexport const ERROR_CATALOGUE = {\r\n // ── Engine lifecycle ────────────────────────────────────────────────────────\r\n 'engine/duplicate-instance': {\r\n fatal: true,\r\n message: 'A Softphone instance is already running on this page.',\r\n solution:\r\n 'Construct one Softphone and share it. React StrictMode double-mounts in development — hold the instance above the tree, or in a ref, and call destroy() on unmount.',\r\n },\r\n 'engine/not-started': {\r\n fatal: false,\r\n message: 'The softphone has not been started.',\r\n solution: 'Await softphone.start() before placing or answering a call.',\r\n },\r\n 'engine/destroyed': {\r\n fatal: true,\r\n message: 'This Softphone has been destroyed and cannot be reused.',\r\n solution: 'Construct a new Softphone.',\r\n },\r\n\r\n // ── Auth ────────────────────────────────────────────────────────────────────\r\n 'auth/token-fetch-failed': {\r\n fatal: true,\r\n message: 'Could not obtain a voice token from the token endpoint.',\r\n solution:\r\n 'Check that the token endpoint is reachable and that the headers callback returns a valid host-app session token.',\r\n },\r\n 'auth/token-rejected': {\r\n fatal: true,\r\n message: 'The voice token was rejected.',\r\n solution:\r\n 'The token is expired, malformed, or scoped to a different organization. Mint a fresh one.',\r\n },\r\n\r\n // ── Media ───────────────────────────────────────────────────────────────────\r\n /*\r\n NONE of the four codes below are ever actually raised via `createError` — nothing in the SDK\r\n constructs a `SoftphoneError` with these codes today. Deleting them was the other option\r\n (SPEC discussion); they stay, commented, because the taxonomy and Spanish copy (`error-text.ts`)\r\n are worth keeping intact for whenever a real caller wants them, and a `SoftphoneErrorCode` union\r\n is a cheap thing to keep honest with a comment, expensive to keep churning as a breaking change.\r\n */\r\n 'media/permission-denied': {\r\n fatal: true,\r\n message: 'Microphone permission was denied.',\r\n solution:\r\n 'The permission prompt names the TOP-LEVEL origin. Ask for it from a user gesture, and re-request via the browser UI if previously blocked.',\r\n // NEVER RAISED. `MicPermissionGate` (ui/permission/) handles this condition today — it predates\r\n // this catalogue and has BETTER, browser-specific unblock instructions than a generic gate could\r\n // show. It does not import this catalogue at all; the two are unconnected on purpose.\r\n },\r\n 'media/no-input-device': {\r\n fatal: true,\r\n message: 'No microphone is available.',\r\n solution: 'Ask the user to connect a microphone, then retry.',\r\n // NEVER RAISED — same reason as `media/permission-denied` above: `MicPermissionGate` already\r\n // owns this.\r\n },\r\n 'media/webrtc-unsupported': {\r\n fatal: true,\r\n message: 'This browser does not expose the WebRTC APIs.',\r\n solution:\r\n 'Usually a privacy extension (uBlock Origin Extra, WebRTC Leak Prevent) disabling RTCPeerConnection — Twilio documents the same. Ask the user to disable it for this origin.',\r\n // NEVER RAISED — same reason again: `MicPermissionGate` already owns this.\r\n },\r\n 'media/playback-blocked': {\r\n fatal: false,\r\n message: 'The browser blocked audio playback (autoplay policy).',\r\n solution:\r\n 'Call softphone.startAudio() from a click handler. Until then the user cannot hear the caller, and an INCOMING RINGTONE WILL BE SILENT — this is how calls get missed.',\r\n /*\r\n NEVER RAISED — different reason from the other three. This is not superseded by an existing\r\n UI; the feature it describes (autoplay-recovery) is simply not built yet. `Softphone.canPlayAudio`\r\n / `startAudio()` (core/softphone.ts) are Phase 2 and currently no-op stubs — `canPlayAudio`\r\n always reports `true`. This is where that future work would report the block once it exists.\r\n */\r\n },\r\n\r\n // ── Network / signalling ────────────────────────────────────────────────────\r\n 'net/registration-failed': {\r\n fatal: true,\r\n message: 'SIP registration failed.',\r\n solution: 'Usually a stale credential. The engine refreshes and retries; if it persists, the extension may not exist.',\r\n },\r\n 'net/transport-failed': {\r\n fatal: true,\r\n message: 'The signalling transport could not be established.',\r\n solution: 'Check the CSP connect-src directive and any corporate proxy blocking WSS.',\r\n },\r\n 'net/ice-failed': {\r\n fatal: false,\r\n message: 'Media connectivity was lost; attempting an ICE restart.',\r\n solution:\r\n 'Usually a network handoff (wifi → cellular). The engine recovers on its own; surface a \"reconnecting\" banner rather than ending the call.',\r\n /*\r\n NEVER RAISED. The \"reconnecting\" banner this `solution` describes already exists — CallScreen\r\n shows \"Reconectando…\" directly off the call's own `reconnecting` STATE (`STATUS_LABEL` in\r\n views/CallScreen.tsx), which the SIP engine drives without going through the error channel at\r\n all. Raising this too would say the same fact twice through two different mechanisms. It would\r\n only earn its keep for host-side OBSERVABILITY — counting ICE restarts independently of the UI\r\n — which nobody has asked for yet.\r\n */\r\n },\r\n\r\n // ── Call ────────────────────────────────────────────────────────────────────\r\n 'call/not-answerable': {\r\n fatal: false,\r\n message: 'This call cannot be answered in its current state.',\r\n solution: 'Only a call in `ringing` may be answered. Check call.state first.',\r\n },\r\n 'call/busy': {\r\n fatal: false,\r\n message: 'The remote party is busy.',\r\n solution:\r\n 'Nothing to fix. Note SIP 486 is also what most endpoints send for a manual decline, so do not ' +\r\n 'label this \"busy\" alone in your UI — it is equally often \"did not answer\".',\r\n },\r\n 'call/no-answer': {\r\n fatal: false,\r\n message: 'The remote party did not answer.',\r\n solution: 'Nothing to fix. Surface it to the user.',\r\n },\r\n 'call/rejected': {\r\n fatal: false,\r\n message: 'The remote party rejected the call.',\r\n solution: 'Nothing to fix. Surface it to the user.',\r\n },\r\n 'call/missed': {\r\n fatal: false,\r\n message: 'An incoming call was cancelled before it was answered.',\r\n solution:\r\n 'Nothing to fix — the caller gave up while it rang. Surface it, and log it as a missed call.',\r\n },\r\n 'call/failed': {\r\n fatal: false,\r\n message: 'The call could not be completed.',\r\n solution:\r\n 'The catch-all for a call that never connected — a bad number, a rejected offer, a media ' +\r\n 'failure. `cause` carries the underlying SIP cause. Nothing is fatal: the phone still works.',\r\n },\r\n 'call/transfer-failed': {\r\n fatal: false,\r\n message: 'The transfer could not be completed.',\r\n solution: 'The original call is still up. Let the user retry or resume.',\r\n },\r\n 'call/unsupported': {\r\n fatal: false,\r\n message: 'The provider does not support this operation.',\r\n solution: 'Guard the UI on the operation before offering it.',\r\n },\r\n\r\n // ── Line owner (docs/specs/line-owner.md §5) ────────────────────────────────\r\n // What `takeLine()` rejects with — and `call()` from `standby`, which takes the line first. Never\r\n // emitted on `SoftphoneEvent.Error`: the promise IS the answer, and nothing about the phone is broken.\r\n 'line/claim-refused': {\r\n fatal: false,\r\n message: 'The line was not moved here: the user is on a call on the softphone that has it.',\r\n solution:\r\n '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.',\r\n },\r\n 'line/unavailable': {\r\n fatal: false,\r\n message: 'The line could not be taken: nothing decided where it is.',\r\n solution:\r\n '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.',\r\n },\r\n\r\n // ── Provider ────────────────────────────────────────────────────────────────\r\n 'provider/provision-failed': {\r\n fatal: true,\r\n message: 'The provider could not issue SIP credentials.',\r\n solution:\r\n 'The backend reached the carrier but got no usable credential. Check the org has a SIP extension provisioned.',\r\n },\r\n} as const satisfies Record<string, ErrorSpec>;\r\n\r\nexport type SoftphoneErrorCode = keyof typeof ERROR_CATALOGUE;\r\n\r\n/** Codes whose spec says `fatal: true`. Derived — cannot drift from the catalogue. */\r\nexport type FatalErrorCode = {\r\n [K in SoftphoneErrorCode]: (typeof ERROR_CATALOGUE)[K]['fatal'] extends true ? K : never;\r\n}[SoftphoneErrorCode];\r\n\r\nexport type NonFatalErrorCode = Exclude<SoftphoneErrorCode, FatalErrorCode>;\r\n\r\n/**\r\n * A fatal error: the call, or the engine, is over.\r\n *\r\n * `if (err.fatal)` narrows `err.code` to the fatal codes — which is the entire point of the split,\r\n * and the thing JSDoc could not have expressed (SPEC §2.2).\r\n */\r\nexport interface FatalSoftphoneError extends Error {\r\n readonly fatal: true;\r\n readonly code: FatalErrorCode;\r\n readonly solution: string;\r\n}\r\n\r\n/** A non-fatal error: a warning. The call, if any, continues. */\r\nexport interface NonFatalSoftphoneError extends Error {\r\n readonly fatal: false;\r\n readonly code: NonFatalErrorCode;\r\n readonly solution: string;\r\n}\r\n\r\nexport type SoftphoneError = FatalSoftphoneError | NonFatalSoftphoneError;\r\n\r\n/**\r\n * The one concrete error class. Not exported: the public contract is the `SoftphoneError` union and\r\n * the `code` on it, so nothing outside should be reaching for `instanceof`. It is a real `Error`\r\n * subclass, so `instanceof Error`, `.stack` and console formatting all behave as a host expects.\r\n */\r\nclass CatalogueError extends Error {\r\n readonly fatal: boolean;\r\n readonly code: SoftphoneErrorCode;\r\n readonly solution: string;\r\n\r\n constructor(code: SoftphoneErrorCode, cause?: unknown) {\r\n const spec: ErrorSpec = ERROR_CATALOGUE[code];\r\n // `cause` is only passed when there IS one: `{ cause: undefined }` would still define the\r\n // property, and an error carrying an explicit `undefined` cause reads as \"we looked and found\r\n // nothing\" rather than \"nothing was passed\".\r\n super(spec.message, cause === undefined ? undefined : { cause });\r\n this.name = 'SoftphoneError';\r\n this.code = code;\r\n this.fatal = spec.fatal;\r\n this.solution = spec.solution;\r\n }\r\n}\r\n\r\n/**\r\n * Is this already one of ours?\r\n *\r\n * Used where a layer catches whatever a lower one threw: a failure that already carries a catalogue\r\n * code should be reported AS that code, not flattened into the generic one for the layer that caught\r\n * it. Without this, a rejected voice token surfaces as \"the provider could not issue credentials\",\r\n * which is true and useless.\r\n *\r\n * Structural, not `instanceof` — the concrete class is not exported, and a duck-typed object with a\r\n * real code is as good as one of ours.\r\n */\r\nexport function isSoftphoneError(value: unknown): value is SoftphoneError {\r\n if (typeof value !== 'object' || value === null) return false;\r\n const candidate = value as { code?: unknown; fatal?: unknown };\r\n return typeof candidate.fatal === 'boolean' && (candidate.code as string) in ERROR_CATALOGUE;\r\n}\r\n\r\n/** Type guard, for hosts that would rather not rely on structural narrowing. */\r\nexport function isFatal(error: SoftphoneError): error is FatalSoftphoneError {\r\n return error.fatal;\r\n}\r\n\r\n/**\r\n * Construct a catalogue-backed error. **The only way the SDK raises one** — every `message` and\r\n * `solution` a host sees comes from the table above, so no call site invents error prose.\r\n *\r\n * The return type narrows on the code: `createError('call/busy')` is a `NonFatalSoftphoneError`, and\r\n * reading `.fatal` on it gives `false`, not `boolean`.\r\n */\r\nexport function createError<C extends SoftphoneErrorCode>(\r\n code: C,\r\n cause?: unknown,\r\n): C extends FatalErrorCode ? FatalSoftphoneError : NonFatalSoftphoneError {\r\n // The one cast in this module. A conditional return type cannot be verified from inside the\r\n // implementation — TypeScript has no way to prove the branch for an unresolved `C` — so the\r\n // guarantee rests on `fatal` being copied from the catalogue that DERIVES `FatalErrorCode`. The\r\n // test suite asserts that agreement for every code, which is the check the compiler cannot make.\r\n return new CatalogueError(code, cause) as C extends FatalErrorCode\r\n ? FatalSoftphoneError\r\n : NonFatalSoftphoneError;\r\n}\r\n"]}
@@ -44,6 +44,31 @@ function useIsLeader() {
44
44
  useCallback(() => phone.isLeader, [phone])
45
45
  );
46
46
  }
47
+ function useLine() {
48
+ const phone = useSoftphone();
49
+ const cache = useRef(null);
50
+ return useSyncExternalStore(
51
+ useCallback(
52
+ (cb) => {
53
+ const offLine = phone.on("lineChanged" /* LineChanged */, () => cb());
54
+ const offClaim = phone.on("lineClaimChanged" /* LineClaimChanged */, () => cb());
55
+ return () => {
56
+ offLine();
57
+ offClaim();
58
+ };
59
+ },
60
+ [phone]
61
+ ),
62
+ useCallback(() => {
63
+ const { line, lineWhere: where, lineClaim: claim } = phone;
64
+ const cached = cache.current;
65
+ if (cached?.line === line && cached.where === where && cached.claim === claim) return cached;
66
+ const view = { line, where, claim };
67
+ cache.current = view;
68
+ return view;
69
+ }, [phone])
70
+ );
71
+ }
47
72
  function useActiveCall() {
48
73
  const phone = useSoftphone();
49
74
  return useSyncExternalStore(
@@ -140,6 +165,6 @@ function useDevices() {
140
165
  return useSyncExternalStore(subscribe, getSnapshot);
141
166
  }
142
167
 
143
- export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, usePresence, useSoftphone, useSoftphoneState };
144
- //# sourceMappingURL=chunk-UHNLEBBO.js.map
145
- //# sourceMappingURL=chunk-UHNLEBBO.js.map
168
+ export { SoftphoneProvider, useActiveCall, useCall, useDevices, useIsLeader, useLastError, useLine, usePresence, useSoftphone, useSoftphoneState };
169
+ //# sourceMappingURL=chunk-I3RSC52R.js.map
170
+ //# sourceMappingURL=chunk-I3RSC52R.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/react/context.tsx","../src/react/hooks.ts"],"names":[],"mappings":"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","file":"chunk-I3RSC52R.js","sourcesContent":["import { createContext, useContext, type ReactNode } from 'react';\r\nimport type { Softphone } from '../core/softphone.js';\r\n\r\n/**\r\n * The provider takes an INSTANCE the host constructed — not config props.\r\n *\r\n * This is LiveKit's hard-won lesson (SPEC §5.1): they are deprecating their config-props\r\n * `<LiveKitRoom>` in favour of a provider that accepts an instance. Holding the `Softphone` above the\r\n * React tree lets the app own the connection, survive route changes and unmounts without dropping a\r\n * call, and unit-test the engine with no React at all. Config-props would rebuild the engine on\r\n * every prop change — catastrophic for something holding a live `RTCPeerConnection`.\r\n */\r\nconst SoftphoneContext = createContext<Softphone | null>(null);\r\n\r\nexport interface SoftphoneProviderProps {\r\n readonly softphone: Softphone;\r\n readonly children: ReactNode;\r\n}\r\n\r\nexport function SoftphoneProvider({ softphone, children }: SoftphoneProviderProps) {\r\n return <SoftphoneContext.Provider value={softphone}>{children}</SoftphoneContext.Provider>;\r\n}\r\n\r\n/** The `Softphone` instance from context. Stable — does not re-render on state changes. */\r\nexport function useSoftphone(): Softphone {\r\n const softphone = useContext(SoftphoneContext);\r\n if (!softphone) {\r\n throw new Error('useSoftphone() must be used within a <SoftphoneProvider>.');\r\n }\r\n return softphone;\r\n}\r\n","// Built-in, no shim: the peer floor is React 18 (SPEC §2.7), which ships useSyncExternalStore.\r\nimport { useCallback, useRef, useSyncExternalStore } from 'react';\r\nimport { useSoftphone } from './context.js';\r\nimport { SoftphoneEvent, CallEvent } from '../core/events.js';\r\nimport type { Call, CallState } from '../core/call.js';\r\nimport type { 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"]}
@@ -0,0 +1,13 @@
1
+ // src/core/api-base.ts
2
+ function normalizeApiBase(url) {
3
+ const trimmed = url.replace(/\/+$/, "");
4
+ if (!trimmed.endsWith("/api/v1")) return trimmed;
5
+ console.warn(
6
+ "[softphone] apiBase should be Coventia's domain only, without /api/v1. Ignoring the /api/v1 suffix."
7
+ );
8
+ return trimmed.slice(0, -"/api/v1".length).replace(/\/+$/, "");
9
+ }
10
+
11
+ export { normalizeApiBase };
12
+ //# sourceMappingURL=chunk-M76SAXXM.js.map
13
+ //# sourceMappingURL=chunk-M76SAXXM.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/api-base.ts"],"names":[],"mappings":";AAaO,SAAS,iBAAiB,GAAA,EAAqB;AACpD,EAAA,MAAM,OAAA,GAAU,GAAA,CAAI,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AACtC,EAAA,IAAI,CAAC,OAAA,CAAQ,QAAA,CAAS,SAAS,GAAG,OAAO,OAAA;AAEzC,EAAA,OAAA,CAAQ,IAAA;AAAA,IACN;AAAA,GACF;AACA,EAAA,OAAO,OAAA,CAAQ,MAAM,CAAA,EAAG,CAAC,UAAU,MAAM,CAAA,CAAE,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AAC/D","file":"chunk-M76SAXXM.js","sourcesContent":["/**\r\n * A Coventia base URL the way every request is built from it: no trailing slash, and no `/api/v1`.\r\n *\r\n * `apiBase` is Coventia's domain only — the SDK builds every route itself\r\n * (docs/specs/softphone-voice-token-allowlist.md §3.4). A trailing `/api/v1` is the one mistake worth\r\n * absorbing rather than refusing: the Partner API a host's backend calls does live under it, and the first\r\n * partner configured exactly that. Every softphone request then went to `/api/v1/phone/…`, where nothing\r\n * answers a voice token, and the 401 they got read as \"the SDK wants a different token\". Stripping it keeps\r\n * that configuration working; the warning says how to tidy it up.\r\n *\r\n * Only a FINAL `/api/v1` goes: a host proxying Coventia under a path of its own\r\n * (`https://example.com/coventia`) is left alone.\r\n */\r\nexport function normalizeApiBase(url: string): string {\r\n const trimmed = url.replace(/\\/+$/, '');\r\n if (!trimmed.endsWith('/api/v1')) return trimmed;\r\n\r\n console.warn(\r\n \"[softphone] apiBase should be Coventia's domain only, without /api/v1. Ignoring the /api/v1 suffix.\",\r\n );\r\n return trimmed.slice(0, -'/api/v1'.length).replace(/\\/+$/, '');\r\n}\r\n"]}
@@ -9,6 +9,8 @@ var SoftphoneEvent = /* @__PURE__ */ ((SoftphoneEvent2) => {
9
9
  SoftphoneEvent2["LeaderChanged"] = "leaderChanged";
10
10
  SoftphoneEvent2["TransferOutcome"] = "transferOutcome";
11
11
  SoftphoneEvent2["PresenceChanged"] = "presenceChanged";
12
+ SoftphoneEvent2["LineChanged"] = "lineChanged";
13
+ SoftphoneEvent2["LineClaimChanged"] = "lineClaimChanged";
12
14
  return SoftphoneEvent2;
13
15
  })(SoftphoneEvent || {});
14
16
  var CallEvent = /* @__PURE__ */ ((CallEvent2) => {
@@ -27,5 +29,5 @@ var CallEvent = /* @__PURE__ */ ((CallEvent2) => {
27
29
  })(CallEvent || {});
28
30
 
29
31
  export { CallEvent, SoftphoneEvent };
30
- //# sourceMappingURL=chunk-7R6LOOAI.js.map
31
- //# sourceMappingURL=chunk-7R6LOOAI.js.map
32
+ //# sourceMappingURL=chunk-N4PLEYFJ.js.map
33
+ //# sourceMappingURL=chunk-N4PLEYFJ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/core/events.ts"],"names":["SoftphoneEvent","CallEvent"],"mappings":";AAqBO,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;AAQlB,EAAAA,gBAAA,iBAAA,CAAA,GAAkB,iBAAA;AAQlB,EAAAA,gBAAA,aAAA,CAAA,GAAc,aAAA;AAMd,EAAAA,gBAAA,kBAAA,CAAA,GAAmB,kBAAA;AAhET,EAAA,OAAAA,eAAAA;AAAA,CAAA,EAAA,cAAA,IAAA,EAAA;AAiFL,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-N4PLEYFJ.js","sourcesContent":["import type { Call, CallState, HangupReason, HoldOriginator } from './call.js';\r\nimport type { SoftphoneError } from './errors.js';\r\nimport type {\r\n LineClaim,\r\n LineLocation,\r\n LineState,\r\n PresenceEntry,\r\n SoftphoneState,\r\n TransferOutcomeEvent,\r\n} 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 a\r\n * started leader — `online`, or waiting in `standby` for the line; never fires at all otherwise,\r\n * same reasoning as `TransferOutcome`.\r\n */\r\n PresenceChanged = 'presenceChanged',\r\n /**\r\n * `softphone.line` / `lineWhere` changed (`docs/specs/line-owner.md` §5): this softphone got the line,\r\n * lost it to another softphone of the same extension, or stopped knowing (`unknown` — it then listens,\r\n * as before the line owner existed). A third axis beside `StateChanged` and `LeaderChanged`: losing the\r\n * line is what takes the softphone to `standby`, but not at once while a call is in progress — the line\r\n * can read `elsewhere` while the state is still `online`, until that call ends.\r\n */\r\n LineChanged = 'lineChanged',\r\n /**\r\n * `softphone.lineClaim` changed: an attempt to bring the line here started, was refused, failed, or ended\r\n * (the line came here, or the softphone stopped). Kept apart from `LineChanged`, which only ever means the\r\n * line itself moved.\r\n */\r\n LineClaimChanged = 'lineClaimChanged',\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 [SoftphoneEvent.LineChanged]: { readonly line: LineState; readonly where: LineLocation | null };\r\n [SoftphoneEvent.LineClaimChanged]: { readonly claim: LineClaim };\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"]}
@@ -57,8 +57,62 @@ interface CallOptions {
57
57
  * Zadarma registers on-demand, per call, in response to a push (SPEC §3.2); a persistent UA is
58
58
  * ignored by its PBX. Leaking "registered" into the public API would force every future provider —
59
59
  * including our own SIP infrastructure, which registers persistently — to fake it.
60
+ *
61
+ * `standby` means started, and this browser's leader, but ANOTHER softphone of the same extension has
62
+ * the line — a tab in another browser, the desktop app, a partner page (`docs/specs/line-owner.md` §5).
63
+ * Its push socket is closed on purpose, so nothing here registers or rings: `line` says where the calls
64
+ * go, and `takeLine()` — or a `call()` — brings them here. A host that reads `online` as "ready"
65
+ * correctly reads this as not ready.
66
+ */
67
+ type SoftphoneState = 'stopped' | 'starting' | 'online' | 'standby' | 'offline' | 'reconnecting' | 'destroyed';
68
+ /**
69
+ * Where this extension's incoming calls ring, as far as this softphone knows. `here`: it has the line.
70
+ * `elsewhere`: another softphone of the same extension has it, and this one waits in `standby`. `unknown`:
71
+ * nobody has said — no presence source, a backend that predates the line owner, a presence socket that is
72
+ * down — and this softphone listens, exactly as before the line owner existed (§5.1 R7, fail-open).
73
+ */
74
+ type LineState = 'here' | 'elsewhere' | 'unknown';
75
+ /** Who has the line when it is `elsewhere`: the desktop app, or any other softphone (a browser tab). */
76
+ type LineLocation = 'desktop' | 'browser';
77
+ /**
78
+ * The last attempt to bring the line here — `takeLine()`, or a `call()` from `standby`, whoever made it:
79
+ * `claiming` while it is in flight, `refused` when the user is on a call where the line is, `failed` when
80
+ * nothing could decide. `idle` before any attempt, once the line comes here, and after a `stop()`.
81
+ */
82
+ type LineClaim = 'idle' | 'claiming' | 'refused' | 'failed';
83
+ /** The server's word on this softphone's line, as a `PresenceSource` hands it over (`PresenceCallbacks.onLine`). */
84
+ interface LineUpdate {
85
+ readonly state: 'here' | 'elsewhere';
86
+ /** Who has the line when it is `elsewhere`; `null` when it is here, or the server did not say. */
87
+ readonly where: LineLocation | null;
88
+ }
89
+ /**
90
+ * A call offered to this user's line, as the backend announces it over the presence socket
91
+ * (incoming-call-reliability.md §5): `start` each time the call is offered to the extension (a re-offer of the
92
+ * same call repeats it, same `callId`), `end` when the call is over. `callId` is opaque; `from` is the caller,
93
+ * `null` (or, on an `end`, absent) when the backend did not say.
94
+ *
95
+ * The softphone uses it for one thing: after a decline, to know when the declined call's re-offers stop, so a
96
+ * new call from the same number rings (§4.4). A presence source that never sends it loses nothing but that.
60
97
  */
61
- type SoftphoneState = 'stopped' | 'starting' | 'online' | 'offline' | 'reconnecting' | 'destroyed';
98
+ type CallOffer = {
99
+ readonly state: 'start';
100
+ readonly callId: string;
101
+ readonly from: string | null;
102
+ } | {
103
+ readonly state: 'end';
104
+ readonly callId: string;
105
+ readonly from?: string | null;
106
+ };
107
+ /** The answer to `PresenceConnection.claimLine()`. */
108
+ interface LineClaimResult {
109
+ readonly ok: boolean;
110
+ /**
111
+ * Why it was refused: `'on_call'` while the user is on a call (the line never moves during one). Any
112
+ * other value means nothing was decided. Absent when `ok`.
113
+ */
114
+ readonly reason?: string;
115
+ }
62
116
  interface ContactsPage {
63
117
  readonly items: readonly Contact[];
64
118
  /**
@@ -210,6 +264,18 @@ interface PresenceConnection {
210
264
  * own source that has no such message simply omits it and gets the grace. Sent just before `close()`.
211
265
  */
212
266
  readonly bye?: () => void;
267
+ /**
268
+ * The line owner (`docs/specs/line-owner.md` §5.2): says which softphone this connection speaks for, so
269
+ * the server can tell it whether it has the line — through `PresenceCallbacks.onLine`, right after this
270
+ * and on every change. Sent as the FIRST message of every connection. Optional: a source without it never
271
+ * takes part, and its softphone listens exactly as before (fail-open), reading its line as `unknown`.
272
+ */
273
+ readonly hello?: (clientId: string) => void;
274
+ /**
275
+ * "Recibir aquí": asks for the line. Answered through `PresenceCallbacks.onClaimResult` — and, when it
276
+ * succeeds, by `onLine` saying `here` first. Optional, like `hello`.
277
+ */
278
+ readonly claimLine?: () => void;
213
279
  /**
214
280
  * Ends this connection deliberately. Never causes the SAME callbacks' `onClose` to fire — that is
215
281
  * reserved for the connection ending on its OWN (a network drop, a server-side close, a rejected
@@ -229,6 +295,19 @@ interface PresenceCallbacks {
229
295
  * identical from here, and both get the same treatment (spec §4.4 — quiet retry, nothing surfaced).
230
296
  */
231
297
  readonly onClose: () => void;
298
+ /**
299
+ * The line, as the server says it — right after `PresenceConnection.hello`, and on every change after
300
+ * that. Optional, so callbacks written before the line owner existed still type-check.
301
+ */
302
+ readonly onLine?: (update: LineUpdate) => void;
303
+ /** The answer to `PresenceConnection.claimLine()`. Optional, like `onLine`. */
304
+ readonly onClaimResult?: (result: LineClaimResult) => void;
305
+ /**
306
+ * A call offered to this line started or ended (`CallOffer`). Sent only to the line's owner. Optional, like
307
+ * `onLine`: callbacks written before it existed still type-check, and a source that never calls it leaves the
308
+ * softphone on its fallback after a decline (incoming-call-reliability.md §4.4).
309
+ */
310
+ readonly onOffer?: (offer: CallOffer) => void;
232
311
  }
233
312
  /**
234
313
  * Opens one live presence connection. Coventia-backed by default (mints a one-time ticket, opens
@@ -470,6 +549,16 @@ declare const ERROR_CATALOGUE: {
470
549
  readonly message: "The provider does not support this operation.";
471
550
  readonly solution: "Guard the UI on the operation before offering it.";
472
551
  };
552
+ readonly 'line/claim-refused': {
553
+ readonly fatal: false;
554
+ readonly message: "The line was not moved here: the user is on a call on the softphone that has it.";
555
+ readonly solution: "Nothing to fix. The line never moves during a call; take it again once that call has ended. Incoming calls keep ringing where they ring now.";
556
+ };
557
+ readonly 'line/unavailable': {
558
+ readonly fatal: false;
559
+ readonly message: "The line could not be taken: nothing decided where it is.";
560
+ readonly solution: "The presence connection is down, the backend predates the line owner, or it did not answer in time. Retry in a moment; incoming calls keep ringing where they ring now.";
561
+ };
473
562
  readonly 'provider/provision-failed': {
474
563
  readonly fatal: true;
475
564
  readonly message: "The provider could not issue SIP credentials.";
@@ -556,10 +645,25 @@ declare enum SoftphoneEvent {
556
645
  /**
557
646
  * `softphone.presence` changed — the live agent-presence list (Phase B,
558
647
  * `docs/specs/transfer-presence-picker.md`) gained a snapshot or an updated entry. Requires a
559
- * `PresenceSource` (from `coventia`/`apiBase`, or a host override) AND this tab currently being
560
- * `online && isLeader`; never fires at all otherwise, same reasoning as `TransferOutcome`.
648
+ * `PresenceSource` (from `coventia`/`apiBase`, or a host override) AND this tab currently being a
649
+ * started leader — `online`, or waiting in `standby` for the line; never fires at all otherwise,
650
+ * same reasoning as `TransferOutcome`.
651
+ */
652
+ PresenceChanged = "presenceChanged",
653
+ /**
654
+ * `softphone.line` / `lineWhere` changed (`docs/specs/line-owner.md` §5): this softphone got the line,
655
+ * lost it to another softphone of the same extension, or stopped knowing (`unknown` — it then listens,
656
+ * as before the line owner existed). A third axis beside `StateChanged` and `LeaderChanged`: losing the
657
+ * line is what takes the softphone to `standby`, but not at once while a call is in progress — the line
658
+ * can read `elsewhere` while the state is still `online`, until that call ends.
659
+ */
660
+ LineChanged = "lineChanged",
661
+ /**
662
+ * `softphone.lineClaim` changed: an attempt to bring the line here started, was refused, failed, or ended
663
+ * (the line came here, or the softphone stopped). Kept apart from `LineChanged`, which only ever means the
664
+ * line itself moved.
561
665
  */
562
- PresenceChanged = "presenceChanged"
666
+ LineClaimChanged = "lineClaimChanged"
563
667
  }
564
668
  interface SoftphoneEventMap {
565
669
  [SoftphoneEvent.StateChanged]: {
@@ -580,6 +684,13 @@ interface SoftphoneEventMap {
580
684
  [SoftphoneEvent.PresenceChanged]: {
581
685
  readonly agents: readonly PresenceEntry[];
582
686
  };
687
+ [SoftphoneEvent.LineChanged]: {
688
+ readonly line: LineState;
689
+ readonly where: LineLocation | null;
690
+ };
691
+ [SoftphoneEvent.LineClaimChanged]: {
692
+ readonly claim: LineClaim;
693
+ };
583
694
  }
584
695
  declare enum CallEvent {
585
696
  /** Any transition. The catch-all; the specific events below are conveniences. */
@@ -680,7 +791,7 @@ interface TypedEmitter<M> {
680
791
  *
681
792
  * - **`rejecting`.** Today's engine has one, purely to swallow Zadarma's `return_timeout` retries
682
793
  * for 35 s after a decline (SPEC §3.4). That is a PROVIDER quirk — it belongs in
683
- * `providers/zadarma.ts` as `quirks.rejectGuardMs`, not in the shared state machine. A rejected
794
+ * `providers/zadarma.ts` as `quirks.declinedCallerGuardMs`, not in the shared state machine. A rejected
684
795
  * call is simply `ended`. Our own SIP infrastructure would not need it at all (SPEC §2.3).
685
796
  *
686
797
  * - **A pre-INVITE state.** A Call exists only once an INVITE has landed. The "user answered
@@ -878,6 +989,39 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
878
989
  * "active in another tab" state for that duration (the `/ui` widget already does).
879
990
  */
880
991
  readonly isLeader: boolean;
992
+ /**
993
+ * Where this extension's incoming calls ring (`docs/specs/line-owner.md` §5) — the axis ACROSS browsers,
994
+ * devices and the desktop app that `isLeader` cannot see. Only one softphone of an extension listens:
995
+ * the first one started keeps the line, and the others wait in `standby` until it is free or the user
996
+ * takes it (`takeLine()`).
997
+ *
998
+ * `unknown` until the backend has said, and whenever it cannot (no presence source, a backend that
999
+ * predates the line owner, a presence socket that is down): the softphone then listens, exactly as
1000
+ * before this existed — it never leaves nobody listening. Changes fire `SoftphoneEvent.LineChanged`.
1001
+ */
1002
+ readonly line: LineState;
1003
+ /**
1004
+ * Who has the line while it is `elsewhere` — the desktop app, or another softphone (a browser tab) —
1005
+ * or `null`: the line is here, unknown, or the backend did not say.
1006
+ */
1007
+ readonly lineWhere: LineLocation | null;
1008
+ /**
1009
+ * How the last attempt to bring the line here went, whoever made it (`takeLine()`, or a `call()` from
1010
+ * `standby`) — what the waiting screen shows under its title, so a refused click-to-call explains itself
1011
+ * just like a refused "Recibir aquí". Changes fire `SoftphoneEvent.LineClaimChanged`.
1012
+ */
1013
+ readonly lineClaim: LineClaim;
1014
+ /**
1015
+ * "Recibir aquí": takes the line from whichever softphone has it, so incoming calls ring here. Resolves
1016
+ * once the line IS here (at once when it already is); the softphone then leaves `standby` and starts
1017
+ * listening on its own.
1018
+ *
1019
+ * Rejects with a non-fatal `SoftphoneError`: `line/claim-refused` while the user is on a call on the
1020
+ * softphone that has the line (it never moves during a call — retry once it ends), or `line/unavailable`
1021
+ * when nothing could decide (no presence connection, a backend without the line owner, no answer in
1022
+ * time). Neither is emitted on `SoftphoneEvent.Error` — this promise is the answer.
1023
+ */
1024
+ takeLine(): Promise<void>;
881
1025
  /**
882
1026
  * Start.
883
1027
  *
@@ -890,6 +1034,11 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
890
1034
  stop(): Promise<void>;
891
1035
  /** Idempotent. After this the instance is dead — construct a new one. */
892
1036
  destroy(): Promise<void>;
1037
+ /**
1038
+ * Dial. From `standby` this takes the line first (`takeLine()`), then dials — a call the host starts is
1039
+ * the user asking for the phone here — and rejects with that method's `line/*` error if the line cannot
1040
+ * be taken.
1041
+ */
893
1042
  call(destination: string, options?: CallOptions): Promise<Call>;
894
1043
  /** The live call, or null. The consult leg of an attended transfer is reachable from it. */
895
1044
  readonly activeCall: Call | null;
@@ -904,8 +1053,9 @@ interface Softphone extends TypedEmitter<SoftphoneEventMap> {
904
1053
  /**
905
1054
  * The live agent-presence list (Phase B, `docs/specs/transfer-presence-picker.md`) — every org
906
1055
  * member with a phone extension, whatever their state. Empty when there is no `PresenceSource`
907
- * (no Coventia backend, or a host that omitted one), or while this tab is not `online && isLeader`
908
- * — never an error, just nothing to show (spec §4.4's own success criterion). Updates fire
1056
+ * (no Coventia backend, or a host that omitted one), or while this tab is not a started leader —
1057
+ * `online`, or waiting in `standby` for the line — or its connection is down: never an error, just
1058
+ * nothing to show (spec §4.4's own success criterion). Updates fire
909
1059
  * `SoftphoneEvent.PresenceChanged`; `usePresence()` is the reactive read.
910
1060
  */
911
1061
  readonly presence: readonly PresenceEntry[];
@@ -1053,6 +1203,16 @@ interface SoftphoneElementEventMap {
1053
1203
  'cv-leadership-changed': CustomEvent<{
1054
1204
  readonly isLeader: boolean;
1055
1205
  }>;
1206
+ /**
1207
+ * Where this extension's calls ring changed (`docs/specs/line-owner.md` §5) — the axis across browsers,
1208
+ * devices and the desktop app that `cv-leadership-changed` cannot see. `line: 'elsewhere'` is what puts
1209
+ * the phone in `standby` (`cv-state-changed`), and **the widget already shows its own waiting screen with
1210
+ * "Recibir aquí" for it** — listen here for your own indicator; `el.softphone.takeLine()` takes the line.
1211
+ */
1212
+ 'cv-line-changed': CustomEvent<{
1213
+ readonly line: LineState;
1214
+ readonly where: LineLocation | null;
1215
+ }>;
1056
1216
  /**
1057
1217
  * A transfer's final handoff step resolved — `status` says whether the caller actually reached
1058
1218
  * `target`. Polled, not instant (Phase A, `docs/specs/transfer-outcome-relay.md`); only for that