bx-rest 1.3.0 → 1.4.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.
@@ -1,5 +1,5 @@
1
- import { switchMap, map, catchError, take, mergeMap as mergeMap$1 } from 'rxjs/operators';
2
- import { Subscription, of, throwError, from, forkJoin, defer, last, shareReplay, mergeMap, concatMap, toArray, map as map$1, tap } from 'rxjs';
1
+ import { switchMap, catchError, map, take, mergeMap as mergeMap$1 } from 'rxjs/operators';
2
+ import { Subscription, of, throwError, from, defer, forkJoin, last, shareReplay, mergeMap, concatMap, toArray, map as map$1, tap } from 'rxjs';
3
3
  import clone from 'just-clone';
4
4
  import axios from 'axios';
5
5
  import flatten from 'just-flatten-it';
@@ -930,6 +930,178 @@ function serializeBitrixParams(params) {
930
930
  return stringify(params, BITRIX_STRINGIFY_OPTIONS);
931
931
  }
932
932
 
933
+ /**
934
+ * Why `BXRestSettings.auth.refresh` failed. When a request fails with an
935
+ * authorization error and the refresh fails too, the library rethrows the
936
+ * original request error and puts the refresh failure into its
937
+ * `authRefreshError` field — on the `AxiosError` of `.resVanilla()` and the
938
+ * like, and on the `BXRestApiError` / `Error` of `.res()`.
939
+ *
940
+ * Returns `undefined` when there was no refresh or it succeeded.
941
+ */
942
+ function getAuthRefreshError(error) {
943
+ return (error && typeof error === 'object')
944
+ ? error.authRefreshError
945
+ : undefined;
946
+ }
947
+ /**
948
+ * A Bitrix-level API error, e.g. `INSUFFICIENT_SCOPE` or `expired_token`.
949
+ *
950
+ * `.res()` fails with it whenever the portal answers with a Bitrix error and
951
+ * a non-2xx HTTP status — which is how Bitrix sends most of its errors. When
952
+ * the error comes in the body of a 2xx response, `.res()` resolves as
953
+ * `undefined` by default and fails with it only with
954
+ * `{ throwOnApiError: true }` — see the "Error handling" section of the README.
955
+ */
956
+ class BXRestApiError extends Error {
957
+ constructor(error, error_description, details) {
958
+ super(error_description || error || 'Bitrix API error', details?.cause !== undefined ? { cause: details.cause } : undefined);
959
+ this.error = error;
960
+ this.error_description = error_description;
961
+ this.name = 'BXRestApiError';
962
+ this.status = details?.status;
963
+ this.method = details?.method;
964
+ this.authRefreshError = details?.authRefreshError;
965
+ }
966
+ }
967
+ /**
968
+ * Recognises a Bitrix error body: REST v2 `{ error, error_description }` and
969
+ * REST v3 `{ error: { code, message } }`. Returns `undefined` for anything
970
+ * else — a success payload, an HTML page from a proxy, an empty body.
971
+ *
972
+ * An empty `error` still counts: Bitrix answers e.g. `crm.deal.get` for a
973
+ * missing ID with `{ error: '', error_description: 'Not found' }`.
974
+ */
975
+ function parseBXRestErrorBody(body) {
976
+ if (!body || typeof body !== 'object' || !('error' in body)) {
977
+ return undefined;
978
+ }
979
+ const { error, error_description } = body;
980
+ if (typeof error === 'string') {
981
+ return {
982
+ error,
983
+ error_description: typeof error_description === 'string' ? error_description : undefined
984
+ };
985
+ }
986
+ if (error && typeof error === 'object' && 'code' in error) {
987
+ const { code, message } = error;
988
+ return {
989
+ error: String(code),
990
+ error_description: typeof message === 'string' ? message : undefined
991
+ };
992
+ }
993
+ return undefined;
994
+ }
995
+ /**
996
+ * Turns an HTTP failure into a `BXRestApiError` when the portal answered with
997
+ * a Bitrix error body. Bitrix sends most errors with a non-2xx status
998
+ * (401 `expired_token`, 404 `ERROR_METHOD_NOT_FOUND`, 503
999
+ * `QUERY_LIMIT_EXCEEDED`, 400 "Not found" from `crm.*.get`, ...), and axios
1000
+ * rejects on those.
1001
+ *
1002
+ * Returns `undefined` for network errors and for responses that are not a
1003
+ * Bitrix error body, so the caller can keep its own handling for them.
1004
+ */
1005
+ function bxRestApiErrorFromHttp(err, method) {
1006
+ if (!axios.isAxiosError(err) || !err.response) {
1007
+ return undefined;
1008
+ }
1009
+ const parsed = parseBXRestErrorBody(err.response.data);
1010
+ if (!parsed) {
1011
+ return undefined;
1012
+ }
1013
+ return new BXRestApiError(parsed.error, parsed.error_description, {
1014
+ status: err.response.status,
1015
+ method,
1016
+ cause: err,
1017
+ authRefreshError: getAuthRefreshError(err)
1018
+ });
1019
+ }
1020
+
1021
+ /**
1022
+ * Коды ошибок Bitrix, которые лечатся продлением токена. `expired_token` —
1023
+ * единственный системный код, для которого документация Bitrix24 советует
1024
+ * "обновить токены и повторить вызов". `invalid_token` в списке системных
1025
+ * ошибок документации нет, но так же считает официальный `@bitrix24/b24jssdk`.
1026
+ *
1027
+ * Остальные системные ошибки тоже приходят со статусом 401
1028
+ * (`insufficient_scope`, `NO_AUTH_FOUND`, `ACCESS_DENIED`,
1029
+ * `INVALID_CREDENTIALS`, `user_access_error`, ...), но новый токен их
1030
+ * не исправит — поэтому смотрим на код, а не только на статус.
1031
+ */
1032
+ const AUTH_ERROR_CODES = ['expired_token', 'invalid_token'];
1033
+ /**
1034
+ * Условие продления по умолчанию (`BXRestSettings.auth.isAuthError`):
1035
+ * - ответ с ошибкой Bitrix `expired_token` или `invalid_token` — при любом статусе;
1036
+ * - ответ 401 без тела ошибки Bitrix — например, пустой ответ своего
1037
+ * бэкенд-прокси, у которого нет действующего токена.
1038
+ *
1039
+ * Ответ 401 с другим кодом Bitrix ошибкой авторизации не считается.
1040
+ */
1041
+ function isBXRestAuthError(error) {
1042
+ if (!axios.isAxiosError(error) || !error.response) {
1043
+ return false;
1044
+ }
1045
+ const parsed = parseBXRestErrorBody(error.response.data);
1046
+ if (parsed) {
1047
+ return AUTH_ERROR_CODES.includes(parsed.error);
1048
+ }
1049
+ return error.response.status === 401;
1050
+ }
1051
+ // Общее на всю библиотеку состояние продления — как и сами BXRestSettings.
1052
+ // pendingRefresh — идущее сейчас продление, его ждут все упавшие запросы;
1053
+ // refreshCount — сколько продлений уже завершилось успешно.
1054
+ let pendingRefresh;
1055
+ let refreshCount = 0;
1056
+ function sharedRefresh(refresh) {
1057
+ if (!pendingRefresh) {
1058
+ pendingRefresh = Promise.resolve()
1059
+ // Через then, чтобы синхронное исключение в refresh() тоже стало
1060
+ // отклонённым промисом, а не вылетело из catchError
1061
+ .then(() => refresh())
1062
+ .then(() => {
1063
+ refreshCount++;
1064
+ })
1065
+ .finally(() => {
1066
+ pendingRefresh = undefined;
1067
+ });
1068
+ }
1069
+ return pendingRefresh;
1070
+ }
1071
+ /**
1072
+ * Повтор запроса после продления авторизации (`BXRestSettings.auth.refresh`).
1073
+ *
1074
+ * `request$` должен быть "холодным" и читать настройки при подписке (как
1075
+ * `defer()` в HttpBXServices): повтор — это повторная подписка, поэтому
1076
+ * токен, адрес и заголовки берутся уже новые. На второй попытке ошибка не
1077
+ * перехватывается — запрос повторяется не больше одного раза.
1078
+ */
1079
+ function withAuthRefresh(request$) {
1080
+ return defer(() => {
1081
+ const { refresh, isAuthError = isBXRestAuthError } = BXRestSettings.value.auth;
1082
+ if (!refresh) {
1083
+ return request$;
1084
+ }
1085
+ const refreshCountAtStart = refreshCount;
1086
+ return request$.pipe(catchError((error) => {
1087
+ if (!isAuthError(error)) {
1088
+ return throwError(() => error);
1089
+ }
1090
+ // Пока этот запрос был в пути, токен уже продлили по ошибке другого
1091
+ // запроса — повторяем без нового продления
1092
+ const refreshed = (refreshCount !== refreshCountAtStart)
1093
+ ? Promise.resolve()
1094
+ : sharedRefresh(refresh);
1095
+ return from(refreshed).pipe(catchError((refreshError) => {
1096
+ if (error && typeof error === 'object') {
1097
+ error.authRefreshError = refreshError;
1098
+ }
1099
+ return throwError(() => error);
1100
+ }), switchMap(() => request$));
1101
+ }));
1102
+ });
1103
+ }
1104
+
933
1105
  class HttpBXServices extends HttpServices {
934
1106
  /**
935
1107
  * Не считайте этот метод образцом/финальным API.
@@ -1002,7 +1174,13 @@ class HttpBXServices extends HttpServices {
1002
1174
  // геттером в SessionKeyServices настройки читались бы в момент вызова
1003
1175
  // .res(), а не в момент subscribe()/firstValueFrom(), что и обещано
1004
1176
  // в README как поведение "ленивого" REST-вызова.
1005
- return defer(() => {
1177
+ //
1178
+ // withAuthRefresh() повторяет запрос после BXRestSettings.auth.refresh —
1179
+ // повторной подпиской на этот же defer(), поэтому повтор заново читает
1180
+ // токен, адрес и заголовки. Через httpPost/httpGet идут все запросы
1181
+ // библиотеки (post/get, batch, все виды Navvy-запросов), так что повтор
1182
+ // встроен только здесь.
1183
+ return withAuthRefresh(defer(() => {
1006
1184
  const paramsClone = clone(params);
1007
1185
  const key = this.session.getKeyAuth();
1008
1186
  const auth = this.session.getAuthParams();
@@ -1032,14 +1210,15 @@ class HttpBXServices extends HttpServices {
1032
1210
  headers: this.buildHeaders(headers, { 'Content-Type': 'application/json' }),
1033
1211
  }).then((response) => response.data));
1034
1212
  }));
1035
- });
1213
+ }));
1036
1214
  }
1037
1215
  httpGet(url, params = {}, settings, headers) {
1038
1216
  // Та же причина, что и у httpPost выше: откладываем чтение BXRestSettings
1039
1217
  // до подписки, чтобы `get()` вёл себя так же "лениво", как и `post()`
1040
1218
  // (симметрия, которой не было до этого — GET резолвил настройки в момент
1041
- // вызова, а не в момент subscribe()).
1042
- return defer(() => {
1219
+ // вызова, а не в момент subscribe()). Повтор после продления
1220
+ // авторизации — как в httpPost.
1221
+ return withAuthRefresh(defer(() => {
1043
1222
  const paramsClone = clone(params);
1044
1223
  const auth = this.session.getAuthParams();
1045
1224
  const checkIsOn = this.session.getCheckAuthParamsIsOn();
@@ -1060,7 +1239,7 @@ class HttpBXServices extends HttpServices {
1060
1239
  headers: this.buildHeaders(headers),
1061
1240
  }).then((response) => response.data));
1062
1241
  }));
1063
- });
1242
+ }));
1064
1243
  }
1065
1244
  /**
1066
1245
  * Заголовки запроса: `BXRestSettings.headers`, поверх них — заголовки
@@ -1079,78 +1258,6 @@ class HttpBXServices extends HttpServices {
1079
1258
  }
1080
1259
  }
1081
1260
 
1082
- /**
1083
- * A Bitrix-level API error, e.g. `INSUFFICIENT_SCOPE` or `expired_token`.
1084
- *
1085
- * `.res()` fails with it whenever the portal answers with a Bitrix error and
1086
- * a non-2xx HTTP status — which is how Bitrix sends most of its errors. When
1087
- * the error comes in the body of a 2xx response, `.res()` resolves as
1088
- * `undefined` by default and fails with it only with
1089
- * `{ throwOnApiError: true }` — see the "Error handling" section of the README.
1090
- */
1091
- class BXRestApiError extends Error {
1092
- constructor(error, error_description, details) {
1093
- super(error_description || error || 'Bitrix API error', details?.cause !== undefined ? { cause: details.cause } : undefined);
1094
- this.error = error;
1095
- this.error_description = error_description;
1096
- this.name = 'BXRestApiError';
1097
- this.status = details?.status;
1098
- this.method = details?.method;
1099
- }
1100
- }
1101
- /**
1102
- * Recognises a Bitrix error body: REST v2 `{ error, error_description }` and
1103
- * REST v3 `{ error: { code, message } }`. Returns `undefined` for anything
1104
- * else — a success payload, an HTML page from a proxy, an empty body.
1105
- *
1106
- * An empty `error` still counts: Bitrix answers e.g. `crm.deal.get` for a
1107
- * missing ID with `{ error: '', error_description: 'Not found' }`.
1108
- */
1109
- function parseBXRestErrorBody(body) {
1110
- if (!body || typeof body !== 'object' || !('error' in body)) {
1111
- return undefined;
1112
- }
1113
- const { error, error_description } = body;
1114
- if (typeof error === 'string') {
1115
- return {
1116
- error,
1117
- error_description: typeof error_description === 'string' ? error_description : undefined
1118
- };
1119
- }
1120
- if (error && typeof error === 'object' && 'code' in error) {
1121
- const { code, message } = error;
1122
- return {
1123
- error: String(code),
1124
- error_description: typeof message === 'string' ? message : undefined
1125
- };
1126
- }
1127
- return undefined;
1128
- }
1129
- /**
1130
- * Turns an HTTP failure into a `BXRestApiError` when the portal answered with
1131
- * a Bitrix error body. Bitrix sends most errors with a non-2xx status
1132
- * (401 `expired_token`, 404 `ERROR_METHOD_NOT_FOUND`, 503
1133
- * `QUERY_LIMIT_EXCEEDED`, 400 "Not found" from `crm.*.get`, ...), and axios
1134
- * rejects on those.
1135
- *
1136
- * Returns `undefined` for network errors and for responses that are not a
1137
- * Bitrix error body, so the caller can keep its own handling for them.
1138
- */
1139
- function bxRestApiErrorFromHttp(err, method) {
1140
- if (!axios.isAxiosError(err) || !err.response) {
1141
- return undefined;
1142
- }
1143
- const parsed = parseBXRestErrorBody(err.response.data);
1144
- if (!parsed) {
1145
- return undefined;
1146
- }
1147
- return new BXRestApiError(parsed.error, parsed.error_description, {
1148
- status: err.response.status,
1149
- method,
1150
- cause: err
1151
- });
1152
- }
1153
-
1154
1261
  class NavvyPagBase {
1155
1262
  constructor(url, param, map = undefined, restApi = false) {
1156
1263
  this.url = url;
@@ -1212,7 +1319,15 @@ class NavvyPagBase {
1212
1319
  if (apiError) {
1213
1320
  throw apiError;
1214
1321
  }
1215
- throw new Error((err.error) ? err.error : err);
1322
+ // Сообщение прежнее, но исходная ошибка остаётся в cause — без неё
1323
+ // терялся HTTP-статус ответа не от Bitrix (например, пустого 401 от
1324
+ // бэкенд-прокси): он был только в тексте сообщения.
1325
+ const error = new Error((err.error) ? err.error : err, { cause: err });
1326
+ const authRefreshError = getAuthRefreshError(err);
1327
+ if (authRefreshError !== undefined) {
1328
+ Object.assign(error, { authRefreshError });
1329
+ }
1330
+ throw error;
1216
1331
  }));
1217
1332
  }
1218
1333
  }
@@ -20231,6 +20346,576 @@ class BXRestMap {
20231
20346
  }
20232
20347
  }
20233
20348
 
20349
+ /**
20350
+ * Обмен сообщениями между iframe приложения и порталом Bitrix24 — тот
20351
+ * транспорт, на котором построены команды BX24.js: страница приложения шлёт
20352
+ * команду родительскому окну портала через postMessage, портал отвечает
20353
+ * сообщением с тем же callbackId.
20354
+ *
20355
+ * ВНИМАНИЕ: протокол внутренний и недокументированный — Bitrix24 может
20356
+ * изменить его без предупреждения. Транспорт повторяет BX24.js
20357
+ * (https://api.bitrix24.com/api/v1/, скачан 2026-10-29, Last-Modified
20358
+ * 2022-03-17, sha256 945ad491df8ffb7490b89f4914a439b5a3f575c950c3480562815716a3e07f23)
20359
+ * и сверен с ответной стороной портала — `/bitrix/js/rest/applayout.js`
20360
+ * (Last-Modified 2024-05-15, sha256 3efd6ab57a4778493a26d10ae0987a82e19fc012ed9935f5c6afa78c4052fcb5).
20361
+ * Всё знание о протоколе — только в этом модуле (`services/frame`).
20362
+ */
20363
+ /**
20364
+ * Ошибка обращения к порталу из iframe. `code`:
20365
+ * - `NOT_IN_FRAME` — страница открыта не в iframe приложения Bitrix24;
20366
+ * - `TIMEOUT` — портал не ответил за отведённое время;
20367
+ * - `NO_AUTH` — портал ответил без токена;
20368
+ * - `BAD_RESPONSE` — ответ портала не удалось разобрать;
20369
+ * - `CANCELLED` — ожидание отменено через `signal` или новым вызовом того же диалога;
20370
+ * - `ACCESS_DENIED` — команда доступна только администратору портала.
20371
+ */
20372
+ class BXRestFrameError extends Error {
20373
+ constructor(code, message) {
20374
+ super(message);
20375
+ this.code = code;
20376
+ this.name = 'BXRestFrameError';
20377
+ }
20378
+ }
20379
+ /**
20380
+ * Параметры портала (DOMAIN, PROTOCOL, APP_SID) читаются в конструкторе —
20381
+ * так же, как это делает BX24.js, из `window.name`: портал задаёт имя
20382
+ * iframe приложения в виде `DOMAIN|PROTOCOL|APP_SID`. Если там их нет —
20383
+ * из query страницы (`?DOMAIN=…&PROTOCOL=…&APP_SID=…`), с которым портал
20384
+ * открывает обработчик приложения. Конструктор не бросает исключений:
20385
+ * вне iframe ошибку вернут сами команды.
20386
+ */
20387
+ class FrameTransport {
20388
+ constructor() {
20389
+ this.frame = readPortalFrame();
20390
+ }
20391
+ /** Домен портала из параметров фрейма — как `BX24.getDomain()`. */
20392
+ get domain() {
20393
+ return this.frame?.domain ?? '';
20394
+ }
20395
+ /**
20396
+ * Команда, на которую портал не отвечает (`reloadWindow`, `imCallTo`, ...).
20397
+ * Вне iframe бросает `BXRestFrameError` с кодом `NOT_IN_FRAME`.
20398
+ */
20399
+ post(command, params) {
20400
+ this.send(this.requireFrame(), command, params, '');
20401
+ }
20402
+ /** Команда с одним ответом портала. */
20403
+ request(command, params, options) {
20404
+ return new Promise((resolve, reject) => {
20405
+ let frame;
20406
+ try {
20407
+ frame = this.requireFrame();
20408
+ }
20409
+ catch (error) {
20410
+ reject(error);
20411
+ return;
20412
+ }
20413
+ const { timeout, onTimeout = 'reject', signal } = options;
20414
+ if (signal?.aborted) {
20415
+ reject(cancelled(command));
20416
+ return;
20417
+ }
20418
+ const callbackId = uniqId();
20419
+ let timer;
20420
+ const finish = () => {
20421
+ clearTimeout(timer);
20422
+ stopListening();
20423
+ signal?.removeEventListener('abort', onAbort);
20424
+ };
20425
+ const onAbort = () => {
20426
+ finish();
20427
+ reject(cancelled(command));
20428
+ };
20429
+ const stopListening = this.listen(frame, callbackId, payload => {
20430
+ finish();
20431
+ const answer = parsePayload(payload);
20432
+ if (answer.ok) {
20433
+ resolve(answer.value);
20434
+ }
20435
+ else {
20436
+ // В тексте ошибки — только команда, без тела ответа: в нём бывают токены
20437
+ reject(new BXRestFrameError('BAD_RESPONSE', `Bitrix24 answered ${command} with a payload that is not JSON.`));
20438
+ }
20439
+ });
20440
+ if (timeout !== false) {
20441
+ timer = setTimeout(() => {
20442
+ finish();
20443
+ if (onTimeout === 'resolve') {
20444
+ resolve(undefined);
20445
+ }
20446
+ else {
20447
+ reject(new BXRestFrameError('TIMEOUT', [
20448
+ `Bitrix24 did not answer ${command} within ${timeout} ms:`,
20449
+ 'the page is not opened inside a Bitrix24 portal iframe, or the portal could not process the command.'
20450
+ ].join(' ')));
20451
+ }
20452
+ }, timeout);
20453
+ }
20454
+ signal?.addEventListener('abort', onAbort, { once: true });
20455
+ this.send(frame, command, params, callbackId);
20456
+ });
20457
+ }
20458
+ /**
20459
+ * Команда, на которую портал отвечает многократно (`placementBindEvent`):
20460
+ * `handler` вызывается на каждый ответ. Возвращает функцию отписки —
20461
+ * она только перестаёт слушать: отменить подписку на стороне портала
20462
+ * протокол не позволяет. Вне iframe бросает `BXRestFrameError`.
20463
+ */
20464
+ subscribe(command, params, handler) {
20465
+ const frame = this.requireFrame();
20466
+ const callbackId = uniqId();
20467
+ const stopListening = this.listen(frame, callbackId, payload => {
20468
+ const answer = parsePayload(payload);
20469
+ if (answer.ok) {
20470
+ handler(answer.value);
20471
+ }
20472
+ });
20473
+ this.send(frame, command, params, callbackId);
20474
+ return stopListening;
20475
+ }
20476
+ requireFrame() {
20477
+ if (!this.frame) {
20478
+ throw new BXRestFrameError('NOT_IN_FRAME', 'Bitrix24 frame parameters (DOMAIN, APP_SID) were not found in window.name or in the URL query: '
20479
+ + 'the page is not opened as a Bitrix24 application.');
20480
+ }
20481
+ if (window.parent === window) {
20482
+ throw new BXRestFrameError('NOT_IN_FRAME', 'The page is not opened inside an iframe, so there is no Bitrix24 portal to send commands to.');
20483
+ }
20484
+ return this.frame;
20485
+ }
20486
+ /**
20487
+ * Формат — как у BX24.js. Обычно строка `команда:параметры:callbackId:APP_SID`
20488
+ * (портал разбирает её с конца, поэтому параметры и callbackId могут быть
20489
+ * пустыми); для команд с `:` в имени — объект.
20490
+ */
20491
+ send(frame, command, params, callbackId) {
20492
+ const message = command.includes(':')
20493
+ ? { method: command, params: params ? params : '', callback: callbackId, appSid: frame.appSid }
20494
+ : `${command}:${params ? JSON.stringify(params) : ''}:${callbackId}:${frame.appSid}`;
20495
+ window.parent.postMessage(message, frame.origin);
20496
+ }
20497
+ /** Ответ портала — строка `callbackId:JSON` (или `callbackId:`, если ответ пустой). */
20498
+ listen(frame, callbackId, onAnswer) {
20499
+ const portalWindow = window.parent;
20500
+ const onMessage = (event) => {
20501
+ // Ответ принимаем только от родительского окна и только с origin
20502
+ // портала — BX24.js проверяет лишь origin
20503
+ if (event.source !== portalWindow || event.origin !== frame.origin || typeof event.data !== 'string') {
20504
+ return;
20505
+ }
20506
+ const separator = event.data.indexOf(':');
20507
+ const id = separator < 0 ? event.data : event.data.slice(0, separator);
20508
+ if (id === callbackId) {
20509
+ onAnswer(separator < 0 ? '' : event.data.slice(separator + 1));
20510
+ }
20511
+ };
20512
+ window.addEventListener('message', onMessage);
20513
+ return () => window.removeEventListener('message', onMessage);
20514
+ }
20515
+ }
20516
+ function parsePayload(payload) {
20517
+ if (payload === '') {
20518
+ return { ok: true, value: undefined };
20519
+ }
20520
+ try {
20521
+ return { ok: true, value: JSON.parse(payload) };
20522
+ }
20523
+ catch {
20524
+ return { ok: false };
20525
+ }
20526
+ }
20527
+ function cancelled(command) {
20528
+ return new BXRestFrameError('CANCELLED', `Waiting for the Bitrix24 answer to ${command} was cancelled.`);
20529
+ }
20530
+ function readPortalFrame() {
20531
+ if (typeof window === 'undefined') {
20532
+ return undefined;
20533
+ }
20534
+ // BX24.js: window.name = 'DOMAIN|PROTOCOL|APP_SID'
20535
+ const [nameDomain, nameProtocol, nameAppSid] = (typeof window.name === 'string' ? window.name : '').split('|');
20536
+ const fromName = toPortalFrame(nameDomain, nameProtocol, nameAppSid);
20537
+ if (fromName) {
20538
+ return fromName;
20539
+ }
20540
+ try {
20541
+ const query = new URLSearchParams(window.location?.search ?? '');
20542
+ return toPortalFrame(query.get('DOMAIN'), query.get('PROTOCOL'), query.get('APP_SID'));
20543
+ }
20544
+ catch {
20545
+ return undefined;
20546
+ }
20547
+ }
20548
+ function toPortalFrame(rawDomain, rawProtocol, appSid) {
20549
+ // Как BX24.js: порт по умолчанию отрезается, PROTOCOL 1 — https
20550
+ const domain = (rawDomain ?? '').replace(/:(80|443)$/, '');
20551
+ if (!domain || !appSid) {
20552
+ return undefined;
20553
+ }
20554
+ const protocol = (Number.parseInt(rawProtocol ?? '', 10) || 0) ? 'https' : 'http';
20555
+ try {
20556
+ const url = new URL(`${protocol}://${domain}`);
20557
+ // В DOMAIN должен быть только хост (и порт) — никаких путей и логинов
20558
+ if (url.host !== domain.toLowerCase() || url.username || url.password) {
20559
+ return undefined;
20560
+ }
20561
+ return { domain, origin: url.origin, appSid };
20562
+ }
20563
+ catch {
20564
+ return undefined;
20565
+ }
20566
+ }
20567
+ function uniqId() {
20568
+ const bytes = new Uint8Array(16);
20569
+ globalThis.crypto.getRandomValues(bytes);
20570
+ return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
20571
+ }
20572
+
20573
+ const DEFAULT_TIMEOUT = 10000;
20574
+ /**
20575
+ * Команды порталу Bitrix24 из iframe приложения — без подключения BX24.js.
20576
+ * Те же команды и те же имена, что у BX24.js, но вместо колбэков —
20577
+ * промисы. Протокол внутренний и недокументированный, см. `transport.ts`.
20578
+ *
20579
+ * Создавайте один экземпляр на страницу и при старте приложения: параметры
20580
+ * портала читаются в конструкторе (до того, как роутер перепишет адрес), а
20581
+ * `firstRun` портал сообщает только в первом ответе на `getInitData`.
20582
+ *
20583
+ * Токены класс не логирует и никуда не сохраняет: `getAuth()` и
20584
+ * `refreshAuth()` каждый раз спрашивают портал, а в `init()` токенов нет.
20585
+ *
20586
+ * @example
20587
+ * const frame = new BXRestFrame()
20588
+ * const { lang, placement } = await frame.init()
20589
+ * await frame.fitWindow()
20590
+ * const user = await frame.selectUser()
20591
+ */
20592
+ class BXRestFrame {
20593
+ constructor(options = {}) {
20594
+ this.transport = new FrameTransport();
20595
+ // Открытый диалог каждого вида — новый вызов того же диалога отменяет прошлый
20596
+ this.dialogs = new Map();
20597
+ /** Мессенджер — как `BX24.im`. Портал на эти команды не отвечает. */
20598
+ this.im = {
20599
+ /** Позвонить сотруднику — `BX24.im.callTo()`. */
20600
+ callTo: async (userId, video) => {
20601
+ this.transport.post('imCallTo', { userId, video });
20602
+ },
20603
+ /** Позвонить на телефон — `BX24.im.phoneTo()`. */
20604
+ phoneTo: async (phone) => {
20605
+ this.transport.post('imPhoneTo', { phone });
20606
+ },
20607
+ /** Открыть чат — `BX24.im.openMessenger()`. */
20608
+ openMessenger: async (dialogId) => {
20609
+ this.transport.post('imOpenMessenger', { dialogId });
20610
+ },
20611
+ /** Открыть историю чата — `BX24.im.openHistory()`. */
20612
+ openHistory: async (dialogId) => {
20613
+ this.transport.post('imOpenHistory', { dialogId });
20614
+ },
20615
+ };
20616
+ /** Встраивание — как `BX24.placement`. */
20617
+ this.placement = {
20618
+ /** Код и параметры встраивания — `BX24.placement.info()`. */
20619
+ info: async () => {
20620
+ const { placement, placementOptions } = await this.init();
20621
+ return { placement, options: placementOptions };
20622
+ },
20623
+ /** Команды и события текущего встраивания — `BX24.placement.getInterface()`. */
20624
+ getInterface: async () => {
20625
+ return await this.transport.request('getInterface', null, { timeout: this.timeout });
20626
+ },
20627
+ /**
20628
+ * Команда встраивания — `BX24.placement.call()`. Набор команд зависит от
20629
+ * встраивания (см. `getInterface()`). Не на все команды портал отвечает:
20630
+ * без ответа за `timeout` промис завершается с `undefined`.
20631
+ */
20632
+ call: async (command, params, options = {}) => {
20633
+ return this.transport.request(command, params, {
20634
+ timeout: options.timeout ?? this.timeout,
20635
+ onTimeout: 'resolve',
20636
+ });
20637
+ },
20638
+ /**
20639
+ * Подписка на событие встраивания — `BX24.placement.bindEvent()`.
20640
+ * `handler` вызывается при каждом событии. Возвращает функцию отписки:
20641
+ * она только перестаёт вызывать `handler` — отменить подписку на
20642
+ * стороне портала протокол не позволяет. Вне iframe бросает
20643
+ * `BXRestFrameError` с кодом `NOT_IN_FRAME`.
20644
+ */
20645
+ bindEvent: (event, handler) => {
20646
+ return this.transport.subscribe('placementBindEvent', { event }, handler);
20647
+ },
20648
+ };
20649
+ /** Настройки приложения для текущего пользователя — как `BX24.userOption`. */
20650
+ this.userOption = {
20651
+ get: async (name) => (await this.init()).userOptions[name],
20652
+ set: async (name, value) => {
20653
+ const init = await this.init();
20654
+ init.userOptions[name] = value;
20655
+ await this.ack('setUserOption', { name, value });
20656
+ },
20657
+ };
20658
+ /** Настройки приложения для всего портала — как `BX24.appOption`. */
20659
+ this.appOption = {
20660
+ get: async (name) => (await this.init()).appOptions[name],
20661
+ /**
20662
+ * Менять их может только администратор портала: остальным — ошибка
20663
+ * `ACCESS_DENIED` (BX24.js в этом случае молча ничего не делает).
20664
+ * Возвращает настройки приложения после сохранения.
20665
+ */
20666
+ set: async (name, value) => {
20667
+ const init = await this.init();
20668
+ if (!init.isAdmin) {
20669
+ throw new BXRestFrameError('ACCESS_DENIED', 'Only a Bitrix24 administrator can change application options.');
20670
+ }
20671
+ init.appOptions[name] = value;
20672
+ return await this.transport.request('setAppOption', { name, value }, { timeout: this.timeout });
20673
+ },
20674
+ };
20675
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
20676
+ }
20677
+ /**
20678
+ * Данные запуска приложения — аналог `BX24.init()`: язык, домен, права,
20679
+ * встраивание, настройки, признаки установки. Запрашиваются у портала
20680
+ * один раз, дальше берутся из памяти. Токенов здесь нет — см. `getAuth()`.
20681
+ */
20682
+ init() {
20683
+ if (this.initValue) {
20684
+ return Promise.resolve(this.initValue);
20685
+ }
20686
+ if (!this.initRequest) {
20687
+ this.initRequest = this.getInitData()
20688
+ .then(() => this.initValue)
20689
+ .finally(() => {
20690
+ this.initRequest = undefined;
20691
+ });
20692
+ }
20693
+ return this.initRequest;
20694
+ }
20695
+ /**
20696
+ * Текущая авторизация приложения — аналог `BX24.getAuth()`. Каждый вызов —
20697
+ * новый запрос `getInitData` к порталу.
20698
+ *
20699
+ * `expires_in` портал считает от срока жизни токена в момент выдачи, а не
20700
+ * от остатка: если страница открыта давно, значение будет завышено (так
20701
+ * же и у BX24.js). Для свежего токена используйте `refreshAuth()`.
20702
+ */
20703
+ async getAuth() {
20704
+ const data = await this.getInitData();
20705
+ return this.toAuth('getInitData', data, String(data['MEMBER_ID'] || ''));
20706
+ }
20707
+ /**
20708
+ * Новая авторизация приложения — аналог `BX24.refreshAuth()`: портал
20709
+ * выпускает новую пару `access_token`/`refresh_token`. Если портал не может
20710
+ * выдать токен, он показывает `alert` «Unable to get new token!» и не
20711
+ * отвечает — тогда ошибка `TIMEOUT`.
20712
+ */
20713
+ async refreshAuth() {
20714
+ // member_id приходит только в getInitData — refreshAuth его не возвращает
20715
+ const { memberId } = await this.init();
20716
+ const data = asObject('refreshAuth', await this.transport.request('refreshAuth', {}, { timeout: this.timeout }));
20717
+ return this.toAuth('refreshAuth', data, memberId);
20718
+ }
20719
+ /**
20720
+ * Завершить установку — аналог `BX24.installFinish()`: портал отмечает
20721
+ * приложение установленным и перезагружает страницу. Делает что-то только
20722
+ * на странице установщика (`init().install`), иначе — ничего, как и BX24.js.
20723
+ */
20724
+ async installFinish() {
20725
+ if ((await this.init()).install) {
20726
+ this.transport.post('setInstallFinish', {});
20727
+ }
20728
+ }
20729
+ /**
20730
+ * Отметить, что первый запуск у этого пользователя пройден, — то, что
20731
+ * BX24.js делает после последнего обработчика `BX24.install()`. Без этого
20732
+ * `init().firstRun` останется `true` и при следующих запусках. Делает
20733
+ * что-то только при первом запуске.
20734
+ */
20735
+ async firstRunFinish() {
20736
+ const init = await this.init();
20737
+ if (init.firstRun) {
20738
+ await this.ack('setInstall', { install: true });
20739
+ init.firstRun = false;
20740
+ }
20741
+ }
20742
+ /** Задать размер фрейма — `BX24.resizeWindow()`. Возвращает размер, который выставил портал. */
20743
+ async resizeWindow(width, height) {
20744
+ const w = Number.parseInt(String(width), 10);
20745
+ const h = Number.parseInt(String(height), 10);
20746
+ if (!(w > 0 && h > 0)) {
20747
+ throw new TypeError('resizeWindow: width and height must be positive numbers.');
20748
+ }
20749
+ return await this.ack('resizeWindow', { width: w, height: h });
20750
+ }
20751
+ /** Подогнать высоту фрейма под содержимое — `BX24.fitWindow()`. */
20752
+ async fitWindow() {
20753
+ const root = document.documentElement;
20754
+ const height = Math.max(root.scrollHeight, root.offsetHeight);
20755
+ return await this.ack('resizeWindow', { width: '100%', height });
20756
+ }
20757
+ /**
20758
+ * Перезагрузить страницу портала с приложением — `BX24.reloadWindow()`.
20759
+ * Портал на эту команду не отвечает.
20760
+ */
20761
+ async reloadWindow() {
20762
+ this.transport.post('reloadWindow', {});
20763
+ }
20764
+ /** Заголовок страницы портала — `BX24.setTitle()`. */
20765
+ async setTitle(title) {
20766
+ return await this.ack('setTitle', { title: String(title) });
20767
+ }
20768
+ /** Прокрутить страницу портала — `BX24.scrollParentWindow()`. */
20769
+ async scrollParentWindow(scroll) {
20770
+ const value = Number.parseInt(String(scroll), 10);
20771
+ if (Number.isNaN(value)) {
20772
+ throw new TypeError('scrollParentWindow: scroll must be a number.');
20773
+ }
20774
+ return await this.ack('setScroll', { scroll: value });
20775
+ }
20776
+ /**
20777
+ * Диалог выбора одного сотрудника — `BX24.selectUser()`. Ждёт выбора без
20778
+ * ограничения по времени: если пользователь закрыл диалог, портал не
20779
+ * отвечает вовсе. Отменить ожидание — `signal` или новый вызов
20780
+ * `selectUser()`/`selectUsers()` (ошибка `CANCELLED`).
20781
+ */
20782
+ selectUser(options = {}) {
20783
+ return this.dialog('user', 'selectUser', { title: options.title ?? '', mult: false }, options.signal);
20784
+ }
20785
+ /** Диалог выбора нескольких сотрудников — `BX24.selectUsers()`. Ожидание — как у `selectUser()`. */
20786
+ selectUsers(options = {}) {
20787
+ return this.dialog('user', 'selectUser', { title: options.title ?? '', mult: true }, options.signal);
20788
+ }
20789
+ /**
20790
+ * Диалог выбора прав доступа — `BX24.selectAccess()`. `value` — коды,
20791
+ * которые нельзя выбрать. Ожидание — как у `selectUser()`.
20792
+ */
20793
+ selectAccess(options = {}) {
20794
+ return this.dialog('access', 'selectAccess', { value: options.value ?? [] }, options.signal);
20795
+ }
20796
+ /** Диалог выбора объектов CRM — `BX24.selectCRM()`. Ожидание — как у `selectUser()`. */
20797
+ selectCRM(options = {}) {
20798
+ const { entityType, multiple, value, signal } = options;
20799
+ return this.dialog('crm', 'selectCRM', { entityType, multiple, value }, signal);
20800
+ }
20801
+ /**
20802
+ * Открыть приложение в слайдере — `BX24.openApplication()`. Промис
20803
+ * завершается, когда слайдер закрыли. `settings` — настройки слайдера
20804
+ * (уходят как `bx24_<ключ>`, как в BX24.js).
20805
+ */
20806
+ async openApplication(params = {}, settings = {}, options = {}) {
20807
+ const message = { ...params };
20808
+ for (const [key, value] of Object.entries(settings)) {
20809
+ message[`bx24_${key}`] = value;
20810
+ }
20811
+ await this.transport.request('openApplication', message, { timeout: false, signal: options.signal });
20812
+ }
20813
+ /**
20814
+ * Открыть страницу портала в слайдере — `BX24.openPath()`. Промис
20815
+ * завершается, когда слайдер закрыли (`{ result: 'close' }`), или сразу,
20816
+ * если путь не открылся (`{ result: 'error', errorCode }`).
20817
+ */
20818
+ async openPath(path, options = {}) {
20819
+ return await this.transport.request('openPath', { path }, { timeout: false, signal: options.signal });
20820
+ }
20821
+ /**
20822
+ * Закрыть слайдер с приложением — `BX24.closeApplication()`. Если
20823
+ * приложение открыто не в слайдере, ничего не происходит. Узнать о
20824
+ * закрытии может открывший слайдер — через `openApplication()`.
20825
+ */
20826
+ async closeApplication() {
20827
+ this.transport.post('closeApplication', null);
20828
+ }
20829
+ /**
20830
+ * Команда, на которую портал отвечает подтверждением. В некоторых
20831
+ * встраиваниях портал на такие команды не отвечает (так считает и
20832
+ * b24jssdk) — тогда промис завершается с `undefined`, а не ошибкой.
20833
+ */
20834
+ ack(command, params) {
20835
+ return this.transport.request(command, params, { timeout: this.timeout, onTimeout: 'resolve' });
20836
+ }
20837
+ /**
20838
+ * Диалоги ждут пользователя без таймаута. Портал держит один обработчик
20839
+ * на диалог (у выбора сотрудников он общий для `selectUser` и
20840
+ * `selectUsers`) и на прошлый вызов уже не ответит — поэтому новый вызов
20841
+ * отменяет ожидание прошлого.
20842
+ */
20843
+ dialog(kind, command, params, signal) {
20844
+ this.dialogs.get(kind)?.abort();
20845
+ const controller = new AbortController();
20846
+ this.dialogs.set(kind, controller);
20847
+ const onAbort = () => controller.abort();
20848
+ if (signal?.aborted) {
20849
+ controller.abort();
20850
+ }
20851
+ signal?.addEventListener('abort', onAbort, { once: true });
20852
+ return this.transport.request(command, params, { timeout: false, signal: controller.signal })
20853
+ .finally(() => {
20854
+ signal?.removeEventListener('abort', onAbort);
20855
+ if (this.dialogs.get(kind) === controller) {
20856
+ this.dialogs.delete(kind);
20857
+ }
20858
+ });
20859
+ }
20860
+ async getInitData() {
20861
+ const data = asObject('getInitData', await this.transport.request('getInitData', null, { timeout: this.timeout }));
20862
+ // firstRun портал отдаёт только в первом ответе за загрузку страницы —
20863
+ // поэтому запоминаем данные первого ответа, кто бы его ни запросил
20864
+ if (!this.initValue) {
20865
+ this.initValue = this.toInit(data);
20866
+ }
20867
+ return data;
20868
+ }
20869
+ toInit(data) {
20870
+ return {
20871
+ // Как BX24.js: домен — из параметров фрейма, DOMAIN ответа — запасной
20872
+ domain: this.transport.domain || String(data['DOMAIN'] || ''),
20873
+ lang: String(data['LANG'] || ''),
20874
+ path: String(data['PATH'] || ''),
20875
+ https: Number.parseInt(String(data['PROTOCOL']), 10) === 1,
20876
+ memberId: String(data['MEMBER_ID'] || ''),
20877
+ isAdmin: !!data['IS_ADMIN'],
20878
+ install: !!data['INSTALL'],
20879
+ firstRun: !!data['FIRST_RUN'],
20880
+ placement: String(data['PLACEMENT'] || 'DEFAULT'),
20881
+ placementOptions: data['PLACEMENT_OPTIONS'],
20882
+ userOptions: toRecord(data['USER_OPTIONS']),
20883
+ appOptions: toRecord(data['APP_OPTIONS']),
20884
+ };
20885
+ }
20886
+ toAuth(command, data, memberId) {
20887
+ const { AUTH_ID, REFRESH_ID, AUTH_EXPIRES } = data;
20888
+ if (!AUTH_ID || typeof AUTH_ID !== 'string') {
20889
+ throw new BXRestFrameError('NO_AUTH', `Bitrix24 answered ${command} without an access token.`);
20890
+ }
20891
+ // AUTH_EXPIRES — срок жизни токена в секундах. BX24.js переводит его в
20892
+ // момент истечения в миллисекундах — так же, как здесь.
20893
+ const expiresIn = Number.parseInt(String(AUTH_EXPIRES), 10);
20894
+ if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
20895
+ throw new BXRestFrameError('BAD_RESPONSE', `Bitrix24 answered ${command} without a usable AUTH_EXPIRES.`);
20896
+ }
20897
+ return {
20898
+ access_token: AUTH_ID,
20899
+ refresh_token: typeof REFRESH_ID === 'string' ? REFRESH_ID : '',
20900
+ expires_in: Date.now() + expiresIn * 1000,
20901
+ domain: this.transport.domain,
20902
+ member_id: memberId,
20903
+ };
20904
+ }
20905
+ }
20906
+ function asObject(command, data) {
20907
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
20908
+ throw new BXRestFrameError('BAD_RESPONSE', `Bitrix24 answered ${command} with a payload that is not an object.`);
20909
+ }
20910
+ return data;
20911
+ }
20912
+ // Пустые настройки PHP отдаёт массивом `[]`
20913
+ function toRecord(value) {
20914
+ return (value && typeof value === 'object' && !Array.isArray(value))
20915
+ ? { ...value }
20916
+ : {};
20917
+ }
20918
+
20234
20919
  /**
20235
20920
  * Public API Surface of bx-rest
20236
20921
  */
@@ -20239,5 +20924,5 @@ class BXRestMap {
20239
20924
  * Generated bundle index. Do not edit.
20240
20925
  */
20241
20926
 
20242
- export { base as BXBaseServices, BXRestApiError, BXRestMap, BXRestMapResult, BXRestNavvy, HttpBXServices as BXRestRequest, BXRestSettings, HttpNavvyServices, Navvy, SessionKeyError, SessionKeyServices, instanceOfiBXRestAnswerSuccess as isBXRestAnswerSuccess, partNameMethods$1 as partNameMethods };
20927
+ export { base as BXBaseServices, BXRestApiError, BXRestFrame, BXRestFrameError, BXRestMap, BXRestMapResult, BXRestNavvy, HttpBXServices as BXRestRequest, BXRestSettings, HttpNavvyServices, Navvy, SessionKeyError, SessionKeyServices, getAuthRefreshError, instanceOfiBXRestAnswerSuccess as isBXRestAnswerSuccess, isBXRestAuthError, partNameMethods$1 as partNameMethods };
20243
20928
  //# sourceMappingURL=bx-rest.mjs.map