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.
@@ -586,8 +586,15 @@ var ItdErrorCode = Object.freeze({
586
586
  BUSINESS_RULE_VIOLATION: "BUSINESS_RULE_VIOLATION",
587
587
  RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
588
588
  UNKNOWN_ERROR: "UNKNOWN_ERROR",
589
+ /** Сервер отвечает так на `404`, `ENTITY_NOT_FOUND` в этом случае не приходит. */
590
+ NOT_FOUND: "NOT_FOUND",
591
+ /** На практике не приходит: вместо него сервер шлёт `TURNSTILE_VERIFICATION_FAILED`. */
589
592
  CAPTCHA_FAILED: "CAPTCHA_FAILED",
593
+ /** Капча не пройдена: токен Turnstile недействителен, просрочен или уже использован. */
594
+ TURNSTILE_VERIFICATION_FAILED: "TURNSTILE_VERIFICATION_FAILED",
590
595
  OTP_INVALID: "OTP_INVALID",
596
+ /** `flowToken` неизвестен или просрочен — поток подтверждения нужно начинать заново. */
597
+ INVALID_FLOW_TOKEN: "INVALID_FLOW_TOKEN",
591
598
  ACCOUNT_DEACTIVATED: "ACCOUNT_DEACTIVATED",
592
599
  ACCOUNT_EMAIL_DOMAIN_NOT_ALLOWED: "ACCOUNT_EMAIL_DOMAIN_NOT_ALLOWED",
593
600
  ACCOUNT_INVALID_CREDENTIALS: "ACCOUNT_INVALID_CREDENTIALS",
@@ -596,6 +603,10 @@ var ItdErrorCode = Object.freeze({
596
603
  SESSION_EXPIRED: "SESSION_EXPIRED",
597
604
  SESSION_REVOKED: "SESSION_REVOKED",
598
605
  SESSION_INVALID_REFRESH_TOKEN: "SESSION_INVALID_REFRESH_TOKEN",
606
+ /** Запрос обновления пришёл без cookie `refresh_token` — продлевать нечего. */
607
+ REFRESH_TOKEN_MISSING: "REFRESH_TOKEN_MISSING",
608
+ /** Cookie `refresh_token` есть, но сессии за ней уже нет: отозвана или истекла. */
609
+ SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
599
610
  MISSING_FLOW_TOKEN: "MISSING_FLOW_TOKEN",
600
611
  PROFILE_USERNAME_TAKEN: "PROFILE_USERNAME_TAKEN",
601
612
  PROFILE_RESTRICTION_ACTIVE: "PROFILE_RESTRICTION_ACTIVE",
@@ -859,6 +870,8 @@ parseSetCookie.splitCookiesString = splitCookiesString;
859
870
 
860
871
  // src/core/cookies.ts
861
872
  var AUTH_FLAG_COOKIE = "is_auth";
873
+ var REFRESH_COOKIE = "refresh_token";
874
+ var REFRESH_COOKIE_PATH = "/api/v1/auth";
862
875
  var SERIALIZED_SEPARATOR = " ";
863
876
  function originOf(url) {
864
877
  try {
@@ -898,6 +911,15 @@ var CookieJar = class {
898
911
  const raw = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : splitCookiesString(headers.get("set-cookie") ?? "");
899
912
  if (raw.length > 0) this.setFromStrings(url, raw);
900
913
  }
914
+ /**
915
+ * Кладёт cookie напрямую, минуя `Set-Cookie`.
916
+ *
917
+ * Нужно ровно в одном случае: пользователь передал refresh-токен строкой, а сервер читает
918
+ * его только из cookie. Значение не кодируется — оно уходит в заголовок как есть.
919
+ */
920
+ set(url, name, value, path = "/") {
921
+ this.setFromStrings(url, [`${name}=${value}; Path=${path}`]);
922
+ }
901
923
  /** Сохраняет cookie из готовых строк `Set-Cookie`. */
902
924
  setFromStrings(url, setCookieStrings) {
903
925
  const origin = originOf(url);
@@ -931,6 +953,23 @@ var CookieJar = class {
931
953
  if (cookies.length === 0) return void 0;
932
954
  return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
933
955
  }
956
+ /**
957
+ * Значение действующей cookie.
958
+ *
959
+ * Нужно, чтобы забрать обновлённый refresh-токен: сервер ротирует его при каждом
960
+ * продлении сессии, и сохранять надо именно новое значение.
961
+ *
962
+ * @param url если указан, учитываются origin, путь и флаг `Secure`
963
+ */
964
+ getValue(name, url) {
965
+ if (url) return this.#matching(url).find((cookie) => cookie.name === name)?.value;
966
+ const now = Date.now();
967
+ for (const jar of this.#byOrigin.values()) {
968
+ const cookie = jar.get(name);
969
+ if (cookie && (cookie.expires === void 0 || cookie.expires > now)) return cookie.value;
970
+ }
971
+ return void 0;
972
+ }
934
973
  /**
935
974
  * Есть ли действующая cookie с таким именем.
936
975
  *
@@ -1059,11 +1098,67 @@ var Emitter = class {
1059
1098
  }
1060
1099
  };
1061
1100
 
1101
+ // src/core/runtime.ts
1102
+ function detectRuntime() {
1103
+ const nav = globalThis.navigator;
1104
+ if (nav?.product === "ReactNative") return "react-native";
1105
+ if (typeof document !== "undefined") return "browser";
1106
+ return "server";
1107
+ }
1108
+ function shouldUseCookieJar(mode) {
1109
+ if (mode === "browser") return false;
1110
+ if (mode === "server") return true;
1111
+ return detectRuntime() === "server";
1112
+ }
1113
+ function shouldSendCredentials(mode) {
1114
+ if (mode === "browser") return true;
1115
+ if (mode === "server") return false;
1116
+ return detectRuntime() === "browser";
1117
+ }
1118
+ function resolveFetch(custom) {
1119
+ if (custom) return custom;
1120
+ if (typeof globalThis.fetch === "function") {
1121
+ return globalThis.fetch.bind(globalThis);
1122
+ }
1123
+ throw new ItdConfigError(
1124
+ "\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."
1125
+ );
1126
+ }
1127
+ function supportsStreamingBody() {
1128
+ return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
1129
+ }
1130
+ function createDeviceId() {
1131
+ const webCrypto = globalThis.crypto;
1132
+ if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
1133
+ const bytes = new Uint8Array(16);
1134
+ if (typeof webCrypto?.getRandomValues === "function") webCrypto.getRandomValues(bytes);
1135
+ else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
1136
+ bytes[6] = bytes[6] & 15 | 64;
1137
+ bytes[8] = bytes[8] & 63 | 128;
1138
+ const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
1139
+ return [
1140
+ hex.slice(0, 8),
1141
+ hex.slice(8, 12),
1142
+ hex.slice(12, 16),
1143
+ hex.slice(16, 20),
1144
+ hex.slice(20, 32)
1145
+ ].join("-");
1146
+ }
1147
+ function hasLocalStorage() {
1148
+ try {
1149
+ return typeof globalThis.localStorage !== "undefined" && globalThis.localStorage !== null;
1150
+ } catch {
1151
+ return false;
1152
+ }
1153
+ }
1154
+
1062
1155
  // src/core/auth.ts
1063
1156
  var AUTH_PATHS = {
1064
1157
  signIn: "/api/v1/auth/sign-in",
1065
1158
  refresh: "/api/v1/auth/refresh"
1066
1159
  };
1160
+ var TURNSTILE_SITE_KEY = "0x4AAAAAACHhxczw6fJGwPBg";
1161
+ var DEVICE_ID_HEADER = "X-Device-Id";
1067
1162
  function readAccessToken(payload) {
1068
1163
  if (typeof payload !== "object" || payload === null) return void 0;
1069
1164
  const token = payload.accessToken;
@@ -1080,6 +1175,13 @@ var AuthManager = class {
1080
1175
  #refreshing = null;
1081
1176
  /** Общий промис входа по логину и паролю. */
1082
1177
  #signingIn = null;
1178
+ /**
1179
+ * Идентификатор устройства.
1180
+ *
1181
+ * Держится отдельно от сессии намеренно: выход из аккаунта не меняет устройство,
1182
+ * поэтому `clear()` его не трогает.
1183
+ */
1184
+ #deviceId;
1083
1185
  constructor(config, http, jar) {
1084
1186
  this.#config = config;
1085
1187
  this.#http = http;
@@ -1110,6 +1212,23 @@ var AuthManager = class {
1110
1212
  const token = await this.getAccessToken();
1111
1213
  return token ? { Authorization: `Bearer ${token}` } : {};
1112
1214
  }
1215
+ /**
1216
+ * Идентификатор устройства для заголовка `X-Device-Id`.
1217
+ *
1218
+ * Заводится один раз и сохраняется в сессии, чтобы пережить перезапуск процесса:
1219
+ * сервер связывает с ним запись в списке сессий, и плавающее значение плодило бы
1220
+ * по новой сессии на каждый старт.
1221
+ */
1222
+ async getDeviceId() {
1223
+ if (this.#deviceId) return this.#deviceId;
1224
+ const session = await this.#loadSession();
1225
+ const deviceId = this.#config.deviceId ?? session?.deviceId ?? createDeviceId();
1226
+ this.#deviceId = deviceId;
1227
+ if (session?.deviceId !== deviceId) {
1228
+ await this.#saveSession({ ...session ?? {}, deviceId });
1229
+ }
1230
+ return deviceId;
1231
+ }
1113
1232
  /**
1114
1233
  * Текущий токен доступа.
1115
1234
  *
@@ -1125,7 +1244,7 @@ var AuthManager = class {
1125
1244
  return await auth.getToken() ?? null;
1126
1245
  }
1127
1246
  if (typeof auth === "object" && "email" in auth) {
1128
- return this.#signInWithCredentials(auth.email, auth.password);
1247
+ return this.#signInWithCredentials(auth);
1129
1248
  }
1130
1249
  return null;
1131
1250
  }
@@ -1145,12 +1264,19 @@ var AuthManager = class {
1145
1264
  return false;
1146
1265
  }
1147
1266
  }
1148
- /** Ошибка «сессию продлить нечем» — одна и та же для `refresh()` и для реакции на 401. */
1267
+ /**
1268
+ * Ошибка «сессию продлить нечем».
1269
+ *
1270
+ * Возникает, только когда обновление даже не начиналось: нет ни cookie `is_auth`,
1271
+ * ни refresh-токена. Если сервер ответил отказом, наружу уходит **его** ошибка —
1272
+ * подменять её этой значило бы прятать причину (`REFRESH_TOKEN_MISSING`,
1273
+ * `SESSION_NOT_FOUND`, `SESSION_REVOKED` — разные поводы и разные действия).
1274
+ */
1149
1275
  #noRefreshSessionError() {
1150
1276
  return new ItdAuthError({
1151
1277
  status: 401,
1152
1278
  code: "SESSION_EXPIRED",
1153
- 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",
1279
+ 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.",
1154
1280
  method: "POST",
1155
1281
  path: AUTH_PATHS.refresh,
1156
1282
  raw: void 0
@@ -1180,13 +1306,21 @@ var AuthManager = class {
1180
1306
  /** Заменяет сессию целиком. */
1181
1307
  async setSession(session) {
1182
1308
  this.#jar.deserialize(session.cookies);
1309
+ this.#deviceId ??= session.deviceId;
1183
1310
  await this.#saveSession(session);
1311
+ this.#seedRefreshCookie();
1184
1312
  }
1185
- /** Забывает сессию и cookie. Сетевой запрос не выполняется. */
1313
+ /**
1314
+ * Забывает сессию и cookie. Сетевой запрос не выполняется.
1315
+ *
1316
+ * Идентификатор устройства выход переживает: иначе каждая пара «выход — вход» плодила бы
1317
+ * новую запись в списке сессий.
1318
+ */
1186
1319
  async clear() {
1187
1320
  this.#session = null;
1188
1321
  this.#jar.clear();
1189
1322
  await this.#config.storage.clear();
1323
+ if (this.#deviceId) await this.#saveSession({ deviceId: this.#deviceId });
1190
1324
  this.#emitter.emit("signOut", void 0);
1191
1325
  }
1192
1326
  async #loadSession() {
@@ -1199,8 +1333,23 @@ var AuthManager = class {
1199
1333
  accessToken: stored.accessToken ?? fromConfig.accessToken,
1200
1334
  refreshToken: stored.refreshToken ?? fromConfig.refreshToken
1201
1335
  } : stored ?? fromConfig;
1336
+ this.#seedRefreshCookie();
1202
1337
  return this.#session;
1203
1338
  }
1339
+ /**
1340
+ * Кладёт refresh-токен в jar как cookie `refresh_token`.
1341
+ *
1342
+ * `POST /api/v1/auth/refresh` читает токен только из cookie, поэтому переданный строкой
1343
+ * приходится превращать в неё. В браузере это невозможно — cookie помечена `HttpOnly`,
1344
+ * и там обновление работает только на той, что поставил сам сервер.
1345
+ */
1346
+ #seedRefreshCookie() {
1347
+ if (!this.#config.useCookieJar) return;
1348
+ const refreshToken = this.#session?.refreshToken;
1349
+ if (!refreshToken) return;
1350
+ if (this.#jar.has(REFRESH_COOKIE)) return;
1351
+ this.#jar.set(this.#config.baseUrl, REFRESH_COOKIE, refreshToken, REFRESH_COOKIE_PATH);
1352
+ }
1204
1353
  #sessionFromConfig(auth) {
1205
1354
  if (!auth) return null;
1206
1355
  if (typeof auth === "string") return { accessToken: auth, obtainedAt: Date.now() };
@@ -1215,7 +1364,12 @@ var AuthManager = class {
1215
1364
  }
1216
1365
  async #saveSession(session) {
1217
1366
  const cookies = this.#config.useCookieJar ? this.#jar.serialize() : void 0;
1218
- const next = { ...session, ...cookies?.length ? { cookies } : {} };
1367
+ const deviceId = session.deviceId ?? this.#deviceId;
1368
+ const next = {
1369
+ ...session,
1370
+ ...cookies?.length ? { cookies } : {},
1371
+ ...deviceId ? { deviceId } : {}
1372
+ };
1219
1373
  this.#session = next;
1220
1374
  await this.#config.storage.set(next);
1221
1375
  }
@@ -1241,17 +1395,22 @@ var AuthManager = class {
1241
1395
  const payload = await this.#http.request({
1242
1396
  method: "POST",
1243
1397
  path: AUTH_PATHS.refresh,
1244
- // Обновление опирается на cookie, а не на устаревший Bearer.
1398
+ // Тела нет намеренно: сервер читает refresh-токен только из cookie — см.
1399
+ // #seedRefreshCookie. По той же причине не нужен и устаревший Bearer.
1245
1400
  skipAuth: true,
1246
1401
  // Без этого 401 на самом обновлении вызвал бы новое обновление — и так по кругу.
1247
- skipAuthRefresh: true,
1248
- ...this.#session?.refreshToken ? { body: { refreshToken: this.#session.refreshToken } } : {}
1402
+ skipAuthRefresh: true
1249
1403
  });
1250
1404
  const accessToken = readAccessToken(payload);
1251
1405
  if (!accessToken) return this.#reloginOrNull();
1406
+ const rotated = this.#jar.getValue(
1407
+ REFRESH_COOKIE,
1408
+ this.#config.baseUrl + REFRESH_COOKIE_PATH
1409
+ );
1252
1410
  await this.#saveSession({
1253
1411
  ...this.#session ?? {},
1254
1412
  accessToken,
1413
+ ...rotated ? { refreshToken: rotated } : {},
1255
1414
  obtainedAt: Date.now()
1256
1415
  });
1257
1416
  this.#emitter.emit("tokens", { accessToken });
@@ -1259,8 +1418,11 @@ var AuthManager = class {
1259
1418
  } catch (error) {
1260
1419
  if (error instanceof ItdApiError) {
1261
1420
  this.#session = null;
1421
+ this.#jar.clear();
1262
1422
  await this.#config.storage.clear();
1263
- return this.#reloginOrNull();
1423
+ const relogged = await this.#reloginOrNull();
1424
+ if (relogged !== null) return relogged;
1425
+ throw error;
1264
1426
  }
1265
1427
  throw error;
1266
1428
  }
@@ -1272,7 +1434,7 @@ var AuthManager = class {
1272
1434
  return null;
1273
1435
  }
1274
1436
  try {
1275
- return await this.#signInWithCredentials(auth.email, auth.password);
1437
+ return await this.#signInWithCredentials(auth);
1276
1438
  } catch {
1277
1439
  return null;
1278
1440
  }
@@ -1283,19 +1445,36 @@ var AuthManager = class {
1283
1445
  * Параллельные вызовы объединяются: одновременный старт нескольких запросов не должен
1284
1446
  * приводить к нескольким попыткам входа и блокировке аккаунта.
1285
1447
  */
1286
- #signInWithCredentials(email, password) {
1448
+ #signInWithCredentials(credentials) {
1287
1449
  if (this.#signingIn) return this.#signingIn;
1288
- const promise = this.#performSignIn(email, password).finally(() => {
1450
+ const promise = this.#performSignIn(credentials).finally(() => {
1289
1451
  this.#signingIn = null;
1290
1452
  });
1291
1453
  this.#signingIn = promise;
1292
1454
  return promise;
1293
1455
  }
1294
- async #performSignIn(email, password) {
1456
+ /**
1457
+ * Берёт токен капчи для входа.
1458
+ *
1459
+ * `getTurnstileToken` приоритетнее готовой строки: токен Turnstile одноразовый и живёт
1460
+ * несколько минут, поэтому при повторном входе через сутки годится только свежий.
1461
+ */
1462
+ async #resolveTurnstileToken(credentials) {
1463
+ if (credentials.getTurnstileToken) {
1464
+ const token = await credentials.getTurnstileToken();
1465
+ if (token) return token;
1466
+ }
1467
+ if (credentials.turnstileToken) return credentials.turnstileToken;
1468
+ throw new ItdConfigError(
1469
+ "\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."
1470
+ );
1471
+ }
1472
+ async #performSignIn(credentials) {
1473
+ const turnstileToken = await this.#resolveTurnstileToken(credentials);
1295
1474
  const payload = await this.#http.request({
1296
1475
  method: "POST",
1297
1476
  path: AUTH_PATHS.signIn,
1298
- body: { email, password },
1477
+ body: { email: credentials.email, password: credentials.password, turnstileToken },
1299
1478
  skipAuth: true,
1300
1479
  skipAuthRefresh: true
1301
1480
  });
@@ -1312,43 +1491,6 @@ var AuthManager = class {
1312
1491
  }
1313
1492
  };
1314
1493
 
1315
- // src/core/runtime.ts
1316
- function detectRuntime() {
1317
- const nav = globalThis.navigator;
1318
- if (nav?.product === "ReactNative") return "react-native";
1319
- if (typeof document !== "undefined") return "browser";
1320
- return "server";
1321
- }
1322
- function shouldUseCookieJar(mode) {
1323
- if (mode === "browser") return false;
1324
- if (mode === "server") return true;
1325
- return detectRuntime() === "server";
1326
- }
1327
- function shouldSendCredentials(mode) {
1328
- if (mode === "browser") return true;
1329
- if (mode === "server") return false;
1330
- return detectRuntime() === "browser";
1331
- }
1332
- function resolveFetch(custom) {
1333
- if (custom) return custom;
1334
- if (typeof globalThis.fetch === "function") {
1335
- return globalThis.fetch.bind(globalThis);
1336
- }
1337
- throw new ItdConfigError(
1338
- "\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."
1339
- );
1340
- }
1341
- function supportsStreamingBody() {
1342
- return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
1343
- }
1344
- function hasLocalStorage() {
1345
- try {
1346
- return typeof globalThis.localStorage !== "undefined" && globalThis.localStorage !== null;
1347
- } catch {
1348
- return false;
1349
- }
1350
- }
1351
-
1352
1494
  // src/core/storage.ts
1353
1495
  var MemoryTokenStorage = class {
1354
1496
  #session = null;
@@ -1463,6 +1605,8 @@ function normalizeBaseUrl(baseUrl) {
1463
1605
  // src/core/config.ts
1464
1606
  var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
1465
1607
  var DEFAULT_TIMEOUT = 3e4;
1608
+ var LIBRARY_VERSION = "0.0.2";
1609
+ var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
1466
1610
  var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
1467
1611
  function requirePositive(value, name) {
1468
1612
  if (!Number.isFinite(value) || value < 0) {
@@ -1554,13 +1698,19 @@ function validateAuth(auth) {
1554
1698
  return auth;
1555
1699
  }
1556
1700
  if ("email" in auth || "password" in auth) {
1557
- const { email, password } = auth;
1701
+ const { email, password, turnstileToken, getTurnstileToken } = auth;
1558
1702
  if (typeof email !== "string" || email.trim() === "") {
1559
1703
  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");
1560
1704
  }
1561
1705
  if (typeof password !== "string" || password === "") {
1562
1706
  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");
1563
1707
  }
1708
+ if (getTurnstileToken !== void 0 && typeof getTurnstileToken !== "function") {
1709
+ throw new ItdConfigError("auth.getTurnstileToken \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0444\u0443\u043D\u043A\u0446\u0438\u0435\u0439");
1710
+ }
1711
+ if (turnstileToken !== void 0 && (typeof turnstileToken !== "string" || turnstileToken.trim() === "")) {
1712
+ 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");
1713
+ }
1564
1714
  return auth;
1565
1715
  }
1566
1716
  throw new ItdConfigError(
@@ -1573,6 +1723,9 @@ function resolveConfig(options = {}) {
1573
1723
  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}`);
1574
1724
  }
1575
1725
  const timeout = requirePositive(options.timeout ?? DEFAULT_TIMEOUT, "timeout");
1726
+ if (options.deviceId !== void 0 && (typeof options.deviceId !== "string" || options.deviceId.trim() === "")) {
1727
+ 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");
1728
+ }
1576
1729
  return {
1577
1730
  baseUrl: normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL),
1578
1731
  auth: validateAuth(options.auth),
@@ -1586,12 +1739,61 @@ function resolveConfig(options = {}) {
1586
1739
  hooks: options.hooks ?? {},
1587
1740
  logger: options.logger === true ? consoleLogger() : options.logger || void 0,
1588
1741
  headers: { ...options.headers },
1742
+ deviceId: options.deviceId,
1743
+ // `false` — способ не слать заголовок вовсе; строка заменяет умолчание.
1744
+ userAgent: options.userAgent === false ? void 0 : options.userAgent ?? DEFAULT_USER_AGENT,
1589
1745
  mode,
1590
1746
  useCookieJar: shouldUseCookieJar(mode),
1591
1747
  sendCredentials: shouldSendCredentials(mode)
1592
1748
  };
1593
1749
  }
1594
1750
 
1751
+ // src/core/redact.ts
1752
+ var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
1753
+ var SECRET_FIELDS = /* @__PURE__ */ new Set([
1754
+ "password",
1755
+ "oldpassword",
1756
+ "newpassword",
1757
+ "accesstoken",
1758
+ "refreshtoken",
1759
+ "currentpassword",
1760
+ "flowtoken",
1761
+ "token",
1762
+ "turnstiletoken",
1763
+ "otp"
1764
+ ]);
1765
+ function maskSecret(value) {
1766
+ if (value.length <= 8) return "\u2026";
1767
+ return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
1768
+ }
1769
+ function redactHeaders(headers) {
1770
+ const result = {};
1771
+ headers.forEach((value, name) => {
1772
+ if (SECRET_HEADERS.has(name.toLowerCase())) {
1773
+ const spaceAt = value.indexOf(" ");
1774
+ result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
1775
+ return;
1776
+ }
1777
+ result[name] = value;
1778
+ });
1779
+ return result;
1780
+ }
1781
+ function redactBody(body) {
1782
+ if (body === null || body === void 0) return body;
1783
+ if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
1784
+ if (typeof Blob !== "undefined" && body instanceof Blob) return "[Blob]";
1785
+ if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
1786
+ if (Array.isArray(body)) return body.map(redactBody);
1787
+ if (typeof body === "object") {
1788
+ const result = {};
1789
+ for (const [key, value] of Object.entries(body)) {
1790
+ result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
1791
+ }
1792
+ return result;
1793
+ }
1794
+ return body;
1795
+ }
1796
+
1595
1797
  // src/core/error-factory.ts
1596
1798
  function isRecord(value) {
1597
1799
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -1707,6 +1909,10 @@ var CODE_TO_CLASS = {
1707
1909
  SESSION_EXPIRED: ItdAuthError,
1708
1910
  SESSION_REVOKED: ItdAuthError,
1709
1911
  SESSION_INVALID_REFRESH_TOKEN: ItdAuthError,
1912
+ // Оба приходят с `/auth/refresh`: первый — когда cookie refresh_token не долетела,
1913
+ // второй — когда она есть, но сессия за ней уже мертва.
1914
+ REFRESH_TOKEN_MISSING: ItdAuthError,
1915
+ SESSION_NOT_FOUND: ItdAuthError,
1710
1916
  ACCOUNT_INVALID_CREDENTIALS: ItdAuthError,
1711
1917
  ACCESS_DENIED: ItdForbiddenError,
1712
1918
  ENTITY_NOT_FOUND: ItdNotFoundError,
@@ -1724,6 +1930,10 @@ function classByStatus(status) {
1724
1930
  if (status >= 500) return ItdServerError;
1725
1931
  return ItdApiError;
1726
1932
  }
1933
+ function safeRawBody(body) {
1934
+ if (!isRecord(body) || body.type !== "validation" || !isRecord(body.found)) return body;
1935
+ return { ...body, found: redactBody(body.found) };
1936
+ }
1727
1937
  function createApiError(context) {
1728
1938
  const parsed = parseErrorBody(context.body, context.status, context.statusText);
1729
1939
  const rateLimit = readRateLimit(context.headers);
@@ -1739,7 +1949,7 @@ function createApiError(context) {
1739
1949
  requestId: getRequestId(context.headers),
1740
1950
  method: context.method,
1741
1951
  path: context.path,
1742
- raw: context.body,
1952
+ raw: safeRawBody(context.body),
1743
1953
  response: context.response,
1744
1954
  retryAfter: parseRetryAfter(context.headers?.get("retry-after"))
1745
1955
  };
@@ -1750,50 +1960,6 @@ function createApiError(context) {
1750
1960
  return new Ctor(init);
1751
1961
  }
1752
1962
 
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
- "flowtoken",
1762
- "token",
1763
- "otp"
1764
- ]);
1765
- function maskSecret(value) {
1766
- if (value.length <= 8) return "\u2026";
1767
- return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
1768
- }
1769
- function redactHeaders(headers) {
1770
- const result = {};
1771
- headers.forEach((value, name) => {
1772
- if (SECRET_HEADERS.has(name.toLowerCase())) {
1773
- const spaceAt = value.indexOf(" ");
1774
- result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
1775
- return;
1776
- }
1777
- result[name] = value;
1778
- });
1779
- return result;
1780
- }
1781
- function redactBody(body) {
1782
- if (body === null || body === void 0) return body;
1783
- if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
1784
- if (typeof Blob !== "undefined" && body instanceof Blob) return "[Blob]";
1785
- if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
1786
- if (Array.isArray(body)) return body.map(redactBody);
1787
- if (typeof body === "object") {
1788
- const result = {};
1789
- for (const [key, value] of Object.entries(body)) {
1790
- result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
1791
- }
1792
- return result;
1793
- }
1794
- return body;
1795
- }
1796
-
1797
1963
  // src/core/unwrap.ts
1798
1964
  function unwrapData(body) {
1799
1965
  if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
@@ -1945,6 +2111,11 @@ var HttpClient = class {
1945
2111
  async #buildHeaders(options, url) {
1946
2112
  const headers = new Headers();
1947
2113
  headers.set("Accept", "application/json");
2114
+ headers.set("X-Requested-With", "XMLHttpRequest");
2115
+ if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
2116
+ if (this.#collaborators.getDeviceId) {
2117
+ setHeader(headers, "X-Device-Id", await this.#collaborators.getDeviceId());
2118
+ }
1948
2119
  for (const [name, value] of Object.entries(this.#config.headers))
1949
2120
  setHeader(headers, name, value);
1950
2121
  if (!options.skipAuth && this.#collaborators.getAuthHeaders) {
@@ -3018,6 +3189,8 @@ var AuthResource = class extends BaseResource {
3018
3189
  * тогда продолжайте через {@link verifyOtp} либо воспользуйтесь {@link signInWithOtp}.
3019
3190
  *
3020
3191
  * При успешном входе токен сохраняется в клиенте автоматически.
3192
+ *
3193
+ * @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
3021
3194
  */
3022
3195
  async signIn(credentials, options = {}) {
3023
3196
  const body = await this.http.request({
@@ -3095,7 +3268,15 @@ var AuthResource = class extends BaseResource {
3095
3268
  );
3096
3269
  }
3097
3270
  const otp = await getOtp();
3098
- return this.verifyOtp({ ...credentials, otp, flowToken: result.flowToken }, options);
3271
+ return this.verifyOtp(
3272
+ {
3273
+ email: credentials.email,
3274
+ password: credentials.password,
3275
+ otp,
3276
+ flowToken: result.flowToken
3277
+ },
3278
+ options
3279
+ );
3099
3280
  }
3100
3281
  /**
3101
3282
  * Обновляет токен доступа.
@@ -3126,32 +3307,48 @@ var AuthResource = class extends BaseResource {
3126
3307
  });
3127
3308
  await this.#auth.clear();
3128
3309
  }
3129
- /** Завершает все сессии пользователя и очищает локальную. */
3310
+ /**
3311
+ * Завершает все сессии пользователя и очищает локальную.
3312
+ *
3313
+ * Собран из двух запросов, потому что единого эндпоинта на сервере нет:
3314
+ * `POST /api/v1/auth/logout-all` отвечает `404`. Сначала отзываются все прочие сессии
3315
+ * (`DELETE /api/v1/auth/sessions`), затем завершается текущая — в обратном порядке
3316
+ * отзывать было бы уже нечем.
3317
+ */
3130
3318
  async logoutAll(options = {}) {
3131
- await this.http.request({
3132
- method: "POST",
3133
- path: "/api/v1/auth/logout-all",
3134
- skipAuthRefresh: true,
3135
- ...this.requestOptions(options)
3136
- });
3137
- await this.#auth.clear();
3319
+ await this.revokeOtherSessions(options);
3320
+ await this.logout(options);
3138
3321
  }
3139
3322
  /** Забывает сессию локально, не обращаясь к серверу. */
3140
3323
  signOut() {
3141
3324
  return this.#auth.clear();
3142
3325
  }
3143
- /** Запрашивает письмо для сброса пароля. */
3144
- forgotPassword(email, options = {}) {
3145
- return this.http.request({
3326
+ /**
3327
+ * Запрашивает письмо с кодом для сброса пароля.
3328
+ *
3329
+ * @returns `flowToken`, который нужно передать в {@link resetPassword}
3330
+ */
3331
+ async forgotPassword(input, options = {}) {
3332
+ const body = await this.http.request({
3146
3333
  method: "POST",
3147
3334
  path: "/api/v1/auth/forgot-password",
3148
- body: { email },
3335
+ body: input,
3149
3336
  skipAuth: true,
3150
3337
  skipAuthRefresh: true,
3151
3338
  ...this.requestOptions(options)
3152
3339
  });
3340
+ const flowToken = pickString(body, "flowToken");
3341
+ if (!flowToken) {
3342
+ 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");
3343
+ }
3344
+ return flowToken;
3153
3345
  }
3154
- /** Устанавливает новый пароль по токену из письма. */
3346
+ /**
3347
+ * Устанавливает новый пароль по коду из письма.
3348
+ *
3349
+ * Сервер ждёт все четыре поля сразу — `email`, `otp`, `flowToken` и `newPassword`;
3350
+ * при нехватке любого отвечает `422`.
3351
+ */
3155
3352
  resetPassword(input, options = {}) {
3156
3353
  return this.http.request({
3157
3354
  method: "POST",
@@ -3162,12 +3359,45 @@ var AuthResource = class extends BaseResource {
3162
3359
  ...this.requestOptions(options)
3163
3360
  });
3164
3361
  }
3165
- /** Меняет пароль. Требует действующей сессии обновления. */
3362
+ /**
3363
+ * Полный сброс пароля с кодом из письма.
3364
+ *
3365
+ * Тот же приём, что и {@link signInWithOtp}: код запрашивается функцией `getOtp`,
3366
+ * остальное библиотека делает сама.
3367
+ *
3368
+ * @example
3369
+ * ```ts
3370
+ * await itd.auth.resetPasswordWithOtp({
3371
+ * email,
3372
+ * turnstileToken,
3373
+ * newPassword,
3374
+ * getOtp: () => rl.question('Код из письма: '),
3375
+ * });
3376
+ * ```
3377
+ */
3378
+ async resetPasswordWithOtp(input, options = {}) {
3379
+ const flowToken = await this.forgotPassword(
3380
+ { email: input.email, turnstileToken: input.turnstileToken },
3381
+ options
3382
+ );
3383
+ const otp = await input.getOtp();
3384
+ await this.resetPassword(
3385
+ { email: input.email, otp, flowToken, newPassword: input.newPassword },
3386
+ options
3387
+ );
3388
+ }
3389
+ /**
3390
+ * Меняет пароль. Требует действующей сессии.
3391
+ *
3392
+ * При неверном текущем пароле сервер отвечает `ACCOUNT_CURRENT_PASSWORD_INCORRECT`.
3393
+ */
3166
3394
  changePassword(input, options = {}) {
3167
3395
  return this.http.request({
3168
3396
  method: "POST",
3169
3397
  path: "/api/v1/auth/change-password",
3170
- body: input,
3398
+ // Текущий пароль уходит под двумя именами: какое из них ждёт сервер, снаружи
3399
+ // не проверить, а лишнее поле он игнорирует.
3400
+ body: { ...input, currentPassword: input.oldPassword },
3171
3401
  ...this.requestOptions(options)
3172
3402
  });
3173
3403
  }
@@ -4550,6 +4780,7 @@ var ItdClient = class {
4550
4780
  this.#queue = this.#config.rateLimit ? new RequestQueue(this.#config.rateLimit) : void 0;
4551
4781
  this.#http.setCollaborators({
4552
4782
  getAuthHeaders: () => this.#authManager.getAuthHeaders(),
4783
+ getDeviceId: () => this.#authManager.getDeviceId(),
4553
4784
  onUnauthorized: () => this.#authManager.onUnauthorized(),
4554
4785
  getCookieHeader: (url) => this.#jar.getHeader(url),
4555
4786
  saveCookies: (url, response) => this.#jar.setFromResponse(url, response),
@@ -4818,6 +5049,6 @@ function toDate(value) {
4818
5049
  return Number.isFinite(date.getTime()) ? date : null;
4819
5050
  }
4820
5051
 
4821
- export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AttachmentType, CommentSort, DEFAULT_BASE_URL, DEFAULT_TIMEOUT, FeedTab, IMAGE_MIME_TYPES, ItdAbortError, ItdApiError, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LikesVisibility, LocalStorageTokenStorage, MAX_RECONNECT_ATTEMPTS, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, RealtimeStatus, ReportReason, ReportTargetType, STREAM_PATH, VIDEO_MIME_TYPES, WallAccess, canonicalNotificationType, comment, createClient, createTokenStorage, formatNotificationText, isBuilder, isItdApiError, isItdAuthError, isItdError, isItdRateLimitError, isItdValidationError, isKnownNotificationType, isMyProfile, normalizeNotification, poll, post, readNotificationEvent, readUnreadCountEvent, report, resolveNotificationUrl, toDate };
4822
- //# sourceMappingURL=chunk-76C65H5J.js.map
4823
- //# sourceMappingURL=chunk-76C65H5J.js.map
5052
+ export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AttachmentType, CommentSort, DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, FeedTab, IMAGE_MIME_TYPES, ItdAbortError, ItdApiError, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, LocalStorageTokenStorage, MAX_RECONNECT_ATTEMPTS, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, RealtimeStatus, ReportReason, ReportTargetType, STREAM_PATH, TURNSTILE_SITE_KEY, VIDEO_MIME_TYPES, WallAccess, canonicalNotificationType, comment, createClient, createTokenStorage, formatNotificationText, isBuilder, isItdApiError, isItdAuthError, isItdError, isItdRateLimitError, isItdValidationError, isKnownNotificationType, isMyProfile, normalizeNotification, poll, post, readNotificationEvent, readUnreadCountEvent, report, resolveNotificationUrl, toDate };
5053
+ //# sourceMappingURL=chunk-JIT55WDH.js.map
5054
+ //# sourceMappingURL=chunk-JIT55WDH.js.map