@softomnitel/omnicall-kit 0.1.0-rc.0 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +765 -23
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,34 +1,776 @@
|
|
|
1
1
|
# @softomnitel/omnicall-kit
|
|
2
2
|
|
|
3
|
-
Browser SDK client for OmniCall Desktop.
|
|
3
|
+
Browser SDK client for OmniCall Desktop (local protocol).
|
|
4
|
+
Каноническая документация для интеграторов — ниже (русский гайд). Полная копия в репозитории: `docs/guide/RU-DEVELOPER-GUIDE.md`. EN pages: `docs/guide/`.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
```bash
|
|
7
|
+
npm install @softomnitel/omnicall-kit
|
|
8
|
+
```
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
Application, Electron, JsSIP, React, or Zustand.
|
|
10
|
+
---
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
# OmniCall Kit — руководство для разработчиков
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
- `createOmniCallClient` — product API on top of auth
|
|
15
|
-
- `createBrowserWebSocketTransport` — official `TransportPort` over browser `WebSocket`
|
|
16
|
-
- `createBrowserScheduler` / `createBrowserJitterSource` — production timer/jitter defaults
|
|
17
|
-
- `createIndexedDbPopKeyStore` / `createMemoryPopKeyStore` — PoP persistence
|
|
18
|
-
- Type helpers: `OmniCallEventOf`, snapshot/capability re-exports, typed error readers
|
|
19
|
-
(`readInteractionRequiredDetails`, …) — see `docs/guide/typescript.md`
|
|
14
|
+
Канонический русскоязычный гайд для интеграторов CRM: один файл — от установки до продакшена. Источники правды по символам API: [`etc/api/sdk.api.md`](../../etc/api/sdk.api.md) и EN pages в `docs/guide/`. Если формулировка здесь расходится с API report — побеждает report.
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
browsers (defaults above). Unit tests should still inject FakeTransport + fake scheduler.
|
|
16
|
+
## Статус пакета
|
|
23
17
|
|
|
24
|
-
|
|
18
|
+
Пакет `@softomnitel/omnicall-kit` — тонкий браузерный клиент к локальному gateway OmniCall Desktop. SIP, OCP и Call Engine остаются на desktop; в браузере нет второго softphone и нет SIP-стека.
|
|
25
19
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
-
|
|
29
|
-
|
|
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 |
|
|
30
29
|
|
|
31
|
-
|
|
32
|
-
excluded from the published `dist/` tarball.
|
|
30
|
+
---
|
|
33
31
|
|
|
34
|
-
|
|
32
|
+
## Кому и зачем
|
|
33
|
+
|
|
34
|
+
Аудитория: frontend/CRM-разработчики (JS/TS), которые встраивают SDK в HTTPS-страницу CRM при уже установленном OmniCall Desktop.
|
|
35
|
+
|
|
36
|
+
Цель интеграции:
|
|
37
|
+
|
|
38
|
+
1. Pairing + PoP-сессия к loopback WebSocket gateway.
|
|
39
|
+
2. UI CRM = redacted **snapshot** + публичные **events**.
|
|
40
|
+
3. Мутации звонков / оператора / окна / logout / activate — только через namespaced API с `expectedRevision` и server-issued capabilities.
|
|
41
|
+
|
|
42
|
+
Не цель: зеркалировать Domain Events desktop, хранить секреты в браузере, изобретать второй Call Engine.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Быстрый старт (5 минут)
|
|
47
|
+
|
|
48
|
+
Конструктор **без** сетевых side effects. Сеть начинается только на `connect()`.
|
|
49
|
+
|
|
50
|
+
<details>
|
|
51
|
+
<summary>Пример: минимальный connect → ready → snapshot</summary>
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
import {
|
|
55
|
+
createOmniCallClient,
|
|
56
|
+
createIndexedDbPopKeyStore,
|
|
57
|
+
createMemoryPopKeyStore,
|
|
58
|
+
isOmniCallClientError
|
|
59
|
+
} from '@softomnitel/omnicall-kit';
|
|
60
|
+
|
|
61
|
+
const keyStore =
|
|
62
|
+
typeof indexedDB !== 'undefined'
|
|
63
|
+
? createIndexedDbPopKeyStore({ installId: 'crm-install-1' })
|
|
64
|
+
: createMemoryPopKeyStore();
|
|
65
|
+
|
|
66
|
+
const client = createOmniCallClient({
|
|
67
|
+
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',
|
|
71
|
+
requestedProfile: 'call_controller',
|
|
72
|
+
requestedCapabilities: [
|
|
73
|
+
'session.read.redacted',
|
|
74
|
+
'window.show',
|
|
75
|
+
'call.originate',
|
|
76
|
+
'call.control',
|
|
77
|
+
'session.logout',
|
|
78
|
+
'operator.status.write'
|
|
79
|
+
],
|
|
80
|
+
keyStore
|
|
81
|
+
// transportFactory / scheduler / jitter опущены → browser defaults
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
client.onPairingRequired((info) => {
|
|
85
|
+
// Пользователь должен Allow Origin (TOFU) и Approve pairing в OmniCall
|
|
86
|
+
void info.origin;
|
|
87
|
+
void info.requestedProfile;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
client.onStateChange((state) => {
|
|
91
|
+
void state;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await client.connect();
|
|
95
|
+
await client.waitUntil((s) => s === 'ready');
|
|
96
|
+
|
|
97
|
+
const snapshot = await client.getSnapshot();
|
|
98
|
+
const revision = snapshot.revision;
|
|
99
|
+
|
|
100
|
+
client.subscribe('call:incoming', (event) => {
|
|
101
|
+
void event.type; // не логируйте полный payload в проде
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
void revision;
|
|
105
|
+
void isOmniCallClientError;
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
</details>
|
|
109
|
+
|
|
110
|
+
<details>
|
|
111
|
+
<summary>Почему так / на что смотреть</summary>
|
|
112
|
+
|
|
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/).
|
|
117
|
+
|
|
118
|
+
</details>
|
|
119
|
+
|
|
120
|
+
Краткая лестница состояний:
|
|
121
|
+
|
|
122
|
+
| Состояние | UI хоста |
|
|
123
|
+
| --- | --- |
|
|
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 при необходимости |
|
|
130
|
+
|
|
131
|
+
EN: [pairing-quick-start.md](../../docs/guide/pairing-quick-start.md).
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Модель системы (как это устроено)
|
|
136
|
+
|
|
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
|
|
144
|
+
```
|
|
145
|
+
|
|
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 |
|
|
152
|
+
|
|
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`.
|
|
154
|
+
|
|
155
|
+
Официальный транспорт: `createBrowserWebSocketTransport`. Не парсите JSON сами. Defaults: `createBrowserScheduler` / `createBrowserJitterSource`. В unit-тестах inject FakeTransport + fake scheduler/jitter.
|
|
156
|
+
|
|
157
|
+
Shared desk (ADR-0021): любой авторизованный paired-клиент с grant в Origin matrix может управлять тем же call state. Ownership на wire — информационный; transfer/conference на SDK **не** экспонированы.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Установка и окружение
|
|
162
|
+
|
|
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
|
|
167
|
+
```
|
|
168
|
+
|
|
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 |
|
|
176
|
+
|
|
177
|
+
Импорты для CRM — из одного пакета:
|
|
178
|
+
|
|
179
|
+
```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';
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
`@softomnitel/omnicall-protocol` нужен только для Zod schemas / fixtures — не для day-to-day CRM. EN: [typescript.md](../../docs/guide/typescript.md), [installation.md](../../docs/guide/installation.md).
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Жизненный цикл клиента и состояния
|
|
196
|
+
|
|
197
|
+
### Создание
|
|
198
|
+
|
|
199
|
+
`createOmniCallClient(options)` / `createAuthClient(options)` — `OmniCallClientOptions` = `AuthClientOptions`.
|
|
200
|
+
|
|
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 |
|
|
251
|
+
|
|
252
|
+
<details>
|
|
253
|
+
<summary>Пример: waitUntil ready + обработка connect error</summary>
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
client.onStateChange((state) => {
|
|
257
|
+
if (state === 'failed') {
|
|
258
|
+
const err = client.getConnectError();
|
|
259
|
+
void err?.code;
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
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
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
</details>
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Pairing, Origin и capabilities
|
|
279
|
+
|
|
280
|
+
### Profiles
|
|
281
|
+
|
|
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` |
|
|
287
|
+
|
|
288
|
+
Источник: protocol `DEFAULT_CAPABILITY_PROFILES`. `account.activate` и `window.hide` **никогда** не в defaults.
|
|
289
|
+
|
|
290
|
+
### Origin
|
|
291
|
+
|
|
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.
|
|
296
|
+
|
|
297
|
+
### Capabilities: request vs grant
|
|
298
|
+
|
|
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 (оператор) |
|
|
306
|
+
|
|
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).
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## Snapshot и revision
|
|
328
|
+
|
|
329
|
+
Snapshot — источник UI state после `ready` и после каждого успешного reconnect.
|
|
330
|
+
|
|
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
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
</details>
|
|
364
|
+
|
|
365
|
+
---
|
|
366
|
+
|
|
367
|
+
## API (namespaces)
|
|
368
|
+
|
|
369
|
+
Публичный клиент: `OmniCallClient`. Ниже — только символы из API report.
|
|
370
|
+
|
|
371
|
+
### `client.calls`
|
|
372
|
+
|
|
373
|
+
Все мутации требуют `expectedRevision`. Результат: `{ callId, revision }`.
|
|
374
|
+
|
|
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` |
|
|
382
|
+
|
|
383
|
+
Shared desk: см. ADR-0021 — ownership не блокирует control. Transfer/conference на SDK нет.
|
|
384
|
+
|
|
385
|
+
<details>
|
|
386
|
+
<summary>Пример: originate с обработкой ошибок</summary>
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
import {
|
|
390
|
+
isOmniCallClientError,
|
|
391
|
+
isConflictError,
|
|
392
|
+
isOperationFailedError,
|
|
393
|
+
readOperationFailedDetails
|
|
394
|
+
} from '@softomnitel/omnicall-kit';
|
|
395
|
+
|
|
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
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
</details>
|
|
421
|
+
|
|
422
|
+
### `client.window`
|
|
423
|
+
|
|
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 }` |
|
|
429
|
+
|
|
430
|
+
`hide` не запрашивайте на pairing. Grant: Settings → Origin matrix.
|
|
431
|
+
|
|
432
|
+
### `client.account`
|
|
433
|
+
|
|
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 |
|
|
438
|
+
|
|
439
|
+
Константы таймаутов activate (re-export из protocol):
|
|
440
|
+
|
|
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) |
|
|
447
|
+
|
|
448
|
+
EN: [saved-profile-activation.md](../../docs/guide/saved-profile-activation.md), [logout-workflow.md](../../docs/guide/logout-workflow.md).
|
|
449
|
+
|
|
450
|
+
<details>
|
|
451
|
+
<summary>Пример: activate только после feature-detect</summary>
|
|
452
|
+
|
|
453
|
+
```ts
|
|
454
|
+
if (!client.getGrantedCapabilities().includes('account.activate')) {
|
|
455
|
+
// Попросите оператора включить account.activate для Origin
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
await client.account.activateProfile({
|
|
460
|
+
login: 'agent42',
|
|
461
|
+
expectedRevision,
|
|
462
|
+
mode: 'sip_only'
|
|
463
|
+
});
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
</details>
|
|
467
|
+
|
|
468
|
+
### `client.operator`
|
|
469
|
+
|
|
470
|
+
| Метод | Capability | Результат |
|
|
471
|
+
| --- | --- | --- |
|
|
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` |
|
|
475
|
+
|
|
476
|
+
**Не** изобретайте отдельный reserve API. Всегда `changeStatus`; читайте `kind`. Booking виден в snapshot/event: `reservedTarget` / `reservedReasonId`.
|
|
477
|
+
|
|
478
|
+
| Ситуация | `changeStatus` |
|
|
479
|
+
| --- | --- |
|
|
480
|
+
| Idle Ready/Break | `applied` |
|
|
481
|
+
| Busy / PCP | `reserved` (chip не сразу Break/Ready) |
|
|
482
|
+
|
|
483
|
+
EN: [operator-status-reservation.md](../../docs/guide/operator-status-reservation.md).
|
|
484
|
+
|
|
485
|
+
### Logout (single-shot)
|
|
486
|
+
|
|
487
|
+
Нет prepare/confirm handshake и нет `logoutToken`.
|
|
488
|
+
|
|
489
|
+
1. При необходимости: `getReasons()` → `kind === 'logout'`.
|
|
490
|
+
2. `account.logout({ expectedRevision })` или с `reasonId`.
|
|
491
|
+
3. При `interaction_required` — modal с reasons; повторный `logout` со свежим revision.
|
|
492
|
+
4. Cancel = просто не вызывать снова. `disconnect()` ≠ logout.
|
|
493
|
+
|
|
494
|
+
<details>
|
|
495
|
+
<summary>Пример: logout с interaction_required</summary>
|
|
496
|
+
|
|
497
|
+
```ts
|
|
498
|
+
import {
|
|
499
|
+
isOmniCallClientError,
|
|
500
|
+
isInteractionRequiredError,
|
|
501
|
+
readInteractionRequiredDetails
|
|
502
|
+
} from '@softomnitel/omnicall-kit';
|
|
503
|
+
|
|
504
|
+
const { reasons } = await client.operator.getReasons();
|
|
505
|
+
const logoutReasons = reasons.filter((r) => r.kind === 'logout');
|
|
506
|
+
|
|
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
|
+
}
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
</details>
|
|
524
|
+
|
|
525
|
+
Полный перечень публичных символов (77): [api-reference.md](../../docs/guide/api-reference.md).
|
|
526
|
+
|
|
527
|
+
---
|
|
528
|
+
|
|
529
|
+
## События
|
|
530
|
+
|
|
531
|
+
Подписка только на имена из `PUBLIC_EVENT_TYPES`. Domain Event names desktop — запрещены.
|
|
532
|
+
|
|
533
|
+
| Событие | Зачем хосту | Snapshot-участок |
|
|
534
|
+
| --- | --- | --- |
|
|
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 | — |
|
|
548
|
+
|
|
549
|
+
Redaction: телефоны маскированы; никаких OCP wire objects в логах. Sequence gap → SDK делает fresh `getSnapshot()`.
|
|
550
|
+
|
|
551
|
+
Не через `subscribe` (by design): `sdk:permission-changed` → перечитайте `getGrantedCapabilities()`; `sdk:revoked` → state `revoked`.
|
|
552
|
+
|
|
553
|
+
Типизация: `OmniCallEventOf<'call:incoming'>`. EN: [events.md](../../docs/guide/events.md), [typescript.md](../../docs/guide/typescript.md).
|
|
554
|
+
|
|
555
|
+
<details>
|
|
556
|
+
<summary>Пример: subscribe + campaign recovery</summary>
|
|
557
|
+
|
|
558
|
+
```ts
|
|
559
|
+
const stop = client.subscribe('operator:status-changed', (event) => {
|
|
560
|
+
void event.payload.status;
|
|
561
|
+
void event.payload.reservedTarget;
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
client.subscribe('call:acd-context', (event) => {
|
|
565
|
+
void event.payload.acallid;
|
|
566
|
+
void event.payload.queue;
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
// После reconnect — snapshot, не replay events
|
|
570
|
+
const snap = await client.getSnapshot();
|
|
571
|
+
void snap.sections.operator?.campaign;
|
|
572
|
+
void snap.sections.calls;
|
|
573
|
+
|
|
574
|
+
stop();
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
</details>
|
|
578
|
+
|
|
579
|
+
`queueLabel` на `call:*` — additive desktop-safe title; повтор с тем же `callId` — enrichment, не второй lead.
|
|
580
|
+
|
|
581
|
+
---
|
|
582
|
+
|
|
583
|
+
## Ошибки
|
|
584
|
+
|
|
585
|
+
Класс: `OmniCallClientError` (`code`, `retryable`, `currentRevision?`, `details?`).
|
|
586
|
+
|
|
587
|
+
| Guard / reader | Когда |
|
|
588
|
+
| --- | --- |
|
|
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 |
|
|
594
|
+
|
|
595
|
+
| Code | Смысл | Next step |
|
|
596
|
+
| --- | --- | --- |
|
|
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).
|
|
619
|
+
|
|
620
|
+
---
|
|
621
|
+
|
|
622
|
+
## Reconnect и несколько вкладок
|
|
623
|
+
|
|
624
|
+
| Свойство | Поведение |
|
|
625
|
+
| --- | --- |
|
|
626
|
+
| Policy | Bounded (`maxAttempts`), jittered, cancellable |
|
|
627
|
+
| После успеха | Новый auth + **fresh snapshot** |
|
|
628
|
+
| In-flight mutations | Rejected typed; **не** auto-resent |
|
|
629
|
+
| Host intent | Переиздать после нового `expectedRevision` |
|
|
630
|
+
|
|
631
|
+
`disconnect()` закрывает транспорт и чистит timers/pending — **без** hangup / logout / activate / hide и без teardown desktop SIP/account.
|
|
632
|
+
|
|
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 |
|
|
639
|
+
|
|
640
|
+
Никогда не делайте «retry all failed mutations on reconnect». EN: [reconnect-multi-tab.md](../../docs/guide/reconnect-multi-tab.md).
|
|
641
|
+
|
|
642
|
+
<details>
|
|
643
|
+
<summary>Пример: banner на reconnecting</summary>
|
|
644
|
+
|
|
645
|
+
```ts
|
|
646
|
+
client.onStateChange((state) => {
|
|
647
|
+
if (state === 'reconnecting') {
|
|
648
|
+
// Banner only — не resend originate/hangup/logout/activate
|
|
649
|
+
}
|
|
650
|
+
if (state === 'ready') {
|
|
651
|
+
void client.getSnapshot();
|
|
652
|
+
}
|
|
653
|
+
});
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
</details>
|
|
657
|
+
|
|
658
|
+
---
|
|
659
|
+
|
|
660
|
+
## Типовые сценарии CRM
|
|
661
|
+
|
|
662
|
+
### 1. Softphone control panel (presentation / operator)
|
|
663
|
+
|
|
664
|
+
Pair с `presentation` или `operator` → snapshot registration/operator → `window.show` → status UI → logout при необходимости.
|
|
665
|
+
|
|
666
|
+
### 2. Dial from CRM card (`call_controller`)
|
|
667
|
+
|
|
668
|
+
`ready` → проверка `call.originate` → `originate({ destination, expectedRevision })` → UI из `call:*` events + snapshot calls.
|
|
669
|
+
|
|
670
|
+
### 3. Incoming queue agent
|
|
671
|
+
|
|
672
|
+
Subscribe `call:incoming` (+ optional `queueLabel` / `call:acd-context`) → `answer` / `reject` → hold/mute/hangup через `call.control`.
|
|
673
|
+
|
|
674
|
+
### 4. Post-call processing
|
|
675
|
+
|
|
676
|
+
`changeStatus({ target: 'break', … })` во время разговора → `kind: 'reserved'` → UI booking из `reservedTarget` → при `post_call_processing` → `finishAppeal`.
|
|
677
|
+
|
|
678
|
+
### 5. Saved-account activate (privileged)
|
|
679
|
+
|
|
680
|
+
Оператор включает `account.activate` в Origin matrix → feature-detect → `activateProfile({ login, mode? })` → consent modal на desktop → без паролей в CRM.
|
|
681
|
+
|
|
682
|
+
### 6. Hide softphone (privileged)
|
|
683
|
+
|
|
684
|
+
Grant `window.hide` → `hide({ expectedRevision })` только когда telephony idle; при `conflict` — tray / `show()`.
|
|
685
|
+
|
|
686
|
+
---
|
|
687
|
+
|
|
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.
|
|
702
|
+
|
|
703
|
+
---
|
|
704
|
+
|
|
705
|
+
## Частые ошибки и anti-patterns
|
|
706
|
+
|
|
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).
|
|
729
|
+
|
|
730
|
+
---
|
|
731
|
+
|
|
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))
|
|
753
|
+
|
|
754
|
+
---
|
|
755
|
+
|
|
756
|
+
## Куда смотреть дальше
|
|
757
|
+
|
|
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 |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@softomnitel/omnicall-kit",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Browser client for OmniCall Desktop local protocol (OmniCallClient read path + call control)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"clean": "node -e \"import('node:fs').then((fs)=>fs.promises.rm('dist',{recursive:true,force:true}))\""
|
|
29
29
|
},
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@softomnitel/omnicall-protocol": "0.1.0
|
|
31
|
+
"@softomnitel/omnicall-protocol": "0.1.0"
|
|
32
32
|
},
|
|
33
33
|
"publishConfig": {
|
|
34
34
|
"access": "public",
|