itd-api 0.0.1 → 0.0.3

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.
@@ -588,8 +588,15 @@ var ItdErrorCode = Object.freeze({
588
588
  BUSINESS_RULE_VIOLATION: "BUSINESS_RULE_VIOLATION",
589
589
  RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
590
590
  UNKNOWN_ERROR: "UNKNOWN_ERROR",
591
+ /** Сервер отвечает так на `404`, `ENTITY_NOT_FOUND` в этом случае не приходит. */
592
+ NOT_FOUND: "NOT_FOUND",
593
+ /** На практике не приходит: вместо него сервер шлёт `TURNSTILE_VERIFICATION_FAILED`. */
591
594
  CAPTCHA_FAILED: "CAPTCHA_FAILED",
595
+ /** Капча не пройдена: токен Turnstile недействителен, просрочен или уже использован. */
596
+ TURNSTILE_VERIFICATION_FAILED: "TURNSTILE_VERIFICATION_FAILED",
592
597
  OTP_INVALID: "OTP_INVALID",
598
+ /** `flowToken` неизвестен или просрочен — поток подтверждения нужно начинать заново. */
599
+ INVALID_FLOW_TOKEN: "INVALID_FLOW_TOKEN",
593
600
  ACCOUNT_DEACTIVATED: "ACCOUNT_DEACTIVATED",
594
601
  ACCOUNT_EMAIL_DOMAIN_NOT_ALLOWED: "ACCOUNT_EMAIL_DOMAIN_NOT_ALLOWED",
595
602
  ACCOUNT_INVALID_CREDENTIALS: "ACCOUNT_INVALID_CREDENTIALS",
@@ -598,6 +605,10 @@ var ItdErrorCode = Object.freeze({
598
605
  SESSION_EXPIRED: "SESSION_EXPIRED",
599
606
  SESSION_REVOKED: "SESSION_REVOKED",
600
607
  SESSION_INVALID_REFRESH_TOKEN: "SESSION_INVALID_REFRESH_TOKEN",
608
+ /** Запрос обновления пришёл без cookie `refresh_token` — продлевать нечего. */
609
+ REFRESH_TOKEN_MISSING: "REFRESH_TOKEN_MISSING",
610
+ /** Cookie `refresh_token` есть, но сессии за ней уже нет: отозвана или истекла. */
611
+ SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
601
612
  MISSING_FLOW_TOKEN: "MISSING_FLOW_TOKEN",
602
613
  PROFILE_USERNAME_TAKEN: "PROFILE_USERNAME_TAKEN",
603
614
  PROFILE_RESTRICTION_ACTIVE: "PROFILE_RESTRICTION_ACTIVE",
@@ -861,6 +872,8 @@ parseSetCookie.splitCookiesString = splitCookiesString;
861
872
 
862
873
  // src/core/cookies.ts
863
874
  var AUTH_FLAG_COOKIE = "is_auth";
875
+ var REFRESH_COOKIE = "refresh_token";
876
+ var REFRESH_COOKIE_PATH = "/api/v1/auth";
864
877
  var SERIALIZED_SEPARATOR = " ";
865
878
  function originOf(url) {
866
879
  try {
@@ -900,6 +913,15 @@ var CookieJar = class {
900
913
  const raw = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : splitCookiesString(headers.get("set-cookie") ?? "");
901
914
  if (raw.length > 0) this.setFromStrings(url, raw);
902
915
  }
916
+ /**
917
+ * Кладёт cookie напрямую, минуя `Set-Cookie`.
918
+ *
919
+ * Нужно ровно в одном случае: пользователь передал refresh-токен строкой, а сервер читает
920
+ * его только из cookie. Значение не кодируется — оно уходит в заголовок как есть.
921
+ */
922
+ set(url, name, value, path = "/") {
923
+ this.setFromStrings(url, [`${name}=${value}; Path=${path}`]);
924
+ }
903
925
  /** Сохраняет cookie из готовых строк `Set-Cookie`. */
904
926
  setFromStrings(url, setCookieStrings) {
905
927
  const origin = originOf(url);
@@ -933,6 +955,23 @@ var CookieJar = class {
933
955
  if (cookies.length === 0) return void 0;
934
956
  return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
935
957
  }
958
+ /**
959
+ * Значение действующей cookie.
960
+ *
961
+ * Нужно, чтобы забрать обновлённый refresh-токен: сервер ротирует его при каждом
962
+ * продлении сессии, и сохранять надо именно новое значение.
963
+ *
964
+ * @param url если указан, учитываются origin, путь и флаг `Secure`
965
+ */
966
+ getValue(name, url) {
967
+ if (url) return this.#matching(url).find((cookie) => cookie.name === name)?.value;
968
+ const now = Date.now();
969
+ for (const jar of this.#byOrigin.values()) {
970
+ const cookie = jar.get(name);
971
+ if (cookie && (cookie.expires === void 0 || cookie.expires > now)) return cookie.value;
972
+ }
973
+ return void 0;
974
+ }
936
975
  /**
937
976
  * Есть ли действующая cookie с таким именем.
938
977
  *
@@ -1061,11 +1100,67 @@ var Emitter = class {
1061
1100
  }
1062
1101
  };
1063
1102
 
1103
+ // src/core/runtime.ts
1104
+ function detectRuntime() {
1105
+ const nav = globalThis.navigator;
1106
+ if (nav?.product === "ReactNative") return "react-native";
1107
+ if (typeof document !== "undefined") return "browser";
1108
+ return "server";
1109
+ }
1110
+ function shouldUseCookieJar(mode) {
1111
+ if (mode === "browser") return false;
1112
+ if (mode === "server") return true;
1113
+ return detectRuntime() === "server";
1114
+ }
1115
+ function shouldSendCredentials(mode) {
1116
+ if (mode === "browser") return true;
1117
+ if (mode === "server") return false;
1118
+ return detectRuntime() === "browser";
1119
+ }
1120
+ function resolveFetch(custom) {
1121
+ if (custom) return custom;
1122
+ if (typeof globalThis.fetch === "function") {
1123
+ return globalThis.fetch.bind(globalThis);
1124
+ }
1125
+ throw new ItdConfigError(
1126
+ "\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
+ );
1128
+ }
1129
+ function supportsStreamingBody() {
1130
+ return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
1131
+ }
1132
+ function createDeviceId() {
1133
+ const webCrypto = globalThis.crypto;
1134
+ if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
1135
+ const bytes = new Uint8Array(16);
1136
+ if (typeof webCrypto?.getRandomValues === "function") webCrypto.getRandomValues(bytes);
1137
+ else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
1138
+ bytes[6] = bytes[6] & 15 | 64;
1139
+ bytes[8] = bytes[8] & 63 | 128;
1140
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1141
+ return [
1142
+ hex.slice(0, 8),
1143
+ hex.slice(8, 12),
1144
+ hex.slice(12, 16),
1145
+ hex.slice(16, 20),
1146
+ hex.slice(20, 32)
1147
+ ].join("-");
1148
+ }
1149
+ function hasLocalStorage() {
1150
+ try {
1151
+ return typeof globalThis.localStorage !== "undefined" && globalThis.localStorage !== null;
1152
+ } catch {
1153
+ return false;
1154
+ }
1155
+ }
1156
+
1064
1157
  // src/core/auth.ts
1065
1158
  var AUTH_PATHS = {
1066
1159
  signIn: "/api/v1/auth/sign-in",
1067
1160
  refresh: "/api/v1/auth/refresh"
1068
1161
  };
1162
+ var TURNSTILE_SITE_KEY = "0x4AAAAAACHhxczw6fJGwPBg";
1163
+ var DEVICE_ID_HEADER = "X-Device-Id";
1069
1164
  function readAccessToken(payload) {
1070
1165
  if (typeof payload !== "object" || payload === null) return void 0;
1071
1166
  const token = payload.accessToken;
@@ -1082,6 +1177,13 @@ var AuthManager = class {
1082
1177
  #refreshing = null;
1083
1178
  /** Общий промис входа по логину и паролю. */
1084
1179
  #signingIn = null;
1180
+ /**
1181
+ * Идентификатор устройства.
1182
+ *
1183
+ * Держится отдельно от сессии намеренно: выход из аккаунта не меняет устройство,
1184
+ * поэтому `clear()` его не трогает.
1185
+ */
1186
+ #deviceId;
1085
1187
  constructor(config, http, jar) {
1086
1188
  this.#config = config;
1087
1189
  this.#http = http;
@@ -1112,6 +1214,23 @@ var AuthManager = class {
1112
1214
  const token = await this.getAccessToken();
1113
1215
  return token ? { Authorization: `Bearer ${token}` } : {};
1114
1216
  }
1217
+ /**
1218
+ * Идентификатор устройства для заголовка `X-Device-Id`.
1219
+ *
1220
+ * Заводится один раз и сохраняется в сессии, чтобы пережить перезапуск процесса:
1221
+ * сервер связывает с ним запись в списке сессий, и плавающее значение плодило бы
1222
+ * по новой сессии на каждый старт.
1223
+ */
1224
+ async getDeviceId() {
1225
+ if (this.#deviceId) return this.#deviceId;
1226
+ const session = await this.#loadSession();
1227
+ const deviceId = this.#config.deviceId ?? session?.deviceId ?? createDeviceId();
1228
+ this.#deviceId = deviceId;
1229
+ if (session?.deviceId !== deviceId) {
1230
+ await this.#saveSession({ ...session ?? {}, deviceId });
1231
+ }
1232
+ return deviceId;
1233
+ }
1115
1234
  /**
1116
1235
  * Текущий токен доступа.
1117
1236
  *
@@ -1127,7 +1246,7 @@ var AuthManager = class {
1127
1246
  return await auth.getToken() ?? null;
1128
1247
  }
1129
1248
  if (typeof auth === "object" && "email" in auth) {
1130
- return this.#signInWithCredentials(auth.email, auth.password);
1249
+ return this.#signInWithCredentials(auth);
1131
1250
  }
1132
1251
  return null;
1133
1252
  }
@@ -1147,12 +1266,19 @@ var AuthManager = class {
1147
1266
  return false;
1148
1267
  }
1149
1268
  }
1150
- /** Ошибка «сессию продлить нечем» — одна и та же для `refresh()` и для реакции на 401. */
1269
+ /**
1270
+ * Ошибка «сессию продлить нечем».
1271
+ *
1272
+ * Возникает, только когда обновление даже не начиналось: нет ни cookie `is_auth`,
1273
+ * ни refresh-токена. Если сервер ответил отказом, наружу уходит **его** ошибка —
1274
+ * подменять её этой значило бы прятать причину (`REFRESH_TOKEN_MISSING`,
1275
+ * `SESSION_NOT_FOUND`, `SESSION_REVOKED` — разные поводы и разные действия).
1276
+ */
1151
1277
  #noRefreshSessionError() {
1152
1278
  return new ItdAuthError({
1153
1279
  status: 401,
1154
1280
  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",
1281
+ 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
1282
  method: "POST",
1157
1283
  path: AUTH_PATHS.refresh,
1158
1284
  raw: void 0
@@ -1182,13 +1308,21 @@ var AuthManager = class {
1182
1308
  /** Заменяет сессию целиком. */
1183
1309
  async setSession(session) {
1184
1310
  this.#jar.deserialize(session.cookies);
1311
+ this.#deviceId ??= session.deviceId;
1185
1312
  await this.#saveSession(session);
1313
+ this.#seedRefreshCookie();
1186
1314
  }
1187
- /** Забывает сессию и cookie. Сетевой запрос не выполняется. */
1315
+ /**
1316
+ * Забывает сессию и cookie. Сетевой запрос не выполняется.
1317
+ *
1318
+ * Идентификатор устройства выход переживает: иначе каждая пара «выход — вход» плодила бы
1319
+ * новую запись в списке сессий.
1320
+ */
1188
1321
  async clear() {
1189
1322
  this.#session = null;
1190
1323
  this.#jar.clear();
1191
1324
  await this.#config.storage.clear();
1325
+ if (this.#deviceId) await this.#saveSession({ deviceId: this.#deviceId });
1192
1326
  this.#emitter.emit("signOut", void 0);
1193
1327
  }
1194
1328
  async #loadSession() {
@@ -1201,8 +1335,23 @@ var AuthManager = class {
1201
1335
  accessToken: stored.accessToken ?? fromConfig.accessToken,
1202
1336
  refreshToken: stored.refreshToken ?? fromConfig.refreshToken
1203
1337
  } : stored ?? fromConfig;
1338
+ this.#seedRefreshCookie();
1204
1339
  return this.#session;
1205
1340
  }
1341
+ /**
1342
+ * Кладёт refresh-токен в jar как cookie `refresh_token`.
1343
+ *
1344
+ * `POST /api/v1/auth/refresh` читает токен только из cookie, поэтому переданный строкой
1345
+ * приходится превращать в неё. В браузере это невозможно — cookie помечена `HttpOnly`,
1346
+ * и там обновление работает только на той, что поставил сам сервер.
1347
+ */
1348
+ #seedRefreshCookie() {
1349
+ if (!this.#config.useCookieJar) return;
1350
+ const refreshToken = this.#session?.refreshToken;
1351
+ if (!refreshToken) return;
1352
+ if (this.#jar.has(REFRESH_COOKIE)) return;
1353
+ this.#jar.set(this.#config.baseUrl, REFRESH_COOKIE, refreshToken, REFRESH_COOKIE_PATH);
1354
+ }
1206
1355
  #sessionFromConfig(auth) {
1207
1356
  if (!auth) return null;
1208
1357
  if (typeof auth === "string") return { accessToken: auth, obtainedAt: Date.now() };
@@ -1217,7 +1366,12 @@ var AuthManager = class {
1217
1366
  }
1218
1367
  async #saveSession(session) {
1219
1368
  const cookies = this.#config.useCookieJar ? this.#jar.serialize() : void 0;
1220
- const next = { ...session, ...cookies?.length ? { cookies } : {} };
1369
+ const deviceId = session.deviceId ?? this.#deviceId;
1370
+ const next = {
1371
+ ...session,
1372
+ ...cookies?.length ? { cookies } : {},
1373
+ ...deviceId ? { deviceId } : {}
1374
+ };
1221
1375
  this.#session = next;
1222
1376
  await this.#config.storage.set(next);
1223
1377
  }
@@ -1243,17 +1397,22 @@ var AuthManager = class {
1243
1397
  const payload = await this.#http.request({
1244
1398
  method: "POST",
1245
1399
  path: AUTH_PATHS.refresh,
1246
- // Обновление опирается на cookie, а не на устаревший Bearer.
1400
+ // Тела нет намеренно: сервер читает refresh-токен только из cookie — см.
1401
+ // #seedRefreshCookie. По той же причине не нужен и устаревший Bearer.
1247
1402
  skipAuth: true,
1248
1403
  // Без этого 401 на самом обновлении вызвал бы новое обновление — и так по кругу.
1249
- skipAuthRefresh: true,
1250
- ...this.#session?.refreshToken ? { body: { refreshToken: this.#session.refreshToken } } : {}
1404
+ skipAuthRefresh: true
1251
1405
  });
1252
1406
  const accessToken = readAccessToken(payload);
1253
1407
  if (!accessToken) return this.#reloginOrNull();
1408
+ const rotated = this.#jar.getValue(
1409
+ REFRESH_COOKIE,
1410
+ this.#config.baseUrl + REFRESH_COOKIE_PATH
1411
+ );
1254
1412
  await this.#saveSession({
1255
1413
  ...this.#session ?? {},
1256
1414
  accessToken,
1415
+ ...rotated ? { refreshToken: rotated } : {},
1257
1416
  obtainedAt: Date.now()
1258
1417
  });
1259
1418
  this.#emitter.emit("tokens", { accessToken });
@@ -1261,8 +1420,11 @@ var AuthManager = class {
1261
1420
  } catch (error) {
1262
1421
  if (error instanceof ItdApiError) {
1263
1422
  this.#session = null;
1423
+ this.#jar.clear();
1264
1424
  await this.#config.storage.clear();
1265
- return this.#reloginOrNull();
1425
+ const relogged = await this.#reloginOrNull();
1426
+ if (relogged !== null) return relogged;
1427
+ throw error;
1266
1428
  }
1267
1429
  throw error;
1268
1430
  }
@@ -1274,7 +1436,7 @@ var AuthManager = class {
1274
1436
  return null;
1275
1437
  }
1276
1438
  try {
1277
- return await this.#signInWithCredentials(auth.email, auth.password);
1439
+ return await this.#signInWithCredentials(auth);
1278
1440
  } catch {
1279
1441
  return null;
1280
1442
  }
@@ -1285,19 +1447,36 @@ var AuthManager = class {
1285
1447
  * Параллельные вызовы объединяются: одновременный старт нескольких запросов не должен
1286
1448
  * приводить к нескольким попыткам входа и блокировке аккаунта.
1287
1449
  */
1288
- #signInWithCredentials(email, password) {
1450
+ #signInWithCredentials(credentials) {
1289
1451
  if (this.#signingIn) return this.#signingIn;
1290
- const promise = this.#performSignIn(email, password).finally(() => {
1452
+ const promise = this.#performSignIn(credentials).finally(() => {
1291
1453
  this.#signingIn = null;
1292
1454
  });
1293
1455
  this.#signingIn = promise;
1294
1456
  return promise;
1295
1457
  }
1296
- async #performSignIn(email, password) {
1458
+ /**
1459
+ * Берёт токен капчи для входа.
1460
+ *
1461
+ * `getTurnstileToken` приоритетнее готовой строки: токен Turnstile одноразовый и живёт
1462
+ * несколько минут, поэтому при повторном входе через сутки годится только свежий.
1463
+ */
1464
+ async #resolveTurnstileToken(credentials) {
1465
+ if (credentials.getTurnstileToken) {
1466
+ const token = await credentials.getTurnstileToken();
1467
+ if (token) return token;
1468
+ }
1469
+ if (credentials.turnstileToken) return credentials.turnstileToken;
1470
+ 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."
1472
+ );
1473
+ }
1474
+ async #performSignIn(credentials) {
1475
+ const turnstileToken = await this.#resolveTurnstileToken(credentials);
1297
1476
  const payload = await this.#http.request({
1298
1477
  method: "POST",
1299
1478
  path: AUTH_PATHS.signIn,
1300
- body: { email, password },
1479
+ body: { email: credentials.email, password: credentials.password, turnstileToken },
1301
1480
  skipAuth: true,
1302
1481
  skipAuthRefresh: true
1303
1482
  });
@@ -1314,43 +1493,6 @@ var AuthManager = class {
1314
1493
  }
1315
1494
  };
1316
1495
 
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
1496
  // src/core/storage.ts
1355
1497
  var MemoryTokenStorage = class {
1356
1498
  #session = null;
@@ -1465,6 +1607,8 @@ function normalizeBaseUrl(baseUrl) {
1465
1607
  // src/core/config.ts
1466
1608
  var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
1467
1609
  var DEFAULT_TIMEOUT = 3e4;
1610
+ var LIBRARY_VERSION = "0.0.2";
1611
+ var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
1468
1612
  var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
1469
1613
  function requirePositive(value, name) {
1470
1614
  if (!Number.isFinite(value) || value < 0) {
@@ -1556,13 +1700,19 @@ function validateAuth(auth) {
1556
1700
  return auth;
1557
1701
  }
1558
1702
  if ("email" in auth || "password" in auth) {
1559
- const { email, password } = auth;
1703
+ const { email, password, turnstileToken, getTurnstileToken } = auth;
1560
1704
  if (typeof email !== "string" || email.trim() === "") {
1561
1705
  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
1706
  }
1563
1707
  if (typeof password !== "string" || password === "") {
1564
1708
  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
1709
  }
1710
+ if (getTurnstileToken !== void 0 && typeof getTurnstileToken !== "function") {
1711
+ throw new ItdConfigError("auth.getTurnstileToken \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0444\u0443\u043D\u043A\u0446\u0438\u0435\u0439");
1712
+ }
1713
+ if (turnstileToken !== void 0 && (typeof turnstileToken !== "string" || turnstileToken.trim() === "")) {
1714
+ 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");
1715
+ }
1566
1716
  return auth;
1567
1717
  }
1568
1718
  throw new ItdConfigError(
@@ -1575,6 +1725,9 @@ function resolveConfig(options = {}) {
1575
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}`);
1576
1726
  }
1577
1727
  const timeout = requirePositive(options.timeout ?? DEFAULT_TIMEOUT, "timeout");
1728
+ if (options.deviceId !== void 0 && (typeof options.deviceId !== "string" || options.deviceId.trim() === "")) {
1729
+ 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");
1730
+ }
1578
1731
  return {
1579
1732
  baseUrl: normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL),
1580
1733
  auth: validateAuth(options.auth),
@@ -1588,12 +1741,61 @@ function resolveConfig(options = {}) {
1588
1741
  hooks: options.hooks ?? {},
1589
1742
  logger: options.logger === true ? consoleLogger() : options.logger || void 0,
1590
1743
  headers: { ...options.headers },
1744
+ deviceId: options.deviceId,
1745
+ // `false` — способ не слать заголовок вовсе; строка заменяет умолчание.
1746
+ userAgent: options.userAgent === false ? void 0 : options.userAgent ?? DEFAULT_USER_AGENT,
1591
1747
  mode,
1592
1748
  useCookieJar: shouldUseCookieJar(mode),
1593
1749
  sendCredentials: shouldSendCredentials(mode)
1594
1750
  };
1595
1751
  }
1596
1752
 
1753
+ // src/core/redact.ts
1754
+ var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
1755
+ var SECRET_FIELDS = /* @__PURE__ */ new Set([
1756
+ "password",
1757
+ "oldpassword",
1758
+ "newpassword",
1759
+ "accesstoken",
1760
+ "refreshtoken",
1761
+ "currentpassword",
1762
+ "flowtoken",
1763
+ "token",
1764
+ "turnstiletoken",
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
+
1597
1799
  // src/core/error-factory.ts
1598
1800
  function isRecord(value) {
1599
1801
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -1709,6 +1911,10 @@ var CODE_TO_CLASS = {
1709
1911
  SESSION_EXPIRED: ItdAuthError,
1710
1912
  SESSION_REVOKED: ItdAuthError,
1711
1913
  SESSION_INVALID_REFRESH_TOKEN: ItdAuthError,
1914
+ // Оба приходят с `/auth/refresh`: первый — когда cookie refresh_token не долетела,
1915
+ // второй — когда она есть, но сессия за ней уже мертва.
1916
+ REFRESH_TOKEN_MISSING: ItdAuthError,
1917
+ SESSION_NOT_FOUND: ItdAuthError,
1712
1918
  ACCOUNT_INVALID_CREDENTIALS: ItdAuthError,
1713
1919
  ACCESS_DENIED: ItdForbiddenError,
1714
1920
  ENTITY_NOT_FOUND: ItdNotFoundError,
@@ -1726,6 +1932,10 @@ function classByStatus(status) {
1726
1932
  if (status >= 500) return ItdServerError;
1727
1933
  return ItdApiError;
1728
1934
  }
1935
+ function safeRawBody(body) {
1936
+ if (!isRecord(body) || body.type !== "validation" || !isRecord(body.found)) return body;
1937
+ return { ...body, found: redactBody(body.found) };
1938
+ }
1729
1939
  function createApiError(context) {
1730
1940
  const parsed = parseErrorBody(context.body, context.status, context.statusText);
1731
1941
  const rateLimit = readRateLimit(context.headers);
@@ -1741,7 +1951,7 @@ function createApiError(context) {
1741
1951
  requestId: getRequestId(context.headers),
1742
1952
  method: context.method,
1743
1953
  path: context.path,
1744
- raw: context.body,
1954
+ raw: safeRawBody(context.body),
1745
1955
  response: context.response,
1746
1956
  retryAfter: parseRetryAfter(context.headers?.get("retry-after"))
1747
1957
  };
@@ -1752,50 +1962,6 @@ function createApiError(context) {
1752
1962
  return new Ctor(init);
1753
1963
  }
1754
1964
 
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
1965
  // src/core/unwrap.ts
1800
1966
  function unwrapData(body) {
1801
1967
  if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
@@ -1947,6 +2113,11 @@ var HttpClient = class {
1947
2113
  async #buildHeaders(options, url) {
1948
2114
  const headers = new Headers();
1949
2115
  headers.set("Accept", "application/json");
2116
+ headers.set("X-Requested-With", "XMLHttpRequest");
2117
+ if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
2118
+ if (this.#collaborators.getDeviceId) {
2119
+ setHeader(headers, "X-Device-Id", await this.#collaborators.getDeviceId());
2120
+ }
1950
2121
  for (const [name, value] of Object.entries(this.#config.headers))
1951
2122
  setHeader(headers, name, value);
1952
2123
  if (!options.skipAuth && this.#collaborators.getAuthHeaders) {
@@ -3020,6 +3191,8 @@ var AuthResource = class extends BaseResource {
3020
3191
  * тогда продолжайте через {@link verifyOtp} либо воспользуйтесь {@link signInWithOtp}.
3021
3192
  *
3022
3193
  * При успешном входе токен сохраняется в клиенте автоматически.
3194
+ *
3195
+ * @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
3023
3196
  */
3024
3197
  async signIn(credentials, options = {}) {
3025
3198
  const body = await this.http.request({
@@ -3097,7 +3270,15 @@ var AuthResource = class extends BaseResource {
3097
3270
  );
3098
3271
  }
3099
3272
  const otp = await getOtp();
3100
- return this.verifyOtp({ ...credentials, otp, flowToken: result.flowToken }, options);
3273
+ return this.verifyOtp(
3274
+ {
3275
+ email: credentials.email,
3276
+ password: credentials.password,
3277
+ otp,
3278
+ flowToken: result.flowToken
3279
+ },
3280
+ options
3281
+ );
3101
3282
  }
3102
3283
  /**
3103
3284
  * Обновляет токен доступа.
@@ -3128,32 +3309,48 @@ var AuthResource = class extends BaseResource {
3128
3309
  });
3129
3310
  await this.#auth.clear();
3130
3311
  }
3131
- /** Завершает все сессии пользователя и очищает локальную. */
3312
+ /**
3313
+ * Завершает все сессии пользователя и очищает локальную.
3314
+ *
3315
+ * Собран из двух запросов, потому что единого эндпоинта на сервере нет:
3316
+ * `POST /api/v1/auth/logout-all` отвечает `404`. Сначала отзываются все прочие сессии
3317
+ * (`DELETE /api/v1/auth/sessions`), затем завершается текущая — в обратном порядке
3318
+ * отзывать было бы уже нечем.
3319
+ */
3132
3320
  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();
3321
+ await this.revokeOtherSessions(options);
3322
+ await this.logout(options);
3140
3323
  }
3141
3324
  /** Забывает сессию локально, не обращаясь к серверу. */
3142
3325
  signOut() {
3143
3326
  return this.#auth.clear();
3144
3327
  }
3145
- /** Запрашивает письмо для сброса пароля. */
3146
- forgotPassword(email, options = {}) {
3147
- return this.http.request({
3328
+ /**
3329
+ * Запрашивает письмо с кодом для сброса пароля.
3330
+ *
3331
+ * @returns `flowToken`, который нужно передать в {@link resetPassword}
3332
+ */
3333
+ async forgotPassword(input, options = {}) {
3334
+ const body = await this.http.request({
3148
3335
  method: "POST",
3149
3336
  path: "/api/v1/auth/forgot-password",
3150
- body: { email },
3337
+ body: input,
3151
3338
  skipAuth: true,
3152
3339
  skipAuthRefresh: true,
3153
3340
  ...this.requestOptions(options)
3154
3341
  });
3342
+ const flowToken = pickString(body, "flowToken");
3343
+ if (!flowToken) {
3344
+ 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");
3345
+ }
3346
+ return flowToken;
3155
3347
  }
3156
- /** Устанавливает новый пароль по токену из письма. */
3348
+ /**
3349
+ * Устанавливает новый пароль по коду из письма.
3350
+ *
3351
+ * Сервер ждёт все четыре поля сразу — `email`, `otp`, `flowToken` и `newPassword`;
3352
+ * при нехватке любого отвечает `422`.
3353
+ */
3157
3354
  resetPassword(input, options = {}) {
3158
3355
  return this.http.request({
3159
3356
  method: "POST",
@@ -3164,12 +3361,45 @@ var AuthResource = class extends BaseResource {
3164
3361
  ...this.requestOptions(options)
3165
3362
  });
3166
3363
  }
3167
- /** Меняет пароль. Требует действующей сессии обновления. */
3364
+ /**
3365
+ * Полный сброс пароля с кодом из письма.
3366
+ *
3367
+ * Тот же приём, что и {@link signInWithOtp}: код запрашивается функцией `getOtp`,
3368
+ * остальное библиотека делает сама.
3369
+ *
3370
+ * @example
3371
+ * ```ts
3372
+ * await itd.auth.resetPasswordWithOtp({
3373
+ * email,
3374
+ * turnstileToken,
3375
+ * newPassword,
3376
+ * getOtp: () => rl.question('Код из письма: '),
3377
+ * });
3378
+ * ```
3379
+ */
3380
+ async resetPasswordWithOtp(input, options = {}) {
3381
+ const flowToken = await this.forgotPassword(
3382
+ { email: input.email, turnstileToken: input.turnstileToken },
3383
+ options
3384
+ );
3385
+ const otp = await input.getOtp();
3386
+ await this.resetPassword(
3387
+ { email: input.email, otp, flowToken, newPassword: input.newPassword },
3388
+ options
3389
+ );
3390
+ }
3391
+ /**
3392
+ * Меняет пароль. Требует действующей сессии.
3393
+ *
3394
+ * При неверном текущем пароле сервер отвечает `ACCOUNT_CURRENT_PASSWORD_INCORRECT`.
3395
+ */
3168
3396
  changePassword(input, options = {}) {
3169
3397
  return this.http.request({
3170
3398
  method: "POST",
3171
3399
  path: "/api/v1/auth/change-password",
3172
- body: input,
3400
+ // Текущий пароль уходит под двумя именами: какое из них ждёт сервер, снаружи
3401
+ // не проверить, а лишнее поле он игнорирует.
3402
+ body: { ...input, currentPassword: input.oldPassword },
3173
3403
  ...this.requestOptions(options)
3174
3404
  });
3175
3405
  }
@@ -4552,6 +4782,7 @@ var ItdClient = class {
4552
4782
  this.#queue = this.#config.rateLimit ? new RequestQueue(this.#config.rateLimit) : void 0;
4553
4783
  this.#http.setCollaborators({
4554
4784
  getAuthHeaders: () => this.#authManager.getAuthHeaders(),
4785
+ getDeviceId: () => this.#authManager.getDeviceId(),
4555
4786
  onUnauthorized: () => this.#authManager.onUnauthorized(),
4556
4787
  getCookieHeader: (url) => this.#jar.getHeader(url),
4557
4788
  saveCookies: (url, response) => this.#jar.setFromResponse(url, response),
@@ -4822,10 +5053,14 @@ function toDate(value) {
4822
5053
 
4823
5054
  exports.ALLOWED_MIME_TYPES = ALLOWED_MIME_TYPES;
4824
5055
  exports.AUDIO_MIME_TYPES = AUDIO_MIME_TYPES;
5056
+ exports.AUTH_FLAG_COOKIE = AUTH_FLAG_COOKIE;
5057
+ exports.AUTH_PATHS = AUTH_PATHS;
4825
5058
  exports.AttachmentType = AttachmentType;
4826
5059
  exports.CommentSort = CommentSort;
4827
5060
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
4828
5061
  exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
5062
+ exports.DEFAULT_USER_AGENT = DEFAULT_USER_AGENT;
5063
+ exports.DEVICE_ID_HEADER = DEVICE_ID_HEADER;
4829
5064
  exports.FeedTab = FeedTab;
4830
5065
  exports.IMAGE_MIME_TYPES = IMAGE_MIME_TYPES;
4831
5066
  exports.ItdAbortError = ItdAbortError;
@@ -4845,6 +5080,7 @@ exports.ItdRealtime = ItdRealtime;
4845
5080
  exports.ItdServerError = ItdServerError;
4846
5081
  exports.ItdTimeoutError = ItdTimeoutError;
4847
5082
  exports.ItdValidationError = ItdValidationError;
5083
+ exports.LIBRARY_VERSION = LIBRARY_VERSION;
4848
5084
  exports.LikesVisibility = LikesVisibility;
4849
5085
  exports.LocalStorageTokenStorage = LocalStorageTokenStorage;
4850
5086
  exports.MAX_RECONNECT_ATTEMPTS = MAX_RECONNECT_ATTEMPTS;
@@ -4854,10 +5090,13 @@ exports.NotificationType = NotificationType;
4854
5090
  exports.Paginator = Paginator;
4855
5091
  exports.RECONNECT_BACKOFF = RECONNECT_BACKOFF;
4856
5092
  exports.RECONNECT_JITTER = RECONNECT_JITTER;
5093
+ exports.REFRESH_COOKIE = REFRESH_COOKIE;
5094
+ exports.REFRESH_COOKIE_PATH = REFRESH_COOKIE_PATH;
4857
5095
  exports.RealtimeStatus = RealtimeStatus;
4858
5096
  exports.ReportReason = ReportReason;
4859
5097
  exports.ReportTargetType = ReportTargetType;
4860
5098
  exports.STREAM_PATH = STREAM_PATH;
5099
+ exports.TURNSTILE_SITE_KEY = TURNSTILE_SITE_KEY;
4861
5100
  exports.VIDEO_MIME_TYPES = VIDEO_MIME_TYPES;
4862
5101
  exports.WallAccess = WallAccess;
4863
5102
  exports.canonicalNotificationType = canonicalNotificationType;
@@ -4881,5 +5120,5 @@ exports.readUnreadCountEvent = readUnreadCountEvent;
4881
5120
  exports.report = report;
4882
5121
  exports.resolveNotificationUrl = resolveNotificationUrl;
4883
5122
  exports.toDate = toDate;
4884
- //# sourceMappingURL=chunk-QILCVTJI.cjs.map
4885
- //# sourceMappingURL=chunk-QILCVTJI.cjs.map
5123
+ //# sourceMappingURL=chunk-3ODOEKQO.cjs.map
5124
+ //# sourceMappingURL=chunk-3ODOEKQO.cjs.map