bx-rest 1.3.0 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/README.en.md +207 -3
- package/README.md +207 -3
- package/esm2022/functions/errors.mjs +19 -2
- package/esm2022/public-api.mjs +6 -2
- package/esm2022/request-navvy/crm/item.mjs +1 -1
- package/esm2022/request-navvy/crm/type.mjs +1 -1
- package/esm2022/services/frame/frame.mjs +347 -0
- package/esm2022/services/frame/transport.mjs +224 -0
- package/esm2022/services/frame/types.mjs +2 -0
- package/esm2022/services/http/HttpBX.mjs +14 -6
- package/esm2022/services/http/authRefresh.mjs +89 -0
- package/esm2022/services/navvy/extends/navvy-pag-base.mjs +28 -5
- package/esm2022/services/navvy/navvy-alternative-pagination-navigation.mjs +2 -2
- package/esm2022/services/navvy/navvy-pag-nav-result-key-object.mjs +6 -7
- package/esm2022/services/navvy/navvy-pag-nav-result-key.mjs +6 -3
- package/esm2022/services/navvy/navvy-pag-nav-tasks.mjs +5 -4
- package/esm2022/services/navvy/navvy-pag-nav-with-useless-key.mjs +2 -2
- package/esm2022/services/navvy/navvy-pag.mjs +2 -2
- package/esm2022/typification/rest/crm/universal.mjs +1 -1
- package/esm2022/typification/settings.mjs +1 -1
- package/fesm2022/bx-rest.mjs +801 -97
- package/fesm2022/bx-rest.mjs.map +1 -1
- package/functions/errors.d.ts +18 -0
- package/package.json +4 -1
- package/public-api.d.ts +5 -1
- package/request-navvy/crm/item.d.ts +6 -6
- package/request-navvy/crm/type.d.ts +6 -6
- package/services/frame/frame.d.ts +191 -0
- package/services/frame/transport.d.ts +75 -0
- package/services/frame/types.d.ts +123 -0
- package/services/http/authRefresh.d.ts +19 -0
- package/services/navvy/extends/navvy-pag-base.d.ts +6 -0
- package/services/navvy/navvy-pag-nav-tasks.d.ts +3 -0
- package/typification/rest/crm/universal.d.ts +116 -0
- package/typification/settings.d.ts +17 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,89 @@
|
|
|
7
7
|
История до версии 1.0.0 здесь не восстанавливалась — смотрите `git log` для
|
|
8
8
|
более ранних изменений.
|
|
9
9
|
|
|
10
|
+
## [1.5.0] - 2026-11-02
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Типы для универсальных методов CRM, раньше здесь было `Record<string, unknown>`:
|
|
15
|
+
- теперь `iBXRestCrmItem` — элемент в `crm.item.list`, с ответом `iBXRestCrmItemResult`
|
|
16
|
+
и в `crm.item.add`, `crm.item.get`, `crm.item.update`. Описаны поля, общие
|
|
17
|
+
для типов объектов; `parentId<entityTypeId>` и `ufCrm...` доступны по индексу.
|
|
18
|
+
В `crm.item.list` при заданном `select` поле `id` приходит, только если
|
|
19
|
+
указано в нём, поэтому там оно необязательное;
|
|
20
|
+
- `iBXRestCrmItemField`, `iBXRestCrmItemFieldType`,
|
|
21
|
+
`iBXRestCrmItemFieldSettings` и `iBXRestCrmItemFieldsResult` — ответ
|
|
22
|
+
`crm.item.fields`;
|
|
23
|
+
- `iBXRestCrmType` — смарт-процесс в `crm.type.list`, а через
|
|
24
|
+
`iBXRestCrmTypeResult` и в `crm.type.add`, `crm.type.get`,
|
|
25
|
+
`crm.type.getByEntityTypeId`, `crm.type.update`.
|
|
26
|
+
- Подпуть `bx-rest/typification/crm`: типы CRM теперь можно импортировать, как и
|
|
27
|
+
остальные разделы `typification`.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- Списки на `Navvy.pagNavResultKey` без своего `map` (`crm.item.list`,
|
|
32
|
+
`crm.type.list`, `crm.category.list`, `socialnetwork.api.workgroup.list`):
|
|
33
|
+
- `.resAll()` падал с `TypeError: Spread syntax requires ...iterable`, если
|
|
34
|
+
записей было 50 или меньше;
|
|
35
|
+
- `.res()` и `.mapForVanilla()` отдавали весь объект `{ items: [...] }`, хотя
|
|
36
|
+
по типу возвращали массив. Теперь они отдают массив. Код, который читал
|
|
37
|
+
`result.items` в обход типов, нужно поправить.
|
|
38
|
+
- То же при прямом вызове `Navvy` без своего `map`. Методов SDK это не касается,
|
|
39
|
+
они передают свой `map`:
|
|
40
|
+
- `Navvy.pagNavTasks`: `.resAll()` падал с тем же `TypeError`, а `.res()` и
|
|
41
|
+
`.mapForVanilla()` отдавали весь объект `{ tasks: [...] }`;
|
|
42
|
+
- `Navvy.pagNavResultKeyObject`: `.res()` и `.mapForVanilla()` отдавали весь
|
|
43
|
+
объект `{ ключ: { id: значение } }`. Теперь они отдают массив значений.
|
|
44
|
+
- `crm.documentgenerator.template.list` и другие списки на
|
|
45
|
+
`Navvy.pagNavResultKeyObject`: `.resAll()` падал с `EmptyError`, если записей
|
|
46
|
+
было больше 50.
|
|
47
|
+
- `.resAll()` при ошибке Bitrix в теле ответа 2xx теперь завершается ошибкой
|
|
48
|
+
`BXRestApiError` с кодом и описанием. Раньше код терялся: `pagNav` завершался
|
|
49
|
+
без значения, `pagNavWithUselessKey` (`lists.field.get`) отдавал `undefined`,
|
|
50
|
+
а остальные падали с `EmptyError`. Ошибки со статусом не 2xx `.resAll()`
|
|
51
|
+
по-прежнему отдаёт исходной ошибкой axios.
|
|
52
|
+
|
|
53
|
+
## [1.4.0] - 2026-10-29
|
|
54
|
+
|
|
55
|
+
### Added
|
|
56
|
+
|
|
57
|
+
- Автоматическое продление авторизации — `BXRestSettings.auth.refresh`.
|
|
58
|
+
Если запрос упал с ошибкой авторизации, библиотека вызывает эту функцию и
|
|
59
|
+
повторяет запрос один раз, заново читая токен, адрес и заголовки из
|
|
60
|
+
настроек. Работает во всех запросах: `.res()`, `.resAll()` (включая
|
|
61
|
+
batch), `.resVanilla()`, `.mapForVanilla()`, `BXRestRequest.post()`/`.get()`,
|
|
62
|
+
`BXRest.batch()`. Одновременно упавшие запросы ждут одно общее продление.
|
|
63
|
+
Если продление упало, наружу уходит исходная ошибка запроса, а причина —
|
|
64
|
+
в её поле `authRefreshError` (`getAuthRefreshError(err)`). Без
|
|
65
|
+
`auth.refresh` запросы не повторяются, как и раньше.
|
|
66
|
+
- `isBXRestAuthError()` — условие продления по умолчанию: ошибка Bitrix
|
|
67
|
+
`expired_token` или `invalid_token`, либо ответ 401 без тела ошибки Bitrix
|
|
68
|
+
(например, пустой 401 от бэкенд-прокси). Другие ошибки Bitrix со статусом
|
|
69
|
+
401 (`insufficient_scope`, `ACCESS_DENIED`, ...) продлением не лечатся и не
|
|
70
|
+
повторяются. Условие переопределяется через `auth.isAuthError`.
|
|
71
|
+
- `BXRestFrame` — команды порталу из iframe приложения без подключения
|
|
72
|
+
BX24.js: те же команды и имена, что у BX24.js, но с промисами вместо
|
|
73
|
+
колбэков. Данные запуска (`init()`), токен приложения (`getAuth()`,
|
|
74
|
+
`refreshAuth()`), установка (`installFinish()`, `firstRunFinish()`), размер
|
|
75
|
+
и заголовок фрейма, прокрутка и перезагрузка страницы, диалоги выбора
|
|
76
|
+
сотрудников, прав доступа и объектов CRM (с отменой через `signal`),
|
|
77
|
+
слайдеры (`openApplication()`, `openPath()`, `closeApplication()`),
|
|
78
|
+
мессенджер (`im.*`), встраивания (`placement.info/getInterface/call/bindEvent`)
|
|
79
|
+
и настройки (`userOption`, `appOption`). Ошибки — `BXRestFrameError` с
|
|
80
|
+
полем `code`. Использует внутренний недокументированный протокол
|
|
81
|
+
postMessage, который Bitrix24 может изменить; сверен с BX24.js от
|
|
82
|
+
2022-03-17 и с ответной стороной портала — подробности в README, раздел
|
|
83
|
+
«Работа в iframe без BX24.js».
|
|
84
|
+
- У `BXRestApiError` появилось поле `authRefreshError`.
|
|
85
|
+
|
|
86
|
+
### Changed
|
|
87
|
+
|
|
88
|
+
- Обычный `Error`, которым `.res()` завершается на ответ не от Bitrix
|
|
89
|
+
(например, HTML-страницу или пустой 401 от прокси), теперь хранит исходную
|
|
90
|
+
ошибку axios в `cause` — с HTTP-статусом в `cause.response.status`. Тип и
|
|
91
|
+
текст ошибки прежние.
|
|
92
|
+
|
|
10
93
|
## [1.3.0] - 2026-10-28
|
|
11
94
|
|
|
12
95
|
### Added
|
package/README.en.md
CHANGED
|
@@ -20,6 +20,11 @@ Also known as: Bitrix24 REST client, BX24 REST SDK, Bitrix24 API client, Bitrix
|
|
|
20
20
|
- [`withCredentials`](#withcredentials)
|
|
21
21
|
- [HTTP headers](#http-headers)
|
|
22
22
|
- [Error handling](#error-handling)
|
|
23
|
+
- [Authorization refresh](#authorization-refresh)
|
|
24
|
+
- [A token from the portal without BX24.js](#a-token-from-the-portal-without-bx24js)
|
|
25
|
+
- [Recipe: straight to the portal](#recipe-straight-to-the-portal)
|
|
26
|
+
- [Recipe: through a backend proxy](#recipe-through-a-backend-proxy)
|
|
27
|
+
- [Working in an iframe without BX24.js](#working-in-an-iframe-without-bx24js)
|
|
23
28
|
- [Usage in Angular](#usage-in-angular)
|
|
24
29
|
- [SessionKeyServices](#sessionkeyservices)
|
|
25
30
|
- [Usage in Vue](#usage-in-vue)
|
|
@@ -191,7 +196,7 @@ For cross-origin requests, the server — the portal or your proxy — must allo
|
|
|
191
196
|
| --- | --- |
|
|
192
197
|
| Bitrix API error with a non-2xx HTTP status — this is how Bitrix sends most errors: `expired_token` (401), `ERROR_METHOD_NOT_FOUND` (404), `QUERY_LIMIT_EXCEEDED` (503), `Not found` from `crm.*.get` (400) | The returned `Observable` errors with a `BXRestApiError`, with or without `throwOnApiError`. |
|
|
193
198
|
| Bitrix API error in the body of a 2xx response | By default, `.res()` resolves normally with `undefined` — **it does not throw**. Pass `{ throwOnApiError: true }` to make it throw a `BXRestApiError` instead. |
|
|
194
|
-
| Transport / configuration errors — network failure, missing auth, missing base URL, a response that is not from Bitrix (e.g. a proxy's HTML error page) | The returned `Observable` errors with a plain `Error`. |
|
|
199
|
+
| Transport / configuration errors — network failure, missing auth, missing base URL, a response that is not from Bitrix (e.g. a proxy's HTML error page) | The returned `Observable` errors with a plain `Error`. The original error is in its `cause`: for a response that is not from Bitrix it is the axios error, with the HTTP status in `cause.response.status`. |
|
|
195
200
|
|
|
196
201
|
`BXRestApiError` fields:
|
|
197
202
|
|
|
@@ -222,7 +227,7 @@ bxRest.crm.deal.list({
|
|
|
222
227
|
if (err instanceof BXRestApiError) {
|
|
223
228
|
console.error('Bitrix API error:', err.status, err.error, err.error_description)
|
|
224
229
|
if (err.error === 'expired_token') {
|
|
225
|
-
// refresh
|
|
230
|
+
// auth.refresh is not set or did not help — see "Authorization refresh"
|
|
226
231
|
}
|
|
227
232
|
} else {
|
|
228
233
|
console.error('Request failed:', err.message)
|
|
@@ -233,7 +238,7 @@ bxRest.crm.deal.list({
|
|
|
233
238
|
|
|
234
239
|
Without `throwOnApiError`, plain `.res()` can't tell "no records" apart from "an error in a 2xx response" — both resolve as `undefined`.
|
|
235
240
|
|
|
236
|
-
**Alternative — check the raw response.** `.resVanilla()` returns the raw Bitrix body for 2xx responses; check it with the exported `isBXRestAnswerSuccess()` type guard. Note that `.resVanilla()`, `.mapForVanilla()` and `.resAll()` don't convert errors: on a Bitrix error with a non-2xx status they fail with the original axios error, with the response body in `err.response.data`.
|
|
241
|
+
**Alternative — check the raw response.** `.resVanilla()` returns the raw Bitrix body for 2xx responses; check it with the exported `isBXRestAnswerSuccess()` type guard. Note that `.resVanilla()`, `.mapForVanilla()` and `.resAll()` don't convert errors: on a Bitrix error with a non-2xx status they fail with the original axios error, with the response body in `err.response.data`. An error in the body of a 2xx response comes back as is from `.resVanilla()` and `.mapForVanilla()`, while `.resAll()` fails with a `BXRestApiError` — it has no `throwOnApiError` option.
|
|
237
242
|
|
|
238
243
|
```typescript
|
|
239
244
|
import { firstValueFrom } from 'rxjs'
|
|
@@ -256,6 +261,205 @@ if (!answer || !isBXRestAnswerSuccess(answer)) {
|
|
|
256
261
|
}
|
|
257
262
|
```
|
|
258
263
|
|
|
264
|
+
## Authorization refresh
|
|
265
|
+
|
|
266
|
+
With `auth.refresh` set, the library refreshes an expired authorization by itself. When a request fails with an authorization error, the library calls `auth.refresh()` and retries the request, and your code just gets the result.
|
|
267
|
+
|
|
268
|
+
```typescript
|
|
269
|
+
BXRestSettings.update({
|
|
270
|
+
auth: {
|
|
271
|
+
source: () => accessToken,
|
|
272
|
+
key: 'auth',
|
|
273
|
+
// get a new authorization and put it where source reads it from
|
|
274
|
+
refresh: async () => {
|
|
275
|
+
accessToken = await getNewToken()
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
// ...
|
|
279
|
+
})
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
| `auth` field | Value |
|
|
283
|
+
| --- | --- |
|
|
284
|
+
| `refresh` | `() => Promise<unknown>`. Gets a new authorization. The promise value is not used: the retry reads the token, the URL and the headers from the settings again — so the function has to put the new token where `auth.source` reads it from (or, through a proxy, hand it to the server). |
|
|
285
|
+
| `isAuthError` | `(error: unknown) => boolean`, optional. Which request error counts as an authorization error. It gets the original HTTP request error — usually an `AxiosError`. Defaults to the exported `isBXRestAuthError`, which is easy to extend: `error => isBXRestAuthError(error) \|\| myCheck(error)`. |
|
|
286
|
+
|
|
287
|
+
Without `auth.refresh` the library behaves as before: requests are not retried. `update()` merges settings at the top level only, so `BXRestSettings.update({ auth: {...} })` replaces the whole `auth` — `refresh` included. When you change `auth`, pass `refresh` again.
|
|
288
|
+
|
|
289
|
+
How the retry works:
|
|
290
|
+
|
|
291
|
+
- **What counts as an authorization error** (`isBXRestAuthError`): a response with the Bitrix error `expired_token` or `invalid_token`, whatever the HTTP status; a 401 response without a Bitrix error body — e.g. an empty 401 from a backend proxy that has no valid token. The other Bitrix system errors come with status 401 too (`insufficient_scope`, `NO_AUTH_FOUND`, `ACCESS_DENIED`, `INVALID_CREDENTIALS`, `user_access_error`, `OVERLOAD_LIMIT`), but a new token does not fix them — they are not retried.
|
|
292
|
+
- **One retry.** If the retry fails as well, its error goes out; there is no second refresh.
|
|
293
|
+
- **One refresh for everybody.** When several requests fail at once, `refresh()` is called once and all of them are retried. A request that was in flight while the token was being refreshed is retried without another refresh.
|
|
294
|
+
- **The refresh failed** (`refresh()` rejected or threw) — the original request error goes out, and the reason the refresh failed is in its `authRefreshError` field. `getAuthRefreshError(err)` reads it from any error:
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { BXRestApiError, getAuthRefreshError } from 'bx-rest'
|
|
298
|
+
|
|
299
|
+
bxRest.crm.deal.get({ ID: 1 }).res().subscribe({
|
|
300
|
+
error: err => {
|
|
301
|
+
if (err instanceof BXRestApiError && err.error === 'expired_token') {
|
|
302
|
+
console.error('Could not refresh the authorization:', getAuthRefreshError(err))
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
})
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
- **Where it works.** In every library request: `.res()`, `.resAll()` (the first page and every batch request), `.resVanilla()`, `.mapForVanilla()`, `BXRestRequest.post()`/`.get()`, `BXRest.batch()`. The retry lives in the HTTP layer, so `.res()` already gets the result of the retry.
|
|
309
|
+
- **What is not retried:** errors in the body of a 2xx response (Bitrix sends `expired_token` with status 401) and errors of single commands inside a successful batch response (`result_error`) — the whole HTTP request is what gets retried.
|
|
310
|
+
|
|
311
|
+
### A token from the portal without BX24.js
|
|
312
|
+
|
|
313
|
+
A browser cannot refresh a token through OAuth — that takes the `client_secret`. But the application is open in a portal iframe, the user is logged in to the portal, and the portal itself hands the application a token over postMessage — this is how `BX24.getAuth()` and `BX24.refreshAuth()` work. `BXRestFrame` does the same without loading BX24.js (the other BX24.js commands are in [Working in an iframe without BX24.js](#working-in-an-iframe-without-bx24js)):
|
|
314
|
+
|
|
315
|
+
```typescript
|
|
316
|
+
import { BXRestFrame } from 'bx-rest'
|
|
317
|
+
|
|
318
|
+
const frame = new BXRestFrame() // optional: { timeout: 10000 }
|
|
319
|
+
|
|
320
|
+
const auth = await frame.getAuth() // the current authorization — like BX24.getAuth()
|
|
321
|
+
const fresh = await frame.refreshAuth() // a new authorization — like BX24.refreshAuth()
|
|
322
|
+
// { access_token, refresh_token, expires_in, domain, member_id }
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
| Result field | Value |
|
|
326
|
+
| --- | --- |
|
|
327
|
+
| `access_token`, `refresh_token` | The application tokens. |
|
|
328
|
+
| `expires_in` | When the `access_token` expires — **milliseconds** since January 1, 1970, as in `BX24.getAuth()`. Not the lifetime in seconds, as in the OAuth server response. |
|
|
329
|
+
| `domain` | The portal address without the protocol, e.g. `mycompany.bitrix24.com`. |
|
|
330
|
+
| `member_id` | The permanent portal identifier. |
|
|
331
|
+
|
|
332
|
+
- **Tokens.** `BXRestFrame` does not log tokens and does not store them anywhere: every `getAuth()` and `refreshAuth()` call asks the portal again, and your code gets the result.
|
|
333
|
+
- **Errors** — `BXRestFrameError` with a `code` field: `NOT_IN_FRAME` (the page is not opened in an application iframe — fails right away), `TIMEOUT` (the portal did not answer within `timeout`, 10 seconds by default), `NO_AUTH` (the portal answered without a token), `BAD_RESPONSE` (the answer could not be parsed). When the portal cannot issue a new token (e.g. the application trial or paid period is over), it shows the "Unable to get new token!" `alert` and does not answer at all — such a refusal comes as `TIMEOUT`.
|
|
334
|
+
- **`getAuth()` and the expiry.** The portal gives the token lifetime at the moment of issue, not what is left of it, so on a page that has been open for a while `expires_in` from `getAuth()` points too far ahead (the same with BX24.js). Use `refreshAuth()` for a fresh token.
|
|
335
|
+
- **The portal domain.** Like BX24.js, `BXRestFrame` cannot vouch for it — it is set by whoever embedded the page in an iframe. So a server must check a token that came from the browser with the portal before trusting it.
|
|
336
|
+
|
|
337
|
+
### Recipe: straight to the portal
|
|
338
|
+
|
|
339
|
+
The frontend keeps the token, requests go to `https://<portal>/rest/`. The refresh function gets a new token from the portal and puts it where `auth.source` reads it from — an in-memory variable here:
|
|
340
|
+
|
|
341
|
+
```typescript
|
|
342
|
+
import { BXRestSettings, BXRestFrame } from 'bx-rest'
|
|
343
|
+
|
|
344
|
+
const frame = new BXRestFrame()
|
|
345
|
+
const auth = await frame.getAuth()
|
|
346
|
+
let accessToken = auth.access_token
|
|
347
|
+
|
|
348
|
+
BXRestSettings.update({
|
|
349
|
+
auth: {
|
|
350
|
+
source: () => accessToken,
|
|
351
|
+
key: 'auth',
|
|
352
|
+
refresh: async () => {
|
|
353
|
+
accessToken = (await frame.refreshAuth()).access_token
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
urls: {
|
|
357
|
+
source: 'string',
|
|
358
|
+
key: `https://${auth.domain}`,
|
|
359
|
+
additional_part: 'rest'
|
|
360
|
+
}
|
|
361
|
+
})
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
With `auth.source: 'localStorage'` the refresh function writes the token to `localStorage[auth.key]` in the same way.
|
|
365
|
+
|
|
366
|
+
### Recipe: through a backend proxy
|
|
367
|
+
|
|
368
|
+
The server keeps the token, and the browser authorizes with the proxy through a cookie session (`auth.source: 'off'`). A proxy without a valid token answers 401 with an empty body — an authorization error by default. The refresh function gets a new token from the portal and sends it to the server:
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
import { BXRestSettings, BXRestFrame } from 'bx-rest'
|
|
372
|
+
|
|
373
|
+
const APP = 'smart-process-pro'
|
|
374
|
+
const frame = new BXRestFrame()
|
|
375
|
+
|
|
376
|
+
BXRestSettings.update({
|
|
377
|
+
auth: {
|
|
378
|
+
source: 'off',
|
|
379
|
+
key: 'auth',
|
|
380
|
+
refresh: async () => {
|
|
381
|
+
const { access_token, refresh_token, expires_in } = await frame.refreshAuth()
|
|
382
|
+
const response = await fetch(`/api/bitrix/token/${APP}`, {
|
|
383
|
+
method: 'POST',
|
|
384
|
+
credentials: 'same-origin',
|
|
385
|
+
headers: { 'Content-Type': 'application/json' },
|
|
386
|
+
body: JSON.stringify({ access_token, refresh_token, expires_in })
|
|
387
|
+
})
|
|
388
|
+
if (!response.ok) {
|
|
389
|
+
// no request body in the message — it holds the tokens
|
|
390
|
+
throw new Error(`Token upload failed: HTTP ${response.status}`)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
urls: {
|
|
395
|
+
source: 'string',
|
|
396
|
+
key: '/api/bitrix/rest/',
|
|
397
|
+
additional_part: '' // otherwise one more `rest/` is appended to the URL
|
|
398
|
+
},
|
|
399
|
+
headers: {
|
|
400
|
+
'X-Bitrix-App': APP
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Errors of the refresh function end up in `authRefreshError` of the original error, and `.res()` may print the original error to the console — so keep tokens out of the error messages of the refresh function.
|
|
406
|
+
|
|
407
|
+
## Working in an iframe without BX24.js
|
|
408
|
+
|
|
409
|
+
`BXRestFrame` is the commands an application in an iframe sends to the portal: the frame size, the selection dialogs, sliders, the messenger, placements, options, installation. The same commands and the same names as in BX24.js, but with promises instead of callbacks, and with no need to load BX24.js. bx-rest itself makes the REST requests, and `getAuth()`/`refreshAuth()` give the token for them (see [Authorization refresh](#authorization-refresh)).
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
import { BXRestFrame } from 'bx-rest'
|
|
413
|
+
|
|
414
|
+
const frame = new BXRestFrame()
|
|
415
|
+
|
|
416
|
+
const { lang, isAdmin, placement } = await frame.init()
|
|
417
|
+
await frame.fitWindow()
|
|
418
|
+
const user = await frame.selectUser()
|
|
419
|
+
const unsubscribe = frame.placement.bindEvent('Tab:onChange', data => console.log(data))
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Create one instance per page, when the application starts: the portal parameters are read in the constructor, and the portal reports the first run only in its first answer per page load. For the same reason, do not use `BXRestFrame` and BX24.js on one page.
|
|
423
|
+
|
|
424
|
+
| BX24.js | `BXRestFrame` | Answer |
|
|
425
|
+
| --- | --- | --- |
|
|
426
|
+
| `BX24.init()`, `isAdmin()`, `getLang()`, `getDomain()` | `init()` — `{ domain, lang, path, https, memberId, isAdmin, install, firstRun, placement, placementOptions, userOptions, appOptions }` | Asked once, without tokens |
|
|
427
|
+
| `BX24.getAuth()`, `BX24.refreshAuth()` | `getAuth()`, `refreshAuth()` | Asked from the portal every time |
|
|
428
|
+
| `BX24.installFinish()` | `installFinish()` | Only on the installer page (`install`); the portal reloads the page |
|
|
429
|
+
| the end of the `BX24.install()` chain | `firstRunFinish()` | Marks the first run as done; without it `firstRun` stays `true` |
|
|
430
|
+
| `BX24.resizeWindow(w, h)`, `BX24.fitWindow()` | `resizeWindow(w, h)`, `fitWindow()` | `{ width, height }` or `undefined` |
|
|
431
|
+
| `BX24.setTitle(title)` | `setTitle(title)` | `{ title }` or `undefined` |
|
|
432
|
+
| `BX24.scrollParentWindow(scroll)` | `scrollParentWindow(scroll)` | `{ scroll }` or `undefined` |
|
|
433
|
+
| `BX24.reloadWindow()` | `reloadWindow()` | No answer |
|
|
434
|
+
| `BX24.selectUser()`, `BX24.selectUsers()` | `selectUser({ title?, signal? })`, `selectUsers(...)` | An employee / an array of employees |
|
|
435
|
+
| `BX24.selectAccess(value)` | `selectAccess({ value?, signal? })` — `value`: codes that cannot be selected | An array of `{ provider, id, name }` |
|
|
436
|
+
| `BX24.selectCRM(params)` | `selectCRM({ entityType?, multiple?, value?, signal? })` | CRM items by type |
|
|
437
|
+
| `BX24.openApplication(params, cb, settings)` | `openApplication(params?, settings?, { signal? })` | When the slider is closed |
|
|
438
|
+
| `BX24.openPath(path)` | `openPath(path, { signal? })` | `{ result: 'close' }` or `{ result: 'error', errorCode }` |
|
|
439
|
+
| `BX24.closeApplication()` | `closeApplication()` | No answer |
|
|
440
|
+
| `BX24.im.callTo/phoneTo/openMessenger/openHistory` | `im.callTo/phoneTo/openMessenger/openHistory` | No answer |
|
|
441
|
+
| `BX24.placement.info()` | `placement.info()` | `{ placement, options }` from `init()` |
|
|
442
|
+
| `BX24.placement.getInterface()` | `placement.getInterface()` | `{ command, event }` |
|
|
443
|
+
| `BX24.placement.call(cmd, params)` | `placement.call(cmd, params?, { timeout? })` | The command answer or `undefined` |
|
|
444
|
+
| `BX24.placement.bindEvent(event, cb)` | `placement.bindEvent(event, handler)` | `handler` on every event; returns an unsubscribe function |
|
|
445
|
+
| `BX24.userOption.get/set` | `userOption.get/set` | A user option |
|
|
446
|
+
| `BX24.appOption.get/set` | `appOption.get/set` | `set` — administrators only, otherwise `ACCESS_DENIED` |
|
|
447
|
+
|
|
448
|
+
How the portal answers a command decides when the promise settles:
|
|
449
|
+
|
|
450
|
+
- **With an answer** (`init()`, `getAuth()`, `refreshAuth()`, `placement.getInterface()`, `appOption.set()`) — no answer within `timeout` (10 seconds by default) is a `TIMEOUT` error.
|
|
451
|
+
- **With an acknowledgement** (`resizeWindow()`, `fitWindow()`, `setTitle()`, `scrollParentWindow()`, `userOption.set()`, `firstRunFinish()`, `placement.call()`) — in some placements the portal does not answer them, so without an answer within `timeout` the promise resolves with `undefined`, not with an error.
|
|
452
|
+
- **Without an answer** (`reloadWindow()`, `closeApplication()`, `installFinish()`, `im.*`) — the promise resolves right after sending.
|
|
453
|
+
- **Waiting for the user** (`select*()`, `openApplication()`, `openPath()`) — with no time limit. When the user closes a dialog without selecting anything, the portal does not answer at all. Cancel the wait with `signal` (`AbortController`); calling the same dialog again cancels the previous wait, because the portal keeps one handler per dialog. A cancelled wait is a `CANCELLED` error.
|
|
454
|
+
|
|
455
|
+
`placement.bindEvent()` returns an unsubscribe function, but it only stops calling `handler`: the protocol has no way to cancel the subscription on the portal side.
|
|
456
|
+
|
|
457
|
+
- **Where it looks for the portal.** Like BX24.js, in `window.name`: the portal names the application iframe `DOMAIN|PROTOCOL|APP_SID`. If it is empty — in the page query (`?DOMAIN=…&PROTOCOL=…&APP_SID=…`) the portal opens the application handler with.
|
|
458
|
+
- **Security.** Commands go to the portal origin only (not `'*'`), answers are accepted only from `window.parent` and only from the portal origin.
|
|
459
|
+
- **Errors** — `BXRestFrameError` with a `code` field: `NOT_IN_FRAME` (the page is not opened in an application iframe — fails right away), `TIMEOUT`, `NO_AUTH`, `BAD_RESPONSE`, `CANCELLED`, `ACCESS_DENIED`. Wrong arguments of `resizeWindow()` and `scrollParentWindow()` are a `TypeError`.
|
|
460
|
+
|
|
461
|
+
> **The protocol is internal and undocumented.** The messages between an application and a Bitrix24 portal are not described in the documentation and may change without notice. `BXRestFrame` follows BX24.js — `https://api.bitrix24.com/api/v1/`, downloaded 2026-10-29, `Last-Modified: 2022-03-17`, SHA-256 `945ad491df8ffb7490b89f4914a439b5a3f575c950c3480562815716a3e07f23` — and is checked against the portal side, `/bitrix/js/rest/applayout.js` (`Last-Modified: 2024-05-15`, SHA-256 `3efd6ab57a4778493a26d10ae0987a82e19fc012ed9935f5c6afa78c4052fcb5`), and against the BX24.js documentation. It is tested against a fake portal, not against a live one.
|
|
462
|
+
|
|
259
463
|
## Usage in Angular
|
|
260
464
|
|
|
261
465
|
Register the public clients and `Navvy` when you want to inject them into Angular services or components:
|
package/README.md
CHANGED
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
- [`withCredentials`](#withcredentials)
|
|
21
21
|
- [HTTP-заголовки](#http-заголовки)
|
|
22
22
|
- [Обработка ошибок](#обработка-ошибок)
|
|
23
|
+
- [Продление авторизации](#продление-авторизации)
|
|
24
|
+
- [Токен от портала без BX24.js](#токен-от-портала-без-bx24js)
|
|
25
|
+
- [Рецепт: напрямую в портал](#рецепт-напрямую-в-портал)
|
|
26
|
+
- [Рецепт: через бэкенд-прокси](#рецепт-через-бэкенд-прокси)
|
|
27
|
+
- [Работа в iframe без BX24.js](#работа-в-iframe-без-bx24js)
|
|
23
28
|
- [Использование в Angular](#использование-в-angular)
|
|
24
29
|
- [SessionKeyServices](#sessionkeyservices)
|
|
25
30
|
- [Использование в Vue](#использование-в-vue)
|
|
@@ -201,7 +206,7 @@ const deals = await firstValueFrom(
|
|
|
201
206
|
| --- | --- |
|
|
202
207
|
| Ошибка API Bitrix с HTTP-статусом не 2xx — так Bitrix отдаёт большинство ошибок: `expired_token` (401), `ERROR_METHOD_NOT_FOUND` (404), `QUERY_LIMIT_EXCEEDED` (503), `Not found` у `crm.*.get` (400) | Возвращённый `Observable` завершается ошибкой `BXRestApiError` — с `throwOnApiError` и без него. |
|
|
203
208
|
| Ошибка API Bitrix в теле ответа со статусом 2xx | По умолчанию `.res()` резолвится обычным образом со значением `undefined` — **исключение не выбрасывается**. Передайте `{ throwOnApiError: true }`, чтобы вместо этого выбрасывалось `BXRestApiError`. |
|
|
204
|
-
| Ошибки транспорта / конфигурации — сбой сети, отсутствие авторизации, отсутствие базового URL, ответ не от Bitrix (например, HTML-страница ошибки прокси) | Возвращённый `Observable` завершается обычной ошибкой `Error`. |
|
|
209
|
+
| Ошибки транспорта / конфигурации — сбой сети, отсутствие авторизации, отсутствие базового URL, ответ не от Bitrix (например, HTML-страница ошибки прокси) | Возвращённый `Observable` завершается обычной ошибкой `Error`. Исходная ошибка — в её `cause`: для ответа не от Bitrix это ошибка axios, HTTP-статус — в `cause.response.status`. |
|
|
205
210
|
|
|
206
211
|
Поля `BXRestApiError`:
|
|
207
212
|
|
|
@@ -232,7 +237,7 @@ bxRest.crm.deal.list({
|
|
|
232
237
|
if (err instanceof BXRestApiError) {
|
|
233
238
|
console.error('Bitrix API error:', err.status, err.error, err.error_description)
|
|
234
239
|
if (err.error === 'expired_token') {
|
|
235
|
-
//
|
|
240
|
+
// auth.refresh не задан или не помог — см. «Продление авторизации»
|
|
236
241
|
}
|
|
237
242
|
} else {
|
|
238
243
|
console.error('Request failed:', err.message)
|
|
@@ -243,7 +248,7 @@ bxRest.crm.deal.list({
|
|
|
243
248
|
|
|
244
249
|
Без `throwOnApiError` по голому `.res()` нельзя отличить «нет записей» от «ошибки в ответе 2xx» — оба случая резолвятся как `undefined`.
|
|
245
250
|
|
|
246
|
-
**Альтернатива — проверять сырой ответ.** `.resVanilla()` возвращает сырое тело ответа Bitrix для ответов 2xx; проверяйте его экспортируемым type guard `isBXRestAnswerSuccess()`. Учтите, что `.resVanilla()`, `.mapForVanilla()` и `.resAll()` ошибки не преобразуют: на ошибку Bitrix со статусом не 2xx они завершаются исходной ошибкой axios, тело ответа — в `err.response.data`.
|
|
251
|
+
**Альтернатива — проверять сырой ответ.** `.resVanilla()` возвращает сырое тело ответа Bitrix для ответов 2xx; проверяйте его экспортируемым type guard `isBXRestAnswerSuccess()`. Учтите, что `.resVanilla()`, `.mapForVanilla()` и `.resAll()` ошибки не преобразуют: на ошибку Bitrix со статусом не 2xx они завершаются исходной ошибкой axios, тело ответа — в `err.response.data`. Ошибку в теле ответа 2xx `.resVanilla()` и `.mapForVanilla()` отдают как есть, а `.resAll()` завершается ошибкой `BXRestApiError`: опции `throwOnApiError` у него нет.
|
|
247
252
|
|
|
248
253
|
```typescript
|
|
249
254
|
import { firstValueFrom } from 'rxjs'
|
|
@@ -266,6 +271,205 @@ if (!answer || !isBXRestAnswerSuccess(answer)) {
|
|
|
266
271
|
}
|
|
267
272
|
```
|
|
268
273
|
|
|
274
|
+
## Продление авторизации
|
|
275
|
+
|
|
276
|
+
Если задать `auth.refresh`, библиотека сама продлевает истёкшую авторизацию. Запрос упал с ошибкой авторизации — библиотека вызывает `auth.refresh()` и повторяет запрос, а прикладной код получает обычный результат.
|
|
277
|
+
|
|
278
|
+
```typescript
|
|
279
|
+
BXRestSettings.update({
|
|
280
|
+
auth: {
|
|
281
|
+
source: () => accessToken,
|
|
282
|
+
key: 'auth',
|
|
283
|
+
// получить новую авторизацию и положить её туда, откуда её читает source
|
|
284
|
+
refresh: async () => {
|
|
285
|
+
accessToken = await getNewToken()
|
|
286
|
+
}
|
|
287
|
+
},
|
|
288
|
+
// ...
|
|
289
|
+
})
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
| Поле `auth` | Значение |
|
|
293
|
+
| --- | --- |
|
|
294
|
+
| `refresh` | `() => Promise<unknown>`. Получает новую авторизацию. Значение промиса не используется: повтор заново читает токен, адрес и заголовки из настроек — поэтому функция должна положить новый токен туда, откуда его читает `auth.source` (или, при работе через прокси, передать его на сервер). |
|
|
295
|
+
| `isAuthError` | `(error: unknown) => boolean`, необязательно. Какую ошибку запроса считать ошибкой авторизации. Получает исходную ошибку HTTP-запроса — обычно `AxiosError`. По умолчанию — экспортируемая `isBXRestAuthError`, её удобно дополнять: `error => isBXRestAuthError(error) \|\| myCheck(error)`. |
|
|
296
|
+
|
|
297
|
+
Без `auth.refresh` библиотека ведёт себя как раньше: запросы не повторяются. `update()` сливает настройки только на верхнем уровне, поэтому `BXRestSettings.update({ auth: {...} })` заменяет `auth` целиком — вместе с `refresh`. Если меняете `auth`, передайте `refresh` снова.
|
|
298
|
+
|
|
299
|
+
Как работает повтор:
|
|
300
|
+
|
|
301
|
+
- **Что считается ошибкой авторизации** (`isBXRestAuthError`): ответ с ошибкой Bitrix `expired_token` или `invalid_token` — при любом HTTP-статусе; ответ 401 без тела ошибки Bitrix — например, пустой 401 от бэкенд-прокси, у которого нет действующего токена. Остальные системные ошибки Bitrix тоже приходят со статусом 401 (`insufficient_scope`, `NO_AUTH_FOUND`, `ACCESS_DENIED`, `INVALID_CREDENTIALS`, `user_access_error`, `OVERLOAD_LIMIT`), но новый токен их не исправит — они не повторяются.
|
|
302
|
+
- **Один повтор.** Если и повтор упал — наружу уходит его ошибка, второго продления не будет.
|
|
303
|
+
- **Одно продление на всех.** Если одновременно упало несколько запросов, `refresh()` вызывается один раз, а повторяются все. Запрос, который был в пути, пока токен продлевали, повторяется без нового продления.
|
|
304
|
+
- **Продление упало** (`refresh()` отклонил промис или бросил исключение) — наружу уходит исходная ошибка запроса, а причина сбоя продления — в её поле `authRefreshError`. Достать её из любой ошибки — `getAuthRefreshError(err)`:
|
|
305
|
+
|
|
306
|
+
```typescript
|
|
307
|
+
import { BXRestApiError, getAuthRefreshError } from 'bx-rest'
|
|
308
|
+
|
|
309
|
+
bxRest.crm.deal.get({ ID: 1 }).res().subscribe({
|
|
310
|
+
error: err => {
|
|
311
|
+
if (err instanceof BXRestApiError && err.error === 'expired_token') {
|
|
312
|
+
console.error('Авторизацию продлить не удалось:', getAuthRefreshError(err))
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
- **Где работает.** Во всех запросах библиотеки: `.res()`, `.resAll()` (и первая страница, и каждый batch-запрос), `.resVanilla()`, `.mapForVanilla()`, `BXRestRequest.post()`/`.get()`, `BXRest.batch()`. Повтор встроен в HTTP-слой, поэтому `.res()` получает уже результат повтора.
|
|
319
|
+
- **Что не повторяется:** ошибки в теле ответа со статусом 2xx (Bitrix отдаёт `expired_token` со статусом 401) и ошибки отдельных команд внутри успешного batch-ответа (`result_error`) — повторяется HTTP-запрос целиком.
|
|
320
|
+
|
|
321
|
+
### Токен от портала без BX24.js
|
|
322
|
+
|
|
323
|
+
Обновить токен по OAuth прямо в браузере нельзя — для этого нужен `client_secret`. Но приложение открыто в iframe портала, пользователь в портале залогинен, и портал сам выдаёт приложению токен по postMessage — так работают `BX24.getAuth()` и `BX24.refreshAuth()`. `BXRestFrame` делает то же самое без подключения BX24.js (остальные команды BX24.js — в разделе [Работа в iframe без BX24.js](#работа-в-iframe-без-bx24js)):
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
import { BXRestFrame } from 'bx-rest'
|
|
327
|
+
|
|
328
|
+
const frame = new BXRestFrame() // необязательно: { timeout: 10000 }
|
|
329
|
+
|
|
330
|
+
const auth = await frame.getAuth() // текущая авторизация — как BX24.getAuth()
|
|
331
|
+
const fresh = await frame.refreshAuth() // новая авторизация — как BX24.refreshAuth()
|
|
332
|
+
// { access_token, refresh_token, expires_in, domain, member_id }
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
| Поле результата | Значение |
|
|
336
|
+
| --- | --- |
|
|
337
|
+
| `access_token`, `refresh_token` | Токены приложения. |
|
|
338
|
+
| `expires_in` | Момент истечения `access_token` — **миллисекунды** с 1 января 1970 года, как у `BX24.getAuth()`. Не срок жизни в секундах, как в ответе OAuth-сервера. |
|
|
339
|
+
| `domain` | Адрес портала без протокола, например `mycompany.bitrix24.ru`. |
|
|
340
|
+
| `member_id` | Постоянный идентификатор портала. |
|
|
341
|
+
|
|
342
|
+
- **Токены** `BXRestFrame` не логирует и никуда не сохраняет: каждый вызов `getAuth()` и `refreshAuth()` спрашивает портал заново, результат получает ваш код.
|
|
343
|
+
- **Ошибки** — `BXRestFrameError` с полем `code`: `NOT_IN_FRAME` (страница открыта не в iframe приложения — ошибка сразу), `TIMEOUT` (портал не ответил за `timeout`, по умолчанию 10 секунд), `NO_AUTH` (портал ответил без токена), `BAD_RESPONSE` (ответ не удалось разобрать). Если портал не может выдать новый токен (например, у приложения закончился пробный или платный период), он показывает `alert` «Unable to get new token!» и не отвечает вовсе — такой отказ приходит как `TIMEOUT`.
|
|
344
|
+
- **`getAuth()` и срок действия.** Портал отдаёт срок жизни токена на момент выдачи, а не остаток, поэтому `expires_in` от `getAuth()` на давно открытой странице завышен (у BX24.js так же). Для свежего токена используйте `refreshAuth()`.
|
|
345
|
+
- **Домен портала** `BXRestFrame`, как и BX24.js, удостоверить не может — его задаёт тот, кто встроил страницу в iframe. Поэтому токен, пришедший из браузера, сервер должен проверить у портала, прежде чем ему доверять.
|
|
346
|
+
|
|
347
|
+
### Рецепт: напрямую в портал
|
|
348
|
+
|
|
349
|
+
Токен хранит фронтенд, запросы идут в `https://<портал>/rest/`. Функция продления берёт новый токен у портала и кладёт его туда, откуда его читает `auth.source` — здесь это переменная в памяти:
|
|
350
|
+
|
|
351
|
+
```typescript
|
|
352
|
+
import { BXRestSettings, BXRestFrame } from 'bx-rest'
|
|
353
|
+
|
|
354
|
+
const frame = new BXRestFrame()
|
|
355
|
+
const auth = await frame.getAuth()
|
|
356
|
+
let accessToken = auth.access_token
|
|
357
|
+
|
|
358
|
+
BXRestSettings.update({
|
|
359
|
+
auth: {
|
|
360
|
+
source: () => accessToken,
|
|
361
|
+
key: 'auth',
|
|
362
|
+
refresh: async () => {
|
|
363
|
+
accessToken = (await frame.refreshAuth()).access_token
|
|
364
|
+
}
|
|
365
|
+
},
|
|
366
|
+
urls: {
|
|
367
|
+
source: 'string',
|
|
368
|
+
key: `https://${auth.domain}`,
|
|
369
|
+
additional_part: 'rest'
|
|
370
|
+
}
|
|
371
|
+
})
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
С `auth.source: 'localStorage'` функция продления так же записывает токен в `localStorage[auth.key]`.
|
|
375
|
+
|
|
376
|
+
### Рецепт: через бэкенд-прокси
|
|
377
|
+
|
|
378
|
+
Токен хранит сервер, браузер авторизуется у прокси cookie-сессией (`auth.source: 'off'`). Прокси без действующего токена отвечает 401 с пустым телом — это ошибка авторизации по умолчанию. Функция продления берёт новый токен у портала и отправляет его на сервер:
|
|
379
|
+
|
|
380
|
+
```typescript
|
|
381
|
+
import { BXRestSettings, BXRestFrame } from 'bx-rest'
|
|
382
|
+
|
|
383
|
+
const APP = 'smart-process-pro'
|
|
384
|
+
const frame = new BXRestFrame()
|
|
385
|
+
|
|
386
|
+
BXRestSettings.update({
|
|
387
|
+
auth: {
|
|
388
|
+
source: 'off',
|
|
389
|
+
key: 'auth',
|
|
390
|
+
refresh: async () => {
|
|
391
|
+
const { access_token, refresh_token, expires_in } = await frame.refreshAuth()
|
|
392
|
+
const response = await fetch(`/api/bitrix/token/${APP}`, {
|
|
393
|
+
method: 'POST',
|
|
394
|
+
credentials: 'same-origin',
|
|
395
|
+
headers: { 'Content-Type': 'application/json' },
|
|
396
|
+
body: JSON.stringify({ access_token, refresh_token, expires_in })
|
|
397
|
+
})
|
|
398
|
+
if (!response.ok) {
|
|
399
|
+
// без тела запроса в сообщении — в нём токены
|
|
400
|
+
throw new Error(`Token upload failed: HTTP ${response.status}`)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
},
|
|
404
|
+
urls: {
|
|
405
|
+
source: 'string',
|
|
406
|
+
key: '/api/bitrix/rest/',
|
|
407
|
+
additional_part: '' // иначе к адресу добавится ещё один `rest/`
|
|
408
|
+
},
|
|
409
|
+
headers: {
|
|
410
|
+
'X-Bitrix-App': APP
|
|
411
|
+
}
|
|
412
|
+
})
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
Ошибки функции продления попадают в `authRefreshError` исходной ошибки, а `.res()` может вывести исходную ошибку в консоль — поэтому не кладите токены в текст ошибок функции продления.
|
|
416
|
+
|
|
417
|
+
## Работа в iframe без BX24.js
|
|
418
|
+
|
|
419
|
+
`BXRestFrame` — команды, которые приложение из iframe отправляет порталу: размер фрейма, диалоги выбора, слайдеры, мессенджер, встраивания, настройки, установка. Те же команды и те же имена, что у BX24.js, но вместо колбэков — промисы, и подключать BX24.js не нужно. REST-запросы делает сама bx-rest, токен для них — `getAuth()`/`refreshAuth()` (см. [Продление авторизации](#продление-авторизации)).
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
import { BXRestFrame } from 'bx-rest'
|
|
423
|
+
|
|
424
|
+
const frame = new BXRestFrame()
|
|
425
|
+
|
|
426
|
+
const { lang, isAdmin, placement } = await frame.init()
|
|
427
|
+
await frame.fitWindow()
|
|
428
|
+
const user = await frame.selectUser()
|
|
429
|
+
const unsubscribe = frame.placement.bindEvent('Tab:onChange', data => console.log(data))
|
|
430
|
+
```
|
|
431
|
+
|
|
432
|
+
Создавайте один экземпляр на страницу и при старте приложения: параметры портала читаются в конструкторе, а признак первого запуска портал сообщает только в первом ответе за загрузку страницы. По той же причине не используйте `BXRestFrame` и BX24.js на одной странице.
|
|
433
|
+
|
|
434
|
+
| BX24.js | `BXRestFrame` | Ответ |
|
|
435
|
+
| --- | --- | --- |
|
|
436
|
+
| `BX24.init()`, `isAdmin()`, `getLang()`, `getDomain()` | `init()` — `{ domain, lang, path, https, memberId, isAdmin, install, firstRun, placement, placementOptions, userOptions, appOptions }` | Запрашивается один раз, без токенов |
|
|
437
|
+
| `BX24.getAuth()`, `BX24.refreshAuth()` | `getAuth()`, `refreshAuth()` | Каждый раз у портала |
|
|
438
|
+
| `BX24.installFinish()` | `installFinish()` | Только на странице установщика (`install`); портал перезагружает страницу |
|
|
439
|
+
| конец цепочки `BX24.install()` | `firstRunFinish()` | Отмечает, что первый запуск пройден; без него `firstRun` останется `true` |
|
|
440
|
+
| `BX24.resizeWindow(w, h)`, `BX24.fitWindow()` | `resizeWindow(w, h)`, `fitWindow()` | `{ width, height }` или `undefined` |
|
|
441
|
+
| `BX24.setTitle(title)` | `setTitle(title)` | `{ title }` или `undefined` |
|
|
442
|
+
| `BX24.scrollParentWindow(scroll)` | `scrollParentWindow(scroll)` | `{ scroll }` или `undefined` |
|
|
443
|
+
| `BX24.reloadWindow()` | `reloadWindow()` | Без ответа |
|
|
444
|
+
| `BX24.selectUser()`, `BX24.selectUsers()` | `selectUser({ title?, signal? })`, `selectUsers(...)` | Сотрудник / массив сотрудников |
|
|
445
|
+
| `BX24.selectAccess(value)` | `selectAccess({ value?, signal? })` — `value`: коды, которые нельзя выбрать | Массив `{ provider, id, name }` |
|
|
446
|
+
| `BX24.selectCRM(params)` | `selectCRM({ entityType?, multiple?, value?, signal? })` | Объекты CRM по типам |
|
|
447
|
+
| `BX24.openApplication(params, cb, settings)` | `openApplication(params?, settings?, { signal? })` | Когда слайдер закрыли |
|
|
448
|
+
| `BX24.openPath(path)` | `openPath(path, { signal? })` | `{ result: 'close' }` или `{ result: 'error', errorCode }` |
|
|
449
|
+
| `BX24.closeApplication()` | `closeApplication()` | Без ответа |
|
|
450
|
+
| `BX24.im.callTo/phoneTo/openMessenger/openHistory` | `im.callTo/phoneTo/openMessenger/openHistory` | Без ответа |
|
|
451
|
+
| `BX24.placement.info()` | `placement.info()` | `{ placement, options }` из `init()` |
|
|
452
|
+
| `BX24.placement.getInterface()` | `placement.getInterface()` | `{ command, event }` |
|
|
453
|
+
| `BX24.placement.call(cmd, params)` | `placement.call(cmd, params?, { timeout? })` | Ответ команды или `undefined` |
|
|
454
|
+
| `BX24.placement.bindEvent(event, cb)` | `placement.bindEvent(event, handler)` | `handler` на каждое событие; возвращает функцию отписки |
|
|
455
|
+
| `BX24.userOption.get/set` | `userOption.get/set` | Настройка пользователя |
|
|
456
|
+
| `BX24.appOption.get/set` | `appOption.get/set` | `set` — только администратору, иначе `ACCESS_DENIED` |
|
|
457
|
+
|
|
458
|
+
Как портал отвечает на команды — от этого зависит, когда завершается промис:
|
|
459
|
+
|
|
460
|
+
- **С ответом** (`init()`, `getAuth()`, `refreshAuth()`, `placement.getInterface()`, `appOption.set()`) — нет ответа за `timeout` (по умолчанию 10 секунд) — ошибка `TIMEOUT`.
|
|
461
|
+
- **С подтверждением** (`resizeWindow()`, `fitWindow()`, `setTitle()`, `scrollParentWindow()`, `userOption.set()`, `firstRunFinish()`, `placement.call()`) — в некоторых встраиваниях портал на них не отвечает, поэтому без ответа за `timeout` промис завершается с `undefined`, а не ошибкой.
|
|
462
|
+
- **Без ответа** (`reloadWindow()`, `closeApplication()`, `installFinish()`, `im.*`) — промис завершается сразу после отправки.
|
|
463
|
+
- **Ждут пользователя** (`select*()`, `openApplication()`, `openPath()`) — без ограничения по времени. Если пользователь закрыл диалог, ничего не выбрав, портал не отвечает вовсе. Отменить ожидание можно через `signal` (`AbortController`), а повторный вызов того же диалога отменяет прошлый: портал держит один обработчик на диалог. Отменённое ожидание — ошибка `CANCELLED`.
|
|
464
|
+
|
|
465
|
+
`placement.bindEvent()` возвращает функцию отписки, но она только перестаёт вызывать `handler`: отменить подписку на стороне портала протокол не позволяет.
|
|
466
|
+
|
|
467
|
+
- **Где ищет портал.** Как BX24.js — в `window.name`: портал называет iframe приложения `DOMAIN|PROTOCOL|APP_SID`. Если там пусто — в query страницы (`?DOMAIN=…&PROTOCOL=…&APP_SID=…`), с которым портал открывает обработчик приложения.
|
|
468
|
+
- **Безопасность.** Команды уходят только на origin портала (не `'*'`), ответы принимаются только от `window.parent` и только с origin портала.
|
|
469
|
+
- **Ошибки** — `BXRestFrameError` с полем `code`: `NOT_IN_FRAME` (страница открыта не в iframe приложения — ошибка сразу), `TIMEOUT`, `NO_AUTH`, `BAD_RESPONSE`, `CANCELLED`, `ACCESS_DENIED`. Неверные аргументы `resizeWindow()` и `scrollParentWindow()` — `TypeError`.
|
|
470
|
+
|
|
471
|
+
> **Протокол внутренний и недокументированный.** Обмен сообщениями между приложением и порталом Bitrix24 не описан в документации и может измениться без предупреждения. `BXRestFrame` повторяет BX24.js — `https://api.bitrix24.com/api/v1/`, скачан 2026-10-29, `Last-Modified: 2022-03-17`, SHA-256 `945ad491df8ffb7490b89f4914a439b5a3f575c950c3480562815716a3e07f23` — и сверен с ответной стороной портала, `/bitrix/js/rest/applayout.js` (`Last-Modified: 2024-05-15`, SHA-256 `3efd6ab57a4778493a26d10ae0987a82e19fc012ed9935f5c6afa78c4052fcb5`) и с документацией BX24.js. Проверен на поддельном портале в тестах, но не на живом портале.
|
|
472
|
+
|
|
269
473
|
## Использование в Angular
|
|
270
474
|
|
|
271
475
|
Зарегистрируйте публичные клиенты и `Navvy`, когда нужно внедрять их в сервисы или компоненты Angular:
|