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.
@@ -614,6 +614,20 @@ var RealtimeStatus = Object.freeze({
614
614
  Error: "error",
615
615
  Disconnected: "disconnected"
616
616
  });
617
+ var ServiceState = Object.freeze({
618
+ /** Работает штатно. */
619
+ Operational: "operational",
620
+ /** Работает с деградацией. */
621
+ Degraded: "degraded",
622
+ /** Недоступен. */
623
+ Downtime: "downtime"
624
+ });
625
+ var IncidentKind = Object.freeze({
626
+ /** Недоступен. */
627
+ Down: "down",
628
+ /** Деградация. */
629
+ Degraded: "deg"
630
+ });
617
631
  var AccessType = Object.freeze({
618
632
  /** Никто. */
619
633
  Nobody: "nobody",
@@ -1283,8 +1297,18 @@ function hasLocalStorage() {
1283
1297
 
1284
1298
  // src/core/auth.ts
1285
1299
  var AUTH_PATHS = {
1300
+ signUp: "/api/v1/auth/sign-up",
1286
1301
  signIn: "/api/v1/auth/sign-in",
1287
- refresh: "/api/v1/auth/refresh"
1302
+ verifyOtp: "/api/v1/auth/verify-otp",
1303
+ resendOtp: "/api/v1/auth/resend-otp",
1304
+ refresh: "/api/v1/auth/refresh",
1305
+ logout: "/api/v1/auth/logout",
1306
+ forgotPassword: "/api/v1/auth/forgot-password",
1307
+ resetPassword: "/api/v1/auth/reset-password",
1308
+ changePassword: "/api/v1/auth/change-password",
1309
+ sessions: "/api/v1/auth/sessions",
1310
+ /** Префикс внешнего входа: к нему дописывается имя провайдера. */
1311
+ oauthLogin: "/api/v1/auth/login"
1288
1312
  };
1289
1313
  var TURNSTILE_SITE_KEY = "0x4AAAAAACHhxczw6fJGwPBg";
1290
1314
  var DEVICE_ID_HEADER = "X-Device-Id";
@@ -1356,7 +1380,7 @@ var AuthManager = class {
1356
1380
  /** То же самое, но без чтения хранилища — для вызовов, где сессия уже загружена. */
1357
1381
  #hasRefreshSession() {
1358
1382
  if (!this.#config.useCookieJar) return true;
1359
- if (this.#jar.has(AUTH_FLAG_COOKIE)) return true;
1383
+ if (this.#jar.has(AUTH_FLAG_COOKIE, this.#config.baseUrl)) return true;
1360
1384
  return Boolean(this.#session?.refreshToken);
1361
1385
  }
1362
1386
  /** Заголовки авторизации для очередного запроса. Пустой объект, если токена нет. */
@@ -1461,12 +1485,14 @@ var AuthManager = class {
1461
1485
  async getSession() {
1462
1486
  return this.#loadSession();
1463
1487
  }
1464
- /** Заменяет сессию целиком. */
1488
+ /** Заменяет сессию и связанные с ней cookie целиком. */
1465
1489
  async setSession(session) {
1490
+ this.#jar.clear();
1466
1491
  this.#jar.deserialize(session.cookies);
1467
- this.#deviceId ??= session.deviceId;
1468
- await this.#saveSession(session);
1492
+ if (session.deviceId) this.#deviceId = session.deviceId;
1493
+ this.#session = session;
1469
1494
  this.#seedRefreshCookie();
1495
+ await this.#saveSession(session);
1470
1496
  }
1471
1497
  /**
1472
1498
  * Забывает сессию и cookie. Сетевой запрос не выполняется.
@@ -1630,7 +1656,7 @@ var AuthManager = class {
1630
1656
  }
1631
1657
  if (credentials.turnstileToken) return credentials.turnstileToken;
1632
1658
  throw new ItdConfigError(
1633
- "\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()."
1659
+ "\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()."
1634
1660
  );
1635
1661
  }
1636
1662
  async #performSignIn(credentials) {
@@ -1675,6 +1701,7 @@ var MemoryTokenStorage = class {
1675
1701
  var LocalStorageTokenStorage = class {
1676
1702
  #key;
1677
1703
  #fallback = new MemoryTokenStorage();
1704
+ /** Доступен ли `localStorage` для дальнейших операций. */
1678
1705
  #available;
1679
1706
  /** @param key ключ в `localStorage`. По умолчанию `itd-api:session`. */
1680
1707
  constructor(key = "itd-api:session") {
@@ -1693,26 +1720,31 @@ var LocalStorageTokenStorage = class {
1693
1720
  }
1694
1721
  }
1695
1722
  set(session) {
1696
- if (!this.#available) {
1697
- this.#fallback.set(session);
1698
- return;
1699
- }
1700
- try {
1701
- globalThis.localStorage.setItem(this.#key, JSON.stringify(session));
1702
- } catch {
1703
- this.#fallback.set(session);
1723
+ if (this.#available) {
1724
+ try {
1725
+ globalThis.localStorage.setItem(this.#key, JSON.stringify(session));
1726
+ return;
1727
+ } catch {
1728
+ this.#degrade();
1729
+ }
1704
1730
  }
1731
+ this.#fallback.set(session);
1705
1732
  }
1706
1733
  clear() {
1707
- if (!this.#available) {
1708
- this.#fallback.clear();
1709
- return;
1710
- }
1711
- try {
1712
- globalThis.localStorage.removeItem(this.#key);
1713
- } catch {
1714
- this.#fallback.clear();
1734
+ if (this.#available) {
1735
+ try {
1736
+ globalThis.localStorage.removeItem(this.#key);
1737
+ return;
1738
+ } catch {
1739
+ this.#degrade();
1740
+ }
1715
1741
  }
1742
+ this.#fallback.clear();
1743
+ }
1744
+ /** Переводит хранилище в память без переноса прежнего значения. */
1745
+ #degrade() {
1746
+ this.#available = false;
1747
+ this.#fallback.clear();
1716
1748
  }
1717
1749
  };
1718
1750
  function createTokenStorage(handlers) {
@@ -1768,10 +1800,15 @@ function normalizeBaseUrl(baseUrl) {
1768
1800
  }
1769
1801
 
1770
1802
  // src/core/version.ts
1771
- var LIBRARY_VERSION = "0.0.8";
1803
+ var LIBRARY_VERSION = "0.0.9";
1772
1804
 
1773
1805
  // src/core/config.ts
1774
1806
  var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
1807
+ var DEFAULT_STATUS_BASE_URL = "https://xn--80a7abcbg.xn--d1ah4a.com";
1808
+ var STATUS_SERVICE = "status";
1809
+ var BUILT_IN_SERVICES = Object.freeze([
1810
+ Object.freeze({ name: STATUS_SERVICE, baseUrl: DEFAULT_STATUS_BASE_URL, auth: false })
1811
+ ]);
1775
1812
  var DEFAULT_TIMEOUT = 3e4;
1776
1813
  var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
1777
1814
  var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
@@ -1884,6 +1921,12 @@ function validateAuth(auth) {
1884
1921
  "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 }"
1885
1922
  );
1886
1923
  }
1924
+ function resolveServices(services) {
1925
+ if (!services) return [];
1926
+ return Object.entries(services).map(
1927
+ ([name, value]) => typeof value === "string" ? { name, baseUrl: value } : { ...value, name }
1928
+ );
1929
+ }
1887
1930
  function resolveConfig(options = {}) {
1888
1931
  const mode = options.mode ?? RuntimeMode.Auto;
1889
1932
  if (!Object.values(RuntimeMode).includes(mode)) {
@@ -1897,6 +1940,7 @@ function resolveConfig(options = {}) {
1897
1940
  }
1898
1941
  return {
1899
1942
  baseUrl: normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL),
1943
+ services: resolveServices(options.services),
1900
1944
  auth: validateAuth(options.auth),
1901
1945
  storage: options.storage ?? new MemoryTokenStorage(),
1902
1946
  autoRefresh: options.autoRefresh ?? true,
@@ -2003,7 +2047,7 @@ function sleep(ms) {
2003
2047
  return new Promise((resolve) => setTimeout(resolve, ms));
2004
2048
  }
2005
2049
  function createQueueMiddleware(schedule) {
2006
- return (request, next) => request.skipQueue ? next(request) : schedule(() => next(request));
2050
+ return (request, next) => request.skipQueue ? next(request) : schedule(request, () => next(request));
2007
2051
  }
2008
2052
  function createPluginsMiddleware(plugins) {
2009
2053
  return (request, next) => {
@@ -2011,6 +2055,18 @@ function createPluginsMiddleware(plugins) {
2011
2055
  return plugins.run(request, next);
2012
2056
  };
2013
2057
  }
2058
+ function createServicesMiddleware(registry) {
2059
+ return async (request, next) => {
2060
+ if (request.service === void 0) return next(request);
2061
+ const service = registry.require(request.service);
2062
+ let prepared = request.baseUrl === void 0 ? { ...request, baseUrl: service.baseUrl } : request;
2063
+ if (service.headers) prepared = withLayerHeaders(prepared, service.headers);
2064
+ if (service.auth === false && prepared.skipAuth === void 0) {
2065
+ prepared = { ...prepared, skipAuth: true };
2066
+ }
2067
+ return next(prepared);
2068
+ };
2069
+ }
2014
2070
  async function applyAuth(request, deps) {
2015
2071
  if (request.skipAuth) return request;
2016
2072
  const headers = await deps.getAuthHeaders();
@@ -2040,11 +2096,11 @@ function resolveBackoff(retry, global) {
2040
2096
  }
2041
2097
  function createRetryMiddleware(deps) {
2042
2098
  const globalScheduler = deps.retry ? createRetryScheduler(deps.retry) : void 0;
2043
- const nextDelay = (error, attempt, method, backoff) => {
2099
+ const nextDelay = (error, attempt, request, method, backoff) => {
2044
2100
  if (isItdRateLimitError(error)) {
2045
2101
  const wait = error.retryAfter ?? deps.rateLimitDelays[attempt - 1];
2046
2102
  if (wait === void 0) return void 0;
2047
- deps.pauseQueue?.(wait);
2103
+ deps.pauseQueue?.(wait, request);
2048
2104
  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`);
2049
2105
  return wait;
2050
2106
  }
@@ -2057,13 +2113,14 @@ function createRetryMiddleware(deps) {
2057
2113
  try {
2058
2114
  return await next({ ...request, attempt });
2059
2115
  } catch (error) {
2060
- const delay = nextDelay(error, attempt, method, backoff);
2116
+ const delay = nextDelay(error, attempt, request, method, backoff);
2061
2117
  if (delay === void 0) throw error;
2062
2118
  await deps.hooks.onRetry?.({
2063
2119
  method,
2064
2120
  path: request.path,
2065
2121
  url: deps.buildUrl(request),
2066
- headers: new Headers(),
2122
+ // Умолчания транспорта добавляются после слоя повторов и сюда не входят.
2123
+ headers: new Headers({ ...request.layerHeaders, ...request.headers }),
2067
2124
  attempt,
2068
2125
  error,
2069
2126
  delay
@@ -2086,6 +2143,8 @@ var RESERVED_OPTION_KEYS = /* @__PURE__ */ new Set([
2086
2143
  "retry",
2087
2144
  "method",
2088
2145
  "path",
2146
+ "service",
2147
+ "baseUrl",
2089
2148
  "query",
2090
2149
  "body",
2091
2150
  "skipAuth",
@@ -2253,6 +2312,107 @@ var RequestQueue = class {
2253
2312
  this.#drain();
2254
2313
  }
2255
2314
  };
2315
+ var RequestQueuePool = class {
2316
+ #options;
2317
+ #main;
2318
+ /** Очереди сервисов заводятся при первом запросе — обычно не нужна ни одна. */
2319
+ #byService = /* @__PURE__ */ new Map();
2320
+ constructor(options) {
2321
+ this.#options = options;
2322
+ this.#main = new RequestQueue(options);
2323
+ }
2324
+ /** Очередь хоста. */
2325
+ for(service) {
2326
+ if (service === void 0) return this.#main;
2327
+ let queue = this.#byService.get(service);
2328
+ if (!queue) {
2329
+ queue = new RequestQueue(this.#options);
2330
+ this.#byService.set(service, queue);
2331
+ }
2332
+ return queue;
2333
+ }
2334
+ /** Останавливает все очереди. */
2335
+ stop() {
2336
+ this.#main.stop();
2337
+ for (const queue of this.#byService.values()) queue.stop();
2338
+ }
2339
+ };
2340
+
2341
+ // src/core/services.ts
2342
+ function hostOf(url) {
2343
+ try {
2344
+ return new URL(url).hostname.toLowerCase();
2345
+ } catch {
2346
+ return "";
2347
+ }
2348
+ }
2349
+ function isSameSite(primaryHost, host) {
2350
+ if (!primaryHost || !host) return false;
2351
+ return host === primaryHost || host.endsWith(`.${primaryHost}`);
2352
+ }
2353
+ var ServiceRegistry = class {
2354
+ #services = /* @__PURE__ */ new Map();
2355
+ /** Хост основного API. */
2356
+ #primaryHost;
2357
+ /** @param primaryBaseUrl базовый URL клиента */
2358
+ constructor(primaryBaseUrl) {
2359
+ this.#primaryHost = primaryBaseUrl ? hostOf(primaryBaseUrl) : "";
2360
+ }
2361
+ /**
2362
+ * Регистрирует сервис. Имя очищается от краевых пробелов, базовый URL приводится
2363
+ * к каноничному виду, а незаданный `auth` выводится из хоста.
2364
+ *
2365
+ * @throws {ItdConfigError} если имя пустое, имя занято или `baseUrl` не абсолютный URL
2366
+ */
2367
+ define(definition) {
2368
+ const raw = definition?.name;
2369
+ if (typeof raw !== "string" || raw.trim() === "") {
2370
+ 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");
2371
+ }
2372
+ const name = raw.trim();
2373
+ if (this.#services.has(name)) {
2374
+ 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`);
2375
+ }
2376
+ const baseUrl = normalizeBaseUrl(definition.baseUrl);
2377
+ this.#services.set(name, {
2378
+ ...definition,
2379
+ name,
2380
+ baseUrl,
2381
+ auth: definition.auth ?? isSameSite(this.#primaryHost, hostOf(baseUrl))
2382
+ });
2383
+ }
2384
+ /** Определение сервиса либо `undefined`, если такого нет. */
2385
+ get(name) {
2386
+ return this.#services.get(name);
2387
+ }
2388
+ /** Зарегистрирован ли сервис с таким именем. */
2389
+ has(name) {
2390
+ return this.#services.has(name);
2391
+ }
2392
+ /**
2393
+ * Определение сервиса.
2394
+ *
2395
+ * @throws {ItdConfigError} если сервис не зарегистрирован
2396
+ */
2397
+ require(name) {
2398
+ const service = this.#services.get(name);
2399
+ if (!service) {
2400
+ const known = [...this.#services.keys()];
2401
+ throw new ItdConfigError(
2402
+ `\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")
2403
+ );
2404
+ }
2405
+ return service;
2406
+ }
2407
+ /**
2408
+ * Базовый URL сервиса.
2409
+ *
2410
+ * @throws {ItdConfigError} если сервис не зарегистрирован
2411
+ */
2412
+ resolveBaseUrl(name) {
2413
+ return this.require(name).baseUrl;
2414
+ }
2415
+ };
2256
2416
 
2257
2417
  // src/core/redact.ts
2258
2418
  var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
@@ -2530,6 +2690,29 @@ async function readBody(response) {
2530
2690
  const text = await response.text();
2531
2691
  return text === "" ? void 0 : text;
2532
2692
  }
2693
+ function createAbortError() {
2694
+ const error = new Error("\u041E\u043F\u0435\u0440\u0430\u0446\u0438\u044F \u043F\u0440\u0435\u0440\u0432\u0430\u043D\u0430");
2695
+ error.name = "AbortError";
2696
+ return error;
2697
+ }
2698
+ function abortable(promise, signal) {
2699
+ if (signal.aborted) {
2700
+ return Promise.race([
2701
+ promise,
2702
+ new Promise((_resolve, reject) => {
2703
+ setTimeout(() => reject(createAbortError()), 0);
2704
+ })
2705
+ ]);
2706
+ }
2707
+ let onAbort;
2708
+ const interrupted = new Promise((_resolve, reject) => {
2709
+ onAbort = () => reject(createAbortError());
2710
+ signal.addEventListener("abort", onAbort, { once: true });
2711
+ });
2712
+ return Promise.race([promise, interrupted]).finally(() => {
2713
+ if (onAbort) signal.removeEventListener("abort", onAbort);
2714
+ });
2715
+ }
2533
2716
  function createAbortBundle(userSignal, timeout) {
2534
2717
  const controller = new AbortController();
2535
2718
  let timedOut = false;
@@ -2593,70 +2776,104 @@ var Transport = class {
2593
2776
  headers: redactHeaders(headers),
2594
2777
  body: redactBody(request.body)
2595
2778
  });
2596
- let response;
2597
2779
  try {
2598
- response = await this.#config.fetch(url, {
2780
+ let response;
2781
+ try {
2782
+ response = await this.#config.fetch(url, {
2783
+ method,
2784
+ headers,
2785
+ signal: abort.signal,
2786
+ ...body !== void 0 ? { body } : {},
2787
+ ...this.#config.sendCredentials ? { credentials: "include" } : {}
2788
+ });
2789
+ } catch (error) {
2790
+ const duration2 = Date.now() - startedAt;
2791
+ const failure = this.#toTransportError(error, abort, request, method, timeout);
2792
+ await this.#config.hooks.onError?.({ ...context, duration: duration2, error: failure });
2793
+ this.#config.logger?.warn(
2794
+ `\xD7 ${method} ${request.path} (${duration2} \u043C\u0441): ${failure.message}`
2795
+ );
2796
+ throw failure;
2797
+ }
2798
+ if (this.#deps.onRateLimit) {
2799
+ const { limit, remaining } = readRateLimit(response.headers);
2800
+ this.#deps.onRateLimit(limit, remaining, request);
2801
+ }
2802
+ if (this.#config.useCookieJar) this.#deps.cookies?.setFromResponse(url, response);
2803
+ if (response.ok) {
2804
+ await this.#config.hooks.onResponse?.({
2805
+ ...context,
2806
+ status: response.status,
2807
+ duration: Date.now() - startedAt,
2808
+ response
2809
+ });
2810
+ }
2811
+ const payload = await this.#readBodyOrFail(
2812
+ response,
2813
+ context,
2814
+ request,
2599
2815
  method,
2600
- headers,
2601
- signal: abort.signal,
2602
- ...body !== void 0 ? { body } : {},
2603
- ...this.#config.sendCredentials ? { credentials: "include" } : {}
2604
- });
2605
- } catch (error) {
2606
- const duration2 = Date.now() - startedAt;
2607
- const failure = this.#toTransportError(error, abort, request, method, timeout);
2608
- await this.#config.hooks.onError?.({ ...context, duration: duration2, error: failure });
2609
- this.#config.logger?.warn(`\xD7 ${method} ${request.path} (${duration2} \u043C\u0441): ${failure.message}`);
2610
- throw failure;
2816
+ abort,
2817
+ timeout
2818
+ );
2819
+ const duration = Date.now() - startedAt;
2820
+ if (!response.ok) {
2821
+ const error = createApiError({
2822
+ method,
2823
+ path: request.path,
2824
+ status: response.status,
2825
+ statusText: response.statusText,
2826
+ headers: response.headers,
2827
+ response,
2828
+ body: payload
2829
+ });
2830
+ await this.#config.hooks.onError?.({ ...context, duration, error });
2831
+ this.#config.logger?.warn(
2832
+ `\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441): ${error.message}`
2833
+ );
2834
+ throw error;
2835
+ }
2836
+ this.#config.logger?.debug(`\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441)`);
2837
+ return request.raw ? payload : unwrapData(payload);
2611
2838
  } finally {
2612
2839
  abort.cleanup();
2613
2840
  }
2614
- const duration = Date.now() - startedAt;
2615
- if (this.#deps.onRateLimit) {
2616
- const { limit, remaining } = readRateLimit(response.headers);
2617
- this.#deps.onRateLimit(limit, remaining);
2618
- }
2619
- if (this.#config.useCookieJar) this.#deps.cookies?.setFromResponse(url, response);
2620
- const payload = await readBody(response);
2621
- if (!response.ok) {
2622
- const error = createApiError({
2623
- method,
2624
- path: request.path,
2625
- status: response.status,
2626
- statusText: response.statusText,
2627
- headers: response.headers,
2628
- response,
2629
- body: payload
2841
+ };
2842
+ /** Читает тело и преобразует ошибку чтения в транспортную ошибку библиотеки. */
2843
+ async #readBodyOrFail(response, context, request, method, abort, timeout) {
2844
+ const startedAt = Date.now();
2845
+ try {
2846
+ return await abortable(readBody(response), abort.signal);
2847
+ } catch (error) {
2848
+ await response.body?.cancel().catch(() => {
2849
+ });
2850
+ const failure = this.#toTransportError(error, abort, request, method, timeout);
2851
+ await this.#config.hooks.onError?.({
2852
+ ...context,
2853
+ duration: Date.now() - startedAt,
2854
+ error: failure
2630
2855
  });
2631
- await this.#config.hooks.onError?.({ ...context, duration, error });
2632
2856
  this.#config.logger?.warn(
2633
- `\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441): ${error.message}`
2857
+ `\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}`
2634
2858
  );
2635
- throw error;
2859
+ throw failure;
2636
2860
  }
2637
- await this.#config.hooks.onResponse?.({
2638
- ...context,
2639
- status: response.status,
2640
- duration,
2641
- response
2642
- });
2643
- this.#config.logger?.debug(`\u2190 ${response.status} ${method} ${request.path} (${duration} \u043C\u0441)`);
2644
- return request.raw ? payload : unwrapData(payload);
2645
- };
2646
- /** Итоговый URL со строкой запроса. Нужен и слою повторов — для хука `onRetry`. */
2647
- buildUrl(request) {
2648
- return joinUrl(this.#config.baseUrl, request.path) + buildQuery(request.query);
2649
2861
  }
2650
2862
  /**
2651
- * Собирает заголовки запроса.
2863
+ * Итоговый URL со строкой запроса. Нужен и слою повторов — для хука `onRetry`.
2652
2864
  *
2653
- * Порядок важен: сначала умолчания библиотеки, затем заголовки клиента, затем то,
2654
- * что добавили слои конвейера (авторизация), и только в самом конце — заголовки
2655
- * конкретного вызова. Так пользователь может переопределить что угодно.
2865
+ * Хост берётся из самого запроса, если он там задан: у сервисов платформы свои домены.
2656
2866
  */
2657
- async #buildHeaders(request, url) {
2867
+ buildUrl(request) {
2868
+ const base = request.baseUrl ?? this.#config.baseUrl;
2869
+ return joinUrl(base, request.path) + buildQuery(request.query);
2870
+ }
2871
+ /**
2872
+ * Собирает общие заголовки клиента: `User-Agent`, идентификатор устройства,
2873
+ * заголовки конфигурации и cookie для указанного адреса.
2874
+ */
2875
+ async platformHeaders(url) {
2658
2876
  const headers = new Headers();
2659
- headers.set("Accept", "application/json");
2660
2877
  headers.set("X-Requested-With", "XMLHttpRequest");
2661
2878
  if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
2662
2879
  if (this.#deps.getDeviceId) {
@@ -2664,12 +2881,21 @@ var Transport = class {
2664
2881
  }
2665
2882
  for (const [name, value] of Object.entries(this.#config.headers))
2666
2883
  setHeader(headers, name, value);
2667
- for (const [name, value] of Object.entries(request.layerHeaders ?? {}))
2668
- setHeader(headers, name, value);
2669
2884
  if (this.#config.useCookieJar && this.#deps.cookies) {
2670
2885
  const cookie = this.#deps.cookies.getHeader(url);
2671
2886
  if (cookie) setHeader(headers, "Cookie", cookie);
2672
2887
  }
2888
+ return headers;
2889
+ }
2890
+ /**
2891
+ * Дополняет общие заголовки значением `Accept`, заголовками конвейера и вызова.
2892
+ * Заголовки вызова применяются последними.
2893
+ */
2894
+ async #buildHeaders(request, url) {
2895
+ const headers = await this.platformHeaders(url);
2896
+ if (!headers.has("Accept")) headers.set("Accept", "application/json");
2897
+ for (const [name, value] of Object.entries(request.layerHeaders ?? {}))
2898
+ setHeader(headers, name, value);
2673
2899
  for (const [name, value] of Object.entries(request.headers ?? {})) {
2674
2900
  setHeader(headers, name, value);
2675
2901
  }
@@ -2796,14 +3022,15 @@ var PollTransport = class {
2796
3022
  while (!context.signal.aborted) {
2797
3023
  const token = await context.getToken();
2798
3024
  if (!token) throw new UnauthorizedStreamError();
2799
- const headers = new Headers({
2800
- Accept: "application/json",
2801
- Authorization: `Bearer ${token}`
3025
+ const url = `${joinUrl(context.baseUrl, "/api/notifications/")}?limit=${this.#limit}&offset=0`;
3026
+ const headers = await context.baseHeaders(url);
3027
+ headers.set("Accept", "application/json");
3028
+ headers.set("Authorization", `Bearer ${token}`);
3029
+ const response = await context.fetch(url, {
3030
+ method: "GET",
3031
+ headers,
3032
+ signal: context.signal
2802
3033
  });
2803
- const response = await context.fetch(
2804
- `${joinUrl(context.baseUrl, "/api/notifications/")}?limit=${this.#limit}&offset=0`,
2805
- { method: "GET", headers, signal: context.signal }
2806
- );
2807
3034
  if (response.status === 401) throw new UnauthorizedStreamError();
2808
3035
  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}`);
2809
3036
  const body = await response.json();
@@ -3059,13 +3286,13 @@ var SseTransport = class {
3059
3286
  async connect(context) {
3060
3287
  const token = await context.getToken();
3061
3288
  if (!token) throw new UnauthorizedStreamError();
3062
- const headers = new Headers({
3063
- Accept: "text/event-stream",
3064
- Authorization: `Bearer ${token}`,
3065
- "Cache-Control": "no-cache"
3066
- });
3289
+ const url = joinUrl(context.baseUrl, STREAM_PATH);
3290
+ const headers = await context.baseHeaders(url);
3291
+ headers.set("Accept", "text/event-stream");
3292
+ headers.set("Authorization", `Bearer ${token}`);
3293
+ headers.set("Cache-Control", "no-cache");
3067
3294
  if (this.#lastEventId) headers.set("Last-Event-ID", this.#lastEventId);
3068
- const response = await context.fetch(joinUrl(context.baseUrl, STREAM_PATH), {
3295
+ const response = await context.fetch(url, {
3069
3296
  method: "GET",
3070
3297
  headers,
3071
3298
  signal: context.signal
@@ -3126,6 +3353,38 @@ var RealtimeTransportKind = Object.freeze({
3126
3353
  /** Периодический опрос REST. */
3127
3354
  Poll: "poll"
3128
3355
  });
3356
+ function validateRealtimeOptions(options) {
3357
+ const positiveInteger = (value, name) => {
3358
+ if (value === void 0) return;
3359
+ if (!Number.isInteger(value) || value < 0) {
3360
+ throw new ItdConfigError(
3361
+ `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}`
3362
+ );
3363
+ }
3364
+ };
3365
+ const duration = (value, name, min) => {
3366
+ if (value === void 0) return;
3367
+ if (!Number.isFinite(value) || value < min) {
3368
+ throw new ItdConfigError(
3369
+ `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}`
3370
+ );
3371
+ }
3372
+ };
3373
+ positiveInteger(options.maxAttempts, "maxAttempts");
3374
+ duration(options.pollInterval, "pollInterval", 1);
3375
+ duration(options.idleTimeout, "idleTimeout", 0);
3376
+ if (options.jitter !== void 0 && !(options.jitter >= 0 && options.jitter <= 1)) {
3377
+ throw new ItdConfigError(
3378
+ `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}`
3379
+ );
3380
+ }
3381
+ if (options.backoff !== void 0) {
3382
+ if (!Array.isArray(options.backoff) || options.backoff.length === 0) {
3383
+ 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");
3384
+ }
3385
+ for (const delay of options.backoff) duration(delay, "backoff", 0);
3386
+ }
3387
+ }
3129
3388
  var ItdRealtime = class {
3130
3389
  #deps;
3131
3390
  #options;
@@ -3147,6 +3406,7 @@ var ItdRealtime = class {
3147
3406
  #timer;
3148
3407
  #detachEnvironment;
3149
3408
  constructor(deps, options = {}) {
3409
+ validateRealtimeOptions(options);
3150
3410
  this.#deps = deps;
3151
3411
  this.#options = options;
3152
3412
  this.#maxAttempts = options.maxAttempts ?? MAX_RECONNECT_ATTEMPTS;
@@ -3227,6 +3487,7 @@ var ItdRealtime = class {
3227
3487
  }
3228
3488
  /** Запускает попытку подключения; повторы планирует сам. */
3229
3489
  #run() {
3490
+ if (!this.#wanted) return;
3230
3491
  this.#controller?.abort();
3231
3492
  const controller = new AbortController();
3232
3493
  this.#controller = controller;
@@ -3234,6 +3495,7 @@ var ItdRealtime = class {
3234
3495
  void this.#transport.connect({
3235
3496
  baseUrl: this.#deps.baseUrl,
3236
3497
  fetch: this.#deps.fetch,
3498
+ baseHeaders: this.#deps.baseHeaders,
3237
3499
  getToken: this.#deps.getToken,
3238
3500
  signal: controller.signal,
3239
3501
  onOpen: () => {
@@ -3284,17 +3546,17 @@ var ItdRealtime = class {
3284
3546
  async #refreshAndReconnect(error) {
3285
3547
  this.#setStatus(RealtimeStatus.Error);
3286
3548
  const refreshed = await this.#deps.refresh().catch(() => false);
3549
+ if (!this.#wanted) return;
3287
3550
  if (!refreshed) {
3288
- this.#emitter.emit("error", { error, willReconnect: false });
3289
- this.#emitter.emit("giveup", void 0);
3551
+ this.#giveUp(error);
3290
3552
  return;
3291
3553
  }
3292
3554
  this.#scheduleReconnect(error);
3293
3555
  }
3294
3556
  #scheduleReconnect(error) {
3557
+ if (!this.#wanted) return;
3295
3558
  if (this.#attempt >= this.#maxAttempts) {
3296
- this.#emitter.emit("error", { error, willReconnect: false });
3297
- this.#emitter.emit("giveup", void 0);
3559
+ this.#giveUp(error);
3298
3560
  return;
3299
3561
  }
3300
3562
  const delay = reconnectDelay(this.#attempt, this.#options);
@@ -3306,6 +3568,15 @@ var ItdRealtime = class {
3306
3568
  this.#run();
3307
3569
  }, delay);
3308
3570
  }
3571
+ /** Завершает автоматические попытки переподключения. */
3572
+ #giveUp(error) {
3573
+ this.#wanted = false;
3574
+ this.#attempt = 0;
3575
+ this.#detachEnvironment?.();
3576
+ this.#detachEnvironment = void 0;
3577
+ this.#emitter.emit("error", { error, willReconnect: false });
3578
+ this.#emitter.emit("giveup", void 0);
3579
+ }
3309
3580
  /**
3310
3581
  * Подписывается на события среды.
3311
3582
  *
@@ -3353,6 +3624,9 @@ var PaginationMode = Object.freeze({
3353
3624
  /** Следующая страница запрашивается смещением от начала списка. */
3354
3625
  Offset: "offset"
3355
3626
  });
3627
+ function mapPage(page, map) {
3628
+ return { ...page, items: page.items.map(map) };
3629
+ }
3356
3630
  function readItems(body, fields) {
3357
3631
  if (Array.isArray(body)) return body;
3358
3632
  for (const field of fields) {
@@ -3629,7 +3903,7 @@ var AuthResource = class extends BaseResource {
3629
3903
  async signUp(credentials, options = {}) {
3630
3904
  const body = await this.http.request({
3631
3905
  method: "POST",
3632
- path: "/api/v1/auth/sign-up",
3906
+ path: AUTH_PATHS.signUp,
3633
3907
  body: credentials,
3634
3908
  skipAuth: true,
3635
3909
  skipAuthRefresh: true,
@@ -3654,7 +3928,7 @@ var AuthResource = class extends BaseResource {
3654
3928
  async signIn(credentials, options = {}) {
3655
3929
  const body = await this.http.request({
3656
3930
  method: "POST",
3657
- path: "/api/v1/auth/sign-in",
3931
+ path: AUTH_PATHS.signIn,
3658
3932
  body: credentials,
3659
3933
  skipAuth: true,
3660
3934
  skipAuthRefresh: true,
@@ -3675,7 +3949,7 @@ var AuthResource = class extends BaseResource {
3675
3949
  async verifyOtp(input, options = {}) {
3676
3950
  const body = await this.http.request({
3677
3951
  method: "POST",
3678
- path: "/api/v1/auth/verify-otp",
3952
+ path: AUTH_PATHS.verifyOtp,
3679
3953
  body: input,
3680
3954
  skipAuth: true,
3681
3955
  skipAuthRefresh: true,
@@ -3692,7 +3966,7 @@ var AuthResource = class extends BaseResource {
3692
3966
  resendOtp(input, options = {}) {
3693
3967
  return this.http.request({
3694
3968
  method: "POST",
3695
- path: "/api/v1/auth/resend-otp",
3969
+ path: AUTH_PATHS.resendOtp,
3696
3970
  body: input,
3697
3971
  skipAuth: true,
3698
3972
  skipAuthRefresh: true,
@@ -3769,7 +4043,7 @@ var AuthResource = class extends BaseResource {
3769
4043
  async logout(options = {}) {
3770
4044
  await this.http.request({
3771
4045
  method: "POST",
3772
- path: "/api/v1/auth/logout",
4046
+ path: AUTH_PATHS.logout,
3773
4047
  skipAuthRefresh: true,
3774
4048
  ...this.requestOptions(options)
3775
4049
  });
@@ -3799,7 +4073,7 @@ var AuthResource = class extends BaseResource {
3799
4073
  async forgotPassword(input, options = {}) {
3800
4074
  const body = await this.http.request({
3801
4075
  method: "POST",
3802
- path: "/api/v1/auth/forgot-password",
4076
+ path: AUTH_PATHS.forgotPassword,
3803
4077
  body: input,
3804
4078
  skipAuth: true,
3805
4079
  skipAuthRefresh: true,
@@ -3820,7 +4094,7 @@ var AuthResource = class extends BaseResource {
3820
4094
  resetPassword(input, options = {}) {
3821
4095
  return this.http.request({
3822
4096
  method: "POST",
3823
- path: "/api/v1/auth/reset-password",
4097
+ path: AUTH_PATHS.resetPassword,
3824
4098
  body: input,
3825
4099
  skipAuth: true,
3826
4100
  skipAuthRefresh: true,
@@ -3858,14 +4132,17 @@ var AuthResource = class extends BaseResource {
3858
4132
  * Меняет пароль. Требует действующей сессии.
3859
4133
  *
3860
4134
  * При неверном текущем пароле сервер отвечает `ACCOUNT_CURRENT_PASSWORD_INCORRECT`.
4135
+ *
4136
+ * @example
4137
+ * ```ts
4138
+ * await itd.auth.changePassword({ currentPassword, newPassword });
4139
+ * ```
3861
4140
  */
3862
4141
  changePassword(input, options = {}) {
3863
4142
  return this.http.request({
3864
4143
  method: "POST",
3865
- path: "/api/v1/auth/change-password",
3866
- // Текущий пароль уходит под двумя именами: какое из них ждёт сервер, снаружи
3867
- // не проверить, а лишнее поле он игнорирует.
3868
- body: { ...input, currentPassword: input.oldPassword },
4144
+ path: AUTH_PATHS.changePassword,
4145
+ body: { currentPassword: input.currentPassword, newPassword: input.newPassword },
3869
4146
  ...this.requestOptions(options)
3870
4147
  });
3871
4148
  }
@@ -3881,13 +4158,13 @@ var AuthResource = class extends BaseResource {
3881
4158
  * ```
3882
4159
  */
3883
4160
  oauthUrl(provider) {
3884
- return joinUrl(this.http.baseUrl, `/api/v1/auth/login/${provider}`);
4161
+ return joinUrl(this.http.baseUrl, `${AUTH_PATHS.oauthLogin}/${provider}`);
3885
4162
  }
3886
4163
  /** Загружает список активных сессий. У текущей поле `isCurrent` равно `true`. */
3887
4164
  async sessions(options = {}) {
3888
4165
  const body = await this.http.request({
3889
4166
  method: "GET",
3890
- path: "/api/v1/auth/sessions",
4167
+ path: AUTH_PATHS.sessions,
3891
4168
  ...this.requestOptions(options)
3892
4169
  });
3893
4170
  return pickArray(body, "sessions");
@@ -3896,7 +4173,7 @@ var AuthResource = class extends BaseResource {
3896
4173
  revokeSession(sessionId, options = {}) {
3897
4174
  return this.http.request({
3898
4175
  method: "DELETE",
3899
- path: `/api/v1/auth/sessions/${encodeURIComponent(sessionId)}`,
4176
+ path: `${AUTH_PATHS.sessions}/${encodeURIComponent(sessionId)}`,
3900
4177
  ...this.requestOptions(options)
3901
4178
  });
3902
4179
  }
@@ -3904,7 +4181,7 @@ var AuthResource = class extends BaseResource {
3904
4181
  revokeOtherSessions(options = {}) {
3905
4182
  return this.http.request({
3906
4183
  method: "DELETE",
3907
- path: "/api/v1/auth/sessions",
4184
+ path: AUTH_PATHS.sessions,
3908
4185
  ...this.requestOptions(options)
3909
4186
  });
3910
4187
  }
@@ -4376,7 +4653,25 @@ var NotificationsResource = class extends BaseResource {
4376
4653
  }
4377
4654
  };
4378
4655
 
4656
+ // src/core/time.ts
4657
+ var NAIVE_STAMP = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/;
4658
+ function utcStampToIso(value) {
4659
+ const match = typeof value === "string" ? NAIVE_STAMP.exec(value) : null;
4660
+ if (!match) return value;
4661
+ const iso = `${match[1]}T${match[2]}Z`;
4662
+ return Number.isFinite(Date.parse(iso)) ? iso : value;
4663
+ }
4664
+
4379
4665
  // src/resources/platform.ts
4666
+ function normalizeStatus(body) {
4667
+ if (!isRecord(body) || !Array.isArray(body.services)) return body;
4668
+ return {
4669
+ ...body,
4670
+ services: body.services.map(
4671
+ (service) => typeof service?.last_checked === "string" ? { ...service, last_checked: utcStampToIso(service.last_checked) } : service
4672
+ )
4673
+ };
4674
+ }
4380
4675
  var PlatformResource = class extends BaseResource {
4381
4676
  /** Загружает журнал изменений. */
4382
4677
  async changelog(options = {}) {
@@ -4404,6 +4699,31 @@ var PlatformResource = class extends BaseResource {
4404
4699
  ...this.requestOptions(options)
4405
4700
  });
4406
4701
  }
4702
+ /**
4703
+ * Загружает состояние сервисов платформы за последние 90 суток.
4704
+ *
4705
+ * Идёт на хост `статус.итд.com` без авторизации. Ответ кэшируется сервером на минуту.
4706
+ * История по суткам приходит разреженной, ровный массив даёт `statusDays`.
4707
+ *
4708
+ * @example
4709
+ * ```ts
4710
+ * const status = await itd.platform.status();
4711
+ *
4712
+ * if (status.overall_status !== 'operational') {
4713
+ * const broken = status.services.filter((s) => s.current_status !== 'operational');
4714
+ * console.log('лежит:', broken.map((s) => s.name).join(', '));
4715
+ * }
4716
+ * ```
4717
+ */
4718
+ async status(options = {}) {
4719
+ const body = await this.http.request({
4720
+ method: "GET",
4721
+ service: STATUS_SERVICE,
4722
+ path: "/api/status",
4723
+ ...this.requestOptions(options)
4724
+ });
4725
+ return normalizeStatus(body);
4726
+ }
4407
4727
  };
4408
4728
 
4409
4729
  // src/resources/posts.ts
@@ -5168,13 +5488,15 @@ var VerificationResource = class extends BaseResource {
5168
5488
  };
5169
5489
 
5170
5490
  // src/client.ts
5171
- var ItdClient = class {
5491
+ var ItdClient = class _ItdClient {
5172
5492
  #config;
5173
5493
  #http;
5494
+ #transport;
5174
5495
  #authManager;
5175
5496
  #jar;
5176
- #queue;
5497
+ #queues;
5177
5498
  #plugins = new PluginRegistry();
5499
+ #services;
5178
5500
  /** Порождённые потоки уведомлений — чтобы `close()` мог закрыть их разом. */
5179
5501
  #streams = /* @__PURE__ */ new Set();
5180
5502
  /** Авторизация, сессии и пароли. */
@@ -5211,19 +5533,23 @@ var ItdClient = class {
5211
5533
  const config = resolveConfig(options);
5212
5534
  this.#config = config;
5213
5535
  this.#jar = new CookieJar();
5214
- const queue = config.rateLimit ? new RequestQueue(config.rateLimit) : void 0;
5215
- this.#queue = queue;
5536
+ this.#services = new ServiceRegistry(config.baseUrl);
5537
+ for (const service of BUILT_IN_SERVICES) this.#services.define(service);
5538
+ for (const service of config.services) this.#services.define(service);
5539
+ const queues = config.rateLimit ? new RequestQueuePool(config.rateLimit) : void 0;
5540
+ this.#queues = queues;
5216
5541
  let authManager;
5217
5542
  const transport = new Transport(config, {
5218
5543
  cookies: config.useCookieJar ? this.#jar : void 0,
5219
5544
  getDeviceId: () => authManager.getDeviceId(),
5220
- onRateLimit: queue && config.rateLimit?.respectHeaders ? (limit, remaining) => this.#throttleByHeaders(limit, remaining) : void 0
5545
+ onRateLimit: queues && config.rateLimit?.respectHeaders ? (limit, remaining, request) => this.#throttleByHeaders(limit, remaining, request) : void 0
5221
5546
  });
5547
+ this.#transport = transport;
5222
5548
  const pluginsLayer = createPluginsMiddleware(this.#plugins);
5223
5549
  const retriesLayer = createRetryMiddleware({
5224
5550
  retry: config.retry,
5225
5551
  rateLimitDelays: config.rateLimit?.retryDelays ?? [],
5226
- pauseQueue: queue ? (ms) => queue.pause(ms) : void 0,
5552
+ pauseQueue: queues ? (ms, request) => queues.for(request.service).pause(ms) : void 0,
5227
5553
  hooks: config.hooks,
5228
5554
  logger: config.logger,
5229
5555
  buildUrl: (request) => transport.buildUrl(request)
@@ -5241,8 +5567,13 @@ var ItdClient = class {
5241
5567
  authManager = new AuthManager(config, authHandler, this.#jar);
5242
5568
  this.#authManager = authManager;
5243
5569
  const middlewares = [];
5244
- if (queue) middlewares.push(createQueueMiddleware(queue.schedule.bind(queue)));
5570
+ if (queues) {
5571
+ middlewares.push(
5572
+ createQueueMiddleware((request, task) => queues.for(request.service).schedule(task))
5573
+ );
5574
+ }
5245
5575
  middlewares.push(pluginsLayer);
5576
+ middlewares.push(createServicesMiddleware(this.#services));
5246
5577
  middlewares.push(retriesLayer);
5247
5578
  middlewares.push(
5248
5579
  createAuthMiddleware({
@@ -5300,7 +5631,7 @@ var ItdClient = class {
5300
5631
  *
5301
5632
  * @example
5302
5633
  * ```ts
5303
- * import { crypt } from 'itd-api-crypto';
5634
+ * import { crypt } from '@itd-api/crypto';
5304
5635
  *
5305
5636
  * itd.use(crypt());
5306
5637
  * await itd.posts.create({ content: 'секрет' }, { encrypt: 'invis' });
@@ -5310,6 +5641,41 @@ var ItdClient = class {
5310
5641
  this.#plugins.add(plugin, { baseUrl: this.#config.baseUrl, logger: this.#config.logger });
5311
5642
  return this;
5312
5643
  }
5644
+ /**
5645
+ * Регистрирует сервис платформы — домен, отличный от основного.
5646
+ *
5647
+ * Запросы с `{ service: 'имя' }` уходят на его хост с его заголовками. То же самое умеет
5648
+ * опция `services` конструктора. Занятое имя не переопределяется — ни своё, ни встроенное:
5649
+ * разовому запросу хост задаётся полем `baseUrl`.
5650
+ *
5651
+ * Заголовок авторизации по умолчанию уходит только своим — домену клиента и его
5652
+ * поддоменам. Стороннему хосту токен нужно разрешить явно: `auth: true`.
5653
+ *
5654
+ * @throws {ItdConfigError} если определение неверно или имя уже занято
5655
+ *
5656
+ * @example Сервис платформы на поддомене — токен уходит сам
5657
+ * ```ts
5658
+ * itd.defineService({
5659
+ * name: 'pb',
5660
+ * baseUrl: 'https://pbapi.xn--d1ah4a.com',
5661
+ * headers: { Referer: 'https://pixel.xn--d1ah4a.com/' },
5662
+ * });
5663
+ *
5664
+ * await itd.request({ method: 'GET', service: 'pb', path: '/api/pixel-info' });
5665
+ * ```
5666
+ */
5667
+ defineService(definition) {
5668
+ this.#services.define(definition);
5669
+ return this;
5670
+ }
5671
+ /**
5672
+ * Базовый URL зарегистрированного сервиса.
5673
+ *
5674
+ * @throws {ItdConfigError} если сервис не зарегистрирован
5675
+ */
5676
+ serviceBaseUrl(name) {
5677
+ return this.#services.resolveBaseUrl(name);
5678
+ }
5313
5679
  /**
5314
5680
  * Подписывается на события авторизации.
5315
5681
  *
@@ -5351,6 +5717,7 @@ var ItdClient = class {
5351
5717
  {
5352
5718
  baseUrl: this.#config.baseUrl,
5353
5719
  fetch: this.#config.fetch,
5720
+ baseHeaders: (url) => this.#transport.platformHeaders(url),
5354
5721
  getToken: () => this.#authManager.getAccessToken(),
5355
5722
  refresh: () => this.#authManager.onUnauthorized(),
5356
5723
  fetchUnreadCount: () => this.notifications.count(),
@@ -5379,12 +5746,19 @@ var ItdClient = class {
5379
5746
  async close() {
5380
5747
  for (const stream of this.#streams) stream.disconnect();
5381
5748
  this.#streams.clear();
5382
- this.#queue?.stop();
5749
+ this.#queues?.stop();
5383
5750
  }
5384
5751
  /** Позволяет использовать клиент с `await using`. */
5385
5752
  [Symbol.asyncDispose]() {
5386
5753
  return this.close();
5387
5754
  }
5755
+ static {
5756
+ if (typeof Symbol.asyncDispose !== "symbol") {
5757
+ const prototype = _ItdClient.prototype;
5758
+ prototype[/* @__PURE__ */ Symbol.for("Symbol.asyncDispose")] = prototype.undefined;
5759
+ delete prototype.undefined;
5760
+ }
5761
+ }
5388
5762
  /** Текущая сессия целиком — чтобы сохранить её самостоятельно. */
5389
5763
  getSession() {
5390
5764
  return this.#authManager.getSession();
@@ -5404,21 +5778,16 @@ var ItdClient = class {
5404
5778
  * Смысл этой паузы прежде всего в том, чтобы при работе в несколько потоков остальные
5405
5779
  * запросы не улетели в стену все разом.
5406
5780
  */
5407
- #throttleByHeaders(limit, remaining) {
5781
+ #throttleByHeaders(limit, remaining, request) {
5408
5782
  if (remaining === void 0 || remaining > 0) return;
5409
5783
  const first = this.#config.rateLimit?.retryDelays[0];
5410
5784
  if (first === void 0) return;
5411
- this.#queue?.pause(first);
5785
+ this.#queues?.for(request.service).pause(first);
5412
5786
  this.#config.logger?.debug(
5413
5787
  `\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`
5414
5788
  );
5415
5789
  }
5416
5790
  };
5417
- if (typeof Symbol.asyncDispose !== "symbol") {
5418
- const prototype = ItdClient.prototype;
5419
- prototype[/* @__PURE__ */ Symbol.for("Symbol.asyncDispose")] = prototype.undefined;
5420
- delete prototype.undefined;
5421
- }
5422
5791
  function createClient(options = {}) {
5423
5792
  return new ItdClient(options);
5424
5793
  }
@@ -5524,12 +5893,19 @@ function resolveNotificationUrl(notification) {
5524
5893
  function isMyProfile(profile) {
5525
5894
  return "subscription" in profile;
5526
5895
  }
5896
+ var STATUS_WINDOW_DAYS = 90;
5527
5897
  function toDate(value) {
5528
5898
  if (!value) return null;
5529
5899
  const date = new Date(value);
5530
5900
  return Number.isFinite(date.getTime()) ? date : null;
5531
5901
  }
5902
+ function statusDays(service) {
5903
+ return Array.from(
5904
+ { length: STATUS_WINDOW_DAYS },
5905
+ (_, index) => service.days[String(index)] ?? null
5906
+ );
5907
+ }
5532
5908
 
5533
- export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AccessType, AttachmentType, CommentSort, DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, FeedTab, IMAGE_MIME_TYPES, InteractionType, ItdAbortError, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, LocalStorageTokenStorage, MAX_RECONNECT_ATTEMPTS, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, OAuthProvider, PaginationMode, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, RealtimeStatus, RealtimeTransportKind, ReportReason, ReportTargetType, RuntimeMode, STREAM_PATH, SignInStatus, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, canonicalNotificationType, comment, createClient, createTokenStorage, formatNotificationText, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, normalizeNotification, poll, post, readNotificationEvent, readUnreadCountEvent, report, resolveNotificationUrl, toDate };
5534
- //# sourceMappingURL=chunk-JIYN33FG.js.map
5535
- //# sourceMappingURL=chunk-JIYN33FG.js.map
5909
+ export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AccessType, AttachmentType, BUILT_IN_SERVICES, CommentSort, DEFAULT_BASE_URL, DEFAULT_STATUS_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_UPLOAD_TIMEOUT, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, FeedTab, IMAGE_MIME_TYPES, IncidentKind, InteractionType, ItdAbortError, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, LocalStorageTokenStorage, MAX_RECONNECT_ATTEMPTS, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, OAuthProvider, PaginationMode, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, REQUEST_OPTION_KEYS, RealtimeStatus, RealtimeTransportKind, ReportReason, ReportTargetType, RuntimeMode, STATUS_SERVICE, STREAM_PATH, ServiceRegistry, ServiceState, SignInStatus, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, canonicalNotificationType, comment, createClient, createTokenStorage, formatNotificationText, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, normalizeNotification, poll, post, readNotificationEvent, readUnreadCountEvent, report, resolveNotificationUrl, statusDays, toDate, utcStampToIso };
5910
+ //# sourceMappingURL=chunk-MYAU2WJU.js.map
5911
+ //# sourceMappingURL=chunk-MYAU2WJU.js.map