itd-api 0.0.2 → 0.0.4

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
@@ -588,8 +644,15 @@ var ItdErrorCode = Object.freeze({
588
644
  BUSINESS_RULE_VIOLATION: "BUSINESS_RULE_VIOLATION",
589
645
  RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
590
646
  UNKNOWN_ERROR: "UNKNOWN_ERROR",
647
+ /** Сервер отвечает так на `404`, `ENTITY_NOT_FOUND` в этом случае не приходит. */
648
+ NOT_FOUND: "NOT_FOUND",
649
+ /** На практике не приходит: вместо него сервер шлёт `TURNSTILE_VERIFICATION_FAILED`. */
591
650
  CAPTCHA_FAILED: "CAPTCHA_FAILED",
651
+ /** Капча не пройдена: токен Turnstile недействителен, просрочен или уже использован. */
652
+ TURNSTILE_VERIFICATION_FAILED: "TURNSTILE_VERIFICATION_FAILED",
592
653
  OTP_INVALID: "OTP_INVALID",
654
+ /** `flowToken` неизвестен или просрочен — поток подтверждения нужно начинать заново. */
655
+ INVALID_FLOW_TOKEN: "INVALID_FLOW_TOKEN",
593
656
  ACCOUNT_DEACTIVATED: "ACCOUNT_DEACTIVATED",
594
657
  ACCOUNT_EMAIL_DOMAIN_NOT_ALLOWED: "ACCOUNT_EMAIL_DOMAIN_NOT_ALLOWED",
595
658
  ACCOUNT_INVALID_CREDENTIALS: "ACCOUNT_INVALID_CREDENTIALS",
@@ -598,6 +661,10 @@ var ItdErrorCode = Object.freeze({
598
661
  SESSION_EXPIRED: "SESSION_EXPIRED",
599
662
  SESSION_REVOKED: "SESSION_REVOKED",
600
663
  SESSION_INVALID_REFRESH_TOKEN: "SESSION_INVALID_REFRESH_TOKEN",
664
+ /** Запрос обновления пришёл без cookie `refresh_token` — продлевать нечего. */
665
+ REFRESH_TOKEN_MISSING: "REFRESH_TOKEN_MISSING",
666
+ /** Cookie `refresh_token` есть, но сессии за ней уже нет: отозвана или истекла. */
667
+ SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
601
668
  MISSING_FLOW_TOKEN: "MISSING_FLOW_TOKEN",
602
669
  PROFILE_USERNAME_TAKEN: "PROFILE_USERNAME_TAKEN",
603
670
  PROFILE_RESTRICTION_ACTIVE: "PROFILE_RESTRICTION_ACTIVE",
@@ -613,13 +680,14 @@ var ItdErrorCode = Object.freeze({
613
680
 
614
681
  // src/builders/report.ts
615
682
  var REASONS = new Set(Object.values(ReportReason));
683
+ var TARGET_TYPES = new Set(Object.values(ReportTargetType));
616
684
  function validateReport(input) {
617
685
  if (!input?.targetId || typeof input.targetId !== "string") {
618
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)");
619
687
  }
620
- if (input.targetType !== "post" && input.targetType !== "comment" && input.targetType !== "user") {
688
+ if (!TARGET_TYPES.has(input.targetType)) {
621
689
  throw new ItdConfigError(
622
- `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)}`
623
691
  );
624
692
  }
625
693
  if (!REASONS.has(input.reason)) {
@@ -657,11 +725,11 @@ function start(targetType, targetId) {
657
725
  }
658
726
  var report = Object.freeze({
659
727
  /** Жалоба на пост. */
660
- post: (postId) => start("post", postId),
728
+ post: (postId) => start(ReportTargetType.Post, postId),
661
729
  /** Жалоба на комментарий. */
662
- comment: (commentId) => start("comment", commentId),
730
+ comment: (commentId) => start(ReportTargetType.Comment, commentId),
663
731
  /** Жалоба на пользователя. */
664
- user: (userId) => start("user", userId)
732
+ user: (userId) => start(ReportTargetType.User, userId)
665
733
  });
666
734
  function resolveReport(input) {
667
735
  return resolveInput(input, () => new ReportBuilder({}), validateReport);
@@ -861,6 +929,8 @@ parseSetCookie.splitCookiesString = splitCookiesString;
861
929
 
862
930
  // src/core/cookies.ts
863
931
  var AUTH_FLAG_COOKIE = "is_auth";
932
+ var REFRESH_COOKIE = "refresh_token";
933
+ var REFRESH_COOKIE_PATH = "/api/v1/auth";
864
934
  var SERIALIZED_SEPARATOR = " ";
865
935
  function originOf(url) {
866
936
  try {
@@ -900,6 +970,15 @@ var CookieJar = class {
900
970
  const raw = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : splitCookiesString(headers.get("set-cookie") ?? "");
901
971
  if (raw.length > 0) this.setFromStrings(url, raw);
902
972
  }
973
+ /**
974
+ * Кладёт cookie напрямую, минуя `Set-Cookie`.
975
+ *
976
+ * Нужно ровно в одном случае: пользователь передал refresh-токен строкой, а сервер читает
977
+ * его только из cookie. Значение не кодируется — оно уходит в заголовок как есть.
978
+ */
979
+ set(url, name, value, path = "/") {
980
+ this.setFromStrings(url, [`${name}=${value}; Path=${path}`]);
981
+ }
903
982
  /** Сохраняет cookie из готовых строк `Set-Cookie`. */
904
983
  setFromStrings(url, setCookieStrings) {
905
984
  const origin = originOf(url);
@@ -933,6 +1012,23 @@ var CookieJar = class {
933
1012
  if (cookies.length === 0) return void 0;
934
1013
  return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
935
1014
  }
1015
+ /**
1016
+ * Значение действующей cookie.
1017
+ *
1018
+ * Нужно, чтобы забрать обновлённый refresh-токен: сервер ротирует его при каждом
1019
+ * продлении сессии, и сохранять надо именно новое значение.
1020
+ *
1021
+ * @param url если указан, учитываются origin, путь и флаг `Secure`
1022
+ */
1023
+ getValue(name, url) {
1024
+ if (url) return this.#matching(url).find((cookie) => cookie.name === name)?.value;
1025
+ const now = Date.now();
1026
+ for (const jar of this.#byOrigin.values()) {
1027
+ const cookie = jar.get(name);
1028
+ if (cookie && (cookie.expires === void 0 || cookie.expires > now)) return cookie.value;
1029
+ }
1030
+ return void 0;
1031
+ }
936
1032
  /**
937
1033
  * Есть ли действующая cookie с таким именем.
938
1034
  *
@@ -1061,11 +1157,87 @@ var Emitter = class {
1061
1157
  }
1062
1158
  };
1063
1159
 
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
+ });
1175
+ function detectRuntime() {
1176
+ const nav = globalThis.navigator;
1177
+ if (nav?.product === "ReactNative") return DetectedRuntime.ReactNative;
1178
+ if (typeof document !== "undefined") return DetectedRuntime.Browser;
1179
+ return DetectedRuntime.Server;
1180
+ }
1181
+ function shouldUseCookieJar(mode) {
1182
+ if (mode === RuntimeMode.Browser) return false;
1183
+ if (mode === RuntimeMode.Server) return true;
1184
+ return detectRuntime() === DetectedRuntime.Server;
1185
+ }
1186
+ function shouldSendCredentials(mode) {
1187
+ if (mode === RuntimeMode.Browser) return true;
1188
+ if (mode === RuntimeMode.Server) return false;
1189
+ return detectRuntime() === DetectedRuntime.Browser;
1190
+ }
1191
+ function resolveFetch(custom) {
1192
+ if (custom) return custom;
1193
+ if (typeof globalThis.fetch === "function") {
1194
+ return globalThis.fetch.bind(globalThis);
1195
+ }
1196
+ throw new ItdConfigError(
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."
1198
+ );
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
+ }
1206
+ function supportsStreamingBody() {
1207
+ return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
1208
+ }
1209
+ function createDeviceId() {
1210
+ const webCrypto = globalThis.crypto;
1211
+ if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
1212
+ const bytes = new Uint8Array(16);
1213
+ if (typeof webCrypto?.getRandomValues === "function") webCrypto.getRandomValues(bytes);
1214
+ else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
1215
+ bytes[6] = bytes[6] & 15 | 64;
1216
+ bytes[8] = bytes[8] & 63 | 128;
1217
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1218
+ return [
1219
+ hex.slice(0, 8),
1220
+ hex.slice(8, 12),
1221
+ hex.slice(12, 16),
1222
+ hex.slice(16, 20),
1223
+ hex.slice(20, 32)
1224
+ ].join("-");
1225
+ }
1226
+ function hasLocalStorage() {
1227
+ try {
1228
+ return typeof globalThis.localStorage !== "undefined" && globalThis.localStorage !== null;
1229
+ } catch {
1230
+ return false;
1231
+ }
1232
+ }
1233
+
1064
1234
  // src/core/auth.ts
1065
1235
  var AUTH_PATHS = {
1066
1236
  signIn: "/api/v1/auth/sign-in",
1067
1237
  refresh: "/api/v1/auth/refresh"
1068
1238
  };
1239
+ var TURNSTILE_SITE_KEY = "0x4AAAAAACHhxczw6fJGwPBg";
1240
+ var DEVICE_ID_HEADER = "X-Device-Id";
1069
1241
  function readAccessToken(payload) {
1070
1242
  if (typeof payload !== "object" || payload === null) return void 0;
1071
1243
  const token = payload.accessToken;
@@ -1082,6 +1254,13 @@ var AuthManager = class {
1082
1254
  #refreshing = null;
1083
1255
  /** Общий промис входа по логину и паролю. */
1084
1256
  #signingIn = null;
1257
+ /**
1258
+ * Идентификатор устройства.
1259
+ *
1260
+ * Держится отдельно от сессии намеренно: выход из аккаунта не меняет устройство,
1261
+ * поэтому `clear()` его не трогает.
1262
+ */
1263
+ #deviceId;
1085
1264
  constructor(config, http, jar) {
1086
1265
  this.#config = config;
1087
1266
  this.#http = http;
@@ -1098,11 +1277,19 @@ var AuthManager = class {
1098
1277
  /**
1099
1278
  * Есть ли признак живой refresh-сессии.
1100
1279
  *
1101
- * Сайт итд.com ставит рядом с refresh-токеном незакрытую cookie `is_auth` — по ней клиент
1102
- * понимает, что обновление вообще имеет смысл, и не дёргает API у анонимов.
1280
+ * Рядом с refresh-токеном сервер ставит незакрытую cookie `is_auth` — по ней видно,
1281
+ * что продлевать сессию вообще есть смысл, и API не дёргается у анонимов.
1103
1282
  * В браузере cookie ведёт сама среда, поэтому там ответ всегда `true`.
1283
+ *
1284
+ * Асинхронный, потому что признак может лежать в {@link TokenStorage}: до чтения оттуда
1285
+ * ответ был бы `false` даже при полностью рабочей сохранённой сессии.
1104
1286
  */
1105
- hasRefreshSession() {
1287
+ async hasRefreshSession() {
1288
+ await this.#loadSession();
1289
+ return this.#hasRefreshSession();
1290
+ }
1291
+ /** То же самое, но без чтения хранилища — для вызовов, где сессия уже загружена. */
1292
+ #hasRefreshSession() {
1106
1293
  if (!this.#config.useCookieJar) return true;
1107
1294
  if (this.#jar.has(AUTH_FLAG_COOKIE)) return true;
1108
1295
  return Boolean(this.#session?.refreshToken);
@@ -1112,6 +1299,23 @@ var AuthManager = class {
1112
1299
  const token = await this.getAccessToken();
1113
1300
  return token ? { Authorization: `Bearer ${token}` } : {};
1114
1301
  }
1302
+ /**
1303
+ * Идентификатор устройства для заголовка `X-Device-Id`.
1304
+ *
1305
+ * Заводится один раз и сохраняется в сессии, чтобы пережить перезапуск процесса:
1306
+ * сервер связывает с ним запись в списке сессий, и плавающее значение плодило бы
1307
+ * по новой сессии на каждый старт.
1308
+ */
1309
+ async getDeviceId() {
1310
+ if (this.#deviceId) return this.#deviceId;
1311
+ const session = await this.#loadSession();
1312
+ const deviceId = this.#config.deviceId ?? session?.deviceId ?? createDeviceId();
1313
+ this.#deviceId = deviceId;
1314
+ if (session?.deviceId !== deviceId) {
1315
+ await this.#saveSession({ ...session ?? {}, deviceId });
1316
+ }
1317
+ return deviceId;
1318
+ }
1115
1319
  /**
1116
1320
  * Текущий токен доступа.
1117
1321
  *
@@ -1127,7 +1331,7 @@ var AuthManager = class {
1127
1331
  return await auth.getToken() ?? null;
1128
1332
  }
1129
1333
  if (typeof auth === "object" && "email" in auth) {
1130
- return this.#signInWithCredentials(auth.email, auth.password);
1334
+ return this.#signInWithCredentials(auth);
1131
1335
  }
1132
1336
  return null;
1133
1337
  }
@@ -1147,12 +1351,19 @@ var AuthManager = class {
1147
1351
  return false;
1148
1352
  }
1149
1353
  }
1150
- /** Ошибка «сессию продлить нечем» — одна и та же для `refresh()` и для реакции на 401. */
1354
+ /**
1355
+ * Ошибка «сессию продлить нечем».
1356
+ *
1357
+ * Возникает, только когда обновление даже не начиналось: нет ни cookie `is_auth`,
1358
+ * ни refresh-токена. Если сервер ответил отказом, наружу уходит **его** ошибка —
1359
+ * подменять её этой значило бы прятать причину (`REFRESH_TOKEN_MISSING`,
1360
+ * `SESSION_NOT_FOUND`, `SESSION_REVOKED` — разные поводы и разные действия).
1361
+ */
1151
1362
  #noRefreshSessionError() {
1152
1363
  return new ItdAuthError({
1153
1364
  status: 401,
1154
1365
  code: "SESSION_EXPIRED",
1155
- message: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0435\u0441\u0441\u0438\u044E: \u043D\u0435\u0442 \u0434\u0435\u0439\u0441\u0442\u0432\u0443\u044E\u0449\u0435\u0433\u043E refresh-\u0442\u043E\u043A\u0435\u043D\u0430",
1366
+ message: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0435\u0441\u0441\u0438\u044E: \u043D\u0435\u0442 \u043D\u0438 cookie is_auth, \u043D\u0438 refresh-\u0442\u043E\u043A\u0435\u043D\u0430. \u0412\u043E\u0439\u0434\u0438\u0442\u0435 \u0437\u0430\u043D\u043E\u0432\u043E \u043B\u0438\u0431\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 refreshToken \u0432 auth.",
1156
1367
  method: "POST",
1157
1368
  path: AUTH_PATHS.refresh,
1158
1369
  raw: void 0
@@ -1182,13 +1393,21 @@ var AuthManager = class {
1182
1393
  /** Заменяет сессию целиком. */
1183
1394
  async setSession(session) {
1184
1395
  this.#jar.deserialize(session.cookies);
1396
+ this.#deviceId ??= session.deviceId;
1185
1397
  await this.#saveSession(session);
1398
+ this.#seedRefreshCookie();
1186
1399
  }
1187
- /** Забывает сессию и cookie. Сетевой запрос не выполняется. */
1400
+ /**
1401
+ * Забывает сессию и cookie. Сетевой запрос не выполняется.
1402
+ *
1403
+ * Идентификатор устройства выход переживает: иначе каждая пара «выход — вход» плодила бы
1404
+ * новую запись в списке сессий.
1405
+ */
1188
1406
  async clear() {
1189
1407
  this.#session = null;
1190
1408
  this.#jar.clear();
1191
1409
  await this.#config.storage.clear();
1410
+ if (this.#deviceId) await this.#saveSession({ deviceId: this.#deviceId });
1192
1411
  this.#emitter.emit("signOut", void 0);
1193
1412
  }
1194
1413
  async #loadSession() {
@@ -1201,8 +1420,23 @@ var AuthManager = class {
1201
1420
  accessToken: stored.accessToken ?? fromConfig.accessToken,
1202
1421
  refreshToken: stored.refreshToken ?? fromConfig.refreshToken
1203
1422
  } : stored ?? fromConfig;
1423
+ this.#seedRefreshCookie();
1204
1424
  return this.#session;
1205
1425
  }
1426
+ /**
1427
+ * Кладёт refresh-токен в jar как cookie `refresh_token`.
1428
+ *
1429
+ * `POST /api/v1/auth/refresh` читает токен только из cookie, поэтому переданный строкой
1430
+ * приходится превращать в неё. В браузере это невозможно — cookie помечена `HttpOnly`,
1431
+ * и там обновление работает только на той, что поставил сам сервер.
1432
+ */
1433
+ #seedRefreshCookie() {
1434
+ if (!this.#config.useCookieJar) return;
1435
+ const refreshToken = this.#session?.refreshToken;
1436
+ if (!refreshToken) return;
1437
+ if (this.#jar.has(REFRESH_COOKIE)) return;
1438
+ this.#jar.set(this.#config.baseUrl, REFRESH_COOKIE, refreshToken, REFRESH_COOKIE_PATH);
1439
+ }
1206
1440
  #sessionFromConfig(auth) {
1207
1441
  if (!auth) return null;
1208
1442
  if (typeof auth === "string") return { accessToken: auth, obtainedAt: Date.now() };
@@ -1217,7 +1451,12 @@ var AuthManager = class {
1217
1451
  }
1218
1452
  async #saveSession(session) {
1219
1453
  const cookies = this.#config.useCookieJar ? this.#jar.serialize() : void 0;
1220
- const next = { ...session, ...cookies?.length ? { cookies } : {} };
1454
+ const deviceId = session.deviceId ?? this.#deviceId;
1455
+ const next = {
1456
+ ...session,
1457
+ ...cookies?.length ? { cookies } : {},
1458
+ ...deviceId ? { deviceId } : {}
1459
+ };
1221
1460
  this.#session = next;
1222
1461
  await this.#config.storage.set(next);
1223
1462
  }
@@ -1236,24 +1475,32 @@ var AuthManager = class {
1236
1475
  }
1237
1476
  async #performRefresh() {
1238
1477
  await this.#loadSession();
1239
- if (!this.hasRefreshSession()) {
1478
+ if (!this.#hasRefreshSession()) {
1240
1479
  return this.#reloginOrNull();
1241
1480
  }
1242
1481
  try {
1243
1482
  const payload = await this.#http.request({
1244
1483
  method: "POST",
1245
1484
  path: AUTH_PATHS.refresh,
1246
- // Обновление опирается на cookie, а не на устаревший Bearer.
1485
+ // Тела нет намеренно: сервер читает refresh-токен только из cookie — см.
1486
+ // #seedRefreshCookie. По той же причине не нужен и устаревший Bearer.
1247
1487
  skipAuth: true,
1248
1488
  // Без этого 401 на самом обновлении вызвал бы новое обновление — и так по кругу.
1249
1489
  skipAuthRefresh: true,
1250
- ...this.#session?.refreshToken ? { body: { refreshToken: this.#session.refreshToken } } : {}
1490
+ // Обновление почти всегда запускается изнутри запроса, который занимает место
1491
+ // в очереди и ждёт его результата. Встать в ту же очередь — значит зависнуть.
1492
+ skipQueue: true
1251
1493
  });
1252
1494
  const accessToken = readAccessToken(payload);
1253
1495
  if (!accessToken) return this.#reloginOrNull();
1496
+ const rotated = this.#jar.getValue(
1497
+ REFRESH_COOKIE,
1498
+ this.#config.baseUrl + REFRESH_COOKIE_PATH
1499
+ );
1254
1500
  await this.#saveSession({
1255
1501
  ...this.#session ?? {},
1256
1502
  accessToken,
1503
+ ...rotated ? { refreshToken: rotated } : {},
1257
1504
  obtainedAt: Date.now()
1258
1505
  });
1259
1506
  this.#emitter.emit("tokens", { accessToken });
@@ -1261,8 +1508,11 @@ var AuthManager = class {
1261
1508
  } catch (error) {
1262
1509
  if (error instanceof ItdApiError) {
1263
1510
  this.#session = null;
1511
+ this.#jar.clear();
1264
1512
  await this.#config.storage.clear();
1265
- return this.#reloginOrNull();
1513
+ const relogged = await this.#reloginOrNull();
1514
+ if (relogged !== null) return relogged;
1515
+ throw error;
1266
1516
  }
1267
1517
  throw error;
1268
1518
  }
@@ -1274,7 +1524,7 @@ var AuthManager = class {
1274
1524
  return null;
1275
1525
  }
1276
1526
  try {
1277
- return await this.#signInWithCredentials(auth.email, auth.password);
1527
+ return await this.#signInWithCredentials(auth);
1278
1528
  } catch {
1279
1529
  return null;
1280
1530
  }
@@ -1285,21 +1535,41 @@ var AuthManager = class {
1285
1535
  * Параллельные вызовы объединяются: одновременный старт нескольких запросов не должен
1286
1536
  * приводить к нескольким попыткам входа и блокировке аккаунта.
1287
1537
  */
1288
- #signInWithCredentials(email, password) {
1538
+ #signInWithCredentials(credentials) {
1289
1539
  if (this.#signingIn) return this.#signingIn;
1290
- const promise = this.#performSignIn(email, password).finally(() => {
1540
+ const promise = this.#performSignIn(credentials).finally(() => {
1291
1541
  this.#signingIn = null;
1292
1542
  });
1293
1543
  this.#signingIn = promise;
1294
1544
  return promise;
1295
1545
  }
1296
- async #performSignIn(email, password) {
1546
+ /**
1547
+ * Берёт токен капчи для входа.
1548
+ *
1549
+ * `getTurnstileToken` приоритетнее готовой строки: токен Turnstile одноразовый и живёт
1550
+ * несколько минут, поэтому при повторном входе через сутки годится только свежий.
1551
+ */
1552
+ async #resolveTurnstileToken(credentials) {
1553
+ if (credentials.getTurnstileToken) {
1554
+ const token = await credentials.getTurnstileToken();
1555
+ if (token) return token;
1556
+ }
1557
+ if (credentials.turnstileToken) return credentials.turnstileToken;
1558
+ throw new ItdConfigError(
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."
1560
+ );
1561
+ }
1562
+ async #performSignIn(credentials) {
1563
+ const turnstileToken = await this.#resolveTurnstileToken(credentials);
1297
1564
  const payload = await this.#http.request({
1298
1565
  method: "POST",
1299
1566
  path: AUTH_PATHS.signIn,
1300
- body: { email, password },
1567
+ body: { email: credentials.email, password: credentials.password, turnstileToken },
1301
1568
  skipAuth: true,
1302
- skipAuthRefresh: true
1569
+ skipAuthRefresh: true,
1570
+ // Отложенный вход происходит при сборке заголовков уже начатого запроса — тот держит
1571
+ // место в очереди и ждёт токена. См. `skipQueue` в RawRequestOptions.
1572
+ skipQueue: true
1303
1573
  });
1304
1574
  const accessToken = readAccessToken(payload);
1305
1575
  if (!accessToken) {
@@ -1314,43 +1584,6 @@ var AuthManager = class {
1314
1584
  }
1315
1585
  };
1316
1586
 
1317
- // src/core/runtime.ts
1318
- function detectRuntime() {
1319
- const nav = globalThis.navigator;
1320
- if (nav?.product === "ReactNative") return "react-native";
1321
- if (typeof document !== "undefined") return "browser";
1322
- return "server";
1323
- }
1324
- function shouldUseCookieJar(mode) {
1325
- if (mode === "browser") return false;
1326
- if (mode === "server") return true;
1327
- return detectRuntime() === "server";
1328
- }
1329
- function shouldSendCredentials(mode) {
1330
- if (mode === "browser") return true;
1331
- if (mode === "server") return false;
1332
- return detectRuntime() === "browser";
1333
- }
1334
- function resolveFetch(custom) {
1335
- if (custom) return custom;
1336
- if (typeof globalThis.fetch === "function") {
1337
- return globalThis.fetch.bind(globalThis);
1338
- }
1339
- throw new ItdConfigError(
1340
- "\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."
1341
- );
1342
- }
1343
- function supportsStreamingBody() {
1344
- return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
1345
- }
1346
- function hasLocalStorage() {
1347
- try {
1348
- return typeof globalThis.localStorage !== "undefined" && globalThis.localStorage !== null;
1349
- } catch {
1350
- return false;
1351
- }
1352
- }
1353
-
1354
1587
  // src/core/storage.ts
1355
1588
  var MemoryTokenStorage = class {
1356
1589
  #session = null;
@@ -1465,6 +1698,8 @@ function normalizeBaseUrl(baseUrl) {
1465
1698
  // src/core/config.ts
1466
1699
  var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
1467
1700
  var DEFAULT_TIMEOUT = 3e4;
1701
+ var LIBRARY_VERSION = "0.0.4";
1702
+ var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
1468
1703
  var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
1469
1704
  function requirePositive(value, name) {
1470
1705
  if (!Number.isFinite(value) || value < 0) {
@@ -1556,13 +1791,19 @@ function validateAuth(auth) {
1556
1791
  return auth;
1557
1792
  }
1558
1793
  if ("email" in auth || "password" in auth) {
1559
- const { email, password } = auth;
1794
+ const { email, password, turnstileToken, getTurnstileToken } = auth;
1560
1795
  if (typeof email !== "string" || email.trim() === "") {
1561
1796
  throw new ItdConfigError("auth.email \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439");
1562
1797
  }
1563
1798
  if (typeof password !== "string" || password === "") {
1564
1799
  throw new ItdConfigError("auth.password \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439");
1565
1800
  }
1801
+ if (getTurnstileToken !== void 0 && typeof getTurnstileToken !== "function") {
1802
+ throw new ItdConfigError("auth.getTurnstileToken \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0444\u0443\u043D\u043A\u0446\u0438\u0435\u0439");
1803
+ }
1804
+ if (turnstileToken !== void 0 && (typeof turnstileToken !== "string" || turnstileToken.trim() === "")) {
1805
+ throw new ItdConfigError("auth.turnstileToken \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439");
1806
+ }
1566
1807
  return auth;
1567
1808
  }
1568
1809
  throw new ItdConfigError(
@@ -1570,11 +1811,16 @@ function validateAuth(auth) {
1570
1811
  );
1571
1812
  }
1572
1813
  function resolveConfig(options = {}) {
1573
- const mode = options.mode ?? "auto";
1574
- if (mode !== "auto" && mode !== "browser" && mode !== "server") {
1575
- 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
+ );
1576
1819
  }
1577
1820
  const timeout = requirePositive(options.timeout ?? DEFAULT_TIMEOUT, "timeout");
1821
+ if (options.deviceId !== void 0 && (typeof options.deviceId !== "string" || options.deviceId.trim() === "")) {
1822
+ throw new ItdConfigError("deviceId \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439");
1823
+ }
1578
1824
  return {
1579
1825
  baseUrl: normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL),
1580
1826
  auth: validateAuth(options.auth),
@@ -1588,19 +1834,102 @@ function resolveConfig(options = {}) {
1588
1834
  hooks: options.hooks ?? {},
1589
1835
  logger: options.logger === true ? consoleLogger() : options.logger || void 0,
1590
1836
  headers: { ...options.headers },
1837
+ deviceId: options.deviceId,
1838
+ // `false` — способ не слать заголовок вовсе; строка заменяет умолчание.
1839
+ userAgent: options.userAgent === false ? void 0 : options.userAgent ?? DEFAULT_USER_AGENT,
1591
1840
  mode,
1592
1841
  useCookieJar: shouldUseCookieJar(mode),
1593
1842
  sendCredentials: shouldSendCredentials(mode)
1594
1843
  };
1595
1844
  }
1596
1845
 
1597
- // src/core/error-factory.ts
1846
+ // src/core/redact.ts
1847
+ var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
1848
+ var SECRET_FIELDS = /* @__PURE__ */ new Set([
1849
+ "password",
1850
+ "oldpassword",
1851
+ "newpassword",
1852
+ "accesstoken",
1853
+ "refreshtoken",
1854
+ "currentpassword",
1855
+ "flowtoken",
1856
+ "token",
1857
+ "turnstiletoken",
1858
+ "otp"
1859
+ ]);
1860
+ function maskSecret(value) {
1861
+ if (value.length <= 8) return "\u2026";
1862
+ return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
1863
+ }
1864
+ function redactHeaders(headers) {
1865
+ const result = {};
1866
+ headers.forEach((value, name) => {
1867
+ if (SECRET_HEADERS.has(name.toLowerCase())) {
1868
+ const spaceAt = value.indexOf(" ");
1869
+ result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
1870
+ return;
1871
+ }
1872
+ result[name] = value;
1873
+ });
1874
+ return result;
1875
+ }
1876
+ function redactBody(body) {
1877
+ if (body === null || body === void 0) return body;
1878
+ if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
1879
+ if (isBlob(body)) return "[Blob]";
1880
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
1881
+ if (Array.isArray(body)) return body.map(redactBody);
1882
+ if (typeof body === "object") {
1883
+ const result = {};
1884
+ for (const [key, value] of Object.entries(body)) {
1885
+ result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
1886
+ }
1887
+ return result;
1888
+ }
1889
+ return body;
1890
+ }
1891
+
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
+ }
1598
1899
  function isRecord(value) {
1599
1900
  return typeof value === "object" && value !== null && !Array.isArray(value);
1600
1901
  }
1601
1902
  function asString(value) {
1602
1903
  return typeof value === "string" && value.length > 0 ? value : void 0;
1603
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
1604
1933
  function collectFieldErrors(source) {
1605
1934
  const result = {};
1606
1935
  const errors = source.errors;
@@ -1709,6 +2038,10 @@ var CODE_TO_CLASS = {
1709
2038
  SESSION_EXPIRED: ItdAuthError,
1710
2039
  SESSION_REVOKED: ItdAuthError,
1711
2040
  SESSION_INVALID_REFRESH_TOKEN: ItdAuthError,
2041
+ // Оба приходят с `/auth/refresh`: первый — когда cookie refresh_token не долетела,
2042
+ // второй — когда она есть, но сессия за ней уже мертва.
2043
+ REFRESH_TOKEN_MISSING: ItdAuthError,
2044
+ SESSION_NOT_FOUND: ItdAuthError,
1712
2045
  ACCOUNT_INVALID_CREDENTIALS: ItdAuthError,
1713
2046
  ACCESS_DENIED: ItdForbiddenError,
1714
2047
  ENTITY_NOT_FOUND: ItdNotFoundError,
@@ -1726,6 +2059,10 @@ function classByStatus(status) {
1726
2059
  if (status >= 500) return ItdServerError;
1727
2060
  return ItdApiError;
1728
2061
  }
2062
+ function safeRawBody(body) {
2063
+ if (!isRecord(body) || body.type !== "validation" || !isRecord(body.found)) return body;
2064
+ return { ...body, found: redactBody(body.found) };
2065
+ }
1729
2066
  function createApiError(context) {
1730
2067
  const parsed = parseErrorBody(context.body, context.status, context.statusText);
1731
2068
  const rateLimit = readRateLimit(context.headers);
@@ -1741,7 +2078,7 @@ function createApiError(context) {
1741
2078
  requestId: getRequestId(context.headers),
1742
2079
  method: context.method,
1743
2080
  path: context.path,
1744
- raw: context.body,
2081
+ raw: safeRawBody(context.body),
1745
2082
  response: context.response,
1746
2083
  retryAfter: parseRetryAfter(context.headers?.get("retry-after"))
1747
2084
  };
@@ -1752,84 +2089,6 @@ function createApiError(context) {
1752
2089
  return new Ctor(init);
1753
2090
  }
1754
2091
 
1755
- // src/core/redact.ts
1756
- var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
1757
- var SECRET_FIELDS = /* @__PURE__ */ new Set([
1758
- "password",
1759
- "oldpassword",
1760
- "newpassword",
1761
- "accesstoken",
1762
- "refreshtoken",
1763
- "flowtoken",
1764
- "token",
1765
- "otp"
1766
- ]);
1767
- function maskSecret(value) {
1768
- if (value.length <= 8) return "\u2026";
1769
- return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
1770
- }
1771
- function redactHeaders(headers) {
1772
- const result = {};
1773
- headers.forEach((value, name) => {
1774
- if (SECRET_HEADERS.has(name.toLowerCase())) {
1775
- const spaceAt = value.indexOf(" ");
1776
- result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
1777
- return;
1778
- }
1779
- result[name] = value;
1780
- });
1781
- return result;
1782
- }
1783
- function redactBody(body) {
1784
- if (body === null || body === void 0) return body;
1785
- if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
1786
- if (typeof Blob !== "undefined" && body instanceof Blob) return "[Blob]";
1787
- if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
1788
- if (Array.isArray(body)) return body.map(redactBody);
1789
- if (typeof body === "object") {
1790
- const result = {};
1791
- for (const [key, value] of Object.entries(body)) {
1792
- result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
1793
- }
1794
- return result;
1795
- }
1796
- return body;
1797
- }
1798
-
1799
- // src/core/unwrap.ts
1800
- function unwrapData(body) {
1801
- if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
1802
- const keys = Object.keys(body);
1803
- if (keys.length !== 1 || keys[0] !== "data") return body;
1804
- return body.data;
1805
- }
1806
- function pickArray(source, field) {
1807
- if (typeof source !== "object" || source === null) return [];
1808
- const value = source[field];
1809
- return Array.isArray(value) ? value : [];
1810
- }
1811
- function pickObject(source, field) {
1812
- if (typeof source !== "object" || source === null) return void 0;
1813
- const value = source[field];
1814
- if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
1815
- return value;
1816
- }
1817
- function pickBoolean(source, field, fallback = false) {
1818
- if (typeof source !== "object" || source === null) return fallback;
1819
- const value = source[field];
1820
- return typeof value === "boolean" ? value : fallback;
1821
- }
1822
- function pickNumber(source, field, fallback) {
1823
- if (typeof source !== "object" || source === null) return fallback;
1824
- const value = source[field];
1825
- return typeof value === "number" && Number.isFinite(value) ? value : fallback;
1826
- }
1827
- function pickString(source, field) {
1828
- if (typeof source !== "object" || source === null) return void 0;
1829
- const value = source[field];
1830
- return typeof value === "string" && value.length > 0 ? value : void 0;
1831
- }
1832
-
1833
2092
  // src/core/http.ts
1834
2093
  function sleep(ms) {
1835
2094
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -1845,7 +2104,7 @@ function setHeader(headers, name, value) {
1845
2104
  }
1846
2105
  function isRawBody(body) {
1847
2106
  if (typeof body !== "object" || body === null) return typeof body === "string";
1848
- 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);
1849
2108
  }
1850
2109
  async function readBody(response) {
1851
2110
  if (response.status === 204 || response.status === 205) return void 0;
@@ -1915,7 +2174,8 @@ var HttpClient = class {
1915
2174
  */
1916
2175
  async request(options) {
1917
2176
  const task = () => this.#withRetries(options);
1918
- return this.#collaborators.schedule ? this.#collaborators.schedule(task) : task();
2177
+ if (!this.#collaborators.schedule || options.skipQueue) return task();
2178
+ return this.#collaborators.schedule(task);
1919
2179
  }
1920
2180
  async #withRetries(options) {
1921
2181
  const method = options.method.toUpperCase();
@@ -1947,6 +2207,11 @@ var HttpClient = class {
1947
2207
  async #buildHeaders(options, url) {
1948
2208
  const headers = new Headers();
1949
2209
  headers.set("Accept", "application/json");
2210
+ headers.set("X-Requested-With", "XMLHttpRequest");
2211
+ if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
2212
+ if (this.#collaborators.getDeviceId) {
2213
+ setHeader(headers, "X-Device-Id", await this.#collaborators.getDeviceId());
2214
+ }
1950
2215
  for (const [name, value] of Object.entries(this.#config.headers))
1951
2216
  setHeader(headers, name, value);
1952
2217
  if (!options.skipAuth && this.#collaborators.getAuthHeaders) {
@@ -2179,21 +2444,15 @@ function isKnownNotificationType(type) {
2179
2444
  }
2180
2445
 
2181
2446
  // src/notifications/normalize.ts
2182
- function isRecord2(value) {
2183
- return typeof value === "object" && value !== null && !Array.isArray(value);
2184
- }
2185
- function asString2(value) {
2186
- return typeof value === "string" && value.length > 0 ? value : void 0;
2187
- }
2188
2447
  function asActor(value) {
2189
- if (!isRecord2(value)) return void 0;
2190
- const id = asString2(value.id);
2448
+ if (!isRecord(value)) return void 0;
2449
+ const id = asString(value.id);
2191
2450
  if (!id) return void 0;
2192
2451
  return {
2193
2452
  id,
2194
- username: asString2(value.username) ?? "",
2195
- displayName: asString2(value.displayName) ?? "",
2196
- avatar: asString2(value.avatar) ?? "",
2453
+ username: asString(value.username) ?? "",
2454
+ displayName: asString(value.displayName) ?? "",
2455
+ avatar: asString(value.avatar) ?? "",
2197
2456
  ...typeof value.isFollowing === "boolean" ? { isFollowing: value.isFollowing } : {},
2198
2457
  ...typeof value.isFollowedBy === "boolean" ? { isFollowedBy: value.isFollowedBy } : {}
2199
2458
  };
@@ -2206,33 +2465,34 @@ function readActors(source) {
2206
2465
  return single ? [single] : [];
2207
2466
  }
2208
2467
  function normalizeNotification(input) {
2209
- const source = isRecord2(input) ? input : {};
2210
- const payload = isRecord2(source.payload) ? source.payload : source;
2211
- const rawType = asString2(payload.type) ?? asString2(source.type) ?? "";
2212
- const createdAt = asString2(payload.createdAt) ?? asString2(source.createdAt) ?? "";
2213
- 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);
2214
2473
  const isRead = typeof payload.isRead === "boolean" ? payload.isRead : typeof payload.read === "boolean" ? payload.read : Boolean(readAt);
2215
- const subjectId = asString2(payload.subjectId);
2216
- const targetId = asString2(payload.targetId);
2474
+ const subjectId = asString(payload.subjectId);
2475
+ const targetId = asString(payload.targetId);
2217
2476
  const subjectIsComment = payload.subjectType === "comment";
2477
+ const clickUrl = asString(payload.clickUrl);
2218
2478
  return {
2219
- id: asString2(payload.id) ?? asString2(source.id) ?? "",
2479
+ id: asString(payload.id) ?? asString(source.id) ?? "",
2220
2480
  type: canonicalNotificationType(rawType),
2221
2481
  rawType,
2222
- entityId: asString2(payload.entityId) ?? (subjectIsComment ? subjectId ?? targetId : targetId) ?? null,
2223
- 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),
2224
2484
  isRead,
2225
2485
  actors: readActors(payload),
2226
2486
  count: typeof payload.count === "number" && payload.count > 0 ? payload.count : 1,
2227
- preview: asString2(payload.entityPreview) ?? asString2(payload.preview) ?? null,
2228
- ...asString2(payload.clickUrl) ? { clickUrl: asString2(payload.clickUrl) } : {},
2487
+ preview: asString(payload.entityPreview) ?? asString(payload.preview) ?? null,
2488
+ ...clickUrl ? { clickUrl } : {},
2229
2489
  createdAt,
2230
- updatedAt: asString2(payload.updatedAt) ?? readAt ?? createdAt,
2490
+ updatedAt: asString(payload.updatedAt) ?? readAt ?? createdAt,
2231
2491
  raw: input
2232
2492
  };
2233
2493
  }
2234
2494
  function readNotificationEvent(data) {
2235
- const source = isRecord2(data) ? data : {};
2495
+ const source = isRecord(data) ? data : {};
2236
2496
  return {
2237
2497
  notification: normalizeNotification(data),
2238
2498
  unreadCount: typeof source.unreadCount === "number" ? source.unreadCount : void 0,
@@ -2240,8 +2500,8 @@ function readNotificationEvent(data) {
2240
2500
  };
2241
2501
  }
2242
2502
  function readUnreadCountEvent(data) {
2243
- if (!isRecord2(data)) return void 0;
2244
- 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;
2245
2505
  if (!payload) return void 0;
2246
2506
  return typeof payload.count === "number" ? payload.count : void 0;
2247
2507
  }
@@ -2320,6 +2580,7 @@ var PollTransport = class {
2320
2580
  }
2321
2581
  /** Ждёт следующего опроса, прерываясь при отмене. */
2322
2582
  #wait(signal) {
2583
+ if (signal.aborted) return Promise.resolve();
2323
2584
  return new Promise((resolve) => {
2324
2585
  const timer = setTimeout(finish, this.#interval);
2325
2586
  function finish() {
@@ -2592,6 +2853,14 @@ var SseTransport = class {
2592
2853
  };
2593
2854
 
2594
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
+ });
2595
2864
  var ItdRealtime = class {
2596
2865
  #deps;
2597
2866
  #options;
@@ -2599,6 +2868,15 @@ var ItdRealtime = class {
2599
2868
  #transport;
2600
2869
  #maxAttempts;
2601
2870
  #controller;
2871
+ /**
2872
+ * Хочет ли вызывающий код, чтобы соединение было живо.
2873
+ *
2874
+ * Отдельно от `#controller`, потому что тот появляется только после `await` внутри
2875
+ * {@link connect}. Без этого флага два вызова подряд проскочили бы проверку оба
2876
+ * и подняли два соединения, а `disconnect()` во время ожидания счётчика не был бы
2877
+ * замечен и соединение поднялось бы уже после отмены.
2878
+ */
2879
+ #wanted = false;
2602
2880
  #status = RealtimeStatus.Disconnected;
2603
2881
  #attempt = 0;
2604
2882
  #timer;
@@ -2634,7 +2912,8 @@ var ItdRealtime = class {
2634
2912
  * Возвращает управление сразу после запуска: соединение живёт в фоне.
2635
2913
  */
2636
2914
  async connect() {
2637
- if (this.#controller) return;
2915
+ if (this.#wanted) return;
2916
+ this.#wanted = true;
2638
2917
  this.#attachEnvironmentListeners();
2639
2918
  if (this.#options.syncCount !== false) {
2640
2919
  try {
@@ -2643,10 +2922,11 @@ var ItdRealtime = class {
2643
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);
2644
2923
  }
2645
2924
  }
2646
- this.#run();
2925
+ if (this.#wanted) this.#run();
2647
2926
  }
2648
2927
  /** Закрывает соединение и отменяет запланированные попытки. */
2649
2928
  disconnect() {
2929
+ this.#wanted = false;
2650
2930
  if (this.#timer !== void 0) {
2651
2931
  clearTimeout(this.#timer);
2652
2932
  this.#timer = void 0;
@@ -2663,9 +2943,9 @@ var ItdRealtime = class {
2663
2943
  this.#emitter.removeAllListeners();
2664
2944
  }
2665
2945
  #createTransport() {
2666
- const kind = this.#options.transport ?? "auto";
2946
+ const kind = this.#options.transport ?? RealtimeTransportKind.Auto;
2667
2947
  if (typeof kind === "object") return kind;
2668
- if (kind === "poll" || kind === "auto" && !supportsStreamingBody()) {
2948
+ if (kind === RealtimeTransportKind.Poll || kind === RealtimeTransportKind.Auto && !supportsStreamingBody()) {
2669
2949
  return new PollTransport({
2670
2950
  ...this.#options.pollInterval !== void 0 ? { interval: this.#options.pollInterval } : {}
2671
2951
  });
@@ -2676,6 +2956,7 @@ var ItdRealtime = class {
2676
2956
  }
2677
2957
  /** Запускает попытку подключения; повторы планирует сам. */
2678
2958
  #run() {
2959
+ this.#controller?.abort();
2679
2960
  const controller = new AbortController();
2680
2961
  this.#controller = controller;
2681
2962
  this.#setStatus(RealtimeStatus.Connecting);
@@ -2705,8 +2986,7 @@ var ItdRealtime = class {
2705
2986
  #handleEvent(name, data) {
2706
2987
  this.#emitter.emit("message", { name, data });
2707
2988
  if (name === "connected") {
2708
- const userId = typeof data === "object" && data !== null && "userId" in data ? String(data.userId) : void 0;
2709
- this.#emitter.emit("ready", { userId });
2989
+ this.#emitter.emit("ready", { userId: pickString(data, "userId") });
2710
2990
  return;
2711
2991
  }
2712
2992
  if (name === "notification") {
@@ -2794,13 +3074,22 @@ var ItdRealtime = class {
2794
3074
  };
2795
3075
 
2796
3076
  // src/core/pagination.ts
3077
+ var PaginationMode = Object.freeze({
3078
+ /** Следующая страница запрашивается непрозрачным курсором. */
3079
+ Cursor: "cursor",
3080
+ /** Следующая страница запрашивается номером. */
3081
+ Page: "page",
3082
+ /** Следующая страница запрашивается смещением от начала списка. */
3083
+ Offset: "offset"
3084
+ });
2797
3085
  function readItems(body, fields) {
2798
3086
  if (Array.isArray(body)) return body;
2799
3087
  for (const field of fields) {
2800
3088
  const items = pickArray(body, field);
2801
3089
  if (items.length > 0) return items;
2802
3090
  }
2803
- return fields.length > 0 ? pickArray(body, fields[0]) : [];
3091
+ const primary = fields[0];
3092
+ return primary === void 0 ? [] : pickArray(body, primary);
2804
3093
  }
2805
3094
  function readCursor(body) {
2806
3095
  const pagination = pickObject(body, "pagination");
@@ -2859,12 +3148,13 @@ function readOffsetPage(body, field, offset) {
2859
3148
  var Paginator = class {
2860
3149
  #options;
2861
3150
  #maxPages;
2862
- #state = {};
3151
+ #state;
2863
3152
  #finished = false;
2864
3153
  #pagesLoaded = 0;
2865
3154
  constructor(options) {
2866
3155
  this.#options = options;
2867
3156
  this.#maxPages = options.maxPages ?? 1e3;
3157
+ this.#state = options.start ?? {};
2868
3158
  }
2869
3159
  /**
2870
3160
  * Загружает следующую страницу.
@@ -2930,7 +3220,7 @@ var Paginator = class {
2930
3220
  this.#finished = true;
2931
3221
  return previous;
2932
3222
  }
2933
- if (this.#options.mode === "cursor") {
3223
+ if (this.#options.mode === PaginationMode.Cursor) {
2934
3224
  const cursor = page.nextCursor ?? void 0;
2935
3225
  if (!cursor || cursor === previous.cursor) {
2936
3226
  this.#finished = true;
@@ -2938,7 +3228,7 @@ var Paginator = class {
2938
3228
  }
2939
3229
  return { cursor };
2940
3230
  }
2941
- if (this.#options.mode === "page") {
3231
+ if (this.#options.mode === PaginationMode.Page) {
2942
3232
  return { page: (previous.page ?? 1) + 1 };
2943
3233
  }
2944
3234
  return { offset: page.nextOffset ?? (previous.offset ?? 0) + page.items.length };
@@ -2967,13 +3257,15 @@ var BaseResource = class {
2967
3257
  *
2968
3258
  * @param mode схема пагинации эндпоинта
2969
3259
  * @param load загружает одну страницу для указанной позиции
3260
+ * @param options `maxPages` и `signal`, а также `start` — позиция, с которой продолжить
2970
3261
  */
2971
3262
  paginate(mode, load, options) {
2972
3263
  return new Paginator({
2973
3264
  mode,
2974
3265
  load,
2975
3266
  ...options?.maxPages !== void 0 ? { maxPages: options.maxPages } : {},
2976
- ...options?.signal !== void 0 ? { signal: options.signal } : {}
3267
+ ...options?.signal !== void 0 ? { signal: options.signal } : {},
3268
+ ...options?.start !== void 0 ? { start: options.start } : {}
2977
3269
  });
2978
3270
  }
2979
3271
  };
@@ -2987,6 +3279,14 @@ function withPageState(query, state) {
2987
3279
  }
2988
3280
 
2989
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
+ });
2990
3290
  var AuthResource = class extends BaseResource {
2991
3291
  #auth;
2992
3292
  constructor(http, deps) {
@@ -3020,6 +3320,8 @@ var AuthResource = class extends BaseResource {
3020
3320
  * тогда продолжайте через {@link verifyOtp} либо воспользуйтесь {@link signInWithOtp}.
3021
3321
  *
3022
3322
  * При успешном входе токен сохраняется в клиенте автоматически.
3323
+ *
3324
+ * @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
3023
3325
  */
3024
3326
  async signIn(credentials, options = {}) {
3025
3327
  const body = await this.http.request({
@@ -3033,9 +3335,9 @@ var AuthResource = class extends BaseResource {
3033
3335
  const accessToken = pickString(body, "accessToken");
3034
3336
  if (accessToken) {
3035
3337
  await this.#auth.setAccessToken(accessToken);
3036
- return { status: "authenticated", accessToken };
3338
+ return { status: SignInStatus.Authenticated, accessToken };
3037
3339
  }
3038
- return { status: "otp_required", flowToken: pickString(body, "flowToken") };
3340
+ return { status: SignInStatus.OtpRequired, flowToken: pickString(body, "flowToken") };
3039
3341
  }
3040
3342
  /**
3041
3343
  * Подтверждает вход кодом из письма.
@@ -3090,14 +3392,22 @@ var AuthResource = class extends BaseResource {
3090
3392
  async signInWithOtp(input, options = {}) {
3091
3393
  const { getOtp, ...credentials } = input;
3092
3394
  const result = await this.signIn(credentials, options);
3093
- if (result.status === "authenticated") return result.accessToken;
3395
+ if (result.status === SignInStatus.Authenticated) return result.accessToken;
3094
3396
  if (!result.flowToken) {
3095
3397
  throw new ItdConfigError(
3096
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"
3097
3399
  );
3098
3400
  }
3099
3401
  const otp = await getOtp();
3100
- return this.verifyOtp({ ...credentials, otp, flowToken: result.flowToken }, options);
3402
+ return this.verifyOtp(
3403
+ {
3404
+ email: credentials.email,
3405
+ password: credentials.password,
3406
+ otp,
3407
+ flowToken: result.flowToken
3408
+ },
3409
+ options
3410
+ );
3101
3411
  }
3102
3412
  /**
3103
3413
  * Обновляет токен доступа.
@@ -3111,9 +3421,18 @@ var AuthResource = class extends BaseResource {
3111
3421
  /**
3112
3422
  * Есть ли признак живой сессии обновления.
3113
3423
  *
3114
- * Проверяет cookie `is_auth`, которую сервер ставит рядом с refresh-токеном. Позволяет
3115
- * не дёргать API у неавторизованного пользователя. В браузере всегда `true`:
3116
- * 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
+ * ```
3117
3436
  */
3118
3437
  hasRefreshSession() {
3119
3438
  return this.#auth.hasRefreshSession();
@@ -3128,32 +3447,48 @@ var AuthResource = class extends BaseResource {
3128
3447
  });
3129
3448
  await this.#auth.clear();
3130
3449
  }
3131
- /** Завершает все сессии пользователя и очищает локальную. */
3450
+ /**
3451
+ * Завершает все сессии пользователя и очищает локальную.
3452
+ *
3453
+ * Собран из двух запросов, потому что единого эндпоинта на сервере нет:
3454
+ * `POST /api/v1/auth/logout-all` отвечает `404`. Сначала отзываются все прочие сессии
3455
+ * (`DELETE /api/v1/auth/sessions`), затем завершается текущая — в обратном порядке
3456
+ * отзывать было бы уже нечем.
3457
+ */
3132
3458
  async logoutAll(options = {}) {
3133
- await this.http.request({
3134
- method: "POST",
3135
- path: "/api/v1/auth/logout-all",
3136
- skipAuthRefresh: true,
3137
- ...this.requestOptions(options)
3138
- });
3139
- await this.#auth.clear();
3459
+ await this.revokeOtherSessions(options);
3460
+ await this.logout(options);
3140
3461
  }
3141
3462
  /** Забывает сессию локально, не обращаясь к серверу. */
3142
3463
  signOut() {
3143
3464
  return this.#auth.clear();
3144
3465
  }
3145
- /** Запрашивает письмо для сброса пароля. */
3146
- forgotPassword(email, options = {}) {
3147
- return this.http.request({
3466
+ /**
3467
+ * Запрашивает письмо с кодом для сброса пароля.
3468
+ *
3469
+ * @returns `flowToken`, который нужно передать в {@link resetPassword}
3470
+ */
3471
+ async forgotPassword(input, options = {}) {
3472
+ const body = await this.http.request({
3148
3473
  method: "POST",
3149
3474
  path: "/api/v1/auth/forgot-password",
3150
- body: { email },
3475
+ body: input,
3151
3476
  skipAuth: true,
3152
3477
  skipAuthRefresh: true,
3153
3478
  ...this.requestOptions(options)
3154
3479
  });
3480
+ const flowToken = pickString(body, "flowToken");
3481
+ if (!flowToken) {
3482
+ throw new ItdConfigError("\u0421\u0435\u0440\u0432\u0435\u0440 \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B flowToken \u043F\u0440\u0438 \u0437\u0430\u043F\u0440\u043E\u0441\u0435 \u0441\u0431\u0440\u043E\u0441\u0430 \u043F\u0430\u0440\u043E\u043B\u044F");
3483
+ }
3484
+ return flowToken;
3155
3485
  }
3156
- /** Устанавливает новый пароль по токену из письма. */
3486
+ /**
3487
+ * Устанавливает новый пароль по коду из письма.
3488
+ *
3489
+ * Сервер ждёт все четыре поля сразу — `email`, `otp`, `flowToken` и `newPassword`;
3490
+ * при нехватке любого отвечает `422`.
3491
+ */
3157
3492
  resetPassword(input, options = {}) {
3158
3493
  return this.http.request({
3159
3494
  method: "POST",
@@ -3164,12 +3499,45 @@ var AuthResource = class extends BaseResource {
3164
3499
  ...this.requestOptions(options)
3165
3500
  });
3166
3501
  }
3167
- /** Меняет пароль. Требует действующей сессии обновления. */
3502
+ /**
3503
+ * Полный сброс пароля с кодом из письма.
3504
+ *
3505
+ * Тот же приём, что и {@link signInWithOtp}: код запрашивается функцией `getOtp`,
3506
+ * остальное библиотека делает сама.
3507
+ *
3508
+ * @example
3509
+ * ```ts
3510
+ * await itd.auth.resetPasswordWithOtp({
3511
+ * email,
3512
+ * turnstileToken,
3513
+ * newPassword,
3514
+ * getOtp: () => rl.question('Код из письма: '),
3515
+ * });
3516
+ * ```
3517
+ */
3518
+ async resetPasswordWithOtp(input, options = {}) {
3519
+ const flowToken = await this.forgotPassword(
3520
+ { email: input.email, turnstileToken: input.turnstileToken },
3521
+ options
3522
+ );
3523
+ const otp = await input.getOtp();
3524
+ await this.resetPassword(
3525
+ { email: input.email, otp, flowToken, newPassword: input.newPassword },
3526
+ options
3527
+ );
3528
+ }
3529
+ /**
3530
+ * Меняет пароль. Требует действующей сессии.
3531
+ *
3532
+ * При неверном текущем пароле сервер отвечает `ACCOUNT_CURRENT_PASSWORD_INCORRECT`.
3533
+ */
3168
3534
  changePassword(input, options = {}) {
3169
3535
  return this.http.request({
3170
3536
  method: "POST",
3171
3537
  path: "/api/v1/auth/change-password",
3172
- body: input,
3538
+ // Текущий пароль уходит под двумя именами: какое из них ждёт сервер, снаружи
3539
+ // не проверить, а лишнее поле он игнорирует.
3540
+ body: { ...input, currentPassword: input.oldPassword },
3173
3541
  ...this.requestOptions(options)
3174
3542
  });
3175
3543
  }
@@ -3239,7 +3607,7 @@ var CommentsResource = class extends BaseResource {
3239
3607
  iterateReplies(commentId, params = {}) {
3240
3608
  const path = `/api/comments/${encodePathSegment(commentId, "commentId")}/replies`;
3241
3609
  return this.paginate(
3242
- "page",
3610
+ PaginationMode.Page,
3243
3611
  async (state) => {
3244
3612
  const body = await this.http.request({
3245
3613
  method: "GET",
@@ -3249,7 +3617,7 @@ var CommentsResource = class extends BaseResource {
3249
3617
  });
3250
3618
  return readPagedPage(body, "replies");
3251
3619
  },
3252
- params
3620
+ { ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
3253
3621
  );
3254
3622
  }
3255
3623
  /**
@@ -3330,7 +3698,11 @@ var IMAGE_MIME_TYPES = Object.freeze([
3330
3698
  "image/heic",
3331
3699
  "image/heif"
3332
3700
  ]);
3333
- 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
+ ]);
3334
3706
  var AUDIO_MIME_TYPES = Object.freeze(["audio/ogg"]);
3335
3707
  var ALLOWED_MIME_TYPES = Object.freeze([
3336
3708
  ...IMAGE_MIME_TYPES,
@@ -3409,7 +3781,7 @@ var FilesResource = class extends BaseResource {
3409
3781
  * ```
3410
3782
  */
3411
3783
  async upload(input, options = {}) {
3412
- const prepared = await this.prepare(input, options);
3784
+ const prepared = await this.#prepare(input, options);
3413
3785
  const form = new FormData();
3414
3786
  form.set("file", prepared.blob, prepared.filename);
3415
3787
  return this.http.request({
@@ -3460,11 +3832,11 @@ var FilesResource = class extends BaseResource {
3460
3832
  });
3461
3833
  }
3462
3834
  /** Приводит любой поддерживаемый вход к `Blob` с именем и проверенным типом. */
3463
- async prepare(input, options) {
3835
+ async #prepare(input, options) {
3464
3836
  const { data, filename, contentType } = await this.#normalize(input, options);
3465
- const type = contentType ?? ((data instanceof Blob ? data.type : void 0) || mimeFromFilename(filename));
3837
+ const type = contentType ?? ((isBlob(data) ? data.type : void 0) || mimeFromFilename(filename));
3466
3838
  if (options.validateMime !== false) assertAllowedMime(type || void 0, filename);
3467
- 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 ?? "" });
3468
3840
  return { blob, filename };
3469
3841
  }
3470
3842
  async #normalize(input, options) {
@@ -3481,8 +3853,8 @@ var FilesResource = class extends BaseResource {
3481
3853
  ...options.contentType ? { contentType: options.contentType } : {}
3482
3854
  };
3483
3855
  }
3484
- if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || input instanceof Blob) {
3485
- 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);
3486
3858
  return {
3487
3859
  data: input,
3488
3860
  filename: options.filename ?? fallbackName,
@@ -3548,7 +3920,7 @@ var HashtagsResource = class extends BaseResource {
3548
3920
  iteratePosts(tag, params = {}) {
3549
3921
  const path = `/api/hashtags/${encodePathSegment(tag, "tag")}/posts`;
3550
3922
  return this.paginate(
3551
- "cursor",
3923
+ PaginationMode.Cursor,
3552
3924
  async (state) => {
3553
3925
  const body = await this.http.request({
3554
3926
  method: "GET",
@@ -3558,7 +3930,7 @@ var HashtagsResource = class extends BaseResource {
3558
3930
  });
3559
3931
  return readCursorPage(body, "posts");
3560
3932
  },
3561
- params
3933
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
3562
3934
  );
3563
3935
  }
3564
3936
  };
@@ -3789,8 +4161,11 @@ var NotificationsResource = class extends BaseResource {
3789
4161
  * const next = await itd.notifications.list({ limit: 20, offset: page.nextOffset });
3790
4162
  * ```
3791
4163
  */
3792
- async list(params = {}) {
3793
- 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) {
3794
4169
  const body = await this.http.request({
3795
4170
  method: "GET",
3796
4171
  // Завершающий слэш обязателен: без него сервер отвечает ошибкой.
@@ -3813,19 +4188,9 @@ var NotificationsResource = class extends BaseResource {
3813
4188
  */
3814
4189
  iterate(params = {}) {
3815
4190
  return this.paginate(
3816
- "offset",
3817
- async (state) => {
3818
- const offset = state.offset ?? params.offset ?? 0;
3819
- const body = await this.http.request({
3820
- method: "GET",
3821
- path: "/api/notifications/",
3822
- query: { limit: params.limit, offset },
3823
- ...this.requestOptions(params)
3824
- });
3825
- const page = readOffsetPage(body, "notifications", offset);
3826
- return { ...page, items: page.items.map(normalizeNotification) };
3827
- },
3828
- params
4191
+ PaginationMode.Offset,
4192
+ (state) => this.#loadPage(params, state.offset ?? 0),
4193
+ { ...params, ...params.offset !== void 0 ? { start: { offset: params.offset } } : {} }
3829
4194
  );
3830
4195
  }
3831
4196
  /** Загружает число непрочитанных уведомлений. */
@@ -3957,7 +4322,7 @@ var PostsResource = class extends BaseResource {
3957
4322
  */
3958
4323
  iterate(params = {}) {
3959
4324
  return this.paginate(
3960
- "cursor",
4325
+ PaginationMode.Cursor,
3961
4326
  async (state) => {
3962
4327
  const body = await this.http.request({
3963
4328
  method: "GET",
@@ -3967,7 +4332,7 @@ var PostsResource = class extends BaseResource {
3967
4332
  });
3968
4333
  return readCursorPage(body, "posts");
3969
4334
  },
3970
- params
4335
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
3971
4336
  );
3972
4337
  }
3973
4338
  /**
@@ -4131,7 +4496,7 @@ var PostsResource = class extends BaseResource {
4131
4496
  iterateByUser(user, params = {}) {
4132
4497
  const path = `/api/posts/user/${encodePathSegment(user, "user")}`;
4133
4498
  return this.paginate(
4134
- "cursor",
4499
+ PaginationMode.Cursor,
4135
4500
  async (state) => {
4136
4501
  const body = await this.http.request({
4137
4502
  method: "GET",
@@ -4144,7 +4509,7 @@ var PostsResource = class extends BaseResource {
4144
4509
  });
4145
4510
  return readCursorPage(body, "posts");
4146
4511
  },
4147
- params
4512
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
4148
4513
  );
4149
4514
  }
4150
4515
  /** Загружает страницу постов, которые пользователь отметил реакцией. */
@@ -4161,7 +4526,7 @@ var PostsResource = class extends BaseResource {
4161
4526
  iterateLikedByUser(user, params = {}) {
4162
4527
  const path = `/api/posts/user/${encodePathSegment(user, "user")}/liked`;
4163
4528
  return this.paginate(
4164
- "cursor",
4529
+ PaginationMode.Cursor,
4165
4530
  async (state) => {
4166
4531
  const body = await this.http.request({
4167
4532
  method: "GET",
@@ -4171,7 +4536,7 @@ var PostsResource = class extends BaseResource {
4171
4536
  });
4172
4537
  return readCursorPage(body, "posts");
4173
4538
  },
4174
- params
4539
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
4175
4540
  );
4176
4541
  }
4177
4542
  /**
@@ -4193,7 +4558,7 @@ var PostsResource = class extends BaseResource {
4193
4558
  iterateComments(postId, params = {}) {
4194
4559
  const path = `/api/posts/${encodePathSegment(postId, "postId")}/comments`;
4195
4560
  return this.paginate(
4196
- "cursor",
4561
+ PaginationMode.Cursor,
4197
4562
  async (state) => {
4198
4563
  const body = await this.http.request({
4199
4564
  method: "GET",
@@ -4203,7 +4568,7 @@ var PostsResource = class extends BaseResource {
4203
4568
  });
4204
4569
  return readFlatCursorPage(body, "comments");
4205
4570
  },
4206
- params
4571
+ { ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
4207
4572
  );
4208
4573
  }
4209
4574
  /**
@@ -4481,28 +4846,33 @@ var UsersResource = class extends BaseResource {
4481
4846
  ...this.requestOptions(options)
4482
4847
  });
4483
4848
  }
4484
- async #userPage(path, params) {
4849
+ /**
4850
+ * Загружает одну страницу списка пользователей.
4851
+ *
4852
+ * Имена полей перечислены с запасом: списки подписчиков и заблокированных приходят
4853
+ * под `users`, но альтернативное имя ничего не стоит и спасает, если эндпоинт назовёт
4854
+ * список по-своему.
4855
+ */
4856
+ async #loadUserPage(path, params, state) {
4485
4857
  const body = await this.http.request({
4486
4858
  method: "GET",
4487
4859
  path,
4488
- query: { limit: params.limit, page: params.page },
4860
+ query: withPageState({ limit: params.limit }, state),
4489
4861
  ...this.requestOptions(params)
4490
4862
  });
4491
- return readPagedPage(body, "users");
4863
+ return readPagedPage(body, "users", "followers", "following", "blocked");
4864
+ }
4865
+ #userPage(path, params) {
4866
+ return this.#loadUserPage(path, params, {
4867
+ ...params.page !== void 0 ? { page: params.page } : {}
4868
+ });
4492
4869
  }
4493
4870
  #userPaginator(path, params) {
4494
4871
  return this.paginate(
4495
- "page",
4496
- async (state) => {
4497
- const body = await this.http.request({
4498
- method: "GET",
4499
- path,
4500
- query: withPageState({ limit: params.limit }, state),
4501
- ...this.requestOptions(params)
4502
- });
4503
- return readPagedPage(body, "users");
4504
- },
4505
- params
4872
+ PaginationMode.Page,
4873
+ (state) => this.#loadUserPage(path, params, state),
4874
+ // Без `start` перебор начинался бы с первой страницы, молча игнорируя `page`.
4875
+ { ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
4506
4876
  );
4507
4877
  }
4508
4878
  };
@@ -4552,6 +4922,7 @@ var ItdClient = class {
4552
4922
  this.#queue = this.#config.rateLimit ? new RequestQueue(this.#config.rateLimit) : void 0;
4553
4923
  this.#http.setCollaborators({
4554
4924
  getAuthHeaders: () => this.#authManager.getAuthHeaders(),
4925
+ getDeviceId: () => this.#authManager.getDeviceId(),
4555
4926
  onUnauthorized: () => this.#authManager.onUnauthorized(),
4556
4927
  getCookieHeader: (url) => this.#jar.getHeader(url),
4557
4928
  saveCookies: (url, response) => this.#jar.setFromResponse(url, response),
@@ -4822,20 +5193,27 @@ function toDate(value) {
4822
5193
 
4823
5194
  exports.ALLOWED_MIME_TYPES = ALLOWED_MIME_TYPES;
4824
5195
  exports.AUDIO_MIME_TYPES = AUDIO_MIME_TYPES;
5196
+ exports.AUTH_FLAG_COOKIE = AUTH_FLAG_COOKIE;
5197
+ exports.AUTH_PATHS = AUTH_PATHS;
4825
5198
  exports.AttachmentType = AttachmentType;
4826
5199
  exports.CommentSort = CommentSort;
4827
5200
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
4828
5201
  exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
5202
+ exports.DEFAULT_USER_AGENT = DEFAULT_USER_AGENT;
5203
+ exports.DEVICE_ID_HEADER = DEVICE_ID_HEADER;
5204
+ exports.DetectedRuntime = DetectedRuntime;
4829
5205
  exports.FeedTab = FeedTab;
4830
5206
  exports.IMAGE_MIME_TYPES = IMAGE_MIME_TYPES;
4831
5207
  exports.ItdAbortError = ItdAbortError;
4832
5208
  exports.ItdApiError = ItdApiError;
5209
+ exports.ItdApiErrorKind = ItdApiErrorKind;
4833
5210
  exports.ItdAuthError = ItdAuthError;
4834
5211
  exports.ItdClient = ItdClient;
4835
5212
  exports.ItdConfigError = ItdConfigError;
4836
5213
  exports.ItdConflictError = ItdConflictError;
4837
5214
  exports.ItdError = ItdError;
4838
5215
  exports.ItdErrorCode = ItdErrorCode;
5216
+ exports.ItdErrorKind = ItdErrorKind;
4839
5217
  exports.ItdForbiddenError = ItdForbiddenError;
4840
5218
  exports.ItdNetworkError = ItdNetworkError;
4841
5219
  exports.ItdNotFoundError = ItdNotFoundError;
@@ -4845,19 +5223,29 @@ exports.ItdRealtime = ItdRealtime;
4845
5223
  exports.ItdServerError = ItdServerError;
4846
5224
  exports.ItdTimeoutError = ItdTimeoutError;
4847
5225
  exports.ItdValidationError = ItdValidationError;
5226
+ exports.LIBRARY_VERSION = LIBRARY_VERSION;
4848
5227
  exports.LikesVisibility = LikesVisibility;
4849
5228
  exports.LocalStorageTokenStorage = LocalStorageTokenStorage;
4850
5229
  exports.MAX_RECONNECT_ATTEMPTS = MAX_RECONNECT_ATTEMPTS;
4851
5230
  exports.MemoryTokenStorage = MemoryTokenStorage;
4852
5231
  exports.NOTIFICATION_TYPE_ALIASES = NOTIFICATION_TYPE_ALIASES;
4853
5232
  exports.NotificationType = NotificationType;
5233
+ exports.OAuthProvider = OAuthProvider;
5234
+ exports.PaginationMode = PaginationMode;
4854
5235
  exports.Paginator = Paginator;
4855
5236
  exports.RECONNECT_BACKOFF = RECONNECT_BACKOFF;
4856
5237
  exports.RECONNECT_JITTER = RECONNECT_JITTER;
5238
+ exports.REFRESH_COOKIE = REFRESH_COOKIE;
5239
+ exports.REFRESH_COOKIE_PATH = REFRESH_COOKIE_PATH;
4857
5240
  exports.RealtimeStatus = RealtimeStatus;
5241
+ exports.RealtimeTransportKind = RealtimeTransportKind;
4858
5242
  exports.ReportReason = ReportReason;
4859
5243
  exports.ReportTargetType = ReportTargetType;
5244
+ exports.RuntimeMode = RuntimeMode;
4860
5245
  exports.STREAM_PATH = STREAM_PATH;
5246
+ exports.SignInStatus = SignInStatus;
5247
+ exports.TURNSTILE_SITE_KEY = TURNSTILE_SITE_KEY;
5248
+ exports.UnauthorizedStreamError = UnauthorizedStreamError;
4861
5249
  exports.VIDEO_MIME_TYPES = VIDEO_MIME_TYPES;
4862
5250
  exports.WallAccess = WallAccess;
4863
5251
  exports.canonicalNotificationType = canonicalNotificationType;
@@ -4868,8 +5256,13 @@ exports.formatNotificationText = formatNotificationText;
4868
5256
  exports.isBuilder = isBuilder;
4869
5257
  exports.isItdApiError = isItdApiError;
4870
5258
  exports.isItdAuthError = isItdAuthError;
5259
+ exports.isItdConflictError = isItdConflictError;
4871
5260
  exports.isItdError = isItdError;
5261
+ exports.isItdForbiddenError = isItdForbiddenError;
5262
+ exports.isItdNotFoundError = isItdNotFoundError;
5263
+ exports.isItdPhoneVerificationError = isItdPhoneVerificationError;
4872
5264
  exports.isItdRateLimitError = isItdRateLimitError;
5265
+ exports.isItdServerError = isItdServerError;
4873
5266
  exports.isItdValidationError = isItdValidationError;
4874
5267
  exports.isKnownNotificationType = isKnownNotificationType;
4875
5268
  exports.isMyProfile = isMyProfile;
@@ -4881,5 +5274,5 @@ exports.readUnreadCountEvent = readUnreadCountEvent;
4881
5274
  exports.report = report;
4882
5275
  exports.resolveNotificationUrl = resolveNotificationUrl;
4883
5276
  exports.toDate = toDate;
4884
- //# sourceMappingURL=chunk-QILCVTJI.cjs.map
4885
- //# sourceMappingURL=chunk-QILCVTJI.cjs.map
5277
+ //# sourceMappingURL=chunk-XG43KEYF.cjs.map
5278
+ //# sourceMappingURL=chunk-XG43KEYF.cjs.map