itd-api 0.7.1 → 0.8.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 (84) hide show
  1. package/README.md +14 -14
  2. package/dist/events/index.cjs +53 -0
  3. package/dist/events/index.d.cts +3 -0
  4. package/dist/events/index.d.ts +3 -0
  5. package/dist/events/index.js +5 -0
  6. package/dist/index.cjs +269 -175
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +77 -56
  9. package/dist/index.d.ts +77 -56
  10. package/dist/index.js +251 -160
  11. package/dist/index.js.map +1 -1
  12. package/dist/node/index.cjs +4 -4
  13. package/dist/node/index.d.cts +1 -1
  14. package/dist/node/index.d.ts +1 -1
  15. package/dist/node/index.js +4 -4
  16. package/dist/rest/index.cjs +72 -14
  17. package/dist/rest/index.cjs.map +1 -1
  18. package/dist/rest/index.d.cts +11 -3
  19. package/dist/rest/index.d.ts +11 -3
  20. package/dist/rest/index.js +66 -8
  21. package/dist/rest/index.js.map +1 -1
  22. package/dist/shared/{errors-DfU8M5eS.cjs → errors-BmP3TKoW.cjs} +3 -3
  23. package/dist/shared/{errors-DfU8M5eS.cjs.map → errors-BmP3TKoW.cjs.map} +1 -1
  24. package/dist/shared/{errors-Bhrd2fJd.js → errors-GI10kZxk.js} +3 -3
  25. package/dist/shared/{errors-Bhrd2fJd.js.map → errors-GI10kZxk.js.map} +1 -1
  26. package/dist/shared/events-Br2r7coi.d.cts +698 -0
  27. package/dist/shared/events-QyhQZrnC.d.ts +698 -0
  28. package/dist/shared/{websocket-CbzB1Leq.js → events-TfhKh26b.js} +770 -490
  29. package/dist/shared/events-TfhKh26b.js.map +1 -0
  30. package/dist/shared/{websocket-sYlynrr0.cjs → events-iTteVsd_.cjs} +834 -518
  31. package/dist/shared/events-iTteVsd_.cjs.map +1 -0
  32. package/dist/shared/{multi-storage-CkvTUC5m.js → multi-storage-1AEZTczk.js} +4 -4
  33. package/dist/shared/{multi-storage-CkvTUC5m.js.map → multi-storage-1AEZTczk.js.map} +1 -1
  34. package/dist/shared/{multi-storage--yTEqiod.cjs → multi-storage-CCUgkqvI.cjs} +6 -6
  35. package/dist/shared/{multi-storage--yTEqiod.cjs.map → multi-storage-CCUgkqvI.cjs.map} +1 -1
  36. package/dist/shared/{options-DtATYdLr.js → options-BNP633A5.js} +8 -6
  37. package/dist/shared/options-BNP633A5.js.map +1 -0
  38. package/dist/shared/{options-Dg5N3r1V.cjs → options-BUfoHE9G.cjs} +8 -6
  39. package/dist/shared/options-BUfoHE9G.cjs.map +1 -0
  40. package/dist/shared/{cookies-DZwFq6kr.cjs → redact-DB9EMGJx.cjs} +301 -2
  41. package/dist/shared/redact-DB9EMGJx.cjs.map +1 -0
  42. package/dist/shared/{cookies-tX2sNwxb.js → redact-FNK29Fv-.js} +224 -3
  43. package/dist/shared/redact-FNK29Fv-.js.map +1 -0
  44. package/dist/shared/{render-DMp_3Nzk.d.cts → render-BszcKF7S.d.cts} +432 -254
  45. package/dist/shared/{render-vtLixIiU.js → render-CiL1VS0b.js} +1261 -621
  46. package/dist/shared/render-CiL1VS0b.js.map +1 -0
  47. package/dist/shared/{render-DyeHJNBw.cjs → render-DNTESBSl.cjs} +1335 -671
  48. package/dist/shared/render-DNTESBSl.cjs.map +1 -0
  49. package/dist/shared/{render-DZrxhC5_.d.ts → render-DVB7T8pm.d.ts} +432 -254
  50. package/dist/shared/{storage-BPJR_k4-.cjs → storage-BLmXmzKF.cjs} +2 -2
  51. package/dist/shared/{storage-BPJR_k4-.cjs.map → storage-BLmXmzKF.cjs.map} +1 -1
  52. package/dist/shared/{storage-D86edNCB.js → storage-CCxn0Kkv.js} +2 -2
  53. package/dist/shared/{storage-D86edNCB.js.map → storage-CCxn0Kkv.js.map} +1 -1
  54. package/dist/shared/{url-CYXgxqGx.js → url-B3ocFlHj.js} +491 -425
  55. package/dist/shared/url-B3ocFlHj.js.map +1 -0
  56. package/dist/shared/{url-B6-bXHKt.d.ts → url-Be9jlWRv.d.cts} +1233 -1195
  57. package/dist/shared/{url-B6-bXHKt.d.cts → url-Be9jlWRv.d.ts} +1233 -1195
  58. package/dist/shared/{url-yjl2c8Ie.cjs → url-CFN1sT9S.cjs} +562 -478
  59. package/dist/shared/url-CFN1sT9S.cjs.map +1 -0
  60. package/dist/web/index.cjs +2 -2
  61. package/dist/web/index.js +2 -2
  62. package/package.json +16 -15
  63. package/dist/realtime/index.cjs +0 -165
  64. package/dist/realtime/index.cjs.map +0 -1
  65. package/dist/realtime/index.d.cts +0 -51
  66. package/dist/realtime/index.d.ts +0 -51
  67. package/dist/realtime/index.js +0 -120
  68. package/dist/realtime/index.js.map +0 -1
  69. package/dist/shared/auth-provider-BfogACAb.js +0 -91
  70. package/dist/shared/auth-provider-BfogACAb.js.map +0 -1
  71. package/dist/shared/auth-provider-CTJkKfgy.cjs +0 -108
  72. package/dist/shared/auth-provider-CTJkKfgy.cjs.map +0 -1
  73. package/dist/shared/cookies-DZwFq6kr.cjs.map +0 -1
  74. package/dist/shared/cookies-tX2sNwxb.js.map +0 -1
  75. package/dist/shared/options-Dg5N3r1V.cjs.map +0 -1
  76. package/dist/shared/options-DtATYdLr.js.map +0 -1
  77. package/dist/shared/render-DyeHJNBw.cjs.map +0 -1
  78. package/dist/shared/render-vtLixIiU.js.map +0 -1
  79. package/dist/shared/url-CYXgxqGx.js.map +0 -1
  80. package/dist/shared/url-yjl2c8Ie.cjs.map +0 -1
  81. package/dist/shared/websocket-BMtihD56.d.ts +0 -562
  82. package/dist/shared/websocket-CbzB1Leq.js.map +0 -1
  83. package/dist/shared/websocket-D1p32SB2.d.cts +0 -562
  84. package/dist/shared/websocket-sYlynrr0.cjs.map +0 -1
package/dist/index.js CHANGED
@@ -1,11 +1,63 @@
1
- import { $ as RuntimeMode, A as operationRetrySafety, C as ViewSource, D as isBuiltInOperationId, E as OPERATIONS, F as pickArray, G as orderPluginDefinitions, H as RetrySafety, J as resolveRateLimit, K as DEFAULT_BASE_URL, M as DEFAULT_RATE_LIMIT_BUCKET, N as createClientRuntime, O as operationBucket, Q as RateLimitPacing, S as ViewReason, T as ITD_CATALOG, U as RequestQueuePool, W as assertPluginRemovable, X as isRecord, Y as LIBRARY_VERSION, Z as requireOptionalBoolean, _ as RealtimeStatus, at as systemClock, b as ServiceState, c as AccessType, d as FeedTab, et as createDeviceId, f as IncidentKind, g as NotificationType, h as LikesVisibility, it as createDeadline, j as BUCKET_LIMITS, k as operationMethod, l as AttachmentType, m as ItdErrorCode, n as formatNotificationText, o as canonicalNotificationType, ot as installAsyncDisposeFallback, p as InteractionType, q as STATUS_SERVICE, r as normalizeNotification, s as isKnownNotificationType, t as resolveNotificationUrl, u as CommentSort, v as ReportReason, w as WallAccess, x as SpanType, y as ReportTargetType, z as pickString } from "./shared/url-CYXgxqGx.js";
2
- import { A as isItdStateError, C as isItdError, D as isItdPhoneVerificationError, E as isItdNotFoundError, O as isItdRateLimitError, S as isItdConflictError, T as isItdForbiddenError, _ as ItdStateError, a as ItdConfigError, b as isItdApiError, c as ItdErrorKind, d as ItdForbiddenError, f as ItdNetworkError, g as ItdServerError, h as ItdRateLimitError, i as ItdAuthError, j as isItdValidationError, k as isItdServerError, l as ItdFileError, m as ItdPhoneVerificationError, n as ItdApiError, o as ItdConflictError, p as ItdNotFoundError, r as ItdApiErrorKind, s as ItdError, t as ItdAbortError, u as ItdFileErrorReason, v as ItdTimeoutError, w as isItdFileError, x as isItdAuthError, y as ItdValidationError } from "./shared/errors-Bhrd2fJd.js";
3
- import { i as REFRESH_COOKIE_PATH, r as REFRESH_COOKIE } from "./shared/cookies-tX2sNwxb.js";
4
- import { a as RealtimeTransportKind, c as RealtimeUpdateType, d as Emitter, f as reportListenerError, i as ItdRealtime, l as UnauthorizedStreamError, n as RealtimeComposer, o as setRealtimeConnectGuard, r as RealtimeRouter, s as RealtimeUpdateOrigin, t as WebSocketTransport, u as runRealtimeMiddleware } from "./shared/websocket-CbzB1Leq.js";
5
- import { c as isEnumerableKeyValueStore, i as MemoryKeyValueStore, l as withCodec, n as copySession, o as createKeyValueStore, r as createTokenStorage, s as createRecordKeyValueStore, t as MemoryTokenStorage, u as withNamespace } from "./shared/storage-D86edNCB.js";
6
- import { C as BaseResource, E as mapPage, S as isBuilder, T as Paginator, _ as ALLOWED_MIME_TYPES, a as fromStream, b as VIDEO_MIME_TYPES, c as report, d as autoSpans, f as markup, g as utcStampToIso, h as toDate, i as isMyProfile, l as post, m as parseMarkdown, n as renderSpans, o as fromUrl, p as parseHtml, r as statusDays, s as createResources, t as SpanRenderFormat, u as poll, v as AUDIO_MIME_TYPES, w as PaginationMode, x as comment, y as IMAGE_MIME_TYPES } from "./shared/render-vtLixIiU.js";
7
- import { c as FileTransferMode } from "./shared/options-DtATYdLr.js";
8
- import { a as scopedTokenStorage, i as isRestorableSession, n as controlledTokenStorage, r as createMultiTokenStorage, t as MemoryMultiTokenStorage } from "./shared/multi-storage-CkvTUC5m.js";
1
+ import { $ as DEFAULT_BASE_URL, A as ITD_CATALOG, C as ReportReason, D as ViewReason, E as SpanType, F as operationMethod, G as pickString, I as operationRetrySafety, K as RetrySafety, L as BUCKET_LIMITS, M as defineBuiltInOperation, N as isBuiltInOperationId, O as ViewSource, P as operationBucket, Q as orderPluginDefinitions, R as DEFAULT_RATE_LIMIT_BUCKET, S as NotificationType, T as ServiceState, V as pickArray, X as RequestQueuePool, Z as assertPluginRemovable, _ as FeedTab, at as requireOptionalBoolean, b as ItdErrorCode, c as normalizeNotification, ct as systemClock, d as canonicalNotificationType, f as isKnownNotificationType, g as EventChannelStatus, h as CommentSort, it as isRecord, j as OPERATIONS, k as WallAccess, lt as installAsyncDisposeFallback, m as AttachmentType, n as formatNotificationText, nt as resolveRateLimit, o as passthroughOperation, ot as RateLimitPacing, p as AccessType, rt as LIBRARY_VERSION, s as voidOperation, t as resolveNotificationUrl, tt as STATUS_SERVICE, v as IncidentKind, w as ReportTargetType, x as LikesVisibility, y as InteractionType, z as createClientRuntime } from "./shared/url-B3ocFlHj.js";
2
+ import { A as isItdStateError, C as isItdError, D as isItdPhoneVerificationError, E as isItdNotFoundError, O as isItdRateLimitError, S as isItdConflictError, T as isItdForbiddenError, _ as ItdStateError, a as ItdConfigError, b as isItdApiError, c as ItdErrorKind, d as ItdForbiddenError, f as ItdNetworkError, g as ItdServerError, h as ItdRateLimitError, i as ItdAuthError, j as isItdValidationError, k as isItdServerError, l as ItdFileError, m as ItdPhoneVerificationError, n as ItdApiError, o as ItdConflictError, p as ItdNotFoundError, r as ItdApiErrorKind, s as ItdError, t as ItdAbortError, u as ItdFileErrorReason, v as ItdTimeoutError, w as isItdFileError, x as isItdAuthError, y as ItdValidationError } from "./shared/errors-GI10kZxk.js";
3
+ import { _ as RuntimeMode, c as REFRESH_COOKIE, l as REFRESH_COOKIE_PATH, v as createDeviceId } from "./shared/redact-FNK29Fv-.js";
4
+ import { A as ExtensibleOperationCatalog, C as utcStampToIso, D as Paginator, E as PaginationMode, O as mapPage, S as toDate, T as BaseResource, _ as IMAGE_MIME_TYPES, a as fromStream, b as isBuilder, c as report, d as autoSpans, f as markup, g as AUDIO_MIME_TYPES, h as ALLOWED_MIME_TYPES, i as isMyProfile, k as createFeatureHost, l as post, m as parseMarkdown, n as renderSpans, o as fromUrl, p as parseHtml, r as statusDays, s as createResources, t as SpanRenderFormat, u as poll, v as VIDEO_MIME_TYPES, w as NotificationsResource, x as createStatusFeature, y as comment } from "./shared/render-CiL1VS0b.js";
5
+ import { c as FileTransferMode } from "./shared/options-BNP633A5.js";
6
+ import { _ as Emitter, a as WebSocketTransport, c as NotificationEventsTransport, d as NotificationUpdateOrigin, f as NotificationUpdateType, g as runEventMiddleware, h as UnauthorizedStreamError, i as createNotificationEventsClient, l as resolveNotificationEventsOptions, n as EventRouter, o as EVENT_OPERATIONS, p as EventChannel, r as NotificationEventsClient, s as NotificationEvents, t as EventComposer, u as setNotificationEventsConnectGuard, v as reportListenerError } from "./shared/events-TfhKh26b.js";
7
+ import { c as isEnumerableKeyValueStore, i as MemoryKeyValueStore, l as withCodec, n as copySession, o as createKeyValueStore, r as createTokenStorage, s as createRecordKeyValueStore, t as MemoryTokenStorage, u as withNamespace } from "./shared/storage-CCxn0Kkv.js";
8
+ import { a as scopedTokenStorage, i as isRestorableSession, n as controlledTokenStorage, r as createMultiTokenStorage, t as MemoryMultiTokenStorage } from "./shared/multi-storage-1AEZTczk.js";
9
+ //#region src/notifications-api.ts
10
+ var ClientNotifications = class extends NotificationsResource {
11
+ #createEvents;
12
+ #events;
13
+ constructor(http, createEvents) {
14
+ super(http);
15
+ this.#createEvents = createEvents;
16
+ }
17
+ get events() {
18
+ this.#events ??= this.#createEvents(this);
19
+ return this.#events;
20
+ }
21
+ };
22
+ /** Создаёт API уведомлений полного клиента. @internal */
23
+ function createNotificationsApi(http, createEvents) {
24
+ return new ClientNotifications(http, createEvents);
25
+ }
26
+ //#endregion
27
+ //#region src/operations/auth.ts
28
+ /** Чем закончился запрос входа. */
29
+ const SignInStatus = Object.freeze({
30
+ Authenticated: "authenticated",
31
+ OtpRequired: "otp_required"
32
+ });
33
+ function requireString(body, key, message) {
34
+ const value = pickString(body, key);
35
+ if (!value) throw new ItdConfigError(message);
36
+ return value;
37
+ }
38
+ const AUTH_CHECK = passthroughOperation("auth.check");
39
+ const AUTH_SIGN_UP = defineBuiltInOperation("auth.signUp", (body) => requireString(body, "flowToken", "Сервер не вернул flowToken при регистрации"));
40
+ const AUTH_SIGN_IN = defineBuiltInOperation("auth.signIn", (body) => {
41
+ const accessToken = pickString(body, "accessToken");
42
+ return accessToken ? {
43
+ status: SignInStatus.Authenticated,
44
+ accessToken
45
+ } : {
46
+ status: SignInStatus.OtpRequired,
47
+ flowToken: pickString(body, "flowToken")
48
+ };
49
+ });
50
+ const AUTH_VERIFY_OTP = defineBuiltInOperation("auth.verifyOtp", (body) => requireString(body, "accessToken", "Сервер не вернул accessToken после подтверждения кода"));
51
+ const AUTH_REFRESH = defineBuiltInOperation("auth.refresh", (body) => pickString(body, "accessToken"));
52
+ const AUTH_RESEND_OTP = voidOperation("auth.resendOtp");
53
+ const AUTH_LOGOUT = voidOperation("auth.logout");
54
+ const AUTH_FORGOT_PASSWORD = defineBuiltInOperation("auth.forgotPassword", (body) => requireString(body, "flowToken", "Сервер не вернул flowToken при запросе сброса пароля"));
55
+ const AUTH_RESET_PASSWORD = voidOperation("auth.resetPassword");
56
+ const AUTH_CHANGE_PASSWORD = voidOperation("auth.changePassword");
57
+ const AUTH_SESSIONS = defineBuiltInOperation("auth.sessions", (body) => pickArray(body, "sessions"));
58
+ const AUTH_REVOKE_SESSION = voidOperation("auth.revokeSession");
59
+ const AUTH_REVOKE_OTHER_SESSIONS = voidOperation("auth.revokeOtherSessions");
60
+ //#endregion
9
61
  //#region src/session/jwt.ts
10
62
  /**
11
63
  * Разбирает строку base64url.
@@ -122,7 +174,7 @@ function resolveSessionConfig(options, runtime) {
122
174
  * достижимой из любой сборки, включая анонимную.
123
175
  */
124
176
  function createItdAuth(options, deps) {
125
- return new AuthManager(resolveSessionConfig(options, deps.config), deps.handler, deps.cookies, { onAccountChange: deps.onAccountChange });
177
+ return new AuthManager(resolveSessionConfig(options, deps.config), deps.http, deps.cookies, { onAccountChange: deps.onAccountChange });
126
178
  }
127
179
  /** Пути эндпоинтов авторизации. */
128
180
  const AUTH_PATHS = {
@@ -157,11 +209,6 @@ const AUTH_PATHS = {
157
209
  * ```
158
210
  */
159
211
  const TURNSTILE_SITE_KEY = "0x4AAAAAACHhxczw6fJGwPBg";
160
- function readAccessToken(payload) {
161
- if (typeof payload !== "object" || payload === null) return void 0;
162
- const token = payload.accessToken;
163
- return typeof token === "string" && token.length > 0 ? token : void 0;
164
- }
165
212
  /** Убирает поле, которое сохраняли промежуточные версии с поддержкой `getUserId()`. */
166
213
  function withoutLegacyUserId(session) {
167
214
  const clean = { ...session };
@@ -183,7 +230,7 @@ function nextAuthScope() {
183
230
  */
184
231
  var AuthManager = class {
185
232
  #config;
186
- #send;
233
+ #http;
187
234
  #jar;
188
235
  #emitter;
189
236
  #hooks;
@@ -222,9 +269,9 @@ var AuthManager = class {
222
269
  * воскресить уже очищенную сессию поверх `signOut`.
223
270
  */
224
271
  #authEpoch = 0;
225
- constructor(config, send, jar, hooks = {}) {
272
+ constructor(config, http, jar, hooks = {}) {
226
273
  this.#config = config;
227
- this.#send = send;
274
+ this.#http = http;
228
275
  this.#jar = jar;
229
276
  this.#hooks = hooks;
230
277
  this.#emitter = new Emitter((error) => reportListenerError(config.logger, "авторизации", error));
@@ -237,7 +284,7 @@ var AuthManager = class {
237
284
  get once() {
238
285
  return this.#emitter.once.bind(this.#emitter);
239
286
  }
240
- /** Снимает lifecycle-подписки при терминальном освобождении владельца. @internal */
287
+ /** Снимает служебные подписки при окончательном освобождении владельца. @internal */
241
288
  dispose() {
242
289
  this.#invalidateInFlight();
243
290
  this.#emitter.removeAllListeners();
@@ -293,7 +340,7 @@ var AuthManager = class {
293
340
  this.#externalIdentity = current;
294
341
  return token;
295
342
  }
296
- /** Меняет fallback и завершает realtime, только если фактически сменился аккаунт. */
343
+ /** Обновляет резервный идентификатор и закрывает события при смене аккаунта. */
297
344
  #transitionAuth(accessToken, rotateFallback = true) {
298
345
  const knownPrevious = this.#session !== void 0;
299
346
  const previous = this.#identityForToken(this.#session?.accessToken);
@@ -565,13 +612,11 @@ var AuthManager = class {
565
612
  if (!this.#hasRefreshSession()) return this.#reloginOrNull();
566
613
  const epoch = this.#authEpoch;
567
614
  try {
568
- const accessToken = readAccessToken(await this.#send({
569
- operationId: "auth.refresh",
570
- method: operationMethod("auth.refresh"),
615
+ const accessToken = await this.#http.execute(AUTH_REFRESH, {
571
616
  path: AUTH_PATHS.refresh,
572
617
  skipAuth: true,
573
618
  skipAuthRefresh: true
574
- }));
619
+ });
575
620
  if (!accessToken) return this.#reloginOrNull();
576
621
  if (this.#authEpoch !== epoch) return this.#session?.accessToken ?? null;
577
622
  const rotated = this.#jar.getValue(REFRESH_COOKIE, this.#config.baseUrl + REFRESH_COOKIE_PATH);
@@ -638,9 +683,7 @@ var AuthManager = class {
638
683
  }
639
684
  async #performSignIn(credentials) {
640
685
  const turnstileToken = await this.#resolveTurnstileToken(credentials);
641
- const accessToken = readAccessToken(await this.#send({
642
- operationId: "auth.signIn",
643
- method: operationMethod("auth.signIn"),
686
+ const result = await this.#http.execute(AUTH_SIGN_IN, {
644
687
  path: AUTH_PATHS.signIn,
645
688
  body: {
646
689
  email: credentials.email,
@@ -649,8 +692,9 @@ var AuthManager = class {
649
692
  },
650
693
  skipAuth: true,
651
694
  skipAuthRefresh: true
652
- }));
653
- if (!accessToken) throw new ItdConfigError("Вход по email и паролю требует подтверждения кодом из письма. Автоматический вход невозможен: воспользуйтесь itd.auth.signInWithOtp() и передайте полученный accessToken в конфигурацию клиента.");
695
+ });
696
+ if (result.status !== SignInStatus.Authenticated) throw new ItdConfigError("Вход по email и паролю требует подтверждения кодом из письма. Автоматический вход невозможен: воспользуйтесь itd.auth.signInWithOtp() и передайте полученный accessToken в конфигурацию клиента.");
697
+ const accessToken = result.accessToken;
654
698
  this.#invalidateInFlight();
655
699
  this.#transitionAuth(accessToken);
656
700
  await this.#saveSession({
@@ -664,11 +708,12 @@ var AuthManager = class {
664
708
  };
665
709
  //#endregion
666
710
  //#region src/resources/auth.ts
667
- /** Чем закончился вход: сразу токеном или запросом кода подтверждения. */
668
- const SignInStatus = Object.freeze({
669
- Authenticated: "authenticated",
670
- OtpRequired: "otp_required"
671
- });
711
+ /**
712
+ * Результат входа.
713
+ *
714
+ * Сервер может как сразу выдать токен, так и потребовать код подтверждения — размеченное
715
+ * объединение делает оба случая явными.
716
+ */
672
717
  /**
673
718
  * Авторизация, сессии и пароли.
674
719
  *
@@ -686,7 +731,7 @@ var AuthResource = class extends BaseResource {
686
731
  * Без авторизации возвращает `authenticated: false` и `user: null`.
687
732
  */
688
733
  check(options = {}) {
689
- return this.http.operation("auth.check", {
734
+ return this.http.execute(AUTH_CHECK, {
690
735
  path: "/api/profile",
691
736
  ...options
692
737
  });
@@ -696,17 +741,14 @@ var AuthResource = class extends BaseResource {
696
741
  *
697
742
  * @returns `flowToken`, который нужно передать в {@link verifyOtp}
698
743
  */
699
- async signUp(credentials, options = {}) {
700
- const body = await this.http.operation("auth.signUp", {
744
+ signUp(credentials, options = {}) {
745
+ return this.http.execute(AUTH_SIGN_UP, {
701
746
  path: AUTH_PATHS.signUp,
702
747
  body: credentials,
703
748
  skipAuth: true,
704
749
  skipAuthRefresh: true,
705
750
  ...options
706
751
  });
707
- const flowToken = pickString(body, "flowToken");
708
- if (!flowToken) throw new ItdConfigError("Сервер не вернул flowToken при регистрации");
709
- return flowToken;
710
752
  }
711
753
  /**
712
754
  * Выполняет вход.
@@ -719,25 +761,15 @@ var AuthResource = class extends BaseResource {
719
761
  * @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
720
762
  */
721
763
  async signIn(credentials, options = {}) {
722
- const body = await this.http.operation("auth.signIn", {
764
+ const result = await this.http.execute(AUTH_SIGN_IN, {
723
765
  path: AUTH_PATHS.signIn,
724
766
  body: credentials,
725
767
  skipAuth: true,
726
768
  skipAuthRefresh: true,
727
769
  ...options
728
770
  });
729
- const accessToken = pickString(body, "accessToken");
730
- if (accessToken) {
731
- await this.#auth.setAccessToken(accessToken);
732
- return {
733
- status: SignInStatus.Authenticated,
734
- accessToken
735
- };
736
- }
737
- return {
738
- status: SignInStatus.OtpRequired,
739
- flowToken: pickString(body, "flowToken")
740
- };
771
+ if (result.status === SignInStatus.Authenticated) await this.#auth.setAccessToken(result.accessToken);
772
+ return result;
741
773
  }
742
774
  /**
743
775
  * Подтверждает вход кодом из письма.
@@ -745,21 +777,19 @@ var AuthResource = class extends BaseResource {
745
777
  * Полученный токен сохраняется в клиенте автоматически.
746
778
  */
747
779
  async verifyOtp(input, options = {}) {
748
- const body = await this.http.operation("auth.verifyOtp", {
780
+ const accessToken = await this.http.execute(AUTH_VERIFY_OTP, {
749
781
  path: AUTH_PATHS.verifyOtp,
750
782
  body: input,
751
783
  skipAuth: true,
752
784
  skipAuthRefresh: true,
753
785
  ...options
754
786
  });
755
- const accessToken = pickString(body, "accessToken");
756
- if (!accessToken) throw new ItdConfigError("Сервер не вернул accessToken после подтверждения кода");
757
787
  await this.#auth.setAccessToken(accessToken);
758
788
  return accessToken;
759
789
  }
760
790
  /** Отправляет код подтверждения повторно. */
761
791
  resendOtp(input, options = {}) {
762
- return this.http.operation("auth.resendOtp", {
792
+ return this.voidOperation(AUTH_RESEND_OTP, {
763
793
  path: AUTH_PATHS.resendOtp,
764
794
  body: input,
765
795
  skipAuth: true,
@@ -828,7 +858,7 @@ var AuthResource = class extends BaseResource {
828
858
  }
829
859
  /** Завершает текущую сессию на сервере и очищает локальную. */
830
860
  async logout(options = {}) {
831
- await this.http.operation("auth.logout", {
861
+ await this.voidOperation(AUTH_LOGOUT, {
832
862
  path: AUTH_PATHS.logout,
833
863
  skipAuthRefresh: true,
834
864
  ...options
@@ -856,17 +886,14 @@ var AuthResource = class extends BaseResource {
856
886
  *
857
887
  * @returns `flowToken`, который нужно передать в {@link resetPassword}
858
888
  */
859
- async forgotPassword(input, options = {}) {
860
- const body = await this.http.operation("auth.forgotPassword", {
889
+ forgotPassword(input, options = {}) {
890
+ return this.http.execute(AUTH_FORGOT_PASSWORD, {
861
891
  path: AUTH_PATHS.forgotPassword,
862
892
  body: input,
863
893
  skipAuth: true,
864
894
  skipAuthRefresh: true,
865
895
  ...options
866
896
  });
867
- const flowToken = pickString(body, "flowToken");
868
- if (!flowToken) throw new ItdConfigError("Сервер не вернул flowToken при запросе сброса пароля");
869
- return flowToken;
870
897
  }
871
898
  /**
872
899
  * Устанавливает новый пароль по коду из письма.
@@ -875,7 +902,7 @@ var AuthResource = class extends BaseResource {
875
902
  * при нехватке любого отвечает `422`.
876
903
  */
877
904
  resetPassword(input, options = {}) {
878
- return this.http.operation("auth.resetPassword", {
905
+ return this.voidOperation(AUTH_RESET_PASSWORD, {
879
906
  path: AUTH_PATHS.resetPassword,
880
907
  body: input,
881
908
  skipAuth: true,
@@ -923,7 +950,7 @@ var AuthResource = class extends BaseResource {
923
950
  * ```
924
951
  */
925
952
  changePassword(input, options = {}) {
926
- return this.http.operation("auth.changePassword", {
953
+ return this.voidOperation(AUTH_CHANGE_PASSWORD, {
927
954
  path: AUTH_PATHS.changePassword,
928
955
  body: {
929
956
  currentPassword: input.currentPassword,
@@ -933,23 +960,22 @@ var AuthResource = class extends BaseResource {
933
960
  });
934
961
  }
935
962
  /** Загружает список активных сессий. У текущей поле `isCurrent` равно `true`. */
936
- async sessions(options = {}) {
937
- const body = await this.http.operation("auth.sessions", {
963
+ sessions(options = {}) {
964
+ return this.http.execute(AUTH_SESSIONS, {
938
965
  path: AUTH_PATHS.sessions,
939
966
  ...options
940
967
  });
941
- return pickArray(body, "sessions");
942
968
  }
943
969
  /** Завершает указанную сессию. */
944
970
  revokeSession(sessionId, options = {}) {
945
- return this.http.operation("auth.revokeSession", {
971
+ return this.voidOperation(AUTH_REVOKE_SESSION, {
946
972
  path: `${AUTH_PATHS.sessions}/${encodeURIComponent(sessionId)}`,
947
973
  ...options
948
974
  });
949
975
  }
950
976
  /** Завершает все сессии, кроме текущей. */
951
977
  revokeOtherSessions(options = {}) {
952
- return this.http.operation("auth.revokeOtherSessions", {
978
+ return this.voidOperation(AUTH_REVOKE_OTHER_SESSIONS, {
953
979
  path: AUTH_PATHS.sessions,
954
980
  ...options
955
981
  });
@@ -1012,8 +1038,7 @@ function createManagedClient(options, internals) {
1012
1038
  */
1013
1039
  var ItdClient = class ItdClient {
1014
1040
  #runtime;
1015
- /** Порождённые потоки уведомлений — чтобы `close()` мог закрыть их разом. */
1016
- #streams = /* @__PURE__ */ new Set();
1041
+ #features;
1017
1042
  /** Общий результат терминальной очистки для идемпотентных повторных вызовов. */
1018
1043
  #disposePromise;
1019
1044
  /** Ресурсы конвейера запросов; создаются при первом обращении. */
@@ -1074,19 +1099,71 @@ var ItdClient = class ItdClient {
1074
1099
  return this.#resources.telemetry;
1075
1100
  }
1076
1101
  constructor(options = {}) {
1102
+ const notificationEventsOptions = resolveNotificationEventsOptions(options.events?.notifications);
1077
1103
  const internals = CLIENT_INTERNALS.get(options) ?? {};
1104
+ const catalog = new ExtensibleOperationCatalog(ITD_CATALOG);
1105
+ let featureHost;
1078
1106
  this.#runtime = createClientRuntime(options, {
1079
- catalog: ITD_CATALOG,
1107
+ catalog,
1080
1108
  queues: internals.queues,
1081
1109
  assertActive: (action) => assertClientActive(this, action),
1082
1110
  auth: (deps) => createItdAuth(options, {
1083
1111
  ...deps,
1084
- onAccountChange: () => this.#disconnectStreams()
1112
+ onAccountChange: () => featureHost?.stop()
1085
1113
  })
1086
1114
  });
1115
+ this.#features = featureHost = createFeatureHost(this.#runtime, {
1116
+ catalog,
1117
+ assertActive: (action) => assertClientActive(this, action)
1118
+ });
1119
+ const status = this.#features.install(createStatusFeature());
1087
1120
  this.#resources = createResources({
1088
1121
  http: this.#runtime.http,
1089
- fetch: this.#runtime.config.fetch
1122
+ files: this.#features.files,
1123
+ status,
1124
+ createNotifications: (http) => createNotificationsApi(http, (notifications) => {
1125
+ let events;
1126
+ let lifecycleGeneration = 0;
1127
+ let lifecycleActive = false;
1128
+ let unregister;
1129
+ const releaseAfterDrain = (generation) => {
1130
+ events.drain().then(() => {
1131
+ if (lifecycleActive || generation !== lifecycleGeneration) return;
1132
+ unregister?.();
1133
+ unregister = void 0;
1134
+ });
1135
+ };
1136
+ events = new NotificationEvents({
1137
+ connection: this.#runtime.connection(),
1138
+ request: ({ operationId, path, query, signal }) => this.#runtime.http.execute(EVENT_OPERATIONS[operationId], {
1139
+ path,
1140
+ query,
1141
+ signal
1142
+ }),
1143
+ getAuthIdentity: () => this.#runtime.auth.getCurrentAuthIdentity(),
1144
+ getAuthScope: () => this.#runtime.auth.getAuthScope(),
1145
+ fetchUnreadCount: (signal) => notifications.count({ signal }),
1146
+ onConnect: () => {
1147
+ lifecycleGeneration += 1;
1148
+ unregister ??= this.#features.manage({
1149
+ kind: `notifications:${events.transport}`,
1150
+ stop: () => {
1151
+ if (lifecycleActive) events.disconnect();
1152
+ },
1153
+ drain: () => events.drain()
1154
+ });
1155
+ lifecycleActive = true;
1156
+ },
1157
+ onClose: () => {
1158
+ lifecycleActive = false;
1159
+ releaseAfterDrain(++lifecycleGeneration);
1160
+ },
1161
+ logger: this.#runtime.config.logger,
1162
+ clock: this.#runtime.config.clock
1163
+ }, notificationEventsOptions);
1164
+ setNotificationEventsConnectGuard(events, () => assertClientActive(this, "подключить канал событий уведомлений"));
1165
+ return events;
1166
+ })
1090
1167
  });
1091
1168
  CLIENT_PLUGIN_REGISTRIES.set(this, this.#runtime.plugins);
1092
1169
  }
@@ -1130,11 +1207,46 @@ var ItdClient = class ItdClient {
1130
1207
  return this.#runtime.rateLimitState();
1131
1208
  }
1132
1209
  /**
1210
+ * Устанавливает предметный модуль с общей сессией и обработкой запросов клиента.
1211
+ *
1212
+ * Сервисы, операции и бакеты регистрируются до синхронного `setup()`. Возвращает
1213
+ * типизированный API модуля.
1214
+ */
1215
+ install(feature) {
1216
+ return this.#features.install(feature);
1217
+ }
1218
+ /**
1219
+ * Устанавливает модуль и добавляет его API в свойство клиента только для чтения.
1220
+ *
1221
+ * Возвращаемое пересечение сохраняет тип уже подключённых свойств, поэтому вызовы можно
1222
+ * объединять в цепочку: `new ItdClient().withFeature('pixelBattle', pixelBattleFeature)`.
1223
+ */
1224
+ withFeature(key, feature) {
1225
+ assertClientActive(this, "установить feature");
1226
+ if (typeof key !== "string" || key.trim() === "" || key !== key.trim()) throw new ItdConfigError("Свойство feature должно иметь непустое имя без краевых пробелов");
1227
+ if (key === "then" || key in this) throw new ItdConfigError(`Свойство клиента «${key}» уже занято или зарезервировано`);
1228
+ if (!Object.isExtensible(this)) throw new ItdConfigError("Нельзя опубликовать feature на нерасширяемом клиенте");
1229
+ const api = this.install(feature);
1230
+ Object.defineProperty(this, key, {
1231
+ value: api,
1232
+ enumerable: true,
1233
+ configurable: false,
1234
+ writable: false
1235
+ });
1236
+ return this;
1237
+ }
1238
+ /** Имена установленных модулей в порядке установки. */
1239
+ featureNames() {
1240
+ return this.#features.names();
1241
+ }
1242
+ /** Установлен ли модуль с таким именем. */
1243
+ hasFeature(name) {
1244
+ return this.#features.has(name);
1245
+ }
1246
+ /**
1133
1247
  * Подключает плагин.
1134
1248
  *
1135
- * Плагин может независимо регистрировать transformer логической операции и interceptor
1136
- * транспортной попытки. Оба контракта охватывают все методы клиента. Подключать плагин
1137
- * можно в любой момент, но обычно это делают сразу после создания клиента.
1249
+ * Плагин может регистрировать обёртки операций и перехватчики сетевых попыток.
1138
1250
  *
1139
1251
  * @throws {ItdConfigError} если плагин задан неверно или уже подключён
1140
1252
  * @throws {ItdStateError} если клиент уже освобождён через {@link dispose}
@@ -1237,86 +1349,42 @@ var ItdClient = class ItdClient {
1237
1349
  return this.#runtime.auth.on(event, listener);
1238
1350
  }
1239
1351
  /**
1240
- * Создаёт поток уведомлений в реальном времени.
1241
- *
1242
- * Каждый вызов даёт новый независимый поток; обычно он нужен один на приложение.
1243
- * Соединение поднимается методом `connect()` и держится само. Замена авторизации на токен
1244
- * другого пользователя завершает все потоки клиента; смена только сессии их не затрагивает.
1245
- *
1246
- * @example
1247
- * ```ts
1248
- * import { NotificationType } from 'itd-api';
1249
- *
1250
- * const stream = itd.realtime();
1251
- *
1252
- * stream.onNotification(NotificationType.PostComment, async ({ update }) => {
1253
- * await handleComment(update.data.notification);
1254
- * });
1255
- * stream.on('unreadCount', (count) => setBadge(count));
1256
- *
1257
- * await stream.connect();
1258
- * ```
1259
- *
1260
- * @throws {ItdStateError} если клиент уже освобождён через {@link dispose}
1261
- */
1262
- realtime(options = {}) {
1263
- assertClientActive(this, "создать realtime-поток");
1264
- let stream;
1265
- stream = new ItdRealtime({
1266
- baseUrl: this.#runtime.config.baseUrl,
1267
- fetch: this.#runtime.config.fetch,
1268
- request: ({ operationId, path, query, signal }) => this.#runtime.http.operation(operationId, {
1269
- path,
1270
- query,
1271
- signal
1272
- }),
1273
- baseHeaders: (url) => this.#runtime.platformHeaders(url),
1274
- getAuthIdentity: () => this.#runtime.auth.getCurrentAuthIdentity(),
1275
- getAuthScope: () => this.#runtime.auth.getAuthScope(),
1276
- getToken: () => this.#runtime.auth.token(),
1277
- refresh: () => this.#runtime.auth.onUnauthorized(),
1278
- fetchUnreadCount: () => this.notifications.count(),
1279
- onConnect: () => this.#streams.add(stream),
1280
- onClose: () => this.#streams.delete(stream),
1281
- logger: this.#runtime.config.logger,
1282
- clock: this.#runtime.config.clock
1283
- }, options);
1284
- setRealtimeConnectGuard(stream, () => assertClientActive(this, "подключить realtime-поток"));
1285
- this.#streams.add(stream);
1286
- return stream;
1287
- }
1288
- /**
1289
1352
  * Освобождает ресурсы клиента: закрывает все потоки уведомлений, отправляет открытые
1290
1353
  * накопители {@link telemetry}, затем останавливает очередь запросов.
1291
1354
  *
1292
- * Метод дожидается активных обработчиков потока, но не дольше `shutdownTimeout`. После
1293
- * вызова клиентом можно пользоваться снова; ранее созданный поток можно запустить
1294
- * повторным `connect()`.
1355
+ * Метод дожидается завершения активных обработчиков и освобождения ресурсов остановленных
1356
+ * событийных соединений, но не дольше `shutdownTimeout`. После вызова клиентом можно
1357
+ * пользоваться снова; ранее созданный поток можно запустить повторным `connect()`.
1295
1358
  *
1296
1359
  * Общая очередь, полученная от {@link ItdAccounts}, не останавливается: её гасит сам
1297
1360
  * контейнер, когда закрывает все аккаунты разом.
1298
1361
  *
1299
1362
  * Терминальное освобождение — это {@link dispose}.
1300
1363
  *
1301
- * @throws {ItdStateError} если обработчики потока не завершились за отведённый срок
1364
+ * @throws {ItdStateError} если остановка событийного соединения не завершилась за отведённый срок
1302
1365
  */
1303
1366
  async close() {
1304
1367
  return this.#close(false);
1305
1368
  }
1306
1369
  async #close(disposeCleanup) {
1307
- const streams = this.#disconnectStreams();
1308
1370
  if (disposeCleanup) this.#resources.prepareTelemetryClose();
1309
- const { shutdownTimeout, clock } = this.#runtime.config;
1310
- const deadline = createDeadline(shutdownTimeout, clock);
1311
- let stuck = [];
1371
+ const errors = [];
1312
1372
  try {
1313
- stuck = (await Promise.all(streams.map(async (stream) => await deadline.wait(stream.drain()) ? void 0 : stream))).filter((stream) => stream !== void 0);
1314
- await this.#resources.closeTelemetry(disposeCleanup);
1373
+ try {
1374
+ await this.#features.close();
1375
+ } catch (error) {
1376
+ errors.push(error);
1377
+ }
1378
+ try {
1379
+ await this.#resources.closeTelemetry(disposeCleanup);
1380
+ } catch (error) {
1381
+ errors.push(error);
1382
+ }
1315
1383
  } finally {
1316
- deadline.cancel();
1317
1384
  this.#runtime.close();
1318
1385
  }
1319
- if (stuck.length > 0) throw new ItdStateError(`обработчики потоков (${stuck.map((stream) => stream.transport).join(", ")}) не завершились за ${shutdownTimeout} мс; ожидание прекращено`);
1386
+ if (errors.length === 1) throw errors[0];
1387
+ if (errors.length > 1) throw new AggregateError(errors, "Не удалось закрыть клиент");
1320
1388
  }
1321
1389
  /**
1322
1390
  * Окончательно освобождает клиент: выполняет {@link close}, отменяет незавершённые
@@ -1324,10 +1392,11 @@ var ItdClient = class ItdClient {
1324
1392
  *
1325
1393
  * Терминальное состояние устанавливается сразу при первом вызове. После этого новые
1326
1394
  * запросы, подключение плагинов, регистрация сервисов и создание или повторный запуск
1327
- * realtime-потоков завершаются с {@link ItdStateError}. Повторные вызовы возвращают
1395
+ * событийных каналов завершаются с {@link ItdStateError}. Повторные вызовы возвращают
1328
1396
  * тот же результат очистки.
1329
1397
  *
1330
- * Ожидание обработчиков потока и операций плагинов ограничено `shutdownTimeout`.
1398
+ * Ожидание остановки событийных соединений и операций плагинов ограничено
1399
+ * `shutdownTimeout`.
1331
1400
  *
1332
1401
  * @example
1333
1402
  * ```ts
@@ -1349,6 +1418,11 @@ var ItdClient = class ItdClient {
1349
1418
  } catch (error) {
1350
1419
  errors.push(error);
1351
1420
  }
1421
+ try {
1422
+ await this.#features.dispose();
1423
+ } catch (error) {
1424
+ errors.push(error);
1425
+ }
1352
1426
  try {
1353
1427
  await this.#runtime.dispose();
1354
1428
  } catch (error) {
@@ -1356,13 +1430,6 @@ var ItdClient = class ItdClient {
1356
1430
  }
1357
1431
  if (errors.length > 0) throw new AggregateError(errors, "Не удалось освободить клиент");
1358
1432
  }
1359
- /** Завершает потоки до того, как запросы начнут использовать другой аккаунт. */
1360
- #disconnectStreams() {
1361
- const streams = [...this.#streams];
1362
- for (const stream of streams) stream.disconnect();
1363
- this.#streams.clear();
1364
- return streams;
1365
- }
1366
1433
  /** Позволяет использовать клиент с `await using`. */
1367
1434
  [Symbol.asyncDispose]() {
1368
1435
  return this.dispose();
@@ -1428,6 +1495,25 @@ function createClient(options = {}) {
1428
1495
  function validateAccountName(name) {
1429
1496
  if (typeof name !== "string" || name.trim() === "") throw new ItdConfigError("имя аккаунта должно быть непустой строкой");
1430
1497
  }
1498
+ /** Проверяет фабрики до создания первого аккаунта. */
1499
+ function resolveAccountFeatures(features) {
1500
+ if (features === void 0) return [];
1501
+ if (!Array.isArray(features)) throw new ItdConfigError("features должен быть массивом фабрик подключаемых модулей");
1502
+ const keys = /* @__PURE__ */ new Set();
1503
+ return features.map((feature, index) => {
1504
+ if (typeof feature !== "object" || feature === null || typeof feature.create !== "function") throw new ItdConfigError(`features[${index}] должен предоставлять create()`);
1505
+ if (feature.key !== void 0) {
1506
+ if (typeof feature.key !== "string" || feature.key.trim() === "" || feature.key !== feature.key.trim()) throw new ItdConfigError(`features[${index}].key должен быть непустой строкой без пробелов`);
1507
+ if (keys.has(feature.key)) throw new ItdConfigError(`Свойство подключаемого модуля «${feature.key}» объявлено повторно`);
1508
+ keys.add(feature.key);
1509
+ }
1510
+ const create = feature.create.bind(feature);
1511
+ return Object.freeze({
1512
+ ...feature.key === void 0 ? {} : { key: feature.key },
1513
+ create
1514
+ });
1515
+ });
1516
+ }
1431
1517
  const DISPOSED_ACCOUNTS = /* @__PURE__ */ new WeakSet();
1432
1518
  function assertAccountsActive(accounts, action) {
1433
1519
  if (!DISPOSED_ACCOUNTS.has(accounts)) return;
@@ -1479,6 +1565,8 @@ var ItdAccounts = class ItdAccounts {
1479
1565
  #eventUnsubscribers = /* @__PURE__ */ new Map();
1480
1566
  /** Плагины для всех: и для уже заведённых аккаунтов, и для будущих. */
1481
1567
  #plugins;
1568
+ /** Фабрики модулей для новых и восстанавливаемых аккаунтов. */
1569
+ #features;
1482
1570
  /** Имена плагинов, чья асинхронная очистка ещё не завершилась. */
1483
1571
  #removingPlugins = /* @__PURE__ */ new Set();
1484
1572
  /** Общая очередь. `undefined`, когда у каждого аккаунта своя. */
@@ -1489,11 +1577,12 @@ var ItdAccounts = class ItdAccounts {
1489
1577
  /** Общий результат терминальной очистки для идемпотентных повторных вызовов. */
1490
1578
  #disposePromise;
1491
1579
  constructor(options = {}) {
1492
- const { storage, plugins, rateLimitScope, ...base } = options;
1580
+ const { storage, plugins, features, rateLimitScope, ...base } = options;
1493
1581
  if (rateLimitScope !== void 0 && rateLimitScope !== "account" && rateLimitScope !== "shared") throw new ItdConfigError("rateLimitScope должен быть 'account' или 'shared'");
1494
1582
  this.#base = base;
1495
1583
  this.#storage = storage ?? new MemoryMultiTokenStorage();
1496
1584
  this.#plugins = orderPluginDefinitions(plugins ?? []);
1585
+ this.#features = resolveAccountFeatures(features);
1497
1586
  this.#rateLimitScope = rateLimitScope ?? "shared";
1498
1587
  const rateLimit = this.#rateLimitScope === "shared" ? resolveRateLimit(base.rateLimit, ITD_CATALOG) : void 0;
1499
1588
  this.#queues = rateLimit ? new RequestQueuePool(rateLimit, base.clock ?? systemClock) : void 0;
@@ -1520,7 +1609,7 @@ var ItdAccounts = class ItdAccounts {
1520
1609
  /**
1521
1610
  * Заводит аккаунт.
1522
1611
  *
1523
- * Возвращается обычный `ItdClient` — со всеми ресурсами, плагинами и `realtime()`.
1612
+ * Возвращается обычный `ItdClient` — со всеми ресурсами, плагинами и каналом событий.
1524
1613
  * Хранилище ему подставляется само: срез общего по имени аккаунта.
1525
1614
  *
1526
1615
  * Опция `auth` не обязательна: когда сессия этого аккаунта уже лежит в хранилище,
@@ -1546,10 +1635,15 @@ var ItdAccounts = class ItdAccounts {
1546
1635
  try {
1547
1636
  client = createManagedClient(this.#mergeOptions(options, storageControl.storage), this.#queues ? { queues: this.#queues } : {});
1548
1637
  for (const plugin of this.#plugins) client.use(plugin);
1638
+ for (const feature of this.#features) {
1639
+ const manifest = feature.create();
1640
+ if (feature.key === void 0) client.install(manifest);
1641
+ else client.withFeature(feature.key, manifest);
1642
+ }
1549
1643
  } catch (error) {
1550
1644
  storageControl.revoke();
1551
1645
  if (client) client.dispose().catch((cleanupError) => {
1552
- this.#reportPluginCleanup("неудачного добавления аккаунта", cleanupError);
1646
+ this.#reportCleanup("неудачного добавления аккаунта", cleanupError);
1553
1647
  });
1554
1648
  throw error;
1555
1649
  }
@@ -1673,7 +1767,7 @@ var ItdAccounts = class ItdAccounts {
1673
1767
  }
1674
1768
  } catch (error) {
1675
1769
  for (const client of installed.reverse()) client.unuse(plugin.name).catch((cleanupError) => {
1676
- this.#reportPluginCleanup(`отката плагина «${plugin.name}»`, cleanupError);
1770
+ this.#reportCleanup(`отката плагина «${plugin.name}»`, cleanupError);
1677
1771
  });
1678
1772
  throw error;
1679
1773
  }
@@ -1789,6 +1883,7 @@ var ItdAccounts = class ItdAccounts {
1789
1883
  Promise.resolve().then(() => this.#queues?.clear())
1790
1884
  ]);
1791
1885
  this.#plugins.splice(0);
1886
+ this.#features.splice(0);
1792
1887
  this.#removingPlugins.clear();
1793
1888
  const errors = results.filter((result) => result.status === "rejected").map((result) => result.reason);
1794
1889
  if (errors.length > 0) throw new AggregateError(errors, "Не удалось освободить аккаунты");
@@ -1798,11 +1893,7 @@ var ItdAccounts = class ItdAccounts {
1798
1893
  return this.dispose();
1799
1894
  }
1800
1895
  static {
1801
- if (typeof Symbol.asyncDispose !== "symbol") {
1802
- const prototype = ItdAccounts.prototype;
1803
- prototype[Symbol.for("Symbol.asyncDispose")] = prototype.undefined;
1804
- delete prototype.undefined;
1805
- }
1896
+ installAsyncDisposeFallback(ItdAccounts);
1806
1897
  }
1807
1898
  /**
1808
1899
  * Собирает опции клиента: общие, поверх них — аккаунтные, и обязательно свой срез
@@ -1827,9 +1918,9 @@ var ItdAccounts = class ItdAccounts {
1827
1918
  storage
1828
1919
  };
1829
1920
  }
1830
- /** Не теряет ошибку фонового teardown, который синхронный API не может await-нуть. */
1831
- #reportPluginCleanup(scope, error) {
1832
- const message = `Не удалось завершить teardown после ${scope}`;
1921
+ /** Не теряет ошибку фонового освобождения ресурсов, которого синхронный API не ждёт. */
1922
+ #reportCleanup(scope, error) {
1923
+ const message = `Не удалось освободить ресурсы после ${scope}`;
1833
1924
  if (this.#logger) this.#logger.error(message, error);
1834
1925
  else console.error(`[itd-api] ${message}`, error);
1835
1926
  }
@@ -1861,6 +1952,6 @@ function createAccounts(options = {}) {
1861
1952
  return new ItdAccounts(options);
1862
1953
  }
1863
1954
  //#endregion
1864
- export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AccessType, AttachmentType, BUCKET_LIMITS, CommentSort, DEFAULT_BASE_URL, DEFAULT_RATE_LIMIT_BUCKET, FeedTab, FileTransferMode, IMAGE_MIME_TYPES, IncidentKind, InteractionType, ItdAbortError, ItdAccounts, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdFileError, ItdFileErrorReason, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdStateError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, MemoryKeyValueStore, MemoryMultiTokenStorage, MemoryTokenStorage, NotificationType, OPERATIONS, PaginationMode, Paginator, RateLimitPacing, RealtimeComposer, RealtimeRouter, RealtimeStatus, RealtimeTransportKind, RealtimeUpdateOrigin, RealtimeUpdateType, ReportReason, ReportTargetType, RetrySafety, RuntimeMode, STATUS_SERVICE, ServiceState, SignInStatus, SpanRenderFormat, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, WebSocketTransport, autoSpans, canonicalNotificationType, comment, createAccounts, createClient, createKeyValueStore, createMultiTokenStorage, createRecordKeyValueStore, createTokenStorage, formatNotificationText, fromStream, fromUrl, isBuilder, isBuiltInOperationId, isEnumerableKeyValueStore, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdFileError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdStateError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, markup, normalizeNotification, operationBucket, operationMethod, operationRetrySafety, parseHtml, parseMarkdown, poll, post, renderSpans, report, resolveNotificationUrl, runRealtimeMiddleware, scopedTokenStorage, statusDays, systemClock, toDate, utcStampToIso, withCodec, withNamespace };
1955
+ export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AccessType, AttachmentType, BUCKET_LIMITS, CommentSort, DEFAULT_BASE_URL, DEFAULT_RATE_LIMIT_BUCKET, EventChannel, EventChannelStatus, EventComposer, EventRouter, FeedTab, FileTransferMode, IMAGE_MIME_TYPES, IncidentKind, InteractionType, ItdAbortError, ItdAccounts, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdFileError, ItdFileErrorReason, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdServerError, ItdStateError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, MemoryKeyValueStore, MemoryMultiTokenStorage, MemoryTokenStorage, NotificationEvents, NotificationEventsClient, NotificationEventsTransport, NotificationType, NotificationUpdateOrigin, NotificationUpdateType, OPERATIONS, PaginationMode, Paginator, RateLimitPacing, ReportReason, ReportTargetType, RetrySafety, RuntimeMode, STATUS_SERVICE, ServiceState, SignInStatus, SpanRenderFormat, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, WebSocketTransport, autoSpans, canonicalNotificationType, comment, createAccounts, createClient, createKeyValueStore, createMultiTokenStorage, createNotificationEventsClient, createRecordKeyValueStore, createTokenStorage, formatNotificationText, fromStream, fromUrl, isBuilder, isBuiltInOperationId, isEnumerableKeyValueStore, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdFileError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdStateError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, markup, normalizeNotification, operationBucket, operationMethod, operationRetrySafety, parseHtml, parseMarkdown, poll, post, renderSpans, report, resolveNotificationUrl, runEventMiddleware, scopedTokenStorage, statusDays, systemClock, toDate, utcStampToIso, withCodec, withNamespace };
1865
1956
 
1866
1957
  //# sourceMappingURL=index.js.map