itd-api 0.0.3 → 0.0.5

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.
@@ -16,6 +16,30 @@ function resolveInput(input, factory, validate) {
16
16
 
17
17
  // src/core/errors.ts
18
18
  var ITD_ERROR = /* @__PURE__ */ Symbol.for("itd.error");
19
+ var ItdErrorKind = Object.freeze({
20
+ /** Сервер ответил статусом ≥ 400. */
21
+ Api: "api",
22
+ /** Запрос не дошёл до сервера. */
23
+ Network: "network",
24
+ /** Истёк таймаут запроса. */
25
+ Timeout: "timeout",
26
+ /** Запрос отменён через `AbortSignal`. */
27
+ Abort: "abort",
28
+ /** Некорректная конфигурация или аргументы — обнаружено до обращения к сети. */
29
+ Config: "config"
30
+ });
31
+ var ItdApiErrorKind = Object.freeze({
32
+ /** Ни одна из специализаций не подошла. */
33
+ Generic: "generic",
34
+ Validation: "validation",
35
+ Auth: "auth",
36
+ Forbidden: "forbidden",
37
+ NotFound: "not_found",
38
+ Conflict: "conflict",
39
+ RateLimit: "rate_limit",
40
+ PhoneVerification: "phone_verification",
41
+ Server: "server"
42
+ });
19
43
  var ItdError = class extends Error {
20
44
  /** @internal */
21
45
  [ITD_ERROR] = true;
@@ -28,6 +52,13 @@ var ItdError = class extends Error {
28
52
  }
29
53
  };
30
54
  var ItdApiError = class extends ItdError {
55
+ /**
56
+ * Разновидность ошибки: та же информация, что и класс, но пригодная для сравнения.
57
+ *
58
+ * Позволяет разбирать ошибку через `switch`, а проверкам вроде {@link isItdAuthError} —
59
+ * работать даже когда в проекте оказались две копии библиотеки.
60
+ */
61
+ apiKind;
31
62
  /** HTTP-статус ответа. */
32
63
  status;
33
64
  /** Строковый код ошибки, например `VALIDATION_ERROR`. */
@@ -58,9 +89,13 @@ var ItdApiError = class extends ItdError {
58
89
  rateLimit;
59
90
  /** Сколько запросов осталось в окне — заголовок `x-ratelimit-remaining`. */
60
91
  rateLimitRemaining;
61
- constructor(init) {
62
- super("api", init.message);
92
+ /**
93
+ * @param apiKind разновидность; подставляется подклассами, снаружи задавать не нужно
94
+ */
95
+ constructor(init, apiKind = ItdApiErrorKind.Generic) {
96
+ super(ItdErrorKind.Api, init.message);
63
97
  this.name = "ItdApiError";
98
+ this.apiKind = apiKind;
64
99
  this.status = init.status;
65
100
  this.code = init.code;
66
101
  this.detail = init.detail;
@@ -93,37 +128,37 @@ var ItdApiError = class extends ItdError {
93
128
  };
94
129
  var ItdValidationError = class extends ItdApiError {
95
130
  constructor(init) {
96
- super(init);
131
+ super(init, ItdApiErrorKind.Validation);
97
132
  this.name = "ItdValidationError";
98
133
  }
99
134
  };
100
135
  var ItdAuthError = class extends ItdApiError {
101
136
  constructor(init) {
102
- super(init);
137
+ super(init, ItdApiErrorKind.Auth);
103
138
  this.name = "ItdAuthError";
104
139
  }
105
140
  };
106
141
  var ItdForbiddenError = class extends ItdApiError {
107
142
  constructor(init) {
108
- super(init);
143
+ super(init, ItdApiErrorKind.Forbidden);
109
144
  this.name = "ItdForbiddenError";
110
145
  }
111
146
  };
112
147
  var ItdNotFoundError = class extends ItdApiError {
113
148
  constructor(init) {
114
- super(init);
149
+ super(init, ItdApiErrorKind.NotFound);
115
150
  this.name = "ItdNotFoundError";
116
151
  }
117
152
  };
118
153
  var ItdConflictError = class extends ItdApiError {
119
154
  constructor(init) {
120
- super(init);
155
+ super(init, ItdApiErrorKind.Conflict);
121
156
  this.name = "ItdConflictError";
122
157
  }
123
158
  };
124
159
  var ItdRateLimitError = class extends ItdApiError {
125
160
  constructor(init) {
126
- super(init);
161
+ super(init, ItdApiErrorKind.RateLimit);
127
162
  this.name = "ItdRateLimitError";
128
163
  }
129
164
  };
@@ -131,14 +166,14 @@ var ItdPhoneVerificationError = class extends ItdApiError {
131
166
  /** Ссылка на бота подтверждения, если удалось определить идентификатор пользователя. */
132
167
  verificationUrl;
133
168
  constructor(init) {
134
- super(init);
169
+ super(init, ItdApiErrorKind.PhoneVerification);
135
170
  this.name = "ItdPhoneVerificationError";
136
171
  this.verificationUrl = init.userId ? `https://t.me/itd_verification_bot?start=${encodeURIComponent(init.userId)}` : void 0;
137
172
  }
138
173
  };
139
174
  var ItdServerError = class extends ItdApiError {
140
175
  constructor(init) {
141
- super(init);
176
+ super(init, ItdApiErrorKind.Server);
142
177
  this.name = "ItdServerError";
143
178
  }
144
179
  };
@@ -148,7 +183,7 @@ var ItdNetworkError = class extends ItdError {
148
183
  /** Путь запроса без базового URL. */
149
184
  path;
150
185
  constructor(message, init) {
151
- super("network", message, { cause: init.cause });
186
+ super(ItdErrorKind.Network, message, { cause: init.cause });
152
187
  this.name = "ItdNetworkError";
153
188
  this.method = init.method;
154
189
  this.path = init.path;
@@ -162,7 +197,10 @@ var ItdTimeoutError = class extends ItdError {
162
197
  /** Путь запроса без базового URL. */
163
198
  path;
164
199
  constructor(init) {
165
- super("timeout", `\u0417\u0430\u043F\u0440\u043E\u0441 ${init.method} ${init.path} \u043F\u0440\u0435\u0432\u044B\u0441\u0438\u043B \u0442\u0430\u0439\u043C\u0430\u0443\u0442 ${init.timeout} \u043C\u0441`);
200
+ super(
201
+ ItdErrorKind.Timeout,
202
+ `\u0417\u0430\u043F\u0440\u043E\u0441 ${init.method} ${init.path} \u043F\u0440\u0435\u0432\u044B\u0441\u0438\u043B \u0442\u0430\u0439\u043C\u0430\u0443\u0442 ${init.timeout} \u043C\u0441`
203
+ );
166
204
  this.name = "ItdTimeoutError";
167
205
  this.timeout = init.timeout;
168
206
  this.method = init.method;
@@ -171,13 +209,13 @@ var ItdTimeoutError = class extends ItdError {
171
209
  };
172
210
  var ItdAbortError = class extends ItdError {
173
211
  constructor(message = "\u0417\u0430\u043F\u0440\u043E\u0441 \u043E\u0442\u043C\u0435\u043D\u0451\u043D") {
174
- super("abort", message);
212
+ super(ItdErrorKind.Abort, message);
175
213
  this.name = "ItdAbortError";
176
214
  }
177
215
  };
178
216
  var ItdConfigError = class extends ItdError {
179
217
  constructor(message) {
180
- super("config", message);
218
+ super(ItdErrorKind.Config, message);
181
219
  this.name = "ItdConfigError";
182
220
  }
183
221
  };
@@ -185,16 +223,34 @@ function isItdError(value) {
185
223
  return typeof value === "object" && value !== null && ITD_ERROR in value;
186
224
  }
187
225
  function isItdApiError(value) {
188
- return isItdError(value) && value.kind === "api";
226
+ return isItdError(value) && value.kind === ItdErrorKind.Api;
227
+ }
228
+ function hasApiKind(value, kind) {
229
+ return isItdApiError(value) && value.apiKind === kind;
189
230
  }
190
231
  function isItdValidationError(value) {
191
- return isItdApiError(value) && value instanceof ItdValidationError;
232
+ return hasApiKind(value, ItdApiErrorKind.Validation);
192
233
  }
193
234
  function isItdAuthError(value) {
194
- return isItdApiError(value) && value instanceof ItdAuthError;
235
+ return hasApiKind(value, ItdApiErrorKind.Auth);
236
+ }
237
+ function isItdForbiddenError(value) {
238
+ return hasApiKind(value, ItdApiErrorKind.Forbidden);
239
+ }
240
+ function isItdNotFoundError(value) {
241
+ return hasApiKind(value, ItdApiErrorKind.NotFound);
242
+ }
243
+ function isItdConflictError(value) {
244
+ return hasApiKind(value, ItdApiErrorKind.Conflict);
195
245
  }
196
246
  function isItdRateLimitError(value) {
197
- return isItdApiError(value) && value instanceof ItdRateLimitError;
247
+ return hasApiKind(value, ItdApiErrorKind.RateLimit);
248
+ }
249
+ function isItdPhoneVerificationError(value) {
250
+ return hasApiKind(value, ItdApiErrorKind.PhoneVerification);
251
+ }
252
+ function isItdServerError(value) {
253
+ return hasApiKind(value, ItdApiErrorKind.Server);
198
254
  }
199
255
 
200
256
  // src/builders/comment.ts
@@ -624,13 +680,14 @@ var ItdErrorCode = Object.freeze({
624
680
 
625
681
  // src/builders/report.ts
626
682
  var REASONS = new Set(Object.values(ReportReason));
683
+ var TARGET_TYPES = new Set(Object.values(ReportTargetType));
627
684
  function validateReport(input) {
628
685
  if (!input?.targetId || typeof input.targetId !== "string") {
629
686
  throw new ItdConfigError("\u0416\u0430\u043B\u043E\u0431\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043E\u0431\u044A\u0435\u043A\u0442\u0430 (targetId)");
630
687
  }
631
- if (input.targetType !== "post" && input.targetType !== "comment" && input.targetType !== "user") {
688
+ if (!TARGET_TYPES.has(input.targetType)) {
632
689
  throw new ItdConfigError(
633
- `targetType \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C 'post', 'comment' \u0438\u043B\u0438 'user', \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${String(input.targetType)}`
690
+ `targetType \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043E\u0434\u043D\u0438\u043C \u0438\u0437 ${[...TARGET_TYPES].join(", ")}, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${String(input.targetType)}`
634
691
  );
635
692
  }
636
693
  if (!REASONS.has(input.reason)) {
@@ -668,11 +725,11 @@ function start(targetType, targetId) {
668
725
  }
669
726
  var report = Object.freeze({
670
727
  /** Жалоба на пост. */
671
- post: (postId) => start("post", postId),
728
+ post: (postId) => start(ReportTargetType.Post, postId),
672
729
  /** Жалоба на комментарий. */
673
- comment: (commentId) => start("comment", commentId),
730
+ comment: (commentId) => start(ReportTargetType.Comment, commentId),
674
731
  /** Жалоба на пользователя. */
675
- user: (userId) => start("user", userId)
732
+ user: (userId) => start(ReportTargetType.User, userId)
676
733
  });
677
734
  function resolveReport(input) {
678
735
  return resolveInput(input, () => new ReportBuilder({}), validateReport);
@@ -1101,21 +1158,35 @@ var Emitter = class {
1101
1158
  };
1102
1159
 
1103
1160
  // src/core/runtime.ts
1161
+ var RuntimeMode = Object.freeze({
1162
+ /** Определяется по среде исполнения. Значение по умолчанию. */
1163
+ Auto: "auto",
1164
+ /** Cookie ведёт браузер, запросы уходят с `credentials: 'include'`. */
1165
+ Browser: "browser",
1166
+ /** Cookie ведёт встроенный jar, заголовок `Cookie` подставляется вручную. */
1167
+ Server: "server"
1168
+ });
1169
+ var DetectedRuntime = Object.freeze({
1170
+ Browser: "browser",
1171
+ /** Есть `window`, но нет `document`; cookie ведёт нативный сетевой слой. */
1172
+ ReactNative: "react-native",
1173
+ Server: "server"
1174
+ });
1104
1175
  function detectRuntime() {
1105
1176
  const nav = globalThis.navigator;
1106
- if (nav?.product === "ReactNative") return "react-native";
1107
- if (typeof document !== "undefined") return "browser";
1108
- return "server";
1177
+ if (nav?.product === "ReactNative") return DetectedRuntime.ReactNative;
1178
+ if (typeof document !== "undefined") return DetectedRuntime.Browser;
1179
+ return DetectedRuntime.Server;
1109
1180
  }
1110
1181
  function shouldUseCookieJar(mode) {
1111
- if (mode === "browser") return false;
1112
- if (mode === "server") return true;
1113
- return detectRuntime() === "server";
1182
+ if (mode === RuntimeMode.Browser) return false;
1183
+ if (mode === RuntimeMode.Server) return true;
1184
+ return detectRuntime() === DetectedRuntime.Server;
1114
1185
  }
1115
1186
  function shouldSendCredentials(mode) {
1116
- if (mode === "browser") return true;
1117
- if (mode === "server") return false;
1118
- return detectRuntime() === "browser";
1187
+ if (mode === RuntimeMode.Browser) return true;
1188
+ if (mode === RuntimeMode.Server) return false;
1189
+ return detectRuntime() === DetectedRuntime.Browser;
1119
1190
  }
1120
1191
  function resolveFetch(custom) {
1121
1192
  if (custom) return custom;
@@ -1126,6 +1197,12 @@ function resolveFetch(custom) {
1126
1197
  "\u0412 \u044D\u0442\u043E\u0439 \u0441\u0440\u0435\u0434\u0435 \u043D\u0435\u0442 \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u043E\u0433\u043E fetch. \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u0435\u0441\u044C \u0434\u043E Node 18+ \u043B\u0438\u0431\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 \u0441\u0432\u043E\u044E \u0440\u0435\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044E \u0447\u0435\u0440\u0435\u0437 \u043E\u043F\u0446\u0438\u044E fetch."
1127
1198
  );
1128
1199
  }
1200
+ function isBlob(value) {
1201
+ return typeof Blob !== "undefined" && value instanceof Blob;
1202
+ }
1203
+ function isFile(value) {
1204
+ return typeof File !== "undefined" && value instanceof File;
1205
+ }
1129
1206
  function supportsStreamingBody() {
1130
1207
  return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
1131
1208
  }
@@ -1200,11 +1277,19 @@ var AuthManager = class {
1200
1277
  /**
1201
1278
  * Есть ли признак живой refresh-сессии.
1202
1279
  *
1203
- * Сайт итд.com ставит рядом с refresh-токеном незакрытую cookie `is_auth` — по ней клиент
1204
- * понимает, что обновление вообще имеет смысл, и не дёргает API у анонимов.
1280
+ * Рядом с refresh-токеном сервер ставит незакрытую cookie `is_auth` — по ней видно,
1281
+ * что продлевать сессию вообще есть смысл, и API не дёргается у анонимов.
1205
1282
  * В браузере cookie ведёт сама среда, поэтому там ответ всегда `true`.
1283
+ *
1284
+ * Асинхронный, потому что признак может лежать в {@link TokenStorage}: до чтения оттуда
1285
+ * ответ был бы `false` даже при полностью рабочей сохранённой сессии.
1206
1286
  */
1207
- hasRefreshSession() {
1287
+ async hasRefreshSession() {
1288
+ await this.#loadSession();
1289
+ return this.#hasRefreshSession();
1290
+ }
1291
+ /** То же самое, но без чтения хранилища — для вызовов, где сессия уже загружена. */
1292
+ #hasRefreshSession() {
1208
1293
  if (!this.#config.useCookieJar) return true;
1209
1294
  if (this.#jar.has(AUTH_FLAG_COOKIE)) return true;
1210
1295
  return Boolean(this.#session?.refreshToken);
@@ -1390,7 +1475,7 @@ var AuthManager = class {
1390
1475
  }
1391
1476
  async #performRefresh() {
1392
1477
  await this.#loadSession();
1393
- if (!this.hasRefreshSession()) {
1478
+ if (!this.#hasRefreshSession()) {
1394
1479
  return this.#reloginOrNull();
1395
1480
  }
1396
1481
  try {
@@ -1401,7 +1486,10 @@ var AuthManager = class {
1401
1486
  // #seedRefreshCookie. По той же причине не нужен и устаревший Bearer.
1402
1487
  skipAuth: true,
1403
1488
  // Без этого 401 на самом обновлении вызвал бы новое обновление — и так по кругу.
1404
- skipAuthRefresh: true
1489
+ skipAuthRefresh: true,
1490
+ // Обновление почти всегда запускается изнутри запроса, который занимает место
1491
+ // в очереди и ждёт его результата. Встать в ту же очередь — значит зависнуть.
1492
+ skipQueue: true
1405
1493
  });
1406
1494
  const accessToken = readAccessToken(payload);
1407
1495
  if (!accessToken) return this.#reloginOrNull();
@@ -1468,7 +1556,7 @@ var AuthManager = class {
1468
1556
  }
1469
1557
  if (credentials.turnstileToken) return credentials.turnstileToken;
1470
1558
  throw new ItdConfigError(
1471
- "\u0412\u0445\u043E\u0434 \u043F\u043E email \u0438 \u043F\u0430\u0440\u043E\u043B\u044E \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u043A\u0430\u043F\u0447\u0438 Cloudflare Turnstile: \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 422. \u041F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 auth.getTurnstileToken (\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0442\u043E\u043A\u0435\u043D\u0430) \u043B\u0438\u0431\u043E \u0440\u0430\u0437\u043E\u0432\u044B\u0439 auth.turnstileToken. \u041A\u043B\u044E\u0447 \u0432\u0438\u0434\u0436\u0435\u0442\u0430 \u2014 TURNSTILE_SITE_KEY."
1559
+ "\u0412\u0445\u043E\u0434 \u043F\u043E email \u0438 \u043F\u0430\u0440\u043E\u043B\u044E \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u043A\u0430\u043F\u0447\u0438 Cloudflare Turnstile: \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 422. \u041F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 auth.getTurnstileToken (\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0442\u043E\u043A\u0435\u043D\u0430) \u043B\u0438\u0431\u043E \u0440\u0430\u0437\u043E\u0432\u044B\u0439 auth.turnstileToken. \u041A\u043B\u044E\u0447 \u0432\u0438\u0434\u0436\u0435\u0442\u0430 \u2014 TURNSTILE_SITE_KEY. \u0412 Node \u0442\u043E\u043A\u0435\u043D \u0443\u043C\u0435\u0435\u0442 \u0434\u043E\u0431\u044B\u0432\u0430\u0442\u044C \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442: npm i itd-api-turnstile, \u0437\u0430\u0442\u0435\u043C getTurnstileToken: createTurnstileSolver()."
1472
1560
  );
1473
1561
  }
1474
1562
  async #performSignIn(credentials) {
@@ -1478,7 +1566,10 @@ var AuthManager = class {
1478
1566
  path: AUTH_PATHS.signIn,
1479
1567
  body: { email: credentials.email, password: credentials.password, turnstileToken },
1480
1568
  skipAuth: true,
1481
- skipAuthRefresh: true
1569
+ skipAuthRefresh: true,
1570
+ // Отложенный вход происходит при сборке заголовков уже начатого запроса — тот держит
1571
+ // место в очереди и ждёт токена. См. `skipQueue` в RawRequestOptions.
1572
+ skipQueue: true
1482
1573
  });
1483
1574
  const accessToken = readAccessToken(payload);
1484
1575
  if (!accessToken) {
@@ -1607,7 +1698,7 @@ function normalizeBaseUrl(baseUrl) {
1607
1698
  // src/core/config.ts
1608
1699
  var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
1609
1700
  var DEFAULT_TIMEOUT = 3e4;
1610
- var LIBRARY_VERSION = "0.0.2";
1701
+ var LIBRARY_VERSION = "0.0.5";
1611
1702
  var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
1612
1703
  var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
1613
1704
  function requirePositive(value, name) {
@@ -1720,9 +1811,11 @@ function validateAuth(auth) {
1720
1811
  );
1721
1812
  }
1722
1813
  function resolveConfig(options = {}) {
1723
- const mode = options.mode ?? "auto";
1724
- if (mode !== "auto" && mode !== "browser" && mode !== "server") {
1725
- throw new ItdConfigError(`mode \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C 'auto', 'browser' \u0438\u043B\u0438 'server', \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${mode}`);
1814
+ const mode = options.mode ?? RuntimeMode.Auto;
1815
+ if (!Object.values(RuntimeMode).includes(mode)) {
1816
+ throw new ItdConfigError(
1817
+ `mode \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043E\u0434\u043D\u0438\u043C \u0438\u0437 ${Object.values(RuntimeMode).join(", ")}, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${mode}`
1818
+ );
1726
1819
  }
1727
1820
  const timeout = requirePositive(options.timeout ?? DEFAULT_TIMEOUT, "timeout");
1728
1821
  if (options.deviceId !== void 0 && (typeof options.deviceId !== "string" || options.deviceId.trim() === "")) {
@@ -1783,7 +1876,7 @@ function redactHeaders(headers) {
1783
1876
  function redactBody(body) {
1784
1877
  if (body === null || body === void 0) return body;
1785
1878
  if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
1786
- if (typeof Blob !== "undefined" && body instanceof Blob) return "[Blob]";
1879
+ if (isBlob(body)) return "[Blob]";
1787
1880
  if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
1788
1881
  if (Array.isArray(body)) return body.map(redactBody);
1789
1882
  if (typeof body === "object") {
@@ -1796,13 +1889,47 @@ function redactBody(body) {
1796
1889
  return body;
1797
1890
  }
1798
1891
 
1799
- // src/core/error-factory.ts
1892
+ // src/core/unwrap.ts
1893
+ function unwrapData(body) {
1894
+ if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
1895
+ const keys = Object.keys(body);
1896
+ if (keys.length !== 1 || keys[0] !== "data") return body;
1897
+ return body.data;
1898
+ }
1800
1899
  function isRecord(value) {
1801
1900
  return typeof value === "object" && value !== null && !Array.isArray(value);
1802
1901
  }
1803
1902
  function asString(value) {
1804
1903
  return typeof value === "string" && value.length > 0 ? value : void 0;
1805
1904
  }
1905
+ function pickArray(source, field) {
1906
+ if (typeof source !== "object" || source === null) return [];
1907
+ const value = source[field];
1908
+ return Array.isArray(value) ? value : [];
1909
+ }
1910
+ function pickObject(source, field) {
1911
+ if (typeof source !== "object" || source === null) return void 0;
1912
+ const value = source[field];
1913
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1914
+ return value;
1915
+ }
1916
+ function pickBoolean(source, field, fallback = false) {
1917
+ if (typeof source !== "object" || source === null) return fallback;
1918
+ const value = source[field];
1919
+ return typeof value === "boolean" ? value : fallback;
1920
+ }
1921
+ function pickNumber(source, field, fallback) {
1922
+ if (typeof source !== "object" || source === null) return fallback;
1923
+ const value = source[field];
1924
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
1925
+ }
1926
+ function pickString(source, field) {
1927
+ if (typeof source !== "object" || source === null) return void 0;
1928
+ const value = source[field];
1929
+ return typeof value === "string" && value.length > 0 ? value : void 0;
1930
+ }
1931
+
1932
+ // src/core/error-factory.ts
1806
1933
  function collectFieldErrors(source) {
1807
1934
  const result = {};
1808
1935
  const errors = source.errors;
@@ -1962,40 +2089,6 @@ function createApiError(context) {
1962
2089
  return new Ctor(init);
1963
2090
  }
1964
2091
 
1965
- // src/core/unwrap.ts
1966
- function unwrapData(body) {
1967
- if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
1968
- const keys = Object.keys(body);
1969
- if (keys.length !== 1 || keys[0] !== "data") return body;
1970
- return body.data;
1971
- }
1972
- function pickArray(source, field) {
1973
- if (typeof source !== "object" || source === null) return [];
1974
- const value = source[field];
1975
- return Array.isArray(value) ? value : [];
1976
- }
1977
- function pickObject(source, field) {
1978
- if (typeof source !== "object" || source === null) return void 0;
1979
- const value = source[field];
1980
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1981
- return value;
1982
- }
1983
- function pickBoolean(source, field, fallback = false) {
1984
- if (typeof source !== "object" || source === null) return fallback;
1985
- const value = source[field];
1986
- return typeof value === "boolean" ? value : fallback;
1987
- }
1988
- function pickNumber(source, field, fallback) {
1989
- if (typeof source !== "object" || source === null) return fallback;
1990
- const value = source[field];
1991
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
1992
- }
1993
- function pickString(source, field) {
1994
- if (typeof source !== "object" || source === null) return void 0;
1995
- const value = source[field];
1996
- return typeof value === "string" && value.length > 0 ? value : void 0;
1997
- }
1998
-
1999
2092
  // src/core/http.ts
2000
2093
  function sleep(ms) {
2001
2094
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -2011,7 +2104,7 @@ function setHeader(headers, name, value) {
2011
2104
  }
2012
2105
  function isRawBody(body) {
2013
2106
  if (typeof body !== "object" || body === null) return typeof body === "string";
2014
- return typeof FormData !== "undefined" && body instanceof FormData || typeof Blob !== "undefined" && body instanceof Blob || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body instanceof ArrayBuffer || ArrayBuffer.isView(body);
2107
+ return typeof FormData !== "undefined" && body instanceof FormData || isBlob(body) || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body instanceof ArrayBuffer || ArrayBuffer.isView(body);
2015
2108
  }
2016
2109
  async function readBody(response) {
2017
2110
  if (response.status === 204 || response.status === 205) return void 0;
@@ -2081,7 +2174,8 @@ var HttpClient = class {
2081
2174
  */
2082
2175
  async request(options) {
2083
2176
  const task = () => this.#withRetries(options);
2084
- return this.#collaborators.schedule ? this.#collaborators.schedule(task) : task();
2177
+ if (!this.#collaborators.schedule || options.skipQueue) return task();
2178
+ return this.#collaborators.schedule(task);
2085
2179
  }
2086
2180
  async #withRetries(options) {
2087
2181
  const method = options.method.toUpperCase();
@@ -2350,21 +2444,15 @@ function isKnownNotificationType(type) {
2350
2444
  }
2351
2445
 
2352
2446
  // src/notifications/normalize.ts
2353
- function isRecord2(value) {
2354
- return typeof value === "object" && value !== null && !Array.isArray(value);
2355
- }
2356
- function asString2(value) {
2357
- return typeof value === "string" && value.length > 0 ? value : void 0;
2358
- }
2359
2447
  function asActor(value) {
2360
- if (!isRecord2(value)) return void 0;
2361
- const id = asString2(value.id);
2448
+ if (!isRecord(value)) return void 0;
2449
+ const id = asString(value.id);
2362
2450
  if (!id) return void 0;
2363
2451
  return {
2364
2452
  id,
2365
- username: asString2(value.username) ?? "",
2366
- displayName: asString2(value.displayName) ?? "",
2367
- avatar: asString2(value.avatar) ?? "",
2453
+ username: asString(value.username) ?? "",
2454
+ displayName: asString(value.displayName) ?? "",
2455
+ avatar: asString(value.avatar) ?? "",
2368
2456
  ...typeof value.isFollowing === "boolean" ? { isFollowing: value.isFollowing } : {},
2369
2457
  ...typeof value.isFollowedBy === "boolean" ? { isFollowedBy: value.isFollowedBy } : {}
2370
2458
  };
@@ -2377,33 +2465,34 @@ function readActors(source) {
2377
2465
  return single ? [single] : [];
2378
2466
  }
2379
2467
  function normalizeNotification(input) {
2380
- const source = isRecord2(input) ? input : {};
2381
- const payload = isRecord2(source.payload) ? source.payload : source;
2382
- const rawType = asString2(payload.type) ?? asString2(source.type) ?? "";
2383
- const createdAt = asString2(payload.createdAt) ?? asString2(source.createdAt) ?? "";
2384
- const readAt = asString2(payload.readAt) ?? asString2(source.readAt);
2468
+ const source = isRecord(input) ? input : {};
2469
+ const payload = isRecord(source.payload) ? source.payload : source;
2470
+ const rawType = asString(payload.type) ?? asString(source.type) ?? "";
2471
+ const createdAt = asString(payload.createdAt) ?? asString(source.createdAt) ?? "";
2472
+ const readAt = asString(payload.readAt) ?? asString(source.readAt);
2385
2473
  const isRead = typeof payload.isRead === "boolean" ? payload.isRead : typeof payload.read === "boolean" ? payload.read : Boolean(readAt);
2386
- const subjectId = asString2(payload.subjectId);
2387
- const targetId = asString2(payload.targetId);
2474
+ const subjectId = asString(payload.subjectId);
2475
+ const targetId = asString(payload.targetId);
2388
2476
  const subjectIsComment = payload.subjectType === "comment";
2477
+ const clickUrl = asString(payload.clickUrl);
2389
2478
  return {
2390
- id: asString2(payload.id) ?? asString2(source.id) ?? "",
2479
+ id: asString(payload.id) ?? asString(source.id) ?? "",
2391
2480
  type: canonicalNotificationType(rawType),
2392
2481
  rawType,
2393
- entityId: asString2(payload.entityId) ?? (subjectIsComment ? subjectId ?? targetId : targetId) ?? null,
2394
- parentEntityId: asString2(payload.parentEntityId) ?? (subjectIsComment ? targetId ?? null : null),
2482
+ entityId: asString(payload.entityId) ?? (subjectIsComment ? subjectId ?? targetId : targetId) ?? null,
2483
+ parentEntityId: asString(payload.parentEntityId) ?? (subjectIsComment ? targetId ?? null : null),
2395
2484
  isRead,
2396
2485
  actors: readActors(payload),
2397
2486
  count: typeof payload.count === "number" && payload.count > 0 ? payload.count : 1,
2398
- preview: asString2(payload.entityPreview) ?? asString2(payload.preview) ?? null,
2399
- ...asString2(payload.clickUrl) ? { clickUrl: asString2(payload.clickUrl) } : {},
2487
+ preview: asString(payload.entityPreview) ?? asString(payload.preview) ?? null,
2488
+ ...clickUrl ? { clickUrl } : {},
2400
2489
  createdAt,
2401
- updatedAt: asString2(payload.updatedAt) ?? readAt ?? createdAt,
2490
+ updatedAt: asString(payload.updatedAt) ?? readAt ?? createdAt,
2402
2491
  raw: input
2403
2492
  };
2404
2493
  }
2405
2494
  function readNotificationEvent(data) {
2406
- const source = isRecord2(data) ? data : {};
2495
+ const source = isRecord(data) ? data : {};
2407
2496
  return {
2408
2497
  notification: normalizeNotification(data),
2409
2498
  unreadCount: typeof source.unreadCount === "number" ? source.unreadCount : void 0,
@@ -2411,8 +2500,8 @@ function readNotificationEvent(data) {
2411
2500
  };
2412
2501
  }
2413
2502
  function readUnreadCountEvent(data) {
2414
- if (!isRecord2(data)) return void 0;
2415
- const payload = isRecord2(data.payload) ? data.payload : void 0;
2503
+ if (!isRecord(data)) return void 0;
2504
+ const payload = isRecord(data.payload) ? data.payload : void 0;
2416
2505
  if (!payload) return void 0;
2417
2506
  return typeof payload.count === "number" ? payload.count : void 0;
2418
2507
  }
@@ -2491,6 +2580,7 @@ var PollTransport = class {
2491
2580
  }
2492
2581
  /** Ждёт следующего опроса, прерываясь при отмене. */
2493
2582
  #wait(signal) {
2583
+ if (signal.aborted) return Promise.resolve();
2494
2584
  return new Promise((resolve) => {
2495
2585
  const timer = setTimeout(finish, this.#interval);
2496
2586
  function finish() {
@@ -2763,6 +2853,14 @@ var SseTransport = class {
2763
2853
  };
2764
2854
 
2765
2855
  // src/realtime/stream.ts
2856
+ var RealtimeTransportKind = Object.freeze({
2857
+ /** Поток событий, если среда умеет читать тело по частям, иначе опрос. */
2858
+ Auto: "auto",
2859
+ /** Поток `text/event-stream`. */
2860
+ Sse: "sse",
2861
+ /** Периодический опрос REST. */
2862
+ Poll: "poll"
2863
+ });
2766
2864
  var ItdRealtime = class {
2767
2865
  #deps;
2768
2866
  #options;
@@ -2770,6 +2868,15 @@ var ItdRealtime = class {
2770
2868
  #transport;
2771
2869
  #maxAttempts;
2772
2870
  #controller;
2871
+ /**
2872
+ * Хочет ли вызывающий код, чтобы соединение было живо.
2873
+ *
2874
+ * Отдельно от `#controller`, потому что тот появляется только после `await` внутри
2875
+ * {@link connect}. Без этого флага два вызова подряд проскочили бы проверку оба
2876
+ * и подняли два соединения, а `disconnect()` во время ожидания счётчика не был бы
2877
+ * замечен и соединение поднялось бы уже после отмены.
2878
+ */
2879
+ #wanted = false;
2773
2880
  #status = RealtimeStatus.Disconnected;
2774
2881
  #attempt = 0;
2775
2882
  #timer;
@@ -2805,7 +2912,8 @@ var ItdRealtime = class {
2805
2912
  * Возвращает управление сразу после запуска: соединение живёт в фоне.
2806
2913
  */
2807
2914
  async connect() {
2808
- if (this.#controller) return;
2915
+ if (this.#wanted) return;
2916
+ this.#wanted = true;
2809
2917
  this.#attachEnvironmentListeners();
2810
2918
  if (this.#options.syncCount !== false) {
2811
2919
  try {
@@ -2814,10 +2922,11 @@ var ItdRealtime = class {
2814
2922
  this.#deps.logger?.debug("\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u0447\u0438\u0441\u043B\u043E \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0445", error);
2815
2923
  }
2816
2924
  }
2817
- this.#run();
2925
+ if (this.#wanted) this.#run();
2818
2926
  }
2819
2927
  /** Закрывает соединение и отменяет запланированные попытки. */
2820
2928
  disconnect() {
2929
+ this.#wanted = false;
2821
2930
  if (this.#timer !== void 0) {
2822
2931
  clearTimeout(this.#timer);
2823
2932
  this.#timer = void 0;
@@ -2834,9 +2943,9 @@ var ItdRealtime = class {
2834
2943
  this.#emitter.removeAllListeners();
2835
2944
  }
2836
2945
  #createTransport() {
2837
- const kind = this.#options.transport ?? "auto";
2946
+ const kind = this.#options.transport ?? RealtimeTransportKind.Auto;
2838
2947
  if (typeof kind === "object") return kind;
2839
- if (kind === "poll" || kind === "auto" && !supportsStreamingBody()) {
2948
+ if (kind === RealtimeTransportKind.Poll || kind === RealtimeTransportKind.Auto && !supportsStreamingBody()) {
2840
2949
  return new PollTransport({
2841
2950
  ...this.#options.pollInterval !== void 0 ? { interval: this.#options.pollInterval } : {}
2842
2951
  });
@@ -2847,6 +2956,7 @@ var ItdRealtime = class {
2847
2956
  }
2848
2957
  /** Запускает попытку подключения; повторы планирует сам. */
2849
2958
  #run() {
2959
+ this.#controller?.abort();
2850
2960
  const controller = new AbortController();
2851
2961
  this.#controller = controller;
2852
2962
  this.#setStatus(RealtimeStatus.Connecting);
@@ -2876,8 +2986,7 @@ var ItdRealtime = class {
2876
2986
  #handleEvent(name, data) {
2877
2987
  this.#emitter.emit("message", { name, data });
2878
2988
  if (name === "connected") {
2879
- const userId = typeof data === "object" && data !== null && "userId" in data ? String(data.userId) : void 0;
2880
- this.#emitter.emit("ready", { userId });
2989
+ this.#emitter.emit("ready", { userId: pickString(data, "userId") });
2881
2990
  return;
2882
2991
  }
2883
2992
  if (name === "notification") {
@@ -2965,13 +3074,22 @@ var ItdRealtime = class {
2965
3074
  };
2966
3075
 
2967
3076
  // src/core/pagination.ts
3077
+ var PaginationMode = Object.freeze({
3078
+ /** Следующая страница запрашивается непрозрачным курсором. */
3079
+ Cursor: "cursor",
3080
+ /** Следующая страница запрашивается номером. */
3081
+ Page: "page",
3082
+ /** Следующая страница запрашивается смещением от начала списка. */
3083
+ Offset: "offset"
3084
+ });
2968
3085
  function readItems(body, fields) {
2969
3086
  if (Array.isArray(body)) return body;
2970
3087
  for (const field of fields) {
2971
3088
  const items = pickArray(body, field);
2972
3089
  if (items.length > 0) return items;
2973
3090
  }
2974
- return fields.length > 0 ? pickArray(body, fields[0]) : [];
3091
+ const primary = fields[0];
3092
+ return primary === void 0 ? [] : pickArray(body, primary);
2975
3093
  }
2976
3094
  function readCursor(body) {
2977
3095
  const pagination = pickObject(body, "pagination");
@@ -3030,12 +3148,13 @@ function readOffsetPage(body, field, offset) {
3030
3148
  var Paginator = class {
3031
3149
  #options;
3032
3150
  #maxPages;
3033
- #state = {};
3151
+ #state;
3034
3152
  #finished = false;
3035
3153
  #pagesLoaded = 0;
3036
3154
  constructor(options) {
3037
3155
  this.#options = options;
3038
3156
  this.#maxPages = options.maxPages ?? 1e3;
3157
+ this.#state = options.start ?? {};
3039
3158
  }
3040
3159
  /**
3041
3160
  * Загружает следующую страницу.
@@ -3101,7 +3220,7 @@ var Paginator = class {
3101
3220
  this.#finished = true;
3102
3221
  return previous;
3103
3222
  }
3104
- if (this.#options.mode === "cursor") {
3223
+ if (this.#options.mode === PaginationMode.Cursor) {
3105
3224
  const cursor = page.nextCursor ?? void 0;
3106
3225
  if (!cursor || cursor === previous.cursor) {
3107
3226
  this.#finished = true;
@@ -3109,7 +3228,7 @@ var Paginator = class {
3109
3228
  }
3110
3229
  return { cursor };
3111
3230
  }
3112
- if (this.#options.mode === "page") {
3231
+ if (this.#options.mode === PaginationMode.Page) {
3113
3232
  return { page: (previous.page ?? 1) + 1 };
3114
3233
  }
3115
3234
  return { offset: page.nextOffset ?? (previous.offset ?? 0) + page.items.length };
@@ -3138,13 +3257,15 @@ var BaseResource = class {
3138
3257
  *
3139
3258
  * @param mode схема пагинации эндпоинта
3140
3259
  * @param load загружает одну страницу для указанной позиции
3260
+ * @param options `maxPages` и `signal`, а также `start` — позиция, с которой продолжить
3141
3261
  */
3142
3262
  paginate(mode, load, options) {
3143
3263
  return new Paginator({
3144
3264
  mode,
3145
3265
  load,
3146
3266
  ...options?.maxPages !== void 0 ? { maxPages: options.maxPages } : {},
3147
- ...options?.signal !== void 0 ? { signal: options.signal } : {}
3267
+ ...options?.signal !== void 0 ? { signal: options.signal } : {},
3268
+ ...options?.start !== void 0 ? { start: options.start } : {}
3148
3269
  });
3149
3270
  }
3150
3271
  };
@@ -3158,6 +3279,14 @@ function withPageState(query, state) {
3158
3279
  }
3159
3280
 
3160
3281
  // src/resources/auth.ts
3282
+ var OAuthProvider = Object.freeze({
3283
+ Yandex: "yandex",
3284
+ Google: "google"
3285
+ });
3286
+ var SignInStatus = Object.freeze({
3287
+ Authenticated: "authenticated",
3288
+ OtpRequired: "otp_required"
3289
+ });
3161
3290
  var AuthResource = class extends BaseResource {
3162
3291
  #auth;
3163
3292
  constructor(http, deps) {
@@ -3206,9 +3335,9 @@ var AuthResource = class extends BaseResource {
3206
3335
  const accessToken = pickString(body, "accessToken");
3207
3336
  if (accessToken) {
3208
3337
  await this.#auth.setAccessToken(accessToken);
3209
- return { status: "authenticated", accessToken };
3338
+ return { status: SignInStatus.Authenticated, accessToken };
3210
3339
  }
3211
- return { status: "otp_required", flowToken: pickString(body, "flowToken") };
3340
+ return { status: SignInStatus.OtpRequired, flowToken: pickString(body, "flowToken") };
3212
3341
  }
3213
3342
  /**
3214
3343
  * Подтверждает вход кодом из письма.
@@ -3263,7 +3392,7 @@ var AuthResource = class extends BaseResource {
3263
3392
  async signInWithOtp(input, options = {}) {
3264
3393
  const { getOtp, ...credentials } = input;
3265
3394
  const result = await this.signIn(credentials, options);
3266
- if (result.status === "authenticated") return result.accessToken;
3395
+ if (result.status === SignInStatus.Authenticated) return result.accessToken;
3267
3396
  if (!result.flowToken) {
3268
3397
  throw new ItdConfigError(
3269
3398
  "\u0421\u0435\u0440\u0432\u0435\u0440 \u0437\u0430\u043F\u0440\u043E\u0441\u0438\u043B \u043A\u043E\u0434 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F, \u043D\u043E \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B flowToken \u2014 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u0432\u0445\u043E\u0434 \u043D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E"
@@ -3292,9 +3421,18 @@ var AuthResource = class extends BaseResource {
3292
3421
  /**
3293
3422
  * Есть ли признак живой сессии обновления.
3294
3423
  *
3295
- * Проверяет cookie `is_auth`, которую сервер ставит рядом с refresh-токеном. Позволяет
3296
- * не дёргать API у неавторизованного пользователя. В браузере всегда `true`:
3297
- * cookie ведёт сама среда, и прочитать её из JS нельзя.
3424
+ * Проверяет cookie `is_auth`, которую сервер ставит рядом с refresh-токеном, а также
3425
+ * refresh-токен, переданный строкой. Позволяет не дёргать API у неавторизованного
3426
+ * пользователя. В браузере всегда `true`: cookie ведёт сама среда, и прочитать её
3427
+ * из JS нельзя.
3428
+ *
3429
+ * Читает {@link TokenStorage}, поэтому результат верен и до первого запроса.
3430
+ *
3431
+ * @example
3432
+ * ```ts
3433
+ * if (await itd.auth.hasRefreshSession()) await itd.auth.refresh();
3434
+ * else redirectToLogin();
3435
+ * ```
3298
3436
  */
3299
3437
  hasRefreshSession() {
3300
3438
  return this.#auth.hasRefreshSession();
@@ -3469,7 +3607,7 @@ var CommentsResource = class extends BaseResource {
3469
3607
  iterateReplies(commentId, params = {}) {
3470
3608
  const path = `/api/comments/${encodePathSegment(commentId, "commentId")}/replies`;
3471
3609
  return this.paginate(
3472
- "page",
3610
+ PaginationMode.Page,
3473
3611
  async (state) => {
3474
3612
  const body = await this.http.request({
3475
3613
  method: "GET",
@@ -3479,7 +3617,7 @@ var CommentsResource = class extends BaseResource {
3479
3617
  });
3480
3618
  return readPagedPage(body, "replies");
3481
3619
  },
3482
- params
3620
+ { ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
3483
3621
  );
3484
3622
  }
3485
3623
  /**
@@ -3560,7 +3698,11 @@ var IMAGE_MIME_TYPES = Object.freeze([
3560
3698
  "image/heic",
3561
3699
  "image/heif"
3562
3700
  ]);
3563
- var VIDEO_MIME_TYPES = Object.freeze(["video/mp4", "video/webm", "video/quicktime"]);
3701
+ var VIDEO_MIME_TYPES = Object.freeze([
3702
+ "video/mp4",
3703
+ "video/webm",
3704
+ "video/quicktime"
3705
+ ]);
3564
3706
  var AUDIO_MIME_TYPES = Object.freeze(["audio/ogg"]);
3565
3707
  var ALLOWED_MIME_TYPES = Object.freeze([
3566
3708
  ...IMAGE_MIME_TYPES,
@@ -3639,7 +3781,7 @@ var FilesResource = class extends BaseResource {
3639
3781
  * ```
3640
3782
  */
3641
3783
  async upload(input, options = {}) {
3642
- const prepared = await this.prepare(input, options);
3784
+ const prepared = await this.#prepare(input, options);
3643
3785
  const form = new FormData();
3644
3786
  form.set("file", prepared.blob, prepared.filename);
3645
3787
  return this.http.request({
@@ -3690,11 +3832,11 @@ var FilesResource = class extends BaseResource {
3690
3832
  });
3691
3833
  }
3692
3834
  /** Приводит любой поддерживаемый вход к `Blob` с именем и проверенным типом. */
3693
- async prepare(input, options) {
3835
+ async #prepare(input, options) {
3694
3836
  const { data, filename, contentType } = await this.#normalize(input, options);
3695
- const type = contentType ?? ((data instanceof Blob ? data.type : void 0) || mimeFromFilename(filename));
3837
+ const type = contentType ?? ((isBlob(data) ? data.type : void 0) || mimeFromFilename(filename));
3696
3838
  if (options.validateMime !== false) assertAllowedMime(type || void 0, filename);
3697
- const blob = data instanceof Blob && (!type || data.type === type) ? data : new Blob([data], { type: type ?? "" });
3839
+ const blob = isBlob(data) && (!type || data.type === type) ? data : new Blob([data], { type: type ?? "" });
3698
3840
  return { blob, filename };
3699
3841
  }
3700
3842
  async #normalize(input, options) {
@@ -3711,8 +3853,8 @@ var FilesResource = class extends BaseResource {
3711
3853
  ...options.contentType ? { contentType: options.contentType } : {}
3712
3854
  };
3713
3855
  }
3714
- if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || input instanceof Blob) {
3715
- const fallbackName = input instanceof File ? input.name : options.filename ?? this.#nameFromMime(options.contentType);
3856
+ if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || isBlob(input)) {
3857
+ const fallbackName = isFile(input) ? input.name : options.filename ?? this.#nameFromMime(options.contentType);
3716
3858
  return {
3717
3859
  data: input,
3718
3860
  filename: options.filename ?? fallbackName,
@@ -3778,7 +3920,7 @@ var HashtagsResource = class extends BaseResource {
3778
3920
  iteratePosts(tag, params = {}) {
3779
3921
  const path = `/api/hashtags/${encodePathSegment(tag, "tag")}/posts`;
3780
3922
  return this.paginate(
3781
- "cursor",
3923
+ PaginationMode.Cursor,
3782
3924
  async (state) => {
3783
3925
  const body = await this.http.request({
3784
3926
  method: "GET",
@@ -3788,7 +3930,7 @@ var HashtagsResource = class extends BaseResource {
3788
3930
  });
3789
3931
  return readCursorPage(body, "posts");
3790
3932
  },
3791
- params
3933
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
3792
3934
  );
3793
3935
  }
3794
3936
  };
@@ -4019,8 +4161,11 @@ var NotificationsResource = class extends BaseResource {
4019
4161
  * const next = await itd.notifications.list({ limit: 20, offset: page.nextOffset });
4020
4162
  * ```
4021
4163
  */
4022
- async list(params = {}) {
4023
- const offset = params.offset ?? 0;
4164
+ list(params = {}) {
4165
+ return this.#loadPage(params, params.offset ?? 0);
4166
+ }
4167
+ /** Общая загрузка страницы для {@link list} и {@link iterate}. */
4168
+ async #loadPage(params, offset) {
4024
4169
  const body = await this.http.request({
4025
4170
  method: "GET",
4026
4171
  // Завершающий слэш обязателен: без него сервер отвечает ошибкой.
@@ -4043,19 +4188,9 @@ var NotificationsResource = class extends BaseResource {
4043
4188
  */
4044
4189
  iterate(params = {}) {
4045
4190
  return this.paginate(
4046
- "offset",
4047
- async (state) => {
4048
- const offset = state.offset ?? params.offset ?? 0;
4049
- const body = await this.http.request({
4050
- method: "GET",
4051
- path: "/api/notifications/",
4052
- query: { limit: params.limit, offset },
4053
- ...this.requestOptions(params)
4054
- });
4055
- const page = readOffsetPage(body, "notifications", offset);
4056
- return { ...page, items: page.items.map(normalizeNotification) };
4057
- },
4058
- params
4191
+ PaginationMode.Offset,
4192
+ (state) => this.#loadPage(params, state.offset ?? 0),
4193
+ { ...params, ...params.offset !== void 0 ? { start: { offset: params.offset } } : {} }
4059
4194
  );
4060
4195
  }
4061
4196
  /** Загружает число непрочитанных уведомлений. */
@@ -4187,7 +4322,7 @@ var PostsResource = class extends BaseResource {
4187
4322
  */
4188
4323
  iterate(params = {}) {
4189
4324
  return this.paginate(
4190
- "cursor",
4325
+ PaginationMode.Cursor,
4191
4326
  async (state) => {
4192
4327
  const body = await this.http.request({
4193
4328
  method: "GET",
@@ -4197,7 +4332,7 @@ var PostsResource = class extends BaseResource {
4197
4332
  });
4198
4333
  return readCursorPage(body, "posts");
4199
4334
  },
4200
- params
4335
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
4201
4336
  );
4202
4337
  }
4203
4338
  /**
@@ -4342,7 +4477,16 @@ var PostsResource = class extends BaseResource {
4342
4477
  });
4343
4478
  return pickArray(body, "posts");
4344
4479
  }
4345
- /** Загружает страницу постов пользователя (его стену). */
4480
+ /**
4481
+ * Загружает страницу стены пользователя.
4482
+ *
4483
+ * Это **не только его собственные посты**: сюда попадают и записи, которые другие
4484
+ * оставили на его стене — у них `author` чужой, а `wallRecipient` указывает на владельца
4485
+ * стены. Поэтому число записей обычно больше, чем `postsCount` из профиля; чтобы
4486
+ * получить только авторские посты, отфильтруйте по `post.author.id`.
4487
+ *
4488
+ * Принимает и UUID, и имя пользователя.
4489
+ */
4346
4490
  async byUser(user, params = {}) {
4347
4491
  const body = await this.http.request({
4348
4492
  method: "GET",
@@ -4357,11 +4501,11 @@ var PostsResource = class extends BaseResource {
4357
4501
  });
4358
4502
  return readCursorPage(body, "posts");
4359
4503
  }
4360
- /** Перебирает посты пользователя. */
4504
+ /** Перебирает стену пользователя. Что именно в неё входит — см. {@link byUser}. */
4361
4505
  iterateByUser(user, params = {}) {
4362
4506
  const path = `/api/posts/user/${encodePathSegment(user, "user")}`;
4363
4507
  return this.paginate(
4364
- "cursor",
4508
+ PaginationMode.Cursor,
4365
4509
  async (state) => {
4366
4510
  const body = await this.http.request({
4367
4511
  method: "GET",
@@ -4374,7 +4518,7 @@ var PostsResource = class extends BaseResource {
4374
4518
  });
4375
4519
  return readCursorPage(body, "posts");
4376
4520
  },
4377
- params
4521
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
4378
4522
  );
4379
4523
  }
4380
4524
  /** Загружает страницу постов, которые пользователь отметил реакцией. */
@@ -4391,7 +4535,7 @@ var PostsResource = class extends BaseResource {
4391
4535
  iterateLikedByUser(user, params = {}) {
4392
4536
  const path = `/api/posts/user/${encodePathSegment(user, "user")}/liked`;
4393
4537
  return this.paginate(
4394
- "cursor",
4538
+ PaginationMode.Cursor,
4395
4539
  async (state) => {
4396
4540
  const body = await this.http.request({
4397
4541
  method: "GET",
@@ -4401,7 +4545,7 @@ var PostsResource = class extends BaseResource {
4401
4545
  });
4402
4546
  return readCursorPage(body, "posts");
4403
4547
  },
4404
- params
4548
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
4405
4549
  );
4406
4550
  }
4407
4551
  /**
@@ -4423,7 +4567,7 @@ var PostsResource = class extends BaseResource {
4423
4567
  iterateComments(postId, params = {}) {
4424
4568
  const path = `/api/posts/${encodePathSegment(postId, "postId")}/comments`;
4425
4569
  return this.paginate(
4426
- "cursor",
4570
+ PaginationMode.Cursor,
4427
4571
  async (state) => {
4428
4572
  const body = await this.http.request({
4429
4573
  method: "GET",
@@ -4433,7 +4577,7 @@ var PostsResource = class extends BaseResource {
4433
4577
  });
4434
4578
  return readFlatCursorPage(body, "comments");
4435
4579
  },
4436
- params
4580
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
4437
4581
  );
4438
4582
  }
4439
4583
  /**
@@ -4600,19 +4744,34 @@ var UsersResource = class extends BaseResource {
4600
4744
  ...this.requestOptions(options)
4601
4745
  });
4602
4746
  }
4603
- /** Загружает страницу подписчиков. */
4747
+ /**
4748
+ * Загружает подписчиков пользователя.
4749
+ *
4750
+ * ⚠️ **Сервер этот список не листает.** Возвращаются первые 20 записей и только они:
4751
+ * параметр `page` игнорируется (любая страница отдаёт те же записи и `pagination.page: 1`),
4752
+ * `limit` больше 20 молча уменьшается, а `hasMore` всегда `false`. Последнее честно —
4753
+ * получить продолжение нечем.
4754
+ *
4755
+ * Числу `total` доверять тоже не стоит: оно расходится с `followersCount` из профиля —
4756
+ * на проверенных аккаунтах занижено примерно на 1–4%.
4757
+ */
4604
4758
  followers(user, params = {}) {
4605
4759
  return this.#userPage(`/api/users/${encodePathSegment(user, "user")}/followers`, params);
4606
4760
  }
4607
- /** Перебирает подписчиков. */
4761
+ /**
4762
+ * Перебирает подписчиков.
4763
+ *
4764
+ * ⚠️ Перебор закончится после первых 20 записей: сервер список не листает —
4765
+ * см. {@link followers}. Метод оставлен на случай, если пагинацию починят.
4766
+ */
4608
4767
  iterateFollowers(user, params = {}) {
4609
4768
  return this.#userPaginator(`/api/users/${encodePathSegment(user, "user")}/followers`, params);
4610
4769
  }
4611
- /** Загружает страницу подписок. */
4770
+ /** Загружает подписки пользователя. Ограничения те же, что у {@link followers}. */
4612
4771
  following(user, params = {}) {
4613
4772
  return this.#userPage(`/api/users/${encodePathSegment(user, "user")}/following`, params);
4614
4773
  }
4615
- /** Перебирает подписки. */
4774
+ /** Перебирает подписки. Закончится после первых 20 записей — см. {@link followers}. */
4616
4775
  iterateFollowing(user, params = {}) {
4617
4776
  return this.#userPaginator(`/api/users/${encodePathSegment(user, "user")}/following`, params);
4618
4777
  }
@@ -4652,11 +4811,11 @@ var UsersResource = class extends BaseResource {
4652
4811
  ...this.requestOptions(options)
4653
4812
  });
4654
4813
  }
4655
- /** Загружает страницу заблокированных пользователей. */
4814
+ /** Загружает заблокированных пользователей. Ограничения те же, что у {@link followers}. */
4656
4815
  blocked(params = {}) {
4657
4816
  return this.#userPage("/api/users/me/blocked", params);
4658
4817
  }
4659
- /** Перебирает заблокированных пользователей. */
4818
+ /** Перебирает заблокированных. Закончится после первых 20 записей — см. {@link followers}. */
4660
4819
  iterateBlocked(params = {}) {
4661
4820
  return this.#userPaginator("/api/users/me/blocked", params);
4662
4821
  }
@@ -4711,28 +4870,36 @@ var UsersResource = class extends BaseResource {
4711
4870
  ...this.requestOptions(options)
4712
4871
  });
4713
4872
  }
4714
- async #userPage(path, params) {
4873
+ /**
4874
+ * Загружает одну страницу списка пользователей.
4875
+ *
4876
+ * Имена полей перечислены с запасом: списки подписчиков и заблокированных приходят
4877
+ * под `users`, но альтернативное имя ничего не стоит и спасает, если эндпоинт назовёт
4878
+ * список по-своему.
4879
+ *
4880
+ * `page` уходит в запрос, хотя сервер его сейчас не читает (см. {@link followers}):
4881
+ * когда пагинацию починят, работать начнёт само.
4882
+ */
4883
+ async #loadUserPage(path, params, state) {
4715
4884
  const body = await this.http.request({
4716
4885
  method: "GET",
4717
4886
  path,
4718
- query: { limit: params.limit, page: params.page },
4887
+ query: withPageState({ limit: params.limit }, state),
4719
4888
  ...this.requestOptions(params)
4720
4889
  });
4721
- return readPagedPage(body, "users");
4890
+ return readPagedPage(body, "users", "followers", "following", "blocked");
4891
+ }
4892
+ #userPage(path, params) {
4893
+ return this.#loadUserPage(path, params, {
4894
+ ...params.page !== void 0 ? { page: params.page } : {}
4895
+ });
4722
4896
  }
4723
4897
  #userPaginator(path, params) {
4724
4898
  return this.paginate(
4725
- "page",
4726
- async (state) => {
4727
- const body = await this.http.request({
4728
- method: "GET",
4729
- path,
4730
- query: withPageState({ limit: params.limit }, state),
4731
- ...this.requestOptions(params)
4732
- });
4733
- return readPagedPage(body, "users");
4734
- },
4735
- params
4899
+ PaginationMode.Page,
4900
+ (state) => this.#loadUserPage(path, params, state),
4901
+ // Без `start` перебор начинался бы с первой страницы, молча игнорируя `page`.
4902
+ { ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
4736
4903
  );
4737
4904
  }
4738
4905
  };
@@ -5061,16 +5228,19 @@ exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
5061
5228
  exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
5062
5229
  exports.DEFAULT_USER_AGENT = DEFAULT_USER_AGENT;
5063
5230
  exports.DEVICE_ID_HEADER = DEVICE_ID_HEADER;
5231
+ exports.DetectedRuntime = DetectedRuntime;
5064
5232
  exports.FeedTab = FeedTab;
5065
5233
  exports.IMAGE_MIME_TYPES = IMAGE_MIME_TYPES;
5066
5234
  exports.ItdAbortError = ItdAbortError;
5067
5235
  exports.ItdApiError = ItdApiError;
5236
+ exports.ItdApiErrorKind = ItdApiErrorKind;
5068
5237
  exports.ItdAuthError = ItdAuthError;
5069
5238
  exports.ItdClient = ItdClient;
5070
5239
  exports.ItdConfigError = ItdConfigError;
5071
5240
  exports.ItdConflictError = ItdConflictError;
5072
5241
  exports.ItdError = ItdError;
5073
5242
  exports.ItdErrorCode = ItdErrorCode;
5243
+ exports.ItdErrorKind = ItdErrorKind;
5074
5244
  exports.ItdForbiddenError = ItdForbiddenError;
5075
5245
  exports.ItdNetworkError = ItdNetworkError;
5076
5246
  exports.ItdNotFoundError = ItdNotFoundError;
@@ -5087,16 +5257,22 @@ exports.MAX_RECONNECT_ATTEMPTS = MAX_RECONNECT_ATTEMPTS;
5087
5257
  exports.MemoryTokenStorage = MemoryTokenStorage;
5088
5258
  exports.NOTIFICATION_TYPE_ALIASES = NOTIFICATION_TYPE_ALIASES;
5089
5259
  exports.NotificationType = NotificationType;
5260
+ exports.OAuthProvider = OAuthProvider;
5261
+ exports.PaginationMode = PaginationMode;
5090
5262
  exports.Paginator = Paginator;
5091
5263
  exports.RECONNECT_BACKOFF = RECONNECT_BACKOFF;
5092
5264
  exports.RECONNECT_JITTER = RECONNECT_JITTER;
5093
5265
  exports.REFRESH_COOKIE = REFRESH_COOKIE;
5094
5266
  exports.REFRESH_COOKIE_PATH = REFRESH_COOKIE_PATH;
5095
5267
  exports.RealtimeStatus = RealtimeStatus;
5268
+ exports.RealtimeTransportKind = RealtimeTransportKind;
5096
5269
  exports.ReportReason = ReportReason;
5097
5270
  exports.ReportTargetType = ReportTargetType;
5271
+ exports.RuntimeMode = RuntimeMode;
5098
5272
  exports.STREAM_PATH = STREAM_PATH;
5273
+ exports.SignInStatus = SignInStatus;
5099
5274
  exports.TURNSTILE_SITE_KEY = TURNSTILE_SITE_KEY;
5275
+ exports.UnauthorizedStreamError = UnauthorizedStreamError;
5100
5276
  exports.VIDEO_MIME_TYPES = VIDEO_MIME_TYPES;
5101
5277
  exports.WallAccess = WallAccess;
5102
5278
  exports.canonicalNotificationType = canonicalNotificationType;
@@ -5107,8 +5283,13 @@ exports.formatNotificationText = formatNotificationText;
5107
5283
  exports.isBuilder = isBuilder;
5108
5284
  exports.isItdApiError = isItdApiError;
5109
5285
  exports.isItdAuthError = isItdAuthError;
5286
+ exports.isItdConflictError = isItdConflictError;
5110
5287
  exports.isItdError = isItdError;
5288
+ exports.isItdForbiddenError = isItdForbiddenError;
5289
+ exports.isItdNotFoundError = isItdNotFoundError;
5290
+ exports.isItdPhoneVerificationError = isItdPhoneVerificationError;
5111
5291
  exports.isItdRateLimitError = isItdRateLimitError;
5292
+ exports.isItdServerError = isItdServerError;
5112
5293
  exports.isItdValidationError = isItdValidationError;
5113
5294
  exports.isKnownNotificationType = isKnownNotificationType;
5114
5295
  exports.isMyProfile = isMyProfile;
@@ -5120,5 +5301,5 @@ exports.readUnreadCountEvent = readUnreadCountEvent;
5120
5301
  exports.report = report;
5121
5302
  exports.resolveNotificationUrl = resolveNotificationUrl;
5122
5303
  exports.toDate = toDate;
5123
- //# sourceMappingURL=chunk-3ODOEKQO.cjs.map
5124
- //# sourceMappingURL=chunk-3ODOEKQO.cjs.map
5304
+ //# sourceMappingURL=chunk-YM2YUO4D.cjs.map
5305
+ //# sourceMappingURL=chunk-YM2YUO4D.cjs.map