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,7 +1,8 @@
1
- const require_url = require("./url-BaMCQpYH.cjs");
2
- const require_errors = require("./errors-DfU8M5eS.cjs");
3
- const require_cookies = require("./cookies-DZwFq6kr.cjs");
4
- const require_options = require("./options-Dg5N3r1V.cjs");
1
+ const require_url = require("./url-Bsb3xE5U.cjs");
2
+ const require_errors = require("./errors-BmP3TKoW.cjs");
3
+ const require_redact = require("./redact-BP00URmQ.cjs");
4
+ const require_options = require("./options-DOJYtoti.cjs");
5
+ const require_key_value_store = require("./key-value-store-B5GYVEYZ.cjs");
5
6
  //#region src/core/feature-catalog.ts
6
7
  /**
7
8
  * Изменяемый слой над каталогом конкретного API.
@@ -32,6 +33,9 @@ var ExtensibleOperationCatalog = class {
32
33
  get defaultBucket() {
33
34
  return this.#base.defaultBucket;
34
35
  }
36
+ definitionOf(id) {
37
+ return this.#operations.get(id) ?? this.#base.definitionOf(id);
38
+ }
35
39
  retrySafetyOf(id) {
36
40
  return this.#operations.get(id)?.retrySafety ?? this.#base.retrySafetyOf(id);
37
41
  }
@@ -49,7 +53,10 @@ var ExtensibleOperationCatalog = class {
49
53
  if (this.#base.isKnownBucket(name) || this.#bucketOwners.has(name)) throw new require_errors.ItdConfigError(`feature «${owner}»: бакет «${name}» уже зарегистрирован`);
50
54
  this.#bucketOwners.set(name, owner);
51
55
  if (definition.limit !== void 0) this.#bucketLimits[name] = definition.limit;
52
- if (definition.concurrency !== void 0) this.#bucketOverrides[name] = Object.freeze({ concurrency: definition.concurrency });
56
+ if (definition.concurrency !== void 0 || definition.rps !== void 0) this.#bucketOverrides[name] = Object.freeze({
57
+ ...definition.concurrency === void 0 ? {} : { concurrency: definition.concurrency },
58
+ ...definition.rps === void 0 ? {} : { rps: definition.rps }
59
+ });
53
60
  return () => {
54
61
  if (this.#bucketOwners.get(name) !== owner) return;
55
62
  this.#bucketOwners.delete(name);
@@ -74,22 +81,311 @@ var ExtensibleOperationCatalog = class {
74
81
  }
75
82
  };
76
83
  //#endregion
84
+ //#region src/core/attachments/url-source.ts
85
+ /** Убирает параметры MIME и приводит его к форме для сравнения. */
86
+ function normalizeMimeType(contentType) {
87
+ return contentType?.split(";", 1)[0]?.trim().toLowerCase() || void 0;
88
+ }
89
+ /** Достаёт имя файла из пути URL. */
90
+ function filenameFromUrl(url) {
91
+ const last = url.pathname.split("/").pop();
92
+ if (!last) return void 0;
93
+ try {
94
+ return decodeURIComponent(last) || void 0;
95
+ } catch {
96
+ return last;
97
+ }
98
+ }
99
+ /** Проверяет объявленный размер и возвращает его, если заголовок корректен. */
100
+ function declaredSize(response, maxBytes, url) {
101
+ const header = response.headers.get("content-length");
102
+ if (header === null) return void 0;
103
+ const size = Number(header);
104
+ if (!Number.isFinite(size) || size < 0 || !Number.isInteger(size)) return void 0;
105
+ if (maxBytes !== void 0 && size > maxBytes) throw require_options.fileTooLarge(url, maxBytes, size);
106
+ return size;
107
+ }
108
+ /** Получает HTTP-ответ источника и проверяет его статус. */
109
+ async function fetchFile(target, options, context) {
110
+ let requested;
111
+ try {
112
+ requested = new URL(target);
113
+ } catch {
114
+ throw new require_errors.ItdConfigError(`«${require_redact.redactUrl(target)}» не разбирается как адрес`);
115
+ }
116
+ if (requested.protocol !== "http:" && requested.protocol !== "https:") throw new require_errors.ItdConfigError(`вложение по адресу поддерживает только http и https, получено: ${requested.protocol}`);
117
+ let response;
118
+ try {
119
+ response = await context.fetch(requested, { ...context.signal ? { signal: context.signal } : {} });
120
+ } catch (error) {
121
+ if (context.signal?.aborted || error instanceof Error && error.name === "AbortError") throw error;
122
+ const safeUrl = require_redact.redactUrl(requested.href);
123
+ throw new require_errors.ItdFileError(`не удалось получить файл по адресу ${safeUrl}`, {
124
+ reason: require_errors.ItdFileErrorReason.Network,
125
+ url: safeUrl,
126
+ retryable: true,
127
+ cause: require_redact.redactErrorCause(error)
128
+ });
129
+ }
130
+ const finalUrl = response.url ? new URL(response.url) : requested;
131
+ if (!response.ok) {
132
+ await response.body?.cancel().catch(() => {});
133
+ const safeUrl = require_redact.redactUrl(finalUrl.href);
134
+ throw new require_errors.ItdFileError(`источник ${safeUrl} ответил статусом ${response.status}`, {
135
+ reason: require_errors.ItdFileErrorReason.Http,
136
+ url: safeUrl,
137
+ status: response.status,
138
+ retryable: response.status === 408 || response.status === 429 || response.status >= 500
139
+ });
140
+ }
141
+ const { maxBytes } = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
142
+ let size;
143
+ try {
144
+ size = declaredSize(response, maxBytes, finalUrl.href);
145
+ } catch (error) {
146
+ await response.body?.cancel().catch(() => {});
147
+ throw error;
148
+ }
149
+ return {
150
+ response,
151
+ url: finalUrl,
152
+ size
153
+ };
154
+ }
155
+ /** Читает ответ с контролем размера до создания итогового `Blob`. */
156
+ async function responseBlob(response, url, maxBytes, streamBufferBytes, signal) {
157
+ if (!response.body) {
158
+ const blob = await response.blob();
159
+ if (maxBytes !== void 0 && blob.size > maxBytes) throw require_options.fileTooLarge(url, maxBytes, blob.size);
160
+ return blob;
161
+ }
162
+ const chunks = [];
163
+ const reader = require_options.boundedFileStream(response.body, {
164
+ ...maxBytes !== void 0 ? { maxBytes } : {},
165
+ streamBufferBytes,
166
+ ...signal ? { signal } : {},
167
+ url,
168
+ retryableRead: true
169
+ }).getReader();
170
+ try {
171
+ for (;;) {
172
+ const next = await reader.read();
173
+ if (next.done) break;
174
+ chunks.push(next.value);
175
+ }
176
+ } finally {
177
+ reader.releaseLock();
178
+ }
179
+ return new Blob(chunks.map((chunk) => Uint8Array.from(chunk).buffer));
180
+ }
181
+ /** Скачивает файл целиком с ограничением размера. @internal */
182
+ async function downloadFile(target, options, context) {
183
+ const resolved = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
184
+ const { response, url } = await fetchFile(target, options, context);
185
+ const blob = await responseBlob(response, url.href, resolved.maxBytes, resolved.streamBufferBytes, context.signal);
186
+ const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
187
+ const filename = options.filename ?? filenameFromUrl(url);
188
+ return {
189
+ file: new Blob([blob], { type: contentType ?? "" }),
190
+ ...filename ? { filename } : {},
191
+ ...contentType ? { contentType } : {}
192
+ };
193
+ }
194
+ /** Открывает HTTP-ответ как ограниченный поток. @internal */
195
+ async function openUrlFile(target, options, context) {
196
+ const resolved = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
197
+ const { response, url, size } = await fetchFile(target, options, context);
198
+ if (!response.body) {
199
+ const safeUrl = require_redact.redactUrl(url.href);
200
+ throw new require_errors.ItdFileError(`источник ${safeUrl} не предоставил потоковое тело`, {
201
+ reason: require_errors.ItdFileErrorReason.StreamUnavailable,
202
+ url: safeUrl
203
+ });
204
+ }
205
+ const stream = require_options.boundedFileStream(response.body, {
206
+ ...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
207
+ streamBufferBytes: resolved.streamBufferBytes,
208
+ ...context.signal ? { signal: context.signal } : {},
209
+ url: url.href,
210
+ retryableRead: true
211
+ });
212
+ const filename = options.filename ?? filenameFromUrl(url);
213
+ const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
214
+ return {
215
+ stream,
216
+ ...filename ? { filename } : {},
217
+ ...contentType ? { contentType } : {},
218
+ ...size !== void 0 ? { size } : {},
219
+ close: () => stream.cancel().catch(() => {})
220
+ };
221
+ }
222
+ //#endregion
223
+ //#region src/core/attachments/resolver.ts
224
+ /** Возвращает функцию освобождения, которая исполняет действие не более одного раза. */
225
+ function closeOnce(action) {
226
+ let closing;
227
+ return () => closing ??= Promise.resolve().then(action);
228
+ }
229
+ /** Добавляет поле только для непустого значения, не выдумывая имя или MIME. */
230
+ function optionalText(name, value) {
231
+ const normalized = value?.trim();
232
+ return normalized ? { [name]: normalized } : {};
233
+ }
234
+ /** Единая реализация распознавания и открытия `FileInput`. @internal */
235
+ var DefaultFileResolver = class {
236
+ #fetch;
237
+ constructor(fetchImplementation) {
238
+ this.#fetch = fetchImplementation;
239
+ }
240
+ mode(input) {
241
+ if (typeof input !== "object" || input === null) return require_options.FileTransferMode.Buffer;
242
+ if ("open" in input) return require_options.FileTransferMode.Stream;
243
+ if ("url" in input) return require_options.resolveFileStreamOptions(input).mode;
244
+ return require_options.FileTransferMode.Buffer;
245
+ }
246
+ async resolve(input, options = {}, context = {}) {
247
+ const attempt = context.attempt ?? 1;
248
+ if (!Number.isInteger(attempt) || attempt < 1) throw new require_errors.ItdConfigError(`attempt должен быть целым числом от 1, получено: ${attempt}`);
249
+ const maxBytes = require_options.optionalBytes(options.maxBytes, "maxBytes");
250
+ if (context.signal?.aborted) throw context.signal.reason ?? new DOMException("Aborted", "AbortError");
251
+ const fileContext = {
252
+ fetch: this.#fetch,
253
+ attempt,
254
+ ...context.signal ? { signal: context.signal } : {}
255
+ };
256
+ try {
257
+ const source = this.mode(input) === require_options.FileTransferMode.Stream ? await this.#openStream(input, options, fileContext, maxBytes) : await this.#loadBuffer(input, options, fileContext, maxBytes);
258
+ if (context.signal?.aborted) {
259
+ if (source.mode === require_options.FileTransferMode.Stream) await source.close();
260
+ throw context.signal.reason ?? new DOMException("Aborted", "AbortError");
261
+ }
262
+ return source;
263
+ } catch (error) {
264
+ if (error instanceof require_errors.ItdError || context.signal?.aborted) throw error;
265
+ throw new require_errors.ItdFileError("не удалось получить содержимое вложения", {
266
+ reason: require_errors.ItdFileErrorReason.Read,
267
+ retryable: true,
268
+ cause: require_redact.redactErrorCause(error)
269
+ });
270
+ }
271
+ }
272
+ async #loadBuffer(input, options, context, maxBytes) {
273
+ const content = await this.#resolveBuffer(input, context);
274
+ if (typeof content !== "object" || content === null || !("file" in content)) throw new require_errors.ItdConfigError("буферный источник должен вернуть объект с полем file");
275
+ if (!require_redact.isBlob(content.file) && !(content.file instanceof ArrayBuffer) && !ArrayBuffer.isView(content.file)) throw new require_errors.ItdConfigError("поле file должно содержать Blob, ArrayBuffer или Uint8Array");
276
+ const contentType = normalizeMimeType(options.contentType ?? content.contentType) ?? normalizeMimeType(require_redact.isBlob(content.file) ? content.file.type : void 0);
277
+ const blob = require_redact.isBlob(content.file) && (!contentType || content.file.type === contentType) ? content.file : new Blob([content.file], { type: contentType ?? "" });
278
+ if (maxBytes !== void 0 && blob.size > maxBytes) throw require_options.fileTooLarge(void 0, maxBytes, blob.size);
279
+ return {
280
+ mode: require_options.FileTransferMode.Buffer,
281
+ blob,
282
+ size: blob.size,
283
+ ...optionalText("filename", options.filename ?? content.filename),
284
+ contentType
285
+ };
286
+ }
287
+ async #openStream(input, options, context, maxBytes) {
288
+ const opened = await this.#resolveStream(input, context);
289
+ if (!opened || !require_options.isReadableByteStream(opened.stream)) {
290
+ await opened?.close?.();
291
+ throw new require_errors.ItdConfigError("потоковый источник должен вернуть { stream: ReadableStream }");
292
+ }
293
+ let handedOff = false;
294
+ const closeOpened = closeOnce(async () => {
295
+ await opened.close?.();
296
+ });
297
+ try {
298
+ require_options.optionalBytes(opened.size, "size");
299
+ if (maxBytes !== void 0 && opened.size !== void 0 && opened.size > maxBytes) throw require_options.fileTooLarge(void 0, maxBytes, opened.size);
300
+ if (options.requireKnownSize === true && opened.size === void 0) throw new require_errors.ItdConfigError("размер потокового вложения должен быть известен до чтения");
301
+ const limits = require_options.resolveFileStreamOptions({
302
+ mode: require_options.FileTransferMode.Stream,
303
+ ...maxBytes !== void 0 ? { maxBytes } : {},
304
+ ...options.streamBufferBytes !== void 0 ? { streamBufferBytes: options.streamBufferBytes } : {}
305
+ });
306
+ const stream = require_options.isBoundedFileStream(opened.stream) && options.maxBytes === void 0 && options.streamBufferBytes === void 0 ? opened.stream : require_options.boundedFileStream(opened.stream, {
307
+ ...limits.maxBytes !== void 0 ? { maxBytes: limits.maxBytes } : {},
308
+ streamBufferBytes: limits.streamBufferBytes,
309
+ ...context.signal ? { signal: context.signal } : {}
310
+ });
311
+ let detachAbort;
312
+ const close = closeOnce(async () => {
313
+ detachAbort?.();
314
+ detachAbort = void 0;
315
+ await stream.cancel().catch(() => {});
316
+ await closeOpened();
317
+ });
318
+ if (context.signal) {
319
+ const onAbort = () => {
320
+ close().catch(() => {});
321
+ };
322
+ if (context.signal.aborted) {
323
+ await close();
324
+ throw context.signal.reason ?? new DOMException("Aborted", "AbortError");
325
+ } else {
326
+ context.signal.addEventListener("abort", onAbort, { once: true });
327
+ detachAbort = () => context.signal?.removeEventListener("abort", onAbort);
328
+ }
329
+ }
330
+ handedOff = true;
331
+ return {
332
+ mode: require_options.FileTransferMode.Stream,
333
+ stream,
334
+ size: opened.size,
335
+ ...optionalText("filename", options.filename ?? opened.filename),
336
+ contentType: normalizeMimeType(options.contentType ?? opened.contentType),
337
+ close
338
+ };
339
+ } finally {
340
+ if (!handedOff) await closeOpened();
341
+ }
342
+ }
343
+ #resolveBuffer(input, context) {
344
+ if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || require_redact.isBlob(input)) return {
345
+ file: input,
346
+ ...require_redact.isFile(input) ? { filename: input.name } : {}
347
+ };
348
+ if (typeof input !== "object" || input === null) throw new require_errors.ItdConfigError(`вложение задано значением типа ${typeof input}; ожидается бинарное значение или объект { file }, { url }, { load }, { open }. Для файла на диске используйте fromPath('./photo.jpg') из itd-api/node`);
349
+ if ("load" in input) return input.load(context);
350
+ if ("url" in input) {
351
+ const { url, ...urlOptions } = input;
352
+ return downloadFile(url, urlOptions, context);
353
+ }
354
+ if ("file" in input) return input;
355
+ if ("open" in input) throw new require_errors.ItdConfigError("потоковый источник нельзя использовать как буферный");
356
+ throw new require_errors.ItdConfigError("вложение не распознано: ожидается { file }, { url }, { load } или { open }");
357
+ }
358
+ #resolveStream(input, context) {
359
+ if (typeof input === "object" && input !== null && "open" in input) return input.open(context);
360
+ if (typeof input === "object" && input !== null && "url" in input) {
361
+ const { url, ...urlOptions } = input;
362
+ return openUrlFile(url, urlOptions, context);
363
+ }
364
+ throw new require_errors.ItdConfigError("потоковое вложение должно иметь форму { open } или { url, mode }");
365
+ }
366
+ };
367
+ /** Создаёт механизм подготовки файлов, привязанный к настроенному `fetch` клиента. @internal */
368
+ function createFileResolver(fetchImplementation) {
369
+ return new DefaultFileResolver(fetchImplementation);
370
+ }
371
+ //#endregion
77
372
  //#region src/core/features.ts
78
373
  function requireName(value, kind) {
79
374
  if (typeof value !== "string" || value.trim() === "") throw new require_errors.ItdConfigError(`${kind} должен иметь непустое имя`);
80
375
  return value.trim();
81
376
  }
82
- function requireOperationName(value, featureName) {
83
- const name = requireName(value, `Операция feature «${featureName}»`);
84
- if (name.includes(".")) throw new require_errors.ItdConfigError(`feature «${featureName}»: локальное имя операции «${name}» не должно содержать точку`);
377
+ function requireFeatureName(value) {
378
+ const name = requireName(value, "Feature");
379
+ if (!/^[a-z][a-z0-9-]*$/.test(name)) throw new require_errors.ItdConfigError(`имя feature «${name}» должно соответствовать [a-z][a-z0-9-]*`);
85
380
  return name;
86
381
  }
87
382
  function validateBucket(owner, localName, bucket) {
88
383
  if (typeof bucket !== "object" || bucket === null || Array.isArray(bucket)) throw new require_errors.ItdConfigError(`feature «${owner}»: бакет «${localName}» должен быть объектом`);
89
384
  if (bucket.limit !== void 0 && (!Number.isFinite(bucket.limit) || bucket.limit <= 0)) throw new require_errors.ItdConfigError(`feature «${owner}»: limit бакета «${localName}» должен быть положительным числом`);
90
385
  if (bucket.concurrency !== void 0 && (!Number.isInteger(bucket.concurrency) || bucket.concurrency < 1)) throw new require_errors.ItdConfigError(`feature «${owner}»: concurrency бакета «${localName}» должен быть целым числом от 1`);
386
+ if (bucket.rps !== void 0 && (!Number.isFinite(bucket.rps) || bucket.rps <= 0)) throw new require_errors.ItdConfigError(`feature «${owner}»: rps бакета «${localName}» должен быть положительным числом`);
91
387
  }
92
- /** Реестр feature одного facade. @internal */
388
+ /** Реестр подключаемых модулей клиента. @internal */
93
389
  var FeatureRegistry = class {
94
390
  #deps;
95
391
  #features = /* @__PURE__ */ new Map();
@@ -102,7 +398,7 @@ var FeatureRegistry = class {
102
398
  this.#deps.assertActive("установить feature");
103
399
  if (this.#disposed) throw new require_errors.ItdStateError("Реестр feature уже освобождён");
104
400
  if (typeof feature !== "object" || feature === null) throw new require_errors.ItdConfigError("feature должен быть объектом");
105
- const name = requireName(feature.name, "Feature");
401
+ const name = requireFeatureName(feature.name);
106
402
  if (this.#features.has(name)) throw new require_errors.ItdConfigError(`feature «${name}» уже установлен`);
107
403
  if (typeof feature.setup !== "function") throw new require_errors.ItdConfigError(`feature «${name}» должен предоставлять setup()`);
108
404
  if (typeof feature.operations !== "object" || feature.operations === null || Array.isArray(feature.operations)) throw new require_errors.ItdConfigError(`feature «${name}» должен объявлять operations`);
@@ -114,6 +410,11 @@ var FeatureRegistry = class {
114
410
  const operations = /* @__PURE__ */ new Map();
115
411
  const controller = new AbortController();
116
412
  let committed = false;
413
+ const reportRollbackError = (message, error) => {
414
+ try {
415
+ this.#deps.logger?.error(message, error);
416
+ } catch {}
417
+ };
117
418
  try {
118
419
  for (const service of feature.services ?? []) {
119
420
  const serviceName = requireName(service?.name, `Сервис feature «${name}»`);
@@ -147,7 +448,7 @@ var FeatureRegistry = class {
147
448
  bucketNames.set(localName, globalName);
148
449
  }
149
450
  for (const [rawLocalName, definition] of Object.entries(feature.operations)) {
150
- const localName = requireOperationName(rawLocalName, name);
451
+ const localName = requireName(rawLocalName, `Операция feature «${name}»`);
151
452
  if (typeof definition !== "object" || definition === null) throw new require_errors.ItdConfigError(`feature «${name}»: операция «${localName}» должна быть объектом`);
152
453
  if (![
153
454
  "GET",
@@ -159,19 +460,27 @@ var FeatureRegistry = class {
159
460
  if (!Object.values(require_url.RetrySafety).includes(definition.retrySafety)) throw new require_errors.ItdConfigError(`feature «${name}»: операция «${localName}» содержит неизвестную retrySafety`);
160
461
  if (definition.service !== void 0 && typeof definition.service !== "string") throw new require_errors.ItdConfigError(`feature «${name}»: service операции «${localName}» должен быть строкой`);
161
462
  if (definition.bucket !== void 0 && typeof definition.bucket !== "string") throw new require_errors.ItdConfigError(`feature «${name}»: bucket операции «${localName}» должен быть строкой`);
463
+ if (definition.read !== void 0 && typeof definition.read !== "function") throw new require_errors.ItdConfigError(`feature «${name}»: read операции «${localName}» должна быть функцией`);
464
+ if (definition.annotations !== void 0 && (typeof definition.annotations !== "object" || definition.annotations === null || Array.isArray(definition.annotations))) throw new require_errors.ItdConfigError(`feature «${name}»: annotations операции «${localName}» должны быть объектом`);
162
465
  if (definition.service !== void 0 && !services.has(definition.service)) throw new require_errors.ItdConfigError(`feature «${name}»: операция «${localName}» ссылается на необъявленный сервис «${definition.service}»`);
163
466
  const bucket = definition.bucket === void 0 ? this.#deps.catalog.defaultBucket : bucketNames.get(definition.bucket);
164
467
  if (bucket === void 0) throw new require_errors.ItdConfigError(`feature «${name}»: операция «${localName}» ссылается на необъявленный бакет «${definition.bucket}»`);
165
468
  const operationId = `${name}.${localName}`;
166
- const unregister = this.#deps.catalog.registerOperation(name, operationId, {
469
+ const contract = require_url.defineOperation(operationId, {
167
470
  method: definition.method,
168
471
  retrySafety: definition.retrySafety,
169
- bucket
472
+ bucket,
473
+ ...definition.annotations === void 0 ? {} : { annotations: definition.annotations }
474
+ }, definition.read ?? require_url.identityResult);
475
+ const unregister = this.#deps.catalog.registerOperation(name, operationId, {
476
+ method: contract.method,
477
+ retrySafety: contract.retrySafety,
478
+ bucket,
479
+ ...contract.annotations === void 0 ? {} : { annotations: contract.annotations }
170
480
  });
171
481
  rollback.push(unregister);
172
482
  operations.set(localName, {
173
- operationId,
174
- method: definition.method,
483
+ contract,
175
484
  service: definition.service
176
485
  });
177
486
  }
@@ -181,6 +490,15 @@ var FeatureRegistry = class {
181
490
  signal: controller.signal,
182
491
  clock: this.#deps.clock,
183
492
  logger: this.#deps.logger,
493
+ files: Object.freeze({ resolve: (input, options, context) => {
494
+ try {
495
+ this.#deps.assertActive(`открыть файловый источник feature «${name}»`);
496
+ if (!committed || controller.signal.aborted) throw new require_errors.ItdStateError(`feature «${name}» не активен`);
497
+ return this.#deps.files.resolve(input, options, context);
498
+ } catch (error) {
499
+ return Promise.reject(error);
500
+ }
501
+ } }),
184
502
  request: (operation, options) => {
185
503
  try {
186
504
  this.#deps.assertActive(`выполнить операцию feature «${name}»`);
@@ -194,10 +512,8 @@ var FeatureRegistry = class {
194
512
  delete scoped.baseUrl;
195
513
  delete scoped.retrySafety;
196
514
  delete scoped.rateLimitBucket;
197
- return this.#deps.http.request({
515
+ return this.#deps.http.execute(resolved.contract, {
198
516
  ...scoped,
199
- operationId: resolved.operationId,
200
- method: resolved.method,
201
517
  ...resolved.service === void 0 ? {} : { service: resolved.service }
202
518
  });
203
519
  } catch (error) {
@@ -207,6 +523,43 @@ var FeatureRegistry = class {
207
523
  serviceBaseUrl: (serviceName) => {
208
524
  if (!services.has(serviceName)) throw new require_errors.ItdConfigError(`feature «${name}» не объявляет сервис «${serviceName}»`);
209
525
  return this.#deps.services.resolveBaseUrl(serviceName);
526
+ },
527
+ connection: (serviceName) => {
528
+ if (!services.has(serviceName)) throw new require_errors.ItdConfigError(`feature «${name}» не объявляет сервис «${serviceName}»`);
529
+ return this.#deps.connection(serviceName);
530
+ },
531
+ manage: (resource) => {
532
+ this.#deps.assertActive(`зарегистрировать ресурс feature «${name}»`);
533
+ if (controller.signal.aborted) throw new require_errors.ItdStateError(`feature «${name}» не активен`);
534
+ const unregister = this.#deps.manage(resource);
535
+ let registered = true;
536
+ const release = () => {
537
+ if (!registered) return;
538
+ registered = false;
539
+ unregister();
540
+ };
541
+ if (!committed) rollback.push(() => {
542
+ if (!registered) return;
543
+ if (committed) {
544
+ release();
545
+ return;
546
+ }
547
+ try {
548
+ resource.stop();
549
+ } catch (error) {
550
+ reportRollbackError(`Не удалось остановить ресурс «${resource.kind}» при откате feature «${name}»`, error);
551
+ } finally {
552
+ release();
553
+ }
554
+ Promise.resolve().then(() => resource.drain()).catch((error) => {
555
+ reportRollbackError(`Не удалось освободить ресурс «${resource.kind}» при откате feature «${name}»`, error);
556
+ });
557
+ });
558
+ return release;
559
+ },
560
+ assertActive: (action) => {
561
+ this.#deps.assertActive(action);
562
+ if (!committed || controller.signal.aborted) throw new require_errors.ItdStateError(`feature «${name}» не активен`);
210
563
  }
211
564
  });
212
565
  const installation = feature.setup(context);
@@ -235,36 +588,166 @@ var FeatureRegistry = class {
235
588
  return this.#features.has(name);
236
589
  }
237
590
  async close() {
238
- const errors = [];
239
- for (const feature of [...this.#features.values()].reverse()) try {
240
- await feature.close?.();
241
- } catch (error) {
242
- errors.push(error);
243
- }
591
+ const errors = (await Promise.allSettled([...this.#features.values()].reverse().map(async (feature) => feature.close?.()))).filter((result) => result.status === "rejected").map((result) => result.reason);
244
592
  if (errors.length > 0) throw new AggregateError(errors, "Не удалось закрыть feature");
245
593
  }
246
594
  async dispose() {
247
595
  if (this.#disposed) return;
248
596
  this.#disposed = true;
249
597
  const errors = [];
250
- for (const feature of [...this.#features.values()].reverse()) {
598
+ const disposals = [];
599
+ const features = [...this.#features.values()].reverse();
600
+ for (const feature of features) {
251
601
  feature.controller.abort(new require_errors.ItdAbortError(`Feature «${feature.name}» освобождён`));
252
602
  try {
253
- await feature.dispose?.();
254
- } catch (error) {
255
- errors.push(error);
256
- }
257
- for (const cleanup of [...feature.cleanup].reverse()) try {
258
- cleanup();
603
+ disposals.push(Promise.resolve(feature.dispose?.()).then(() => void 0));
259
604
  } catch (error) {
260
605
  errors.push(error);
261
606
  }
262
607
  }
608
+ for (const feature of features) for (const cleanup of [...feature.cleanup].reverse()) try {
609
+ cleanup();
610
+ } catch (error) {
611
+ errors.push(error);
612
+ }
263
613
  this.#features.clear();
614
+ const results = await Promise.allSettled(disposals);
615
+ for (const result of results) if (result.status === "rejected") errors.push(result.reason);
264
616
  if (errors.length > 0) throw new AggregateError(errors, "Не удалось освободить feature");
265
617
  }
266
618
  };
267
619
  //#endregion
620
+ //#region src/core/managed-resources.ts
621
+ /** Реестр активных ресурсов клиента. @internal */
622
+ var ManagedResourceRegistry = class {
623
+ #resources = /* @__PURE__ */ new Set();
624
+ #disposed = false;
625
+ register(resource) {
626
+ if (this.#disposed) throw new require_errors.ItdStateError("Lifecycle клиента уже освобождён");
627
+ if (typeof resource !== "object" || resource === null || typeof resource.kind !== "string" || resource.kind.trim() === "" || typeof resource.stop !== "function" || typeof resource.drain !== "function") throw new require_errors.ItdConfigError("Managed resource должен предоставлять kind, stop() и drain()");
628
+ this.#resources.add(resource);
629
+ let registered = true;
630
+ return () => {
631
+ if (!registered) return;
632
+ registered = false;
633
+ this.#resources.delete(resource);
634
+ };
635
+ }
636
+ /** Останавливает snapshot синхронно, чтобы новые auth-данные ещё не были опубликованы. */
637
+ stopAll() {
638
+ const resources = [...this.#resources];
639
+ const errors = [];
640
+ for (const resource of resources) try {
641
+ resource.stop();
642
+ } catch (error) {
643
+ errors.push(error);
644
+ }
645
+ return {
646
+ resources,
647
+ errors
648
+ };
649
+ }
650
+ dispose() {
651
+ this.#disposed = true;
652
+ this.#resources.clear();
653
+ }
654
+ };
655
+ //#endregion
656
+ //#region src/core/feature-host.ts
657
+ /**
658
+ * Управляет подключаемыми модулями и их ресурсами.
659
+ *
660
+ * @internal
661
+ */
662
+ var ClientFeatureHost = class {
663
+ #runtime;
664
+ #features;
665
+ #resources = new ManagedResourceRegistry();
666
+ /** Общий файловый порт ресурсов и подключаемых модулей этого клиента. @internal */
667
+ files;
668
+ constructor(runtime, options) {
669
+ this.#runtime = runtime;
670
+ this.files = createFileResolver(runtime.config.fetch);
671
+ this.#features = new FeatureRegistry({
672
+ http: runtime.http,
673
+ services: runtime.services,
674
+ serviceOverrides: runtime.config.services,
675
+ catalog: options.catalog,
676
+ baseUrl: runtime.config.baseUrl,
677
+ clock: runtime.config.clock,
678
+ logger: runtime.config.logger,
679
+ files: this.files,
680
+ assertActive: options.assertActive,
681
+ connection: (serviceName) => runtime.connection(serviceName),
682
+ manage: (resource) => this.#resources.register(resource),
683
+ registerBucket: (name, definition) => runtime.registerRateLimitBucket(name, definition)
684
+ });
685
+ }
686
+ install(feature) {
687
+ return this.#features.install(feature);
688
+ }
689
+ names() {
690
+ return this.#features.names();
691
+ }
692
+ has(name) {
693
+ return this.#features.has(name);
694
+ }
695
+ /** Регистрирует ресурс самого клиента. @internal */
696
+ manage(resource) {
697
+ return this.#resources.register(resource);
698
+ }
699
+ /** Останавливает ресурсы перед сменой авторизации. */
700
+ stop() {
701
+ const { errors } = this.#resources.stopAll();
702
+ if (errors.length > 0) this.#runtime.config.logger?.error("Не удалось остановить ресурсы клиента", errors);
703
+ }
704
+ /** Останавливает ресурсы и ждёт функции закрытия модулей. */
705
+ async close() {
706
+ const snapshot = this.#resources.stopAll();
707
+ const errors = [...snapshot.errors];
708
+ const deadline = require_url.createDeadline(this.#runtime.config.shutdownTimeout, this.#runtime.config.clock);
709
+ const tasks = [...snapshot.resources.map((resource) => ({
710
+ kind: resource.kind,
711
+ promise: Promise.resolve().then(() => resource.drain())
712
+ })), {
713
+ kind: "feature lifecycle",
714
+ promise: Promise.resolve().then(() => this.#features.close())
715
+ }];
716
+ try {
717
+ const stuck = (await Promise.all(tasks.map(async ({ kind, promise }) => {
718
+ const result = promise.catch((error) => {
719
+ errors.push(error);
720
+ });
721
+ return await deadline.wait(result) ? void 0 : kind;
722
+ }))).filter((kind) => kind !== void 0);
723
+ if (stuck.length > 0) errors.push(new require_errors.ItdStateError(`ресурсы (${stuck.join(", ")}) не завершились за ${this.#runtime.config.shutdownTimeout} мс`));
724
+ } finally {
725
+ deadline.cancel();
726
+ }
727
+ if (errors.length === 1) throw errors[0];
728
+ if (errors.length > 1) throw new AggregateError(errors, "Не удалось закрыть feature-host");
729
+ }
730
+ async dispose() {
731
+ this.#resources.dispose();
732
+ const deadline = require_url.createDeadline(this.#runtime.config.shutdownTimeout, this.#runtime.config.clock);
733
+ let failed = false;
734
+ let failure;
735
+ const settled = this.#features.dispose().catch((error) => {
736
+ failed = true;
737
+ failure = error;
738
+ });
739
+ try {
740
+ if (!await deadline.wait(settled)) throw new require_errors.ItdStateError(`освобождение подключаемых модулей не завершилось за ${this.#runtime.config.shutdownTimeout} мс`);
741
+ } finally {
742
+ deadline.cancel();
743
+ }
744
+ if (failed) throw failure;
745
+ }
746
+ };
747
+ function createFeatureHost(runtime, options) {
748
+ return new ClientFeatureHost(runtime, options);
749
+ }
750
+ //#endregion
268
751
  //#region src/resources/pagination.ts
269
752
  /** Схема пагинации эндпоинта. */
270
753
  const PaginationMode = Object.freeze({
@@ -382,6 +865,10 @@ function readOffsetPage(body, field, offset) {
382
865
  raw: body
383
866
  };
384
867
  }
868
+ /** Создаёт единый контракт пагинированной операции. */
869
+ function pageOperation(id, read) {
870
+ return require_url.defineBuiltInOperation(id, read);
871
+ }
385
872
  /**
386
873
  * Перебор страниц списка.
387
874
  *
@@ -507,6 +994,10 @@ var BaseResource = class {
507
994
  constructor(http) {
508
995
  this.http = http;
509
996
  }
997
+ /** Выполняет операцию без возвращаемого значения и отбрасывает служебное тело ответа. */
998
+ voidOperation(operation, options) {
999
+ return this.http.execute(operation, options);
1000
+ }
510
1001
  /**
511
1002
  * Собирает перебор страниц.
512
1003
  *
@@ -532,24 +1023,22 @@ var BaseResource = class {
532
1023
  * @example
533
1024
  * ```ts
534
1025
  * #feed = this.paginated<Post, FeedParams>({
535
- * operationId: 'posts.list',
1026
+ * operation: POSTS_LIST,
536
1027
  * path: () => '/api/posts',
537
1028
  * query: (p) => ({ tab: p.tab, limit: p.limit }),
538
1029
  * start: (p) => (p.cursor ? { cursor: p.cursor } : {}),
539
- * read: (body) => readCursorPage<Post>(body, 'posts'),
540
1030
  * mode: PaginationMode.Cursor,
541
1031
  * });
542
1032
  * ```
543
1033
  */
544
1034
  paginated(spec) {
545
1035
  const load = async (params, state, options = {}) => {
546
- const operationId = typeof spec.operationId === "function" ? spec.operationId(params) : spec.operationId;
547
- const body = await this.http.operation(operationId, {
1036
+ const operation = typeof spec.operation === "function" ? spec.operation(params) : spec.operation;
1037
+ return this.http.execute(operation, {
548
1038
  path: spec.path(params),
549
1039
  query: withPageState(spec.query(params), state),
550
1040
  ...options
551
1041
  });
552
- return spec.read(body, state);
553
1042
  };
554
1043
  return {
555
1044
  list: (params, options) => load(params, spec.start(params), options),
@@ -574,6 +1063,163 @@ function withPageState(query, state) {
574
1063
  };
575
1064
  }
576
1065
  //#endregion
1066
+ //#region src/resources/notifications.ts
1067
+ const NOTIFICATIONS_LIST = pageOperation("notifications.list", (body, request) => {
1068
+ const offset = Number(request.query?.offset ?? 0);
1069
+ const page = readOffsetPage(body, "notifications", Number.isFinite(offset) ? offset : 0);
1070
+ return {
1071
+ ...page,
1072
+ items: page.items.map(require_url.normalizeNotification)
1073
+ };
1074
+ });
1075
+ const NOTIFICATIONS_COUNT = require_url.defineBuiltInOperation("notifications.count", (body) => require_url.pickNumber(body, "count", 0));
1076
+ const markedCountOperation = (id) => require_url.defineBuiltInOperation(id, (body) => require_url.pickNumber(body, "markedCount", 0));
1077
+ const NOTIFICATIONS_MARK_READ = markedCountOperation("notifications.markRead");
1078
+ const NOTIFICATIONS_MARK_READ_BATCH = markedCountOperation("notifications.markReadBatch");
1079
+ const NOTIFICATIONS_MARK_ALL_READ = markedCountOperation("notifications.markAllRead");
1080
+ const NOTIFICATIONS_GET_SETTINGS = require_url.defineBuiltInOperation("notifications.getSettings", readSettings);
1081
+ const NOTIFICATIONS_UPDATE_SETTINGS = require_url.defineBuiltInOperation("notifications.updateSettings", readSettings);
1082
+ const NOTIFICATION_SETTING_KEYS = [
1083
+ "enabled",
1084
+ "sound",
1085
+ "follows",
1086
+ "wallPosts",
1087
+ "likes",
1088
+ "comments",
1089
+ "mentions"
1090
+ ];
1091
+ /**
1092
+ * Сколько идентификаторов уходит в одном запросе на отметку прочтения.
1093
+ *
1094
+ * Столько же отправляет сайт итд.com — значит на сервере, скорее всего, есть ограничение.
1095
+ */
1096
+ const READ_BATCH_SIZE = 20;
1097
+ /**
1098
+ * Читает настройки уведомлений.
1099
+ *
1100
+ * Сервер отдаёт плоский объект: `enabled`, `sound`, `follows`, `wallPosts`, `likes`,
1101
+ * `comments`, `mentions`. Отсутствующая настройка считается включённой — так же
1102
+ * ведёт себя сайт итд.com.
1103
+ */
1104
+ function readSettings(body) {
1105
+ const settings = {};
1106
+ for (const key of NOTIFICATION_SETTING_KEYS) settings[key] = require_url.pickBoolean(body, key, true);
1107
+ return settings;
1108
+ }
1109
+ /**
1110
+ * Уведомления: список, счётчик, отметки о прочтении, настройки.
1111
+ *
1112
+ * Доступна как `itd.notifications`. Все уведомления приведены к единой форме, поэтому
1113
+ * объекты отсюда и из потока событий можно складывать в один список.
1114
+ */
1115
+ var NotificationsResource = class extends BaseResource {
1116
+ /** Уведомления: `/api/notifications/`, пагинация по смещению. */
1117
+ #list = this.paginated({
1118
+ operation: NOTIFICATIONS_LIST,
1119
+ path: () => "/api/notifications/",
1120
+ query: (p) => ({ limit: p.limit }),
1121
+ start: (p) => ({ offset: p.offset ?? 0 }),
1122
+ mode: PaginationMode.Offset
1123
+ });
1124
+ /**
1125
+ * Загружает страницу уведомлений.
1126
+ *
1127
+ * Пагинация здесь основана на смещении.
1128
+ *
1129
+ * @example
1130
+ * ```ts
1131
+ * const page = await itd.notifications.list({ limit: 20 });
1132
+ * const next = await itd.notifications.list({ limit: 20, offset: page.nextOffset });
1133
+ * ```
1134
+ */
1135
+ list(params = {}, options = {}) {
1136
+ return this.#list.list(params, options);
1137
+ }
1138
+ /**
1139
+ * Перебирает уведомления.
1140
+ *
1141
+ * @example
1142
+ * ```ts
1143
+ * for await (const notification of itd.notifications.iterate()) {
1144
+ * console.log(formatNotificationText(notification));
1145
+ * }
1146
+ * ```
1147
+ */
1148
+ iterate(params = {}, options = {}) {
1149
+ return this.#list.iterate(params, options);
1150
+ }
1151
+ /** Загружает число непрочитанных уведомлений. */
1152
+ count(options = {}) {
1153
+ return this.http.execute(NOTIFICATIONS_COUNT, {
1154
+ path: "/api/notifications/count",
1155
+ ...options
1156
+ });
1157
+ }
1158
+ /**
1159
+ * Отмечает уведомление прочитанным.
1160
+ *
1161
+ * @returns сколько записей отметил сервер
1162
+ */
1163
+ markRead(notificationId, options = {}) {
1164
+ return this.http.execute(NOTIFICATIONS_MARK_READ, {
1165
+ path: `/api/notifications/${require_redact.encodePathSegment(notificationId, "notificationId")}/read`,
1166
+ ...options
1167
+ });
1168
+ }
1169
+ /**
1170
+ * Отмечает прочитанными сразу несколько уведомлений.
1171
+ *
1172
+ * Список автоматически режется на части по 20 идентификаторов — столько же отправляет
1173
+ * сайт итд.com, поэтому на сервере вероятен предел. Части уходят последовательно,
1174
+ * результат суммируется.
1175
+ *
1176
+ * @returns сколько записей отметил сервер суммарно
1177
+ */
1178
+ async markReadBatch(ids, options = {}) {
1179
+ let marked = 0;
1180
+ for (let index = 0; index < ids.length; index += READ_BATCH_SIZE) {
1181
+ const chunk = ids.slice(index, index + READ_BATCH_SIZE);
1182
+ marked += await this.http.execute(NOTIFICATIONS_MARK_READ_BATCH, {
1183
+ path: "/api/notifications/read-batch",
1184
+ body: { ids: chunk },
1185
+ ...options
1186
+ });
1187
+ }
1188
+ return marked;
1189
+ }
1190
+ /** Отмечает прочитанными все уведомления. */
1191
+ markAllRead(options = {}) {
1192
+ return this.http.execute(NOTIFICATIONS_MARK_ALL_READ, {
1193
+ path: "/api/notifications/read-all",
1194
+ ...options
1195
+ });
1196
+ }
1197
+ /** Загружает настройки уведомлений. */
1198
+ getSettings(options = {}) {
1199
+ return this.http.execute(NOTIFICATIONS_GET_SETTINGS, {
1200
+ path: "/api/notifications/settings",
1201
+ ...options
1202
+ });
1203
+ }
1204
+ /**
1205
+ * Обновляет настройки уведомлений.
1206
+ *
1207
+ * Отправляются только изменяемые поля, в том же виде, в каком сервер их возвращает.
1208
+ */
1209
+ async updateSettings(input, options = {}) {
1210
+ const payload = {};
1211
+ for (const key of NOTIFICATION_SETTING_KEYS) {
1212
+ const value = input[key];
1213
+ if (value !== void 0) payload[key] = value;
1214
+ }
1215
+ return this.http.execute(NOTIFICATIONS_UPDATE_SETTINGS, {
1216
+ path: "/api/notifications/settings",
1217
+ body: payload,
1218
+ ...options
1219
+ });
1220
+ }
1221
+ };
1222
+ //#endregion
577
1223
  //#region src/domain/time.ts
578
1224
  const NAIVE_STAMP = /^(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}(?:\.\d+)?)$/;
579
1225
  /**
@@ -627,11 +1273,11 @@ var StatusResource = class {
627
1273
  constructor(context) {
628
1274
  this.#context = context;
629
1275
  }
630
- async get(options = {}) {
631
- return normalizeStatus(await this.#context.request("get", {
1276
+ get(options = {}) {
1277
+ return this.#context.request("get", {
632
1278
  path: "/api/status",
633
1279
  ...options
634
- }));
1280
+ });
635
1281
  }
636
1282
  };
637
1283
  /** Встроенный модуль страницы состояния платформы. @internal */
@@ -646,7 +1292,8 @@ function createStatusFeature() {
646
1292
  operations: { get: {
647
1293
  method: "GET",
648
1294
  retrySafety: require_url.RetrySafety.Safe,
649
- service: require_url.STATUS_SERVICE
1295
+ service: require_url.STATUS_SERVICE,
1296
+ read: (body) => normalizeStatus(body)
650
1297
  } },
651
1298
  setup: (context) => ({ api: new StatusResource(context) })
652
1299
  };
@@ -811,6 +1458,13 @@ function resolveComment(input, allowReplyTo = false) {
811
1458
  }
812
1459
  //#endregion
813
1460
  //#region src/resources/comments.ts
1461
+ const COMMENTS_REPLIES = pageOperation("comments.replies", (body) => readPagedPage(body, "replies"));
1462
+ const COMMENTS_REPLY = require_url.passthroughOperation("comments.reply");
1463
+ const COMMENTS_UPDATE = require_url.passthroughOperation("comments.update");
1464
+ const COMMENTS_RESTORE = require_url.passthroughOperation("comments.restore");
1465
+ const COMMENTS_LIKE = require_url.passthroughOperation("comments.like");
1466
+ const COMMENTS_UNLIKE = require_url.passthroughOperation("comments.unlike");
1467
+ const COMMENTS_REMOVE = require_url.voidOperation("comments.remove");
814
1468
  /**
815
1469
  * Комментарии и ответы на них.
816
1470
  *
@@ -821,11 +1475,10 @@ var CommentsResource = class extends BaseResource {
821
1475
  #uploadFiles;
822
1476
  /** Ответы на комментарий: `/api/comments/{id}/replies`, постраничная пагинация. */
823
1477
  #replies = this.paginated({
824
- operationId: "comments.replies",
825
- path: (p) => `/api/comments/${require_cookies.encodePathSegment(p.commentId, "commentId")}/replies`,
1478
+ operation: COMMENTS_REPLIES,
1479
+ path: (p) => `/api/comments/${require_redact.encodePathSegment(p.commentId, "commentId")}/replies`,
826
1480
  query: (p) => ({ limit: p.limit }),
827
1481
  start: (p) => p.page !== void 0 ? { page: p.page } : {},
828
- read: (body) => readPagedPage(body, "replies"),
829
1482
  mode: PaginationMode.Page
830
1483
  });
831
1484
  constructor(http, deps) {
@@ -864,8 +1517,8 @@ var CommentsResource = class extends BaseResource {
864
1517
  const existing = data.attachmentIds ?? [];
865
1518
  const files = data.files ?? [];
866
1519
  const attachmentIds = files.length > 0 ? [...existing, ...await this.#uploadFiles(files, options)] : existing;
867
- return this.http.operation("comments.reply", {
868
- path: `/api/comments/${require_cookies.encodePathSegment(commentId, "commentId")}/replies`,
1520
+ return this.http.execute(COMMENTS_REPLY, {
1521
+ path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}/replies`,
869
1522
  body: {
870
1523
  content: data.content ?? "",
871
1524
  attachmentIds,
@@ -876,176 +1529,42 @@ var CommentsResource = class extends BaseResource {
876
1529
  }
877
1530
  /** Редактирует текст комментария. */
878
1531
  update(commentId, content, options = {}) {
879
- return this.http.operation("comments.update", {
880
- path: `/api/comments/${require_cookies.encodePathSegment(commentId, "commentId")}`,
1532
+ return this.http.execute(COMMENTS_UPDATE, {
1533
+ path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}`,
881
1534
  body: { content },
882
1535
  ...options
883
1536
  });
884
1537
  }
885
1538
  /** Удаляет комментарий. Восстановить его можно через {@link restore}. */
886
1539
  remove(commentId, options = {}) {
887
- return this.http.operation("comments.remove", {
888
- path: `/api/comments/${require_cookies.encodePathSegment(commentId, "commentId")}`,
1540
+ return this.voidOperation(COMMENTS_REMOVE, {
1541
+ path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}`,
889
1542
  ...options
890
1543
  });
891
1544
  }
892
1545
  /** Восстанавливает удалённый комментарий. */
893
1546
  restore(commentId, options = {}) {
894
- return this.http.operation("comments.restore", {
895
- path: `/api/comments/${require_cookies.encodePathSegment(commentId, "commentId")}/restore`,
1547
+ return this.http.execute(COMMENTS_RESTORE, {
1548
+ path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}/restore`,
896
1549
  ...options
897
1550
  });
898
1551
  }
899
1552
  /** Ставит реакцию на комментарий. */
900
1553
  like(commentId, options = {}) {
901
- return this.http.operation("comments.like", {
902
- path: `/api/comments/${require_cookies.encodePathSegment(commentId, "commentId")}/like`,
1554
+ return this.http.execute(COMMENTS_LIKE, {
1555
+ path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}/like`,
903
1556
  ...options
904
1557
  });
905
1558
  }
906
1559
  /** Убирает реакцию с комментария. */
907
1560
  unlike(commentId, options = {}) {
908
- return this.http.operation("comments.unlike", {
909
- path: `/api/comments/${require_cookies.encodePathSegment(commentId, "commentId")}/like`,
1561
+ return this.http.execute(COMMENTS_UNLIKE, {
1562
+ path: `/api/comments/${require_redact.encodePathSegment(commentId, "commentId")}/like`,
910
1563
  ...options
911
1564
  });
912
1565
  }
913
1566
  };
914
1567
  //#endregion
915
- //#region src/core/attachments/url-source.ts
916
- /** Убирает параметры MIME и приводит его к форме для сравнения. */
917
- function normalizeMimeType(contentType) {
918
- return contentType?.split(";", 1)[0]?.trim().toLowerCase() || void 0;
919
- }
920
- /** Достаёт имя файла из пути URL. */
921
- function filenameFromUrl(url) {
922
- const last = url.pathname.split("/").pop();
923
- if (!last) return void 0;
924
- try {
925
- return decodeURIComponent(last) || void 0;
926
- } catch {
927
- return last;
928
- }
929
- }
930
- /** Проверяет объявленный размер и возвращает его, если заголовок корректен. */
931
- function declaredSize(response, maxBytes, url) {
932
- const header = response.headers.get("content-length");
933
- if (header === null) return void 0;
934
- const size = Number(header);
935
- if (!Number.isFinite(size) || size < 0 || !Number.isInteger(size)) return void 0;
936
- if (maxBytes !== void 0 && size > maxBytes) throw require_options.fileTooLarge(url, maxBytes, size);
937
- return size;
938
- }
939
- /** Получает HTTP-ответ источника и проверяет его статус. */
940
- async function fetchFile(target, options, context) {
941
- let requested;
942
- try {
943
- requested = new URL(target);
944
- } catch {
945
- throw new require_errors.ItdConfigError(`«${target}» не разбирается как адрес`);
946
- }
947
- if (requested.protocol !== "http:" && requested.protocol !== "https:") throw new require_errors.ItdConfigError(`вложение по адресу поддерживает только http и https, получено: ${requested.protocol}`);
948
- let response;
949
- try {
950
- response = await context.fetch(requested, { ...context.signal ? { signal: context.signal } : {} });
951
- } catch (error) {
952
- if (context.signal?.aborted || error instanceof Error && error.name === "AbortError") throw error;
953
- throw new require_errors.ItdFileError(`не удалось получить файл по адресу ${requested.href}`, {
954
- reason: require_errors.ItdFileErrorReason.Network,
955
- url: requested.href,
956
- retryable: true,
957
- cause: error
958
- });
959
- }
960
- const finalUrl = response.url ? new URL(response.url) : requested;
961
- if (!response.ok) {
962
- await response.body?.cancel().catch(() => {});
963
- throw new require_errors.ItdFileError(`источник ${finalUrl.href} ответил статусом ${response.status}`, {
964
- reason: require_errors.ItdFileErrorReason.Http,
965
- url: finalUrl.href,
966
- status: response.status,
967
- retryable: response.status === 408 || response.status === 429 || response.status >= 500
968
- });
969
- }
970
- const { maxBytes } = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
971
- let size;
972
- try {
973
- size = declaredSize(response, maxBytes, finalUrl.href);
974
- } catch (error) {
975
- await response.body?.cancel().catch(() => {});
976
- throw error;
977
- }
978
- return {
979
- response,
980
- url: finalUrl,
981
- size
982
- };
983
- }
984
- /** Читает ответ с контролем размера до создания итогового `Blob`. */
985
- async function responseBlob(response, url, maxBytes, streamBufferBytes, signal) {
986
- if (!response.body) {
987
- const blob = await response.blob();
988
- if (maxBytes !== void 0 && blob.size > maxBytes) throw require_options.fileTooLarge(url, maxBytes, blob.size);
989
- return blob;
990
- }
991
- const chunks = [];
992
- const reader = require_options.boundedFileStream(response.body, {
993
- ...maxBytes !== void 0 ? { maxBytes } : {},
994
- streamBufferBytes,
995
- ...signal ? { signal } : {},
996
- url,
997
- retryableRead: true
998
- }).getReader();
999
- try {
1000
- for (;;) {
1001
- const next = await reader.read();
1002
- if (next.done) break;
1003
- chunks.push(next.value);
1004
- }
1005
- } finally {
1006
- reader.releaseLock();
1007
- }
1008
- return new Blob(chunks.map((chunk) => Uint8Array.from(chunk).buffer));
1009
- }
1010
- /** Скачивает файл целиком с ограничением размера. @internal */
1011
- async function downloadFile(target, options, context) {
1012
- const resolved = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
1013
- const { response, url } = await fetchFile(target, options, context);
1014
- const blob = await responseBlob(response, url.href, resolved.maxBytes, resolved.streamBufferBytes, context.signal);
1015
- const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
1016
- const filename = options.filename ?? filenameFromUrl(url);
1017
- return {
1018
- file: new Blob([blob], { type: contentType ?? "" }),
1019
- ...filename ? { filename } : {},
1020
- ...contentType ? { contentType } : {}
1021
- };
1022
- }
1023
- /** Открывает HTTP-ответ как ограниченный поток. @internal */
1024
- async function openUrlFile(target, options, context) {
1025
- const resolved = require_options.resolveFileStreamOptions(options, require_options.DEFAULT_URL_FILE_MAX_BYTES);
1026
- const { response, url, size } = await fetchFile(target, options, context);
1027
- if (!response.body) throw new require_errors.ItdFileError(`источник ${url.href} не предоставил потоковое тело`, {
1028
- reason: require_errors.ItdFileErrorReason.StreamUnavailable,
1029
- url: url.href
1030
- });
1031
- const stream = require_options.boundedFileStream(response.body, {
1032
- ...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
1033
- streamBufferBytes: resolved.streamBufferBytes,
1034
- ...context.signal ? { signal: context.signal } : {},
1035
- url: url.href,
1036
- retryableRead: true
1037
- });
1038
- const filename = options.filename ?? filenameFromUrl(url);
1039
- const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
1040
- return {
1041
- stream,
1042
- ...filename ? { filename } : {},
1043
- ...contentType ? { contentType } : {},
1044
- ...size !== void 0 ? { size } : {},
1045
- close: () => stream.cancel().catch(() => {})
1046
- };
1047
- }
1048
- //#endregion
1049
1568
  //#region src/core/multipart.ts
1050
1569
  const encoder = new TextEncoder();
1051
1570
  /** Убирает возможность внедрить дополнительный multipart-заголовок через имя файла. */
@@ -1182,12 +1701,17 @@ function assertAllowedMime(mimeType, filename) {
1182
1701
  if (!mimeType) throw new require_errors.ItdConfigError(`Не удалось определить тип файла${filename ? ` «${filename}»` : ""}. Укажите его явно: { file, filename, contentType }.`);
1183
1702
  if (!isAllowedMime(mimeType)) throw new require_errors.ItdConfigError(`Тип «${mimeType}» не поддерживается загрузкой. Допустимые: ${ALLOWED_MIME_TYPES.join(", ")}.`);
1184
1703
  }
1704
+ //#endregion
1705
+ //#region src/resources/files.ts
1706
+ const FILES_UPLOAD = require_url.passthroughOperation("files.upload");
1707
+ const FILES_GET = require_url.passthroughOperation("files.get");
1708
+ const FILES_REMOVE = require_url.voidOperation("files.remove");
1185
1709
  /** Файлы и медиа. */
1186
1710
  var FilesResource = class extends BaseResource {
1187
- #fetch;
1711
+ #files;
1188
1712
  constructor(http, deps) {
1189
1713
  super(http);
1190
- this.#fetch = deps.fetch;
1714
+ this.#files = deps.files;
1191
1715
  }
1192
1716
  /**
1193
1717
  * Загружает файл и возвращает его идентификатор.
@@ -1197,7 +1721,7 @@ var FilesResource = class extends BaseResource {
1197
1721
  */
1198
1722
  upload(input, uploadOptions = {}, requestOptions = {}) {
1199
1723
  const bodyFactory = this.#createBodyFactory(input, uploadOptions);
1200
- return this.http.operation("files.upload", {
1724
+ return this.http.execute(FILES_UPLOAD, {
1201
1725
  path: "/api/files/upload",
1202
1726
  bodyFactory,
1203
1727
  ...requestOptions,
@@ -1216,372 +1740,150 @@ var FilesResource = class extends BaseResource {
1216
1740
  * Для ещё не прикреплённого файла сервер может ответить `404`.
1217
1741
  */
1218
1742
  get(fileId, options = {}) {
1219
- return this.http.operation("files.get", {
1220
- path: `/api/files/${require_cookies.encodePathSegment(fileId, "fileId")}`,
1743
+ return this.http.execute(FILES_GET, {
1744
+ path: `/api/files/${require_redact.encodePathSegment(fileId, "fileId")}`,
1221
1745
  ...options
1222
1746
  });
1223
1747
  }
1224
1748
  /** Удаляет загруженный файл. */
1225
1749
  remove(fileId, options = {}) {
1226
- return this.http.operation("files.remove", {
1227
- path: `/api/files/${require_cookies.encodePathSegment(fileId, "fileId")}`,
1228
- ...options
1229
- });
1230
- }
1231
- /**
1232
- * Создаёт фабрику тела. Успешно подготовленный буфер кешируется, а поток открывается
1233
- * заново, поэтому каждая транспортная попытка получает непрочитанное тело.
1234
- */
1235
- #createBodyFactory(input, options) {
1236
- const streamMode = this.#isStreamInput(input);
1237
- let buffered;
1238
- return async ({ signal, attempt }) => {
1239
- const context = {
1240
- fetch: this.#fetch,
1241
- signal,
1242
- attempt
1243
- };
1244
- let prepared;
1245
- try {
1246
- if (streamMode) prepared = await this.#prepareStream(input, options, context);
1247
- else {
1248
- buffered ??= this.#prepareBuffer(input, options, context).catch((error) => {
1249
- buffered = void 0;
1250
- throw error;
1251
- });
1252
- prepared = await buffered;
1253
- }
1254
- } catch (error) {
1255
- if (error instanceof require_errors.ItdError || signal.aborted) throw error;
1256
- throw new require_errors.ItdFileError("не удалось получить содержимое вложения", {
1257
- reason: require_errors.ItdFileErrorReason.Read,
1258
- retryable: true,
1259
- cause: error
1260
- });
1261
- }
1262
- if (prepared.mode === "buffer") {
1263
- const form = new FormData();
1264
- form.set("file", prepared.blob, prepared.filename);
1265
- return { body: form };
1266
- }
1267
- const multipart = createMultipartFileBody(prepared);
1268
- return {
1269
- body: multipart.body,
1270
- headers: { "Content-Type": multipart.contentType },
1271
- cleanup: async () => {
1272
- await multipart.cancel();
1273
- await prepared.close?.();
1274
- }
1275
- };
1276
- };
1277
- }
1278
- /** Определяет режим без чтения источника. */
1279
- #isStreamInput(input) {
1280
- if (typeof input !== "object" || input === null) return false;
1281
- if ("open" in input) return true;
1282
- if ("url" in input) return require_options.resolveFileStreamOptions(input).mode === require_options.FileTransferMode.Stream;
1283
- return false;
1284
- }
1285
- /** Получает и проверяет буферный источник. */
1286
- async #prepareBuffer(input, options, context) {
1287
- const content = await this.#resolveBuffer(input, context);
1288
- const filename = options.filename ?? content.filename ?? this.#nameFromMime(options.contentType ?? content.contentType);
1289
- const contentType = normalizeMimeType(options.contentType ?? content.contentType) ?? normalizeMimeType(require_url.isBlob(content.file) ? content.file.type : void 0) ?? mimeFromFilename(filename);
1290
- if (options.validateMime !== false) assertAllowedMime(contentType, filename);
1291
- const blob = require_url.isBlob(content.file) && (!contentType || content.file.type === contentType) ? content.file : new Blob([content.file], { type: contentType ?? "" });
1292
- const limits = require_options.resolveFileStreamOptions(options);
1293
- if (limits.maxBytes !== void 0 && blob.size > limits.maxBytes) throw new require_errors.ItdFileError(`файл больше предела в ${limits.maxBytes} байт: ${blob.size}`, {
1294
- reason: require_errors.ItdFileErrorReason.TooLarge,
1295
- limit: limits.maxBytes,
1296
- actual: blob.size
1297
- });
1298
- return {
1299
- mode: "buffer",
1300
- blob,
1301
- filename
1302
- };
1303
- }
1304
- /** Открывает и проверяет потоковый источник. */
1305
- async #prepareStream(input, options, context) {
1306
- let opened;
1307
- if (typeof input === "object" && input !== null && "open" in input) opened = await input.open(context);
1308
- else if (typeof input === "object" && input !== null && "url" in input) {
1309
- const { url, ...urlOptions } = input;
1310
- opened = await openUrlFile(url, urlOptions, context);
1311
- } else throw new require_errors.ItdConfigError("потоковое вложение должно иметь форму { open } или { url, mode }");
1312
- if (!opened || typeof ReadableStream === "undefined" || !(opened.stream instanceof ReadableStream)) {
1313
- await opened?.close?.();
1314
- throw new require_errors.ItdConfigError("потоковый источник должен вернуть { stream: ReadableStream }");
1315
- }
1316
- try {
1317
- const filename = options.filename ?? opened.filename ?? this.#nameFromMime(options.contentType ?? opened.contentType);
1318
- const contentType = normalizeMimeType(options.contentType ?? opened.contentType) ?? mimeFromFilename(filename);
1319
- if (options.validateMime !== false) assertAllowedMime(contentType, filename);
1320
- const limits = require_options.resolveFileStreamOptions({
1321
- mode: require_options.FileTransferMode.Stream,
1322
- ...options.maxBytes !== void 0 ? { maxBytes: options.maxBytes } : {},
1323
- ...options.streamBufferBytes !== void 0 ? { streamBufferBytes: options.streamBufferBytes } : {}
1324
- });
1325
- if (limits.maxBytes !== void 0 && opened.size !== void 0 && opened.size > limits.maxBytes) throw new require_errors.ItdFileError(`файл больше предела в ${limits.maxBytes} байт: ${opened.size}`, {
1326
- reason: require_errors.ItdFileErrorReason.TooLarge,
1327
- limit: limits.maxBytes,
1328
- actual: opened.size
1329
- });
1330
- return {
1331
- mode: "stream",
1332
- stream: require_options.isBoundedFileStream(opened.stream) && options.maxBytes === void 0 && options.streamBufferBytes === void 0 ? opened.stream : require_options.boundedFileStream(opened.stream, {
1333
- ...limits.maxBytes !== void 0 ? { maxBytes: limits.maxBytes } : {},
1334
- streamBufferBytes: limits.streamBufferBytes,
1335
- signal: context.signal
1336
- }),
1337
- filename,
1338
- contentType: contentType ?? "",
1339
- ...opened.close ? { close: opened.close } : {}
1340
- };
1341
- } catch (error) {
1342
- await opened.close?.();
1343
- throw error;
1344
- }
1345
- }
1346
- /** Разрешает только буферные формы. */
1347
- #resolveBuffer(input, context) {
1348
- if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || require_url.isBlob(input)) return {
1349
- file: input,
1350
- ...require_url.isFile(input) ? { filename: input.name } : {}
1351
- };
1352
- if (typeof input !== "object" || input === null) throw new require_errors.ItdConfigError(`вложение задано значением типа ${typeof input}; ожидается бинарное значение или объект { file }, { url }, { load }, { open }. Для файла на диске используйте fromPath('./photo.jpg') из itd-api/node`);
1353
- if ("load" in input) return input.load(context);
1354
- if ("url" in input) {
1355
- const { url, ...urlOptions } = input;
1356
- return downloadFile(url, urlOptions, context);
1357
- }
1358
- if ("file" in input) return input;
1359
- if ("open" in input) throw new require_errors.ItdConfigError("потоковый источник нельзя использовать как буферный");
1360
- throw new require_errors.ItdConfigError("вложение не распознано: ожидается { file }, { url }, { load } или { open }");
1361
- }
1362
- /** Подбирает непустое имя, обязательное для multipart. */
1363
- #nameFromMime(contentType) {
1364
- const extension = normalizeMimeType(contentType)?.split("/")[1];
1365
- return extension ? `file.${extension}` : "file";
1366
- }
1367
- };
1368
- //#endregion
1369
- //#region src/resources/hashtags.ts
1370
- /**
1371
- * Хэштеги.
1372
- *
1373
- * Доступна как `itd.hashtags`.
1374
- */
1375
- var HashtagsResource = class extends BaseResource {
1376
- /** Посты по хэштегу: `/api/hashtags/{tag}/posts`, курсорная пагинация. */
1377
- #posts = this.paginated({
1378
- operationId: "hashtags.posts",
1379
- path: (p) => `/api/hashtags/${require_cookies.encodePathSegment(p.tag, "tag")}/posts`,
1380
- query: (p) => ({ limit: p.limit }),
1381
- start: (p) => p.cursor ? { cursor: p.cursor } : {},
1382
- read: (body) => readCursorPage(body, "posts"),
1383
- mode: PaginationMode.Cursor
1384
- });
1385
- /**
1386
- * Ищет хэштеги.
1387
- *
1388
- * Без строки запроса возвращает общий список.
1389
- */
1390
- async search(query, params = {}, options = {}) {
1391
- const body = await this.http.operation("hashtags.search", {
1392
- path: "/api/hashtags",
1393
- query: {
1394
- q: query,
1395
- limit: params.limit
1396
- },
1397
- ...options
1398
- });
1399
- return require_url.pickArray(body, "hashtags");
1400
- }
1401
- /** Загружает трендовые хэштеги. */
1402
- async trending(params = {}, options = {}) {
1403
- const body = await this.http.operation("hashtags.trending", {
1404
- path: "/api/hashtags/trending",
1405
- query: { limit: params.limit },
1406
- ...options
1407
- });
1408
- return require_url.pickArray(body, "hashtags");
1409
- }
1410
- /**
1411
- * Загружает страницу постов по хэштегу.
1412
- *
1413
- * @param tag название без решётки; кодируется автоматически, поэтому кириллица
1414
- * и пробелы допустимы
1415
- */
1416
- posts(tag, params = {}, options = {}) {
1417
- return this.#posts.list({
1418
- ...params,
1419
- tag
1420
- }, options);
1421
- }
1422
- /** Перебирает посты по хэштегу. */
1423
- iteratePosts(tag, params = {}, options = {}) {
1424
- return this.#posts.iterate({
1425
- ...params,
1426
- tag
1427
- }, options);
1428
- }
1429
- };
1430
- //#endregion
1431
- //#region src/resources/notifications.ts
1432
- const NOTIFICATION_SETTING_KEYS = [
1433
- "enabled",
1434
- "sound",
1435
- "follows",
1436
- "wallPosts",
1437
- "likes",
1438
- "comments",
1439
- "mentions"
1440
- ];
1441
- /**
1442
- * Сколько идентификаторов уходит в одном запросе на отметку прочтения.
1443
- *
1444
- * Столько же отправляет сайт итд.com — значит на сервере, скорее всего, есть ограничение.
1445
- */
1446
- const READ_BATCH_SIZE = 20;
1447
- /**
1448
- * Читает настройки уведомлений.
1449
- *
1450
- * Сервер отдаёт плоский объект: `enabled`, `sound`, `follows`, `wallPosts`, `likes`,
1451
- * `comments`, `mentions`. Отсутствующая настройка считается включённой — так же
1452
- * ведёт себя сайт итд.com.
1453
- */
1454
- function readSettings(body) {
1455
- const settings = {};
1456
- for (const key of NOTIFICATION_SETTING_KEYS) settings[key] = require_url.pickBoolean(body, key, true);
1457
- return settings;
1458
- }
1459
- /**
1460
- * Уведомления: список, счётчик, отметки о прочтении, настройки.
1461
- *
1462
- * Доступна как `itd.notifications`. Все уведомления приведены к единой форме, поэтому
1463
- * объекты отсюда и из потока событий можно складывать в один список.
1464
- */
1465
- var NotificationsResource = class extends BaseResource {
1466
- /** Уведомления: `/api/notifications/`, пагинация по смещению. */
1467
- #list = this.paginated({
1468
- operationId: "notifications.list",
1469
- path: () => "/api/notifications/",
1470
- query: (p) => ({ limit: p.limit }),
1471
- start: (p) => ({ offset: p.offset ?? 0 }),
1472
- read: (body, state) => {
1473
- const page = readOffsetPage(body, "notifications", state.offset ?? 0);
1474
- return {
1475
- ...page,
1476
- items: page.items.map(require_url.normalizeNotification)
1477
- };
1478
- },
1479
- mode: PaginationMode.Offset
1480
- });
1481
- /**
1482
- * Загружает страницу уведомлений.
1483
- *
1484
- * Пагинация здесь основана на смещении.
1485
- *
1486
- * @example
1487
- * ```ts
1488
- * const page = await itd.notifications.list({ limit: 20 });
1489
- * const next = await itd.notifications.list({ limit: 20, offset: page.nextOffset });
1490
- * ```
1491
- */
1492
- list(params = {}, options = {}) {
1493
- return this.#list.list(params, options);
1494
- }
1495
- /**
1496
- * Перебирает уведомления.
1497
- *
1498
- * @example
1499
- * ```ts
1500
- * for await (const notification of itd.notifications.iterate()) {
1501
- * console.log(formatNotificationText(notification));
1502
- * }
1503
- * ```
1504
- */
1505
- iterate(params = {}, options = {}) {
1506
- return this.#list.iterate(params, options);
1507
- }
1508
- /** Загружает число непрочитанных уведомлений. */
1509
- async count(options = {}) {
1510
- const body = await this.http.operation("notifications.count", {
1511
- path: "/api/notifications/count",
1512
- ...options
1513
- });
1514
- return require_url.pickNumber(body, "count", 0);
1515
- }
1516
- /**
1517
- * Отмечает уведомление прочитанным.
1518
- *
1519
- * @returns сколько записей отметил сервер
1520
- */
1521
- async markRead(notificationId, options = {}) {
1522
- const body = await this.http.operation("notifications.markRead", {
1523
- path: `/api/notifications/${require_cookies.encodePathSegment(notificationId, "notificationId")}/read`,
1750
+ return this.voidOperation(FILES_REMOVE, {
1751
+ path: `/api/files/${require_redact.encodePathSegment(fileId, "fileId")}`,
1524
1752
  ...options
1525
1753
  });
1526
- return require_url.pickNumber(body, "markedCount", 0);
1527
1754
  }
1528
1755
  /**
1529
- * Отмечает прочитанными сразу несколько уведомлений.
1530
- *
1531
- * Список автоматически режется на части по 20 идентификаторов — столько же отправляет
1532
- * сайт итд.com, поэтому на сервере вероятен предел. Части уходят последовательно,
1533
- * результат суммируется.
1534
- *
1535
- * @returns сколько записей отметил сервер суммарно
1756
+ * Создаёт фабрику тела. Успешно подготовленный буфер кешируется, а поток открывается
1757
+ * заново, поэтому каждая транспортная попытка получает непрочитанное тело.
1536
1758
  */
1537
- async markReadBatch(ids, options = {}) {
1538
- let marked = 0;
1539
- for (let index = 0; index < ids.length; index += READ_BATCH_SIZE) {
1540
- const chunk = ids.slice(index, index + READ_BATCH_SIZE);
1541
- const body = await this.http.operation("notifications.markReadBatch", {
1542
- path: "/api/notifications/read-batch",
1543
- body: { ids: chunk },
1544
- ...options
1759
+ #createBodyFactory(input, options) {
1760
+ const streamMode = this.#files.mode(input) === require_options.FileTransferMode.Stream;
1761
+ let buffered;
1762
+ return async ({ signal, attempt }) => {
1763
+ let prepared;
1764
+ if (streamMode) prepared = await this.#files.resolve(input, options, {
1765
+ signal,
1766
+ attempt
1545
1767
  });
1546
- marked += require_url.pickNumber(body, "markedCount", 0);
1547
- }
1548
- return marked;
1768
+ else {
1769
+ buffered ??= this.#files.resolve(input, options, {
1770
+ signal,
1771
+ attempt
1772
+ }).then((source) => source).catch((error) => {
1773
+ buffered = void 0;
1774
+ throw error;
1775
+ });
1776
+ prepared = await buffered;
1777
+ }
1778
+ if (prepared.mode === require_options.FileTransferMode.Buffer) {
1779
+ const filename = this.#filename(prepared, options);
1780
+ const contentType = prepared.contentType ?? mimeFromFilename(filename);
1781
+ if (options.validateMime !== false) assertAllowedMime(contentType, filename);
1782
+ const blob = !contentType || prepared.blob.type === contentType ? prepared.blob : new Blob([prepared.blob], { type: contentType });
1783
+ const form = new FormData();
1784
+ form.set("file", blob, filename);
1785
+ return { body: form };
1786
+ }
1787
+ let multipart;
1788
+ try {
1789
+ const filename = this.#filename(prepared, options);
1790
+ const contentType = prepared.contentType ?? mimeFromFilename(filename);
1791
+ if (options.validateMime !== false) assertAllowedMime(contentType, filename);
1792
+ multipart = createMultipartFileBody({
1793
+ stream: prepared.stream,
1794
+ filename,
1795
+ contentType: contentType ?? ""
1796
+ });
1797
+ return {
1798
+ body: multipart.body,
1799
+ headers: { "Content-Type": multipart.contentType },
1800
+ cleanup: async () => {
1801
+ await multipart?.cancel();
1802
+ await prepared.close();
1803
+ }
1804
+ };
1805
+ } catch (error) {
1806
+ await prepared.close();
1807
+ throw error;
1808
+ }
1809
+ };
1549
1810
  }
1550
- /** Отмечает прочитанными все уведомления. */
1551
- async markAllRead(options = {}) {
1552
- const body = await this.http.operation("notifications.markAllRead", {
1553
- path: "/api/notifications/read-all",
1811
+ /** Подбирает непустое имя, обязательное для multipart. */
1812
+ #filename(source, options) {
1813
+ if (options.filename) return options.filename;
1814
+ if (source.filename) return source.filename;
1815
+ const extension = options.contentType?.split(";", 1)[0]?.trim().split("/")[1];
1816
+ return extension ? `file.${extension}` : "file";
1817
+ }
1818
+ };
1819
+ //#endregion
1820
+ //#region src/resources/hashtags.ts
1821
+ const HASHTAG_POSTS = pageOperation("hashtags.posts", (body) => readCursorPage(body, "posts"));
1822
+ const HASHTAGS_SEARCH = require_url.defineBuiltInOperation("hashtags.search", (body) => require_url.pickArray(body, "hashtags"));
1823
+ const HASHTAGS_TRENDING = require_url.defineBuiltInOperation("hashtags.trending", (body) => require_url.pickArray(body, "hashtags"));
1824
+ /**
1825
+ * Хэштеги.
1826
+ *
1827
+ * Доступна как `itd.hashtags`.
1828
+ */
1829
+ var HashtagsResource = class extends BaseResource {
1830
+ /** Посты по хэштегу: `/api/hashtags/{tag}/posts`, курсорная пагинация. */
1831
+ #posts = this.paginated({
1832
+ operation: HASHTAG_POSTS,
1833
+ path: (p) => `/api/hashtags/${require_redact.encodePathSegment(p.tag, "tag")}/posts`,
1834
+ query: (p) => ({ limit: p.limit }),
1835
+ start: (p) => p.cursor ? { cursor: p.cursor } : {},
1836
+ mode: PaginationMode.Cursor
1837
+ });
1838
+ /**
1839
+ * Ищет хэштеги.
1840
+ *
1841
+ * Без строки запроса возвращает общий список.
1842
+ */
1843
+ search(query, params = {}, options = {}) {
1844
+ return this.http.execute(HASHTAGS_SEARCH, {
1845
+ path: "/api/hashtags",
1846
+ query: {
1847
+ q: query,
1848
+ limit: params.limit
1849
+ },
1554
1850
  ...options
1555
1851
  });
1556
- return require_url.pickNumber(body, "markedCount", 0);
1557
1852
  }
1558
- /** Загружает настройки уведомлений. */
1559
- async getSettings(options = {}) {
1560
- return readSettings(await this.http.operation("notifications.getSettings", {
1561
- path: "/api/notifications/settings",
1853
+ /** Загружает трендовые хэштеги. */
1854
+ trending(params = {}, options = {}) {
1855
+ return this.http.execute(HASHTAGS_TRENDING, {
1856
+ path: "/api/hashtags/trending",
1857
+ query: { limit: params.limit },
1562
1858
  ...options
1563
- }));
1859
+ });
1564
1860
  }
1565
1861
  /**
1566
- * Обновляет настройки уведомлений.
1862
+ * Загружает страницу постов по хэштегу.
1567
1863
  *
1568
- * Отправляются только изменяемые поля, в том же виде, в каком сервер их возвращает.
1864
+ * @param tag название без решётки; кодируется автоматически, поэтому кириллица
1865
+ * и пробелы допустимы
1569
1866
  */
1570
- async updateSettings(input, options = {}) {
1571
- const payload = {};
1572
- for (const key of NOTIFICATION_SETTING_KEYS) {
1573
- const value = input[key];
1574
- if (value !== void 0) payload[key] = value;
1575
- }
1576
- return readSettings(await this.http.operation("notifications.updateSettings", {
1577
- path: "/api/notifications/settings",
1578
- body: payload,
1579
- ...options
1580
- }));
1867
+ posts(tag, params = {}, options = {}) {
1868
+ return this.#posts.list({
1869
+ ...params,
1870
+ tag
1871
+ }, options);
1872
+ }
1873
+ /** Перебирает посты по хэштегу. */
1874
+ iteratePosts(tag, params = {}, options = {}) {
1875
+ return this.#posts.iterate({
1876
+ ...params,
1877
+ tag
1878
+ }, options);
1581
1879
  }
1582
1880
  };
1583
1881
  //#endregion
1584
1882
  //#region src/resources/platform.ts
1883
+ const PLATFORM_CHANGELOG = require_url.defineBuiltInOperation("platform.changelog", (body) => Array.isArray(body) ? body : []);
1884
+ const PLATFORM_ANNOUNCEMENTS = require_url.defineBuiltInOperation("platform.announcements", (body) => require_url.pickArray(body, "announcements"));
1885
+ const PLATFORM_VERSION = require_url.passthroughOperation("platform.version");
1886
+ const PLATFORM_PORTAL = require_url.passthroughOperation("platform.portal");
1585
1887
  /**
1586
1888
  * Сведения о платформе: версии приложений, изменения, анонсы, баннер события.
1587
1889
  *
@@ -1606,31 +1908,29 @@ var PlatformResource = class extends BaseResource {
1606
1908
  * ```
1607
1909
  */
1608
1910
  version(options = {}) {
1609
- return this.http.operation("platform.version", {
1911
+ return this.http.execute(PLATFORM_VERSION, {
1610
1912
  path: "/api/platform/version",
1611
1913
  skipAuth: true,
1612
1914
  ...options
1613
1915
  });
1614
1916
  }
1615
1917
  /** Загружает журнал изменений. */
1616
- async changelog(options = {}) {
1617
- const body = await this.http.operation("platform.changelog", {
1918
+ changelog(options = {}) {
1919
+ return this.http.execute(PLATFORM_CHANGELOG, {
1618
1920
  path: "/api/platform/changelog",
1619
1921
  ...options
1620
1922
  });
1621
- return Array.isArray(body) ? body : [];
1622
1923
  }
1623
1924
  /** Загружает анонсы платформы. */
1624
- async announcements(options = {}) {
1625
- const body = await this.http.operation("platform.announcements", {
1925
+ announcements(options = {}) {
1926
+ return this.http.execute(PLATFORM_ANNOUNCEMENTS, {
1626
1927
  path: "/api/platform/announcements",
1627
1928
  ...options
1628
1929
  });
1629
- return require_url.pickArray(body, "announcements");
1630
1930
  }
1631
1931
  /** Загружает баннер текущего события — виджет «портал». */
1632
1932
  portal(options = {}) {
1633
- return this.http.operation("platform.portal", {
1933
+ return this.http.execute(PLATFORM_PORTAL, {
1634
1934
  path: "/api/v1/portal",
1635
1935
  ...options
1636
1936
  });
@@ -2347,7 +2647,7 @@ function autoSpans(text, options = {}) {
2347
2647
  });
2348
2648
  }
2349
2649
  if (includeLinks) spans.push(...links);
2350
- if (includeHashtags || includeMentions) for (const match of text.matchAll(/#([\p{L}\p{M}\p{N}_]+)|@([a-z\d][a-z\d._-]*)/giu)) {
2650
+ if (includeHashtags || includeMentions) for (const match of text.matchAll(/#([\p{L}\p{M}\p{N}_]+)|@([a-z\d](?:[a-z\d_]*[a-z\d])?)/giu)) {
2351
2651
  if (match.index === void 0) continue;
2352
2652
  const offset = match.index;
2353
2653
  const previous = previousCodePoint(text, offset);
@@ -2355,7 +2655,7 @@ function autoSpans(text, options = {}) {
2355
2655
  const end = offset + match[0].length;
2356
2656
  if (links.some((span) => overlaps(span, offset, end))) continue;
2357
2657
  const hashtag = match[1];
2358
- const username = match[2]?.replace(/[._-]+$/u, "");
2658
+ const username = match[2];
2359
2659
  if (hashtag !== void 0 && includeHashtags) spans.push({
2360
2660
  type: require_url.SpanType.Hashtag,
2361
2661
  offset,
@@ -2748,6 +3048,24 @@ function resolvePostUpdate(input) {
2748
3048
  }
2749
3049
  //#endregion
2750
3050
  //#region src/resources/posts.ts
3051
+ const POSTS_LIST = pageOperation("posts.list", (body) => readCursorPage(body, "posts"));
3052
+ const POSTS_BY_USER = pageOperation("posts.byUser", (body) => readCursorPage(body, "posts"));
3053
+ const POSTS_LIKED_BY_USER = pageOperation("posts.likedByUser", (body) => readCursorPage(body, "posts"));
3054
+ const POSTS_COMMENTS = pageOperation("posts.comments", (body) => readFlatCursorPage(body, "comments"));
3055
+ const POSTS_STATS = require_url.defineBuiltInOperation("posts.stats", (body) => require_url.pickArray(body, "posts"));
3056
+ const POSTS_CREATE = require_url.passthroughOperation("posts.create");
3057
+ const POSTS_GET = require_url.passthroughOperation("posts.get");
3058
+ const POSTS_UPDATE = require_url.passthroughOperation("posts.update");
3059
+ const POSTS_RESTORE = require_url.passthroughOperation("posts.restore");
3060
+ const POSTS_LIKE = require_url.passthroughOperation("posts.like");
3061
+ const POSTS_UNLIKE = require_url.passthroughOperation("posts.unlike");
3062
+ const POSTS_REPOST = require_url.passthroughOperation("posts.repost");
3063
+ const POSTS_PIN = require_url.passthroughOperation("posts.pin");
3064
+ const POSTS_UNPIN = require_url.passthroughOperation("posts.unpin");
3065
+ const POSTS_VOTE = require_url.passthroughOperation("posts.vote");
3066
+ const POSTS_COMMENT = require_url.passthroughOperation("posts.comment");
3067
+ const POSTS_REMOVE = require_url.voidOperation("posts.remove");
3068
+ const POSTS_UNREPOST = require_url.voidOperation("posts.unrepost");
2751
3069
  /** Курсорная позиция из параметров: если курсор задан — с него, иначе с начала. */
2752
3070
  function cursorStart(params) {
2753
3071
  return params.cursor ? { cursor: params.cursor } : {};
@@ -2761,48 +3079,44 @@ var PostsResource = class extends BaseResource {
2761
3079
  #uploadFiles;
2762
3080
  /** Лента: `/api/posts`, курсорная пагинация. */
2763
3081
  #feed = this.paginated({
2764
- operationId: "posts.list",
3082
+ operation: POSTS_LIST,
2765
3083
  path: () => "/api/posts",
2766
3084
  query: (p) => ({
2767
3085
  tab: p.tab,
2768
3086
  limit: p.limit
2769
3087
  }),
2770
3088
  start: cursorStart,
2771
- read: (body) => readCursorPage(body, "posts"),
2772
3089
  mode: PaginationMode.Cursor
2773
3090
  });
2774
3091
  /** Стена пользователя: `/api/posts/user/{user}`. */
2775
3092
  #wall = this.paginated({
2776
- operationId: "posts.byUser",
2777
- path: (p) => `/api/posts/user/${require_cookies.encodePathSegment(p.user, "user")}`,
3093
+ operation: POSTS_BY_USER,
3094
+ path: (p) => `/api/posts/user/${require_redact.encodePathSegment(p.user, "user")}`,
2778
3095
  query: (p) => ({
2779
3096
  limit: p.limit,
2780
3097
  sort: p.sort,
2781
3098
  pinnedPostId: p.pinnedPostId
2782
3099
  }),
2783
3100
  start: cursorStart,
2784
- read: (body) => readCursorPage(body, "posts"),
2785
3101
  mode: PaginationMode.Cursor
2786
3102
  });
2787
3103
  /** Понравившиеся посты пользователя: `/api/posts/user/{user}/liked`. */
2788
3104
  #liked = this.paginated({
2789
- operationId: "posts.likedByUser",
2790
- path: (p) => `/api/posts/user/${require_cookies.encodePathSegment(p.user, "user")}/liked`,
3105
+ operation: POSTS_LIKED_BY_USER,
3106
+ path: (p) => `/api/posts/user/${require_redact.encodePathSegment(p.user, "user")}/liked`,
2791
3107
  query: (p) => ({ limit: p.limit }),
2792
3108
  start: cursorStart,
2793
- read: (body) => readCursorPage(body, "posts"),
2794
3109
  mode: PaginationMode.Cursor
2795
3110
  });
2796
3111
  /** Комментарии к посту: курсор лежит рядом со списком, поэтому свой reader. */
2797
3112
  #comments = this.paginated({
2798
- operationId: "posts.comments",
2799
- path: (p) => `/api/posts/${require_cookies.encodePathSegment(p.postId, "postId")}/comments`,
3113
+ operation: POSTS_COMMENTS,
3114
+ path: (p) => `/api/posts/${require_redact.encodePathSegment(p.postId, "postId")}/comments`,
2800
3115
  query: (p) => ({
2801
3116
  limit: p.limit,
2802
3117
  sort: p.sort
2803
3118
  }),
2804
3119
  start: cursorStart,
2805
- read: (body) => readFlatCursorPage(body, "comments"),
2806
3120
  mode: PaginationMode.Cursor
2807
3121
  });
2808
3122
  constructor(http, deps) {
@@ -2849,7 +3163,7 @@ var PostsResource = class extends BaseResource {
2849
3163
  async create(input, options = {}) {
2850
3164
  const data = resolvePost(input);
2851
3165
  const attachmentIds = await this.#collectAttachments(data, options);
2852
- return this.http.operation("posts.create", {
3166
+ return this.http.execute(POSTS_CREATE, {
2853
3167
  path: "/api/posts",
2854
3168
  body: {
2855
3169
  content: data.content ?? "",
@@ -2867,8 +3181,8 @@ var PostsResource = class extends BaseResource {
2867
3181
  * В отличие от списков, здесь у поста заполнено поле `comments`.
2868
3182
  */
2869
3183
  get(postId, options = {}) {
2870
- return this.http.operation("posts.get", {
2871
- path: `/api/posts/${require_cookies.encodePathSegment(postId, "postId")}`,
3184
+ return this.http.execute(POSTS_GET, {
3185
+ path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}`,
2872
3186
  ...options
2873
3187
  });
2874
3188
  }
@@ -2881,8 +3195,8 @@ var PostsResource = class extends BaseResource {
2881
3195
  */
2882
3196
  update(postId, input, options = {}) {
2883
3197
  const data = resolvePostUpdate(input);
2884
- return this.http.operation("posts.update", {
2885
- path: `/api/posts/${require_cookies.encodePathSegment(postId, "postId")}`,
3198
+ return this.http.execute(POSTS_UPDATE, {
3199
+ path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}`,
2886
3200
  body: {
2887
3201
  content: data.content,
2888
3202
  ...data.spans ? { spans: data.spans } : {}
@@ -2892,29 +3206,29 @@ var PostsResource = class extends BaseResource {
2892
3206
  }
2893
3207
  /** Удаляет пост. Восстановить его можно через {@link restore}. */
2894
3208
  remove(postId, options = {}) {
2895
- return this.http.operation("posts.remove", {
2896
- path: `/api/posts/${require_cookies.encodePathSegment(postId, "postId")}`,
3209
+ return this.voidOperation(POSTS_REMOVE, {
3210
+ path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}`,
2897
3211
  ...options
2898
3212
  });
2899
3213
  }
2900
3214
  /** Восстанавливает удалённый пост. */
2901
3215
  restore(postId, options = {}) {
2902
- return this.http.operation("posts.restore", {
2903
- path: `/api/posts/${require_cookies.encodePathSegment(postId, "postId")}/restore`,
3216
+ return this.http.execute(POSTS_RESTORE, {
3217
+ path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/restore`,
2904
3218
  ...options
2905
3219
  });
2906
3220
  }
2907
3221
  /** Ставит реакцию на пост. */
2908
3222
  like(postId, options = {}) {
2909
- return this.http.operation("posts.like", {
2910
- path: `/api/posts/${require_cookies.encodePathSegment(postId, "postId")}/like`,
3223
+ return this.http.execute(POSTS_LIKE, {
3224
+ path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/like`,
2911
3225
  ...options
2912
3226
  });
2913
3227
  }
2914
3228
  /** Убирает реакцию с поста. */
2915
3229
  unlike(postId, options = {}) {
2916
- return this.http.operation("posts.unlike", {
2917
- path: `/api/posts/${require_cookies.encodePathSegment(postId, "postId")}/like`,
3230
+ return this.http.execute(POSTS_UNLIKE, {
3231
+ path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/like`,
2918
3232
  ...options
2919
3233
  });
2920
3234
  }
@@ -2925,30 +3239,30 @@ var PostsResource = class extends BaseResource {
2925
3239
  * для файлов здесь нет.
2926
3240
  */
2927
3241
  repost(postId, content = "", options = {}) {
2928
- return this.http.operation("posts.repost", {
2929
- path: `/api/posts/${require_cookies.encodePathSegment(postId, "postId")}/repost`,
3242
+ return this.http.execute(POSTS_REPOST, {
3243
+ path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/repost`,
2930
3244
  body: { content },
2931
3245
  ...options
2932
3246
  });
2933
3247
  }
2934
3248
  /** Отменяет репост. */
2935
3249
  unrepost(postId, options = {}) {
2936
- return this.http.operation("posts.unrepost", {
2937
- path: `/api/posts/${require_cookies.encodePathSegment(postId, "postId")}/repost`,
3250
+ return this.voidOperation(POSTS_UNREPOST, {
3251
+ path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/repost`,
2938
3252
  ...options
2939
3253
  });
2940
3254
  }
2941
3255
  /** Закрепляет пост в профиле. */
2942
3256
  pin(postId, options = {}) {
2943
- return this.http.operation("posts.pin", {
2944
- path: `/api/posts/${require_cookies.encodePathSegment(postId, "postId")}/pin`,
3257
+ return this.http.execute(POSTS_PIN, {
3258
+ path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/pin`,
2945
3259
  ...options
2946
3260
  });
2947
3261
  }
2948
3262
  /** Открепляет пост. */
2949
3263
  unpin(postId, options = {}) {
2950
- return this.http.operation("posts.unpin", {
2951
- path: `/api/posts/${require_cookies.encodePathSegment(postId, "postId")}/pin`,
3264
+ return this.http.execute(POSTS_UNPIN, {
3265
+ path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/pin`,
2952
3266
  ...options
2953
3267
  });
2954
3268
  }
@@ -2958,20 +3272,19 @@ var PostsResource = class extends BaseResource {
2958
3272
  * @param optionIds выбранные варианты; несколько допустимы только при `multipleChoice`
2959
3273
  */
2960
3274
  vote(postId, optionIds, options = {}) {
2961
- return this.http.operation("posts.vote", {
2962
- path: `/api/posts/${require_cookies.encodePathSegment(postId, "postId")}/poll/vote`,
3275
+ return this.http.execute(POSTS_VOTE, {
3276
+ path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/poll/vote`,
2963
3277
  body: { optionIds },
2964
3278
  ...options
2965
3279
  });
2966
3280
  }
2967
3281
  /** Запрашивает счётчики сразу для нескольких постов. */
2968
- async stats(ids, options = {}) {
2969
- const body = await this.http.operation("posts.stats", {
3282
+ stats(ids, options = {}) {
3283
+ return this.http.execute(POSTS_STATS, {
2970
3284
  path: "/api/posts/stats",
2971
3285
  body: { ids },
2972
3286
  ...options
2973
3287
  });
2974
- return require_url.pickArray(body, "posts");
2975
3288
  }
2976
3289
  /**
2977
3290
  * Загружает страницу стены пользователя.
@@ -3041,8 +3354,8 @@ var PostsResource = class extends BaseResource {
3041
3354
  async comment(postId, input, options = {}) {
3042
3355
  const data = resolveComment(typeof input === "string" ? { content: input } : input);
3043
3356
  const attachmentIds = await this.#collectAttachments(data, options);
3044
- return this.http.operation("posts.comment", {
3045
- path: `/api/posts/${require_cookies.encodePathSegment(postId, "postId")}/comments`,
3357
+ return this.http.execute(POSTS_COMMENT, {
3358
+ path: `/api/posts/${require_redact.encodePathSegment(postId, "postId")}/comments`,
3046
3359
  body: {
3047
3360
  content: data.content ?? "",
3048
3361
  attachmentIds
@@ -3162,6 +3475,7 @@ function resolveReport(input) {
3162
3475
  }
3163
3476
  //#endregion
3164
3477
  //#region src/resources/reports.ts
3478
+ const REPORTS_CREATE = require_url.passthroughOperation("reports.create");
3165
3479
  /**
3166
3480
  * Жалобы на контент и пользователей.
3167
3481
  *
@@ -3182,7 +3496,7 @@ var ReportsResource = class extends BaseResource {
3182
3496
  */
3183
3497
  create(input, options = {}) {
3184
3498
  const data = resolveReport(input);
3185
- return this.http.operation("reports.create", {
3499
+ return this.http.execute(REPORTS_CREATE, {
3186
3500
  path: "/api/reports",
3187
3501
  body: data,
3188
3502
  ...options
@@ -3191,6 +3505,10 @@ var ReportsResource = class extends BaseResource {
3191
3505
  };
3192
3506
  //#endregion
3193
3507
  //#region src/resources/search.ts
3508
+ const SEARCH_ALL = require_url.defineBuiltInOperation("search.all", (body) => ({
3509
+ users: require_url.pickArray(body, "users"),
3510
+ hashtags: require_url.pickArray(body, "hashtags")
3511
+ }));
3194
3512
  /**
3195
3513
  * Глобальный поиск.
3196
3514
  *
@@ -3205,20 +3523,212 @@ var SearchResource = class extends BaseResource {
3205
3523
  * const { users, hashtags } = await itd.search.all('арт');
3206
3524
  * ```
3207
3525
  */
3208
- async all(query, options = {}) {
3209
- const body = await this.http.operation("search.all", {
3526
+ all(query, options = {}) {
3527
+ return this.http.execute(SEARCH_ALL, {
3210
3528
  path: "/api/search",
3211
3529
  query: { q: query },
3212
3530
  ...options
3213
3531
  });
3214
- return {
3215
- users: require_url.pickArray(body, "users"),
3216
- hashtags: require_url.pickArray(body, "hashtags")
3217
- };
3532
+ }
3533
+ };
3534
+ //#endregion
3535
+ //#region src/resources/shop.ts
3536
+ const PRODUCTS_LIST = require_url.passthroughOperation("shop.products.list");
3537
+ const PRODUCTS_GET = require_url.passthroughOperation("shop.products.get");
3538
+ const DELIVERY_COUNTRIES = require_url.passthroughOperation("shop.delivery.countries");
3539
+ const DELIVERY_CITIES = require_url.passthroughOperation("shop.delivery.cities");
3540
+ const DELIVERY_POINTS = require_url.passthroughOperation("shop.delivery.points");
3541
+ const DELIVERY_CALCULATE = require_url.passthroughOperation("shop.delivery.calculate");
3542
+ const ORDERS_CREATE = require_url.passthroughOperation("shop.orders.create");
3543
+ const ORDERS_LIST = require_url.passthroughOperation("shop.orders.list");
3544
+ const ORDERS_GET = require_url.passthroughOperation("shop.orders.get");
3545
+ const ORDERS_PAY = require_url.passthroughOperation("shop.orders.pay");
3546
+ const ORDERS_REQUEST_ACCESS_CODE = require_url.voidOperation("shop.orders.requestAccessCode");
3547
+ const ORDERS_VERIFY_ACCESS_CODE = require_url.passthroughOperation("shop.orders.verifyAccessCode");
3548
+ const CONSENTS_RECORD = require_url.voidOperation("shop.consents.record");
3549
+ function orderRequestOptions(options) {
3550
+ const { orderAccessToken, useItdAuth, headers, ...request } = options;
3551
+ return {
3552
+ ...request,
3553
+ ...useItdAuth === false ? { skipAuth: true } : {},
3554
+ headers: {
3555
+ ...headers,
3556
+ ...orderAccessToken ? { "X-Order-Token": orderAccessToken } : {}
3557
+ }
3558
+ };
3559
+ }
3560
+ /** Каталог товаров магазина. */
3561
+ var ShopProductsResource = class extends BaseResource {
3562
+ /** Загружает все товары. */
3563
+ list(options = {}) {
3564
+ return this.http.execute(PRODUCTS_LIST, {
3565
+ path: "/api/v1/shop/products",
3566
+ raw: true,
3567
+ ...options
3568
+ });
3569
+ }
3570
+ /** Загружает товар по идентификатору. */
3571
+ async get(productId, options = {}) {
3572
+ try {
3573
+ return await this.http.execute(PRODUCTS_GET, {
3574
+ path: `/api/v1/shop/products/${require_redact.encodePathSegment(productId, "идентификатор товара")}`,
3575
+ raw: true,
3576
+ ...options
3577
+ });
3578
+ } catch (error) {
3579
+ if (require_errors.isItdApiError(error) && error.status === 404) return null;
3580
+ throw error;
3581
+ }
3582
+ }
3583
+ };
3584
+ /** Расчёт и выбор доставки. */
3585
+ var ShopDeliveryResource = class extends BaseResource {
3586
+ /** Загружает страны, в которые доступна доставка. */
3587
+ countries(options = {}) {
3588
+ return this.http.execute(DELIVERY_COUNTRIES, {
3589
+ path: "/api/v1/shop/delivery/countries",
3590
+ raw: true,
3591
+ ...options
3592
+ });
3593
+ }
3594
+ /** Ищет города службы доставки. */
3595
+ cities(query, countryCode, options = {}) {
3596
+ return this.http.execute(DELIVERY_CITIES, {
3597
+ path: "/api/v1/shop/delivery/cities",
3598
+ query: {
3599
+ q: query,
3600
+ country: countryCode
3601
+ },
3602
+ raw: true,
3603
+ ...options
3604
+ });
3605
+ }
3606
+ /** Загружает пункты выдачи в городе. */
3607
+ points(cityCode, options = {}) {
3608
+ return this.http.execute(DELIVERY_POINTS, {
3609
+ path: "/api/v1/shop/delivery/points",
3610
+ query: { cityCode },
3611
+ raw: true,
3612
+ ...options
3613
+ });
3614
+ }
3615
+ /** Рассчитывает стоимость и срок доставки. */
3616
+ calculate(items, destination, options = {}) {
3617
+ return this.http.execute(DELIVERY_CALCULATE, {
3618
+ path: "/api/v1/shop/delivery/calculate",
3619
+ body: {
3620
+ items,
3621
+ to: destination
3622
+ },
3623
+ raw: true,
3624
+ ...options
3625
+ });
3626
+ }
3627
+ };
3628
+ /** Создание, просмотр и оплата заказов. */
3629
+ var ShopOrdersResource = class extends BaseResource {
3630
+ /** Создаёт заказ. */
3631
+ create(input, options = {}) {
3632
+ const { idempotencyKey = require_redact.createDeviceId(), headers, ...request } = options;
3633
+ return this.http.execute(ORDERS_CREATE, {
3634
+ path: "/api/v1/shop/orders",
3635
+ body: input,
3636
+ headers: {
3637
+ ...headers,
3638
+ "Idempotency-Key": idempotencyKey
3639
+ },
3640
+ raw: true,
3641
+ ...request
3642
+ });
3643
+ }
3644
+ /** Загружает заказы аккаунта ИТД или владельца временного токена. */
3645
+ list(options = {}) {
3646
+ return this.http.execute(ORDERS_LIST, {
3647
+ path: "/api/v1/shop/orders/my",
3648
+ raw: true,
3649
+ ...orderRequestOptions(options)
3650
+ });
3651
+ }
3652
+ /** Загружает заказ по номеру. */
3653
+ get(orderNumber, options = {}) {
3654
+ return this.http.execute(ORDERS_GET, {
3655
+ path: `/api/v1/shop/orders/${require_redact.encodePathSegment(orderNumber, "номер заказа")}`,
3656
+ raw: true,
3657
+ ...orderRequestOptions(options)
3658
+ });
3659
+ }
3660
+ /** Создаёт ссылку на оплату заказа. */
3661
+ pay(orderNumber, options) {
3662
+ const { returnUrl, ...access } = options;
3663
+ return this.http.execute(ORDERS_PAY, {
3664
+ path: `/api/v1/shop/orders/${require_redact.encodePathSegment(orderNumber, "номер заказа")}/pay`,
3665
+ body: { returnUrl },
3666
+ raw: true,
3667
+ ...orderRequestOptions(access)
3668
+ });
3669
+ }
3670
+ /** Отправляет на почту код доступа к гостевым заказам. */
3671
+ requestAccessCode(email, options = {}) {
3672
+ return this.http.execute(ORDERS_REQUEST_ACCESS_CODE, {
3673
+ path: "/api/v1/shop/orders/lookup/request",
3674
+ body: { email },
3675
+ raw: true,
3676
+ skipAuth: true,
3677
+ ...options
3678
+ });
3679
+ }
3680
+ /** Проверяет код из письма и возвращает временный токен заказов. */
3681
+ verifyAccessCode(email, code, options = {}) {
3682
+ return this.http.execute(ORDERS_VERIFY_ACCESS_CODE, {
3683
+ path: "/api/v1/shop/orders/lookup/verify",
3684
+ body: {
3685
+ email,
3686
+ code
3687
+ },
3688
+ raw: true,
3689
+ skipAuth: true,
3690
+ ...options
3691
+ });
3692
+ }
3693
+ };
3694
+ /** Запись согласий магазина. */
3695
+ var ShopConsentsResource = class extends BaseResource {
3696
+ /** Записывает согласия покупателя. */
3697
+ record(consents, consentContext, options = {}) {
3698
+ return this.http.execute(CONSENTS_RECORD, {
3699
+ path: "/api/v1/shop/consents",
3700
+ body: {
3701
+ consents,
3702
+ consentContext
3703
+ },
3704
+ raw: true,
3705
+ ...options
3706
+ });
3707
+ }
3708
+ };
3709
+ /** Магазин ИТД. */
3710
+ var ShopResource = class {
3711
+ products;
3712
+ delivery;
3713
+ orders;
3714
+ consents;
3715
+ /** @internal */
3716
+ constructor(http) {
3717
+ this.products = new ShopProductsResource(http);
3718
+ this.delivery = new ShopDeliveryResource(http);
3719
+ this.orders = new ShopOrdersResource(http);
3720
+ this.consents = new ShopConsentsResource(http);
3218
3721
  }
3219
3722
  };
3220
3723
  //#endregion
3221
3724
  //#region src/resources/subscription.ts
3725
+ const SUBSCRIPTION_METHODS = require_url.defineBuiltInOperation("subscription.methods", (body) => Array.isArray(body) ? body : []);
3726
+ const SUBSCRIPTION_STATUS = require_url.passthroughOperation("subscription.status");
3727
+ const SUBSCRIPTION_PAY = require_url.passthroughOperation("subscription.pay");
3728
+ const SUBSCRIPTION_SET_AUTO_RENEWAL = require_url.passthroughOperation("subscription.setAutoRenewal");
3729
+ const SUBSCRIPTION_BIND_CARD = require_url.passthroughOperation("subscription.bindCard");
3730
+ const SUBSCRIPTION_SET_DEFAULT_METHOD = require_url.passthroughOperation("subscription.setDefaultMethod");
3731
+ const SUBSCRIPTION_REMOVE_METHOD = require_url.voidOperation("subscription.removeMethod");
3222
3732
  /**
3223
3733
  * Подписка и способы оплаты.
3224
3734
  *
@@ -3227,7 +3737,7 @@ var SearchResource = class extends BaseResource {
3227
3737
  var SubscriptionResource = class extends BaseResource {
3228
3738
  /** Загружает состояние подписки и её цену. */
3229
3739
  status(options = {}) {
3230
- return this.http.operation("subscription.status", {
3740
+ return this.http.execute(SUBSCRIPTION_STATUS, {
3231
3741
  path: "/api/v1/subscription/",
3232
3742
  ...options
3233
3743
  });
@@ -3238,14 +3748,14 @@ var SubscriptionResource = class extends BaseResource {
3238
3748
  * Форма ответа в документации API не описана, поэтому тип результата не уточняется.
3239
3749
  */
3240
3750
  pay(options = {}) {
3241
- return this.http.operation("subscription.pay", {
3751
+ return this.http.execute(SUBSCRIPTION_PAY, {
3242
3752
  path: "/api/v1/subscription/pay",
3243
3753
  ...options
3244
3754
  });
3245
3755
  }
3246
3756
  /** Включает или отключает автопродление. */
3247
3757
  setAutoRenewal(enabled, options = {}) {
3248
- return this.http.operation("subscription.setAutoRenewal", {
3758
+ return this.http.execute(SUBSCRIPTION_SET_AUTO_RENEWAL, {
3249
3759
  path: "/api/v1/subscription/auto-renewal",
3250
3760
  body: { enabled },
3251
3761
  ...options
@@ -3253,30 +3763,29 @@ var SubscriptionResource = class extends BaseResource {
3253
3763
  }
3254
3764
  /** Запускает привязку карты. */
3255
3765
  bindCard(options = {}) {
3256
- return this.http.operation("subscription.bindCard", {
3766
+ return this.http.execute(SUBSCRIPTION_BIND_CARD, {
3257
3767
  path: "/api/v1/subscription/bind-card",
3258
3768
  ...options
3259
3769
  });
3260
3770
  }
3261
3771
  /** Загружает список способов оплаты. Пустой массив, если карт нет. */
3262
- async methods(options = {}) {
3263
- const body = await this.http.operation("subscription.methods", {
3772
+ methods(options = {}) {
3773
+ return this.http.execute(SUBSCRIPTION_METHODS, {
3264
3774
  path: "/api/v1/subscription/methods",
3265
3775
  ...options
3266
3776
  });
3267
- return Array.isArray(body) ? body : [];
3268
3777
  }
3269
3778
  /** Делает способ оплаты основным. */
3270
3779
  setDefaultMethod(methodId, options = {}) {
3271
- return this.http.operation("subscription.setDefaultMethod", {
3272
- path: `/api/v1/subscription/methods/${require_cookies.encodePathSegment(methodId, "methodId")}/default`,
3780
+ return this.http.execute(SUBSCRIPTION_SET_DEFAULT_METHOD, {
3781
+ path: `/api/v1/subscription/methods/${require_redact.encodePathSegment(methodId, "methodId")}/default`,
3273
3782
  ...options
3274
3783
  });
3275
3784
  }
3276
3785
  /** Удаляет способ оплаты. */
3277
3786
  removeMethod(methodId, options = {}) {
3278
- return this.http.operation("subscription.removeMethod", {
3279
- path: `/api/v1/subscription/methods/${require_cookies.encodePathSegment(methodId, "methodId")}`,
3787
+ return this.voidOperation(SUBSCRIPTION_REMOVE_METHOD, {
3788
+ path: `/api/v1/subscription/methods/${require_redact.encodePathSegment(methodId, "methodId")}`,
3280
3789
  ...options
3281
3790
  });
3282
3791
  }
@@ -3434,7 +3943,7 @@ var TelemetryBatchImpl = class {
3434
3943
  close() {
3435
3944
  return this.#close(false);
3436
3945
  }
3437
- /** Внутренняя финализация после перехода клиента в terminal state. */
3946
+ /** Внутренняя отправка данных после вызова `dispose()`. */
3438
3947
  closeForDispose() {
3439
3948
  this.prepareForDispose();
3440
3949
  return this.#close(true);
@@ -3504,7 +4013,7 @@ var TelemetryResource = class extends BaseResource {
3504
4013
  }
3505
4014
  /** Идентификатор сессии телеметрии, общий для всех событий этого ресурса. */
3506
4015
  get sessionId() {
3507
- this.#sessionId ??= require_url.createDeviceId();
4016
+ this.#sessionId ??= require_redact.createDeviceId();
3508
4017
  return this.#sessionId;
3509
4018
  }
3510
4019
  /** Отправляет события просмотра постов (`POST /api/v1/i`). */
@@ -3530,7 +4039,7 @@ var TelemetryResource = class extends BaseResource {
3530
4039
  },
3531
4040
  ...requestOptions
3532
4041
  };
3533
- return disposeCleanup ? this.http.cleanupOperation("telemetry.dwell", options) : this.http.operation("telemetry.dwell", options);
4042
+ return disposeCleanup ? this.http.cleanupOperation(TELEMETRY_DWELL, options) : this.http.execute(TELEMETRY_DWELL, options);
3534
4043
  }
3535
4044
  /** Отправляет события взаимодействия с контентом (`POST /api/v1/x`). */
3536
4045
  interaction(entries, telemetryOptions = {}, requestOptions = {}) {
@@ -3554,7 +4063,7 @@ var TelemetryResource = class extends BaseResource {
3554
4063
  },
3555
4064
  ...requestOptions
3556
4065
  };
3557
- return disposeCleanup ? this.http.cleanupOperation("telemetry.interaction", options) : this.http.operation("telemetry.interaction", options);
4066
+ return disposeCleanup ? this.http.cleanupOperation(TELEMETRY_INTERACTION, options) : this.http.execute(TELEMETRY_INTERACTION, options);
3558
4067
  }
3559
4068
  /** Начинает измерять время просмотра и отправляет результат после `finish()`. */
3560
4069
  startView(input, options = {}, requestOptions = {}) {
@@ -3597,12 +4106,43 @@ var TelemetryResource = class extends BaseResource {
3597
4106
  function prepareTelemetryForDispose(resource) {
3598
4107
  if (resource) TELEMETRY_DISPOSE_PREPARERS.get(resource)?.();
3599
4108
  }
3600
- /** Финализирует созданные накопители после перехода клиента в terminal state. @internal */
4109
+ /** Отправляет и закрывает созданные накопители после вызова `dispose()`. @internal */
3601
4110
  function closeTelemetryForDispose(resource) {
3602
4111
  return resource ? TELEMETRY_DISPOSE_CLOSERS.get(resource)?.() ?? Promise.resolve() : Promise.resolve();
3603
4112
  }
4113
+ const TELEMETRY_DWELL = require_url.defineBuiltInOperation("telemetry.dwell");
4114
+ const TELEMETRY_INTERACTION = require_url.defineBuiltInOperation("telemetry.interaction");
3604
4115
  //#endregion
3605
4116
  //#region src/resources/users.ts
4117
+ const userListOperation = (id) => pageOperation(id, (body) => readPagedPage(body, "users", "followers", "following", "blocked"));
4118
+ const USER_LIST_OPERATIONS = {
4119
+ "users.followers": userListOperation("users.followers"),
4120
+ "users.following": userListOperation("users.following"),
4121
+ "users.blocked": userListOperation("users.blocked")
4122
+ };
4123
+ const USERS_CHECK_USERNAME = require_url.defineBuiltInOperation("users.checkUsername", (body) => require_url.pickBoolean(body, "available"));
4124
+ const USERS_SEARCH = require_url.defineBuiltInOperation("users.search", (body) => require_url.pickArray(body, "users"));
4125
+ const USERS_WHO_TO_FOLLOW = require_url.defineBuiltInOperation("users.whoToFollow", (body) => require_url.pickArray(body, "users"));
4126
+ const USERS_TOP_CLANS = require_url.defineBuiltInOperation("users.topClans", (body) => require_url.pickArray(body, "clans"));
4127
+ const USERS_PINS = require_url.defineBuiltInOperation("users.pins", (body) => ({
4128
+ pins: require_url.pickArray(body, "pins"),
4129
+ activePin: require_url.pickString(body, "activePin") ?? null
4130
+ }));
4131
+ const USERS_ME = require_url.passthroughOperation("users.me");
4132
+ const USERS_UPDATE_ME = require_url.passthroughOperation("users.updateMe");
4133
+ const USERS_CREATE_PROFILE = require_url.passthroughOperation("users.createProfile");
4134
+ const USERS_GET = require_url.passthroughOperation("users.get");
4135
+ const USERS_FOLLOW = require_url.passthroughOperation("users.follow");
4136
+ const USERS_FOLLOW_STATUS = require_url.passthroughOperation("users.followStatus");
4137
+ const USERS_GET_PRIVACY = require_url.passthroughOperation("users.getPrivacy");
4138
+ const USERS_UPDATE_PRIVACY = require_url.passthroughOperation("users.updatePrivacy");
4139
+ const USERS_DEACTIVATE = require_url.voidOperation("users.deactivate");
4140
+ const USERS_RESTORE = require_url.voidOperation("users.restore");
4141
+ const USERS_UNFOLLOW = require_url.voidOperation("users.unfollow");
4142
+ const USERS_BLOCK = require_url.voidOperation("users.block");
4143
+ const USERS_UNBLOCK = require_url.voidOperation("users.unblock");
4144
+ const USERS_SET_PIN = require_url.voidOperation("users.setPin");
4145
+ const USERS_REMOVE_PIN = require_url.voidOperation("users.removePin");
3606
4146
  /**
3607
4147
  * Пользователи: профили, подписки, блокировки, приватность.
3608
4148
  *
@@ -3623,23 +4163,22 @@ var UsersResource = class extends BaseResource {
3623
4163
  * сервер его сейчас не читает (см. {@link followers}): когда починят — заработает само.
3624
4164
  */
3625
4165
  #userList = this.paginated({
3626
- operationId: (p) => p.operationId,
4166
+ operation: (p) => p.operation,
3627
4167
  path: (p) => p.path,
3628
4168
  query: (p) => ({ limit: p.limit }),
3629
4169
  start: (p) => p.page !== void 0 ? { page: p.page } : {},
3630
- read: (body) => readPagedPage(body, "users", "followers", "following", "blocked"),
3631
4170
  mode: PaginationMode.Page
3632
4171
  });
3633
4172
  /** Загружает свой профиль — с подпиской и признаком подтверждённого телефона. */
3634
4173
  me(options = {}) {
3635
- return this.http.operation("users.me", {
4174
+ return this.http.execute(USERS_ME, {
3636
4175
  path: "/api/users/me",
3637
4176
  ...options
3638
4177
  });
3639
4178
  }
3640
4179
  /** Обновляет свой профиль. Передавайте только изменяемые поля. */
3641
4180
  updateMe(input, options = {}) {
3642
- return this.http.operation("users.updateMe", {
4181
+ return this.http.execute(USERS_UPDATE_ME, {
3643
4182
  path: "/api/users/me",
3644
4183
  body: input,
3645
4184
  ...options
@@ -3666,21 +4205,21 @@ var UsersResource = class extends BaseResource {
3666
4205
  }
3667
4206
  /** Деактивирует аккаунт. Вернуть его можно через {@link restore}. */
3668
4207
  deactivate(options = {}) {
3669
- return this.http.operation("users.deactivate", {
4208
+ return this.voidOperation(USERS_DEACTIVATE, {
3670
4209
  path: "/api/users/me",
3671
4210
  ...options
3672
4211
  });
3673
4212
  }
3674
4213
  /** Восстанавливает деактивированный аккаунт. */
3675
4214
  restore(options = {}) {
3676
- return this.http.operation("users.restore", {
4215
+ return this.voidOperation(USERS_RESTORE, {
3677
4216
  path: "/api/users/me/restore",
3678
4217
  ...options
3679
4218
  });
3680
4219
  }
3681
4220
  /** Создаёт профиль после регистрации. */
3682
4221
  createProfile(input, options = {}) {
3683
- return this.http.operation("users.createProfile", {
4222
+ return this.http.execute(USERS_CREATE_PROFILE, {
3684
4223
  path: "/api/users/profile",
3685
4224
  body: input,
3686
4225
  ...options
@@ -3698,23 +4237,22 @@ var UsersResource = class extends BaseResource {
3698
4237
  * ```
3699
4238
  */
3700
4239
  get(user, options = {}) {
3701
- return this.http.operation("users.get", {
3702
- path: `/api/users/${require_cookies.encodePathSegment(user, "user")}`,
4240
+ return this.http.execute(USERS_GET, {
4241
+ path: `/api/users/${require_redact.encodePathSegment(user, "user")}`,
3703
4242
  ...options
3704
4243
  });
3705
4244
  }
3706
4245
  /** Проверяет, свободно ли имя пользователя. */
3707
- async checkUsername(username, options = {}) {
3708
- const body = await this.http.operation("users.checkUsername", {
4246
+ checkUsername(username, options = {}) {
4247
+ return this.http.execute(USERS_CHECK_USERNAME, {
3709
4248
  path: "/api/users/check-username",
3710
4249
  query: { username },
3711
4250
  ...options
3712
4251
  });
3713
- return require_url.pickBoolean(body, "available");
3714
4252
  }
3715
4253
  /** Ищет пользователей по строке запроса. */
3716
- async search(query, params = {}, options = {}) {
3717
- const body = await this.http.operation("users.search", {
4254
+ search(query, params = {}, options = {}) {
4255
+ return this.http.execute(USERS_SEARCH, {
3718
4256
  path: "/api/users/search",
3719
4257
  query: {
3720
4258
  q: query,
@@ -3722,23 +4260,20 @@ var UsersResource = class extends BaseResource {
3722
4260
  },
3723
4261
  ...options
3724
4262
  });
3725
- return require_url.pickArray(body, "users");
3726
4263
  }
3727
4264
  /** Загружает рекомендации, на кого подписаться. */
3728
- async whoToFollow(options = {}) {
3729
- const body = await this.http.operation("users.whoToFollow", {
4265
+ whoToFollow(options = {}) {
4266
+ return this.http.execute(USERS_WHO_TO_FOLLOW, {
3730
4267
  path: "/api/users/suggestions/who-to-follow",
3731
4268
  ...options
3732
4269
  });
3733
- return require_url.pickArray(body, "users");
3734
4270
  }
3735
4271
  /** Загружает рейтинг кланов. */
3736
- async topClans(options = {}) {
3737
- const body = await this.http.operation("users.topClans", {
4272
+ topClans(options = {}) {
4273
+ return this.http.execute(USERS_TOP_CLANS, {
3738
4274
  path: "/api/users/stats/top-clans",
3739
4275
  ...options
3740
4276
  });
3741
- return require_url.pickArray(body, "clans");
3742
4277
  }
3743
4278
  /**
3744
4279
  * Подписывается на пользователя.
@@ -3746,16 +4281,16 @@ var UsersResource = class extends BaseResource {
3746
4281
  * У закрытого профиля вместо подписки отправляется заявка — это видно по полю `status`.
3747
4282
  */
3748
4283
  follow(user, options = {}) {
3749
- return this.http.operation("users.follow", {
3750
- path: `/api/users/${require_cookies.encodePathSegment(user, "user")}/follow`,
4284
+ return this.http.execute(USERS_FOLLOW, {
4285
+ path: `/api/users/${require_redact.encodePathSegment(user, "user")}/follow`,
3751
4286
  body: {},
3752
4287
  ...options
3753
4288
  });
3754
4289
  }
3755
4290
  /** Отписывается от пользователя. */
3756
4291
  unfollow(user, options = {}) {
3757
- return this.http.operation("users.unfollow", {
3758
- path: `/api/users/${require_cookies.encodePathSegment(user, "user")}/follow`,
4292
+ return this.voidOperation(USERS_UNFOLLOW, {
4293
+ path: `/api/users/${require_redact.encodePathSegment(user, "user")}/follow`,
3759
4294
  ...options
3760
4295
  });
3761
4296
  }
@@ -3771,7 +4306,7 @@ var UsersResource = class extends BaseResource {
3771
4306
  * на проверенных аккаунтах занижено примерно на 1–4%.
3772
4307
  */
3773
4308
  followers(user, params = {}, options = {}) {
3774
- return this.#userPage("users.followers", `/api/users/${require_cookies.encodePathSegment(user, "user")}/followers`, params, options);
4309
+ return this.#userPage("users.followers", `/api/users/${require_redact.encodePathSegment(user, "user")}/followers`, params, options);
3775
4310
  }
3776
4311
  /**
3777
4312
  * Перебирает подписчиков.
@@ -3780,15 +4315,15 @@ var UsersResource = class extends BaseResource {
3780
4315
  * см. {@link followers}. Метод оставлен на случай, если пагинацию починят.
3781
4316
  */
3782
4317
  iterateFollowers(user, params = {}, options = {}) {
3783
- return this.#userPaginator("users.followers", `/api/users/${require_cookies.encodePathSegment(user, "user")}/followers`, params, options);
4318
+ return this.#userPaginator("users.followers", `/api/users/${require_redact.encodePathSegment(user, "user")}/followers`, params, options);
3784
4319
  }
3785
4320
  /** Загружает подписки пользователя. Ограничения те же, что у {@link followers}. */
3786
4321
  following(user, params = {}, options = {}) {
3787
- return this.#userPage("users.following", `/api/users/${require_cookies.encodePathSegment(user, "user")}/following`, params, options);
4322
+ return this.#userPage("users.following", `/api/users/${require_redact.encodePathSegment(user, "user")}/following`, params, options);
3788
4323
  }
3789
4324
  /** Перебирает подписки. Закончится после первых 20 записей — см. {@link followers}. */
3790
4325
  iterateFollowing(user, params = {}, options = {}) {
3791
- return this.#userPaginator("users.following", `/api/users/${require_cookies.encodePathSegment(user, "user")}/following`, params, options);
4326
+ return this.#userPaginator("users.following", `/api/users/${require_redact.encodePathSegment(user, "user")}/following`, params, options);
3792
4327
  }
3793
4328
  /**
3794
4329
  * Проверяет, подписаны ли вы, сразу для нескольких пользователей.
@@ -3802,7 +4337,7 @@ var UsersResource = class extends BaseResource {
3802
4337
  * ```
3803
4338
  */
3804
4339
  followStatus(userIds, options = {}) {
3805
- return this.http.operation("users.followStatus", {
4340
+ return this.http.execute(USERS_FOLLOW_STATUS, {
3806
4341
  path: "/api/users/follow-status",
3807
4342
  body: { userIds },
3808
4343
  ...options
@@ -3810,16 +4345,16 @@ var UsersResource = class extends BaseResource {
3810
4345
  }
3811
4346
  /** Блокирует пользователя. */
3812
4347
  block(user, options = {}) {
3813
- return this.http.operation("users.block", {
3814
- path: `/api/users/${require_cookies.encodePathSegment(user, "user")}/block`,
4348
+ return this.voidOperation(USERS_BLOCK, {
4349
+ path: `/api/users/${require_redact.encodePathSegment(user, "user")}/block`,
3815
4350
  body: {},
3816
4351
  ...options
3817
4352
  });
3818
4353
  }
3819
4354
  /** Снимает блокировку. */
3820
4355
  unblock(user, options = {}) {
3821
- return this.http.operation("users.unblock", {
3822
- path: `/api/users/${require_cookies.encodePathSegment(user, "user")}/block`,
4356
+ return this.voidOperation(USERS_UNBLOCK, {
4357
+ path: `/api/users/${require_redact.encodePathSegment(user, "user")}/block`,
3823
4358
  ...options
3824
4359
  });
3825
4360
  }
@@ -3833,14 +4368,14 @@ var UsersResource = class extends BaseResource {
3833
4368
  }
3834
4369
  /** Загружает настройки приватности. */
3835
4370
  getPrivacy(options = {}) {
3836
- return this.http.operation("users.getPrivacy", {
4371
+ return this.http.execute(USERS_GET_PRIVACY, {
3837
4372
  path: "/api/users/me/privacy",
3838
4373
  ...options
3839
4374
  });
3840
4375
  }
3841
4376
  /** Обновляет настройки приватности. Передавайте только изменяемые поля. */
3842
4377
  updatePrivacy(input, options = {}) {
3843
- return this.http.operation("users.updatePrivacy", {
4378
+ return this.http.execute(USERS_UPDATE_PRIVACY, {
3844
4379
  path: "/api/users/me/privacy",
3845
4380
  body: input,
3846
4381
  ...options
@@ -3851,19 +4386,15 @@ var UsersResource = class extends BaseResource {
3851
4386
  *
3852
4387
  * `activePin` — строка-идентификатор, а не объект.
3853
4388
  */
3854
- async pins(options = {}) {
3855
- const body = await this.http.operation("users.pins", {
4389
+ pins(options = {}) {
4390
+ return this.http.execute(USERS_PINS, {
3856
4391
  path: "/api/users/me/pins",
3857
4392
  ...options
3858
4393
  });
3859
- return {
3860
- pins: require_url.pickArray(body, "pins"),
3861
- activePin: require_url.pickString(body, "activePin") ?? null
3862
- };
3863
4394
  }
3864
4395
  /** Выбирает активный значок профиля. */
3865
4396
  setPin(slug, options = {}) {
3866
- return this.http.operation("users.setPin", {
4397
+ return this.voidOperation(USERS_SET_PIN, {
3867
4398
  path: "/api/users/me/pin",
3868
4399
  body: { slug },
3869
4400
  ...options
@@ -3871,7 +4402,7 @@ var UsersResource = class extends BaseResource {
3871
4402
  }
3872
4403
  /** Снимает активный значок. */
3873
4404
  removePin(options = {}) {
3874
- return this.http.operation("users.removePin", {
4405
+ return this.voidOperation(USERS_REMOVE_PIN, {
3875
4406
  path: "/api/users/me/pin",
3876
4407
  ...options
3877
4408
  });
@@ -3879,20 +4410,22 @@ var UsersResource = class extends BaseResource {
3879
4410
  #userPage(operationId, path, params, options) {
3880
4411
  return this.#userList.list({
3881
4412
  ...params,
3882
- operationId,
4413
+ operation: USER_LIST_OPERATIONS[operationId],
3883
4414
  path
3884
4415
  }, options);
3885
4416
  }
3886
4417
  #userPaginator(operationId, path, params, options) {
3887
4418
  return this.#userList.iterate({
3888
4419
  ...params,
3889
- operationId,
4420
+ operation: USER_LIST_OPERATIONS[operationId],
3890
4421
  path
3891
4422
  }, options);
3892
4423
  }
3893
4424
  };
3894
4425
  //#endregion
3895
4426
  //#region src/resources/verification.ts
4427
+ const VERIFICATION_STATUS = require_url.passthroughOperation("verification.status");
4428
+ const VERIFICATION_SUBMIT = require_url.passthroughOperation("verification.submit");
3896
4429
  /**
3897
4430
  * Верификация профиля.
3898
4431
  *
@@ -3901,14 +4434,14 @@ var UsersResource = class extends BaseResource {
3901
4434
  var VerificationResource = class extends BaseResource {
3902
4435
  /** Загружает статус заявки. Значение `none` означает, что заявка не подавалась. */
3903
4436
  status(options = {}) {
3904
- return this.http.operation("verification.status", {
4437
+ return this.http.execute(VERIFICATION_STATUS, {
3905
4438
  path: "/api/verification/status",
3906
4439
  ...options
3907
4440
  });
3908
4441
  }
3909
4442
  /** Подаёт заявку на верификацию с видео. */
3910
4443
  submit(videoUrl, options = {}) {
3911
- return this.http.operation("verification.submit", {
4444
+ return this.http.execute(VERIFICATION_SUBMIT, {
3912
4445
  path: "/api/verification/submit",
3913
4446
  body: { videoUrl },
3914
4447
  ...options
@@ -3939,6 +4472,7 @@ function createResources(deps) {
3939
4472
  let subscription;
3940
4473
  let platform;
3941
4474
  let telemetry;
4475
+ let shop;
3942
4476
  const bag = {
3943
4477
  get users() {
3944
4478
  users ??= new UsersResource(deps.http, { uploadFile });
@@ -3953,11 +4487,11 @@ function createResources(deps) {
3953
4487
  return comments;
3954
4488
  },
3955
4489
  get files() {
3956
- files ??= new FilesResource(deps.http, { fetch: deps.fetch });
4490
+ files ??= new FilesResource(deps.http, { files: deps.files });
3957
4491
  return files;
3958
4492
  },
3959
4493
  get notifications() {
3960
- notifications ??= new NotificationsResource(deps.http);
4494
+ notifications ??= deps.createNotifications(deps.http);
3961
4495
  return notifications;
3962
4496
  },
3963
4497
  get hashtags() {
@@ -3988,6 +4522,10 @@ function createResources(deps) {
3988
4522
  telemetry ??= new TelemetryResource(deps.http);
3989
4523
  return telemetry;
3990
4524
  },
4525
+ get shop() {
4526
+ shop ??= new ShopResource(deps.http);
4527
+ return shop;
4528
+ },
3991
4529
  closeTelemetry: (forDispose) => forDispose ? closeTelemetryForDispose(telemetry) : telemetry?.close() ?? Promise.resolve(),
3992
4530
  prepareTelemetryClose: () => prepareTelemetryForDispose(telemetry)
3993
4531
  };
@@ -4021,7 +4559,7 @@ function fromStream(factory, options = {}) {
4021
4559
  throw new require_errors.ItdFileError("не удалось открыть поток вложения", {
4022
4560
  reason: require_errors.ItdFileErrorReason.Read,
4023
4561
  retryable: true,
4024
- cause: error
4562
+ cause: require_redact.redactErrorCause(error)
4025
4563
  });
4026
4564
  }
4027
4565
  const content = require_options.isReadableByteStream(opened) ? { stream: opened } : opened;
@@ -4061,6 +4599,36 @@ function isMyProfile(profile) {
4061
4599
  return "subscription" in profile;
4062
4600
  }
4063
4601
  //#endregion
4602
+ //#region src/models/shop.ts
4603
+ /** Категория товара магазина. */
4604
+ const ShopProductCategory = Object.freeze({
4605
+ Apparel: "apparel",
4606
+ Accessories: "accessories",
4607
+ Print: "print"
4608
+ });
4609
+ /** Состояние товара в каталоге. */
4610
+ const ShopProductStatus = Object.freeze({
4611
+ Available: "available",
4612
+ Preorder: "preorder",
4613
+ SoldOut: "soldout"
4614
+ });
4615
+ /** Вид согласия, которое принимает покупатель. */
4616
+ const ShopConsentKind = Object.freeze({
4617
+ Offer: "offer",
4618
+ PersonalData: "personal_data",
4619
+ Cookie: "cookie"
4620
+ });
4621
+ /** Состояние заказа. */
4622
+ const ShopOrderStatus = Object.freeze({
4623
+ New: "new",
4624
+ Paid: "paid",
4625
+ Shipping: "ship",
4626
+ Done: "done",
4627
+ Cancelled: "cancelled",
4628
+ Refunded: "refunded",
4629
+ Expired: "expired"
4630
+ });
4631
+ //#endregion
4064
4632
  //#region src/models/status-helpers.ts
4065
4633
  const STATUS_WINDOW_DAYS = 90;
4066
4634
  /**
@@ -4082,6 +4650,186 @@ function statusDays(service) {
4082
4650
  return Array.from({ length: STATUS_WINDOW_DAYS }, (_, index) => service.days[String(index)] ?? null);
4083
4651
  }
4084
4652
  //#endregion
4653
+ //#region src/shop/order-access.ts
4654
+ function copySession(session) {
4655
+ return { ...session };
4656
+ }
4657
+ /** Хранилище доступа к заказам в памяти процесса. */
4658
+ var MemoryShopOrderAccessStorage = class {
4659
+ #session;
4660
+ constructor(initial = null) {
4661
+ this.#session = initial ? copySession(initial) : null;
4662
+ }
4663
+ get() {
4664
+ return this.#session ? copySession(this.#session) : null;
4665
+ }
4666
+ set(session) {
4667
+ this.#session = copySession(session);
4668
+ }
4669
+ clear() {
4670
+ this.#session = null;
4671
+ }
4672
+ };
4673
+ /** Создаёт хранилище доступа к заказам поверх `KeyValueStore`. */
4674
+ function createShopOrderAccessStorage(store, options = {}) {
4675
+ const key = options.key ?? "shop-order-access";
4676
+ if (typeof key !== "string" || key.length === 0) throw new require_errors.ItdConfigError("ключ хранилища доступа к заказам должен быть непустой строкой");
4677
+ const backend = require_key_value_store.createKeyValueStore(store);
4678
+ return {
4679
+ async get() {
4680
+ const session = await backend.get(key);
4681
+ return session ? copySession(session) : null;
4682
+ },
4683
+ set: (session) => backend.set(key, copySession(session)),
4684
+ clear: () => backend.delete(key)
4685
+ };
4686
+ }
4687
+ /** Создаёт независимое хранилище доступа к заказам в памяти. */
4688
+ function createMemoryShopOrderAccessStorage() {
4689
+ return createShopOrderAccessStorage(new require_key_value_store.MemoryKeyValueStore());
4690
+ }
4691
+ //#endregion
4692
+ //#region src/shop/feature.ts
4693
+ const Operation = Object.freeze({
4694
+ RequestCode: "request-code",
4695
+ VerifyCode: "verify-code",
4696
+ List: "list",
4697
+ Get: "get",
4698
+ Pay: "pay"
4699
+ });
4700
+ function requireEmail(value) {
4701
+ const email = value.trim();
4702
+ if (!email?.includes("@")) throw new require_errors.ItdConfigError("укажите адрес почты");
4703
+ return email;
4704
+ }
4705
+ /** Создаёт подключаемый модуль для доступа к заказам по коду из письма. */
4706
+ function createShopFeature(options = {}) {
4707
+ return {
4708
+ name: "shop",
4709
+ buckets: {
4710
+ [Operation.RequestCode]: { limit: 4 },
4711
+ [Operation.VerifyCode]: { limit: 13 },
4712
+ [Operation.List]: { limit: 150 },
4713
+ [Operation.Get]: { limit: 150 },
4714
+ [Operation.Pay]: { limit: 13 }
4715
+ },
4716
+ operations: {
4717
+ [Operation.RequestCode]: {
4718
+ method: "POST",
4719
+ retrySafety: require_url.RetrySafety.Unsafe,
4720
+ read: require_url.voidResult,
4721
+ bucket: Operation.RequestCode
4722
+ },
4723
+ [Operation.VerifyCode]: {
4724
+ method: "POST",
4725
+ retrySafety: require_url.RetrySafety.Unsafe,
4726
+ bucket: Operation.VerifyCode
4727
+ },
4728
+ [Operation.List]: {
4729
+ method: "GET",
4730
+ retrySafety: require_url.RetrySafety.Safe,
4731
+ bucket: Operation.List
4732
+ },
4733
+ [Operation.Get]: {
4734
+ method: "GET",
4735
+ retrySafety: require_url.RetrySafety.Safe,
4736
+ bucket: Operation.Get
4737
+ },
4738
+ [Operation.Pay]: {
4739
+ method: "POST",
4740
+ retrySafety: require_url.RetrySafety.Unsafe,
4741
+ bucket: Operation.Pay
4742
+ }
4743
+ },
4744
+ setup(context) {
4745
+ const storage = options.storage ?? createMemoryShopOrderAccessStorage();
4746
+ let pendingEmail = null;
4747
+ const readSession = async () => {
4748
+ const session = await storage.get();
4749
+ if (!session) return null;
4750
+ if (session.expiresAt > context.clock.now()) return { ...session };
4751
+ await storage.clear();
4752
+ return null;
4753
+ };
4754
+ const requireSession = async () => {
4755
+ const session = await readSession();
4756
+ if (!session) throw new require_errors.ItdConfigError("доступ к заказам отсутствует или истёк; запросите новый код");
4757
+ return session;
4758
+ };
4759
+ const withAccess = async (run) => {
4760
+ const session = await requireSession();
4761
+ try {
4762
+ return await run(session.token);
4763
+ } catch (error) {
4764
+ if (require_errors.isItdApiError(error) && error.status === 401) await storage.clear();
4765
+ throw error;
4766
+ }
4767
+ };
4768
+ const request = (operation, token, requestOptions) => context.request(operation, {
4769
+ ...requestOptions,
4770
+ headers: {
4771
+ ...requestOptions.headers,
4772
+ "X-Order-Token": token
4773
+ },
4774
+ skipAuth: true,
4775
+ raw: true
4776
+ });
4777
+ return { api: Object.freeze({
4778
+ async requestCode(email, requestOptions = {}) {
4779
+ const normalized = requireEmail(email);
4780
+ await context.request(Operation.RequestCode, {
4781
+ path: "/api/v1/shop/orders/lookup/request",
4782
+ body: { email: normalized },
4783
+ skipAuth: true,
4784
+ raw: true,
4785
+ ...requestOptions
4786
+ });
4787
+ pendingEmail = normalized;
4788
+ },
4789
+ async verifyCode(code, requestOptions = {}) {
4790
+ if (!pendingEmail) throw new require_errors.ItdConfigError("сначала запросите код для адреса почты");
4791
+ const verified = await context.request(Operation.VerifyCode, {
4792
+ path: "/api/v1/shop/orders/lookup/verify",
4793
+ body: {
4794
+ email: pendingEmail,
4795
+ code
4796
+ },
4797
+ skipAuth: true,
4798
+ raw: true,
4799
+ ...requestOptions
4800
+ });
4801
+ const session = {
4802
+ email: pendingEmail,
4803
+ token: verified.token,
4804
+ expiresAt: context.clock.now() + verified.expiresInSec * 1e3
4805
+ };
4806
+ await storage.set(session);
4807
+ pendingEmail = null;
4808
+ return { ...session };
4809
+ },
4810
+ session: readSession,
4811
+ async clear() {
4812
+ pendingEmail = null;
4813
+ await storage.clear();
4814
+ },
4815
+ list: (requestOptions = {}) => withAccess((token) => request(Operation.List, token, {
4816
+ path: "/api/v1/shop/orders/my",
4817
+ ...requestOptions
4818
+ })),
4819
+ get: (orderNumber, requestOptions = {}) => withAccess((token) => request(Operation.Get, token, {
4820
+ path: `/api/v1/shop/orders/${require_redact.encodePathSegment(orderNumber, "номер заказа")}`,
4821
+ ...requestOptions
4822
+ })),
4823
+ pay: (orderNumber, returnUrl, requestOptions = {}) => withAccess((token) => request(Operation.Pay, token, {
4824
+ path: `/api/v1/shop/orders/${require_redact.encodePathSegment(orderNumber, "номер заказа")}/pay`,
4825
+ body: { returnUrl },
4826
+ ...requestOptions
4827
+ }))
4828
+ }) };
4829
+ }
4830
+ };
4831
+ }
4832
+ //#endregion
4085
4833
  //#region src/spans/render.ts
4086
4834
  /** Формат результата {@link renderSpans}. */
4087
4835
  const SpanRenderFormat = Object.freeze({
@@ -4308,16 +5056,22 @@ Object.defineProperty(exports, "ExtensibleOperationCatalog", {
4308
5056
  return ExtensibleOperationCatalog;
4309
5057
  }
4310
5058
  });
4311
- Object.defineProperty(exports, "FeatureRegistry", {
5059
+ Object.defineProperty(exports, "IMAGE_MIME_TYPES", {
4312
5060
  enumerable: true,
4313
5061
  get: function() {
4314
- return FeatureRegistry;
5062
+ return IMAGE_MIME_TYPES;
4315
5063
  }
4316
5064
  });
4317
- Object.defineProperty(exports, "IMAGE_MIME_TYPES", {
5065
+ Object.defineProperty(exports, "MemoryShopOrderAccessStorage", {
4318
5066
  enumerable: true,
4319
5067
  get: function() {
4320
- return IMAGE_MIME_TYPES;
5068
+ return MemoryShopOrderAccessStorage;
5069
+ }
5070
+ });
5071
+ Object.defineProperty(exports, "NotificationsResource", {
5072
+ enumerable: true,
5073
+ get: function() {
5074
+ return NotificationsResource;
4321
5075
  }
4322
5076
  });
4323
5077
  Object.defineProperty(exports, "PaginationMode", {
@@ -4332,6 +5086,30 @@ Object.defineProperty(exports, "Paginator", {
4332
5086
  return Paginator;
4333
5087
  }
4334
5088
  });
5089
+ Object.defineProperty(exports, "ShopConsentKind", {
5090
+ enumerable: true,
5091
+ get: function() {
5092
+ return ShopConsentKind;
5093
+ }
5094
+ });
5095
+ Object.defineProperty(exports, "ShopOrderStatus", {
5096
+ enumerable: true,
5097
+ get: function() {
5098
+ return ShopOrderStatus;
5099
+ }
5100
+ });
5101
+ Object.defineProperty(exports, "ShopProductCategory", {
5102
+ enumerable: true,
5103
+ get: function() {
5104
+ return ShopProductCategory;
5105
+ }
5106
+ });
5107
+ Object.defineProperty(exports, "ShopProductStatus", {
5108
+ enumerable: true,
5109
+ get: function() {
5110
+ return ShopProductStatus;
5111
+ }
5112
+ });
4335
5113
  Object.defineProperty(exports, "SpanRenderFormat", {
4336
5114
  enumerable: true,
4337
5115
  get: function() {
@@ -4356,12 +5134,36 @@ Object.defineProperty(exports, "comment", {
4356
5134
  return comment;
4357
5135
  }
4358
5136
  });
5137
+ Object.defineProperty(exports, "createFeatureHost", {
5138
+ enumerable: true,
5139
+ get: function() {
5140
+ return createFeatureHost;
5141
+ }
5142
+ });
5143
+ Object.defineProperty(exports, "createMemoryShopOrderAccessStorage", {
5144
+ enumerable: true,
5145
+ get: function() {
5146
+ return createMemoryShopOrderAccessStorage;
5147
+ }
5148
+ });
4359
5149
  Object.defineProperty(exports, "createResources", {
4360
5150
  enumerable: true,
4361
5151
  get: function() {
4362
5152
  return createResources;
4363
5153
  }
4364
5154
  });
5155
+ Object.defineProperty(exports, "createShopFeature", {
5156
+ enumerable: true,
5157
+ get: function() {
5158
+ return createShopFeature;
5159
+ }
5160
+ });
5161
+ Object.defineProperty(exports, "createShopOrderAccessStorage", {
5162
+ enumerable: true,
5163
+ get: function() {
5164
+ return createShopOrderAccessStorage;
5165
+ }
5166
+ });
4365
5167
  Object.defineProperty(exports, "createStatusFeature", {
4366
5168
  enumerable: true,
4367
5169
  get: function() {
@@ -4459,4 +5261,4 @@ Object.defineProperty(exports, "utcStampToIso", {
4459
5261
  }
4460
5262
  });
4461
5263
 
4462
- //# sourceMappingURL=render-mcuELiYi.cjs.map
5264
+ //# sourceMappingURL=render-Dqi8Owqk.cjs.map