itd-api 0.7.2 → 0.9.0

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.
Files changed (104) hide show
  1. package/README.md +72 -27
  2. package/dist/events/index.cjs +53 -0
  3. package/dist/events/index.d.cts +3 -0
  4. package/dist/events/index.d.ts +3 -0
  5. package/dist/events/index.js +5 -0
  6. package/dist/index.cjs +234 -200
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +70 -64
  9. package/dist/index.d.ts +70 -64
  10. package/dist/index.js +202 -179
  11. package/dist/index.js.map +1 -1
  12. package/dist/node/index.cjs +6 -5
  13. package/dist/node/index.cjs.map +1 -1
  14. package/dist/node/index.d.cts +2 -2
  15. package/dist/node/index.d.ts +2 -2
  16. package/dist/node/index.js +5 -4
  17. package/dist/node/index.js.map +1 -1
  18. package/dist/rest/index.cjs +32 -26
  19. package/dist/rest/index.cjs.map +1 -1
  20. package/dist/rest/index.d.cts +9 -7
  21. package/dist/rest/index.d.ts +9 -7
  22. package/dist/rest/index.js +19 -21
  23. package/dist/rest/index.js.map +1 -1
  24. package/dist/shared/{errors-DfU8M5eS.cjs → errors-BmP3TKoW.cjs} +3 -3
  25. package/dist/shared/{errors-DfU8M5eS.cjs.map → errors-BmP3TKoW.cjs.map} +1 -1
  26. package/dist/shared/{errors-Bhrd2fJd.js → errors-GI10kZxk.js} +3 -3
  27. package/dist/shared/{errors-Bhrd2fJd.js.map → errors-GI10kZxk.js.map} +1 -1
  28. package/dist/shared/{websocket-BLR8eVJV.js → events-C5wnTXPT.js} +770 -490
  29. package/dist/shared/events-C5wnTXPT.js.map +1 -0
  30. package/dist/shared/events-CqtkP65d.d.ts +698 -0
  31. package/dist/shared/events-DGAWlCaI.d.cts +698 -0
  32. package/dist/shared/{websocket-C_eI4H2o.cjs → events-FZfnEez0.cjs} +834 -518
  33. package/dist/shared/events-FZfnEez0.cjs.map +1 -0
  34. package/dist/shared/{storage-BPJR_k4-.cjs → key-value-store-B5GYVEYZ.cjs} +2 -86
  35. package/dist/shared/key-value-store-B5GYVEYZ.cjs.map +1 -0
  36. package/dist/shared/{storage-D86edNCB.js → key-value-store-Bt2PSMxY.js} +3 -69
  37. package/dist/shared/key-value-store-Bt2PSMxY.js.map +1 -0
  38. package/dist/shared/key-value-store-COZaNHZS.d.cts +69 -0
  39. package/dist/shared/key-value-store-COZaNHZS.d.ts +69 -0
  40. package/dist/shared/{multi-storage-DccjD7Ww.d.ts → multi-storage-BEL7tcKn.d.cts} +3 -2
  41. package/dist/shared/{multi-storage--yTEqiod.cjs → multi-storage-CDKLa956.cjs} +11 -10
  42. package/dist/shared/{multi-storage--yTEqiod.cjs.map → multi-storage-CDKLa956.cjs.map} +1 -1
  43. package/dist/shared/{multi-storage-CjAPB5Kq.d.cts → multi-storage-LkljOSzA.d.ts} +3 -2
  44. package/dist/shared/{multi-storage-CkvTUC5m.js → multi-storage-SoW05uzO.js} +5 -4
  45. package/dist/shared/{multi-storage-CkvTUC5m.js.map → multi-storage-SoW05uzO.js.map} +1 -1
  46. package/dist/shared/{options-Dg5N3r1V.cjs → options-DOJYtoti.cjs} +8 -6
  47. package/dist/shared/options-DOJYtoti.cjs.map +1 -0
  48. package/dist/shared/{options-DtATYdLr.js → options-DhgGg7Ms.js} +8 -6
  49. package/dist/shared/options-DhgGg7Ms.js.map +1 -0
  50. package/dist/shared/{cookies-DZwFq6kr.cjs → redact-BP00URmQ.cjs} +312 -2
  51. package/dist/shared/redact-BP00URmQ.cjs.map +1 -0
  52. package/dist/shared/{cookies-tX2sNwxb.js → redact-Ba5iSVYZ.js} +235 -3
  53. package/dist/shared/redact-Ba5iSVYZ.js.map +1 -0
  54. package/dist/shared/{render-CgwKdOzu.d.ts → render-B4qis7Sb.d.ts} +775 -313
  55. package/dist/shared/{render-DO0F5YSm.d.cts → render-DDwy2Ame.d.cts} +775 -313
  56. package/dist/shared/{render-mcuELiYi.cjs → render-Dqi8Owqk.cjs} +1464 -662
  57. package/dist/shared/render-Dqi8Owqk.cjs.map +1 -0
  58. package/dist/shared/{render-C6HRPs10.js → render-hiWFh2s4.js} +1366 -618
  59. package/dist/shared/render-hiWFh2s4.js.map +1 -0
  60. package/dist/shared/storage-BQrtKon9.d.cts +86 -0
  61. package/dist/shared/storage-CTRGT5wF.d.ts +86 -0
  62. package/dist/shared/storage-CfHKzVHf.js +71 -0
  63. package/dist/shared/storage-CfHKzVHf.js.map +1 -0
  64. package/dist/shared/storage-QNVxzpDB.cjs +88 -0
  65. package/dist/shared/storage-QNVxzpDB.cjs.map +1 -0
  66. package/dist/shared/{url-BaMCQpYH.cjs → url-Bsb3xE5U.cjs} +577 -466
  67. package/dist/shared/url-Bsb3xE5U.cjs.map +1 -0
  68. package/dist/shared/{url-IU0xN9wX.js → url-DP33mp3s.js} +508 -409
  69. package/dist/shared/url-DP33mp3s.js.map +1 -0
  70. package/dist/shared/{url-DTfZ2toq.d.ts → url-XBsMdAcv.d.cts} +1313 -1200
  71. package/dist/shared/{url-DTfZ2toq.d.cts → url-XBsMdAcv.d.ts} +1313 -1200
  72. package/dist/web/index.cjs +5 -4
  73. package/dist/web/index.cjs.map +1 -1
  74. package/dist/web/index.d.cts +1 -1
  75. package/dist/web/index.d.ts +1 -1
  76. package/dist/web/index.js +3 -2
  77. package/dist/web/index.js.map +1 -1
  78. package/package.json +16 -15
  79. package/dist/realtime/index.cjs +0 -165
  80. package/dist/realtime/index.cjs.map +0 -1
  81. package/dist/realtime/index.d.cts +0 -51
  82. package/dist/realtime/index.d.ts +0 -51
  83. package/dist/realtime/index.js +0 -120
  84. package/dist/realtime/index.js.map +0 -1
  85. package/dist/shared/auth-provider-CG8oCQ9F.cjs +0 -108
  86. package/dist/shared/auth-provider-CG8oCQ9F.cjs.map +0 -1
  87. package/dist/shared/auth-provider-mYqxsSVa.js +0 -91
  88. package/dist/shared/auth-provider-mYqxsSVa.js.map +0 -1
  89. package/dist/shared/cookies-DZwFq6kr.cjs.map +0 -1
  90. package/dist/shared/cookies-tX2sNwxb.js.map +0 -1
  91. package/dist/shared/options-Dg5N3r1V.cjs.map +0 -1
  92. package/dist/shared/options-DtATYdLr.js.map +0 -1
  93. package/dist/shared/render-C6HRPs10.js.map +0 -1
  94. package/dist/shared/render-mcuELiYi.cjs.map +0 -1
  95. package/dist/shared/storage-BPJR_k4-.cjs.map +0 -1
  96. package/dist/shared/storage-C_eICCep.d.cts +0 -152
  97. package/dist/shared/storage-C_eICCep.d.ts +0 -152
  98. package/dist/shared/storage-D86edNCB.js.map +0 -1
  99. package/dist/shared/url-BaMCQpYH.cjs.map +0 -1
  100. package/dist/shared/url-IU0xN9wX.js.map +0 -1
  101. package/dist/shared/websocket-BLR8eVJV.js.map +0 -1
  102. package/dist/shared/websocket-C_eI4H2o.cjs.map +0 -1
  103. package/dist/shared/websocket-DF7XIMiX.d.cts +0 -562
  104. package/dist/shared/websocket-DYKBr8HF.d.ts +0 -562
@@ -1,5 +1,5 @@
1
- const require_errors = require("./errors-DfU8M5eS.cjs");
2
- const require_cookies = require("./cookies-DZwFq6kr.cjs");
1
+ const require_errors = require("./errors-BmP3TKoW.cjs");
2
+ const require_redact = require("./redact-BP00URmQ.cjs");
3
3
  //#region src/core/async-dispose.ts
4
4
  /**
5
5
  * Подставляет метод `await using` там, где `Symbol.asyncDispose` отсутствует.
@@ -51,120 +51,6 @@ function createDeadline(timeout, clock = systemClock) {
51
51
  };
52
52
  }
53
53
  //#endregion
54
- //#region src/core/runtime.ts
55
- /**
56
- * Как библиотека обращается с cookie.
57
- *
58
- * - `browser` — cookie ведёт браузер, запросы уходят с `credentials: 'include'`;
59
- * - `server` — cookie ведёт встроенный jar, заголовок `Cookie` подставляется вручную;
60
- * - `auto` — определяется по среде исполнения (значение по умолчанию).
61
- */
62
- const RuntimeMode = Object.freeze({
63
- /** Определяется по среде исполнения. Значение по умолчанию. */
64
- Auto: "auto",
65
- /** Cookie ведёт браузер, запросы уходят с `credentials: 'include'`. */
66
- Browser: "browser",
67
- /** Cookie ведёт встроенный jar, заголовок `Cookie` подставляется вручную. */
68
- Server: "server"
69
- });
70
- /** Распознанная среда исполнения. */
71
- const DetectedRuntime = Object.freeze({
72
- Browser: "browser",
73
- /** Есть `window`, но нет `document`; cookie ведёт нативный сетевой слой. */
74
- ReactNative: "react-native",
75
- Server: "server"
76
- });
77
- /**
78
- * Определяет среду исполнения.
79
- *
80
- * React Native выделен отдельно намеренно: там есть `window`, но нет `document`, а cookie
81
- * хранит нативный сетевой слой (`NSHTTPCookieStorage` / `OkHttp`). Свой jar в этой среде
82
- * привёл бы к отправке cookie дважды.
83
- */
84
- function detectRuntime() {
85
- if (globalThis.navigator?.product === "ReactNative") return DetectedRuntime.ReactNative;
86
- if (typeof document !== "undefined") return DetectedRuntime.Browser;
87
- return DetectedRuntime.Server;
88
- }
89
- /**
90
- * Нужно ли вести собственный cookie-jar.
91
- *
92
- * Только в серверных средах: в браузере `Set-Cookie` из JS не читается и не нужен,
93
- * в React Native cookie ведёт нативный слой.
94
- */
95
- function shouldUseCookieJar(mode) {
96
- if (mode === RuntimeMode.Browser) return false;
97
- if (mode === RuntimeMode.Server) return true;
98
- return detectRuntime() === DetectedRuntime.Server;
99
- }
100
- /**
101
- * Нужно ли отправлять запросы с `credentials: 'include'`.
102
- *
103
- * Только в браузере: именно так refresh-cookie попадает в запрос `POST /api/v1/auth/refresh`.
104
- * Требует настроенного CORS на стороне итд.com — если его нет, укажите свой прокси в `baseUrl`.
105
- */
106
- function shouldSendCredentials(mode) {
107
- if (mode === RuntimeMode.Browser) return true;
108
- if (mode === RuntimeMode.Server) return false;
109
- return detectRuntime() === DetectedRuntime.Browser;
110
- }
111
- /**
112
- * Возвращает реализацию `fetch`.
113
- *
114
- * @param custom реализация из конфигурации клиента, если пользователь её передал
115
- * @throws {ItdConfigError} если `fetch` недоступен — например, на Node ниже 18
116
- */
117
- function resolveFetch(custom) {
118
- if (custom !== void 0) {
119
- if (typeof custom !== "function") throw new require_errors.ItdConfigError("fetch должен быть функцией");
120
- return custom;
121
- }
122
- if (typeof globalThis.fetch === "function") return globalThis.fetch.bind(globalThis);
123
- throw new require_errors.ItdConfigError("В этой среде нет глобального fetch. Обновитесь до Node 18+ либо передайте свою реализацию через опцию fetch.");
124
- }
125
- /**
126
- * Проверки бинарных типов, безопасные в любой среде.
127
- *
128
- * Обычный `instanceof` здесь недостаточен: конструктора может не быть вовсе, и тогда
129
- * проверка падает с `ReferenceError`, а не возвращает `false`. `Blob` есть в Node с 18.0,
130
- * но `File` стал глобальным только в Node 20 — при заявленной поддержке Node 18 голый
131
- * `instanceof File` ронял бы любую загрузку.
132
- */
133
- function isBlob(value) {
134
- return typeof Blob !== "undefined" && value instanceof Blob;
135
- }
136
- /** @see {@link isBlob} */
137
- function isFile(value) {
138
- return typeof File !== "undefined" && value instanceof File;
139
- }
140
- /** Доступно ли потоковое чтение тела ответа — от этого зависит, сработает ли SSE. */
141
- function supportsStreamingBody() {
142
- return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
143
- }
144
- /**
145
- * Создаёт идентификатор устройства для заголовка `X-Device-Id` — UUID v4.
146
- *
147
- * `crypto.randomUUID` есть в Node 19+, браузерах и Deno, но отсутствует в Node 18 вне
148
- * защищённого контекста, поэтому предусмотрен запасной путь.
149
- */
150
- function createDeviceId() {
151
- const webCrypto = globalThis.crypto;
152
- if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
153
- const bytes = /* @__PURE__ */ new Uint8Array(16);
154
- if (typeof webCrypto?.getRandomValues === "function") webCrypto.getRandomValues(bytes);
155
- else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
156
- bytes[6] = bytes[6] & 15 | 64;
157
- bytes[8] = bytes[8] & 63 | 128;
158
- const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
159
- return [
160
- hex.slice(0, 8),
161
- hex.slice(8, 12),
162
- hex.slice(12, 16),
163
- hex.slice(16, 20),
164
- hex.slice(20, 32)
165
- ].join("-");
166
- }
167
- //#endregion
168
54
  //#region src/core/scheduling/pacing.ts
169
55
  /** Реакция на остаток лимита из заголовков ответа. */
170
56
  const RateLimitPacing = Object.freeze({
@@ -199,7 +85,7 @@ function requireOptionalBoolean(value, name) {
199
85
  //#endregion
200
86
  //#region src/core/version.ts
201
87
  /** Версия библиотеки. Попадает в `User-Agent`. */
202
- const LIBRARY_VERSION = "0.7.2";
88
+ const LIBRARY_VERSION = "0.9.0";
203
89
  //#endregion
204
90
  //#region src/core/config.ts
205
91
  /** Базовый URL API итд.com. Домен записан в punycode: `итд.com`. */
@@ -318,6 +204,10 @@ function requireConcurrency(value, name) {
318
204
  if (!Number.isInteger(value) || value < 1) throw new require_errors.ItdConfigError(`${name} должен быть целым числом от 1, получено: ${value}`);
319
205
  return value;
320
206
  }
207
+ function requireRps(value, name) {
208
+ if (!Number.isFinite(value) || value <= 0) throw new require_errors.ItdConfigError(`${name} должен быть положительным числом, получено: ${value}`);
209
+ return value;
210
+ }
321
211
  function resolvePacing(pacing) {
322
212
  const known = Object.values(RateLimitPacing);
323
213
  if (pacing !== void 0 && !known.includes(pacing)) throw new require_errors.ItdConfigError(`rateLimit.pacing должен быть одним из ${known.join(", ")}, получено: ${pacing}`);
@@ -340,9 +230,11 @@ function resolveBucketOverrides(overrides, bucket, catalog) {
340
230
  assertKnownBucket(name, "rateLimit.bucketOverrides", bucket, catalog);
341
231
  if (override.concurrency !== void 0) requireConcurrency(override.concurrency, `rateLimit.bucketOverrides.${name}.concurrency`);
342
232
  if (override.limit !== void 0 && (!Number.isFinite(override.limit) || override.limit <= 0)) throw new require_errors.ItdConfigError(`rateLimit.bucketOverrides.${name}.limit должен быть положительным числом, получено: ${override.limit}`);
233
+ if (override.rps !== void 0) requireRps(override.rps, `rateLimit.bucketOverrides.${name}.rps`);
343
234
  const built = builtIn[name];
344
235
  resolved[name] = Object.freeze({
345
236
  concurrency: override.concurrency ?? built?.concurrency,
237
+ rps: override.rps ?? built?.rps,
346
238
  limit: override.limit ?? built?.limit
347
239
  });
348
240
  }
@@ -375,7 +267,7 @@ function resolveRateLimit(rateLimit, catalog) {
375
267
  };
376
268
  if (!rateLimit) return defaults;
377
269
  const concurrency = requireConcurrency(rateLimit.concurrency ?? 6, "rateLimit.concurrency");
378
- if (rateLimit.rps !== void 0 && (!Number.isFinite(rateLimit.rps) || rateLimit.rps <= 0)) throw new require_errors.ItdConfigError(`rateLimit.rps должен быть положительным числом, получено: ${rateLimit.rps}`);
270
+ if (rateLimit.rps !== void 0) requireRps(rateLimit.rps, "rateLimit.rps");
379
271
  const retryDelays = rateLimit.retryDelays ?? defaults.retryDelays;
380
272
  if (!Array.isArray(retryDelays)) throw new require_errors.ItdConfigError("rateLimit.retryDelays должен быть массивом чисел");
381
273
  for (const delay of retryDelays) requirePositive(delay, "rateLimit.retryDelays");
@@ -424,16 +316,16 @@ function resolveServices(services) {
424
316
  */
425
317
  function resolveRuntimeConfig(options, catalog) {
426
318
  if (!isRecord$1(options)) throw new require_errors.ItdConfigError("опции клиента должны быть объектом");
427
- const mode = options.mode ?? RuntimeMode.Auto;
428
- if (!Object.values(RuntimeMode).includes(mode)) throw new require_errors.ItdConfigError(`mode должен быть одним из ${Object.values(RuntimeMode).join(", ")}, получено: ${mode}`);
319
+ const mode = options.mode ?? require_redact.RuntimeMode.Auto;
320
+ if (!Object.values(require_redact.RuntimeMode).includes(mode)) throw new require_errors.ItdConfigError(`mode должен быть одним из ${Object.values(require_redact.RuntimeMode).join(", ")}, получено: ${mode}`);
429
321
  const timeout = requirePositive(options.timeout ?? 3e4, "timeout");
430
322
  const shutdownTimeout = requirePositive(options.shutdownTimeout ?? 1e4, "shutdownTimeout");
431
323
  if (options.clock !== void 0 && (typeof options.clock !== "object" || options.clock === null || typeof options.clock.now !== "function" || typeof options.clock.schedule !== "function")) throw new require_errors.ItdConfigError("clock должен предоставлять методы now() и schedule()");
432
324
  if (options.userAgent !== void 0 && options.userAgent !== false && typeof options.userAgent !== "string") throw new require_errors.ItdConfigError("userAgent должен быть строкой или false");
433
325
  return {
434
- baseUrl: require_cookies.normalizeBaseUrl(options.baseUrl ?? "https://xn--d1ah4a.com"),
326
+ baseUrl: require_redact.normalizeBaseUrl(options.baseUrl ?? "https://xn--d1ah4a.com"),
435
327
  services: resolveServices(options.services),
436
- fetch: resolveFetch(options.fetch),
328
+ fetch: require_redact.resolveFetch(options.fetch),
437
329
  clock: options.clock ?? systemClock,
438
330
  timeout,
439
331
  shutdownTimeout,
@@ -443,8 +335,8 @@ function resolveRuntimeConfig(options, catalog) {
443
335
  headers: resolveHeaders(options.headers),
444
336
  userAgent: options.userAgent === false ? void 0 : options.userAgent ?? DEFAULT_USER_AGENT,
445
337
  mode,
446
- useCookieJar: shouldUseCookieJar(mode),
447
- sendCredentials: shouldSendCredentials(mode),
338
+ useCookieJar: require_redact.shouldUseCookieJar(mode),
339
+ sendCredentials: require_redact.shouldSendCredentials(mode),
448
340
  logger: resolveLogger(options.logger)
449
341
  };
450
342
  }
@@ -660,7 +552,10 @@ var PluginRegistry = class {
660
552
  };
661
553
  const installed = plugin.install({
662
554
  ...context,
663
- operations: { use: (transformer) => register(transformers, transformer, "operations") },
555
+ operations: {
556
+ get: this.#options.operationMetadata,
557
+ use: (transformer) => register(transformers, transformer, "operations")
558
+ },
664
559
  attempts: { use: (interceptor) => register(interceptors, interceptor, "attempts") }
665
560
  });
666
561
  if (installed !== void 0 && typeof installed !== "function") throw new require_errors.ItdConfigError(`install() плагина «${plugin.name}» должен вернуть функцию или void`);
@@ -699,8 +594,9 @@ var PluginRegistry = class {
699
594
  this.#removing.add(name);
700
595
  const deadline = createDeadline(this.#options.shutdownTimeout, this.#options.clock);
701
596
  const cleanup = this.#trackCleanup((async () => {
702
- const expired = await this.#release(entry, deadline);
703
- if (expired) throw expired;
597
+ const expirationErrors = await this.#release(entry, deadline);
598
+ if (expirationErrors.length === 1) throw expirationErrors[0];
599
+ if (expirationErrors.length > 1) throw new AggregateError(expirationErrors, `Не удалось отключить плагин «${name}»`);
704
600
  })());
705
601
  try {
706
602
  await cleanup;
@@ -728,8 +624,7 @@ var PluginRegistry = class {
728
624
  const previous = await Promise.allSettled(previousCleanups);
729
625
  for (const result of previous) if (result.status === "rejected") errors.push(result.reason);
730
626
  for (const entry of entries) try {
731
- const expired = await this.#release(entry, deadline);
732
- if (expired) errors.push(expired);
627
+ errors.push(...await this.#release(entry, deadline));
733
628
  } catch (error) {
734
629
  errors.push(error);
735
630
  } finally {
@@ -755,13 +650,29 @@ var PluginRegistry = class {
755
650
  * @param execute выполнение логической операции, вызываемое самым внутренним transformer
756
651
  */
757
652
  async run(request, execute) {
758
- const entries = [...this.#ordered];
653
+ const ordered = this.#ordered;
654
+ if (ordered.length === 0) return execute(request);
655
+ const entries = [...ordered];
759
656
  for (const entry of entries) entry.activeRequests += 1;
760
657
  const interceptorScope = entries.flatMap((entry) => entry.interceptors.map((interceptor) => ({
761
658
  plugin: entry.plugin.name,
762
659
  interceptor
763
660
  })));
764
- const scoped = (current) => withAttemptInterceptorScope(current, interceptorScope);
661
+ const operationId = request.operationId;
662
+ const method = request.method.toUpperCase();
663
+ const retrySafety = request.retrySafety;
664
+ const scoped = (current) => {
665
+ const prepared = current.retrySafety === void 0 ? {
666
+ ...current,
667
+ retrySafety
668
+ } : current;
669
+ return interceptorScope.length === 0 ? prepared : withAttemptInterceptorScope(prepared, interceptorScope);
670
+ };
671
+ const validateContract = (prepared) => {
672
+ if (prepared.operationId !== operationId) throw new require_errors.ItdConfigError(`operation transformer не может заменить operationId «${operationId}» на «${prepared.operationId}»`);
673
+ if (prepared.method.toUpperCase() !== method) throw new require_errors.ItdConfigError(`operation transformer не может заменить метод «${method}» операции «${operationId}» на «${prepared.method}»`);
674
+ if (prepared.retrySafety !== void 0 && prepared.retrySafety !== retrySafety) throw new require_errors.ItdConfigError(`operation transformer не может заменить retrySafety операции «${operationId}»`);
675
+ };
765
676
  const chain = entries.flatMap((entry) => entry.transformers.map((transformer) => ({
766
677
  plugin: entry.plugin.name,
767
678
  transformer
@@ -770,9 +681,13 @@ var PluginRegistry = class {
770
681
  return transformer(scoped(current), (prepared) => {
771
682
  if (called) throw new require_errors.ItdConfigError(`operation transformer плагина «${plugin}» вызвал next() больше одного раза`);
772
683
  called = true;
684
+ validateContract(prepared);
773
685
  return next(scoped(prepared));
774
686
  });
775
- }, (current) => execute(scoped(current)));
687
+ }, (current) => {
688
+ validateContract(current);
689
+ return execute(scoped(current));
690
+ });
776
691
  try {
777
692
  return await chain(scoped(request));
778
693
  } finally {
@@ -789,14 +704,23 @@ var PluginRegistry = class {
789
704
  /**
790
705
  * Дожидается операций плагина и освобождает его ресурсы.
791
706
  *
792
- * `teardown` выполняется в любом случае, в том числе после истечения срока.
707
+ * Функция очистки запускается после ожидания активных операций. Обе стадии ограничены
708
+ * общим сроком.
793
709
  *
794
- * @returns ошибка истёкшего срока, если ждать пришлось дольше отведённого
710
+ * @returns ошибки истёкшего срока для незавершённых стадий
795
711
  */
796
712
  async #release(entry, deadline) {
797
- const finished = await deadline.wait(this.#waitForDrain(entry));
798
- await entry.teardown?.();
799
- return finished ? void 0 : new require_errors.ItdStateError(`плагин «${entry.plugin.name}» не завершил операции за ${this.#options.shutdownTimeout} мс; ожидание прекращено, ресурсы плагина освобождены`);
713
+ const errors = [];
714
+ if (!await deadline.wait(this.#waitForDrain(entry))) errors.push(new require_errors.ItdStateError(`плагин «${entry.plugin.name}» не завершил операции за ${this.#options.shutdownTimeout} мс`));
715
+ let teardownFailed = false;
716
+ let teardownFailure;
717
+ const teardown = Promise.resolve().then(() => entry.teardown?.()).catch((error) => {
718
+ teardownFailed = true;
719
+ teardownFailure = error;
720
+ });
721
+ if (!await deadline.wait(teardown)) errors.push(new require_errors.ItdStateError(`плагин «${entry.plugin.name}» не завершил teardown за ${this.#options.shutdownTimeout} мс`));
722
+ else if (teardownFailed) throw teardownFailure;
723
+ return errors;
800
724
  }
801
725
  #waitForDrain(entry) {
802
726
  if (entry.activeRequests === 0) return Promise.resolve();
@@ -821,148 +745,118 @@ function queueAbortError() {
821
745
  function queueStoppedError() {
822
746
  return new require_errors.ItdAbortError("Клиент закрыт, запрос отменён");
823
747
  }
748
+ /** Длина окна лимита на сервере. */
749
+ const RATE_LIMIT_WINDOW = 6e4;
824
750
  /**
825
- * Очередь запросов: ограничивает одновременность и частоту.
826
- *
827
- * Нужна прежде всего ботам: без неё цикл по сотне постов уходит в API одним залпом
828
- * и упирается в `RATE_LIMIT_EXCEEDED`.
751
+ * Пауза при исчерпании бакета неизвестной ёмкости.
829
752
  *
830
- * Частота выдерживается равномерным разносом стартов (`1000 / rps` между запросами),
831
- * а не окном со счётчиком: так нагрузка ровная, без всплеска в начале каждой секунды.
753
+ * Ёмкость приходит в заголовке вместе с остатком, поэтому случай возможен только у чужого
754
+ * прокси, который прислал `remaining` без `limit`.
755
+ */
756
+ const UNKNOWN_CAPACITY_PAUSE = 1e3;
757
+ /**
758
+ * Единый планировщик одного направления.
832
759
  *
833
- * @internal
760
+ * Он выбирает готовый бакет и только перед фактическим запуском атомарно занимает общий
761
+ * и локальный слоты. Одно ближайшее пробуждение обслуживает все бакеты направления.
834
762
  */
835
- var RequestQueue = class {
763
+ var DestinationScheduler = class {
836
764
  #concurrency;
837
- /** Минимальный промежуток между стартами, мс. `0` — без ограничения частоты. */
838
765
  #minGap;
839
- #onDispatch;
840
- #waiting = [];
766
+ #clock;
767
+ #buckets = [];
841
768
  #active = 0;
842
- /** Момент, раньше которого следующий запрос стартовать не должен. */
843
769
  #nextSlot = 0;
844
- #clock;
770
+ #cursor = 0;
845
771
  #cancelTimer;
846
- constructor(options, clock = systemClock) {
772
+ #wakeupAt;
773
+ constructor(options, clock) {
847
774
  this.#concurrency = options.concurrency;
848
775
  this.#minGap = options.rps ? 1e3 / options.rps : 0;
849
- this.#onDispatch = options.onDispatch;
850
776
  this.#clock = clock;
851
777
  }
852
- /** Сколько задач выполняется прямо сейчас. */
853
- get active() {
854
- return this.#active;
778
+ register(bucket) {
779
+ this.#buckets.push(bucket);
855
780
  }
856
- /** Сколько задач ждёт очереди. */
857
- get pending() {
858
- return this.#waiting.length;
781
+ /** Пересчитывает ближайший запуск после изменения очереди или ограничения. */
782
+ changed() {
783
+ this.#drain();
859
784
  }
860
- /**
861
- * Ставит задачу в очередь.
862
- *
863
- * @returns результат задачи; ошибка задачи пробрасывается без изменений
864
- */
865
- schedule(task, signal) {
866
- if (signal?.aborted) return Promise.reject(queueAbortError());
867
- return new Promise((resolve, reject) => {
868
- let queued;
869
- const detach = () => signal?.removeEventListener("abort", onAbort);
870
- const onAbort = () => {
871
- const index = this.#waiting.indexOf(queued);
872
- if (index < 0) return;
873
- this.#waiting.splice(index, 1);
874
- queued.cancel(queueAbortError());
875
- this.#drain();
876
- };
877
- queued = {
878
- run: () => {
879
- detach();
880
- this.#active += 1;
881
- this.#onDispatch?.();
882
- Promise.resolve().then(task).then(resolve, reject).finally(() => {
883
- this.#active -= 1;
884
- this.#drain();
885
- });
886
- },
887
- cancel: (reason) => {
888
- detach();
889
- reject(reason);
890
- }
891
- };
892
- this.#waiting.push(queued);
893
- signal?.addEventListener("abort", onAbort, { once: true });
894
- if (signal?.aborted) onAbort();
895
- else this.#drain();
896
- });
785
+ /** Перепланирует пробуждение после изменения времени готовности бакета. */
786
+ timingChanged() {
787
+ this.#cancelWakeup();
788
+ this.#drain();
897
789
  }
898
- /**
899
- * Останавливает очередь: снимает отложенную паузу и отклоняет ещё не начатые задачи
900
- * ошибкой `ItdAbortError`. Уже выполняющиеся задачи доводятся до конца.
901
- */
790
+ /** Отклоняет ожидающие задачи, сохраняя паузы и темп для повторного запуска клиента. */
902
791
  stop() {
903
- if (this.#cancelTimer) {
904
- this.#cancelTimer();
905
- this.#cancelTimer = void 0;
906
- }
907
- const pending = this.#waiting.splice(0, this.#waiting.length);
908
- for (const task of pending) task.cancel(queueStoppedError());
909
- }
910
- /** Придерживает очередь на заданное время: ждут все её задачи, а не только одна. */
911
- pause(ms) {
912
- if (ms <= 0) return;
913
- this.#nextSlot = Math.max(this.#nextSlot, this.#clock.now() + ms);
792
+ this.#cancelWakeup();
793
+ for (const bucket of this.#buckets) bucket.cancelWaiting(queueStoppedError(), false);
914
794
  }
915
- /** Запускает столько ожидающих задач, сколько позволяют ограничения. */
916
795
  #drain() {
917
- if (this.#waiting.length === 0) {
918
- if (this.#cancelTimer) {
919
- this.#cancelTimer();
920
- this.#cancelTimer = void 0;
921
- }
922
- return;
923
- }
924
796
  if (this.#active >= this.#concurrency) return;
925
- if (this.#cancelTimer) return;
926
797
  const now = this.#clock.now();
927
- if (this.#nextSlot > now) {
928
- this.#cancelTimer = this.#clock.schedule(() => {
929
- this.#cancelTimer = void 0;
930
- this.#drain();
931
- }, this.#nextSlot - now);
798
+ let selected;
799
+ let selectedIndex = -1;
800
+ let earliest = Number.POSITIVE_INFINITY;
801
+ for (let offset = 0; offset < this.#buckets.length; offset += 1) {
802
+ const index = (this.#cursor + offset) % this.#buckets.length;
803
+ const bucket = this.#buckets[index];
804
+ if (!bucket?.canStart) continue;
805
+ const readyAt = Math.max(this.#nextSlot, bucket.readyAt);
806
+ if (readyAt < earliest) earliest = readyAt;
807
+ if (readyAt <= now) {
808
+ selected = bucket;
809
+ selectedIndex = index;
810
+ break;
811
+ }
812
+ }
813
+ if (!selected) {
814
+ if (Number.isFinite(earliest)) this.#armWakeup(Math.max(0, earliest - now));
815
+ else this.#cancelWakeup();
932
816
  return;
933
817
  }
934
- const next = this.#waiting.shift();
935
- if (!next) return;
818
+ this.#cursor = (selectedIndex + 1) % this.#buckets.length;
819
+ this.#active += 1;
936
820
  if (this.#minGap > 0) this.#nextSlot = now + this.#minGap;
937
- next.run();
821
+ selected.dispatch(now, () => {
822
+ this.#active -= 1;
823
+ this.changed();
824
+ });
938
825
  this.#drain();
939
826
  }
827
+ #armWakeup(delay) {
828
+ const wakeupAt = this.#clock.now() + delay;
829
+ if (this.#cancelTimer && (this.#wakeupAt ?? Number.POSITIVE_INFINITY) <= wakeupAt) return;
830
+ this.#cancelWakeup();
831
+ this.#wakeupAt = wakeupAt;
832
+ this.#cancelTimer = this.#clock.schedule(() => {
833
+ this.#cancelTimer = void 0;
834
+ this.#wakeupAt = void 0;
835
+ this.#drain();
836
+ }, delay);
837
+ }
838
+ #cancelWakeup() {
839
+ this.#cancelTimer?.();
840
+ this.#cancelTimer = void 0;
841
+ this.#wakeupAt = void 0;
842
+ }
940
843
  };
941
- /** Длина окна лимита на сервере. */
942
- const RATE_LIMIT_WINDOW = 6e4;
943
- /**
944
- * Пауза при исчерпании бакета неизвестной ёмкости.
945
- *
946
- * Ёмкость приходит в заголовке вместе с остатком, поэтому случай возможен только у чужого
947
- * прокси, который прислал `remaining` без `limit`.
948
- */
949
- const UNKNOWN_CAPACITY_PAUSE = 1e3;
950
844
  /**
951
- * Очередь одного бакета поверх общей очереди направления.
845
+ * Состояние и очередь одного серверного бакета.
952
846
  *
953
- * Задача занимает слот бакета, затем общий слот направления, поэтому суммарная
954
- * одновременность остаётся равной `concurrency`. Пауза бакета удерживает задачу до
955
- * захвата общего слота: притормозивший счётчик не занимает общую ёмкость.
847
+ * Класс хранит локальную конкурентность, темп и серверную квоту, а решение о старте
848
+ * делегирует общему планировщику направления.
956
849
  *
957
850
  * @internal
958
851
  */
959
852
  var BucketQueue = class {
960
853
  #destination;
961
854
  #bucket;
962
- #gate;
963
- #shared;
855
+ #scheduler;
964
856
  #clock;
965
857
  #pacing;
858
+ #concurrency;
859
+ #minGap;
966
860
  /** Ровный темп. Требует раздельных бакетов: без них ёмкость счётчика неизвестна. */
967
861
  #smooth;
968
862
  /**
@@ -979,6 +873,9 @@ var BucketQueue = class {
979
873
  /** Последнее, что сказал сервер. Живёт и в режиме `off` — ради `rateLimitState()`. */
980
874
  #limit;
981
875
  #remaining;
876
+ #active = 0;
877
+ #nextSlot = 0;
878
+ #waiting = [];
982
879
  /**
983
880
  * Оценка остатка для режима `smooth`.
984
881
  *
@@ -987,24 +884,19 @@ var BucketQueue = class {
987
884
  */
988
885
  #tokens = 1;
989
886
  #tokensAt;
990
- /**
991
- * Номер поколения очереди. `stop()` увеличивает его, отсекая задачи, которые уже взяли
992
- * слот бакета, но до общей очереди ещё не дошли.
993
- */
994
- #generation = 0;
995
- constructor(destination, bucket, shared, options, clock, featureDefinition) {
887
+ constructor(destination, bucket, scheduler, options, clock, featureDefinition) {
996
888
  this.#destination = destination;
997
889
  this.#bucket = bucket;
998
- this.#shared = shared;
890
+ this.#scheduler = scheduler;
999
891
  this.#clock = clock;
1000
892
  this.#pacing = options.pacing;
1001
893
  this.#smooth = options.buckets && options.pacing === RateLimitPacing.Smooth;
1002
894
  this.#flatPause = options.buckets ? void 0 : options.retryDelays[0] ?? 0;
1003
895
  this.#seedLimit = options.buckets ? featureDefinition?.limit ?? seedLimit(bucket, options) : void 0;
1004
- this.#gate = new RequestQueue({
1005
- concurrency: options.buckets ? featureDefinition?.concurrency ?? options.bucketOverrides[bucket]?.concurrency ?? options.bucketConcurrency : options.concurrency,
1006
- onDispatch: this.#smooth ? () => this.#spend() : void 0
1007
- }, clock);
896
+ this.#concurrency = options.buckets ? featureDefinition?.concurrency ?? options.bucketOverrides[bucket]?.concurrency ?? options.bucketConcurrency : options.concurrency;
897
+ const rps = options.buckets ? featureDefinition?.rps ?? options.bucketOverrides[bucket]?.rps : void 0;
898
+ this.#minGap = rps ? 1e3 / rps : 0;
899
+ scheduler.register(this);
1008
900
  }
1009
901
  /** Имя счётчика. При `buckets: false` — всегда `default`, каким бы ни был запрос. */
1010
902
  get bucket() {
@@ -1012,19 +904,63 @@ var BucketQueue = class {
1012
904
  }
1013
905
  /** Запросов бакета прошло в общую очередь и ещё не завершилось. */
1014
906
  get active() {
1015
- return this.#gate.active;
907
+ return this.#active;
1016
908
  }
1017
909
  /** Запросов бакета ждёт своей очереди. */
1018
910
  get pending() {
1019
- return this.#gate.pending;
911
+ return this.#waiting.length;
1020
912
  }
1021
- /** Ставит запрос в очередь: сначала слот бакета, затем общий слот направления. */
913
+ /** Есть ли ожидающая задача, для которой свободен локальный слот. */
914
+ get canStart() {
915
+ return this.#waiting.length > 0 && this.#active < this.#concurrency;
916
+ }
917
+ /** Момент, раньше которого локальное ограничение не разрешает следующий старт. */
918
+ get readyAt() {
919
+ return this.#nextSlot;
920
+ }
921
+ /** Ставит запрос в очередь единого планировщика направления. */
1022
922
  schedule(task, signal) {
1023
- const generation = this.#generation;
1024
- return this.#gate.schedule(() => {
1025
- if (generation !== this.#generation) return Promise.reject(queueStoppedError());
1026
- return this.#shared.schedule(task, signal);
1027
- }, signal);
923
+ if (signal?.aborted) return Promise.reject(queueAbortError());
924
+ return new Promise((resolve, reject) => {
925
+ let queued;
926
+ const detach = () => signal?.removeEventListener("abort", onAbort);
927
+ const onAbort = () => {
928
+ const index = this.#waiting.indexOf(queued);
929
+ if (index < 0) return;
930
+ this.#waiting.splice(index, 1);
931
+ queued.cancel(queueAbortError());
932
+ this.#scheduler.timingChanged();
933
+ };
934
+ queued = {
935
+ start: (complete) => {
936
+ detach();
937
+ Promise.resolve().then(task).then(resolve, reject).finally(complete);
938
+ },
939
+ cancel: (reason) => {
940
+ detach();
941
+ reject(reason);
942
+ }
943
+ };
944
+ this.#waiting.push(queued);
945
+ signal?.addEventListener("abort", onAbort, { once: true });
946
+ if (signal?.aborted) onAbort();
947
+ else this.#scheduler.changed();
948
+ });
949
+ }
950
+ /** Запускает первую задачу; вызывается только планировщиком направления. */
951
+ dispatch(now, complete) {
952
+ const task = this.#waiting.shift();
953
+ if (!task) {
954
+ complete();
955
+ return;
956
+ }
957
+ this.#active += 1;
958
+ if (this.#smooth) this.#spend();
959
+ if (this.#minGap > 0) this.#nextSlot = Math.max(this.#nextSlot, now + this.#minGap);
960
+ task.start(() => {
961
+ this.#active -= 1;
962
+ complete();
963
+ });
1028
964
  }
1029
965
  /**
1030
966
  * Учитывает заголовки ответа.
@@ -1040,7 +976,7 @@ var BucketQueue = class {
1040
976
  if (this.#pacing === RateLimitPacing.Off || remaining === void 0) return 0;
1041
977
  if (this.#flatPause !== void 0) {
1042
978
  if (remaining > 0) return 0;
1043
- this.#gate.pause(this.#flatPause);
979
+ this.#hold(this.#flatPause);
1044
980
  return this.#flatPause;
1045
981
  }
1046
982
  const capacity = this.#capacity();
@@ -1048,11 +984,13 @@ var BucketQueue = class {
1048
984
  if (capacity === void 0) return 0;
1049
985
  this.#refill(capacity);
1050
986
  if (remaining < this.#tokens) this.#tokens = remaining;
1051
- return this.#armPause(capacity);
987
+ const wait = this.#armPause(capacity);
988
+ this.#scheduler.timingChanged();
989
+ return wait;
1052
990
  }
1053
991
  if (remaining > 0) return 0;
1054
992
  const wait = capacity === void 0 ? UNKNOWN_CAPACITY_PAUSE : Math.ceil(RATE_LIMIT_WINDOW / Math.max(capacity, 1));
1055
- this.#gate.pause(wait);
993
+ this.#hold(wait);
1056
994
  return wait;
1057
995
  }
1058
996
  /** Придерживает бакет на названное время — путь ответа `429`. Оценка остатка обнуляется. */
@@ -1061,7 +999,7 @@ var BucketQueue = class {
1061
999
  this.#tokens = 0;
1062
1000
  this.#tokensAt = this.#clock.now();
1063
1001
  }
1064
- this.#gate.pause(ms);
1002
+ this.#hold(ms);
1065
1003
  }
1066
1004
  /** Снимок для `rateLimitState()`. */
1067
1005
  state() {
@@ -1070,17 +1008,19 @@ var BucketQueue = class {
1070
1008
  bucket: this.#bucket,
1071
1009
  limit: this.#limit,
1072
1010
  remaining: this.#remaining,
1073
- active: this.#gate.active,
1074
- pending: this.#gate.pending
1011
+ active: this.#active,
1012
+ pending: this.#waiting.length
1075
1013
  };
1076
1014
  }
1077
- /**
1078
- * Останавливает уровень бакета. Общая очередь направления гасится пулом.
1079
- *
1080
- */
1015
+ /** Отклоняет ещё не начатые задачи этого бакета. */
1081
1016
  stop() {
1082
- this.#generation += 1;
1083
- this.#gate.stop();
1017
+ this.cancelWaiting(queueStoppedError());
1018
+ }
1019
+ /** Отклоняет ожидающие задачи; используется также при остановке всего направления. */
1020
+ cancelWaiting(reason, notify = true) {
1021
+ const pending = this.#waiting.splice(0, this.#waiting.length);
1022
+ for (const task of pending) task.cancel(reason);
1023
+ if (notify) this.#scheduler.timingChanged();
1084
1024
  }
1085
1025
  /** Лимит бакета: сказанный сервером, иначе табличный. */
1086
1026
  #capacity() {
@@ -1107,9 +1047,15 @@ var BucketQueue = class {
1107
1047
  #armPause(capacity) {
1108
1048
  if (this.#tokens >= 1) return 0;
1109
1049
  const wait = Math.ceil((1 - this.#tokens) * RATE_LIMIT_WINDOW / capacity);
1110
- this.#gate.pause(wait);
1050
+ this.#nextSlot = Math.max(this.#nextSlot, this.#clock.now() + wait);
1111
1051
  return wait;
1112
1052
  }
1053
+ /** Отодвигает ближайший допустимый старт и перепланирует общее пробуждение. */
1054
+ #hold(ms) {
1055
+ if (ms <= 0) return;
1056
+ this.#nextSlot = Math.max(this.#nextSlot, this.#clock.now() + ms);
1057
+ this.#scheduler.timingChanged();
1058
+ }
1113
1059
  };
1114
1060
  /** Лимит бакета до первого ответа: поправка пользователя важнее табличного значения. */
1115
1061
  function seedLimit(bucket, options) {
@@ -1146,11 +1092,12 @@ var RequestQueuePool = class {
1146
1092
  defineBucket(name, definition) {
1147
1093
  const normalized = Object.freeze({
1148
1094
  ...definition.limit === void 0 ? {} : { limit: definition.limit },
1149
- ...definition.concurrency === void 0 ? {} : { concurrency: definition.concurrency }
1095
+ ...definition.concurrency === void 0 ? {} : { concurrency: definition.concurrency },
1096
+ ...definition.rps === void 0 ? {} : { rps: definition.rps }
1150
1097
  });
1151
1098
  const existing = this.#featureBuckets.get(name);
1152
1099
  if (existing) {
1153
- if (existing.definition.limit !== normalized.limit || existing.definition.concurrency !== normalized.concurrency) throw new require_errors.ItdConfigError(`Бакет feature «${name}» уже зарегистрирован с другими ограничениями`);
1100
+ if (existing.definition.limit !== normalized.limit || existing.definition.concurrency !== normalized.concurrency || existing.definition.rps !== normalized.rps) throw new require_errors.ItdConfigError(`Бакет feature «${name}» уже зарегистрирован с другими ограничениями`);
1154
1101
  existing.references += 1;
1155
1102
  } else this.#featureBuckets.set(name, {
1156
1103
  definition: normalized,
@@ -1174,14 +1121,14 @@ var RequestQueuePool = class {
1174
1121
  let entry = this.#destinations.get(destination);
1175
1122
  if (!entry) {
1176
1123
  entry = {
1177
- shared: new RequestQueue(this.#options, this.#clock),
1124
+ scheduler: new DestinationScheduler(this.#options, this.#clock),
1178
1125
  buckets: /* @__PURE__ */ new Map()
1179
1126
  };
1180
1127
  this.#destinations.set(destination, entry);
1181
1128
  }
1182
1129
  let queue = entry.buckets.get(name);
1183
1130
  if (!queue) {
1184
- queue = new BucketQueue(destination, name, entry.shared, this.#options, this.#clock, this.#featureBuckets.get(name)?.definition);
1131
+ queue = new BucketQueue(destination, name, entry.scheduler, this.#options, this.#clock, this.#featureBuckets.get(name)?.definition);
1185
1132
  entry.buckets.set(name, queue);
1186
1133
  }
1187
1134
  return queue;
@@ -1193,14 +1140,11 @@ var RequestQueuePool = class {
1193
1140
  return states;
1194
1141
  }
1195
1142
  /**
1196
- * Останавливает оба уровня всех очередей: ожидающие задачи отклоняются, а состояние
1197
- * счётчиков и отложенные паузы сохраняются до следующего запуска — путь `close()`.
1143
+ * Останавливает планировщики: ожидающие задачи отклоняются, а состояние счётчиков
1144
+ * и отложенные паузы сохраняются до следующего запуска — путь `close()`.
1198
1145
  */
1199
1146
  stop() {
1200
- for (const entry of this.#destinations.values()) {
1201
- for (const queue of entry.buckets.values()) queue.stop();
1202
- entry.shared.stop();
1203
- }
1147
+ for (const entry of this.#destinations.values()) entry.scheduler.stop();
1204
1148
  }
1205
1149
  /** Останавливает очереди и забывает всё, что известно о счётчиках, — путь `dispose()`. */
1206
1150
  clear() {
@@ -1240,7 +1184,7 @@ var ServiceRegistry = class {
1240
1184
  #primaryHost;
1241
1185
  /** @param primaryBaseUrl базовый URL клиента */
1242
1186
  constructor(primaryBaseUrl) {
1243
- this.#primaryHost = primaryBaseUrl ? require_cookies.hostOf(primaryBaseUrl) : "";
1187
+ this.#primaryHost = primaryBaseUrl ? require_redact.hostOf(primaryBaseUrl) : "";
1244
1188
  }
1245
1189
  /**
1246
1190
  * Регистрирует сервис. Имя очищается от краевых пробелов, базовый URL приводится
@@ -1253,7 +1197,7 @@ var ServiceRegistry = class {
1253
1197
  if (typeof raw !== "string" || raw.trim() === "") throw new require_errors.ItdConfigError("У сервиса должно быть непустое имя");
1254
1198
  const name = raw.trim();
1255
1199
  if (this.#services.has(name)) throw new require_errors.ItdConfigError(`Сервис «${name}» уже зарегистрирован и не может быть заменён`);
1256
- const baseUrl = require_cookies.normalizeBaseUrl(definition.baseUrl);
1200
+ const baseUrl = require_redact.normalizeBaseUrl(definition.baseUrl);
1257
1201
  if (definition.auth !== void 0 && typeof definition.auth !== "boolean") throw new require_errors.ItdConfigError(`services.${name}.auth должен быть boolean`);
1258
1202
  let headers;
1259
1203
  if (definition.headers !== void 0) {
@@ -1265,7 +1209,7 @@ var ServiceRegistry = class {
1265
1209
  ...definition,
1266
1210
  name,
1267
1211
  baseUrl,
1268
- auth: definition.auth ?? require_cookies.isSameSite(this.#primaryHost, require_cookies.hostOf(baseUrl)),
1212
+ auth: definition.auth ?? require_redact.isSameSite(this.#primaryHost, require_redact.hostOf(baseUrl)),
1269
1213
  ...headers ? { headers } : {}
1270
1214
  };
1271
1215
  this.#services.set(name, Object.freeze(normalized));
@@ -1330,7 +1274,7 @@ var ServiceRegistry = class {
1330
1274
  * @internal
1331
1275
  */
1332
1276
  isPrimarySite(baseUrl) {
1333
- return require_cookies.isSameSite(this.#primaryHost, require_cookies.hostOf(baseUrl));
1277
+ return require_redact.isSameSite(this.#primaryHost, require_redact.hostOf(baseUrl));
1334
1278
  }
1335
1279
  };
1336
1280
  //#endregion
@@ -1392,14 +1336,14 @@ function markDisposeCleanupRequest(request) {
1392
1336
  [DISPOSE_CLEANUP_REQUEST]: true
1393
1337
  };
1394
1338
  }
1395
- /** Разрешено ли запросу завершать внутреннюю очистку после перехода клиента в terminal state. @internal */
1339
+ /** Разрешено ли запросу завершать внутреннюю очистку после `dispose()`. @internal */
1396
1340
  function isDisposeCleanupRequest(request) {
1397
1341
  return request[DISPOSE_CLEANUP_REQUEST] === true;
1398
1342
  }
1399
1343
  /**
1400
1344
  * Собирает слои в один обработчик.
1401
1345
  *
1402
- * Первый слой оказывается самым внешним. Порядок задаётся внутренним client runtime.
1346
+ * Первый слой становится внешним.
1403
1347
  *
1404
1348
  * @example
1405
1349
  * ```ts
@@ -1427,24 +1371,17 @@ function withLayerHeaders(request, headers) {
1427
1371
  /**
1428
1372
  * Точка входа ресурсов в конвейер запросов.
1429
1373
  *
1430
- * Принимает готовый обработчик цепочку слоёв поверх транспорта, собранную
1431
- * во внутреннем runtime клиента, — и отдаёт ресурсам методы `request`/`operation`.
1374
+ * Принимает готовую цепочку обработки и предоставляет ресурсам методы `request`/`execute`.
1432
1375
  * О слоях и их порядке ресурсы не знают.
1433
1376
  */
1434
1377
  var HttpClient = class {
1435
1378
  #handler;
1379
+ #plugins;
1436
1380
  #baseUrl;
1437
- #catalog;
1438
1381
  constructor(deps) {
1439
1382
  this.#handler = deps.handler;
1383
+ this.#plugins = deps.plugins;
1440
1384
  this.#baseUrl = deps.baseUrl;
1441
- this.#catalog = deps.catalog;
1442
- }
1443
- /** Метод операции из каталога. Отсутствие описания — ошибка сборки клиента, не запроса. */
1444
- #methodOf(operationId) {
1445
- const method = this.#catalog.methodOf(operationId);
1446
- if (method === void 0) throw new require_errors.ItdConfigError(`каталог операций не знает «${operationId}»`);
1447
- return method;
1448
1385
  }
1449
1386
  /** Базовый URL, к которому обращается клиент. */
1450
1387
  get baseUrl() {
@@ -1460,23 +1397,34 @@ var HttpClient = class {
1460
1397
  * @throws {ItdNetworkError} если запрос не дошёл до сервера
1461
1398
  */
1462
1399
  request(options) {
1463
- return this.#handler(identifyRequest(options));
1400
+ const request = identifyRequest(options);
1401
+ return this.#plugins.run(request, (prepared) => this.#handler(prepared));
1464
1402
  }
1465
- /** Выполняет встроенную семантическую операцию, подставляя её HTTP-метод из каталога. */
1466
- operation(operationId, options) {
1467
- return this.request({
1403
+ /** Выполняет контракт операции; `next()` плагина возвращает результат после `read`. */
1404
+ execute(operation, options) {
1405
+ return this.#run(operation, options);
1406
+ }
1407
+ #run(operation, options) {
1408
+ const request = identifyRequest({
1468
1409
  ...options,
1469
- operationId,
1470
- method: this.#methodOf(operationId)
1410
+ operationId: operation.id,
1411
+ method: operation.method,
1412
+ retrySafety: options.retrySafety ?? operation.retrySafety
1471
1413
  });
1414
+ return this.#plugins.run(request, async (prepared) => operation.read(await this.#handler(prepared), prepared));
1472
1415
  }
1473
1416
  /** Выполняет внутреннюю операцию финализации после начала `ItdClient.dispose()`. @internal */
1474
- cleanupOperation(operationId, options) {
1475
- return this.request(markDisposeCleanupRequest({
1417
+ cleanupOperation(operation, options) {
1418
+ const request = identifyRequest({
1476
1419
  ...options,
1477
- operationId,
1478
- method: this.#methodOf(operationId)
1479
- }));
1420
+ operationId: operation.id,
1421
+ method: operation.method,
1422
+ retrySafety: options.retrySafety ?? operation.retrySafety
1423
+ });
1424
+ return this.#plugins.run(request, async (prepared) => {
1425
+ const cleanupRequest = markDisposeCleanupRequest(prepared);
1426
+ return operation.read(await this.#handler(cleanupRequest), prepared);
1427
+ });
1480
1428
  }
1481
1429
  };
1482
1430
  //#endregion
@@ -1501,6 +1449,50 @@ const RetrySafety = Object.freeze({
1501
1449
  /** Повтор может создать ещё один побочный эффект. */
1502
1450
  Unsafe: "unsafe"
1503
1451
  });
1452
+ function snapshotAnnotation(value, operationId, copies) {
1453
+ if (value === null || typeof value !== "object" && typeof value !== "function") return value;
1454
+ if (typeof value === "function") throw new require_errors.ItdConfigError(`annotations операции «${operationId}» не должны содержать функции`);
1455
+ const existing = copies.get(value);
1456
+ if (existing !== void 0) return existing;
1457
+ if (Array.isArray(value)) {
1458
+ const copy = [];
1459
+ copies.set(value, copy);
1460
+ for (const item of value) copy.push(snapshotAnnotation(item, operationId, copies));
1461
+ return Object.freeze(copy);
1462
+ }
1463
+ const prototype = Object.getPrototypeOf(value);
1464
+ if (prototype !== Object.prototype && prototype !== null) throw new require_errors.ItdConfigError(`annotations операции «${operationId}» должны состоять из обычных объектов, массивов и примитивов`);
1465
+ const copy = Object.create(prototype);
1466
+ copies.set(value, copy);
1467
+ for (const key of Reflect.ownKeys(value)) {
1468
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
1469
+ if (!descriptor?.enumerable) continue;
1470
+ if (!("value" in descriptor)) throw new require_errors.ItdConfigError(`annotations операции «${operationId}» не должны содержать getters или setters`);
1471
+ Object.defineProperty(copy, key, {
1472
+ value: snapshotAnnotation(descriptor.value, operationId, copies),
1473
+ enumerable: true,
1474
+ writable: true,
1475
+ configurable: true
1476
+ });
1477
+ }
1478
+ return Object.freeze(copy);
1479
+ }
1480
+ /** Создаёт неизменяемый контракт результата. @internal */
1481
+ function defineOperation(id, metadata, read) {
1482
+ const annotations = metadata.annotations === void 0 ? void 0 : snapshotAnnotation(metadata.annotations, id, /* @__PURE__ */ new Map());
1483
+ return Object.freeze({
1484
+ id,
1485
+ ...metadata,
1486
+ ...annotations === void 0 ? {} : { annotations },
1487
+ read
1488
+ });
1489
+ }
1490
+ /** Возвращает разобранное тело без дополнительного преобразования. @internal */
1491
+ function identityResult(body) {
1492
+ return body;
1493
+ }
1494
+ /** Отбрасывает служебное тело операции без результата. @internal */
1495
+ function voidResult() {}
1504
1496
  //#endregion
1505
1497
  //#region src/core/scheduling/retry.ts
1506
1498
  /** Методы raw-запроса, безопасность которых гарантирована самим HTTP-контрактом. */
@@ -1610,9 +1602,6 @@ function createQueueMiddleware(schedule) {
1610
1602
  * разу, иначе, например, текст поста зашифруется дважды. Здесь же к операции привязывается
1611
1603
  * snapshot attempt interceptors; сами они выполняются транспортом на каждой попытке.
1612
1604
  */
1613
- function createPluginsMiddleware(plugins) {
1614
- return (request, next) => plugins.run(request, next);
1615
- }
1616
1605
  /**
1617
1606
  * Слой сервисов.
1618
1607
  *
@@ -1626,7 +1615,7 @@ function createServicesMiddleware(registry) {
1626
1615
  const service = request.service === void 0 ? void 0 : registry.require(request.service);
1627
1616
  let prepared = request;
1628
1617
  if (request.baseUrl !== void 0) {
1629
- const baseUrl = require_cookies.normalizeBaseUrl(request.baseUrl);
1618
+ const baseUrl = require_redact.normalizeBaseUrl(request.baseUrl);
1630
1619
  if (baseUrl !== request.baseUrl) prepared = {
1631
1620
  ...prepared,
1632
1621
  baseUrl
@@ -1770,90 +1759,6 @@ function createRetryMiddleware(deps) {
1770
1759
  };
1771
1760
  }
1772
1761
  //#endregion
1773
- //#region src/core/redact.ts
1774
- /** Заголовки, значение которых нельзя писать в лог целиком. */
1775
- const SECRET_HEADERS = /* @__PURE__ */ new Set([
1776
- "authorization",
1777
- "cookie",
1778
- "set-cookie",
1779
- "x-api-key"
1780
- ]);
1781
- /** Поля тела запроса, значение которых нельзя писать в лог. */
1782
- const SECRET_FIELDS = /* @__PURE__ */ new Set([
1783
- "password",
1784
- "oldpassword",
1785
- "newpassword",
1786
- "accesstoken",
1787
- "refreshtoken",
1788
- "currentpassword",
1789
- "flowtoken",
1790
- "token",
1791
- "turnstiletoken",
1792
- "otp"
1793
- ]);
1794
- /** Query-параметры, содержащие секреты. */
1795
- const SECRET_QUERY_PARAMS = /* @__PURE__ */ new Set(["token", "access_token"]);
1796
- /**
1797
- * Прячет середину секрета, оставляя концы для сопоставления.
1798
- *
1799
- * @example
1800
- * ```ts
1801
- * maskSecret('eyJhbGciOiJIUzI1NiJ9.abc'); // 'eyJh…(24)…abc'
1802
- * ```
1803
- */
1804
- function maskSecret(value) {
1805
- if (value.length <= 8) return "…";
1806
- return `${value.slice(0, 4)}…(${value.length})…${value.slice(-3)}`;
1807
- }
1808
- /** Маскирует секретные query-параметры URL перед записью в логи и ошибки. */
1809
- function redactUrl(url) {
1810
- try {
1811
- const parsed = new URL(url);
1812
- const entries = [...parsed.searchParams.entries()];
1813
- parsed.search = "";
1814
- for (const [name, value] of entries) parsed.searchParams.append(name, SECRET_QUERY_PARAMS.has(name.toLowerCase()) ? maskSecret(value) : value);
1815
- return parsed.toString();
1816
- } catch {
1817
- return url.replace(/([?&](?:token|access_token)=)[^&#]*/gi, "$1[скрыто]");
1818
- }
1819
- }
1820
- /**
1821
- * Готовит заголовки к записи в лог.
1822
- *
1823
- * `Authorization` и `Cookie` маскируются: логи часто уезжают в системы сбора,
1824
- * и токен доступа в них попадать не должен.
1825
- */
1826
- function redactHeaders(headers) {
1827
- const result = {};
1828
- headers.forEach((value, name) => {
1829
- if (SECRET_HEADERS.has(name.toLowerCase())) {
1830
- const spaceAt = value.indexOf(" ");
1831
- result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
1832
- return;
1833
- }
1834
- result[name] = value;
1835
- });
1836
- return result;
1837
- }
1838
- /**
1839
- * Готовит тело запроса к записи в лог: пароли, токены и коды OTP заменяются заглушкой.
1840
- *
1841
- * Обходит вложенные объекты и массивы. `FormData` и бинарные тела не раскрываются вовсе.
1842
- */
1843
- function redactBody(body) {
1844
- if (body === null || body === void 0) return body;
1845
- if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
1846
- if (isBlob(body)) return "[Blob]";
1847
- if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
1848
- if (Array.isArray(body)) return body.map(redactBody);
1849
- if (typeof body === "object") {
1850
- const result = {};
1851
- for (const [key, value] of Object.entries(body)) result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[скрыто]" : redactBody(value);
1852
- return result;
1853
- }
1854
- return body;
1855
- }
1856
- //#endregion
1857
1762
  //#region src/core/unwrap.ts
1858
1763
  /**
1859
1764
  * Снимает внешнюю обёртку `{ data: … }`.
@@ -2102,7 +2007,7 @@ function safeRawBody(body) {
2102
2007
  if (!isRecord(body) || body.type !== "validation" || !isRecord(body.found)) return body;
2103
2008
  return {
2104
2009
  ...body,
2105
- found: redactBody(body.found)
2010
+ found: require_redact.redactBody(body.found)
2106
2011
  };
2107
2012
  }
2108
2013
  /**
@@ -2149,7 +2054,7 @@ function setHeader(headers, name, value) {
2149
2054
  /** Тело, которое отправляется как есть, без сериализации в JSON. */
2150
2055
  function isRawBody(body) {
2151
2056
  if (typeof body !== "object" || body === null) return typeof body === "string";
2152
- return typeof FormData !== "undefined" && body instanceof FormData || isBlob(body) || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body instanceof ArrayBuffer || ArrayBuffer.isView(body);
2057
+ return typeof FormData !== "undefined" && body instanceof FormData || require_redact.isBlob(body) || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body instanceof ArrayBuffer || ArrayBuffer.isView(body);
2153
2058
  }
2154
2059
  /**
2155
2060
  * Читает тело ответа один раз.
@@ -2295,8 +2200,8 @@ var Transport = class {
2295
2200
  };
2296
2201
  await dispatchRequestHook(this.#config.hooks, "onRequest", context);
2297
2202
  this.#config.logger?.debug(`→ ${method} ${request.path}`, {
2298
- headers: redactHeaders(headers),
2299
- body: request.bodyFactory ? "[повторяемое тело]" : redactBody(request.body)
2203
+ headers: require_redact.redactHeaders(headers),
2204
+ body: request.bodyFactory ? "[повторяемое тело]" : require_redact.redactBody(request.body)
2300
2205
  });
2301
2206
  let response;
2302
2207
  try {
@@ -2438,7 +2343,7 @@ var Transport = class {
2438
2343
  */
2439
2344
  buildUrl(request) {
2440
2345
  const base = request.baseUrl ?? this.#config.baseUrl;
2441
- return require_cookies.joinUrl(base, request.path) + require_cookies.buildQuery(request.query);
2346
+ return require_redact.joinUrl(base, request.path) + require_redact.buildQuery(request.query);
2442
2347
  }
2443
2348
  /**
2444
2349
  * Собирает общие заголовки клиента: `User-Agent`, идентификатор устройства,
@@ -2489,7 +2394,7 @@ var Transport = class {
2489
2394
  };
2490
2395
  //#endregion
2491
2396
  //#region src/core/execution/client-runtime.ts
2492
- /** Имена стадий основного request pipeline в порядке выполнения. @internal */
2397
+ /** Имена стадий обработки запроса в порядке выполнения. @internal */
2493
2398
  const ClientRuntimeStage = Object.freeze({
2494
2399
  OperationPlugins: "operation_plugins",
2495
2400
  Services: "services",
@@ -2507,14 +2412,15 @@ function createServiceRegistry(config) {
2507
2412
  for (const service of config.services) services.define(service);
2508
2413
  return services;
2509
2414
  }
2510
- /** Собирает внутренний runtime клиента и единственный request pipeline. @internal */
2415
+ /** Собирает внутренние компоненты и цепочку запросов клиента. @internal */
2511
2416
  function createClientRuntime(options, internals) {
2512
2417
  const catalog = internals.catalog;
2513
2418
  const config = resolveRuntimeConfig(options, catalog);
2514
- const jar = new require_cookies.CookieJar();
2419
+ const jar = new require_redact.CookieJar();
2515
2420
  const plugins = new PluginRegistry({
2516
2421
  shutdownTimeout: config.shutdownTimeout,
2517
- clock: config.clock
2422
+ clock: config.clock,
2423
+ operationMetadata: (operationId) => catalog.definitionOf(operationId)
2518
2424
  });
2519
2425
  const services = createServiceRegistry(config);
2520
2426
  const lifetime = new AbortController();
@@ -2538,7 +2444,7 @@ function createClientRuntime(options, internals) {
2538
2444
  }) ?? catalog.bucketOf(request.operationId);
2539
2445
  };
2540
2446
  const queueKeyFor = (request) => requestQueueKey(request, (target) => ({
2541
- destination: require_cookies.originOf(transport.buildUrl(target)) || void 0,
2447
+ destination: require_redact.originOf(transport.buildUrl(target)) || void 0,
2542
2448
  bucket: bucketFor(target)
2543
2449
  }));
2544
2450
  const queueFor = (request) => {
@@ -2563,10 +2469,6 @@ function createClientRuntime(options, internals) {
2563
2469
  lifetimeSignal: lifetime.signal
2564
2470
  });
2565
2471
  const stages = [
2566
- {
2567
- name: ClientRuntimeStage.OperationPlugins,
2568
- middleware: createPluginsMiddleware(plugins)
2569
- },
2570
2472
  {
2571
2473
  name: ClientRuntimeStage.Services,
2572
2474
  middleware: createServicesMiddleware(services)
@@ -2618,17 +2520,21 @@ function createClientRuntime(options, internals) {
2618
2520
  }
2619
2521
  return handler(request);
2620
2522
  };
2523
+ const http = new HttpClient({
2524
+ handler: clientHandler,
2525
+ plugins,
2526
+ baseUrl: config.baseUrl
2527
+ });
2621
2528
  auth = internals.auth({
2622
2529
  config,
2623
- handler: clientHandler,
2530
+ http,
2624
2531
  cookies: jar
2625
2532
  });
2626
- const http = new HttpClient({
2627
- handler: clientHandler,
2628
- baseUrl: config.baseUrl,
2629
- catalog
2630
- });
2631
- const stageOrder = Object.freeze([...stages.map(({ name }) => name), ClientRuntimeStage.Transport]);
2533
+ const stageOrder = Object.freeze([
2534
+ ClientRuntimeStage.OperationPlugins,
2535
+ ...stages.map(({ name }) => name),
2536
+ ClientRuntimeStage.Transport
2537
+ ]);
2632
2538
  return {
2633
2539
  config,
2634
2540
  http,
@@ -2637,6 +2543,24 @@ function createClientRuntime(options, internals) {
2637
2543
  plugins,
2638
2544
  services,
2639
2545
  stageOrder,
2546
+ connection: (serviceName) => {
2547
+ const service = serviceName === void 0 ? void 0 : services.require(serviceName);
2548
+ const baseUrl = service?.baseUrl ?? config.baseUrl;
2549
+ return Object.freeze({
2550
+ baseUrl,
2551
+ authorize: service?.auth ?? true,
2552
+ fetch: config.fetch,
2553
+ clock: config.clock,
2554
+ logger: config.logger,
2555
+ baseHeaders: async (url) => {
2556
+ const headers = await transport.platformHeaders(url);
2557
+ for (const [name, value] of Object.entries(service?.headers ?? {})) headers.set(name, value);
2558
+ return headers;
2559
+ },
2560
+ getToken: () => auth.token(),
2561
+ refreshAuth: () => auth.recover()
2562
+ });
2563
+ },
2640
2564
  platformHeaders: (url) => transport.platformHeaders(url),
2641
2565
  rateLimitState: () => queues?.states() ?? [],
2642
2566
  registerRateLimitBucket: (name, definition) => queues?.defineBucket(name, definition),
@@ -2684,7 +2608,16 @@ const BUCKET_LIMITS = Object.freeze({
2684
2608
  "posts.create": 5,
2685
2609
  "users.updateMe": 3,
2686
2610
  "reports.create": 3,
2687
- "verification.submit": 3
2611
+ "verification.submit": 3,
2612
+ shop: 150,
2613
+ "shop.delivery.cities": 60,
2614
+ "shop.delivery.calculate": 45,
2615
+ "shop.delivery.points": 30,
2616
+ "shop.consents.record": 15,
2617
+ "shop.orders.pay": 13,
2618
+ "shop.orders.verifyAccessCode": 13,
2619
+ "shop.orders.create": 12,
2620
+ "shop.orders.requestAccessCode": 4
2688
2621
  });
2689
2622
  /** Счётчик, из которого списывается путь без собственного правила на сервере. */
2690
2623
  const DEFAULT_RATE_LIMIT_BUCKET = "default";
@@ -3039,12 +2972,12 @@ const OPERATIONS = freezeOperations({
3039
2972
  method: "PUT",
3040
2973
  retrySafety: RetrySafety.Idempotent
3041
2974
  },
3042
- "realtime.poll.updates": {
2975
+ "events.notifications.poll.updates": {
3043
2976
  method: "GET",
3044
2977
  retrySafety: RetrySafety.Safe,
3045
2978
  bucket: "notifications"
3046
2979
  },
3047
- "realtime.poll.unread": {
2980
+ "events.notifications.poll.unread": {
3048
2981
  method: "GET",
3049
2982
  retrySafety: RetrySafety.Safe,
3050
2983
  bucket: "notifications"
@@ -3135,8 +3068,75 @@ const OPERATIONS = freezeOperations({
3135
3068
  "telemetry.interaction": {
3136
3069
  method: "POST",
3137
3070
  retrySafety: RetrySafety.Unsafe
3071
+ },
3072
+ "shop.products.list": {
3073
+ method: "GET",
3074
+ retrySafety: RetrySafety.Safe,
3075
+ bucket: "shop"
3076
+ },
3077
+ "shop.products.get": {
3078
+ method: "GET",
3079
+ retrySafety: RetrySafety.Safe,
3080
+ bucket: "shop"
3081
+ },
3082
+ "shop.delivery.countries": {
3083
+ method: "GET",
3084
+ retrySafety: RetrySafety.Safe,
3085
+ bucket: "shop"
3086
+ },
3087
+ "shop.delivery.cities": {
3088
+ method: "GET",
3089
+ retrySafety: RetrySafety.Safe,
3090
+ bucket: "shop.delivery.cities"
3091
+ },
3092
+ "shop.delivery.points": {
3093
+ method: "GET",
3094
+ retrySafety: RetrySafety.Safe,
3095
+ bucket: "shop.delivery.points"
3096
+ },
3097
+ "shop.delivery.calculate": {
3098
+ method: "POST",
3099
+ retrySafety: RetrySafety.Safe,
3100
+ bucket: "shop.delivery.calculate"
3101
+ },
3102
+ "shop.orders.create": {
3103
+ method: "POST",
3104
+ retrySafety: RetrySafety.Idempotent,
3105
+ bucket: "shop.orders.create"
3106
+ },
3107
+ "shop.orders.list": {
3108
+ method: "GET",
3109
+ retrySafety: RetrySafety.Safe
3110
+ },
3111
+ "shop.orders.get": {
3112
+ method: "GET",
3113
+ retrySafety: RetrySafety.Safe
3114
+ },
3115
+ "shop.orders.pay": {
3116
+ method: "POST",
3117
+ retrySafety: RetrySafety.Unsafe,
3118
+ bucket: "shop.orders.pay"
3119
+ },
3120
+ "shop.orders.requestAccessCode": {
3121
+ method: "POST",
3122
+ retrySafety: RetrySafety.Unsafe,
3123
+ bucket: "shop.orders.requestAccessCode"
3124
+ },
3125
+ "shop.orders.verifyAccessCode": {
3126
+ method: "POST",
3127
+ retrySafety: RetrySafety.Unsafe,
3128
+ bucket: "shop.orders.verifyAccessCode"
3129
+ },
3130
+ "shop.consents.record": {
3131
+ method: "POST",
3132
+ retrySafety: RetrySafety.Unsafe,
3133
+ bucket: "shop.consents.record"
3138
3134
  }
3139
3135
  });
3136
+ /** Создаёт контракт результата встроенной операции с проверяемым ID. */
3137
+ function defineBuiltInOperation(id, read = identityResult) {
3138
+ return defineOperation(id, OPERATIONS[id], read);
3139
+ }
3140
3140
  const BUILT_IN_IDS = new Set(Object.keys(OPERATIONS));
3141
3141
  /** Проверяет принадлежность ID встроенному каталогу. */
3142
3142
  function isBuiltInOperationId(value) {
@@ -3171,6 +3171,7 @@ function operationBucket(id) {
3171
3171
  * @internal
3172
3172
  */
3173
3173
  const ITD_CATALOG = Object.freeze({
3174
+ definitionOf: (id) => isBuiltInOperationId(id) ? OPERATIONS[id] : void 0,
3174
3175
  retrySafetyOf: (id) => isBuiltInOperationId(id) ? operationRetrySafety(id) : void 0,
3175
3176
  methodOf: (id) => isBuiltInOperationId(id) ? operationMethod(id) : void 0,
3176
3177
  bucketOf: (id) => operationBucket(id),
@@ -3257,8 +3258,8 @@ const ReportReason = Object.freeze({
3257
3258
  Fraud: "fraud",
3258
3259
  Other: "other"
3259
3260
  });
3260
- /** Состояние realtime-соединения. */
3261
- const RealtimeStatus = Object.freeze({
3261
+ /** Состояние соединения событийного канала. */
3262
+ const EventChannelStatus = Object.freeze({
3262
3263
  Connecting: "connecting",
3263
3264
  Connected: "connected",
3264
3265
  Error: "error",
@@ -3427,6 +3428,7 @@ const ItdErrorCode = Object.freeze({
3427
3428
  SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
3428
3429
  MISSING_FLOW_TOKEN: "MISSING_FLOW_TOKEN",
3429
3430
  PROFILE_USERNAME_TAKEN: "PROFILE_USERNAME_TAKEN",
3431
+ PROFILE_USERNAME_RESERVED: "PROFILE_USERNAME_RESERVED",
3430
3432
  PROFILE_RESTRICTION_ACTIVE: "PROFILE_RESTRICTION_ACTIVE",
3431
3433
  PROFILE_MODIFICATION_RESTRICTED: "PROFILE_MODIFICATION_RESTRICTED",
3432
3434
  CONTENT_MODERATION_FAILED: "CONTENT_MODERATION_FAILED",
@@ -3577,6 +3579,103 @@ function readUnreadCountEvent(data) {
3577
3579
  return typeof payload.count === "number" ? payload.count : void 0;
3578
3580
  }
3579
3581
  //#endregion
3582
+ //#region src/operations/common.ts
3583
+ /** Контракт встроенной операции без дополнительного преобразования результата. */
3584
+ function passthroughOperation(id) {
3585
+ return defineBuiltInOperation(id);
3586
+ }
3587
+ /** Контракт встроенной операции без возвращаемого значения. */
3588
+ function voidOperation(id) {
3589
+ return defineBuiltInOperation(id, voidResult);
3590
+ }
3591
+ //#endregion
3592
+ //#region src/core/auth-provider.ts
3593
+ /**
3594
+ * Идентификатор устройства, общий для провайдеров без сессии.
3595
+ *
3596
+ * Значение своё у каждого провайдера: сервер различает по нему записи в списке сессий,
3597
+ * поэтому делить его между клиентами нельзя. Хранилища нет, так что перезапуск процесса
3598
+ * значение не переживает — в этом и разница с полноценной сессией.
3599
+ */
3600
+ function localDeviceId() {
3601
+ let deviceId;
3602
+ return () => {
3603
+ deviceId ??= require_redact.createDeviceId();
3604
+ return Promise.resolve(deviceId);
3605
+ };
3606
+ }
3607
+ /**
3608
+ * Авторизации нет: заголовок не подставляется, ответ `401` не восстанавливается.
3609
+ *
3610
+ * @example
3611
+ * ```ts
3612
+ * const api = createRestClient(); // публичные эндпоинты доступны и без токена
3613
+ * ```
3614
+ */
3615
+ function anonymousAuth() {
3616
+ return {
3617
+ token: () => Promise.resolve(null),
3618
+ prepare: () => Promise.resolve(),
3619
+ currentHeaders: () => ({}),
3620
+ recover: () => Promise.resolve(false),
3621
+ deviceId: localDeviceId(),
3622
+ dispose: () => {}
3623
+ };
3624
+ }
3625
+ /**
3626
+ * Готовый Bearer-токен: ни хранилища, ни продления.
3627
+ *
3628
+ * Ответ `401` уходит вызывающему коду как есть — обновить токен провайдеру нечем.
3629
+ * Для сессии, которая продлевает себя сама, нужен полный клиент.
3630
+ *
3631
+ * @example
3632
+ * ```ts
3633
+ * const api = createRestClient({ auth: bearerToken(process.env.ITD_TOKEN) });
3634
+ * ```
3635
+ */
3636
+ function bearerToken(accessToken) {
3637
+ const headers = { Authorization: `Bearer ${accessToken}` };
3638
+ return {
3639
+ token: () => Promise.resolve(accessToken),
3640
+ prepare: () => Promise.resolve(),
3641
+ currentHeaders: () => headers,
3642
+ recover: () => Promise.resolve(false),
3643
+ deviceId: localDeviceId(),
3644
+ dispose: () => {}
3645
+ };
3646
+ }
3647
+ /**
3648
+ * Токен из внешнего источника — хранилища секретов, кэша, соседнего сервиса.
3649
+ *
3650
+ * Источник спрашивается на стадии подготовки, до входа в очередь: там ожидание безопасно,
3651
+ * а слот транспорта ещё не занят. Значение держится до следующей подготовки, потому что
3652
+ * подстановка заголовков обязана быть синхронной.
3653
+ *
3654
+ * @example
3655
+ * ```ts
3656
+ * const api = createRestClient({ auth: tokenProvider(() => vault.read('itd')) });
3657
+ * ```
3658
+ */
3659
+ function tokenProvider(getToken) {
3660
+ let cached = null;
3661
+ const read = async () => {
3662
+ cached = await getToken();
3663
+ return cached;
3664
+ };
3665
+ return {
3666
+ token: read,
3667
+ prepare: async () => {
3668
+ await read();
3669
+ },
3670
+ currentHeaders: () => cached ? { Authorization: `Bearer ${cached}` } : {},
3671
+ recover: () => Promise.resolve(false),
3672
+ deviceId: localDeviceId(),
3673
+ dispose: () => {
3674
+ cached = null;
3675
+ }
3676
+ };
3677
+ }
3678
+ //#endregion
3580
3679
  //#region src/notifications/text.ts
3581
3680
  /** Имя, которое подставляется, если участник неизвестен. */
3582
3681
  const UNKNOWN_ACTOR = "Пользователь";
@@ -3750,6 +3849,12 @@ Object.defineProperty(exports, "DEFAULT_STATUS_BASE_URL", {
3750
3849
  return DEFAULT_STATUS_BASE_URL;
3751
3850
  }
3752
3851
  });
3852
+ Object.defineProperty(exports, "EventChannelStatus", {
3853
+ enumerable: true,
3854
+ get: function() {
3855
+ return EventChannelStatus;
3856
+ }
3857
+ });
3753
3858
  Object.defineProperty(exports, "FeedTab", {
3754
3859
  enumerable: true,
3755
3860
  get: function() {
@@ -3810,12 +3915,6 @@ Object.defineProperty(exports, "RateLimitPacing", {
3810
3915
  return RateLimitPacing;
3811
3916
  }
3812
3917
  });
3813
- Object.defineProperty(exports, "RealtimeStatus", {
3814
- enumerable: true,
3815
- get: function() {
3816
- return RealtimeStatus;
3817
- }
3818
- });
3819
3918
  Object.defineProperty(exports, "ReportReason", {
3820
3919
  enumerable: true,
3821
3920
  get: function() {
@@ -3840,12 +3939,6 @@ Object.defineProperty(exports, "RetrySafety", {
3840
3939
  return RetrySafety;
3841
3940
  }
3842
3941
  });
3843
- Object.defineProperty(exports, "RuntimeMode", {
3844
- enumerable: true,
3845
- get: function() {
3846
- return RuntimeMode;
3847
- }
3848
- });
3849
3942
  Object.defineProperty(exports, "STATUS_SERVICE", {
3850
3943
  enumerable: true,
3851
3944
  get: function() {
@@ -3882,12 +3975,24 @@ Object.defineProperty(exports, "WallAccess", {
3882
3975
  return WallAccess;
3883
3976
  }
3884
3977
  });
3978
+ Object.defineProperty(exports, "anonymousAuth", {
3979
+ enumerable: true,
3980
+ get: function() {
3981
+ return anonymousAuth;
3982
+ }
3983
+ });
3885
3984
  Object.defineProperty(exports, "assertPluginRemovable", {
3886
3985
  enumerable: true,
3887
3986
  get: function() {
3888
3987
  return assertPluginRemovable;
3889
3988
  }
3890
3989
  });
3990
+ Object.defineProperty(exports, "bearerToken", {
3991
+ enumerable: true,
3992
+ get: function() {
3993
+ return bearerToken;
3994
+ }
3995
+ });
3891
3996
  Object.defineProperty(exports, "canonicalNotificationType", {
3892
3997
  enumerable: true,
3893
3998
  get: function() {
@@ -3906,40 +4011,40 @@ Object.defineProperty(exports, "createDeadline", {
3906
4011
  return createDeadline;
3907
4012
  }
3908
4013
  });
3909
- Object.defineProperty(exports, "createDeviceId", {
4014
+ Object.defineProperty(exports, "defineBuiltInOperation", {
3910
4015
  enumerable: true,
3911
4016
  get: function() {
3912
- return createDeviceId;
4017
+ return defineBuiltInOperation;
3913
4018
  }
3914
4019
  });
3915
- Object.defineProperty(exports, "formatNotificationText", {
4020
+ Object.defineProperty(exports, "defineOperation", {
3916
4021
  enumerable: true,
3917
4022
  get: function() {
3918
- return formatNotificationText;
4023
+ return defineOperation;
3919
4024
  }
3920
4025
  });
3921
- Object.defineProperty(exports, "installAsyncDisposeFallback", {
4026
+ Object.defineProperty(exports, "formatNotificationText", {
3922
4027
  enumerable: true,
3923
4028
  get: function() {
3924
- return installAsyncDisposeFallback;
4029
+ return formatNotificationText;
3925
4030
  }
3926
4031
  });
3927
- Object.defineProperty(exports, "isBlob", {
4032
+ Object.defineProperty(exports, "identityResult", {
3928
4033
  enumerable: true,
3929
4034
  get: function() {
3930
- return isBlob;
4035
+ return identityResult;
3931
4036
  }
3932
4037
  });
3933
- Object.defineProperty(exports, "isBuiltInOperationId", {
4038
+ Object.defineProperty(exports, "installAsyncDisposeFallback", {
3934
4039
  enumerable: true,
3935
4040
  get: function() {
3936
- return isBuiltInOperationId;
4041
+ return installAsyncDisposeFallback;
3937
4042
  }
3938
4043
  });
3939
- Object.defineProperty(exports, "isFile", {
4044
+ Object.defineProperty(exports, "isBuiltInOperationId", {
3940
4045
  enumerable: true,
3941
4046
  get: function() {
3942
- return isFile;
4047
+ return isBuiltInOperationId;
3943
4048
  }
3944
4049
  });
3945
4050
  Object.defineProperty(exports, "isKnownNotificationType", {
@@ -3960,12 +4065,6 @@ Object.defineProperty(exports, "isRecord$1", {
3960
4065
  return isRecord$1;
3961
4066
  }
3962
4067
  });
3963
- Object.defineProperty(exports, "maskSecret", {
3964
- enumerable: true,
3965
- get: function() {
3966
- return maskSecret;
3967
- }
3968
- });
3969
4068
  Object.defineProperty(exports, "mergeService", {
3970
4069
  enumerable: true,
3971
4070
  get: function() {
@@ -4002,6 +4101,12 @@ Object.defineProperty(exports, "orderPluginDefinitions", {
4002
4101
  return orderPluginDefinitions;
4003
4102
  }
4004
4103
  });
4104
+ Object.defineProperty(exports, "passthroughOperation", {
4105
+ enumerable: true,
4106
+ get: function() {
4107
+ return passthroughOperation;
4108
+ }
4109
+ });
4005
4110
  Object.defineProperty(exports, "pickArray", {
4006
4111
  enumerable: true,
4007
4112
  get: function() {
@@ -4044,12 +4149,6 @@ Object.defineProperty(exports, "readUnreadCountEvent", {
4044
4149
  return readUnreadCountEvent;
4045
4150
  }
4046
4151
  });
4047
- Object.defineProperty(exports, "redactUrl", {
4048
- enumerable: true,
4049
- get: function() {
4050
- return redactUrl;
4051
- }
4052
- });
4053
4152
  Object.defineProperty(exports, "requireOptionalBoolean", {
4054
4153
  enumerable: true,
4055
4154
  get: function() {
@@ -4068,17 +4167,29 @@ Object.defineProperty(exports, "resolveRateLimit", {
4068
4167
  return resolveRateLimit;
4069
4168
  }
4070
4169
  });
4071
- Object.defineProperty(exports, "supportsStreamingBody", {
4170
+ Object.defineProperty(exports, "systemClock", {
4171
+ enumerable: true,
4172
+ get: function() {
4173
+ return systemClock;
4174
+ }
4175
+ });
4176
+ Object.defineProperty(exports, "tokenProvider", {
4072
4177
  enumerable: true,
4073
4178
  get: function() {
4074
- return supportsStreamingBody;
4179
+ return tokenProvider;
4075
4180
  }
4076
4181
  });
4077
- Object.defineProperty(exports, "systemClock", {
4182
+ Object.defineProperty(exports, "voidOperation", {
4078
4183
  enumerable: true,
4079
4184
  get: function() {
4080
- return systemClock;
4185
+ return voidOperation;
4186
+ }
4187
+ });
4188
+ Object.defineProperty(exports, "voidResult", {
4189
+ enumerable: true,
4190
+ get: function() {
4191
+ return voidResult;
4081
4192
  }
4082
4193
  });
4083
4194
 
4084
- //# sourceMappingURL=url-BaMCQpYH.cjs.map
4195
+ //# sourceMappingURL=url-Bsb3xE5U.cjs.map