@softomnitel/omnicall-kit 0.1.0 → 0.1.2

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 (2) hide show
  1. package/README.md +676 -563
  2. package/package.json +1 -10
package/README.md CHANGED
@@ -1,7 +1,6 @@
1
1
  # @softomnitel/omnicall-kit
2
2
 
3
- Browser SDK client for OmniCall Desktop (local protocol).
4
- Каноническая документация для интеграторов — ниже (русский гайд). Полная копия в репозитории: `docs/guide/RU-DEVELOPER-GUIDE.md`. EN pages: `docs/guide/`.
3
+ Browser SDK client for OmniCall Desktop (local protocol).
5
4
 
6
5
  ```bash
7
6
  npm install @softomnitel/omnicall-kit
@@ -9,768 +8,882 @@ npm install @softomnitel/omnicall-kit
9
8
 
10
9
  ---
11
10
 
12
- # OmniCall Kit — руководство для разработчиков
11
+ # OmniCall Kit
13
12
 
14
- Канонический русскоязычный гайд для интеграторов CRM: один файл — от установки до продакшена. Источники правды по символам API: [`etc/api/sdk.api.md`](../../etc/api/sdk.api.md) и EN pages в `docs/guide/`. Если формулировка здесь расходится с API report — побеждает report.
13
+ `@softomnitel/omnicall-kit` типизированный JavaScript/TypeScript-клиент для
14
+ CRM. Он подключает страницу CRM к уже установленному **OmniCall Desktop** на
15
+ компьютере оператора.
15
16
 
16
- ## Статус пакета
17
+ SDK получает состояние звонков и оператора, а также отправляет разрешённые
18
+ команды: начать звонок, ответить, положить трубку или изменить статус. SIP,
19
+ пароли и внутренняя телефония остаются внутри Desktop и не попадают в браузер.
17
20
 
18
- Пакет `@softomnitel/omnicall-kit` — тонкий браузерный клиент к локальному gateway OmniCall Desktop. SIP, OCP и Call Engine остаются на desktop; в браузере нет второго softphone и нет SIP-стека.
21
+ ```ts
22
+ import {
23
+ createIndexedDbPopKeyStore,
24
+ createOmniCallClient
25
+ } from '@softomnitel/omnicall-kit';
19
26
 
20
- | Поле | Значение |
21
- | --- | --- |
22
- | npm | `@softomnitel/omnicall-kit` (+ транзитивно `@softomnitel/omnicall-protocol`) |
23
- | Stable | `0.1.0` (`latest`) |
24
- | RC | `0.1.0-rc.0` (`rc`) |
25
- | Feature Registry | F-011 — **implemented** (DI-10 closed) |
26
- | Браузеры | Chromium / Edge (Chromium). Firefox/Safari — не заявлены |
27
- | Модуль | ESM only, Node `>=20.19.0` |
28
- | SDK **не** делает | SIP/OCP auth secrets, telephony FSM, `window.Softphone`, fetch-fallback к desktop HTTP |
27
+ const client = createOmniCallClient({ /* параметры ниже */ });
28
+ await client.connect();
29
+ await client.waitUntil((state) => state === 'ready');
30
+ const snapshot = await client.getSnapshot();
31
+ console.log(snapshot.revision);
32
+ ```
29
33
 
30
- ---
34
+ ## Содержание
31
35
 
32
- ## Кому и зачем
36
+ - [Что нужно для работы](#что-нужно-для-работы)
37
+ - [Установка](#установка)
38
+ - [Быстрый старт](#быстрый-старт)
39
+ - [Основные понятия](#основные-понятия)
40
+ - [Состояния и события](#состояния-и-события)
41
+ - [API Reference](#api-reference)
42
+ - [Рецепты](#рецепты)
43
+ - [Ошибки и FAQ](#ошибки-и-faq)
44
+ - [Миграция и совместимость](#миграция-и-совместимость)
33
45
 
34
- Аудитория: frontend/CRM-разработчики (JS/TS), которые встраивают SDK в HTTPS-страницу CRM при уже установленном OmniCall Desktop.
46
+ ## Что нужно для работы
35
47
 
36
- Цель интеграции:
48
+ 1. OmniCall Desktop должен быть установлен, запущен и иметь включённый SDK
49
+ gateway.
50
+ 2. Страница CRM должна работать в Chromium или Edge на Chromium.
51
+ 3. Для сборки проекта нужен Node.js `>=20.19.0` и npm `>=10`.
52
+ 4. В браузере нужны Web Crypto и IndexedDB. Они хранят криптографическую
53
+ идентичность браузера.
54
+ 5. Если CRM работает по HTTPS, браузер может спросить разрешение на связь с
55
+ локальной программой. Объясните это действие оператору в интерфейсе.
37
56
 
38
- 1. Pairing + PoP-сессия к loopback WebSocket gateway.
39
- 2. UI CRM = redacted **snapshot** + публичные **events**.
40
- 3. Мутации звонков / оператора / окна / logout / activate — только через namespaced API с `expectedRevision` и server-issued capabilities.
57
+ Firefox и Safari пока не входят в заявленную матрицу поддержки.
41
58
 
42
- Не цель: зеркалировать Domain Events desktop, хранить секреты в браузере, изобретать второй Call Engine.
59
+ ## Установка
43
60
 
44
- ---
61
+ ```bash
62
+ npm install @softomnitel/omnicall-kit
63
+ ```
64
+
65
+ Для воспроизводимой production-сборки зафиксируйте версию, доступную в вашем
66
+ закрытом npm registry:
67
+
68
+ ```bash
69
+ npm install @softomnitel/omnicall-kit@0.1.2
70
+ ```
45
71
 
46
- ## Быстрый старт (5 минут)
72
+ Пакет ESM-only. Импортируйте его через `import`, а не `require`.
47
73
 
48
- Конструктор **без** сетевых side effects. Сеть начинается только на `connect()`.
74
+ ## Быстрый старт
49
75
 
50
- <details>
51
- <summary>Пример: минимальный connect → ready → snapshot</summary>
76
+ ### 1. Создайте хранилище идентичности браузера
77
+
78
+ При первом подключении оператор подтверждает, что CRM может работать с Desktop.
79
+ Этот процесс называется **pairing**. После подтверждения SDK сохраняет
80
+ криптографическую идентичность браузера в IndexedDB. Не храните её в
81
+ `localStorage` или `sessionStorage`.
52
82
 
53
83
  ```ts
54
- import {
55
- createOmniCallClient,
56
- createIndexedDbPopKeyStore,
57
- createMemoryPopKeyStore,
58
- isOmniCallClientError
59
- } from '@softomnitel/omnicall-kit';
84
+ import { createIndexedDbPopKeyStore } from '@softomnitel/omnicall-kit';
85
+
86
+ const keyStore = createIndexedDbPopKeyStore({
87
+ // Стабильная строка для одной установки CRM в одном браузере.
88
+ installId: 'crm-production'
89
+ });
90
+ ```
91
+
92
+ ### 2. Создайте клиент
60
93
 
61
- const keyStore =
62
- typeof indexedDB !== 'undefined'
63
- ? createIndexedDbPopKeyStore({ installId: 'crm-install-1' })
64
- : createMemoryPopKeyStore();
94
+ `origin` точный адрес CRM из браузера: схема, домен и порт. Например,
95
+ `https://crm.example`. Desktop не принимает маски и части строк.
96
+
97
+ ```ts
98
+ import { createOmniCallClient } from '@softomnitel/omnicall-kit';
65
99
 
66
100
  const client = createOmniCallClient({
67
101
  url: 'ws://127.0.0.1:17341/omnicall/v1/ws',
68
- origin: 'https://crm.example', // exact Origin
69
- application: { name: 'my-crm', version: '1.2.0' },
70
- sdkVersion: '0.1.0',
102
+ origin: window.location.origin,
103
+ application: { name: 'my-crm', version: '1.0.0' },
104
+ sdkVersion: '0.1.2',
71
105
  requestedProfile: 'call_controller',
72
106
  requestedCapabilities: [
73
107
  'session.read.redacted',
74
108
  'window.show',
75
109
  'call.originate',
76
110
  'call.control',
77
- 'session.logout',
78
- 'operator.status.write'
111
+ 'operator.status.write',
112
+ 'session.logout'
79
113
  ],
80
114
  keyStore
81
- // transportFactory / scheduler / jitter опущены → browser defaults
82
115
  });
116
+ ```
117
+
118
+ `requestedCapabilities` — это просьба о правах, а не их выдача. Desktop выдаёт
119
+ только права, разрешённые для этого Origin. Права `account.activate` и
120
+ `window.hide` нельзя запросить при pairing: оператор выдаёт их отдельно в
121
+ настройках Desktop.
83
122
 
123
+ ### 3. Покажите оператору, что pairing ожидает подтверждения
124
+
125
+ ```ts
84
126
  client.onPairingRequired((info) => {
85
- // Пользователь должен Allow Origin (TOFU) и Approve pairing в OmniCall
86
- void info.origin;
87
- void info.requestedProfile;
127
+ console.info(
128
+ `Подтвердите подключение ${info.origin} в окне OmniCall Desktop.`
129
+ );
88
130
  });
89
131
 
90
132
  client.onStateChange((state) => {
91
- void state;
133
+ console.info(`Состояние SDK: ${state}`);
92
134
  });
135
+ ```
136
+
137
+ Подписки возвращают функцию отписки. Вызовите её при размонтировании компонента
138
+ или закрытии вкладки.
93
139
 
140
+ ### 4. Подключитесь и получите состояние
141
+
142
+ `connect()` начинает сетевое подключение и возвращает `Promise`. `Promise`
143
+ означает результат, который появится позже. `await` ждёт этот результат, не
144
+ блокируя браузер.
145
+
146
+ ```ts
94
147
  await client.connect();
95
- await client.waitUntil((s) => s === 'ready');
148
+ await client.waitUntil((state) => state === 'ready', 60_000);
96
149
 
97
150
  const snapshot = await client.getSnapshot();
98
- const revision = snapshot.revision;
151
+ console.log('Версия состояния:', snapshot.revision);
152
+ console.log('Звонки:', snapshot.sections.calls);
153
+ ```
154
+
155
+ Если состояние стало `pairing_required`, оператор должен подтвердить CRM в
156
+ OmniCall Desktop. Не создавайте второй клиент и не повторяйте `connect()` в
157
+ цикле.
158
+
159
+ ## Основные понятия
160
+
161
+ | Понятие | Что это означает для CRM |
162
+ | --- | --- |
163
+ | OmniCall Desktop | Программа на компьютере оператора. Она владеет телефонией и учётной записью. |
164
+ | pairing | Первое подтверждение доступа конкретного сайта к Desktop. |
165
+ | capability | Право на одну группу действий, например `call.originate`. Проверяйте выданные права перед показом кнопки. |
166
+ | snapshot | Полный согласованный снимок состояния Desktop. Это главный источник данных для интерфейса CRM. |
167
+ | event | Уведомление об изменении, например `call:incoming`. Событие помогает быстро обновить UI, но не заменяет snapshot. |
168
+ | revision | Номер версии snapshot. Перед командой, меняющей состояние, передайте актуальный номер в `expectedRevision`. |
169
+
170
+ Поток данных выглядит так:
171
+
172
+ ```text
173
+ CRM в браузере
174
+ → OmniCall Kit
175
+ → локальный WebSocket на 127.0.0.1
176
+ → OmniCall Desktop
177
+ → телефония и операторская платформа
178
+ ```
179
+
180
+ SDK не является вторым телефоном. Он не даёт CRM SIP-пароль, не открывает
181
+ внутренний протокол операторской платформы и не поддерживает legacy
182
+ `window.Softphone`.
183
+
184
+ ### Snapshot, события и revision
185
+
186
+ После `ready` вызовите `getSnapshot()`. Далее подпишитесь на события. После
187
+ reconnect снова получите snapshot.
188
+
189
+ Команда изменения состояния принимает `expectedRevision`. Если Desktop уже
190
+ изменился в другой вкладке, SDK отклонит команду с кодом `stale_state`. Получите
191
+ новый snapshot и дайте пользователю повторить намеренное действие.
99
192
 
100
- client.subscribe('call:incoming', (event) => {
101
- void event.type; // не логируйте полный payload в проде
193
+ ```ts
194
+ async function getRevision(): Promise<number> {
195
+ return client.getRevision() ?? (await client.getSnapshot()).revision;
196
+ }
197
+
198
+ const result = await client.calls.originate({
199
+ destination: '+74951234567',
200
+ expectedRevision: await getRevision()
102
201
  });
103
202
 
104
- void revision;
105
- void isOmniCallClientError;
203
+ console.log(result.callId, result.revision);
106
204
  ```
107
205
 
108
- </details>
206
+ Не повторяйте автоматически `originate`, `hangup`, `logout` или
207
+ `activateProfile` после reconnect. Эти действия могут сработать дважды.
208
+
209
+ ## Состояния и события
109
210
 
110
- <details>
111
- <summary>Почему так / на что смотреть</summary>
211
+ ### Состояния подключения
112
212
 
113
- - PoP: IndexedDB в браузере, memory в тестах — никогда Web Storage.
114
- - Privileged caps (`account.activate`, `window.hide`) **не** запрашивайте на pairing — SDK strips.
115
- - После `ready` берите revision из snapshot / `getRevision()` перед мутациями.
116
- - Runnable (fake peer): [`examples/crm-pairing-lite/`](../../examples/crm-pairing-lite/).
213
+ | Состояние | Что показать пользователю |
214
+ | --- | --- |
215
+ | `idle` | Кнопку подключения. |
216
+ | `connecting`, `handshaking`, `authenticating` | Индикатор процесса. |
217
+ | `pairing_required` | Инструкцию подтвердить CRM в Desktop. |
218
+ | `ready` | Основной интерфейс CRM. |
219
+ | `reconnecting` | Неблокирующее сообщение о восстановлении связи. |
220
+ | `revoked` | Очистите UI сессии и предложите пройти pairing снова. |
221
+ | `incompatible` | Предложите обновить CRM SDK или Desktop. |
222
+ | `failed` | Покажите безопасное описание ошибки из `getConnectError()`. |
223
+ | `closed` | Разрешите пользователю подключиться снова. |
117
224
 
118
- </details>
225
+ ### События
119
226
 
120
- Краткая лестница состояний:
227
+ Подписывайтесь только на имена из `PUBLIC_EVENT_TYPES`.
121
228
 
122
- | Состояние | UI хоста |
229
+ | Событие | Назначение |
123
230
  | --- | --- |
124
- | `idle` | Кнопка Connect |
125
- | `connecting` / `handshaking` / `authenticating` | Spinner |
126
- | `pairing_required` | «Подтвердите в OmniCall» |
127
- | `ready` | Включить product UI |
128
- | `reconnecting` | Non-blocking banner |
129
- | `revoked` / `incompatible` / `failed` / `closed` | Очистить сессию; re-pair при необходимости |
231
+ | `call:incoming` | Показать входящий звонок и кнопки ответа или отклонения. |
232
+ | `call:outgoing`, `call:ringing`, `call:answered` | Обновить ход исходящего или активного звонка. |
233
+ | `call:ended`, `call:failed` | Убрать карточку звонка или показать ошибку. |
234
+ | `call:held`, `call:resumed`, `call:muted`, `call:unmuted` | Обновить кнопки управления разговором. |
235
+ | `call:acd-context` | Получить публичный контекст очереди, если выдано право `ocp.acd_context.read`. |
236
+ | `registration:changed` | Обновить индикатор регистрации телефонии. |
237
+ | `account:session-activated`, `account:session-ended` | Обновить состояние учётной записи. |
238
+ | `operator:session-changed`, `operator:status-changed` | Обновить статус оператора и отложенную смену статуса. |
239
+ | `operator:campaign-offered`, `operator:campaign-cleared` | Обновить данные кампании. |
240
+ | `window:visibility-changed` | Обновить состояние окна Desktop. |
241
+ | `sdk:server-shutdown` | Показать сообщение, что Desktop завершает работу. |
130
242
 
131
- EN: [pairing-quick-start.md](../../docs/guide/pairing-quick-start.md).
243
+ ```ts
244
+ const stop = client.subscribe('call:incoming', (event) => {
245
+ console.log('Входящий звонок:', event.payload.callId);
246
+ });
132
247
 
133
- ---
248
+ // Позднее, например при размонтировании UI:
249
+ stop();
250
+ ```
134
251
 
135
- ## Модель системы (как это устроено)
252
+ ## API Reference
136
253
 
137
- ```text
138
- CRM (HTTPS)
139
- @softomnitel/omnicall-kit (OmniCallClient)
140
- → TransportPort (createBrowserWebSocketTransport)
141
- → ws://127.0.0.1:…/omnicall/v1/ws
142
- OmniCall Desktop gateway (Electron main)
143
- → Application / Call Engine / SIP / OCP
254
+ ### `createOmniCallClient(options)`
255
+
256
+ Создаёт полный клиент CRM. Конструктор не открывает сеть; вызовите `connect()`.
257
+
258
+ ```ts
259
+ function createOmniCallClient(options: OmniCallClientOptions): OmniCallClient;
144
260
  ```
145
261
 
146
- | Роль | Владелец правды |
147
- | --- | --- |
148
- | Telephony / operator FSM / credentials | Desktop |
149
- | Session auth (pairing, PoP, capabilities) | Desktop + SDK auth orchestrator |
150
- | UI CRM state | Snapshot + public events (клиент — thin reader/writer) |
151
- | Transport bytes | Тонкий `TransportPort` — без JSON parse и без reconnect |
262
+ `OmniCallClientOptions` равен `AuthClientOptions`.
152
263
 
153
- Discovery / Local Network Access (HTTPS loopback): см. [installation.md](../../docs/guide/installation.md) и [transport.md](../../docs/guide/transport.md). Ошибки LNA: `local_network_permission_required` / `_denied`.
264
+ | Поле | Тип | Обязательно | Описание |
265
+ | --- | --- | --- | --- |
266
+ | `url` | `string` | Да | WebSocket URL SDK gateway Desktop. |
267
+ | `origin` | `string` | Да | Точный Origin CRM. |
268
+ | `application` | `ApplicationIdentity` | Да | Имя и версия вашего приложения. |
269
+ | `sdkVersion` | `string` | Да | Версия SDK, совместимая с Desktop. |
270
+ | `requestedProfile` | `PairingProfile` | Да | `presentation`, `operator` или `call_controller`. |
271
+ | `requestedCapabilities` | `readonly CapabilityId[]` | Нет | Запрашиваемые неприоритетные права. |
272
+ | `keyStore` | `PopKeyStore` | Да | Хранилище идентичности pairing. |
273
+ | `transportFactory`, `scheduler`, `jitter` | соответствующие интерфейсы | Нет | Заменяйте только в тестах или особой среде. Браузерные значения используются по умолчанию. |
274
+ | `diagnostics` | `DiagnosticsSink` | Нет | Получатель безопасных диагностических событий. |
275
+ | `defaultRequestTimeoutMs` | `number` | Нет | Таймаут команд в миллисекундах. |
276
+ | `reconnect` | `ReconnectPolicy` | Нет | Политика ограниченных повторных подключений. |
277
+ | `heartbeat` | `HeartbeatPolicy` | Нет | Настройка проверки живости соединения. |
154
278
 
155
- Официальный транспорт: `createBrowserWebSocketTransport`. Не парсите JSON сами. Defaults: `createBrowserScheduler` / `createBrowserJitterSource`. В unit-тестах inject FakeTransport + fake scheduler/jitter.
279
+ Возвращает `OmniCallClient`. Ошибки настройки и соединения приходят через
280
+ `Promise` методов клиента как `OmniCallClientError`.
156
281
 
157
- Shared desk (ADR-0021): любой авторизованный paired-клиент с grant в Origin matrix может управлять тем же call state. Ownership на wire — информационный; transfer/conference на SDK **не** экспонированы.
282
+ ### `OmniCallClient`
158
283
 
159
- ---
284
+ Полный клиент состоит из lifecycle-методов и namespaces `calls`, `window`,
285
+ `operator`, `account`.
160
286
 
161
- ## Установка и окружение
287
+ #### `client.connect()`
162
288
 
163
- ```bash
164
- npm install @softomnitel/omnicall-kit
165
- # pin: npm install @softomnitel/omnicall-kit@0.1.0
166
- # RC: npm install @softomnitel/omnicall-kit@rc
289
+ ```ts
290
+ (): Promise<void>
167
291
  ```
168
292
 
169
- | Требование | Значение |
170
- | --- | --- |
171
- | Node (tooling) | `>=20.19.0` |
172
- | Module | ESM only |
173
- | Web Crypto | ECDSA P-256, non-extractable (PoP) |
174
- | IndexedDB | Durable PoP в браузере |
175
- | Desktop | OmniCall с включённым SDK gateway |
293
+ Открывает соединение и запускает pairing либо восстановление сессии. Promise
294
+ может быть отклонён ошибкой соединения. После успешного вызова всё ещё ждите
295
+ `ready` через `waitUntil()` или `onStateChange()`.
176
296
 
177
- Импорты для CRM — из одного пакета:
297
+ #### `client.disconnect()`
178
298
 
179
299
  ```ts
180
- import {
181
- createOmniCallClient,
182
- createIndexedDbPopKeyStore,
183
- isOmniCallClientError,
184
- type OmniCallClient,
185
- type OmniCallEventOf,
186
- type SnapshotMessage,
187
- type CapabilityId
188
- } from '@softomnitel/omnicall-kit';
300
+ (): void
189
301
  ```
190
302
 
191
- `@softomnitel/omnicall-protocol` нужен только для Zod schemas / fixtures не для day-to-day CRM. EN: [typescript.md](../../docs/guide/typescript.md), [installation.md](../../docs/guide/installation.md).
303
+ Закрывает только соединение SDK, отменяет таймеры и ожидающие запросы. Не
304
+ завершает звонок, не выполняет logout и не закрывает Desktop.
192
305
 
193
- ---
306
+ #### `client.getState()`
194
307
 
195
- ## Жизненный цикл клиента и состояния
308
+ ```ts
309
+ (): ConnectionState
310
+ ```
196
311
 
197
- ### Создание
312
+ Возвращает текущее состояние из таблицы выше. Не выполняет сетевой запрос.
198
313
 
199
- `createOmniCallClient(options)` / `createAuthClient(options)` — `OmniCallClientOptions` = `AuthClientOptions`.
314
+ #### `client.waitUntil(predicate, timeoutMs?)`
200
315
 
201
- | Опция | Назначение |
202
- | --- | --- |
203
- | `url` | Loopback WS endpoint |
204
- | `origin` | Exact Origin страницы CRM |
205
- | `application` | `{ name, version }` |
206
- | `sdkVersion` | Версия SDK-строки на wire |
207
- | `requestedProfile` | `presentation` \| `operator` \| `call_controller` |
208
- | `requestedCapabilities?` | Non-privileged only; sanitize + strip privileged |
209
- | `keyStore` | `PopKeyStore` (IndexedDB / memory) |
210
- | `transportFactory?` | Default: browser WS |
211
- | `scheduler?` / `jitter?` | Default: browser; inject в тестах |
212
- | `reconnect?` | `ReconnectPolicy` |
213
- | `heartbeat?` | `HeartbeatPolicy` |
214
- | `diagnostics?` | Redaction-safe sink |
215
- | `defaultRequestTimeoutMs?` | Таймаут обычных команд |
216
-
217
- `createAuthClient` — только lifecycle/auth, без `calls` / `operator` / `account` / `window`.
218
-
219
- ### Состояния `CONNECTION_STATES`
220
-
221
- | State | Смысл | Действие хоста |
222
- | --- | --- | --- |
223
- | `idle` | Сконструирован | Показать Connect |
224
- | `connecting` | Открытие транспорта | Spinner |
225
- | `handshaking` | Hello / protocol | Spinner |
226
- | `pairing_required` | Нужен Approve в desktop | Инструкция оператору |
227
- | `authenticating` | PoP challenge | Spinner |
228
- | `ready` | Сессия + snapshot path | Product UI |
229
- | `reconnecting` | Bounded retry | Banner; не replay мутаций |
230
- | `incompatible` | Protocol mismatch | Upgrade; стоп |
231
- | `revoked` | Сессия отозвана | Clear + re-pair |
232
- | `failed` | Невосстановимый сбой | Показать ошибку (`getConnectError`) |
233
- | `closed` | Закрыто | Connect заново при необходимости |
234
-
235
- Типичный happy path: `idle` → `connecting` → `handshaking` → (`pairing_required` →) `authenticating` → `ready`.
236
-
237
- ### Подписки и методы lifecycle
238
-
239
- | API | Назначение |
240
- | --- | --- |
241
- | `onStateChange(listener)` | Смена `ConnectionState`; возвращает unsubscribe |
242
- | `onPairingRequired(listener)` | Origin/profile/clientId для UX pairing |
243
- | `subscribe(type, listener)` | Публичные product events (`PUBLIC_EVENT_TYPES`) |
244
- | `connect()` | Старт сети |
245
- | `disconnect()` | Закрыть WS; **не** hangup/logout/activate/hide |
246
- | `waitUntil(predicate, timeoutMs?)` | Дождаться состояния |
247
- | `getState()` / `getSession()` / `getGrantedCapabilities()` | Интроспекция |
248
- | `getConnectError()` | Последняя ошибка connect |
249
- | `getSnapshot()` / `getCachedSnapshot()` / `getRevision()` | Snapshot / revision |
250
- | `preauthDropCount()` | Диагностика preauth drops |
316
+ ```ts
317
+ (predicate: (state: ConnectionState) => boolean, timeoutMs?: number)
318
+ => Promise<ConnectionState>
319
+ ```
251
320
 
252
- <details>
253
- <summary>Пример: waitUntil ready + обработка connect error</summary>
321
+ Ждёт состояние, подходящее условию. `timeoutMs` не обязателен. При превышении
322
+ времени Promise отклоняется.
254
323
 
255
324
  ```ts
256
- client.onStateChange((state) => {
257
- if (state === 'failed') {
258
- const err = client.getConnectError();
259
- void err?.code;
260
- }
261
- });
325
+ await client.waitUntil((state) => state === 'ready', 60_000);
326
+ ```
262
327
 
263
- try {
264
- await client.connect();
265
- await client.waitUntil((s) => s === 'ready', 60_000);
266
- } catch (error: unknown) {
267
- if (isOmniCallClientError(error)) {
268
- void error.code;
269
- }
270
- throw error;
271
- }
328
+ #### `client.onStateChange(listener)` и `client.onPairingRequired(listener)`
329
+
330
+ ```ts
331
+ (listener: (state: ConnectionState) => void): () => void
332
+ (listener: (info: PairingRequiredInfo) => void): () => void
272
333
  ```
273
334
 
274
- </details>
335
+ Подписывают на изменение состояния или ожидание pairing. Оба метода возвращают
336
+ функцию отписки. `PairingRequiredInfo` содержит `origin`, `requestedProfile` и
337
+ может содержать `clientId`.
275
338
 
276
- ---
339
+ #### `client.getSession()` и `client.getGrantedCapabilities()`
277
340
 
278
- ## Pairing, Origin и capabilities
341
+ ```ts
342
+ (): AuthSessionSnapshot | undefined
343
+ (): readonly CapabilityId[]
344
+ ```
279
345
 
280
- ### Profiles
346
+ `getSession()` возвращает текущую сессию после авторизации либо `undefined`.
347
+ `getGrantedCapabilities()` возвращает права, которые Desktop действительно
348
+ выдал. Проверяйте их до показа кнопок.
281
349
 
282
- | Profile | Default caps (non-privileged) |
283
- | --- | --- |
284
- | `presentation` | `session.read.redacted`, `window.show` |
285
- | `operator` | presentation + `operator.status.write`, `operator.campaign.read`, `ocp.acd_context.read`, `session.logout` |
286
- | `call_controller` | operator + `call.originate`, `call.control`, granular `call.answer\|reject\|hangup\|hold\|mute` |
350
+ ```ts
351
+ const canDial = client.getGrantedCapabilities().includes('call.originate');
352
+ ```
287
353
 
288
- Источник: protocol `DEFAULT_CAPABILITY_PROFILES`. `account.activate` и `window.hide` **никогда** не в defaults.
354
+ #### `client.getSnapshot()`, `getCachedSnapshot()` и `getRevision()`
289
355
 
290
- ### Origin
356
+ ```ts
357
+ (): Promise<SnapshotMessage>
358
+ (): SnapshotMessage | undefined
359
+ (): number | undefined
360
+ ```
291
361
 
292
- - Exact match строки Origin без wildcard / substring.
293
- - Первый контакт: TOFU Allow/Deny в OmniCall (ADR-0018).
294
- - Blacklist → `origin_blocked` (сокет не поднимается) — Unblock в Settings → OmniCall Kit.
295
- - Deny → `forbidden` + `origin_denied`, затем close.
362
+ `getSnapshot()` запрашивает свежий снимок. `getCachedSnapshot()` не обращается в
363
+ сеть. `getRevision()` возвращает номер кэшированного снимка. Используйте свежий
364
+ snapshot перед мутацией.
296
365
 
297
- ### Capabilities: request vs grant
366
+ #### `client.subscribe(type, listener)`
298
367
 
299
- | Правило | Деталь |
300
- | --- | --- |
301
- | Client request | Только non-privileged; SDK `sanitizeRequestedCapabilities` |
302
- | Strip always | `account.activate`, `window.hide` |
303
- | Desktop grant | Intersection: pairing session ∩ per-Origin matrix |
304
- | Host видит | Только `getGrantedCapabilities()` |
305
- | Privileged elevation | Только Settings → Origin matrix (оператор) |
368
+ ```ts
369
+ <T extends PublicEventType>(
370
+ type: T,
371
+ listener: (event: OmniCallEventOf<T>) => void
372
+ ): () => void
373
+ ```
306
374
 
307
- | Capability | Privileged? | Типичный метод |
308
- | --- | --- | --- |
309
- | `session.read.redacted` | Нет | snapshot / events |
310
- | `window.show` | Нет | `window.show` |
311
- | `window.hide` | **Да** | `window.hide` |
312
- | `call.originate` | Нет | `calls.originate` |
313
- | `call.control` | Нет | umbrella + DTMF |
314
- | `call.answer` / `reject` / `hangup` / `hold` / `mute` | Нет | соответствующие методы |
315
- | `operator.status.write` | Нет | `changeStatus` / `finishAppeal` |
316
- | `operator.campaign.read` | Нет | campaign events + snapshot |
317
- | `ocp.acd_context.read` | Нет | `call:acd-context` |
318
- | `session.logout` | Нет | `account.logout` |
319
- | `account.activate` | **Да** | `account.activateProfile` |
320
-
321
- Revoke / matrix shrink mid-session → `forbidden` / state `revoked`. Хост: очистить UI, предложить re-pair / открыть Settings.
322
-
323
- EN: [capabilities.md](../../docs/guide/capabilities.md), [pairing-quick-start.md](../../docs/guide/pairing-quick-start.md).
375
+ Подписывает на одно публичное событие и возвращает функцию отписки. Тип полезной
376
+ нагрузки выводится из значения `type`.
324
377
 
325
- ---
378
+ #### `client.getConnectError()` и `client.preauthDropCount()`
379
+
380
+ ```ts
381
+ (): OmniCallClientError | undefined
382
+ (): number
383
+ ```
326
384
 
327
- ## Snapshot и revision
385
+ Первый метод возвращает последнюю ошибку подключения. Второй возвращает
386
+ диагностический счётчик сообщений, отброшенных до авторизации. Не используйте его
387
+ как бизнес-метрику CRM.
328
388
 
329
- Snapshot — источник UI state после `ready` и после каждого успешного reconnect.
389
+ ### `client.calls`
330
390
 
331
- | API | Смысл |
332
- | --- | --- |
333
- | `getSnapshot()` | Свежий snapshot с desktop |
334
- | `getCachedSnapshot()` | Последний кэш или `undefined` |
335
- | `getRevision()` | Текущий cached revision или `undefined` |
336
- | Mutation `expectedRevision` | Обязателен; иначе риск `stale_state` |
337
-
338
- Правила:
339
-
340
- 1. Перед мутацией: `client.getRevision() ?? (await client.getSnapshot()).revision`.
341
- 2. После успеха: используйте `revision` из результата / обновите snapshot.
342
- 3. После `stale_state`: новый snapshot; **не** слепой retry со старым числом.
343
- 4. Events **не** патчат snapshot cache при gap SDK сам тянет snapshot.
344
- 5. Desktop coarse-advances revision на смене coarse operator status, reasonId, connected, reservation booking не на каждом talking↔hold внутри `unknown`.
345
-
346
- <details>
347
- <summary>Пример: безопасная мутация с revision</summary>
348
-
349
- ```ts
350
- async function withFreshRevision<T>(
351
- client: {
352
- getRevision: () => number | undefined;
353
- getSnapshot: () => Promise<{ revision: number }>;
354
- },
355
- run: (expectedRevision: number) => Promise<T>
356
- ): Promise<T> {
357
- const expectedRevision =
358
- client.getRevision() ?? (await client.getSnapshot()).revision;
359
- return run(expectedRevision);
360
- }
391
+ Все методы ниже возвращают `Promise<CallMutationResult>`, где есть `callId` и
392
+ новый `revision`. Все требуют актуальный `expectedRevision`.
393
+
394
+ | Метод и сигнатура | Назначение | Право |
395
+ | --- | --- | --- |
396
+ | `originate({ destination, expectedRevision })` | Начать исходящий звонок на строку `destination`. | `call.originate` |
397
+ | `answer({ callId, expectedRevision })` | Ответить на входящий звонок. | `call.answer` или `call.control` |
398
+ | `reject({ callId, expectedRevision })` | Отклонить входящий звонок. | `call.reject` или `call.control` |
399
+ | `hangup({ callId, expectedRevision })` | Завершить звонок. | `call.hangup` или `call.control` |
400
+ | `hold({ callId, expectedRevision })` | Поставить звонок на удержание. | `call.hold` или `call.control` |
401
+ | `resume({ callId, expectedRevision })` | Снять звонок с удержания. | `call.hold` или `call.control` |
402
+ | `mute({ callId, expectedRevision })` | Выключить микрофон. | `call.mute` или `call.control` |
403
+ | `unmute({ callId, expectedRevision })` | Включить микрофон. | `call.mute` или `call.control` |
404
+ | `sendDtmf({ callId, digits, expectedRevision })` | Отправить тональные цифры `digits`. | Только `call.control` |
405
+
406
+ Параметр `callId` — идентификатор звонка из snapshot или события. Возможные
407
+ ошибки: `forbidden`, `not_ready`, `not_found`, `stale_state`, `conflict` и
408
+ `operation_failed`.
409
+
410
+ Каждый метод использует один из двух контрактов:
411
+
412
+ ```ts
413
+ type CallActionInput = { callId: string; expectedRevision: number };
414
+ type OriginateInput = { destination: string; expectedRevision: number };
415
+ type DtmfInput = { callId: string; digits: string; expectedRevision: number };
416
+ type CallAction = (input: CallActionInput) => Promise<CallMutationResult>;
361
417
  ```
362
418
 
363
- </details>
419
+ #### `client.calls.originate(input)`
364
420
 
365
- ---
421
+ ```ts
422
+ (input: OriginateInput): Promise<CallMutationResult>
423
+ ```
366
424
 
367
- ## API (namespaces)
425
+ Набирает `destination`. Передавайте номер в формате, который поддерживает
426
+ ваша телефония. Проверьте `call.originate`; при `operation_failed` с
427
+ `failure_kind: 'sip_not_registered'` сначала восстановите регистрацию Desktop.
368
428
 
369
- Публичный клиент: `OmniCallClient`. Ниже — только символы из API report.
429
+ ```ts
430
+ const revision = client.getRevision() ?? (await client.getSnapshot()).revision;
431
+ const result = await client.calls.originate({
432
+ destination: '+74951234567',
433
+ expectedRevision: revision
434
+ });
435
+ console.log(result.callId);
436
+ ```
370
437
 
371
- ### `client.calls`
438
+ #### `client.calls.answer(input)`
372
439
 
373
- Все мутации требуют `expectedRevision`. Результат: `{ callId, revision }`.
440
+ ```ts
441
+ (input: CallActionInput): Promise<CallMutationResult>
442
+ ```
374
443
 
375
- | Метод | Capability | Типичные ошибки |
376
- | --- | --- | --- |
377
- | `originate({ destination, expectedRevision })` | `call.originate` | `forbidden`, `not_ready`, `stale_state`, `conflict`, `operation_failed` (`sip_not_registered`) |
378
- | `answer` / `reject` / `hangup` | `call.answer` / `reject` / `hangup` или `call.control` | `forbidden`, `not_found`, `stale_state`, `conflict` |
379
- | `hold` / `resume` | `call.hold` или `call.control` | то же |
380
- | `mute` / `unmute` | `call.mute` или `call.control` | то же |
381
- | `sendDtmf({ callId, digits, expectedRevision })` | **только** `call.control` | `forbidden`, `stale_state`, `not_found` |
444
+ Отвечает на звонок `callId`. Вызывайте только для входящего звонка из snapshot
445
+ или `call:incoming`. Если звонок уже завершён в другой вкладке, получите
446
+ `not_found`, `conflict` или `stale_state`.
382
447
 
383
- Shared desk: см. ADR-0021 — ownership не блокирует control. Transfer/conference на SDK нет.
448
+ ```ts
449
+ await client.calls.answer({ callId: 'call-123', expectedRevision: await getRevision() });
450
+ ```
384
451
 
385
- <details>
386
- <summary>Пример: originate с обработкой ошибок</summary>
452
+ #### `client.calls.reject(input)`
387
453
 
388
454
  ```ts
389
- import {
390
- isOmniCallClientError,
391
- isConflictError,
392
- isOperationFailedError,
393
- readOperationFailedDetails
394
- } from '@softomnitel/omnicall-kit';
455
+ (input: CallActionInput): Promise<CallMutationResult>
456
+ ```
395
457
 
396
- async function originateSafe(
397
- client: OmniCallClient,
398
- destination: string
399
- ): Promise<void> {
400
- const revision =
401
- client.getRevision() ?? (await client.getSnapshot()).revision;
402
- try {
403
- await client.calls.originate({ destination, expectedRevision: revision });
404
- } catch (error: unknown) {
405
- if (isOperationFailedError(error)) {
406
- const details = readOperationFailedDetails(error.details);
407
- if (details?.failure_kind === 'sip_not_registered') {
408
- return; // preflight deny — события call:failed не будет
409
- }
410
- }
411
- if (isConflictError(error)) return;
412
- if (!isOmniCallClientError(error)) throw error;
413
- if (error.code === 'stale_state') {
414
- await client.getSnapshot();
415
- }
416
- }
417
- }
458
+ Отклоняет входящий звонок. Аргументы и ошибки совпадают с `answer()`.
459
+
460
+ ```ts
461
+ await client.calls.reject({ callId: 'call-123', expectedRevision: await getRevision() });
418
462
  ```
419
463
 
420
- </details>
464
+ #### `client.calls.hangup(input)`
421
465
 
422
- ### `client.window`
466
+ ```ts
467
+ (input: CallActionInput): Promise<CallMutationResult>
468
+ ```
423
469
 
424
- | Метод | Capability / заметка |
425
- | --- | --- |
426
- | `show()` | `window.show` — focus softphone |
427
- | `hide({ expectedRevision })` | Privileged `window.hide`; busy telephony → `conflict`; recovery: tray Show / `show()` |
428
- | `getState()` | `{ visible, revision }` |
470
+ Завершает звонок. Не вызывайте его автоматически в обработчике `disconnect()`.
429
471
 
430
- `hide` не запрашивайте на pairing. Grant: Settings → Origin matrix.
472
+ ```ts
473
+ await client.calls.hangup({ callId: 'call-123', expectedRevision: await getRevision() });
474
+ ```
431
475
 
432
- ### `client.account`
476
+ #### `client.calls.hold(input)` и `client.calls.resume(input)`
433
477
 
434
- | Метод | Capability | Заметки |
435
- | --- | --- | --- |
436
- | `logout({ reasonId?, expectedRevision })` | `session.logout` | Single-shot; может `interaction_required` |
437
- | `activateProfile({ login, expectedRevision, mode? })` | **`account.activate` (server-granted)** | `mode?: 'sip_only' \| 'ocp'`; никогда passwords |
478
+ ```ts
479
+ (input: CallActionInput): Promise<CallMutationResult>
480
+ ```
438
481
 
439
- Константы таймаутов activate (re-export из protocol):
482
+ `hold()` ставит активный звонок на удержание, а `resume()` возвращает его в
483
+ разговор. Оба требуют `call.hold` или `call.control`.
440
484
 
441
- | Константа | Смысл |
442
- | --- | --- |
443
- | `SDK_ACTIVATE_CONSENT_TTL_MS` | Consent modal (~120 s) |
444
- | `SDK_ACTIVATE_SIP_ONLY_AUTH_BUDGET_MS` | SIP-only auth budget |
445
- | `SDK_ACTIVATE_OCP_AUTH_BUDGET_MS` | OCP auth budget |
446
- | `SDK_ACTIVATE_CLIENT_TIMEOUT_MS` | Client wait ceiling (~420 s) |
485
+ ```ts
486
+ await client.calls.hold({ callId: 'call-123', expectedRevision: await getRevision() });
487
+ await client.calls.resume({ callId: 'call-123', expectedRevision: await getRevision() });
488
+ ```
447
489
 
448
- EN: [saved-profile-activation.md](../../docs/guide/saved-profile-activation.md), [logout-workflow.md](../../docs/guide/logout-workflow.md).
490
+ #### `client.calls.mute(input)` и `client.calls.unmute(input)`
491
+
492
+ ```ts
493
+ (input: CallActionInput): Promise<CallMutationResult>
494
+ ```
449
495
 
450
- <details>
451
- <summary>Пример: activate только после feature-detect</summary>
496
+ `mute()` выключает, а `unmute()` включает микрофон оператора. Оба требуют
497
+ `call.mute` или `call.control`.
452
498
 
453
499
  ```ts
454
- if (!client.getGrantedCapabilities().includes('account.activate')) {
455
- // Попросите оператора включить account.activate для Origin
456
- return;
457
- }
500
+ await client.calls.mute({ callId: 'call-123', expectedRevision: await getRevision() });
501
+ await client.calls.unmute({ callId: 'call-123', expectedRevision: await getRevision() });
502
+ ```
458
503
 
459
- await client.account.activateProfile({
460
- login: 'agent42',
461
- expectedRevision,
462
- mode: 'sip_only'
463
- });
504
+ #### `client.calls.sendDtmf(input)`
505
+
506
+ ```ts
507
+ (input: DtmfInput): Promise<CallMutationResult>
464
508
  ```
465
509
 
466
- </details>
510
+ Отправляет тональные цифры `digits` во время подходящего звонка. Требует только
511
+ `call.control`; не используйте для передачи секретов.
467
512
 
468
- ### `client.operator`
513
+ ```ts
514
+ await client.calls.sendDtmf({
515
+ callId: 'call-123',
516
+ digits: '123#',
517
+ expectedRevision: await getRevision()
518
+ });
519
+ ```
520
+
521
+ ### `client.window`
469
522
 
470
- | Метод | Capability | Результат |
523
+ | Метод | Сигнатура и результат | Условие |
471
524
  | --- | --- | --- |
472
- | `getReasons()` | session read path | `{ reasons, revision }` |
473
- | `changeStatus({ target, reasonId?, expectedRevision })` | `operator.status.write` | `kind: 'applied' \| 'reserved'` |
474
- | `finishAppeal({ expectedRevision })` | `operator.status.write` | только при `post_call_processing` |
525
+ | `show()` | `(): Promise<{ visible: boolean; revision: number }>` | Право `window.show`. |
526
+ | `hide(input)` | `({ expectedRevision: number }) => Promise<{ visible: boolean; revision: number }>` | Привилегированное право `window.hide`; Desktop может вернуть `conflict`, если идёт разговор. |
527
+ | `getState()` | `(): Promise<{ visible: boolean; revision: number }>` | Только чтение состояния окна. |
475
528
 
476
- **Не** изобретайте отдельный reserve API. Всегда `changeStatus`; читайте `kind`. Booking виден в snapshot/event: `reservedTarget` / `reservedReasonId`.
529
+ ```ts
530
+ await client.window.show();
531
+ ```
477
532
 
478
- | Ситуация | `changeStatus` |
479
- | --- | --- |
480
- | Idle Ready/Break | `applied` |
481
- | Busy / PCP | `reserved` (chip не сразу Break/Ready) |
533
+ #### `client.window.show()`
482
534
 
483
- EN: [operator-status-reservation.md](../../docs/guide/operator-status-reservation.md).
535
+ ```ts
536
+ (): Promise<{ visible: boolean; revision: number }>
537
+ ```
484
538
 
485
- ### Logout (single-shot)
539
+ Показывает и фокусирует окно Desktop. Требует `window.show`. Результат содержит
540
+ фактическую видимость и новую версию состояния.
486
541
 
487
- Нет prepare/confirm handshake и нет `logoutToken`.
542
+ #### `client.window.hide(input)`
488
543
 
489
- 1. При необходимости: `getReasons()` → `kind === 'logout'`.
490
- 2. `account.logout({ expectedRevision })` или с `reasonId`.
491
- 3. При `interaction_required` — modal с reasons; повторный `logout` со свежим revision.
492
- 4. Cancel = просто не вызывать снова. `disconnect()` ≠ logout.
544
+ ```ts
545
+ ({ expectedRevision }: { expectedRevision: number })
546
+ => Promise<{ visible: boolean; revision: number }>
547
+ ```
493
548
 
494
- <details>
495
- <summary>Пример: logout с interaction_required</summary>
549
+ Скрывает окно. Требует отдельно выданное привилегированное `window.hide`.
550
+ Desktop отклоняет запрос с `conflict`, когда скрытие небезопасно, например во
551
+ время разговора.
496
552
 
497
553
  ```ts
498
- import {
499
- isOmniCallClientError,
500
- isInteractionRequiredError,
501
- readInteractionRequiredDetails
502
- } from '@softomnitel/omnicall-kit';
554
+ await client.window.hide({ expectedRevision: await getRevision() });
555
+ ```
503
556
 
504
- const { reasons } = await client.operator.getReasons();
505
- const logoutReasons = reasons.filter((r) => r.kind === 'logout');
557
+ #### `client.window.getState()`
506
558
 
507
- try {
508
- await client.account.logout({ expectedRevision });
509
- } catch (error: unknown) {
510
- if (!isInteractionRequiredError(error)) {
511
- if (isOmniCallClientError(error)) throw error;
512
- throw error;
513
- }
514
- const details = readInteractionRequiredDetails(error.details);
515
- const reasonId = details?.reasons[0]?.id ?? logoutReasons[0]?.id;
516
- if (reasonId === undefined) return;
517
- const next =
518
- client.getRevision() ?? (await client.getSnapshot()).revision;
519
- await client.account.logout({ reasonId, expectedRevision: next });
520
- }
559
+ ```ts
560
+ (): Promise<{ visible: boolean; revision: number }>
561
+ ```
562
+
563
+ Запрашивает видимость окна, ничего не меняя.
564
+
565
+ ```ts
566
+ const { visible } = await client.window.getState();
521
567
  ```
522
568
 
523
- </details>
569
+ ### `client.account`
524
570
 
525
- Полный перечень публичных символов (77): [api-reference.md](../../docs/guide/api-reference.md).
571
+ #### `client.account.logout(input)`
526
572
 
527
- ---
573
+ ```ts
574
+ ({ reasonId?, expectedRevision }: {
575
+ reasonId?: number;
576
+ expectedRevision: number;
577
+ }) => Promise<LogoutResult>
578
+ ```
579
+
580
+ Завершает операторскую сессию. Возвращает `{ loggedOut: true, revision }`.
581
+ Desktop может потребовать причину и вернуть `interaction_required`. В этом случае
582
+ получите причины через `client.operator.getReasons()`, покажите их пользователю и
583
+ повторите logout со свежим revision и выбранным `reasonId`.
584
+
585
+ #### `client.account.activateProfile(input)`
528
586
 
529
- ## События
587
+ ```ts
588
+ ({ login, expectedRevision, mode? }: {
589
+ login: string;
590
+ expectedRevision: number;
591
+ mode?: 'sip_only' | 'ocp';
592
+ }) => Promise<ActivateProfileResult>
593
+ ```
530
594
 
531
- Подписка только на имена из `PUBLIC_EVENT_TYPES`. Domain Event names desktop — запрещены.
595
+ Активирует ранее сохранённый в Desktop профиль без передачи пароля в CRM.
596
+ `mode` по умолчанию определяет Desktop. Возвращает `activated`, `mode`,
597
+ необязательные `profileLabel`, `alreadyAuthenticated` и `revision`. Требует
598
+ выданное сервером право `account.activate`.
599
+
600
+ ### `client.operator`
532
601
 
533
- | Событие | Зачем хосту | Snapshot-участок |
602
+ | Метод | Сигнатура и результат | Ошибки и ограничения |
534
603
  | --- | --- | --- |
535
- | `call:incoming` | Answer/reject UI; optional `queueLabel` | `sections.calls` |
536
- | `call:outgoing` / `call:ringing` / `call:answered` | Dial/ring/active | `sections.calls` |
537
- | `call:ended` / `call:failed` | Tear down / toast | `sections.calls` |
538
- | `call:held` / `call:resumed` | Hold | `sections.calls` |
539
- | `call:muted` / `call:unmuted` | Mute | `sections.calls` |
540
- | `call:acd-context` | OCP MainCallIDInfo (`ocp.acd_context.read`) | `calls[].acdContext` |
541
- | `registration:changed` | SIP badge | registration section |
542
- | `account:session-activated` / `account:session-ended` | Signed-in projection | account |
543
- | `operator:session-changed` | `connected` | `sections.operator` |
544
- | `operator:status-changed` | Coarse status + booking fields | `sections.operator` |
545
- | `operator:campaign-offered` / `cleared` | Campaign UI (`operator.campaign.read`) | `operator.campaign` |
546
- | `window:visibility-changed` | Softphone visibility | window |
547
- | `sdk:server-shutdown` | Desktop stopping — wait/reconnect | — |
604
+ | `getReasons()` | `(): Promise<{ reasons: OperatorReason[]; revision: number }>` | Получает причины ready, break и logout. |
605
+ | `changeStatus(input)` | `({ target: 'ready' \| 'break', reasonId?, expectedRevision }) => Promise<OperatorStatusChangeResult>` | Требует `operator.status.write`. Результат содержит `kind: 'applied' \| 'reserved'`. |
606
+ | `finishAppeal(input)` | `({ expectedRevision }) => Promise<OperatorFinishAppealResult>` | Доступен в состоянии post-call processing. |
548
607
 
549
- Redaction: телефоны маскированы; никаких OCP wire objects в логах. Sequence gap → SDK делает fresh `getSnapshot()`.
608
+ `reserved` означает: Desktop запомнил запрошенный статус и применит его после
609
+ текущего разговора. Не создавайте в CRM отдельную команду резервирования.
550
610
 
551
- Не через `subscribe` (by design): `sdk:permission-changed` → перечитайте `getGrantedCapabilities()`; `sdk:revoked` → state `revoked`.
611
+ #### `client.operator.getReasons()`
552
612
 
553
- Типизация: `OmniCallEventOf<'call:incoming'>`. EN: [events.md](../../docs/guide/events.md), [typescript.md](../../docs/guide/typescript.md).
613
+ ```ts
614
+ (): Promise<OperatorReasonsResult>
615
+ ```
554
616
 
555
- <details>
556
- <summary>Пример: subscribe + campaign recovery</summary>
617
+ Возвращает причины статусов и logout с полями `id`, `label`, `kind`, а также
618
+ `revision`. Используйте `id` выбранной оператором причины в следующей команде.
557
619
 
558
620
  ```ts
559
- const stop = client.subscribe('operator:status-changed', (event) => {
560
- void event.payload.status;
561
- void event.payload.reservedTarget;
562
- });
621
+ const { reasons } = await client.operator.getReasons();
622
+ console.log(reasons.filter((reason) => reason.kind === 'break'));
623
+ ```
563
624
 
564
- client.subscribe('call:acd-context', (event) => {
565
- void event.payload.acallid;
566
- void event.payload.queue;
567
- });
625
+ #### `client.operator.changeStatus(input)`
626
+
627
+ ```ts
628
+ ({ target, reasonId?, expectedRevision }: {
629
+ target: 'ready' | 'break';
630
+ reasonId?: number;
631
+ expectedRevision: number;
632
+ }) => Promise<OperatorStatusChangeResult>
633
+ ```
568
634
 
569
- // После reconnect snapshot, не replay events
570
- const snap = await client.getSnapshot();
571
- void snap.sections.operator?.campaign;
572
- void snap.sections.calls;
635
+ Меняет статус на `ready` или `break`. Возвращает `accepted: true`, реальный
636
+ `targetStatus`, `reasonId`, `revision` и `kind`. При активном разговоре `kind`
637
+ может быть `reserved`, а не `applied`.
573
638
 
574
- stop();
639
+ ```ts
640
+ await client.operator.changeStatus({
641
+ target: 'ready',
642
+ expectedRevision: await getRevision()
643
+ });
575
644
  ```
576
645
 
577
- </details>
646
+ #### `client.operator.finishAppeal(input)`
578
647
 
579
- `queueLabel` на `call:*` — additive desktop-safe title; повтор с тем же `callId` — enrichment, не второй lead.
648
+ ```ts
649
+ ({ expectedRevision }: { expectedRevision: number })
650
+ => Promise<OperatorFinishAppealResult>
651
+ ```
580
652
 
581
- ---
653
+ Заканчивает постобработку обращения. Вызывайте только когда публичный статус
654
+ оператора — `post_call_processing`; в остальных случаях Desktop вернёт ошибку
655
+ состояния или конфликта.
656
+
657
+ ```ts
658
+ await client.operator.finishAppeal({ expectedRevision: await getRevision() });
659
+ ```
582
660
 
583
661
  ## Ошибки
584
662
 
585
- Класс: `OmniCallClientError` (`code`, `retryable`, `currentRevision?`, `details?`).
663
+ ### `OmniCallClientError`
664
+
665
+ ```ts
666
+ new OmniCallClientError({
667
+ code: ProtocolErrorCode,
668
+ retryable: boolean,
669
+ currentRevision?: number,
670
+ details?: WireJsonObject
671
+ });
672
+ ```
673
+
674
+ Это класс ошибок SDK. Свойства `code`, `retryable`, `currentRevision` и `details`
675
+ помогают выбрать действие. Не выводите весь `details` в production-логи: там
676
+ могут быть чувствительные данные.
586
677
 
587
- | Guard / reader | Когда |
678
+ | Проверка и reader | Когда применять |
588
679
  | --- | --- |
589
- | `isOmniCallClientError` | Базовая проверка |
590
- | `isConflictError` + `readConflictErrorDetails` | `conflict` |
591
- | `isInteractionRequiredError` + `readInteractionRequiredDetails` | logout reason / UI step |
592
- | `isOperationFailedError` + `readOperationFailedDetails` | e.g. `sip_not_registered` |
593
- | `isOriginBlockedError` | Blacklisted Origin |
680
+ | `isOmniCallClientError(value)` | Проверить любую ошибку SDK. |
681
+ | `isConflictError(error)` + `readConflictErrorDetails(details)` | Обработать `conflict`. |
682
+ | `isInteractionRequiredError(error)` + `readInteractionRequiredDetails(details)` | Выбрать причину logout. |
683
+ | `isOperationFailedError(error)` + `readOperationFailedDetails(details)` | Узнать `failure_kind`, например `sip_not_registered`. |
684
+ | `isOriginBlockedError(value)` | Обработать заблокированный Origin. |
685
+
686
+ ```ts
687
+ import {
688
+ isOmniCallClientError,
689
+ isOperationFailedError,
690
+ readOperationFailedDetails
691
+ } from '@softomnitel/omnicall-kit';
692
+
693
+ try {
694
+ await client.calls.originate({
695
+ destination: '+74951234567',
696
+ expectedRevision: await getRevision()
697
+ });
698
+ } catch (error: unknown) {
699
+ if (isOperationFailedError(error)) {
700
+ console.warn(readOperationFailedDetails(error.details)?.failure_kind);
701
+ } else if (isOmniCallClientError(error)) {
702
+ console.warn(error.code, error.retryable);
703
+ } else {
704
+ throw error;
705
+ }
706
+ }
707
+ ```
708
+
709
+ ## Вспомогательные фабрики и низкоуровневые интерфейсы
594
710
 
595
- | Code | Смысл | Next step |
711
+ Это API для тестов, нестандартных runtime-сред и диагностики. В обычной браузерной
712
+ CRM ничего из этого передавать в `createOmniCallClient()` не нужно.
713
+
714
+ | Экспорт | Сигнатура | Для чего нужен |
596
715
  | --- | --- | --- |
597
- | `forbidden` | Нет cap / Origin policy / consent Deny | Settings; не tight-loop |
598
- | `not_ready` | Не `ready` / broker | Wait / connecting UI |
599
- | `timeout` | Нет ответа | Retry UI; не auto-replay non-idempotent |
600
- | `stale_state` | Revision mismatch | `getSnapshot()`; retry once |
601
- | `conflict` | Busy / race / consent pending / hide while busy | Показать конфликт; logout-first для activate |
602
- | `not_found` | Unknown call/login/reason | Refresh / correct login |
603
- | `invalid_payload` | Wire fail-closed | Bug / mismatch |
604
- | `interaction_required` | Human step | Modal / Account UI |
605
- | `revoked` | Session revoked | Clear; re-pair |
606
- | `incompatible_version` | Protocol mismatch | Upgrade |
607
- | `unauthenticated` | Нужна auth | Reconnect / pair |
608
- | `rate_limited` | Back off | Jitter |
609
- | `operation_failed` | Generic | Log code; check `failure_kind` |
610
- | `local_network_permission_*` | LNA | Объяснить Allow local network |
611
- | `discovery_unreachable` | Desktop down | Запустить OmniCall |
612
- | `origin_blocked` | Blacklist | Unblock в Settings |
613
- | `not_owner` | Reserved; не для shared-desk call control | Unexpected — refresh |
614
- | `invalid_message` / `unsupported_command` | Protocol mismatch | Fail closed |
615
-
616
- Логируйте: `code`, `retryable`, `requestId` / command type. Не логируйте: полный `details`, токены, destinations сверх политики, wire frames.
617
-
618
- EN: [errors.md](../../docs/guide/errors.md).
716
+ | `createAuthClient(options)` | `(AuthClientOptions) => AuthClient` | Клиент только pairing и lifecycle без `calls`, `window`, `operator`, `account`. |
717
+ | `createIndexedDbPopKeyStore({ installId })` | `({ installId: string }) => PopKeyStore` | Постоянное браузерное хранилище pairing. |
718
+ | `createMemoryPopKeyStore(initial?)` | `(StoredPopIdentity?) => PopKeyStore & { peek() }` | Временное хранилище для тестов. |
719
+ | `createBrowserWebSocketTransport(options?)` | `({ webSocket? }?) => TransportPort` | Стандартный транспорт браузера. |
720
+ | `createBrowserScheduler()` | `() => Scheduler` | Реальные таймеры браузера. |
721
+ | `createBrowserJitterSource()` | `() => JitterSource` | Случайная задержка reconnect. |
722
+ | `createFakeScheduler(startMs?)` | `(number?) => FakeScheduler` | Управляемые таймеры в тестах. |
723
+ | `createFixedJitterSource(value)` | `(number) => JitterSource` | Предсказуемый jitter в тестах. |
724
+ | `createRecordingDiagnosticsSink()` | `() => DiagnosticsSink & { events; clear() }` | Сбор безопасных диагностических событий. |
725
+
726
+ `PopKeyStore` имеет методы `load()`, `save(identity)` и `clear()`. Его
727
+ `StoredPopIdentity` содержит `clientId`, открытый ключ, непередаваемый
728
+ `CryptoKey`, профиль и выданные права. Не сериализуйте приватный `CryptoKey` и не
729
+ отправляйте его на сервер CRM.
730
+
731
+ `TransportPort` определяет `connect(url)`, `send(data)`, `close(code?, reason?)`
732
+ и подписки `onOpen`, `onMessage`, `onClose`, `onError`. Он работает только со
733
+ строковыми сообщениями. Не добавляйте в него собственный JSON-парсер или цикл
734
+ reconnect: этим управляет сессия SDK.
735
+
736
+ `Scheduler` определяет `now()` и `setTimeout(callback, delayMs)`. Возвращаемый
737
+ `TimerHandle` имеет `clear()`. `FakeScheduler` добавляет `advanceBy`,
738
+ `advanceByAsync`, `pendingTimerCount` и `clearAll`.
739
+
740
+ ### Политики и диагностика
619
741
 
620
- ---
742
+ ```ts
743
+ type ReconnectPolicy = {
744
+ maxAttempts: number;
745
+ initialDelayMs: number;
746
+ maxDelayMs: number;
747
+ jitterRatio: number;
748
+ };
749
+
750
+ type HeartbeatPolicy = {
751
+ enabled: boolean;
752
+ intervalMs: number;
753
+ timeoutMs: number;
754
+ };
755
+ ```
621
756
 
622
- ## Reconnect и несколько вкладок
757
+ `DiagnosticsSink.emit(event)` получает `DiagnosticEvent`: уровень
758
+ `debug | info | warn | error`, код, состояние и необязательные `requestId`,
759
+ `commandType`, длительность и ошибку. Логируйте коды и идентификаторы запросов,
760
+ но не логируйте номера телефонов, токены или полный payload.
623
761
 
624
- | Свойство | Поведение |
625
- | --- | --- |
626
- | Policy | Bounded (`maxAttempts`), jittered, cancellable |
627
- | После успеха | Новый auth + **fresh snapshot** |
628
- | In-flight mutations | Rejected typed; **не** auto-resent |
629
- | Host intent | Переиздать после нового `expectedRevision` |
762
+ ## Типы, константы и полный индекс экспорта
630
763
 
631
- `disconnect()` закрывает транспорт и чистит timers/pending **без** hangup / logout / activate / hide и без teardown desktop SIP/account.
764
+ Ниже перечислены все публичные экспорты пакета. Для структур сообщений протокола
765
+ `SnapshotMessage`, `SnapshotSections`, `SnapshotCallSummary`,
766
+ `ApplicationIdentity`, `CapabilityId`, `ProtocolErrorCode`,
767
+ `PublicOperatorStatus` и `WireJsonObject` используйте TypeScript-типы из
768
+ `@softomnitel/omnicall-kit` и `@softomnitel/omnicall-protocol`: они
769
+ re-exported из протокольного пакета.
632
770
 
633
- | Multi-tab сценарий | Ожидание | UX |
634
- | --- | --- | --- |
635
- | Две вкладки originate | `conflict` / `stale_state` | Одна «ведущая» вкладка |
636
- | A hold, B hangup | Обе могут успеть; later stale | Sync из snapshot/events |
637
- | Новый браузер после pairing | Тот же call state | `getSnapshot()` + subscribe |
638
- | Общий PoP `installId` | Та же client identity | Один активный controller |
771
+ | Группа | Экспорты |
772
+ | --- | --- |
773
+ | Клиент и auth | `OmniCallClient`, `OmniCallClientOptions`, `AuthClient`, `AuthClientOptions`, `AuthSessionSnapshot`, `PairingRequiredInfo`, `CONNECTION_STATES`, `ConnectionState` |
774
+ | Команды и результаты | `OmniCallCallsApi`, `OmniCallWindowApi`, `OmniCallAccountApi`, `OmniCallOperatorApi`, `CallMutationResult`, `LogoutResult`, `ActivateProfileMode`, `ActivateProfileResult`, `OperatorReason`, `OperatorReasonsResult`, `OperatorStatusChangeKind`, `OperatorStatusChangeResult`, `OperatorFinishAppealResult` |
775
+ | События | `PUBLIC_EVENT_TYPES`, `PublicEventType`, `OmniCallEvent`, `OmniCallEventOf` |
776
+ | Ошибки | `OmniCallClientError`, `ConflictErrorDetails`, `InteractionRequiredDetails`, `OperationFailedDetails`, пять type guard/readers из раздела ошибок |
777
+ | Хранилище | `PopKeyStore`, `StoredPopIdentity`, `createIndexedDbPopKeyStore`, `createMemoryPopKeyStore` |
778
+ | Транспорт | `BrowserWebSocketConstructor`, `BrowserWebSocketLike`, `CreateBrowserWebSocketTransportOptions`, `TransportFactory`, `TransportPort`, `TransportCloseInfo`, `TransportErrorInfo`, `createBrowserWebSocketTransport` |
779
+ | Время и reconnect | `Scheduler`, `TimerHandle`, `FakeScheduler`, `JitterSource`, `ReconnectPolicy`, `HeartbeatPolicy`, `createBrowserScheduler`, `createBrowserJitterSource`, `createFakeScheduler`, `createFixedJitterSource` |
780
+ | Диагностика | `DiagnosticsSink`, `DiagnosticEvent`, `DiagnosticLevel`, `DiagnosticResult`, `createRecordingDiagnosticsSink` |
781
+ | Протокольные re-export | `CapabilityId`, `ProtocolErrorCode`, `PublicOperatorStatus`, `SnapshotMessage`, `SnapshotSections`, `SnapshotCallSummary`, `WireJsonObject` |
782
+ | Константы activation | `SDK_ACTIVATE_CONSENT_TTL_MS`, `SDK_ACTIVATE_SIP_ONLY_AUTH_BUDGET_MS`, `SDK_ACTIVATE_OCP_AUTH_BUDGET_MS`, `SDK_ACTIVATE_CLIENT_TIMEOUT_MS` |
783
+
784
+ Четыре константы activation задают верхние границы ожидания согласия и
785
+ авторизации в миллисекундах. Используйте их, если вашему UI нужно показать
786
+ таймер; не меняйте их смысл локальными таймаутами.
639
787
 
640
- Никогда не делайте «retry all failed mutations on reconnect». EN: [reconnect-multi-tab.md](../../docs/guide/reconnect-multi-tab.md).
788
+ ## Рецепты
641
789
 
642
- <details>
643
- <summary>Пример: banner на reconnecting</summary>
790
+ ### Обновить UI после reconnect
644
791
 
645
792
  ```ts
646
793
  client.onStateChange((state) => {
647
- if (state === 'reconnecting') {
648
- // Banner only — не resend originate/hangup/logout/activate
649
- }
650
794
  if (state === 'ready') {
651
- void client.getSnapshot();
795
+ void client.getSnapshot().then((snapshot) => {
796
+ console.log('Обновите UI из снимка', snapshot.revision);
797
+ });
652
798
  }
653
799
  });
654
800
  ```
655
801
 
656
- </details>
802
+ ### Безопасно сменить статус оператора
657
803
 
658
- ---
804
+ ```ts
805
+ const expectedRevision =
806
+ client.getRevision() ?? (await client.getSnapshot()).revision;
807
+
808
+ const result = await client.operator.changeStatus({
809
+ target: 'break',
810
+ expectedRevision
811
+ });
812
+
813
+ if (result.kind === 'reserved') {
814
+ console.info('Перерыв начнётся после текущего разговора.');
815
+ }
816
+ ```
659
817
 
660
- ## Типовые сценарии CRM
818
+ ### Logout с выбором причины
661
819
 
662
- ### 1. Softphone control panel (presentation / operator)
820
+ ```ts
821
+ const { reasons } = await client.operator.getReasons();
822
+ const reason = reasons.find((item) => item.kind === 'logout');
663
823
 
664
- Pair с `presentation` или `operator` → snapshot registration/operator → `window.show` → status UI → logout при необходимости.
824
+ if (reason !== undefined) {
825
+ await client.account.logout({
826
+ reasonId: reason.id,
827
+ expectedRevision: await getRevision()
828
+ });
829
+ }
830
+ ```
665
831
 
666
- ### 2. Dial from CRM card (`call_controller`)
832
+ ## Ошибки и FAQ
667
833
 
668
- `ready` → проверка `call.originate` `originate({ destination, expectedRevision })` → UI из `call:*` events + snapshot calls.
834
+ ### `stale_state`: команда отклонена
669
835
 
670
- ### 3. Incoming queue agent
836
+ Другая вкладка или Desktop изменили состояние после вашего snapshot. Вызовите
837
+ `getSnapshot()`, обновите UI и попросите пользователя повторить действие. Не
838
+ повторяйте команду со старым `expectedRevision`.
671
839
 
672
- Subscribe `call:incoming` (+ optional `queueLabel` / `call:acd-context`) → `answer` / `reject` → hold/mute/hangup через `call.control`.
840
+ ### `forbidden`: кнопка не работает
673
841
 
674
- ### 4. Post-call processing
842
+ Desktop не выдал capability или оператор запретил Origin. Проверьте
843
+ `getGrantedCapabilities()`. Для `window.hide` и `account.activate` оператор
844
+ должен выдать право в Origin matrix Desktop.
675
845
 
676
- `changeStatus({ target: 'break', … })` во время разговора → `kind: 'reserved'` → UI booking из `reservedTarget` → при `post_call_processing` → `finishAppeal`.
846
+ ### `pairing_required` не заканчивается
677
847
 
678
- ### 5. Saved-account activate (privileged)
848
+ Оператор ещё не подтвердил CRM в OmniCall Desktop. Покажите адрес из
849
+ `PairingRequiredInfo.origin` и не скрывайте это состояние под бесконечным
850
+ спиннером.
679
851
 
680
- Оператор включает `account.activate` в Origin matrix → feature-detect → `activateProfile({ login, mode? })` → consent modal на desktop → без паролей в CRM.
852
+ ### `local_network_permission_required` или `_denied`
681
853
 
682
- ### 6. Hide softphone (privileged)
854
+ Браузер не разрешил HTTPS-странице обратиться к локальной программе. Объясните,
855
+ где выдать разрешение, затем разрешите оператору подключиться снова.
683
856
 
684
- Grant `window.hide` `hide({ expectedRevision })` только когда telephony idle; при `conflict` — tray / `show()`.
857
+ ### Нужен ли отдельный WebSocket-клиент?
685
858
 
686
- ---
859
+ Нет. Используйте `createOmniCallClient()` и транспорт SDK по умолчанию. Свой
860
+ WebSocket-клиент обходит проверку сообщений и может нарушить reconnect.
687
861
 
688
- ## Best practices
689
-
690
- 1. Делайте один `OmniCallClient` на вкладку/сессию CRM.
691
- 2. Держите UI state = snapshot + events; не дублируйте telephony FSM в CRM.
692
- 3. Вызывайте мутации только после `ready` и проверки capability.
693
- 4. Всегда обновляйте revision из последнего успешного результата или fresh snapshot.
694
- 5. Логируйте только `code` / `requestId` / command type — не payload.
695
- 6. Privileged flows — только через `getGrantedCapabilities()` feature-detect.
696
- 7. В тестах: `createMemoryPopKeyStore` + fake transport/scheduler; browser E2E отдельно.
697
- 8. На HTTPS CRM заранее объясняйте пользователю LNA / Local Network permission.
698
- 9. Не оборачивайте `TransportPort` собственной reconnect-логикой.
699
- 10. Pin версию `@softomnitel/omnicall-kit`; читайте [upgrade-deprecation.md](../../docs/guide/upgrade-deprecation.md).
700
- 11. При `stale_state` / `conflict` — один осознанный retry после snapshot, не tight-loop.
701
- 12. Key CRM cards по `callId`; `queueLabel` enrichment — update, не второй lead.
862
+ ### Можно ли хранить SIP-пароль или ключ OCP в CRM?
702
863
 
703
- ---
864
+ Нет. Эти секреты остаются в OmniCall Desktop. Для ранее сохранённого профиля
865
+ используйте `activateProfile({ login, ... })` после явной выдачи capability.
704
866
 
705
- ## Частые ошибки и anti-patterns
867
+ ## Миграция и совместимость
706
868
 
707
- | Нельзя | Почему | Как правильно |
708
- | --- | --- | --- |
709
- | `requestedCapabilities: ['account.activate']` | Strip; не elevates | Grant в Settings Origin matrix |
710
- | Request `window.hide` на pairing | Strip | Matrix grant → `window.hide` |
711
- | SIP password / OCP apiKey в SDK | Secrets только на desktop | `activateProfile({ login })` |
712
- | PoP в `localStorage` / `sessionStorage` | XSS | IndexedDB / memory key store |
713
- | Логировать phones / tokens / payloads | PII / leak | code + retryable + requestId |
714
- | Auto-replay мутаций после reconnect | Double-originate | Fresh revision + user re-issue |
715
- | Hangup/logout/activate на `disconnect()` | Desktop session должна жить | `disconnect` = transport only |
716
- | Custom reconnect в TransportPort | Session owns policy | Thin browser WS adapter |
717
- | Binary frames / свой JSON parser в port | Validation выше порта | Text frames via `onMessage` |
718
- | `account:list-profiles` | Нет в protocol v1 | Передайте известный `login` |
719
- | Auto-logout on disconnect | Destructive | Logout только после confirm |
720
- | Origin substring / wildcard | Exact only | Exact Origin string |
721
- | Отдельный CRM `reserveStatus` | OCP FSM leak | Всегда `changeStatus` → `kind` |
722
- | Mid-call `unknown` как полный OCP enum | Anti-corruption | Coarse + `reservedTarget` |
723
- | `fetch` fallback к desktop HTTP | Forbidden | Только SDK WS protocol |
724
- | Patch call graph только из events | Gaps / drift | Snapshot SSoT + events hints |
725
- | Слепой retry со старым revision | `stale_state` loop | `getSnapshot()` first |
726
- | Два «ведущих» dialer на вкладках | Races | Single-writer UX |
727
-
728
- EN: [security-anti-patterns.md](../../docs/guide/security-anti-patterns.md).
869
+ SDK не совместим с legacy `window.Softphone` и не предоставляет HTTP fallback.
870
+ Переносите интеграцию на `createOmniCallClient()`, snapshot, публичные события и
871
+ namespaces клиента.
729
872
 
730
- ---
873
+ При `incompatible_version` остановите функции телефонии и предложите обновить
874
+ пакет или Desktop. Добавление необязательных полей совместимо. Удаление,
875
+ переименование или изменение смысла API требует новой major-версии. Не опирайтесь
876
+ на недокументированные поля сетевых сообщений.
731
877
 
732
- ## Чеклист перед продом
733
-
734
- - [ ] Установлен `@softomnitel/omnicall-kit@0.1.0` (или осознанный pin/`rc`)
735
- - [ ] OmniCall Desktop запущен; SDK gateway / Integrations включены
736
- - [ ] Origin CRM exact match; TOFU Allow проверен; blacklist path понятен
737
- - [ ] Pairing UX: инструкция при `pairing_required`; revoke → clear + re-pair
738
- - [ ] PoP в IndexedDB (`installId` стабилен); нет Web Storage для ключей
739
- - [ ] `requestedCapabilities` без privileged ids
740
- - [ ] UI строится из snapshot + `PUBLIC_EVENT_TYPES`
741
- - [ ] Все мутации с fresh `expectedRevision`; обработка `stale_state`
742
- - [ ] Calls: originate/answer/reject/hangup/hold/mute/dtmf покрыты UX + errors
743
- - [ ] Operator: `changeStatus`/`finishAppeal`/`getReasons` без самодельного reserve
744
- - [ ] Logout single-shot + `interaction_required` modal
745
- - [ ] Privileged `window.hide` / `account.activate` — только после matrix grant + feature-detect
746
- - [ ] Reconnect: banner, fresh snapshot, no mutation replay
747
- - [ ] Multi-tab: single-writer или явный user retry
748
- - [ ] LNA permission объяснена на HTTPS
749
- - [ ] Логи без PII/secrets; ошибки по `code`
750
- - [ ] Нет SIP/OCP secrets в CRM bundle
751
- - [ ] Версии SDK/desktop совместимы ([compatibility-matrix.md](../../docs/guide/compatibility-matrix.md))
752
- - [ ] Upgrade notes прочитаны ([upgrade-deprecation.md](../../docs/guide/upgrade-deprecation.md))
878
+ Правила для интегратора:
753
879
 
754
- ---
880
+ 1. Используйте только публичные экспорты пакета `@softomnitel/omnicall-kit`.
881
+ 2. Игнорируйте неизвестные необязательные поля во входящих объектах.
882
+ 3. Не стройте логику на недокументированных wire-ключах.
883
+ 4. При `incompatible_version` остановите telephony UI и запросите обновление.
755
884
 
756
- ## Куда смотреть дальше
885
+ ## Лицензия
757
886
 
758
- | Документ | Зачем |
759
- | --- | --- |
760
- | [README гайда (EN index)](../../docs/guide/README.md) | Оглавление EN pages |
761
- | [API reference](../../docs/guide/api-reference.md) | Namespaces + inventory 77 |
762
- | [API report](../../etc/api/sdk.api.md) | Канон публичных символов |
763
- | [Events](../../docs/guide/events.md) | Каталог событий |
764
- | [Errors](../../docs/guide/errors.md) | Коды + next steps |
765
- | [Capabilities](../../docs/guide/capabilities.md) | Profiles / privileged |
766
- | [Pairing quick start](../../docs/guide/pairing-quick-start.md) | Connect ladder |
767
- | [Transport](../../docs/guide/transport.md) | WebSocket port |
768
- | [Installation](../../docs/guide/installation.md) | Engines / LNA |
769
- | [TypeScript](../../docs/guide/typescript.md) | Imports / `OmniCallEventOf` |
770
- | [Reconnect & multi-tab](../../docs/guide/reconnect-multi-tab.md) | Fresh snapshot policy |
771
- | [Logout](../../docs/guide/logout-workflow.md) | Single-shot logout |
772
- | [Saved-account activation](../../docs/guide/saved-profile-activation.md) | activateProfile |
773
- | [Operator status & reservation](../../docs/guide/operator-status-reservation.md) | applied \| reserved |
774
- | [Security anti-patterns](../../docs/guide/security-anti-patterns.md) | Forbidden table |
775
- | [Release & support](../../docs/guide/release-and-support.md) | RC / stable / support |
776
- | [Example crm-pairing-lite](../../examples/crm-pairing-lite/) | Fake-peer demo |
887
+ В `package.json` указано `UNLICENSED`. Это не open-source лицензия: не
888
+ предполагайте право на свободное распространение или изменение вне согласованного
889
+ контура. Уточните условия у владельца пакета.