itd-api 0.0.8 → 0.0.9

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.
@@ -616,6 +616,20 @@ var RealtimeStatus = Object.freeze({
616
616
  Error: "error",
617
617
  Disconnected: "disconnected"
618
618
  });
619
+ var ServiceState = Object.freeze({
620
+ /** Работает штатно. */
621
+ Operational: "operational",
622
+ /** Работает с деградацией. */
623
+ Degraded: "degraded",
624
+ /** Недоступен. */
625
+ Downtime: "downtime"
626
+ });
627
+ var IncidentKind = Object.freeze({
628
+ /** Недоступен. */
629
+ Down: "down",
630
+ /** Деградация. */
631
+ Degraded: "deg"
632
+ });
619
633
  var AccessType = Object.freeze({
620
634
  /** Никто. */
621
635
  Nobody: "nobody",
@@ -1285,8 +1299,18 @@ function hasLocalStorage() {
1285
1299
 
1286
1300
  // src/core/auth.ts
1287
1301
  var AUTH_PATHS = {
1302
+ signUp: "/api/v1/auth/sign-up",
1288
1303
  signIn: "/api/v1/auth/sign-in",
1289
- refresh: "/api/v1/auth/refresh"
1304
+ verifyOtp: "/api/v1/auth/verify-otp",
1305
+ resendOtp: "/api/v1/auth/resend-otp",
1306
+ refresh: "/api/v1/auth/refresh",
1307
+ logout: "/api/v1/auth/logout",
1308
+ forgotPassword: "/api/v1/auth/forgot-password",
1309
+ resetPassword: "/api/v1/auth/reset-password",
1310
+ changePassword: "/api/v1/auth/change-password",
1311
+ sessions: "/api/v1/auth/sessions",
1312
+ /** Префикс внешнего входа: к нему дописывается имя провайдера. */
1313
+ oauthLogin: "/api/v1/auth/login"
1290
1314
  };
1291
1315
  var TURNSTILE_SITE_KEY = "0x4AAAAAACHhxczw6fJGwPBg";
1292
1316
  var DEVICE_ID_HEADER = "X-Device-Id";
@@ -1358,7 +1382,7 @@ var AuthManager = class {
1358
1382
  /** То же самое, но без чтения хранилища — для вызовов, где сессия уже загружена. */
1359
1383
  #hasRefreshSession() {
1360
1384
  if (!this.#config.useCookieJar) return true;
1361
- if (this.#jar.has(AUTH_FLAG_COOKIE)) return true;
1385
+ if (this.#jar.has(AUTH_FLAG_COOKIE, this.#config.baseUrl)) return true;
1362
1386
  return Boolean(this.#session?.refreshToken);
1363
1387
  }
1364
1388
  /** Заголовки авторизации для очередного запроса. Пустой объект, если токена нет. */
@@ -1463,12 +1487,14 @@ var AuthManager = class {
1463
1487
  async getSession() {
1464
1488
  return this.#loadSession();
1465
1489
  }
1466
- /** Заменяет сессию целиком. */
1490
+ /** Заменяет сессию и связанные с ней cookie целиком. */
1467
1491
  async setSession(session) {
1492
+ this.#jar.clear();
1468
1493
  this.#jar.deserialize(session.cookies);
1469
- this.#deviceId ??= session.deviceId;
1470
- await this.#saveSession(session);
1494
+ if (session.deviceId) this.#deviceId = session.deviceId;
1495
+ this.#session = session;
1471
1496
  this.#seedRefreshCookie();
1497
+ await this.#saveSession(session);
1472
1498
  }
1473
1499
  /**
1474
1500
  * Забывает сессию и cookie. Сетевой запрос не выполняется.
@@ -1632,7 +1658,7 @@ var AuthManager = class {
1632
1658
  }
1633
1659
  if (credentials.turnstileToken) return credentials.turnstileToken;
1634
1660
  throw new ItdConfigError(
1635
- "\u0412\u0445\u043E\u0434 \u043F\u043E email \u0438 \u043F\u0430\u0440\u043E\u043B\u044E \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u043A\u0430\u043F\u0447\u0438 Cloudflare Turnstile: \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 422. \u041F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 auth.getTurnstileToken (\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0442\u043E\u043A\u0435\u043D\u0430) \u043B\u0438\u0431\u043E \u0440\u0430\u0437\u043E\u0432\u044B\u0439 auth.turnstileToken. \u041A\u043B\u044E\u0447 \u0432\u0438\u0434\u0436\u0435\u0442\u0430 \u2014 TURNSTILE_SITE_KEY. \u0412 Node \u0442\u043E\u043A\u0435\u043D \u0443\u043C\u0435\u0435\u0442 \u0434\u043E\u0431\u044B\u0432\u0430\u0442\u044C \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442: npm i itd-api-turnstile, \u0437\u0430\u0442\u0435\u043C getTurnstileToken: createTurnstileSolver()."
1661
+ "\u0412\u0445\u043E\u0434 \u043F\u043E email \u0438 \u043F\u0430\u0440\u043E\u043B\u044E \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u043A\u0430\u043F\u0447\u0438 Cloudflare Turnstile: \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 422. \u041F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 auth.getTurnstileToken (\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0442\u043E\u043A\u0435\u043D\u0430) \u043B\u0438\u0431\u043E \u0440\u0430\u0437\u043E\u0432\u044B\u0439 auth.turnstileToken. \u041A\u043B\u044E\u0447 \u0432\u0438\u0434\u0436\u0435\u0442\u0430 \u2014 TURNSTILE_SITE_KEY. \u0412 Node \u0442\u043E\u043A\u0435\u043D \u0443\u043C\u0435\u0435\u0442 \u0434\u043E\u0431\u044B\u0432\u0430\u0442\u044C \u043E\u0442\u0434\u0435\u043B\u044C\u043D\u044B\u0439 \u043F\u0430\u043A\u0435\u0442: npm i @itd-api/turnstile, \u0437\u0430\u0442\u0435\u043C getTurnstileToken: createTurnstileSolver()."
1636
1662
  );
1637
1663
  }
1638
1664
  async #performSignIn(credentials) {
@@ -1677,6 +1703,7 @@ var MemoryTokenStorage = class {
1677
1703
  var LocalStorageTokenStorage = class {
1678
1704
  #key;
1679
1705
  #fallback = new MemoryTokenStorage();
1706
+ /** Доступен ли `localStorage` для дальнейших операций. */
1680
1707
  #available;
1681
1708
  /** @param key ключ в `localStorage`. По умолчанию `itd-api:session`. */
1682
1709
  constructor(key = "itd-api:session") {
@@ -1695,26 +1722,31 @@ var LocalStorageTokenStorage = class {
1695
1722
  }
1696
1723
  }
1697
1724
  set(session) {
1698
- if (!this.#available) {
1699
- this.#fallback.set(session);
1700
- return;
1701
- }
1702
- try {
1703
- globalThis.localStorage.setItem(this.#key, JSON.stringify(session));
1704
- } catch {
1705
- this.#fallback.set(session);
1725
+ if (this.#available) {
1726
+ try {
1727
+ globalThis.localStorage.setItem(this.#key, JSON.stringify(session));
1728
+ return;
1729
+ } catch {
1730
+ this.#degrade();
1731
+ }
1706
1732
  }
1733
+ this.#fallback.set(session);
1707
1734
  }
1708
1735
  clear() {
1709
- if (!this.#available) {
1710
- this.#fallback.clear();
1711
- return;
1712
- }
1713
- try {
1714
- globalThis.localStorage.removeItem(this.#key);
1715
- } catch {
1716
- this.#fallback.clear();
1736
+ if (this.#available) {
1737
+ try {
1738
+ globalThis.localStorage.removeItem(this.#key);
1739
+ return;
1740
+ } catch {
1741
+ this.#degrade();
1742
+ }
1717
1743
  }
1744
+ this.#fallback.clear();
1745
+ }
1746
+ /** Переводит хранилище в память без переноса прежнего значения. */
1747
+ #degrade() {
1748
+ this.#available = false;
1749
+ this.#fallback.clear();
1718
1750
  }
1719
1751
  };
1720
1752
  function createTokenStorage(handlers) {
@@ -1770,10 +1802,15 @@ function normalizeBaseUrl(baseUrl) {
1770
1802
  }
1771
1803
 
1772
1804
  // src/core/version.ts
1773
- var LIBRARY_VERSION = "0.0.8";
1805
+ var LIBRARY_VERSION = "0.0.9";
1774
1806
 
1775
1807
  // src/core/config.ts
1776
1808
  var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
1809
+ var DEFAULT_STATUS_BASE_URL = "https://xn--80a7abcbg.xn--d1ah4a.com";
1810
+ var STATUS_SERVICE = "status";
1811
+ var BUILT_IN_SERVICES = Object.freeze([
1812
+ Object.freeze({ name: STATUS_SERVICE, baseUrl: DEFAULT_STATUS_BASE_URL, auth: false })
1813
+ ]);
1777
1814
  var DEFAULT_TIMEOUT = 3e4;
1778
1815
  var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
1779
1816
  var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
@@ -1886,6 +1923,12 @@ function validateAuth(auth) {
1886
1923
  "auth \u043D\u0435 \u0440\u0430\u0441\u043F\u043E\u0437\u043D\u0430\u043D. \u041E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F \u0441\u0442\u0440\u043E\u043A\u0430 \u0441 accessToken \u043B\u0438\u0431\u043E \u043E\u0431\u044A\u0435\u043A\u0442 { accessToken }, { email, password } \u0438\u043B\u0438 { getToken }"
1887
1924
  );
1888
1925
  }
1926
+ function resolveServices(services) {
1927
+ if (!services) return [];
1928
+ return Object.entries(services).map(
1929
+ ([name, value]) => typeof value === "string" ? { name, baseUrl: value } : { ...value, name }
1930
+ );
1931
+ }
1889
1932
  function resolveConfig(options = {}) {
1890
1933
  const mode = options.mode ?? RuntimeMode.Auto;
1891
1934
  if (!Object.values(RuntimeMode).includes(mode)) {
@@ -1899,6 +1942,7 @@ function resolveConfig(options = {}) {
1899
1942
  }
1900
1943
  return {
1901
1944
  baseUrl: normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL),
1945
+ services: resolveServices(options.services),
1902
1946
  auth: validateAuth(options.auth),
1903
1947
  storage: options.storage ?? new MemoryTokenStorage(),
1904
1948
  autoRefresh: options.autoRefresh ?? true,
@@ -2005,7 +2049,7 @@ function sleep(ms) {
2005
2049
  return new Promise((resolve) => setTimeout(resolve, ms));
2006
2050
  }
2007
2051
  function createQueueMiddleware(schedule) {
2008
- return (request, next) => request.skipQueue ? next(request) : schedule(() => next(request));
2052
+ return (request, next) => request.skipQueue ? next(request) : schedule(request, () => next(request));
2009
2053
  }
2010
2054
  function createPluginsMiddleware(plugins) {
2011
2055
  return (request, next) => {
@@ -2013,6 +2057,18 @@ function createPluginsMiddleware(plugins) {
2013
2057
  return plugins.run(request, next);
2014
2058
  };
2015
2059
  }
2060
+ function createServicesMiddleware(registry) {
2061
+ return async (request, next) => {
2062
+ if (request.service === void 0) return next(request);
2063
+ const service = registry.require(request.service);
2064
+ let prepared = request.baseUrl === void 0 ? { ...request, baseUrl: service.baseUrl } : request;
2065
+ if (service.headers) prepared = withLayerHeaders(prepared, service.headers);
2066
+ if (service.auth === false && prepared.skipAuth === void 0) {
2067
+ prepared = { ...prepared, skipAuth: true };
2068
+ }
2069
+ return next(prepared);
2070
+ };
2071
+ }
2016
2072
  async function applyAuth(request, deps) {
2017
2073
  if (request.skipAuth) return request;
2018
2074
  const headers = await deps.getAuthHeaders();
@@ -2042,11 +2098,11 @@ function resolveBackoff(retry, global) {
2042
2098
  }
2043
2099
  function createRetryMiddleware(deps) {
2044
2100
  const globalScheduler = deps.retry ? createRetryScheduler(deps.retry) : void 0;
2045
- const nextDelay = (error, attempt, method, backoff) => {
2101
+ const nextDelay = (error, attempt, request, method, backoff) => {
2046
2102
  if (isItdRateLimitError(error)) {
2047
2103
  const wait = error.retryAfter ?? deps.rateLimitDelays[attempt - 1];
2048
2104
  if (wait === void 0) return void 0;
2049
- deps.pauseQueue?.(wait);
2105
+ deps.pauseQueue?.(wait, request);
2050
2106
  deps.logger?.debug(`\u043B\u0438\u043C\u0438\u0442 \u0447\u0430\u0441\u0442\u043E\u0442\u044B, \u043F\u043E\u043F\u044B\u0442\u043A\u0430 ${attempt + 1} \u0447\u0435\u0440\u0435\u0437 ${wait} \u043C\u0441`);
2051
2107
  return wait;
2052
2108
  }
@@ -2059,13 +2115,14 @@ function createRetryMiddleware(deps) {
2059
2115
  try {
2060
2116
  return await next({ ...request, attempt });
2061
2117
  } catch (error) {
2062
- const delay = nextDelay(error, attempt, method, backoff);
2118
+ const delay = nextDelay(error, attempt, request, method, backoff);
2063
2119
  if (delay === void 0) throw error;
2064
2120
  await deps.hooks.onRetry?.({
2065
2121
  method,
2066
2122
  path: request.path,
2067
2123
  url: deps.buildUrl(request),
2068
- headers: new Headers(),
2124
+ // Умолчания транспорта добавляются после слоя повторов и сюда не входят.
2125
+ headers: new Headers({ ...request.layerHeaders, ...request.headers }),
2069
2126
  attempt,
2070
2127
  error,
2071
2128
  delay
@@ -2088,6 +2145,8 @@ var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
2088
2145
  "retry",
2089
2146
  "method",
2090
2147
  "path",
2148
+ "service",
2149
+ "baseUrl",
2091
2150
  "query",
2092
2151
  "body",
2093
2152
  "skipAuth",
@@ -2255,6 +2314,107 @@ var RequestQueue = class {
2255
2314
  this.#drain();
2256
2315
  }
2257
2316
  };
2317
+ var RequestQueuePool = class {
2318
+ #options;
2319
+ #main;
2320
+ /** Очереди сервисов заводятся при первом запросе — обычно не нужна ни одна. */
2321
+ #byService = /* @__PURE__ */ new Map();
2322
+ constructor(options) {
2323
+ this.#options = options;
2324
+ this.#main = new RequestQueue(options);
2325
+ }
2326
+ /** Очередь хоста. */
2327
+ for(service) {
2328
+ if (service === void 0) return this.#main;
2329
+ let queue = this.#byService.get(service);
2330
+ if (!queue) {
2331
+ queue = new RequestQueue(this.#options);
2332
+ this.#byService.set(service, queue);
2333
+ }
2334
+ return queue;
2335
+ }
2336
+ /** Останавливает все очереди. */
2337
+ stop() {
2338
+ this.#main.stop();
2339
+ for (const queue of this.#byService.values()) queue.stop();
2340
+ }
2341
+ };
2342
+
2343
+ // src/core/services.ts
2344
+ function hostOf(url) {
2345
+ try {
2346
+ return new URL(url).hostname.toLowerCase();
2347
+ } catch {
2348
+ return "";
2349
+ }
2350
+ }
2351
+ function isSameSite(primaryHost, host) {
2352
+ if (!primaryHost || !host) return false;
2353
+ return host === primaryHost || host.endsWith(`.${primaryHost}`);
2354
+ }
2355
+ var ServiceRegistry = class {
2356
+ #services = /* @__PURE__ */ new Map();
2357
+ /** Хост основного API. */
2358
+ #primaryHost;
2359
+ /** @param primaryBaseUrl базовый URL клиента */
2360
+ constructor(primaryBaseUrl) {
2361
+ this.#primaryHost = primaryBaseUrl ? hostOf(primaryBaseUrl) : "";
2362
+ }
2363
+ /**
2364
+ * Регистрирует сервис. Имя очищается от краевых пробелов, базовый URL приводится
2365
+ * к каноничному виду, а незаданный `auth` выводится из хоста.
2366
+ *
2367
+ * @throws {ItdConfigError} если имя пустое, имя занято или `baseUrl` не абсолютный URL
2368
+ */
2369
+ define(definition) {
2370
+ const raw = definition?.name;
2371
+ if (typeof raw !== "string" || raw.trim() === "") {
2372
+ throw new ItdConfigError("\u0423 \u0441\u0435\u0440\u0432\u0438\u0441\u0430 \u0434\u043E\u043B\u0436\u043D\u043E \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0435 \u0438\u043C\u044F");
2373
+ }
2374
+ const name = raw.trim();
2375
+ if (this.#services.has(name)) {
2376
+ throw new ItdConfigError(`\u0421\u0435\u0440\u0432\u0438\u0441 \xAB${name}\xBB \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D \u0438 \u043D\u0435 \u043C\u043E\u0436\u0435\u0442 \u0431\u044B\u0442\u044C \u0437\u0430\u043C\u0435\u043D\u0451\u043D`);
2377
+ }
2378
+ const baseUrl = normalizeBaseUrl(definition.baseUrl);
2379
+ this.#services.set(name, {
2380
+ ...definition,
2381
+ name,
2382
+ baseUrl,
2383
+ auth: definition.auth ?? isSameSite(this.#primaryHost, hostOf(baseUrl))
2384
+ });
2385
+ }
2386
+ /** Определение сервиса либо `undefined`, если такого нет. */
2387
+ get(name) {
2388
+ return this.#services.get(name);
2389
+ }
2390
+ /** Зарегистрирован ли сервис с таким именем. */
2391
+ has(name) {
2392
+ return this.#services.has(name);
2393
+ }
2394
+ /**
2395
+ * Определение сервиса.
2396
+ *
2397
+ * @throws {ItdConfigError} если сервис не зарегистрирован
2398
+ */
2399
+ require(name) {
2400
+ const service = this.#services.get(name);
2401
+ if (!service) {
2402
+ const known = [...this.#services.keys()];
2403
+ throw new ItdConfigError(
2404
+ `\u0421\u0435\u0440\u0432\u0438\u0441 \xAB${name}\xBB \u043D\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043E\u0432\u0430\u043D. ` + (known.length > 0 ? `\u0418\u0437\u0432\u0435\u0441\u0442\u043D\u044B: ${known.join(", ")}` : "\u0417\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0439\u0442\u0435 \u0435\u0433\u043E \u0447\u0435\u0440\u0435\u0437 itd.defineService() \u0438\u043B\u0438 \u043E\u043F\u0446\u0438\u044E services")
2405
+ );
2406
+ }
2407
+ return service;
2408
+ }
2409
+ /**
2410
+ * Базовый URL сервиса.
2411
+ *
2412
+ * @throws {ItdConfigError} если сервис не зарегистрирован
2413
+ */
2414
+ resolveBaseUrl(name) {
2415
+ return this.require(name).baseUrl;
2416
+ }
2417
+ };
2258
2418
 
2259
2419
  // src/core/redact.ts
2260
2420
  var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
@@ -2532,6 +2692,29 @@ async function readBody(response) {
2532
2692
  const text = await response.text();
2533
2693
  return text === "" ? void 0 : text;
2534
2694
  }
2695
+ function createAbortError() {
2696
+ const error = new Error("\u041E\u043F\u0435\u0440\u0430\u0446\u0438\u044F \u043F\u0440\u0435\u0440\u0432\u0430\u043D\u0430");
2697
+ error.name = "AbortError";
2698
+ return error;
2699
+ }
2700
+ function abortable(promise, signal) {
2701
+ if (signal.aborted) {
2702
+ return Promise.race([
2703
+ promise,
2704
+ new Promise((_resolve, reject) => {
2705
+ setTimeout(() => reject(createAbortError()), 0);
2706
+ })
2707
+ ]);
2708
+ }
2709
+ let onAbort;
2710
+ const interrupted = new Promise((_resolve, reject) => {
2711
+ onAbort = () => reject(createAbortError());
2712
+ signal.addEventListener("abort", onAbort, { once: true });
2713
+ });
2714
+ return Promise.race([promise, interrupted]).finally(() => {
2715
+ if (onAbort) signal.removeEventListener("abort", onAbort);
2716
+ });
2717
+ }
2535
2718
  function createAbortBundle(userSignal, timeout) {
2536
2719
  const controller = new AbortController();
2537
2720
  let timedOut = false;
@@ -2595,70 +2778,104 @@ var Transport = class {
2595
2778
  headers: redactHeaders(headers),
2596
2779
  body: redactBody(request.body)
2597
2780
  });
2598
- let response;
2599
2781
  try {
2600
- response = await this.#config.fetch(url, {
2782
+ let response;
2783
+ try {
2784
+ response = await this.#config.fetch(url, {
2785
+ method,
2786
+ headers,
2787
+ signal: abort.signal,
2788
+ ...body !== void 0 ? { body } : {},
2789
+ ...this.#config.sendCredentials ? { credentials: "include" } : {}
2790
+ });
2791
+ } catch (error) {
2792
+ const duration2 = Date.now() - startedAt;
2793
+ const failure = this.#toTransportError(error, abort, request, method, timeout);
2794
+ await this.#config.hooks.onError?.({ ...context, duration: duration2, error: failure });
2795
+ this.#config.logger?.warn(
2796
+ `\xD7 ${method} ${request.path} (${duration2} \u043C\u0441): ${failure.message}`
2797
+ );
2798
+ throw failure;
2799
+ }
2800
+ if (this.#deps.onRateLimit) {
2801
+ const { limit, remaining } = readRateLimit(response.headers);
2802
+ this.#deps.onRateLimit(limit, remaining, request);
2803
+ }
2804
+ if (this.#config.useCookieJar) this.#deps.cookies?.setFromResponse(url, response);
2805
+ if (response.ok) {
2806
+ await this.#config.hooks.onResponse?.({
2807
+ ...context,
2808
+ status: response.status,
2809
+ duration: Date.now() - startedAt,
2810
+ response
2811
+ });
2812
+ }
2813
+ const payload = await this.#readBodyOrFail(
2814
+ response,
2815
+ context,
2816
+ request,
2601
2817
  method,
2602
- headers,
2603
- signal: abort.signal,
2604
- ...body !== void 0 ? { body } : {},
2605
- ...this.#config.sendCredentials ? { credentials: "include" } : {}
2606
- });
2607
- } catch (error) {
2608
- const duration2 = Date.now() - startedAt;
2609
- const failure = this.#toTransportError(error, abort, request, method, timeout);
2610
- await this.#config.hooks.onError?.({ ...context, duration: duration2, error: failure });
2611
- this.#config.logger?.warn(`\xD7 ${method} ${request.path} (${duration2} \u043C\u0441): ${failure.message}`);
2612
- throw failure;
2818
+ abort,
2819
+ timeout
2820
+ );
2821
+ const duration = Date.now() - startedAt;
2822
+ if (!response.ok) {
2823
+ const error = createApiError({
2824
+ method,
2825
+ path: request.path,
2826
+ status: response.status,
2827
+ statusText: response.statusText,
2828
+ headers: response.headers,
2829
+ response,
2830
+ body: payload
2831
+ });
2832
+ await this.#config.hooks.onError?.({ ...context, duration, error });
2833
+ this.#config.logger?.warn(
2834
+ `\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441): ${error.message}`
2835
+ );
2836
+ throw error;
2837
+ }
2838
+ this.#config.logger?.debug(`\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441)`);
2839
+ return request.raw ? payload : unwrapData(payload);
2613
2840
  } finally {
2614
2841
  abort.cleanup();
2615
2842
  }
2616
- const duration = Date.now() - startedAt;
2617
- if (this.#deps.onRateLimit) {
2618
- const { limit, remaining } = readRateLimit(response.headers);
2619
- this.#deps.onRateLimit(limit, remaining);
2620
- }
2621
- if (this.#config.useCookieJar) this.#deps.cookies?.setFromResponse(url, response);
2622
- const payload = await readBody(response);
2623
- if (!response.ok) {
2624
- const error = createApiError({
2625
- method,
2626
- path: request.path,
2627
- status: response.status,
2628
- statusText: response.statusText,
2629
- headers: response.headers,
2630
- response,
2631
- body: payload
2843
+ };
2844
+ /** Читает тело и преобразует ошибку чтения в транспортную ошибку библиотеки. */
2845
+ async #readBodyOrFail(response, context, request, method, abort, timeout) {
2846
+ const startedAt = Date.now();
2847
+ try {
2848
+ return await abortable(readBody(response), abort.signal);
2849
+ } catch (error) {
2850
+ await response.body?.cancel().catch(() => {
2851
+ });
2852
+ const failure = this.#toTransportError(error, abort, request, method, timeout);
2853
+ await this.#config.hooks.onError?.({
2854
+ ...context,
2855
+ duration: Date.now() - startedAt,
2856
+ error: failure
2632
2857
  });
2633
- await this.#config.hooks.onError?.({ ...context, duration, error });
2634
2858
  this.#config.logger?.warn(
2635
- `\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441): ${error.message}`
2859
+ `\xD7 ${method} ${request.path}: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u0440\u043E\u0447\u0438\u0442\u0430\u0442\u044C \u0442\u0435\u043B\u043E \u043E\u0442\u0432\u0435\u0442\u0430 \u2014 ${failure.message}`
2636
2860
  );
2637
- throw error;
2861
+ throw failure;
2638
2862
  }
2639
- await this.#config.hooks.onResponse?.({
2640
- ...context,
2641
- status: response.status,
2642
- duration,
2643
- response
2644
- });
2645
- this.#config.logger?.debug(`\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441)`);
2646
- return request.raw ? payload : unwrapData(payload);
2647
- };
2648
- /** Итоговый URL со строкой запроса. Нужен и слою повторов — для хука `onRetry`. */
2649
- buildUrl(request) {
2650
- return joinUrl(this.#config.baseUrl, request.path) + buildQuery(request.query);
2651
2863
  }
2652
2864
  /**
2653
- * Собирает заголовки запроса.
2865
+ * Итоговый URL со строкой запроса. Нужен и слою повторов — для хука `onRetry`.
2654
2866
  *
2655
- * Порядок важен: сначала умолчания библиотеки, затем заголовки клиента, затем то,
2656
- * что добавили слои конвейера (авторизация), и только в самом конце — заголовки
2657
- * конкретного вызова. Так пользователь может переопределить что угодно.
2867
+ * Хост берётся из самого запроса, если он там задан: у сервисов платформы свои домены.
2658
2868
  */
2659
- async #buildHeaders(request, url) {
2869
+ buildUrl(request) {
2870
+ const base = request.baseUrl ?? this.#config.baseUrl;
2871
+ return joinUrl(base, request.path) + buildQuery(request.query);
2872
+ }
2873
+ /**
2874
+ * Собирает общие заголовки клиента: `User-Agent`, идентификатор устройства,
2875
+ * заголовки конфигурации и cookie для указанного адреса.
2876
+ */
2877
+ async platformHeaders(url) {
2660
2878
  const headers = new Headers();
2661
- headers.set("Accept", "application/json");
2662
2879
  headers.set("X-Requested-With", "XMLHttpRequest");
2663
2880
  if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
2664
2881
  if (this.#deps.getDeviceId) {
@@ -2666,12 +2883,21 @@ var Transport = class {
2666
2883
  }
2667
2884
  for (const [name, value] of Object.entries(this.#config.headers))
2668
2885
  setHeader(headers, name, value);
2669
- for (const [name, value] of Object.entries(request.layerHeaders ?? {}))
2670
- setHeader(headers, name, value);
2671
2886
  if (this.#config.useCookieJar && this.#deps.cookies) {
2672
2887
  const cookie = this.#deps.cookies.getHeader(url);
2673
2888
  if (cookie) setHeader(headers, "Cookie", cookie);
2674
2889
  }
2890
+ return headers;
2891
+ }
2892
+ /**
2893
+ * Дополняет общие заголовки значением `Accept`, заголовками конвейера и вызова.
2894
+ * Заголовки вызова применяются последними.
2895
+ */
2896
+ async #buildHeaders(request, url) {
2897
+ const headers = await this.platformHeaders(url);
2898
+ if (!headers.has("Accept")) headers.set("Accept", "application/json");
2899
+ for (const [name, value] of Object.entries(request.layerHeaders ?? {}))
2900
+ setHeader(headers, name, value);
2675
2901
  for (const [name, value] of Object.entries(request.headers ?? {})) {
2676
2902
  setHeader(headers, name, value);
2677
2903
  }
@@ -2798,14 +3024,15 @@ var PollTransport = class {
2798
3024
  while (!context.signal.aborted) {
2799
3025
  const token = await context.getToken();
2800
3026
  if (!token) throw new UnauthorizedStreamError();
2801
- const headers = new Headers({
2802
- Accept: "application/json",
2803
- Authorization: `Bearer ${token}`
3027
+ const url = `${joinUrl(context.baseUrl, "/api/notifications/")}?limit=${this.#limit}&offset=0`;
3028
+ const headers = await context.baseHeaders(url);
3029
+ headers.set("Accept", "application/json");
3030
+ headers.set("Authorization", `Bearer ${token}`);
3031
+ const response = await context.fetch(url, {
3032
+ method: "GET",
3033
+ headers,
3034
+ signal: context.signal
2804
3035
  });
2805
- const response = await context.fetch(
2806
- `${joinUrl(context.baseUrl, "/api/notifications/")}?limit=${this.#limit}&offset=0`,
2807
- { method: "GET", headers, signal: context.signal }
2808
- );
2809
3036
  if (response.status === 401) throw new UnauthorizedStreamError();
2810
3037
  if (!response.ok) throw new Error(`\u041E\u043F\u0440\u043E\u0441 \u0443\u0432\u0435\u0434\u043E\u043C\u043B\u0435\u043D\u0438\u0439 \u0432\u0435\u0440\u043D\u0443\u043B \u0441\u0442\u0430\u0442\u0443\u0441 ${response.status}`);
2811
3038
  const body = await response.json();
@@ -3061,13 +3288,13 @@ var SseTransport = class {
3061
3288
  async connect(context) {
3062
3289
  const token = await context.getToken();
3063
3290
  if (!token) throw new UnauthorizedStreamError();
3064
- const headers = new Headers({
3065
- Accept: "text/event-stream",
3066
- Authorization: `Bearer ${token}`,
3067
- "Cache-Control": "no-cache"
3068
- });
3291
+ const url = joinUrl(context.baseUrl, STREAM_PATH);
3292
+ const headers = await context.baseHeaders(url);
3293
+ headers.set("Accept", "text/event-stream");
3294
+ headers.set("Authorization", `Bearer ${token}`);
3295
+ headers.set("Cache-Control", "no-cache");
3069
3296
  if (this.#lastEventId) headers.set("Last-Event-ID", this.#lastEventId);
3070
- const response = await context.fetch(joinUrl(context.baseUrl, STREAM_PATH), {
3297
+ const response = await context.fetch(url, {
3071
3298
  method: "GET",
3072
3299
  headers,
3073
3300
  signal: context.signal
@@ -3128,6 +3355,38 @@ var RealtimeTransportKind = Object.freeze({
3128
3355
  /** Периодический опрос REST. */
3129
3356
  Poll: "poll"
3130
3357
  });
3358
+ function validateRealtimeOptions(options) {
3359
+ const positiveInteger = (value, name) => {
3360
+ if (value === void 0) return;
3361
+ if (!Number.isInteger(value) || value < 0) {
3362
+ throw new ItdConfigError(
3363
+ `realtime.${name} \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0446\u0435\u043B\u044B\u043C \u043D\u0435\u043E\u0442\u0440\u0438\u0446\u0430\u0442\u0435\u043B\u044C\u043D\u044B\u043C \u0447\u0438\u0441\u043B\u043E\u043C, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${value}`
3364
+ );
3365
+ }
3366
+ };
3367
+ const duration = (value, name, min) => {
3368
+ if (value === void 0) return;
3369
+ if (!Number.isFinite(value) || value < min) {
3370
+ throw new ItdConfigError(
3371
+ `realtime.${name} \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0447\u0438\u0441\u043B\u043E\u043C \u043D\u0435 \u043C\u0435\u043D\u044C\u0448\u0435 ${min}, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${value}`
3372
+ );
3373
+ }
3374
+ };
3375
+ positiveInteger(options.maxAttempts, "maxAttempts");
3376
+ duration(options.pollInterval, "pollInterval", 1);
3377
+ duration(options.idleTimeout, "idleTimeout", 0);
3378
+ if (options.jitter !== void 0 && !(options.jitter >= 0 && options.jitter <= 1)) {
3379
+ throw new ItdConfigError(
3380
+ `realtime.jitter \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0432 \u0434\u0438\u0430\u043F\u0430\u0437\u043E\u043D\u0435 0\u20261, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${options.jitter}`
3381
+ );
3382
+ }
3383
+ if (options.backoff !== void 0) {
3384
+ if (!Array.isArray(options.backoff) || options.backoff.length === 0) {
3385
+ throw new ItdConfigError("realtime.backoff \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u044B\u043C \u0441\u043F\u0438\u0441\u043A\u043E\u043C \u043F\u0430\u0443\u0437");
3386
+ }
3387
+ for (const delay of options.backoff) duration(delay, "backoff", 0);
3388
+ }
3389
+ }
3131
3390
  var ItdRealtime = class {
3132
3391
  #deps;
3133
3392
  #options;
@@ -3149,6 +3408,7 @@ var ItdRealtime = class {
3149
3408
  #timer;
3150
3409
  #detachEnvironment;
3151
3410
  constructor(deps, options = {}) {
3411
+ validateRealtimeOptions(options);
3152
3412
  this.#deps = deps;
3153
3413
  this.#options = options;
3154
3414
  this.#maxAttempts = options.maxAttempts ?? MAX_RECONNECT_ATTEMPTS;
@@ -3229,6 +3489,7 @@ var ItdRealtime = class {
3229
3489
  }
3230
3490
  /** Запускает попытку подключения; повторы планирует сам. */
3231
3491
  #run() {
3492
+ if (!this.#wanted) return;
3232
3493
  this.#controller?.abort();
3233
3494
  const controller = new AbortController();
3234
3495
  this.#controller = controller;
@@ -3236,6 +3497,7 @@ var ItdRealtime = class {
3236
3497
  void this.#transport.connect({
3237
3498
  baseUrl: this.#deps.baseUrl,
3238
3499
  fetch: this.#deps.fetch,
3500
+ baseHeaders: this.#deps.baseHeaders,
3239
3501
  getToken: this.#deps.getToken,
3240
3502
  signal: controller.signal,
3241
3503
  onOpen: () => {
@@ -3286,17 +3548,17 @@ var ItdRealtime = class {
3286
3548
  async #refreshAndReconnect(error) {
3287
3549
  this.#setStatus(RealtimeStatus.Error);
3288
3550
  const refreshed = await this.#deps.refresh().catch(() => false);
3551
+ if (!this.#wanted) return;
3289
3552
  if (!refreshed) {
3290
- this.#emitter.emit("error", { error, willReconnect: false });
3291
- this.#emitter.emit("giveup", void 0);
3553
+ this.#giveUp(error);
3292
3554
  return;
3293
3555
  }
3294
3556
  this.#scheduleReconnect(error);
3295
3557
  }
3296
3558
  #scheduleReconnect(error) {
3559
+ if (!this.#wanted) return;
3297
3560
  if (this.#attempt >= this.#maxAttempts) {
3298
- this.#emitter.emit("error", { error, willReconnect: false });
3299
- this.#emitter.emit("giveup", void 0);
3561
+ this.#giveUp(error);
3300
3562
  return;
3301
3563
  }
3302
3564
  const delay = reconnectDelay(this.#attempt, this.#options);
@@ -3308,6 +3570,15 @@ var ItdRealtime = class {
3308
3570
  this.#run();
3309
3571
  }, delay);
3310
3572
  }
3573
+ /** Завершает автоматические попытки переподключения. */
3574
+ #giveUp(error) {
3575
+ this.#wanted = false;
3576
+ this.#attempt = 0;
3577
+ this.#detachEnvironment?.();
3578
+ this.#detachEnvironment = void 0;
3579
+ this.#emitter.emit("error", { error, willReconnect: false });
3580
+ this.#emitter.emit("giveup", void 0);
3581
+ }
3311
3582
  /**
3312
3583
  * Подписывается на события среды.
3313
3584
  *
@@ -3355,6 +3626,9 @@ var PaginationMode = Object.freeze({
3355
3626
  /** Следующая страница запрашивается смещением от начала списка. */
3356
3627
  Offset: "offset"
3357
3628
  });
3629
+ function mapPage(page, map) {
3630
+ return { ...page, items: page.items.map(map) };
3631
+ }
3358
3632
  function readItems(body, fields) {
3359
3633
  if (Array.isArray(body)) return body;
3360
3634
  for (const field of fields) {
@@ -3631,7 +3905,7 @@ var AuthResource = class extends BaseResource {
3631
3905
  async signUp(credentials, options = {}) {
3632
3906
  const body = await this.http.request({
3633
3907
  method: "POST",
3634
- path: "/api/v1/auth/sign-up",
3908
+ path: AUTH_PATHS.signUp,
3635
3909
  body: credentials,
3636
3910
  skipAuth: true,
3637
3911
  skipAuthRefresh: true,
@@ -3656,7 +3930,7 @@ var AuthResource = class extends BaseResource {
3656
3930
  async signIn(credentials, options = {}) {
3657
3931
  const body = await this.http.request({
3658
3932
  method: "POST",
3659
- path: "/api/v1/auth/sign-in",
3933
+ path: AUTH_PATHS.signIn,
3660
3934
  body: credentials,
3661
3935
  skipAuth: true,
3662
3936
  skipAuthRefresh: true,
@@ -3677,7 +3951,7 @@ var AuthResource = class extends BaseResource {
3677
3951
  async verifyOtp(input, options = {}) {
3678
3952
  const body = await this.http.request({
3679
3953
  method: "POST",
3680
- path: "/api/v1/auth/verify-otp",
3954
+ path: AUTH_PATHS.verifyOtp,
3681
3955
  body: input,
3682
3956
  skipAuth: true,
3683
3957
  skipAuthRefresh: true,
@@ -3694,7 +3968,7 @@ var AuthResource = class extends BaseResource {
3694
3968
  resendOtp(input, options = {}) {
3695
3969
  return this.http.request({
3696
3970
  method: "POST",
3697
- path: "/api/v1/auth/resend-otp",
3971
+ path: AUTH_PATHS.resendOtp,
3698
3972
  body: input,
3699
3973
  skipAuth: true,
3700
3974
  skipAuthRefresh: true,
@@ -3771,7 +4045,7 @@ var AuthResource = class extends BaseResource {
3771
4045
  async logout(options = {}) {
3772
4046
  await this.http.request({
3773
4047
  method: "POST",
3774
- path: "/api/v1/auth/logout",
4048
+ path: AUTH_PATHS.logout,
3775
4049
  skipAuthRefresh: true,
3776
4050
  ...this.requestOptions(options)
3777
4051
  });
@@ -3801,7 +4075,7 @@ var AuthResource = class extends BaseResource {
3801
4075
  async forgotPassword(input, options = {}) {
3802
4076
  const body = await this.http.request({
3803
4077
  method: "POST",
3804
- path: "/api/v1/auth/forgot-password",
4078
+ path: AUTH_PATHS.forgotPassword,
3805
4079
  body: input,
3806
4080
  skipAuth: true,
3807
4081
  skipAuthRefresh: true,
@@ -3822,7 +4096,7 @@ var AuthResource = class extends BaseResource {
3822
4096
  resetPassword(input, options = {}) {
3823
4097
  return this.http.request({
3824
4098
  method: "POST",
3825
- path: "/api/v1/auth/reset-password",
4099
+ path: AUTH_PATHS.resetPassword,
3826
4100
  body: input,
3827
4101
  skipAuth: true,
3828
4102
  skipAuthRefresh: true,
@@ -3860,14 +4134,17 @@ var AuthResource = class extends BaseResource {
3860
4134
  * Меняет пароль. Требует действующей сессии.
3861
4135
  *
3862
4136
  * При неверном текущем пароле сервер отвечает `ACCOUNT_CURRENT_PASSWORD_INCORRECT`.
4137
+ *
4138
+ * @example
4139
+ * ```ts
4140
+ * await itd.auth.changePassword({ currentPassword, newPassword });
4141
+ * ```
3863
4142
  */
3864
4143
  changePassword(input, options = {}) {
3865
4144
  return this.http.request({
3866
4145
  method: "POST",
3867
- path: "/api/v1/auth/change-password",
3868
- // Текущий пароль уходит под двумя именами: какое из них ждёт сервер, снаружи
3869
- // не проверить, а лишнее поле он игнорирует.
3870
- body: { ...input, currentPassword: input.oldPassword },
4146
+ path: AUTH_PATHS.changePassword,
4147
+ body: { currentPassword: input.currentPassword, newPassword: input.newPassword },
3871
4148
  ...this.requestOptions(options)
3872
4149
  });
3873
4150
  }
@@ -3883,13 +4160,13 @@ var AuthResource = class extends BaseResource {
3883
4160
  * ```
3884
4161
  */
3885
4162
  oauthUrl(provider) {
3886
- return joinUrl(this.http.baseUrl, `/api/v1/auth/login/${provider}`);
4163
+ return joinUrl(this.http.baseUrl, `${AUTH_PATHS.oauthLogin}/${provider}`);
3887
4164
  }
3888
4165
  /** Загружает список активных сессий. У текущей поле `isCurrent` равно `true`. */
3889
4166
  async sessions(options = {}) {
3890
4167
  const body = await this.http.request({
3891
4168
  method: "GET",
3892
- path: "/api/v1/auth/sessions",
4169
+ path: AUTH_PATHS.sessions,
3893
4170
  ...this.requestOptions(options)
3894
4171
  });
3895
4172
  return pickArray(body, "sessions");
@@ -3898,7 +4175,7 @@ var AuthResource = class extends BaseResource {
3898
4175
  revokeSession(sessionId, options = {}) {
3899
4176
  return this.http.request({
3900
4177
  method: "DELETE",
3901
- path: `/api/v1/auth/sessions/${encodeURIComponent(sessionId)}`,
4178
+ path: `${AUTH_PATHS.sessions}/${encodeURIComponent(sessionId)}`,
3902
4179
  ...this.requestOptions(options)
3903
4180
  });
3904
4181
  }
@@ -3906,7 +4183,7 @@ var AuthResource = class extends BaseResource {
3906
4183
  revokeOtherSessions(options = {}) {
3907
4184
  return this.http.request({
3908
4185
  method: "DELETE",
3909
- path: "/api/v1/auth/sessions",
4186
+ path: AUTH_PATHS.sessions,
3910
4187
  ...this.requestOptions(options)
3911
4188
  });
3912
4189
  }
@@ -4378,7 +4655,25 @@ var NotificationsResource = class extends BaseResource {
4378
4655
  }
4379
4656
  };
4380
4657
 
4658
+ // src/core/time.ts
4659
+ var NAIVE_STAMP = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/;
4660
+ function utcStampToIso(value) {
4661
+ const match = typeof value === "string" ? NAIVE_STAMP.exec(value) : null;
4662
+ if (!match) return value;
4663
+ const iso = `${match[1]}T${match[2]}Z`;
4664
+ return Number.isFinite(Date.parse(iso)) ? iso : value;
4665
+ }
4666
+
4381
4667
  // src/resources/platform.ts
4668
+ function normalizeStatus(body) {
4669
+ if (!isRecord(body) || !Array.isArray(body.services)) return body;
4670
+ return {
4671
+ ...body,
4672
+ services: body.services.map(
4673
+ (service) => typeof service?.last_checked === "string" ? { ...service, last_checked: utcStampToIso(service.last_checked) } : service
4674
+ )
4675
+ };
4676
+ }
4382
4677
  var PlatformResource = class extends BaseResource {
4383
4678
  /** Загружает журнал изменений. */
4384
4679
  async changelog(options = {}) {
@@ -4406,6 +4701,31 @@ var PlatformResource = class extends BaseResource {
4406
4701
  ...this.requestOptions(options)
4407
4702
  });
4408
4703
  }
4704
+ /**
4705
+ * Загружает состояние сервисов платформы за последние 90 суток.
4706
+ *
4707
+ * Идёт на хост `статус.итд.com` без авторизации. Ответ кэшируется сервером на минуту.
4708
+ * История по суткам приходит разреженной, ровный массив даёт `statusDays`.
4709
+ *
4710
+ * @example
4711
+ * ```ts
4712
+ * const status = await itd.platform.status();
4713
+ *
4714
+ * if (status.overall_status !== 'operational') {
4715
+ * const broken = status.services.filter((s) => s.current_status !== 'operational');
4716
+ * console.log('лежит:', broken.map((s) => s.name).join(', '));
4717
+ * }
4718
+ * ```
4719
+ */
4720
+ async status(options = {}) {
4721
+ const body = await this.http.request({
4722
+ method: "GET",
4723
+ service: STATUS_SERVICE,
4724
+ path: "/api/status",
4725
+ ...this.requestOptions(options)
4726
+ });
4727
+ return normalizeStatus(body);
4728
+ }
4409
4729
  };
4410
4730
 
4411
4731
  // src/resources/posts.ts
@@ -5170,13 +5490,15 @@ var VerificationResource = class extends BaseResource {
5170
5490
  };
5171
5491
 
5172
5492
  // src/client.ts
5173
- var ItdClient = class {
5493
+ var ItdClient = class _ItdClient {
5174
5494
  #config;
5175
5495
  #http;
5496
+ #transport;
5176
5497
  #authManager;
5177
5498
  #jar;
5178
- #queue;
5499
+ #queues;
5179
5500
  #plugins = new PluginRegistry();
5501
+ #services;
5180
5502
  /** Порождённые потоки уведомлений — чтобы `close()` мог закрыть их разом. */
5181
5503
  #streams = /* @__PURE__ */ new Set();
5182
5504
  /** Авторизация, сессии и пароли. */
@@ -5213,19 +5535,23 @@ var ItdClient = class {
5213
5535
  const config = resolveConfig(options);
5214
5536
  this.#config = config;
5215
5537
  this.#jar = new CookieJar();
5216
- const queue = config.rateLimit ? new RequestQueue(config.rateLimit) : void 0;
5217
- this.#queue = queue;
5538
+ this.#services = new ServiceRegistry(config.baseUrl);
5539
+ for (const service of BUILT_IN_SERVICES) this.#services.define(service);
5540
+ for (const service of config.services) this.#services.define(service);
5541
+ const queues = config.rateLimit ? new RequestQueuePool(config.rateLimit) : void 0;
5542
+ this.#queues = queues;
5218
5543
  let authManager;
5219
5544
  const transport = new Transport(config, {
5220
5545
  cookies: config.useCookieJar ? this.#jar : void 0,
5221
5546
  getDeviceId: () => authManager.getDeviceId(),
5222
- onRateLimit: queue && config.rateLimit?.respectHeaders ? (limit, remaining) => this.#throttleByHeaders(limit, remaining) : void 0
5547
+ onRateLimit: queues && config.rateLimit?.respectHeaders ? (limit, remaining, request) => this.#throttleByHeaders(limit, remaining, request) : void 0
5223
5548
  });
5549
+ this.#transport = transport;
5224
5550
  const pluginsLayer = createPluginsMiddleware(this.#plugins);
5225
5551
  const retriesLayer = createRetryMiddleware({
5226
5552
  retry: config.retry,
5227
5553
  rateLimitDelays: config.rateLimit?.retryDelays ?? [],
5228
- pauseQueue: queue ? (ms) => queue.pause(ms) : void 0,
5554
+ pauseQueue: queues ? (ms, request) => queues.for(request.service).pause(ms) : void 0,
5229
5555
  hooks: config.hooks,
5230
5556
  logger: config.logger,
5231
5557
  buildUrl: (request) => transport.buildUrl(request)
@@ -5243,8 +5569,13 @@ var ItdClient = class {
5243
5569
  authManager = new AuthManager(config, authHandler, this.#jar);
5244
5570
  this.#authManager = authManager;
5245
5571
  const middlewares = [];
5246
- if (queue) middlewares.push(createQueueMiddleware(queue.schedule.bind(queue)));
5572
+ if (queues) {
5573
+ middlewares.push(
5574
+ createQueueMiddleware((request, task) => queues.for(request.service).schedule(task))
5575
+ );
5576
+ }
5247
5577
  middlewares.push(pluginsLayer);
5578
+ middlewares.push(createServicesMiddleware(this.#services));
5248
5579
  middlewares.push(retriesLayer);
5249
5580
  middlewares.push(
5250
5581
  createAuthMiddleware({
@@ -5302,7 +5633,7 @@ var ItdClient = class {
5302
5633
  *
5303
5634
  * @example
5304
5635
  * ```ts
5305
- * import { crypt } from 'itd-api-crypto';
5636
+ * import { crypt } from '@itd-api/crypto';
5306
5637
  *
5307
5638
  * itd.use(crypt());
5308
5639
  * await itd.posts.create({ content: 'секрет' }, { encrypt: 'invis' });
@@ -5312,6 +5643,41 @@ var ItdClient = class {
5312
5643
  this.#plugins.add(plugin, { baseUrl: this.#config.baseUrl, logger: this.#config.logger });
5313
5644
  return this;
5314
5645
  }
5646
+ /**
5647
+ * Регистрирует сервис платформы — домен, отличный от основного.
5648
+ *
5649
+ * Запросы с `{ service: 'имя' }` уходят на его хост с его заголовками. То же самое умеет
5650
+ * опция `services` конструктора. Занятое имя не переопределяется — ни своё, ни встроенное:
5651
+ * разовому запросу хост задаётся полем `baseUrl`.
5652
+ *
5653
+ * Заголовок авторизации по умолчанию уходит только своим — домену клиента и его
5654
+ * поддоменам. Стороннему хосту токен нужно разрешить явно: `auth: true`.
5655
+ *
5656
+ * @throws {ItdConfigError} если определение неверно или имя уже занято
5657
+ *
5658
+ * @example Сервис платформы на поддомене — токен уходит сам
5659
+ * ```ts
5660
+ * itd.defineService({
5661
+ * name: 'pb',
5662
+ * baseUrl: 'https://pbapi.xn--d1ah4a.com',
5663
+ * headers: { Referer: 'https://pixel.xn--d1ah4a.com/' },
5664
+ * });
5665
+ *
5666
+ * await itd.request({ method: 'GET', service: 'pb', path: '/api/pixel-info' });
5667
+ * ```
5668
+ */
5669
+ defineService(definition) {
5670
+ this.#services.define(definition);
5671
+ return this;
5672
+ }
5673
+ /**
5674
+ * Базовый URL зарегистрированного сервиса.
5675
+ *
5676
+ * @throws {ItdConfigError} если сервис не зарегистрирован
5677
+ */
5678
+ serviceBaseUrl(name) {
5679
+ return this.#services.resolveBaseUrl(name);
5680
+ }
5315
5681
  /**
5316
5682
  * Подписывается на события авторизации.
5317
5683
  *
@@ -5353,6 +5719,7 @@ var ItdClient = class {
5353
5719
  {
5354
5720
  baseUrl: this.#config.baseUrl,
5355
5721
  fetch: this.#config.fetch,
5722
+ baseHeaders: (url) => this.#transport.platformHeaders(url),
5356
5723
  getToken: () => this.#authManager.getAccessToken(),
5357
5724
  refresh: () => this.#authManager.onUnauthorized(),
5358
5725
  fetchUnreadCount: () => this.notifications.count(),
@@ -5381,12 +5748,19 @@ var ItdClient = class {
5381
5748
  async close() {
5382
5749
  for (const stream of this.#streams) stream.disconnect();
5383
5750
  this.#streams.clear();
5384
- this.#queue?.stop();
5751
+ this.#queues?.stop();
5385
5752
  }
5386
5753
  /** Позволяет использовать клиент с `await using`. */
5387
5754
  [Symbol.asyncDispose]() {
5388
5755
  return this.close();
5389
5756
  }
5757
+ static {
5758
+ if (typeof Symbol.asyncDispose !== "symbol") {
5759
+ const prototype = _ItdClient.prototype;
5760
+ prototype[/* @__PURE__ */ Symbol.for("Symbol.asyncDispose")] = prototype.undefined;
5761
+ delete prototype.undefined;
5762
+ }
5763
+ }
5390
5764
  /** Текущая сессия целиком — чтобы сохранить её самостоятельно. */
5391
5765
  getSession() {
5392
5766
  return this.#authManager.getSession();
@@ -5406,21 +5780,16 @@ var ItdClient = class {
5406
5780
  * Смысл этой паузы прежде всего в том, чтобы при работе в несколько потоков остальные
5407
5781
  * запросы не улетели в стену все разом.
5408
5782
  */
5409
- #throttleByHeaders(limit, remaining) {
5783
+ #throttleByHeaders(limit, remaining, request) {
5410
5784
  if (remaining === void 0 || remaining > 0) return;
5411
5785
  const first = this.#config.rateLimit?.retryDelays[0];
5412
5786
  if (first === void 0) return;
5413
- this.#queue?.pause(first);
5787
+ this.#queues?.for(request.service).pause(first);
5414
5788
  this.#config.logger?.debug(
5415
5789
  `\u043B\u0438\u043C\u0438\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u0430 \u0438\u0441\u0447\u0435\u0440\u043F\u0430\u043D (${remaining} \u0438\u0437 ${limit ?? "?"}), \u043E\u0447\u0435\u0440\u0435\u0434\u044C \u0436\u0434\u0451\u0442 ${first} \u043C\u0441`
5416
5790
  );
5417
5791
  }
5418
5792
  };
5419
- if (typeof Symbol.asyncDispose !== "symbol") {
5420
- const prototype = ItdClient.prototype;
5421
- prototype[/* @__PURE__ */ Symbol.for("Symbol.asyncDispose")] = prototype.undefined;
5422
- delete prototype.undefined;
5423
- }
5424
5793
  function createClient(options = {}) {
5425
5794
  return new ItdClient(options);
5426
5795
  }
@@ -5526,11 +5895,18 @@ function resolveNotificationUrl(notification) {
5526
5895
  function isMyProfile(profile) {
5527
5896
  return "subscription" in profile;
5528
5897
  }
5898
+ var STATUS_WINDOW_DAYS = 90;
5529
5899
  function toDate(value) {
5530
5900
  if (!value) return null;
5531
5901
  const date = new Date(value);
5532
5902
  return Number.isFinite(date.getTime()) ? date : null;
5533
5903
  }
5904
+ function statusDays(service) {
5905
+ return Array.from(
5906
+ { length: STATUS_WINDOW_DAYS },
5907
+ (_, index) => service.days[String(index)] ?? null
5908
+ );
5909
+ }
5534
5910
 
5535
5911
  exports.ALLOWED_MIME_TYPES = ALLOWED_MIME_TYPES;
5536
5912
  exports.AUDIO_MIME_TYPES = AUDIO_MIME_TYPES;
@@ -5538,14 +5914,18 @@ exports.AUTH_FLAG_COOKIE = AUTH_FLAG_COOKIE;
5538
5914
  exports.AUTH_PATHS = AUTH_PATHS;
5539
5915
  exports.AccessType = AccessType;
5540
5916
  exports.AttachmentType = AttachmentType;
5917
+ exports.BUILT_IN_SERVICES = BUILT_IN_SERVICES;
5541
5918
  exports.CommentSort = CommentSort;
5542
5919
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
5920
+ exports.DEFAULT_STATUS_BASE_URL = DEFAULT_STATUS_BASE_URL;
5543
5921
  exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
5922
+ exports.DEFAULT_UPLOAD_TIMEOUT = DEFAULT_UPLOAD_TIMEOUT;
5544
5923
  exports.DEFAULT_USER_AGENT = DEFAULT_USER_AGENT;
5545
5924
  exports.DEVICE_ID_HEADER = DEVICE_ID_HEADER;
5546
5925
  exports.DetectedRuntime = DetectedRuntime;
5547
5926
  exports.FeedTab = FeedTab;
5548
5927
  exports.IMAGE_MIME_TYPES = IMAGE_MIME_TYPES;
5928
+ exports.IncidentKind = IncidentKind;
5549
5929
  exports.InteractionType = InteractionType;
5550
5930
  exports.ItdAbortError = ItdAbortError;
5551
5931
  exports.ItdApiError = ItdApiError;
@@ -5580,12 +5960,16 @@ exports.RECONNECT_BACKOFF = RECONNECT_BACKOFF;
5580
5960
  exports.RECONNECT_JITTER = RECONNECT_JITTER;
5581
5961
  exports.REFRESH_COOKIE = REFRESH_COOKIE;
5582
5962
  exports.REFRESH_COOKIE_PATH = REFRESH_COOKIE_PATH;
5963
+ exports.REQUEST_OPTION_KEYS = REQUEST_OPTION_KEYS;
5583
5964
  exports.RealtimeStatus = RealtimeStatus;
5584
5965
  exports.RealtimeTransportKind = RealtimeTransportKind;
5585
5966
  exports.ReportReason = ReportReason;
5586
5967
  exports.ReportTargetType = ReportTargetType;
5587
5968
  exports.RuntimeMode = RuntimeMode;
5969
+ exports.STATUS_SERVICE = STATUS_SERVICE;
5588
5970
  exports.STREAM_PATH = STREAM_PATH;
5971
+ exports.ServiceRegistry = ServiceRegistry;
5972
+ exports.ServiceState = ServiceState;
5589
5973
  exports.SignInStatus = SignInStatus;
5590
5974
  exports.SpanType = SpanType;
5591
5975
  exports.TURNSTILE_SITE_KEY = TURNSTILE_SITE_KEY;
@@ -5612,6 +5996,7 @@ exports.isItdServerError = isItdServerError;
5612
5996
  exports.isItdValidationError = isItdValidationError;
5613
5997
  exports.isKnownNotificationType = isKnownNotificationType;
5614
5998
  exports.isMyProfile = isMyProfile;
5999
+ exports.mapPage = mapPage;
5615
6000
  exports.normalizeNotification = normalizeNotification;
5616
6001
  exports.poll = poll;
5617
6002
  exports.post = post;
@@ -5619,6 +6004,8 @@ exports.readNotificationEvent = readNotificationEvent;
5619
6004
  exports.readUnreadCountEvent = readUnreadCountEvent;
5620
6005
  exports.report = report;
5621
6006
  exports.resolveNotificationUrl = resolveNotificationUrl;
6007
+ exports.statusDays = statusDays;
5622
6008
  exports.toDate = toDate;
5623
- //# sourceMappingURL=chunk-ATCZ4T2K.cjs.map
5624
- //# sourceMappingURL=chunk-ATCZ4T2K.cjs.map
6009
+ exports.utcStampToIso = utcStampToIso;
6010
+ //# sourceMappingURL=chunk-HTF2MOM4.cjs.map
6011
+ //# sourceMappingURL=chunk-HTF2MOM4.cjs.map