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.
Files changed (36) hide show
  1. package/CHANGELOG.md +83 -0
  2. package/README.en.md +207 -3
  3. package/README.md +207 -3
  4. package/esm2022/functions/errors.mjs +19 -2
  5. package/esm2022/public-api.mjs +6 -2
  6. package/esm2022/request-navvy/crm/item.mjs +1 -1
  7. package/esm2022/request-navvy/crm/type.mjs +1 -1
  8. package/esm2022/services/frame/frame.mjs +347 -0
  9. package/esm2022/services/frame/transport.mjs +224 -0
  10. package/esm2022/services/frame/types.mjs +2 -0
  11. package/esm2022/services/http/HttpBX.mjs +14 -6
  12. package/esm2022/services/http/authRefresh.mjs +89 -0
  13. package/esm2022/services/navvy/extends/navvy-pag-base.mjs +28 -5
  14. package/esm2022/services/navvy/navvy-alternative-pagination-navigation.mjs +2 -2
  15. package/esm2022/services/navvy/navvy-pag-nav-result-key-object.mjs +6 -7
  16. package/esm2022/services/navvy/navvy-pag-nav-result-key.mjs +6 -3
  17. package/esm2022/services/navvy/navvy-pag-nav-tasks.mjs +5 -4
  18. package/esm2022/services/navvy/navvy-pag-nav-with-useless-key.mjs +2 -2
  19. package/esm2022/services/navvy/navvy-pag.mjs +2 -2
  20. package/esm2022/typification/rest/crm/universal.mjs +1 -1
  21. package/esm2022/typification/settings.mjs +1 -1
  22. package/fesm2022/bx-rest.mjs +801 -97
  23. package/fesm2022/bx-rest.mjs.map +1 -1
  24. package/functions/errors.d.ts +18 -0
  25. package/package.json +4 -1
  26. package/public-api.d.ts +5 -1
  27. package/request-navvy/crm/item.d.ts +6 -6
  28. package/request-navvy/crm/type.d.ts +6 -6
  29. package/services/frame/frame.d.ts +191 -0
  30. package/services/frame/transport.d.ts +75 -0
  31. package/services/frame/types.d.ts +123 -0
  32. package/services/http/authRefresh.d.ts +19 -0
  33. package/services/navvy/extends/navvy-pag-base.d.ts +6 -0
  34. package/services/navvy/navvy-pag-nav-tasks.d.ts +3 -0
  35. package/typification/rest/crm/universal.d.ts +116 -0
  36. package/typification/settings.d.ts +17 -0
@@ -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,179 @@ 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
+ * `.resAll()` always fails with it on an error in the body of a 2xx response.
956
+ */
957
+ class BXRestApiError extends Error {
958
+ constructor(error, error_description, details) {
959
+ super(error_description || error || 'Bitrix API error', details?.cause !== undefined ? { cause: details.cause } : undefined);
960
+ this.error = error;
961
+ this.error_description = error_description;
962
+ this.name = 'BXRestApiError';
963
+ this.status = details?.status;
964
+ this.method = details?.method;
965
+ this.authRefreshError = details?.authRefreshError;
966
+ }
967
+ }
968
+ /**
969
+ * Recognises a Bitrix error body: REST v2 `{ error, error_description }` and
970
+ * REST v3 `{ error: { code, message } }`. Returns `undefined` for anything
971
+ * else — a success payload, an HTML page from a proxy, an empty body.
972
+ *
973
+ * An empty `error` still counts: Bitrix answers e.g. `crm.deal.get` for a
974
+ * missing ID with `{ error: '', error_description: 'Not found' }`.
975
+ */
976
+ function parseBXRestErrorBody(body) {
977
+ if (!body || typeof body !== 'object' || !('error' in body)) {
978
+ return undefined;
979
+ }
980
+ const { error, error_description } = body;
981
+ if (typeof error === 'string') {
982
+ return {
983
+ error,
984
+ error_description: typeof error_description === 'string' ? error_description : undefined
985
+ };
986
+ }
987
+ if (error && typeof error === 'object' && 'code' in error) {
988
+ const { code, message } = error;
989
+ return {
990
+ error: String(code),
991
+ error_description: typeof message === 'string' ? message : undefined
992
+ };
993
+ }
994
+ return undefined;
995
+ }
996
+ /**
997
+ * Turns an HTTP failure into a `BXRestApiError` when the portal answered with
998
+ * a Bitrix error body. Bitrix sends most errors with a non-2xx status
999
+ * (401 `expired_token`, 404 `ERROR_METHOD_NOT_FOUND`, 503
1000
+ * `QUERY_LIMIT_EXCEEDED`, 400 "Not found" from `crm.*.get`, ...), and axios
1001
+ * rejects on those.
1002
+ *
1003
+ * Returns `undefined` for network errors and for responses that are not a
1004
+ * Bitrix error body, so the caller can keep its own handling for them.
1005
+ */
1006
+ function bxRestApiErrorFromHttp(err, method) {
1007
+ if (!axios.isAxiosError(err) || !err.response) {
1008
+ return undefined;
1009
+ }
1010
+ const parsed = parseBXRestErrorBody(err.response.data);
1011
+ if (!parsed) {
1012
+ return undefined;
1013
+ }
1014
+ return new BXRestApiError(parsed.error, parsed.error_description, {
1015
+ status: err.response.status,
1016
+ method,
1017
+ cause: err,
1018
+ authRefreshError: getAuthRefreshError(err)
1019
+ });
1020
+ }
1021
+
1022
+ /**
1023
+ * Коды ошибок Bitrix, которые лечатся продлением токена. `expired_token` —
1024
+ * единственный системный код, для которого документация Bitrix24 советует
1025
+ * "обновить токены и повторить вызов". `invalid_token` в списке системных
1026
+ * ошибок документации нет, но так же считает официальный `@bitrix24/b24jssdk`.
1027
+ *
1028
+ * Остальные системные ошибки тоже приходят со статусом 401
1029
+ * (`insufficient_scope`, `NO_AUTH_FOUND`, `ACCESS_DENIED`,
1030
+ * `INVALID_CREDENTIALS`, `user_access_error`, ...), но новый токен их
1031
+ * не исправит — поэтому смотрим на код, а не только на статус.
1032
+ */
1033
+ const AUTH_ERROR_CODES = ['expired_token', 'invalid_token'];
1034
+ /**
1035
+ * Условие продления по умолчанию (`BXRestSettings.auth.isAuthError`):
1036
+ * - ответ с ошибкой Bitrix `expired_token` или `invalid_token` — при любом статусе;
1037
+ * - ответ 401 без тела ошибки Bitrix — например, пустой ответ своего
1038
+ * бэкенд-прокси, у которого нет действующего токена.
1039
+ *
1040
+ * Ответ 401 с другим кодом Bitrix ошибкой авторизации не считается.
1041
+ */
1042
+ function isBXRestAuthError(error) {
1043
+ if (!axios.isAxiosError(error) || !error.response) {
1044
+ return false;
1045
+ }
1046
+ const parsed = parseBXRestErrorBody(error.response.data);
1047
+ if (parsed) {
1048
+ return AUTH_ERROR_CODES.includes(parsed.error);
1049
+ }
1050
+ return error.response.status === 401;
1051
+ }
1052
+ // Общее на всю библиотеку состояние продления — как и сами BXRestSettings.
1053
+ // pendingRefresh — идущее сейчас продление, его ждут все упавшие запросы;
1054
+ // refreshCount — сколько продлений уже завершилось успешно.
1055
+ let pendingRefresh;
1056
+ let refreshCount = 0;
1057
+ function sharedRefresh(refresh) {
1058
+ if (!pendingRefresh) {
1059
+ pendingRefresh = Promise.resolve()
1060
+ // Через then, чтобы синхронное исключение в refresh() тоже стало
1061
+ // отклонённым промисом, а не вылетело из catchError
1062
+ .then(() => refresh())
1063
+ .then(() => {
1064
+ refreshCount++;
1065
+ })
1066
+ .finally(() => {
1067
+ pendingRefresh = undefined;
1068
+ });
1069
+ }
1070
+ return pendingRefresh;
1071
+ }
1072
+ /**
1073
+ * Повтор запроса после продления авторизации (`BXRestSettings.auth.refresh`).
1074
+ *
1075
+ * `request$` должен быть "холодным" и читать настройки при подписке (как
1076
+ * `defer()` в HttpBXServices): повтор — это повторная подписка, поэтому
1077
+ * токен, адрес и заголовки берутся уже новые. На второй попытке ошибка не
1078
+ * перехватывается — запрос повторяется не больше одного раза.
1079
+ */
1080
+ function withAuthRefresh(request$) {
1081
+ return defer(() => {
1082
+ const { refresh, isAuthError = isBXRestAuthError } = BXRestSettings.value.auth;
1083
+ if (!refresh) {
1084
+ return request$;
1085
+ }
1086
+ const refreshCountAtStart = refreshCount;
1087
+ return request$.pipe(catchError((error) => {
1088
+ if (!isAuthError(error)) {
1089
+ return throwError(() => error);
1090
+ }
1091
+ // Пока этот запрос был в пути, токен уже продлили по ошибке другого
1092
+ // запроса — повторяем без нового продления
1093
+ const refreshed = (refreshCount !== refreshCountAtStart)
1094
+ ? Promise.resolve()
1095
+ : sharedRefresh(refresh);
1096
+ return from(refreshed).pipe(catchError((refreshError) => {
1097
+ if (error && typeof error === 'object') {
1098
+ error.authRefreshError = refreshError;
1099
+ }
1100
+ return throwError(() => error);
1101
+ }), switchMap(() => request$));
1102
+ }));
1103
+ });
1104
+ }
1105
+
933
1106
  class HttpBXServices extends HttpServices {
934
1107
  /**
935
1108
  * Не считайте этот метод образцом/финальным API.
@@ -1002,7 +1175,13 @@ class HttpBXServices extends HttpServices {
1002
1175
  // геттером в SessionKeyServices настройки читались бы в момент вызова
1003
1176
  // .res(), а не в момент subscribe()/firstValueFrom(), что и обещано
1004
1177
  // в README как поведение "ленивого" REST-вызова.
1005
- return defer(() => {
1178
+ //
1179
+ // withAuthRefresh() повторяет запрос после BXRestSettings.auth.refresh —
1180
+ // повторной подпиской на этот же defer(), поэтому повтор заново читает
1181
+ // токен, адрес и заголовки. Через httpPost/httpGet идут все запросы
1182
+ // библиотеки (post/get, batch, все виды Navvy-запросов), так что повтор
1183
+ // встроен только здесь.
1184
+ return withAuthRefresh(defer(() => {
1006
1185
  const paramsClone = clone(params);
1007
1186
  const key = this.session.getKeyAuth();
1008
1187
  const auth = this.session.getAuthParams();
@@ -1032,14 +1211,15 @@ class HttpBXServices extends HttpServices {
1032
1211
  headers: this.buildHeaders(headers, { 'Content-Type': 'application/json' }),
1033
1212
  }).then((response) => response.data));
1034
1213
  }));
1035
- });
1214
+ }));
1036
1215
  }
1037
1216
  httpGet(url, params = {}, settings, headers) {
1038
1217
  // Та же причина, что и у httpPost выше: откладываем чтение BXRestSettings
1039
1218
  // до подписки, чтобы `get()` вёл себя так же "лениво", как и `post()`
1040
1219
  // (симметрия, которой не было до этого — GET резолвил настройки в момент
1041
- // вызова, а не в момент subscribe()).
1042
- return defer(() => {
1220
+ // вызова, а не в момент subscribe()). Повтор после продления
1221
+ // авторизации — как в httpPost.
1222
+ return withAuthRefresh(defer(() => {
1043
1223
  const paramsClone = clone(params);
1044
1224
  const auth = this.session.getAuthParams();
1045
1225
  const checkIsOn = this.session.getCheckAuthParamsIsOn();
@@ -1060,7 +1240,7 @@ class HttpBXServices extends HttpServices {
1060
1240
  headers: this.buildHeaders(headers),
1061
1241
  }).then((response) => response.data));
1062
1242
  }));
1063
- });
1243
+ }));
1064
1244
  }
1065
1245
  /**
1066
1246
  * Заголовки запроса: `BXRestSettings.headers`, поверх них — заголовки
@@ -1079,78 +1259,6 @@ class HttpBXServices extends HttpServices {
1079
1259
  }
1080
1260
  }
1081
1261
 
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
1262
  class NavvyPagBase {
1155
1263
  constructor(url, param, map = undefined, restApi = false) {
1156
1264
  this.url = url;
@@ -1196,8 +1304,7 @@ class NavvyPagBase {
1196
1304
  const method = this.http.getNameMethod(this.url);
1197
1305
  return request.pipe(map(v => {
1198
1306
  if (options?.throwOnApiError && v && !instanceOfiBXRestAnswerSuccess(v)) {
1199
- const parsed = parseBXRestErrorBody(v);
1200
- throw new BXRestApiError(parsed ? parsed.error : v.error, parsed ? parsed.error_description : v.error_description, { method });
1307
+ throw this.apiErrorFromBody(v);
1201
1308
  }
1202
1309
  return BXRestMapResult(v);
1203
1310
  }), catchError((err) => {
@@ -1212,9 +1319,33 @@ 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
  }
1333
+ /**
1334
+ * Первая страница для `.resAll()`. Ошибка Bitrix в теле ответа 2xx становится `BXRestApiError`:
1335
+ * страниц в таком ответе нет, и без этого `.resAll()` завершился бы безликим `EmptyError`.
1336
+ */
1337
+ resFirstPage() {
1338
+ return this.resVanilla().pipe(map(v => {
1339
+ if (v && !instanceOfiBXRestAnswerSuccess(v)) {
1340
+ throw this.apiErrorFromBody(v);
1341
+ }
1342
+ return v;
1343
+ }));
1344
+ }
1345
+ apiErrorFromBody(v) {
1346
+ const parsed = parseBXRestErrorBody(v);
1347
+ return new BXRestApiError(parsed ? parsed.error : v.error, parsed ? parsed.error_description : v.error_description, { method: this.http.getNameMethod(this.url) });
1348
+ }
1218
1349
  }
1219
1350
 
1220
1351
  class NavvySimple extends NavvyPagBase {
@@ -3165,7 +3296,7 @@ class NavvyAlterPagNav extends NavvyPagBase {
3165
3296
  resultAllEnd() {
3166
3297
  let BXRestClass = new BXRest();
3167
3298
  // return this.mapForVanillaEnd(this.func, this.param)
3168
- return this.resVanilla().pipe(mergeMap(items => {
3299
+ return this.resFirstPage().pipe(mergeMap(items => {
3169
3300
  if (items && instanceOfiBXRestAnswerSuccess(items)) {
3170
3301
  if (items.total && items.next) {
3171
3302
  const count = [...Array(Math.floor(items.total / this.pageSize)).keys()];
@@ -3229,7 +3360,7 @@ class NavvyPagNavWithUselessKey extends NavvyPagBase {
3229
3360
  }
3230
3361
  resAll() {
3231
3362
  let BXRestClass = new BXRest();
3232
- return this.resVanilla().pipe(mergeMap(items => {
3363
+ return this.resFirstPage().pipe(mergeMap(items => {
3233
3364
  if (items && instanceOfiBXRestAnswerSuccess(items)) {
3234
3365
  if (items.total && items.next) {
3235
3366
  const count = [...Array(Math.floor(items.total / this.pageSize)).keys()];
@@ -3297,8 +3428,9 @@ class NavvyPagNavWithUselessKey extends NavvyPagBase {
3297
3428
  }
3298
3429
 
3299
3430
  class NavvyPagNavTasks extends NavvyPagBase {
3300
- constructor() {
3301
- super(...arguments);
3431
+ constructor(url, param, map, restApi) {
3432
+ // Без своего map результат — массив задач, как и обещают типы `.res()` и `.resAll()`.
3433
+ super(url, param, map ?? (result => (result?.tasks ?? [])), restApi);
3302
3434
  this.save = [];
3303
3435
  }
3304
3436
  resAll() {
@@ -3313,7 +3445,7 @@ class NavvyPagNavTasks extends NavvyPagBase {
3313
3445
  }
3314
3446
  resultAllEnd() {
3315
3447
  let BXRestClass = new BXRest();
3316
- return this.resVanilla().pipe(mergeMap(items => {
3448
+ return this.resFirstPage().pipe(mergeMap(items => {
3317
3449
  if (items && instanceOfiBXRestAnswerSuccess(items)) {
3318
3450
  this.save = [];
3319
3451
  // TODO: проверить будет ли быстрее работать метод в случаях когда total < 100 (то есть можно обойтись двумя прямыми запросами)
@@ -3374,7 +3506,7 @@ class NavvyPag extends NavvyPagBase {
3374
3506
  }
3375
3507
  resAll() {
3376
3508
  let BXRestClass = new BXRest();
3377
- return this.resVanilla().pipe(mergeMap(items => {
3509
+ return this.resFirstPage().pipe(mergeMap(items => {
3378
3510
  if (items && instanceOfiBXRestAnswerSuccess(items)) {
3379
3511
  this.save = [];
3380
3512
  // TODO: проверить будет ли быстрее работать метод в случаях когда total < 100 (то есть можно обойтись двумя прямыми запросами)
@@ -3432,7 +3564,10 @@ class NavvyPag extends NavvyPagBase {
3432
3564
 
3433
3565
  class NavvyPagNavResultKey extends NavvyPagBase {
3434
3566
  constructor(url, param, key, map) {
3435
- super(url, param, map);
3567
+ // Без своего map результат — массив по ключу, как и обещают типы `.res()` и `.resAll()`.
3568
+ // Иначе `.res()` отдал бы весь объект `{ items: [...] }`, а `.resAll()` упал бы на нём
3569
+ // при 50 записях и меньше: первая страница разворачивается через `save.push(...v)`.
3570
+ super(url, param, map ?? (result => (result?.[key] ?? [])));
3436
3571
  this.key = key;
3437
3572
  this.save = [];
3438
3573
  }
@@ -3448,7 +3583,7 @@ class NavvyPagNavResultKey extends NavvyPagBase {
3448
3583
  }
3449
3584
  resultAllEnd() {
3450
3585
  let BXRestClass = new BXRest();
3451
- return this.resVanilla().pipe(mergeMap(items => {
3586
+ return this.resFirstPage().pipe(mergeMap(items => {
3452
3587
  if (items && instanceOfiBXRestAnswerSuccess(items)) {
3453
3588
  this.save = [];
3454
3589
  if (items.total && items.total > 50) {
@@ -3503,7 +3638,8 @@ class NavvyPagNavResultKey extends NavvyPagBase {
3503
3638
  /** Пагинация для методов, возвращающих коллекцию как объект с идентификаторами в ключах. */
3504
3639
  class NavvyPagNavResultKeyObject extends NavvyPagBase {
3505
3640
  constructor(url, param, key, mapResult) {
3506
- super(url, param, mapResult);
3641
+ // Без своего map результат — значения объекта по ключу, как и обещают типы `.res()` и `.resAll()`.
3642
+ super(url, param, mapResult ?? (result => Object.values(result?.[key] ?? {})));
3507
3643
  this.key = key;
3508
3644
  }
3509
3645
  resAll() {
@@ -3511,7 +3647,7 @@ class NavvyPagNavResultKeyObject extends NavvyPagBase {
3511
3647
  }
3512
3648
  resultAllEnd() {
3513
3649
  const bxRest = new BXRest();
3514
- return this.resVanilla().pipe(mergeMap(items => {
3650
+ return this.resFirstPage().pipe(mergeMap(items => {
3515
3651
  if (!items || !instanceOfiBXRestAnswerSuccess(items)) {
3516
3652
  return from([]);
3517
3653
  }
@@ -3536,7 +3672,7 @@ class NavvyPagNavResultKeyObject extends NavvyPagBase {
3536
3672
  }
3537
3673
  }
3538
3674
  }
3539
- return this.mapResult({ [this.key]: merged });
3675
+ return this.mapResult({ result: { [this.key]: merged } });
3540
3676
  }));
3541
3677
  }), catchError(error => {
3542
3678
  console.error(error);
@@ -3547,9 +3683,7 @@ class NavvyPagNavResultKeyObject extends NavvyPagBase {
3547
3683
  if (!instanceOfiBXRestAnswerSuccess(items)) {
3548
3684
  return from([]);
3549
3685
  }
3550
- return from([items.result]).pipe(map$1(value => this.map
3551
- ? this.map(value)
3552
- : Object.values(value?.[this.key] ?? {})));
3686
+ return from([items.result]).pipe(map$1(value => this.map?.(value)));
3553
3687
  }
3554
3688
  }
3555
3689
 
@@ -20231,6 +20365,576 @@ class BXRestMap {
20231
20365
  }
20232
20366
  }
20233
20367
 
20368
+ /**
20369
+ * Обмен сообщениями между iframe приложения и порталом Bitrix24 — тот
20370
+ * транспорт, на котором построены команды BX24.js: страница приложения шлёт
20371
+ * команду родительскому окну портала через postMessage, портал отвечает
20372
+ * сообщением с тем же callbackId.
20373
+ *
20374
+ * ВНИМАНИЕ: протокол внутренний и недокументированный — Bitrix24 может
20375
+ * изменить его без предупреждения. Транспорт повторяет BX24.js
20376
+ * (https://api.bitrix24.com/api/v1/, скачан 2026-10-29, Last-Modified
20377
+ * 2022-03-17, sha256 945ad491df8ffb7490b89f4914a439b5a3f575c950c3480562815716a3e07f23)
20378
+ * и сверен с ответной стороной портала — `/bitrix/js/rest/applayout.js`
20379
+ * (Last-Modified 2024-05-15, sha256 3efd6ab57a4778493a26d10ae0987a82e19fc012ed9935f5c6afa78c4052fcb5).
20380
+ * Всё знание о протоколе — только в этом модуле (`services/frame`).
20381
+ */
20382
+ /**
20383
+ * Ошибка обращения к порталу из iframe. `code`:
20384
+ * - `NOT_IN_FRAME` — страница открыта не в iframe приложения Bitrix24;
20385
+ * - `TIMEOUT` — портал не ответил за отведённое время;
20386
+ * - `NO_AUTH` — портал ответил без токена;
20387
+ * - `BAD_RESPONSE` — ответ портала не удалось разобрать;
20388
+ * - `CANCELLED` — ожидание отменено через `signal` или новым вызовом того же диалога;
20389
+ * - `ACCESS_DENIED` — команда доступна только администратору портала.
20390
+ */
20391
+ class BXRestFrameError extends Error {
20392
+ constructor(code, message) {
20393
+ super(message);
20394
+ this.code = code;
20395
+ this.name = 'BXRestFrameError';
20396
+ }
20397
+ }
20398
+ /**
20399
+ * Параметры портала (DOMAIN, PROTOCOL, APP_SID) читаются в конструкторе —
20400
+ * так же, как это делает BX24.js, из `window.name`: портал задаёт имя
20401
+ * iframe приложения в виде `DOMAIN|PROTOCOL|APP_SID`. Если там их нет —
20402
+ * из query страницы (`?DOMAIN=…&PROTOCOL=…&APP_SID=…`), с которым портал
20403
+ * открывает обработчик приложения. Конструктор не бросает исключений:
20404
+ * вне iframe ошибку вернут сами команды.
20405
+ */
20406
+ class FrameTransport {
20407
+ constructor() {
20408
+ this.frame = readPortalFrame();
20409
+ }
20410
+ /** Домен портала из параметров фрейма — как `BX24.getDomain()`. */
20411
+ get domain() {
20412
+ return this.frame?.domain ?? '';
20413
+ }
20414
+ /**
20415
+ * Команда, на которую портал не отвечает (`reloadWindow`, `imCallTo`, ...).
20416
+ * Вне iframe бросает `BXRestFrameError` с кодом `NOT_IN_FRAME`.
20417
+ */
20418
+ post(command, params) {
20419
+ this.send(this.requireFrame(), command, params, '');
20420
+ }
20421
+ /** Команда с одним ответом портала. */
20422
+ request(command, params, options) {
20423
+ return new Promise((resolve, reject) => {
20424
+ let frame;
20425
+ try {
20426
+ frame = this.requireFrame();
20427
+ }
20428
+ catch (error) {
20429
+ reject(error);
20430
+ return;
20431
+ }
20432
+ const { timeout, onTimeout = 'reject', signal } = options;
20433
+ if (signal?.aborted) {
20434
+ reject(cancelled(command));
20435
+ return;
20436
+ }
20437
+ const callbackId = uniqId();
20438
+ let timer;
20439
+ const finish = () => {
20440
+ clearTimeout(timer);
20441
+ stopListening();
20442
+ signal?.removeEventListener('abort', onAbort);
20443
+ };
20444
+ const onAbort = () => {
20445
+ finish();
20446
+ reject(cancelled(command));
20447
+ };
20448
+ const stopListening = this.listen(frame, callbackId, payload => {
20449
+ finish();
20450
+ const answer = parsePayload(payload);
20451
+ if (answer.ok) {
20452
+ resolve(answer.value);
20453
+ }
20454
+ else {
20455
+ // В тексте ошибки — только команда, без тела ответа: в нём бывают токены
20456
+ reject(new BXRestFrameError('BAD_RESPONSE', `Bitrix24 answered ${command} with a payload that is not JSON.`));
20457
+ }
20458
+ });
20459
+ if (timeout !== false) {
20460
+ timer = setTimeout(() => {
20461
+ finish();
20462
+ if (onTimeout === 'resolve') {
20463
+ resolve(undefined);
20464
+ }
20465
+ else {
20466
+ reject(new BXRestFrameError('TIMEOUT', [
20467
+ `Bitrix24 did not answer ${command} within ${timeout} ms:`,
20468
+ 'the page is not opened inside a Bitrix24 portal iframe, or the portal could not process the command.'
20469
+ ].join(' ')));
20470
+ }
20471
+ }, timeout);
20472
+ }
20473
+ signal?.addEventListener('abort', onAbort, { once: true });
20474
+ this.send(frame, command, params, callbackId);
20475
+ });
20476
+ }
20477
+ /**
20478
+ * Команда, на которую портал отвечает многократно (`placementBindEvent`):
20479
+ * `handler` вызывается на каждый ответ. Возвращает функцию отписки —
20480
+ * она только перестаёт слушать: отменить подписку на стороне портала
20481
+ * протокол не позволяет. Вне iframe бросает `BXRestFrameError`.
20482
+ */
20483
+ subscribe(command, params, handler) {
20484
+ const frame = this.requireFrame();
20485
+ const callbackId = uniqId();
20486
+ const stopListening = this.listen(frame, callbackId, payload => {
20487
+ const answer = parsePayload(payload);
20488
+ if (answer.ok) {
20489
+ handler(answer.value);
20490
+ }
20491
+ });
20492
+ this.send(frame, command, params, callbackId);
20493
+ return stopListening;
20494
+ }
20495
+ requireFrame() {
20496
+ if (!this.frame) {
20497
+ throw new BXRestFrameError('NOT_IN_FRAME', 'Bitrix24 frame parameters (DOMAIN, APP_SID) were not found in window.name or in the URL query: '
20498
+ + 'the page is not opened as a Bitrix24 application.');
20499
+ }
20500
+ if (window.parent === window) {
20501
+ throw new BXRestFrameError('NOT_IN_FRAME', 'The page is not opened inside an iframe, so there is no Bitrix24 portal to send commands to.');
20502
+ }
20503
+ return this.frame;
20504
+ }
20505
+ /**
20506
+ * Формат — как у BX24.js. Обычно строка `команда:параметры:callbackId:APP_SID`
20507
+ * (портал разбирает её с конца, поэтому параметры и callbackId могут быть
20508
+ * пустыми); для команд с `:` в имени — объект.
20509
+ */
20510
+ send(frame, command, params, callbackId) {
20511
+ const message = command.includes(':')
20512
+ ? { method: command, params: params ? params : '', callback: callbackId, appSid: frame.appSid }
20513
+ : `${command}:${params ? JSON.stringify(params) : ''}:${callbackId}:${frame.appSid}`;
20514
+ window.parent.postMessage(message, frame.origin);
20515
+ }
20516
+ /** Ответ портала — строка `callbackId:JSON` (или `callbackId:`, если ответ пустой). */
20517
+ listen(frame, callbackId, onAnswer) {
20518
+ const portalWindow = window.parent;
20519
+ const onMessage = (event) => {
20520
+ // Ответ принимаем только от родительского окна и только с origin
20521
+ // портала — BX24.js проверяет лишь origin
20522
+ if (event.source !== portalWindow || event.origin !== frame.origin || typeof event.data !== 'string') {
20523
+ return;
20524
+ }
20525
+ const separator = event.data.indexOf(':');
20526
+ const id = separator < 0 ? event.data : event.data.slice(0, separator);
20527
+ if (id === callbackId) {
20528
+ onAnswer(separator < 0 ? '' : event.data.slice(separator + 1));
20529
+ }
20530
+ };
20531
+ window.addEventListener('message', onMessage);
20532
+ return () => window.removeEventListener('message', onMessage);
20533
+ }
20534
+ }
20535
+ function parsePayload(payload) {
20536
+ if (payload === '') {
20537
+ return { ok: true, value: undefined };
20538
+ }
20539
+ try {
20540
+ return { ok: true, value: JSON.parse(payload) };
20541
+ }
20542
+ catch {
20543
+ return { ok: false };
20544
+ }
20545
+ }
20546
+ function cancelled(command) {
20547
+ return new BXRestFrameError('CANCELLED', `Waiting for the Bitrix24 answer to ${command} was cancelled.`);
20548
+ }
20549
+ function readPortalFrame() {
20550
+ if (typeof window === 'undefined') {
20551
+ return undefined;
20552
+ }
20553
+ // BX24.js: window.name = 'DOMAIN|PROTOCOL|APP_SID'
20554
+ const [nameDomain, nameProtocol, nameAppSid] = (typeof window.name === 'string' ? window.name : '').split('|');
20555
+ const fromName = toPortalFrame(nameDomain, nameProtocol, nameAppSid);
20556
+ if (fromName) {
20557
+ return fromName;
20558
+ }
20559
+ try {
20560
+ const query = new URLSearchParams(window.location?.search ?? '');
20561
+ return toPortalFrame(query.get('DOMAIN'), query.get('PROTOCOL'), query.get('APP_SID'));
20562
+ }
20563
+ catch {
20564
+ return undefined;
20565
+ }
20566
+ }
20567
+ function toPortalFrame(rawDomain, rawProtocol, appSid) {
20568
+ // Как BX24.js: порт по умолчанию отрезается, PROTOCOL 1 — https
20569
+ const domain = (rawDomain ?? '').replace(/:(80|443)$/, '');
20570
+ if (!domain || !appSid) {
20571
+ return undefined;
20572
+ }
20573
+ const protocol = (Number.parseInt(rawProtocol ?? '', 10) || 0) ? 'https' : 'http';
20574
+ try {
20575
+ const url = new URL(`${protocol}://${domain}`);
20576
+ // В DOMAIN должен быть только хост (и порт) — никаких путей и логинов
20577
+ if (url.host !== domain.toLowerCase() || url.username || url.password) {
20578
+ return undefined;
20579
+ }
20580
+ return { domain, origin: url.origin, appSid };
20581
+ }
20582
+ catch {
20583
+ return undefined;
20584
+ }
20585
+ }
20586
+ function uniqId() {
20587
+ const bytes = new Uint8Array(16);
20588
+ globalThis.crypto.getRandomValues(bytes);
20589
+ return Array.from(bytes, byte => byte.toString(16).padStart(2, '0')).join('');
20590
+ }
20591
+
20592
+ const DEFAULT_TIMEOUT = 10000;
20593
+ /**
20594
+ * Команды порталу Bitrix24 из iframe приложения — без подключения BX24.js.
20595
+ * Те же команды и те же имена, что у BX24.js, но вместо колбэков —
20596
+ * промисы. Протокол внутренний и недокументированный, см. `transport.ts`.
20597
+ *
20598
+ * Создавайте один экземпляр на страницу и при старте приложения: параметры
20599
+ * портала читаются в конструкторе (до того, как роутер перепишет адрес), а
20600
+ * `firstRun` портал сообщает только в первом ответе на `getInitData`.
20601
+ *
20602
+ * Токены класс не логирует и никуда не сохраняет: `getAuth()` и
20603
+ * `refreshAuth()` каждый раз спрашивают портал, а в `init()` токенов нет.
20604
+ *
20605
+ * @example
20606
+ * const frame = new BXRestFrame()
20607
+ * const { lang, placement } = await frame.init()
20608
+ * await frame.fitWindow()
20609
+ * const user = await frame.selectUser()
20610
+ */
20611
+ class BXRestFrame {
20612
+ constructor(options = {}) {
20613
+ this.transport = new FrameTransport();
20614
+ // Открытый диалог каждого вида — новый вызов того же диалога отменяет прошлый
20615
+ this.dialogs = new Map();
20616
+ /** Мессенджер — как `BX24.im`. Портал на эти команды не отвечает. */
20617
+ this.im = {
20618
+ /** Позвонить сотруднику — `BX24.im.callTo()`. */
20619
+ callTo: async (userId, video) => {
20620
+ this.transport.post('imCallTo', { userId, video });
20621
+ },
20622
+ /** Позвонить на телефон — `BX24.im.phoneTo()`. */
20623
+ phoneTo: async (phone) => {
20624
+ this.transport.post('imPhoneTo', { phone });
20625
+ },
20626
+ /** Открыть чат — `BX24.im.openMessenger()`. */
20627
+ openMessenger: async (dialogId) => {
20628
+ this.transport.post('imOpenMessenger', { dialogId });
20629
+ },
20630
+ /** Открыть историю чата — `BX24.im.openHistory()`. */
20631
+ openHistory: async (dialogId) => {
20632
+ this.transport.post('imOpenHistory', { dialogId });
20633
+ },
20634
+ };
20635
+ /** Встраивание — как `BX24.placement`. */
20636
+ this.placement = {
20637
+ /** Код и параметры встраивания — `BX24.placement.info()`. */
20638
+ info: async () => {
20639
+ const { placement, placementOptions } = await this.init();
20640
+ return { placement, options: placementOptions };
20641
+ },
20642
+ /** Команды и события текущего встраивания — `BX24.placement.getInterface()`. */
20643
+ getInterface: async () => {
20644
+ return await this.transport.request('getInterface', null, { timeout: this.timeout });
20645
+ },
20646
+ /**
20647
+ * Команда встраивания — `BX24.placement.call()`. Набор команд зависит от
20648
+ * встраивания (см. `getInterface()`). Не на все команды портал отвечает:
20649
+ * без ответа за `timeout` промис завершается с `undefined`.
20650
+ */
20651
+ call: async (command, params, options = {}) => {
20652
+ return this.transport.request(command, params, {
20653
+ timeout: options.timeout ?? this.timeout,
20654
+ onTimeout: 'resolve',
20655
+ });
20656
+ },
20657
+ /**
20658
+ * Подписка на событие встраивания — `BX24.placement.bindEvent()`.
20659
+ * `handler` вызывается при каждом событии. Возвращает функцию отписки:
20660
+ * она только перестаёт вызывать `handler` — отменить подписку на
20661
+ * стороне портала протокол не позволяет. Вне iframe бросает
20662
+ * `BXRestFrameError` с кодом `NOT_IN_FRAME`.
20663
+ */
20664
+ bindEvent: (event, handler) => {
20665
+ return this.transport.subscribe('placementBindEvent', { event }, handler);
20666
+ },
20667
+ };
20668
+ /** Настройки приложения для текущего пользователя — как `BX24.userOption`. */
20669
+ this.userOption = {
20670
+ get: async (name) => (await this.init()).userOptions[name],
20671
+ set: async (name, value) => {
20672
+ const init = await this.init();
20673
+ init.userOptions[name] = value;
20674
+ await this.ack('setUserOption', { name, value });
20675
+ },
20676
+ };
20677
+ /** Настройки приложения для всего портала — как `BX24.appOption`. */
20678
+ this.appOption = {
20679
+ get: async (name) => (await this.init()).appOptions[name],
20680
+ /**
20681
+ * Менять их может только администратор портала: остальным — ошибка
20682
+ * `ACCESS_DENIED` (BX24.js в этом случае молча ничего не делает).
20683
+ * Возвращает настройки приложения после сохранения.
20684
+ */
20685
+ set: async (name, value) => {
20686
+ const init = await this.init();
20687
+ if (!init.isAdmin) {
20688
+ throw new BXRestFrameError('ACCESS_DENIED', 'Only a Bitrix24 administrator can change application options.');
20689
+ }
20690
+ init.appOptions[name] = value;
20691
+ return await this.transport.request('setAppOption', { name, value }, { timeout: this.timeout });
20692
+ },
20693
+ };
20694
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
20695
+ }
20696
+ /**
20697
+ * Данные запуска приложения — аналог `BX24.init()`: язык, домен, права,
20698
+ * встраивание, настройки, признаки установки. Запрашиваются у портала
20699
+ * один раз, дальше берутся из памяти. Токенов здесь нет — см. `getAuth()`.
20700
+ */
20701
+ init() {
20702
+ if (this.initValue) {
20703
+ return Promise.resolve(this.initValue);
20704
+ }
20705
+ if (!this.initRequest) {
20706
+ this.initRequest = this.getInitData()
20707
+ .then(() => this.initValue)
20708
+ .finally(() => {
20709
+ this.initRequest = undefined;
20710
+ });
20711
+ }
20712
+ return this.initRequest;
20713
+ }
20714
+ /**
20715
+ * Текущая авторизация приложения — аналог `BX24.getAuth()`. Каждый вызов —
20716
+ * новый запрос `getInitData` к порталу.
20717
+ *
20718
+ * `expires_in` портал считает от срока жизни токена в момент выдачи, а не
20719
+ * от остатка: если страница открыта давно, значение будет завышено (так
20720
+ * же и у BX24.js). Для свежего токена используйте `refreshAuth()`.
20721
+ */
20722
+ async getAuth() {
20723
+ const data = await this.getInitData();
20724
+ return this.toAuth('getInitData', data, String(data['MEMBER_ID'] || ''));
20725
+ }
20726
+ /**
20727
+ * Новая авторизация приложения — аналог `BX24.refreshAuth()`: портал
20728
+ * выпускает новую пару `access_token`/`refresh_token`. Если портал не может
20729
+ * выдать токен, он показывает `alert` «Unable to get new token!» и не
20730
+ * отвечает — тогда ошибка `TIMEOUT`.
20731
+ */
20732
+ async refreshAuth() {
20733
+ // member_id приходит только в getInitData — refreshAuth его не возвращает
20734
+ const { memberId } = await this.init();
20735
+ const data = asObject('refreshAuth', await this.transport.request('refreshAuth', {}, { timeout: this.timeout }));
20736
+ return this.toAuth('refreshAuth', data, memberId);
20737
+ }
20738
+ /**
20739
+ * Завершить установку — аналог `BX24.installFinish()`: портал отмечает
20740
+ * приложение установленным и перезагружает страницу. Делает что-то только
20741
+ * на странице установщика (`init().install`), иначе — ничего, как и BX24.js.
20742
+ */
20743
+ async installFinish() {
20744
+ if ((await this.init()).install) {
20745
+ this.transport.post('setInstallFinish', {});
20746
+ }
20747
+ }
20748
+ /**
20749
+ * Отметить, что первый запуск у этого пользователя пройден, — то, что
20750
+ * BX24.js делает после последнего обработчика `BX24.install()`. Без этого
20751
+ * `init().firstRun` останется `true` и при следующих запусках. Делает
20752
+ * что-то только при первом запуске.
20753
+ */
20754
+ async firstRunFinish() {
20755
+ const init = await this.init();
20756
+ if (init.firstRun) {
20757
+ await this.ack('setInstall', { install: true });
20758
+ init.firstRun = false;
20759
+ }
20760
+ }
20761
+ /** Задать размер фрейма — `BX24.resizeWindow()`. Возвращает размер, который выставил портал. */
20762
+ async resizeWindow(width, height) {
20763
+ const w = Number.parseInt(String(width), 10);
20764
+ const h = Number.parseInt(String(height), 10);
20765
+ if (!(w > 0 && h > 0)) {
20766
+ throw new TypeError('resizeWindow: width and height must be positive numbers.');
20767
+ }
20768
+ return await this.ack('resizeWindow', { width: w, height: h });
20769
+ }
20770
+ /** Подогнать высоту фрейма под содержимое — `BX24.fitWindow()`. */
20771
+ async fitWindow() {
20772
+ const root = document.documentElement;
20773
+ const height = Math.max(root.scrollHeight, root.offsetHeight);
20774
+ return await this.ack('resizeWindow', { width: '100%', height });
20775
+ }
20776
+ /**
20777
+ * Перезагрузить страницу портала с приложением — `BX24.reloadWindow()`.
20778
+ * Портал на эту команду не отвечает.
20779
+ */
20780
+ async reloadWindow() {
20781
+ this.transport.post('reloadWindow', {});
20782
+ }
20783
+ /** Заголовок страницы портала — `BX24.setTitle()`. */
20784
+ async setTitle(title) {
20785
+ return await this.ack('setTitle', { title: String(title) });
20786
+ }
20787
+ /** Прокрутить страницу портала — `BX24.scrollParentWindow()`. */
20788
+ async scrollParentWindow(scroll) {
20789
+ const value = Number.parseInt(String(scroll), 10);
20790
+ if (Number.isNaN(value)) {
20791
+ throw new TypeError('scrollParentWindow: scroll must be a number.');
20792
+ }
20793
+ return await this.ack('setScroll', { scroll: value });
20794
+ }
20795
+ /**
20796
+ * Диалог выбора одного сотрудника — `BX24.selectUser()`. Ждёт выбора без
20797
+ * ограничения по времени: если пользователь закрыл диалог, портал не
20798
+ * отвечает вовсе. Отменить ожидание — `signal` или новый вызов
20799
+ * `selectUser()`/`selectUsers()` (ошибка `CANCELLED`).
20800
+ */
20801
+ selectUser(options = {}) {
20802
+ return this.dialog('user', 'selectUser', { title: options.title ?? '', mult: false }, options.signal);
20803
+ }
20804
+ /** Диалог выбора нескольких сотрудников — `BX24.selectUsers()`. Ожидание — как у `selectUser()`. */
20805
+ selectUsers(options = {}) {
20806
+ return this.dialog('user', 'selectUser', { title: options.title ?? '', mult: true }, options.signal);
20807
+ }
20808
+ /**
20809
+ * Диалог выбора прав доступа — `BX24.selectAccess()`. `value` — коды,
20810
+ * которые нельзя выбрать. Ожидание — как у `selectUser()`.
20811
+ */
20812
+ selectAccess(options = {}) {
20813
+ return this.dialog('access', 'selectAccess', { value: options.value ?? [] }, options.signal);
20814
+ }
20815
+ /** Диалог выбора объектов CRM — `BX24.selectCRM()`. Ожидание — как у `selectUser()`. */
20816
+ selectCRM(options = {}) {
20817
+ const { entityType, multiple, value, signal } = options;
20818
+ return this.dialog('crm', 'selectCRM', { entityType, multiple, value }, signal);
20819
+ }
20820
+ /**
20821
+ * Открыть приложение в слайдере — `BX24.openApplication()`. Промис
20822
+ * завершается, когда слайдер закрыли. `settings` — настройки слайдера
20823
+ * (уходят как `bx24_<ключ>`, как в BX24.js).
20824
+ */
20825
+ async openApplication(params = {}, settings = {}, options = {}) {
20826
+ const message = { ...params };
20827
+ for (const [key, value] of Object.entries(settings)) {
20828
+ message[`bx24_${key}`] = value;
20829
+ }
20830
+ await this.transport.request('openApplication', message, { timeout: false, signal: options.signal });
20831
+ }
20832
+ /**
20833
+ * Открыть страницу портала в слайдере — `BX24.openPath()`. Промис
20834
+ * завершается, когда слайдер закрыли (`{ result: 'close' }`), или сразу,
20835
+ * если путь не открылся (`{ result: 'error', errorCode }`).
20836
+ */
20837
+ async openPath(path, options = {}) {
20838
+ return await this.transport.request('openPath', { path }, { timeout: false, signal: options.signal });
20839
+ }
20840
+ /**
20841
+ * Закрыть слайдер с приложением — `BX24.closeApplication()`. Если
20842
+ * приложение открыто не в слайдере, ничего не происходит. Узнать о
20843
+ * закрытии может открывший слайдер — через `openApplication()`.
20844
+ */
20845
+ async closeApplication() {
20846
+ this.transport.post('closeApplication', null);
20847
+ }
20848
+ /**
20849
+ * Команда, на которую портал отвечает подтверждением. В некоторых
20850
+ * встраиваниях портал на такие команды не отвечает (так считает и
20851
+ * b24jssdk) — тогда промис завершается с `undefined`, а не ошибкой.
20852
+ */
20853
+ ack(command, params) {
20854
+ return this.transport.request(command, params, { timeout: this.timeout, onTimeout: 'resolve' });
20855
+ }
20856
+ /**
20857
+ * Диалоги ждут пользователя без таймаута. Портал держит один обработчик
20858
+ * на диалог (у выбора сотрудников он общий для `selectUser` и
20859
+ * `selectUsers`) и на прошлый вызов уже не ответит — поэтому новый вызов
20860
+ * отменяет ожидание прошлого.
20861
+ */
20862
+ dialog(kind, command, params, signal) {
20863
+ this.dialogs.get(kind)?.abort();
20864
+ const controller = new AbortController();
20865
+ this.dialogs.set(kind, controller);
20866
+ const onAbort = () => controller.abort();
20867
+ if (signal?.aborted) {
20868
+ controller.abort();
20869
+ }
20870
+ signal?.addEventListener('abort', onAbort, { once: true });
20871
+ return this.transport.request(command, params, { timeout: false, signal: controller.signal })
20872
+ .finally(() => {
20873
+ signal?.removeEventListener('abort', onAbort);
20874
+ if (this.dialogs.get(kind) === controller) {
20875
+ this.dialogs.delete(kind);
20876
+ }
20877
+ });
20878
+ }
20879
+ async getInitData() {
20880
+ const data = asObject('getInitData', await this.transport.request('getInitData', null, { timeout: this.timeout }));
20881
+ // firstRun портал отдаёт только в первом ответе за загрузку страницы —
20882
+ // поэтому запоминаем данные первого ответа, кто бы его ни запросил
20883
+ if (!this.initValue) {
20884
+ this.initValue = this.toInit(data);
20885
+ }
20886
+ return data;
20887
+ }
20888
+ toInit(data) {
20889
+ return {
20890
+ // Как BX24.js: домен — из параметров фрейма, DOMAIN ответа — запасной
20891
+ domain: this.transport.domain || String(data['DOMAIN'] || ''),
20892
+ lang: String(data['LANG'] || ''),
20893
+ path: String(data['PATH'] || ''),
20894
+ https: Number.parseInt(String(data['PROTOCOL']), 10) === 1,
20895
+ memberId: String(data['MEMBER_ID'] || ''),
20896
+ isAdmin: !!data['IS_ADMIN'],
20897
+ install: !!data['INSTALL'],
20898
+ firstRun: !!data['FIRST_RUN'],
20899
+ placement: String(data['PLACEMENT'] || 'DEFAULT'),
20900
+ placementOptions: data['PLACEMENT_OPTIONS'],
20901
+ userOptions: toRecord(data['USER_OPTIONS']),
20902
+ appOptions: toRecord(data['APP_OPTIONS']),
20903
+ };
20904
+ }
20905
+ toAuth(command, data, memberId) {
20906
+ const { AUTH_ID, REFRESH_ID, AUTH_EXPIRES } = data;
20907
+ if (!AUTH_ID || typeof AUTH_ID !== 'string') {
20908
+ throw new BXRestFrameError('NO_AUTH', `Bitrix24 answered ${command} without an access token.`);
20909
+ }
20910
+ // AUTH_EXPIRES — срок жизни токена в секундах. BX24.js переводит его в
20911
+ // момент истечения в миллисекундах — так же, как здесь.
20912
+ const expiresIn = Number.parseInt(String(AUTH_EXPIRES), 10);
20913
+ if (!Number.isFinite(expiresIn) || expiresIn <= 0) {
20914
+ throw new BXRestFrameError('BAD_RESPONSE', `Bitrix24 answered ${command} without a usable AUTH_EXPIRES.`);
20915
+ }
20916
+ return {
20917
+ access_token: AUTH_ID,
20918
+ refresh_token: typeof REFRESH_ID === 'string' ? REFRESH_ID : '',
20919
+ expires_in: Date.now() + expiresIn * 1000,
20920
+ domain: this.transport.domain,
20921
+ member_id: memberId,
20922
+ };
20923
+ }
20924
+ }
20925
+ function asObject(command, data) {
20926
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
20927
+ throw new BXRestFrameError('BAD_RESPONSE', `Bitrix24 answered ${command} with a payload that is not an object.`);
20928
+ }
20929
+ return data;
20930
+ }
20931
+ // Пустые настройки PHP отдаёт массивом `[]`
20932
+ function toRecord(value) {
20933
+ return (value && typeof value === 'object' && !Array.isArray(value))
20934
+ ? { ...value }
20935
+ : {};
20936
+ }
20937
+
20234
20938
  /**
20235
20939
  * Public API Surface of bx-rest
20236
20940
  */
@@ -20239,5 +20943,5 @@ class BXRestMap {
20239
20943
  * Generated bundle index. Do not edit.
20240
20944
  */
20241
20945
 
20242
- export { base as BXBaseServices, BXRestApiError, BXRestMap, BXRestMapResult, BXRestNavvy, HttpBXServices as BXRestRequest, BXRestSettings, HttpNavvyServices, Navvy, SessionKeyError, SessionKeyServices, instanceOfiBXRestAnswerSuccess as isBXRestAnswerSuccess, partNameMethods$1 as partNameMethods };
20946
+ 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
20947
  //# sourceMappingURL=bx-rest.mjs.map