itd-api 0.0.2 → 0.0.4
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.
- package/README.md +133 -11
- package/dist/{chunk-76C65H5J.js → chunk-JV75JWOX.js} +646 -275
- package/dist/chunk-JV75JWOX.js.map +1 -0
- package/dist/{chunk-QILCVTJI.cjs → chunk-XG43KEYF.cjs} +667 -274
- package/dist/chunk-XG43KEYF.cjs.map +1 -0
- package/dist/{index-RzyK1gKg.d.cts → index-olL_q_yu.d.cts} +392 -47
- package/dist/{index-RzyK1gKg.d.ts → index-olL_q_yu.d.ts} +392 -47
- package/dist/index.cjs +150 -62
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/node.cjs +149 -61
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +8 -12
- package/dist/node.d.ts +8 -12
- package/dist/node.js +2 -2
- package/dist/node.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-76C65H5J.js.map +0 -1
- package/dist/chunk-QILCVTJI.cjs.map +0 -1
|
@@ -14,6 +14,30 @@ function resolveInput(input, factory, validate) {
|
|
|
14
14
|
|
|
15
15
|
// src/core/errors.ts
|
|
16
16
|
var ITD_ERROR = /* @__PURE__ */ Symbol.for("itd.error");
|
|
17
|
+
var ItdErrorKind = Object.freeze({
|
|
18
|
+
/** Сервер ответил статусом ≥ 400. */
|
|
19
|
+
Api: "api",
|
|
20
|
+
/** Запрос не дошёл до сервера. */
|
|
21
|
+
Network: "network",
|
|
22
|
+
/** Истёк таймаут запроса. */
|
|
23
|
+
Timeout: "timeout",
|
|
24
|
+
/** Запрос отменён через `AbortSignal`. */
|
|
25
|
+
Abort: "abort",
|
|
26
|
+
/** Некорректная конфигурация или аргументы — обнаружено до обращения к сети. */
|
|
27
|
+
Config: "config"
|
|
28
|
+
});
|
|
29
|
+
var ItdApiErrorKind = Object.freeze({
|
|
30
|
+
/** Ни одна из специализаций не подошла. */
|
|
31
|
+
Generic: "generic",
|
|
32
|
+
Validation: "validation",
|
|
33
|
+
Auth: "auth",
|
|
34
|
+
Forbidden: "forbidden",
|
|
35
|
+
NotFound: "not_found",
|
|
36
|
+
Conflict: "conflict",
|
|
37
|
+
RateLimit: "rate_limit",
|
|
38
|
+
PhoneVerification: "phone_verification",
|
|
39
|
+
Server: "server"
|
|
40
|
+
});
|
|
17
41
|
var ItdError = class extends Error {
|
|
18
42
|
/** @internal */
|
|
19
43
|
[ITD_ERROR] = true;
|
|
@@ -26,6 +50,13 @@ var ItdError = class extends Error {
|
|
|
26
50
|
}
|
|
27
51
|
};
|
|
28
52
|
var ItdApiError = class extends ItdError {
|
|
53
|
+
/**
|
|
54
|
+
* Разновидность ошибки: та же информация, что и класс, но пригодная для сравнения.
|
|
55
|
+
*
|
|
56
|
+
* Позволяет разбирать ошибку через `switch`, а проверкам вроде {@link isItdAuthError} —
|
|
57
|
+
* работать даже когда в проекте оказались две копии библиотеки.
|
|
58
|
+
*/
|
|
59
|
+
apiKind;
|
|
29
60
|
/** HTTP-статус ответа. */
|
|
30
61
|
status;
|
|
31
62
|
/** Строковый код ошибки, например `VALIDATION_ERROR`. */
|
|
@@ -56,9 +87,13 @@ var ItdApiError = class extends ItdError {
|
|
|
56
87
|
rateLimit;
|
|
57
88
|
/** Сколько запросов осталось в окне — заголовок `x-ratelimit-remaining`. */
|
|
58
89
|
rateLimitRemaining;
|
|
59
|
-
|
|
60
|
-
|
|
90
|
+
/**
|
|
91
|
+
* @param apiKind разновидность; подставляется подклассами, снаружи задавать не нужно
|
|
92
|
+
*/
|
|
93
|
+
constructor(init, apiKind = ItdApiErrorKind.Generic) {
|
|
94
|
+
super(ItdErrorKind.Api, init.message);
|
|
61
95
|
this.name = "ItdApiError";
|
|
96
|
+
this.apiKind = apiKind;
|
|
62
97
|
this.status = init.status;
|
|
63
98
|
this.code = init.code;
|
|
64
99
|
this.detail = init.detail;
|
|
@@ -91,37 +126,37 @@ var ItdApiError = class extends ItdError {
|
|
|
91
126
|
};
|
|
92
127
|
var ItdValidationError = class extends ItdApiError {
|
|
93
128
|
constructor(init) {
|
|
94
|
-
super(init);
|
|
129
|
+
super(init, ItdApiErrorKind.Validation);
|
|
95
130
|
this.name = "ItdValidationError";
|
|
96
131
|
}
|
|
97
132
|
};
|
|
98
133
|
var ItdAuthError = class extends ItdApiError {
|
|
99
134
|
constructor(init) {
|
|
100
|
-
super(init);
|
|
135
|
+
super(init, ItdApiErrorKind.Auth);
|
|
101
136
|
this.name = "ItdAuthError";
|
|
102
137
|
}
|
|
103
138
|
};
|
|
104
139
|
var ItdForbiddenError = class extends ItdApiError {
|
|
105
140
|
constructor(init) {
|
|
106
|
-
super(init);
|
|
141
|
+
super(init, ItdApiErrorKind.Forbidden);
|
|
107
142
|
this.name = "ItdForbiddenError";
|
|
108
143
|
}
|
|
109
144
|
};
|
|
110
145
|
var ItdNotFoundError = class extends ItdApiError {
|
|
111
146
|
constructor(init) {
|
|
112
|
-
super(init);
|
|
147
|
+
super(init, ItdApiErrorKind.NotFound);
|
|
113
148
|
this.name = "ItdNotFoundError";
|
|
114
149
|
}
|
|
115
150
|
};
|
|
116
151
|
var ItdConflictError = class extends ItdApiError {
|
|
117
152
|
constructor(init) {
|
|
118
|
-
super(init);
|
|
153
|
+
super(init, ItdApiErrorKind.Conflict);
|
|
119
154
|
this.name = "ItdConflictError";
|
|
120
155
|
}
|
|
121
156
|
};
|
|
122
157
|
var ItdRateLimitError = class extends ItdApiError {
|
|
123
158
|
constructor(init) {
|
|
124
|
-
super(init);
|
|
159
|
+
super(init, ItdApiErrorKind.RateLimit);
|
|
125
160
|
this.name = "ItdRateLimitError";
|
|
126
161
|
}
|
|
127
162
|
};
|
|
@@ -129,14 +164,14 @@ var ItdPhoneVerificationError = class extends ItdApiError {
|
|
|
129
164
|
/** Ссылка на бота подтверждения, если удалось определить идентификатор пользователя. */
|
|
130
165
|
verificationUrl;
|
|
131
166
|
constructor(init) {
|
|
132
|
-
super(init);
|
|
167
|
+
super(init, ItdApiErrorKind.PhoneVerification);
|
|
133
168
|
this.name = "ItdPhoneVerificationError";
|
|
134
169
|
this.verificationUrl = init.userId ? `https://t.me/itd_verification_bot?start=${encodeURIComponent(init.userId)}` : void 0;
|
|
135
170
|
}
|
|
136
171
|
};
|
|
137
172
|
var ItdServerError = class extends ItdApiError {
|
|
138
173
|
constructor(init) {
|
|
139
|
-
super(init);
|
|
174
|
+
super(init, ItdApiErrorKind.Server);
|
|
140
175
|
this.name = "ItdServerError";
|
|
141
176
|
}
|
|
142
177
|
};
|
|
@@ -146,7 +181,7 @@ var ItdNetworkError = class extends ItdError {
|
|
|
146
181
|
/** Путь запроса без базового URL. */
|
|
147
182
|
path;
|
|
148
183
|
constructor(message, init) {
|
|
149
|
-
super(
|
|
184
|
+
super(ItdErrorKind.Network, message, { cause: init.cause });
|
|
150
185
|
this.name = "ItdNetworkError";
|
|
151
186
|
this.method = init.method;
|
|
152
187
|
this.path = init.path;
|
|
@@ -160,7 +195,10 @@ var ItdTimeoutError = class extends ItdError {
|
|
|
160
195
|
/** Путь запроса без базового URL. */
|
|
161
196
|
path;
|
|
162
197
|
constructor(init) {
|
|
163
|
-
super(
|
|
198
|
+
super(
|
|
199
|
+
ItdErrorKind.Timeout,
|
|
200
|
+
`\u0417\u0430\u043F\u0440\u043E\u0441 ${init.method} ${init.path} \u043F\u0440\u0435\u0432\u044B\u0441\u0438\u043B \u0442\u0430\u0439\u043C\u0430\u0443\u0442 ${init.timeout} \u043C\u0441`
|
|
201
|
+
);
|
|
164
202
|
this.name = "ItdTimeoutError";
|
|
165
203
|
this.timeout = init.timeout;
|
|
166
204
|
this.method = init.method;
|
|
@@ -169,13 +207,13 @@ var ItdTimeoutError = class extends ItdError {
|
|
|
169
207
|
};
|
|
170
208
|
var ItdAbortError = class extends ItdError {
|
|
171
209
|
constructor(message = "\u0417\u0430\u043F\u0440\u043E\u0441 \u043E\u0442\u043C\u0435\u043D\u0451\u043D") {
|
|
172
|
-
super(
|
|
210
|
+
super(ItdErrorKind.Abort, message);
|
|
173
211
|
this.name = "ItdAbortError";
|
|
174
212
|
}
|
|
175
213
|
};
|
|
176
214
|
var ItdConfigError = class extends ItdError {
|
|
177
215
|
constructor(message) {
|
|
178
|
-
super(
|
|
216
|
+
super(ItdErrorKind.Config, message);
|
|
179
217
|
this.name = "ItdConfigError";
|
|
180
218
|
}
|
|
181
219
|
};
|
|
@@ -183,16 +221,34 @@ function isItdError(value) {
|
|
|
183
221
|
return typeof value === "object" && value !== null && ITD_ERROR in value;
|
|
184
222
|
}
|
|
185
223
|
function isItdApiError(value) {
|
|
186
|
-
return isItdError(value) && value.kind ===
|
|
224
|
+
return isItdError(value) && value.kind === ItdErrorKind.Api;
|
|
225
|
+
}
|
|
226
|
+
function hasApiKind(value, kind) {
|
|
227
|
+
return isItdApiError(value) && value.apiKind === kind;
|
|
187
228
|
}
|
|
188
229
|
function isItdValidationError(value) {
|
|
189
|
-
return
|
|
230
|
+
return hasApiKind(value, ItdApiErrorKind.Validation);
|
|
190
231
|
}
|
|
191
232
|
function isItdAuthError(value) {
|
|
192
|
-
return
|
|
233
|
+
return hasApiKind(value, ItdApiErrorKind.Auth);
|
|
234
|
+
}
|
|
235
|
+
function isItdForbiddenError(value) {
|
|
236
|
+
return hasApiKind(value, ItdApiErrorKind.Forbidden);
|
|
237
|
+
}
|
|
238
|
+
function isItdNotFoundError(value) {
|
|
239
|
+
return hasApiKind(value, ItdApiErrorKind.NotFound);
|
|
240
|
+
}
|
|
241
|
+
function isItdConflictError(value) {
|
|
242
|
+
return hasApiKind(value, ItdApiErrorKind.Conflict);
|
|
193
243
|
}
|
|
194
244
|
function isItdRateLimitError(value) {
|
|
195
|
-
return
|
|
245
|
+
return hasApiKind(value, ItdApiErrorKind.RateLimit);
|
|
246
|
+
}
|
|
247
|
+
function isItdPhoneVerificationError(value) {
|
|
248
|
+
return hasApiKind(value, ItdApiErrorKind.PhoneVerification);
|
|
249
|
+
}
|
|
250
|
+
function isItdServerError(value) {
|
|
251
|
+
return hasApiKind(value, ItdApiErrorKind.Server);
|
|
196
252
|
}
|
|
197
253
|
|
|
198
254
|
// src/builders/comment.ts
|
|
@@ -586,8 +642,15 @@ var ItdErrorCode = Object.freeze({
|
|
|
586
642
|
BUSINESS_RULE_VIOLATION: "BUSINESS_RULE_VIOLATION",
|
|
587
643
|
RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
|
|
588
644
|
UNKNOWN_ERROR: "UNKNOWN_ERROR",
|
|
645
|
+
/** Сервер отвечает так на `404`, `ENTITY_NOT_FOUND` в этом случае не приходит. */
|
|
646
|
+
NOT_FOUND: "NOT_FOUND",
|
|
647
|
+
/** На практике не приходит: вместо него сервер шлёт `TURNSTILE_VERIFICATION_FAILED`. */
|
|
589
648
|
CAPTCHA_FAILED: "CAPTCHA_FAILED",
|
|
649
|
+
/** Капча не пройдена: токен Turnstile недействителен, просрочен или уже использован. */
|
|
650
|
+
TURNSTILE_VERIFICATION_FAILED: "TURNSTILE_VERIFICATION_FAILED",
|
|
590
651
|
OTP_INVALID: "OTP_INVALID",
|
|
652
|
+
/** `flowToken` неизвестен или просрочен — поток подтверждения нужно начинать заново. */
|
|
653
|
+
INVALID_FLOW_TOKEN: "INVALID_FLOW_TOKEN",
|
|
591
654
|
ACCOUNT_DEACTIVATED: "ACCOUNT_DEACTIVATED",
|
|
592
655
|
ACCOUNT_EMAIL_DOMAIN_NOT_ALLOWED: "ACCOUNT_EMAIL_DOMAIN_NOT_ALLOWED",
|
|
593
656
|
ACCOUNT_INVALID_CREDENTIALS: "ACCOUNT_INVALID_CREDENTIALS",
|
|
@@ -596,6 +659,10 @@ var ItdErrorCode = Object.freeze({
|
|
|
596
659
|
SESSION_EXPIRED: "SESSION_EXPIRED",
|
|
597
660
|
SESSION_REVOKED: "SESSION_REVOKED",
|
|
598
661
|
SESSION_INVALID_REFRESH_TOKEN: "SESSION_INVALID_REFRESH_TOKEN",
|
|
662
|
+
/** Запрос обновления пришёл без cookie `refresh_token` — продлевать нечего. */
|
|
663
|
+
REFRESH_TOKEN_MISSING: "REFRESH_TOKEN_MISSING",
|
|
664
|
+
/** Cookie `refresh_token` есть, но сессии за ней уже нет: отозвана или истекла. */
|
|
665
|
+
SESSION_NOT_FOUND: "SESSION_NOT_FOUND",
|
|
599
666
|
MISSING_FLOW_TOKEN: "MISSING_FLOW_TOKEN",
|
|
600
667
|
PROFILE_USERNAME_TAKEN: "PROFILE_USERNAME_TAKEN",
|
|
601
668
|
PROFILE_RESTRICTION_ACTIVE: "PROFILE_RESTRICTION_ACTIVE",
|
|
@@ -611,13 +678,14 @@ var ItdErrorCode = Object.freeze({
|
|
|
611
678
|
|
|
612
679
|
// src/builders/report.ts
|
|
613
680
|
var REASONS = new Set(Object.values(ReportReason));
|
|
681
|
+
var TARGET_TYPES = new Set(Object.values(ReportTargetType));
|
|
614
682
|
function validateReport(input) {
|
|
615
683
|
if (!input?.targetId || typeof input.targetId !== "string") {
|
|
616
684
|
throw new ItdConfigError("\u0416\u0430\u043B\u043E\u0431\u0430 \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0438\u0434\u0435\u043D\u0442\u0438\u0444\u0438\u043A\u0430\u0442\u043E\u0440 \u043E\u0431\u044A\u0435\u043A\u0442\u0430 (targetId)");
|
|
617
685
|
}
|
|
618
|
-
if (
|
|
686
|
+
if (!TARGET_TYPES.has(input.targetType)) {
|
|
619
687
|
throw new ItdConfigError(
|
|
620
|
-
`targetType \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C
|
|
688
|
+
`targetType \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043E\u0434\u043D\u0438\u043C \u0438\u0437 ${[...TARGET_TYPES].join(", ")}, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${String(input.targetType)}`
|
|
621
689
|
);
|
|
622
690
|
}
|
|
623
691
|
if (!REASONS.has(input.reason)) {
|
|
@@ -655,11 +723,11 @@ function start(targetType, targetId) {
|
|
|
655
723
|
}
|
|
656
724
|
var report = Object.freeze({
|
|
657
725
|
/** Жалоба на пост. */
|
|
658
|
-
post: (postId) => start(
|
|
726
|
+
post: (postId) => start(ReportTargetType.Post, postId),
|
|
659
727
|
/** Жалоба на комментарий. */
|
|
660
|
-
comment: (commentId) => start(
|
|
728
|
+
comment: (commentId) => start(ReportTargetType.Comment, commentId),
|
|
661
729
|
/** Жалоба на пользователя. */
|
|
662
|
-
user: (userId) => start(
|
|
730
|
+
user: (userId) => start(ReportTargetType.User, userId)
|
|
663
731
|
});
|
|
664
732
|
function resolveReport(input) {
|
|
665
733
|
return resolveInput(input, () => new ReportBuilder({}), validateReport);
|
|
@@ -859,6 +927,8 @@ parseSetCookie.splitCookiesString = splitCookiesString;
|
|
|
859
927
|
|
|
860
928
|
// src/core/cookies.ts
|
|
861
929
|
var AUTH_FLAG_COOKIE = "is_auth";
|
|
930
|
+
var REFRESH_COOKIE = "refresh_token";
|
|
931
|
+
var REFRESH_COOKIE_PATH = "/api/v1/auth";
|
|
862
932
|
var SERIALIZED_SEPARATOR = " ";
|
|
863
933
|
function originOf(url) {
|
|
864
934
|
try {
|
|
@@ -898,6 +968,15 @@ var CookieJar = class {
|
|
|
898
968
|
const raw = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : splitCookiesString(headers.get("set-cookie") ?? "");
|
|
899
969
|
if (raw.length > 0) this.setFromStrings(url, raw);
|
|
900
970
|
}
|
|
971
|
+
/**
|
|
972
|
+
* Кладёт cookie напрямую, минуя `Set-Cookie`.
|
|
973
|
+
*
|
|
974
|
+
* Нужно ровно в одном случае: пользователь передал refresh-токен строкой, а сервер читает
|
|
975
|
+
* его только из cookie. Значение не кодируется — оно уходит в заголовок как есть.
|
|
976
|
+
*/
|
|
977
|
+
set(url, name, value, path = "/") {
|
|
978
|
+
this.setFromStrings(url, [`${name}=${value}; Path=${path}`]);
|
|
979
|
+
}
|
|
901
980
|
/** Сохраняет cookie из готовых строк `Set-Cookie`. */
|
|
902
981
|
setFromStrings(url, setCookieStrings) {
|
|
903
982
|
const origin = originOf(url);
|
|
@@ -931,6 +1010,23 @@ var CookieJar = class {
|
|
|
931
1010
|
if (cookies.length === 0) return void 0;
|
|
932
1011
|
return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
|
|
933
1012
|
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Значение действующей cookie.
|
|
1015
|
+
*
|
|
1016
|
+
* Нужно, чтобы забрать обновлённый refresh-токен: сервер ротирует его при каждом
|
|
1017
|
+
* продлении сессии, и сохранять надо именно новое значение.
|
|
1018
|
+
*
|
|
1019
|
+
* @param url если указан, учитываются origin, путь и флаг `Secure`
|
|
1020
|
+
*/
|
|
1021
|
+
getValue(name, url) {
|
|
1022
|
+
if (url) return this.#matching(url).find((cookie) => cookie.name === name)?.value;
|
|
1023
|
+
const now = Date.now();
|
|
1024
|
+
for (const jar of this.#byOrigin.values()) {
|
|
1025
|
+
const cookie = jar.get(name);
|
|
1026
|
+
if (cookie && (cookie.expires === void 0 || cookie.expires > now)) return cookie.value;
|
|
1027
|
+
}
|
|
1028
|
+
return void 0;
|
|
1029
|
+
}
|
|
934
1030
|
/**
|
|
935
1031
|
* Есть ли действующая cookie с таким именем.
|
|
936
1032
|
*
|
|
@@ -1059,11 +1155,87 @@ var Emitter = class {
|
|
|
1059
1155
|
}
|
|
1060
1156
|
};
|
|
1061
1157
|
|
|
1158
|
+
// src/core/runtime.ts
|
|
1159
|
+
var RuntimeMode = Object.freeze({
|
|
1160
|
+
/** Определяется по среде исполнения. Значение по умолчанию. */
|
|
1161
|
+
Auto: "auto",
|
|
1162
|
+
/** Cookie ведёт браузер, запросы уходят с `credentials: 'include'`. */
|
|
1163
|
+
Browser: "browser",
|
|
1164
|
+
/** Cookie ведёт встроенный jar, заголовок `Cookie` подставляется вручную. */
|
|
1165
|
+
Server: "server"
|
|
1166
|
+
});
|
|
1167
|
+
var DetectedRuntime = Object.freeze({
|
|
1168
|
+
Browser: "browser",
|
|
1169
|
+
/** Есть `window`, но нет `document`; cookie ведёт нативный сетевой слой. */
|
|
1170
|
+
ReactNative: "react-native",
|
|
1171
|
+
Server: "server"
|
|
1172
|
+
});
|
|
1173
|
+
function detectRuntime() {
|
|
1174
|
+
const nav = globalThis.navigator;
|
|
1175
|
+
if (nav?.product === "ReactNative") return DetectedRuntime.ReactNative;
|
|
1176
|
+
if (typeof document !== "undefined") return DetectedRuntime.Browser;
|
|
1177
|
+
return DetectedRuntime.Server;
|
|
1178
|
+
}
|
|
1179
|
+
function shouldUseCookieJar(mode) {
|
|
1180
|
+
if (mode === RuntimeMode.Browser) return false;
|
|
1181
|
+
if (mode === RuntimeMode.Server) return true;
|
|
1182
|
+
return detectRuntime() === DetectedRuntime.Server;
|
|
1183
|
+
}
|
|
1184
|
+
function shouldSendCredentials(mode) {
|
|
1185
|
+
if (mode === RuntimeMode.Browser) return true;
|
|
1186
|
+
if (mode === RuntimeMode.Server) return false;
|
|
1187
|
+
return detectRuntime() === DetectedRuntime.Browser;
|
|
1188
|
+
}
|
|
1189
|
+
function resolveFetch(custom) {
|
|
1190
|
+
if (custom) return custom;
|
|
1191
|
+
if (typeof globalThis.fetch === "function") {
|
|
1192
|
+
return globalThis.fetch.bind(globalThis);
|
|
1193
|
+
}
|
|
1194
|
+
throw new ItdConfigError(
|
|
1195
|
+
"\u0412 \u044D\u0442\u043E\u0439 \u0441\u0440\u0435\u0434\u0435 \u043D\u0435\u0442 \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u043E\u0433\u043E fetch. \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u0435\u0441\u044C \u0434\u043E Node 18+ \u043B\u0438\u0431\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 \u0441\u0432\u043E\u044E \u0440\u0435\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044E \u0447\u0435\u0440\u0435\u0437 \u043E\u043F\u0446\u0438\u044E fetch."
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
function isBlob(value) {
|
|
1199
|
+
return typeof Blob !== "undefined" && value instanceof Blob;
|
|
1200
|
+
}
|
|
1201
|
+
function isFile(value) {
|
|
1202
|
+
return typeof File !== "undefined" && value instanceof File;
|
|
1203
|
+
}
|
|
1204
|
+
function supportsStreamingBody() {
|
|
1205
|
+
return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
|
|
1206
|
+
}
|
|
1207
|
+
function createDeviceId() {
|
|
1208
|
+
const webCrypto = globalThis.crypto;
|
|
1209
|
+
if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
|
|
1210
|
+
const bytes = new Uint8Array(16);
|
|
1211
|
+
if (typeof webCrypto?.getRandomValues === "function") webCrypto.getRandomValues(bytes);
|
|
1212
|
+
else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
1213
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
1214
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
1215
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1216
|
+
return [
|
|
1217
|
+
hex.slice(0, 8),
|
|
1218
|
+
hex.slice(8, 12),
|
|
1219
|
+
hex.slice(12, 16),
|
|
1220
|
+
hex.slice(16, 20),
|
|
1221
|
+
hex.slice(20, 32)
|
|
1222
|
+
].join("-");
|
|
1223
|
+
}
|
|
1224
|
+
function hasLocalStorage() {
|
|
1225
|
+
try {
|
|
1226
|
+
return typeof globalThis.localStorage !== "undefined" && globalThis.localStorage !== null;
|
|
1227
|
+
} catch {
|
|
1228
|
+
return false;
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
|
|
1062
1232
|
// src/core/auth.ts
|
|
1063
1233
|
var AUTH_PATHS = {
|
|
1064
1234
|
signIn: "/api/v1/auth/sign-in",
|
|
1065
1235
|
refresh: "/api/v1/auth/refresh"
|
|
1066
1236
|
};
|
|
1237
|
+
var TURNSTILE_SITE_KEY = "0x4AAAAAACHhxczw6fJGwPBg";
|
|
1238
|
+
var DEVICE_ID_HEADER = "X-Device-Id";
|
|
1067
1239
|
function readAccessToken(payload) {
|
|
1068
1240
|
if (typeof payload !== "object" || payload === null) return void 0;
|
|
1069
1241
|
const token = payload.accessToken;
|
|
@@ -1080,6 +1252,13 @@ var AuthManager = class {
|
|
|
1080
1252
|
#refreshing = null;
|
|
1081
1253
|
/** Общий промис входа по логину и паролю. */
|
|
1082
1254
|
#signingIn = null;
|
|
1255
|
+
/**
|
|
1256
|
+
* Идентификатор устройства.
|
|
1257
|
+
*
|
|
1258
|
+
* Держится отдельно от сессии намеренно: выход из аккаунта не меняет устройство,
|
|
1259
|
+
* поэтому `clear()` его не трогает.
|
|
1260
|
+
*/
|
|
1261
|
+
#deviceId;
|
|
1083
1262
|
constructor(config, http, jar) {
|
|
1084
1263
|
this.#config = config;
|
|
1085
1264
|
this.#http = http;
|
|
@@ -1096,11 +1275,19 @@ var AuthManager = class {
|
|
|
1096
1275
|
/**
|
|
1097
1276
|
* Есть ли признак живой refresh-сессии.
|
|
1098
1277
|
*
|
|
1099
|
-
*
|
|
1100
|
-
*
|
|
1278
|
+
* Рядом с refresh-токеном сервер ставит незакрытую cookie `is_auth` — по ней видно,
|
|
1279
|
+
* что продлевать сессию вообще есть смысл, и API не дёргается у анонимов.
|
|
1101
1280
|
* В браузере cookie ведёт сама среда, поэтому там ответ всегда `true`.
|
|
1281
|
+
*
|
|
1282
|
+
* Асинхронный, потому что признак может лежать в {@link TokenStorage}: до чтения оттуда
|
|
1283
|
+
* ответ был бы `false` даже при полностью рабочей сохранённой сессии.
|
|
1102
1284
|
*/
|
|
1103
|
-
hasRefreshSession() {
|
|
1285
|
+
async hasRefreshSession() {
|
|
1286
|
+
await this.#loadSession();
|
|
1287
|
+
return this.#hasRefreshSession();
|
|
1288
|
+
}
|
|
1289
|
+
/** То же самое, но без чтения хранилища — для вызовов, где сессия уже загружена. */
|
|
1290
|
+
#hasRefreshSession() {
|
|
1104
1291
|
if (!this.#config.useCookieJar) return true;
|
|
1105
1292
|
if (this.#jar.has(AUTH_FLAG_COOKIE)) return true;
|
|
1106
1293
|
return Boolean(this.#session?.refreshToken);
|
|
@@ -1110,6 +1297,23 @@ var AuthManager = class {
|
|
|
1110
1297
|
const token = await this.getAccessToken();
|
|
1111
1298
|
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
1112
1299
|
}
|
|
1300
|
+
/**
|
|
1301
|
+
* Идентификатор устройства для заголовка `X-Device-Id`.
|
|
1302
|
+
*
|
|
1303
|
+
* Заводится один раз и сохраняется в сессии, чтобы пережить перезапуск процесса:
|
|
1304
|
+
* сервер связывает с ним запись в списке сессий, и плавающее значение плодило бы
|
|
1305
|
+
* по новой сессии на каждый старт.
|
|
1306
|
+
*/
|
|
1307
|
+
async getDeviceId() {
|
|
1308
|
+
if (this.#deviceId) return this.#deviceId;
|
|
1309
|
+
const session = await this.#loadSession();
|
|
1310
|
+
const deviceId = this.#config.deviceId ?? session?.deviceId ?? createDeviceId();
|
|
1311
|
+
this.#deviceId = deviceId;
|
|
1312
|
+
if (session?.deviceId !== deviceId) {
|
|
1313
|
+
await this.#saveSession({ ...session ?? {}, deviceId });
|
|
1314
|
+
}
|
|
1315
|
+
return deviceId;
|
|
1316
|
+
}
|
|
1113
1317
|
/**
|
|
1114
1318
|
* Текущий токен доступа.
|
|
1115
1319
|
*
|
|
@@ -1125,7 +1329,7 @@ var AuthManager = class {
|
|
|
1125
1329
|
return await auth.getToken() ?? null;
|
|
1126
1330
|
}
|
|
1127
1331
|
if (typeof auth === "object" && "email" in auth) {
|
|
1128
|
-
return this.#signInWithCredentials(auth
|
|
1332
|
+
return this.#signInWithCredentials(auth);
|
|
1129
1333
|
}
|
|
1130
1334
|
return null;
|
|
1131
1335
|
}
|
|
@@ -1145,12 +1349,19 @@ var AuthManager = class {
|
|
|
1145
1349
|
return false;
|
|
1146
1350
|
}
|
|
1147
1351
|
}
|
|
1148
|
-
/**
|
|
1352
|
+
/**
|
|
1353
|
+
* Ошибка «сессию продлить нечем».
|
|
1354
|
+
*
|
|
1355
|
+
* Возникает, только когда обновление даже не начиналось: нет ни cookie `is_auth`,
|
|
1356
|
+
* ни refresh-токена. Если сервер ответил отказом, наружу уходит **его** ошибка —
|
|
1357
|
+
* подменять её этой значило бы прятать причину (`REFRESH_TOKEN_MISSING`,
|
|
1358
|
+
* `SESSION_NOT_FOUND`, `SESSION_REVOKED` — разные поводы и разные действия).
|
|
1359
|
+
*/
|
|
1149
1360
|
#noRefreshSessionError() {
|
|
1150
1361
|
return new ItdAuthError({
|
|
1151
1362
|
status: 401,
|
|
1152
1363
|
code: "SESSION_EXPIRED",
|
|
1153
|
-
message: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0435\u0441\u0441\u0438\u044E: \u043D\u0435\u0442 \
|
|
1364
|
+
message: "\u041D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043E\u0431\u043D\u043E\u0432\u0438\u0442\u044C \u0441\u0435\u0441\u0441\u0438\u044E: \u043D\u0435\u0442 \u043D\u0438 cookie is_auth, \u043D\u0438 refresh-\u0442\u043E\u043A\u0435\u043D\u0430. \u0412\u043E\u0439\u0434\u0438\u0442\u0435 \u0437\u0430\u043D\u043E\u0432\u043E \u043B\u0438\u0431\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 refreshToken \u0432 auth.",
|
|
1154
1365
|
method: "POST",
|
|
1155
1366
|
path: AUTH_PATHS.refresh,
|
|
1156
1367
|
raw: void 0
|
|
@@ -1180,13 +1391,21 @@ var AuthManager = class {
|
|
|
1180
1391
|
/** Заменяет сессию целиком. */
|
|
1181
1392
|
async setSession(session) {
|
|
1182
1393
|
this.#jar.deserialize(session.cookies);
|
|
1394
|
+
this.#deviceId ??= session.deviceId;
|
|
1183
1395
|
await this.#saveSession(session);
|
|
1396
|
+
this.#seedRefreshCookie();
|
|
1184
1397
|
}
|
|
1185
|
-
/**
|
|
1398
|
+
/**
|
|
1399
|
+
* Забывает сессию и cookie. Сетевой запрос не выполняется.
|
|
1400
|
+
*
|
|
1401
|
+
* Идентификатор устройства выход переживает: иначе каждая пара «выход — вход» плодила бы
|
|
1402
|
+
* новую запись в списке сессий.
|
|
1403
|
+
*/
|
|
1186
1404
|
async clear() {
|
|
1187
1405
|
this.#session = null;
|
|
1188
1406
|
this.#jar.clear();
|
|
1189
1407
|
await this.#config.storage.clear();
|
|
1408
|
+
if (this.#deviceId) await this.#saveSession({ deviceId: this.#deviceId });
|
|
1190
1409
|
this.#emitter.emit("signOut", void 0);
|
|
1191
1410
|
}
|
|
1192
1411
|
async #loadSession() {
|
|
@@ -1199,8 +1418,23 @@ var AuthManager = class {
|
|
|
1199
1418
|
accessToken: stored.accessToken ?? fromConfig.accessToken,
|
|
1200
1419
|
refreshToken: stored.refreshToken ?? fromConfig.refreshToken
|
|
1201
1420
|
} : stored ?? fromConfig;
|
|
1421
|
+
this.#seedRefreshCookie();
|
|
1202
1422
|
return this.#session;
|
|
1203
1423
|
}
|
|
1424
|
+
/**
|
|
1425
|
+
* Кладёт refresh-токен в jar как cookie `refresh_token`.
|
|
1426
|
+
*
|
|
1427
|
+
* `POST /api/v1/auth/refresh` читает токен только из cookie, поэтому переданный строкой
|
|
1428
|
+
* приходится превращать в неё. В браузере это невозможно — cookie помечена `HttpOnly`,
|
|
1429
|
+
* и там обновление работает только на той, что поставил сам сервер.
|
|
1430
|
+
*/
|
|
1431
|
+
#seedRefreshCookie() {
|
|
1432
|
+
if (!this.#config.useCookieJar) return;
|
|
1433
|
+
const refreshToken = this.#session?.refreshToken;
|
|
1434
|
+
if (!refreshToken) return;
|
|
1435
|
+
if (this.#jar.has(REFRESH_COOKIE)) return;
|
|
1436
|
+
this.#jar.set(this.#config.baseUrl, REFRESH_COOKIE, refreshToken, REFRESH_COOKIE_PATH);
|
|
1437
|
+
}
|
|
1204
1438
|
#sessionFromConfig(auth) {
|
|
1205
1439
|
if (!auth) return null;
|
|
1206
1440
|
if (typeof auth === "string") return { accessToken: auth, obtainedAt: Date.now() };
|
|
@@ -1215,7 +1449,12 @@ var AuthManager = class {
|
|
|
1215
1449
|
}
|
|
1216
1450
|
async #saveSession(session) {
|
|
1217
1451
|
const cookies = this.#config.useCookieJar ? this.#jar.serialize() : void 0;
|
|
1218
|
-
const
|
|
1452
|
+
const deviceId = session.deviceId ?? this.#deviceId;
|
|
1453
|
+
const next = {
|
|
1454
|
+
...session,
|
|
1455
|
+
...cookies?.length ? { cookies } : {},
|
|
1456
|
+
...deviceId ? { deviceId } : {}
|
|
1457
|
+
};
|
|
1219
1458
|
this.#session = next;
|
|
1220
1459
|
await this.#config.storage.set(next);
|
|
1221
1460
|
}
|
|
@@ -1234,24 +1473,32 @@ var AuthManager = class {
|
|
|
1234
1473
|
}
|
|
1235
1474
|
async #performRefresh() {
|
|
1236
1475
|
await this.#loadSession();
|
|
1237
|
-
if (!this
|
|
1476
|
+
if (!this.#hasRefreshSession()) {
|
|
1238
1477
|
return this.#reloginOrNull();
|
|
1239
1478
|
}
|
|
1240
1479
|
try {
|
|
1241
1480
|
const payload = await this.#http.request({
|
|
1242
1481
|
method: "POST",
|
|
1243
1482
|
path: AUTH_PATHS.refresh,
|
|
1244
|
-
//
|
|
1483
|
+
// Тела нет намеренно: сервер читает refresh-токен только из cookie — см.
|
|
1484
|
+
// #seedRefreshCookie. По той же причине не нужен и устаревший Bearer.
|
|
1245
1485
|
skipAuth: true,
|
|
1246
1486
|
// Без этого 401 на самом обновлении вызвал бы новое обновление — и так по кругу.
|
|
1247
1487
|
skipAuthRefresh: true,
|
|
1248
|
-
|
|
1488
|
+
// Обновление почти всегда запускается изнутри запроса, который занимает место
|
|
1489
|
+
// в очереди и ждёт его результата. Встать в ту же очередь — значит зависнуть.
|
|
1490
|
+
skipQueue: true
|
|
1249
1491
|
});
|
|
1250
1492
|
const accessToken = readAccessToken(payload);
|
|
1251
1493
|
if (!accessToken) return this.#reloginOrNull();
|
|
1494
|
+
const rotated = this.#jar.getValue(
|
|
1495
|
+
REFRESH_COOKIE,
|
|
1496
|
+
this.#config.baseUrl + REFRESH_COOKIE_PATH
|
|
1497
|
+
);
|
|
1252
1498
|
await this.#saveSession({
|
|
1253
1499
|
...this.#session ?? {},
|
|
1254
1500
|
accessToken,
|
|
1501
|
+
...rotated ? { refreshToken: rotated } : {},
|
|
1255
1502
|
obtainedAt: Date.now()
|
|
1256
1503
|
});
|
|
1257
1504
|
this.#emitter.emit("tokens", { accessToken });
|
|
@@ -1259,8 +1506,11 @@ var AuthManager = class {
|
|
|
1259
1506
|
} catch (error) {
|
|
1260
1507
|
if (error instanceof ItdApiError) {
|
|
1261
1508
|
this.#session = null;
|
|
1509
|
+
this.#jar.clear();
|
|
1262
1510
|
await this.#config.storage.clear();
|
|
1263
|
-
|
|
1511
|
+
const relogged = await this.#reloginOrNull();
|
|
1512
|
+
if (relogged !== null) return relogged;
|
|
1513
|
+
throw error;
|
|
1264
1514
|
}
|
|
1265
1515
|
throw error;
|
|
1266
1516
|
}
|
|
@@ -1272,7 +1522,7 @@ var AuthManager = class {
|
|
|
1272
1522
|
return null;
|
|
1273
1523
|
}
|
|
1274
1524
|
try {
|
|
1275
|
-
return await this.#signInWithCredentials(auth
|
|
1525
|
+
return await this.#signInWithCredentials(auth);
|
|
1276
1526
|
} catch {
|
|
1277
1527
|
return null;
|
|
1278
1528
|
}
|
|
@@ -1283,21 +1533,41 @@ var AuthManager = class {
|
|
|
1283
1533
|
* Параллельные вызовы объединяются: одновременный старт нескольких запросов не должен
|
|
1284
1534
|
* приводить к нескольким попыткам входа и блокировке аккаунта.
|
|
1285
1535
|
*/
|
|
1286
|
-
#signInWithCredentials(
|
|
1536
|
+
#signInWithCredentials(credentials) {
|
|
1287
1537
|
if (this.#signingIn) return this.#signingIn;
|
|
1288
|
-
const promise = this.#performSignIn(
|
|
1538
|
+
const promise = this.#performSignIn(credentials).finally(() => {
|
|
1289
1539
|
this.#signingIn = null;
|
|
1290
1540
|
});
|
|
1291
1541
|
this.#signingIn = promise;
|
|
1292
1542
|
return promise;
|
|
1293
1543
|
}
|
|
1294
|
-
|
|
1544
|
+
/**
|
|
1545
|
+
* Берёт токен капчи для входа.
|
|
1546
|
+
*
|
|
1547
|
+
* `getTurnstileToken` приоритетнее готовой строки: токен Turnstile одноразовый и живёт
|
|
1548
|
+
* несколько минут, поэтому при повторном входе через сутки годится только свежий.
|
|
1549
|
+
*/
|
|
1550
|
+
async #resolveTurnstileToken(credentials) {
|
|
1551
|
+
if (credentials.getTurnstileToken) {
|
|
1552
|
+
const token = await credentials.getTurnstileToken();
|
|
1553
|
+
if (token) return token;
|
|
1554
|
+
}
|
|
1555
|
+
if (credentials.turnstileToken) return credentials.turnstileToken;
|
|
1556
|
+
throw new ItdConfigError(
|
|
1557
|
+
"\u0412\u0445\u043E\u0434 \u043F\u043E email \u0438 \u043F\u0430\u0440\u043E\u043B\u044E \u0442\u0440\u0435\u0431\u0443\u0435\u0442 \u0442\u043E\u043A\u0435\u043D \u043A\u0430\u043F\u0447\u0438 Cloudflare Turnstile: \u0431\u0435\u0437 \u043D\u0435\u0433\u043E \u0441\u0435\u0440\u0432\u0435\u0440 \u043E\u0442\u0432\u0435\u0447\u0430\u0435\u0442 422. \u041F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 auth.getTurnstileToken (\u0438\u0441\u0442\u043E\u0447\u043D\u0438\u043A \u0441\u0432\u0435\u0436\u0435\u0433\u043E \u0442\u043E\u043A\u0435\u043D\u0430) \u043B\u0438\u0431\u043E \u0440\u0430\u0437\u043E\u0432\u044B\u0439 auth.turnstileToken. \u041A\u043B\u044E\u0447 \u0432\u0438\u0434\u0436\u0435\u0442\u0430 \u2014 TURNSTILE_SITE_KEY."
|
|
1558
|
+
);
|
|
1559
|
+
}
|
|
1560
|
+
async #performSignIn(credentials) {
|
|
1561
|
+
const turnstileToken = await this.#resolveTurnstileToken(credentials);
|
|
1295
1562
|
const payload = await this.#http.request({
|
|
1296
1563
|
method: "POST",
|
|
1297
1564
|
path: AUTH_PATHS.signIn,
|
|
1298
|
-
body: { email, password },
|
|
1565
|
+
body: { email: credentials.email, password: credentials.password, turnstileToken },
|
|
1299
1566
|
skipAuth: true,
|
|
1300
|
-
skipAuthRefresh: true
|
|
1567
|
+
skipAuthRefresh: true,
|
|
1568
|
+
// Отложенный вход происходит при сборке заголовков уже начатого запроса — тот держит
|
|
1569
|
+
// место в очереди и ждёт токена. См. `skipQueue` в RawRequestOptions.
|
|
1570
|
+
skipQueue: true
|
|
1301
1571
|
});
|
|
1302
1572
|
const accessToken = readAccessToken(payload);
|
|
1303
1573
|
if (!accessToken) {
|
|
@@ -1312,43 +1582,6 @@ var AuthManager = class {
|
|
|
1312
1582
|
}
|
|
1313
1583
|
};
|
|
1314
1584
|
|
|
1315
|
-
// src/core/runtime.ts
|
|
1316
|
-
function detectRuntime() {
|
|
1317
|
-
const nav = globalThis.navigator;
|
|
1318
|
-
if (nav?.product === "ReactNative") return "react-native";
|
|
1319
|
-
if (typeof document !== "undefined") return "browser";
|
|
1320
|
-
return "server";
|
|
1321
|
-
}
|
|
1322
|
-
function shouldUseCookieJar(mode) {
|
|
1323
|
-
if (mode === "browser") return false;
|
|
1324
|
-
if (mode === "server") return true;
|
|
1325
|
-
return detectRuntime() === "server";
|
|
1326
|
-
}
|
|
1327
|
-
function shouldSendCredentials(mode) {
|
|
1328
|
-
if (mode === "browser") return true;
|
|
1329
|
-
if (mode === "server") return false;
|
|
1330
|
-
return detectRuntime() === "browser";
|
|
1331
|
-
}
|
|
1332
|
-
function resolveFetch(custom) {
|
|
1333
|
-
if (custom) return custom;
|
|
1334
|
-
if (typeof globalThis.fetch === "function") {
|
|
1335
|
-
return globalThis.fetch.bind(globalThis);
|
|
1336
|
-
}
|
|
1337
|
-
throw new ItdConfigError(
|
|
1338
|
-
"\u0412 \u044D\u0442\u043E\u0439 \u0441\u0440\u0435\u0434\u0435 \u043D\u0435\u0442 \u0433\u043B\u043E\u0431\u0430\u043B\u044C\u043D\u043E\u0433\u043E fetch. \u041E\u0431\u043D\u043E\u0432\u0438\u0442\u0435\u0441\u044C \u0434\u043E Node 18+ \u043B\u0438\u0431\u043E \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 \u0441\u0432\u043E\u044E \u0440\u0435\u0430\u043B\u0438\u0437\u0430\u0446\u0438\u044E \u0447\u0435\u0440\u0435\u0437 \u043E\u043F\u0446\u0438\u044E fetch."
|
|
1339
|
-
);
|
|
1340
|
-
}
|
|
1341
|
-
function supportsStreamingBody() {
|
|
1342
|
-
return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
|
|
1343
|
-
}
|
|
1344
|
-
function hasLocalStorage() {
|
|
1345
|
-
try {
|
|
1346
|
-
return typeof globalThis.localStorage !== "undefined" && globalThis.localStorage !== null;
|
|
1347
|
-
} catch {
|
|
1348
|
-
return false;
|
|
1349
|
-
}
|
|
1350
|
-
}
|
|
1351
|
-
|
|
1352
1585
|
// src/core/storage.ts
|
|
1353
1586
|
var MemoryTokenStorage = class {
|
|
1354
1587
|
#session = null;
|
|
@@ -1463,6 +1696,8 @@ function normalizeBaseUrl(baseUrl) {
|
|
|
1463
1696
|
// src/core/config.ts
|
|
1464
1697
|
var DEFAULT_BASE_URL = "https://xn--d1ah4a.com";
|
|
1465
1698
|
var DEFAULT_TIMEOUT = 3e4;
|
|
1699
|
+
var LIBRARY_VERSION = "0.0.4";
|
|
1700
|
+
var DEFAULT_USER_AGENT = `Mozilla/5.0 (compatible; itd-api/${LIBRARY_VERSION}; +https://github.com/KiowDev/itd-api)`;
|
|
1466
1701
|
var DEFAULT_RATE_LIMIT_DELAYS = Object.freeze([1e3, 5e3, 3e4, 6e4, 9e4]);
|
|
1467
1702
|
function requirePositive(value, name) {
|
|
1468
1703
|
if (!Number.isFinite(value) || value < 0) {
|
|
@@ -1554,13 +1789,19 @@ function validateAuth(auth) {
|
|
|
1554
1789
|
return auth;
|
|
1555
1790
|
}
|
|
1556
1791
|
if ("email" in auth || "password" in auth) {
|
|
1557
|
-
const { email, password } = auth;
|
|
1792
|
+
const { email, password, turnstileToken, getTurnstileToken } = auth;
|
|
1558
1793
|
if (typeof email !== "string" || email.trim() === "") {
|
|
1559
1794
|
throw new ItdConfigError("auth.email \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439");
|
|
1560
1795
|
}
|
|
1561
1796
|
if (typeof password !== "string" || password === "") {
|
|
1562
1797
|
throw new ItdConfigError("auth.password \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439");
|
|
1563
1798
|
}
|
|
1799
|
+
if (getTurnstileToken !== void 0 && typeof getTurnstileToken !== "function") {
|
|
1800
|
+
throw new ItdConfigError("auth.getTurnstileToken \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u0444\u0443\u043D\u043A\u0446\u0438\u0435\u0439");
|
|
1801
|
+
}
|
|
1802
|
+
if (turnstileToken !== void 0 && (typeof turnstileToken !== "string" || turnstileToken.trim() === "")) {
|
|
1803
|
+
throw new ItdConfigError("auth.turnstileToken \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439");
|
|
1804
|
+
}
|
|
1564
1805
|
return auth;
|
|
1565
1806
|
}
|
|
1566
1807
|
throw new ItdConfigError(
|
|
@@ -1568,11 +1809,16 @@ function validateAuth(auth) {
|
|
|
1568
1809
|
);
|
|
1569
1810
|
}
|
|
1570
1811
|
function resolveConfig(options = {}) {
|
|
1571
|
-
const mode = options.mode ??
|
|
1572
|
-
if (mode
|
|
1573
|
-
throw new ItdConfigError(
|
|
1812
|
+
const mode = options.mode ?? RuntimeMode.Auto;
|
|
1813
|
+
if (!Object.values(RuntimeMode).includes(mode)) {
|
|
1814
|
+
throw new ItdConfigError(
|
|
1815
|
+
`mode \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043E\u0434\u043D\u0438\u043C \u0438\u0437 ${Object.values(RuntimeMode).join(", ")}, \u043F\u043E\u043B\u0443\u0447\u0435\u043D\u043E: ${mode}`
|
|
1816
|
+
);
|
|
1574
1817
|
}
|
|
1575
1818
|
const timeout = requirePositive(options.timeout ?? DEFAULT_TIMEOUT, "timeout");
|
|
1819
|
+
if (options.deviceId !== void 0 && (typeof options.deviceId !== "string" || options.deviceId.trim() === "")) {
|
|
1820
|
+
throw new ItdConfigError("deviceId \u0434\u043E\u043B\u0436\u0435\u043D \u0431\u044B\u0442\u044C \u043D\u0435\u043F\u0443\u0441\u0442\u043E\u0439 \u0441\u0442\u0440\u043E\u043A\u043E\u0439");
|
|
1821
|
+
}
|
|
1576
1822
|
return {
|
|
1577
1823
|
baseUrl: normalizeBaseUrl(options.baseUrl ?? DEFAULT_BASE_URL),
|
|
1578
1824
|
auth: validateAuth(options.auth),
|
|
@@ -1586,19 +1832,102 @@ function resolveConfig(options = {}) {
|
|
|
1586
1832
|
hooks: options.hooks ?? {},
|
|
1587
1833
|
logger: options.logger === true ? consoleLogger() : options.logger || void 0,
|
|
1588
1834
|
headers: { ...options.headers },
|
|
1835
|
+
deviceId: options.deviceId,
|
|
1836
|
+
// `false` — способ не слать заголовок вовсе; строка заменяет умолчание.
|
|
1837
|
+
userAgent: options.userAgent === false ? void 0 : options.userAgent ?? DEFAULT_USER_AGENT,
|
|
1589
1838
|
mode,
|
|
1590
1839
|
useCookieJar: shouldUseCookieJar(mode),
|
|
1591
1840
|
sendCredentials: shouldSendCredentials(mode)
|
|
1592
1841
|
};
|
|
1593
1842
|
}
|
|
1594
1843
|
|
|
1595
|
-
// src/core/
|
|
1844
|
+
// src/core/redact.ts
|
|
1845
|
+
var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
|
|
1846
|
+
var SECRET_FIELDS = /* @__PURE__ */ new Set([
|
|
1847
|
+
"password",
|
|
1848
|
+
"oldpassword",
|
|
1849
|
+
"newpassword",
|
|
1850
|
+
"accesstoken",
|
|
1851
|
+
"refreshtoken",
|
|
1852
|
+
"currentpassword",
|
|
1853
|
+
"flowtoken",
|
|
1854
|
+
"token",
|
|
1855
|
+
"turnstiletoken",
|
|
1856
|
+
"otp"
|
|
1857
|
+
]);
|
|
1858
|
+
function maskSecret(value) {
|
|
1859
|
+
if (value.length <= 8) return "\u2026";
|
|
1860
|
+
return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
|
|
1861
|
+
}
|
|
1862
|
+
function redactHeaders(headers) {
|
|
1863
|
+
const result = {};
|
|
1864
|
+
headers.forEach((value, name) => {
|
|
1865
|
+
if (SECRET_HEADERS.has(name.toLowerCase())) {
|
|
1866
|
+
const spaceAt = value.indexOf(" ");
|
|
1867
|
+
result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
result[name] = value;
|
|
1871
|
+
});
|
|
1872
|
+
return result;
|
|
1873
|
+
}
|
|
1874
|
+
function redactBody(body) {
|
|
1875
|
+
if (body === null || body === void 0) return body;
|
|
1876
|
+
if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
|
|
1877
|
+
if (isBlob(body)) return "[Blob]";
|
|
1878
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
|
|
1879
|
+
if (Array.isArray(body)) return body.map(redactBody);
|
|
1880
|
+
if (typeof body === "object") {
|
|
1881
|
+
const result = {};
|
|
1882
|
+
for (const [key, value] of Object.entries(body)) {
|
|
1883
|
+
result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
|
|
1884
|
+
}
|
|
1885
|
+
return result;
|
|
1886
|
+
}
|
|
1887
|
+
return body;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
// src/core/unwrap.ts
|
|
1891
|
+
function unwrapData(body) {
|
|
1892
|
+
if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
|
|
1893
|
+
const keys = Object.keys(body);
|
|
1894
|
+
if (keys.length !== 1 || keys[0] !== "data") return body;
|
|
1895
|
+
return body.data;
|
|
1896
|
+
}
|
|
1596
1897
|
function isRecord(value) {
|
|
1597
1898
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1598
1899
|
}
|
|
1599
1900
|
function asString(value) {
|
|
1600
1901
|
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1601
1902
|
}
|
|
1903
|
+
function pickArray(source, field) {
|
|
1904
|
+
if (typeof source !== "object" || source === null) return [];
|
|
1905
|
+
const value = source[field];
|
|
1906
|
+
return Array.isArray(value) ? value : [];
|
|
1907
|
+
}
|
|
1908
|
+
function pickObject(source, field) {
|
|
1909
|
+
if (typeof source !== "object" || source === null) return void 0;
|
|
1910
|
+
const value = source[field];
|
|
1911
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1912
|
+
return value;
|
|
1913
|
+
}
|
|
1914
|
+
function pickBoolean(source, field, fallback = false) {
|
|
1915
|
+
if (typeof source !== "object" || source === null) return fallback;
|
|
1916
|
+
const value = source[field];
|
|
1917
|
+
return typeof value === "boolean" ? value : fallback;
|
|
1918
|
+
}
|
|
1919
|
+
function pickNumber(source, field, fallback) {
|
|
1920
|
+
if (typeof source !== "object" || source === null) return fallback;
|
|
1921
|
+
const value = source[field];
|
|
1922
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1923
|
+
}
|
|
1924
|
+
function pickString(source, field) {
|
|
1925
|
+
if (typeof source !== "object" || source === null) return void 0;
|
|
1926
|
+
const value = source[field];
|
|
1927
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// src/core/error-factory.ts
|
|
1602
1931
|
function collectFieldErrors(source) {
|
|
1603
1932
|
const result = {};
|
|
1604
1933
|
const errors = source.errors;
|
|
@@ -1707,6 +2036,10 @@ var CODE_TO_CLASS = {
|
|
|
1707
2036
|
SESSION_EXPIRED: ItdAuthError,
|
|
1708
2037
|
SESSION_REVOKED: ItdAuthError,
|
|
1709
2038
|
SESSION_INVALID_REFRESH_TOKEN: ItdAuthError,
|
|
2039
|
+
// Оба приходят с `/auth/refresh`: первый — когда cookie refresh_token не долетела,
|
|
2040
|
+
// второй — когда она есть, но сессия за ней уже мертва.
|
|
2041
|
+
REFRESH_TOKEN_MISSING: ItdAuthError,
|
|
2042
|
+
SESSION_NOT_FOUND: ItdAuthError,
|
|
1710
2043
|
ACCOUNT_INVALID_CREDENTIALS: ItdAuthError,
|
|
1711
2044
|
ACCESS_DENIED: ItdForbiddenError,
|
|
1712
2045
|
ENTITY_NOT_FOUND: ItdNotFoundError,
|
|
@@ -1724,6 +2057,10 @@ function classByStatus(status) {
|
|
|
1724
2057
|
if (status >= 500) return ItdServerError;
|
|
1725
2058
|
return ItdApiError;
|
|
1726
2059
|
}
|
|
2060
|
+
function safeRawBody(body) {
|
|
2061
|
+
if (!isRecord(body) || body.type !== "validation" || !isRecord(body.found)) return body;
|
|
2062
|
+
return { ...body, found: redactBody(body.found) };
|
|
2063
|
+
}
|
|
1727
2064
|
function createApiError(context) {
|
|
1728
2065
|
const parsed = parseErrorBody(context.body, context.status, context.statusText);
|
|
1729
2066
|
const rateLimit = readRateLimit(context.headers);
|
|
@@ -1739,7 +2076,7 @@ function createApiError(context) {
|
|
|
1739
2076
|
requestId: getRequestId(context.headers),
|
|
1740
2077
|
method: context.method,
|
|
1741
2078
|
path: context.path,
|
|
1742
|
-
raw: context.body,
|
|
2079
|
+
raw: safeRawBody(context.body),
|
|
1743
2080
|
response: context.response,
|
|
1744
2081
|
retryAfter: parseRetryAfter(context.headers?.get("retry-after"))
|
|
1745
2082
|
};
|
|
@@ -1750,84 +2087,6 @@ function createApiError(context) {
|
|
|
1750
2087
|
return new Ctor(init);
|
|
1751
2088
|
}
|
|
1752
2089
|
|
|
1753
|
-
// src/core/redact.ts
|
|
1754
|
-
var SECRET_HEADERS = /* @__PURE__ */ new Set(["authorization", "cookie", "set-cookie", "x-api-key"]);
|
|
1755
|
-
var SECRET_FIELDS = /* @__PURE__ */ new Set([
|
|
1756
|
-
"password",
|
|
1757
|
-
"oldpassword",
|
|
1758
|
-
"newpassword",
|
|
1759
|
-
"accesstoken",
|
|
1760
|
-
"refreshtoken",
|
|
1761
|
-
"flowtoken",
|
|
1762
|
-
"token",
|
|
1763
|
-
"otp"
|
|
1764
|
-
]);
|
|
1765
|
-
function maskSecret(value) {
|
|
1766
|
-
if (value.length <= 8) return "\u2026";
|
|
1767
|
-
return `${value.slice(0, 4)}\u2026(${value.length})\u2026${value.slice(-3)}`;
|
|
1768
|
-
}
|
|
1769
|
-
function redactHeaders(headers) {
|
|
1770
|
-
const result = {};
|
|
1771
|
-
headers.forEach((value, name) => {
|
|
1772
|
-
if (SECRET_HEADERS.has(name.toLowerCase())) {
|
|
1773
|
-
const spaceAt = value.indexOf(" ");
|
|
1774
|
-
result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
|
|
1775
|
-
return;
|
|
1776
|
-
}
|
|
1777
|
-
result[name] = value;
|
|
1778
|
-
});
|
|
1779
|
-
return result;
|
|
1780
|
-
}
|
|
1781
|
-
function redactBody(body) {
|
|
1782
|
-
if (body === null || body === void 0) return body;
|
|
1783
|
-
if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
|
|
1784
|
-
if (typeof Blob !== "undefined" && body instanceof Blob) return "[Blob]";
|
|
1785
|
-
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
|
|
1786
|
-
if (Array.isArray(body)) return body.map(redactBody);
|
|
1787
|
-
if (typeof body === "object") {
|
|
1788
|
-
const result = {};
|
|
1789
|
-
for (const [key, value] of Object.entries(body)) {
|
|
1790
|
-
result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[\u0441\u043A\u0440\u044B\u0442\u043E]" : redactBody(value);
|
|
1791
|
-
}
|
|
1792
|
-
return result;
|
|
1793
|
-
}
|
|
1794
|
-
return body;
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
// src/core/unwrap.ts
|
|
1798
|
-
function unwrapData(body) {
|
|
1799
|
-
if (typeof body !== "object" || body === null || Array.isArray(body)) return body;
|
|
1800
|
-
const keys = Object.keys(body);
|
|
1801
|
-
if (keys.length !== 1 || keys[0] !== "data") return body;
|
|
1802
|
-
return body.data;
|
|
1803
|
-
}
|
|
1804
|
-
function pickArray(source, field) {
|
|
1805
|
-
if (typeof source !== "object" || source === null) return [];
|
|
1806
|
-
const value = source[field];
|
|
1807
|
-
return Array.isArray(value) ? value : [];
|
|
1808
|
-
}
|
|
1809
|
-
function pickObject(source, field) {
|
|
1810
|
-
if (typeof source !== "object" || source === null) return void 0;
|
|
1811
|
-
const value = source[field];
|
|
1812
|
-
if (typeof value !== "object" || value === null || Array.isArray(value)) return void 0;
|
|
1813
|
-
return value;
|
|
1814
|
-
}
|
|
1815
|
-
function pickBoolean(source, field, fallback = false) {
|
|
1816
|
-
if (typeof source !== "object" || source === null) return fallback;
|
|
1817
|
-
const value = source[field];
|
|
1818
|
-
return typeof value === "boolean" ? value : fallback;
|
|
1819
|
-
}
|
|
1820
|
-
function pickNumber(source, field, fallback) {
|
|
1821
|
-
if (typeof source !== "object" || source === null) return fallback;
|
|
1822
|
-
const value = source[field];
|
|
1823
|
-
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
1824
|
-
}
|
|
1825
|
-
function pickString(source, field) {
|
|
1826
|
-
if (typeof source !== "object" || source === null) return void 0;
|
|
1827
|
-
const value = source[field];
|
|
1828
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1829
|
-
}
|
|
1830
|
-
|
|
1831
2090
|
// src/core/http.ts
|
|
1832
2091
|
function sleep(ms) {
|
|
1833
2092
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
@@ -1843,7 +2102,7 @@ function setHeader(headers, name, value) {
|
|
|
1843
2102
|
}
|
|
1844
2103
|
function isRawBody(body) {
|
|
1845
2104
|
if (typeof body !== "object" || body === null) return typeof body === "string";
|
|
1846
|
-
return typeof FormData !== "undefined" && body instanceof FormData ||
|
|
2105
|
+
return typeof FormData !== "undefined" && body instanceof FormData || isBlob(body) || typeof URLSearchParams !== "undefined" && body instanceof URLSearchParams || typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body instanceof ArrayBuffer || ArrayBuffer.isView(body);
|
|
1847
2106
|
}
|
|
1848
2107
|
async function readBody(response) {
|
|
1849
2108
|
if (response.status === 204 || response.status === 205) return void 0;
|
|
@@ -1913,7 +2172,8 @@ var HttpClient = class {
|
|
|
1913
2172
|
*/
|
|
1914
2173
|
async request(options) {
|
|
1915
2174
|
const task = () => this.#withRetries(options);
|
|
1916
|
-
|
|
2175
|
+
if (!this.#collaborators.schedule || options.skipQueue) return task();
|
|
2176
|
+
return this.#collaborators.schedule(task);
|
|
1917
2177
|
}
|
|
1918
2178
|
async #withRetries(options) {
|
|
1919
2179
|
const method = options.method.toUpperCase();
|
|
@@ -1945,6 +2205,11 @@ var HttpClient = class {
|
|
|
1945
2205
|
async #buildHeaders(options, url) {
|
|
1946
2206
|
const headers = new Headers();
|
|
1947
2207
|
headers.set("Accept", "application/json");
|
|
2208
|
+
headers.set("X-Requested-With", "XMLHttpRequest");
|
|
2209
|
+
if (this.#config.userAgent) setHeader(headers, "User-Agent", this.#config.userAgent);
|
|
2210
|
+
if (this.#collaborators.getDeviceId) {
|
|
2211
|
+
setHeader(headers, "X-Device-Id", await this.#collaborators.getDeviceId());
|
|
2212
|
+
}
|
|
1948
2213
|
for (const [name, value] of Object.entries(this.#config.headers))
|
|
1949
2214
|
setHeader(headers, name, value);
|
|
1950
2215
|
if (!options.skipAuth && this.#collaborators.getAuthHeaders) {
|
|
@@ -2177,21 +2442,15 @@ function isKnownNotificationType(type) {
|
|
|
2177
2442
|
}
|
|
2178
2443
|
|
|
2179
2444
|
// src/notifications/normalize.ts
|
|
2180
|
-
function isRecord2(value) {
|
|
2181
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2182
|
-
}
|
|
2183
|
-
function asString2(value) {
|
|
2184
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
2185
|
-
}
|
|
2186
2445
|
function asActor(value) {
|
|
2187
|
-
if (!
|
|
2188
|
-
const id =
|
|
2446
|
+
if (!isRecord(value)) return void 0;
|
|
2447
|
+
const id = asString(value.id);
|
|
2189
2448
|
if (!id) return void 0;
|
|
2190
2449
|
return {
|
|
2191
2450
|
id,
|
|
2192
|
-
username:
|
|
2193
|
-
displayName:
|
|
2194
|
-
avatar:
|
|
2451
|
+
username: asString(value.username) ?? "",
|
|
2452
|
+
displayName: asString(value.displayName) ?? "",
|
|
2453
|
+
avatar: asString(value.avatar) ?? "",
|
|
2195
2454
|
...typeof value.isFollowing === "boolean" ? { isFollowing: value.isFollowing } : {},
|
|
2196
2455
|
...typeof value.isFollowedBy === "boolean" ? { isFollowedBy: value.isFollowedBy } : {}
|
|
2197
2456
|
};
|
|
@@ -2204,33 +2463,34 @@ function readActors(source) {
|
|
|
2204
2463
|
return single ? [single] : [];
|
|
2205
2464
|
}
|
|
2206
2465
|
function normalizeNotification(input) {
|
|
2207
|
-
const source =
|
|
2208
|
-
const payload =
|
|
2209
|
-
const rawType =
|
|
2210
|
-
const createdAt =
|
|
2211
|
-
const readAt =
|
|
2466
|
+
const source = isRecord(input) ? input : {};
|
|
2467
|
+
const payload = isRecord(source.payload) ? source.payload : source;
|
|
2468
|
+
const rawType = asString(payload.type) ?? asString(source.type) ?? "";
|
|
2469
|
+
const createdAt = asString(payload.createdAt) ?? asString(source.createdAt) ?? "";
|
|
2470
|
+
const readAt = asString(payload.readAt) ?? asString(source.readAt);
|
|
2212
2471
|
const isRead = typeof payload.isRead === "boolean" ? payload.isRead : typeof payload.read === "boolean" ? payload.read : Boolean(readAt);
|
|
2213
|
-
const subjectId =
|
|
2214
|
-
const targetId =
|
|
2472
|
+
const subjectId = asString(payload.subjectId);
|
|
2473
|
+
const targetId = asString(payload.targetId);
|
|
2215
2474
|
const subjectIsComment = payload.subjectType === "comment";
|
|
2475
|
+
const clickUrl = asString(payload.clickUrl);
|
|
2216
2476
|
return {
|
|
2217
|
-
id:
|
|
2477
|
+
id: asString(payload.id) ?? asString(source.id) ?? "",
|
|
2218
2478
|
type: canonicalNotificationType(rawType),
|
|
2219
2479
|
rawType,
|
|
2220
|
-
entityId:
|
|
2221
|
-
parentEntityId:
|
|
2480
|
+
entityId: asString(payload.entityId) ?? (subjectIsComment ? subjectId ?? targetId : targetId) ?? null,
|
|
2481
|
+
parentEntityId: asString(payload.parentEntityId) ?? (subjectIsComment ? targetId ?? null : null),
|
|
2222
2482
|
isRead,
|
|
2223
2483
|
actors: readActors(payload),
|
|
2224
2484
|
count: typeof payload.count === "number" && payload.count > 0 ? payload.count : 1,
|
|
2225
|
-
preview:
|
|
2226
|
-
...
|
|
2485
|
+
preview: asString(payload.entityPreview) ?? asString(payload.preview) ?? null,
|
|
2486
|
+
...clickUrl ? { clickUrl } : {},
|
|
2227
2487
|
createdAt,
|
|
2228
|
-
updatedAt:
|
|
2488
|
+
updatedAt: asString(payload.updatedAt) ?? readAt ?? createdAt,
|
|
2229
2489
|
raw: input
|
|
2230
2490
|
};
|
|
2231
2491
|
}
|
|
2232
2492
|
function readNotificationEvent(data) {
|
|
2233
|
-
const source =
|
|
2493
|
+
const source = isRecord(data) ? data : {};
|
|
2234
2494
|
return {
|
|
2235
2495
|
notification: normalizeNotification(data),
|
|
2236
2496
|
unreadCount: typeof source.unreadCount === "number" ? source.unreadCount : void 0,
|
|
@@ -2238,8 +2498,8 @@ function readNotificationEvent(data) {
|
|
|
2238
2498
|
};
|
|
2239
2499
|
}
|
|
2240
2500
|
function readUnreadCountEvent(data) {
|
|
2241
|
-
if (!
|
|
2242
|
-
const payload =
|
|
2501
|
+
if (!isRecord(data)) return void 0;
|
|
2502
|
+
const payload = isRecord(data.payload) ? data.payload : void 0;
|
|
2243
2503
|
if (!payload) return void 0;
|
|
2244
2504
|
return typeof payload.count === "number" ? payload.count : void 0;
|
|
2245
2505
|
}
|
|
@@ -2318,6 +2578,7 @@ var PollTransport = class {
|
|
|
2318
2578
|
}
|
|
2319
2579
|
/** Ждёт следующего опроса, прерываясь при отмене. */
|
|
2320
2580
|
#wait(signal) {
|
|
2581
|
+
if (signal.aborted) return Promise.resolve();
|
|
2321
2582
|
return new Promise((resolve) => {
|
|
2322
2583
|
const timer = setTimeout(finish, this.#interval);
|
|
2323
2584
|
function finish() {
|
|
@@ -2590,6 +2851,14 @@ var SseTransport = class {
|
|
|
2590
2851
|
};
|
|
2591
2852
|
|
|
2592
2853
|
// src/realtime/stream.ts
|
|
2854
|
+
var RealtimeTransportKind = Object.freeze({
|
|
2855
|
+
/** Поток событий, если среда умеет читать тело по частям, иначе опрос. */
|
|
2856
|
+
Auto: "auto",
|
|
2857
|
+
/** Поток `text/event-stream`. */
|
|
2858
|
+
Sse: "sse",
|
|
2859
|
+
/** Периодический опрос REST. */
|
|
2860
|
+
Poll: "poll"
|
|
2861
|
+
});
|
|
2593
2862
|
var ItdRealtime = class {
|
|
2594
2863
|
#deps;
|
|
2595
2864
|
#options;
|
|
@@ -2597,6 +2866,15 @@ var ItdRealtime = class {
|
|
|
2597
2866
|
#transport;
|
|
2598
2867
|
#maxAttempts;
|
|
2599
2868
|
#controller;
|
|
2869
|
+
/**
|
|
2870
|
+
* Хочет ли вызывающий код, чтобы соединение было живо.
|
|
2871
|
+
*
|
|
2872
|
+
* Отдельно от `#controller`, потому что тот появляется только после `await` внутри
|
|
2873
|
+
* {@link connect}. Без этого флага два вызова подряд проскочили бы проверку оба
|
|
2874
|
+
* и подняли два соединения, а `disconnect()` во время ожидания счётчика не был бы
|
|
2875
|
+
* замечен и соединение поднялось бы уже после отмены.
|
|
2876
|
+
*/
|
|
2877
|
+
#wanted = false;
|
|
2600
2878
|
#status = RealtimeStatus.Disconnected;
|
|
2601
2879
|
#attempt = 0;
|
|
2602
2880
|
#timer;
|
|
@@ -2632,7 +2910,8 @@ var ItdRealtime = class {
|
|
|
2632
2910
|
* Возвращает управление сразу после запуска: соединение живёт в фоне.
|
|
2633
2911
|
*/
|
|
2634
2912
|
async connect() {
|
|
2635
|
-
if (this.#
|
|
2913
|
+
if (this.#wanted) return;
|
|
2914
|
+
this.#wanted = true;
|
|
2636
2915
|
this.#attachEnvironmentListeners();
|
|
2637
2916
|
if (this.#options.syncCount !== false) {
|
|
2638
2917
|
try {
|
|
@@ -2641,10 +2920,11 @@ var ItdRealtime = class {
|
|
|
2641
2920
|
this.#deps.logger?.debug("\u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u043F\u043E\u043B\u0443\u0447\u0438\u0442\u044C \u0447\u0438\u0441\u043B\u043E \u043D\u0435\u043F\u0440\u043E\u0447\u0438\u0442\u0430\u043D\u043D\u044B\u0445", error);
|
|
2642
2921
|
}
|
|
2643
2922
|
}
|
|
2644
|
-
this.#run();
|
|
2923
|
+
if (this.#wanted) this.#run();
|
|
2645
2924
|
}
|
|
2646
2925
|
/** Закрывает соединение и отменяет запланированные попытки. */
|
|
2647
2926
|
disconnect() {
|
|
2927
|
+
this.#wanted = false;
|
|
2648
2928
|
if (this.#timer !== void 0) {
|
|
2649
2929
|
clearTimeout(this.#timer);
|
|
2650
2930
|
this.#timer = void 0;
|
|
@@ -2661,9 +2941,9 @@ var ItdRealtime = class {
|
|
|
2661
2941
|
this.#emitter.removeAllListeners();
|
|
2662
2942
|
}
|
|
2663
2943
|
#createTransport() {
|
|
2664
|
-
const kind = this.#options.transport ??
|
|
2944
|
+
const kind = this.#options.transport ?? RealtimeTransportKind.Auto;
|
|
2665
2945
|
if (typeof kind === "object") return kind;
|
|
2666
|
-
if (kind ===
|
|
2946
|
+
if (kind === RealtimeTransportKind.Poll || kind === RealtimeTransportKind.Auto && !supportsStreamingBody()) {
|
|
2667
2947
|
return new PollTransport({
|
|
2668
2948
|
...this.#options.pollInterval !== void 0 ? { interval: this.#options.pollInterval } : {}
|
|
2669
2949
|
});
|
|
@@ -2674,6 +2954,7 @@ var ItdRealtime = class {
|
|
|
2674
2954
|
}
|
|
2675
2955
|
/** Запускает попытку подключения; повторы планирует сам. */
|
|
2676
2956
|
#run() {
|
|
2957
|
+
this.#controller?.abort();
|
|
2677
2958
|
const controller = new AbortController();
|
|
2678
2959
|
this.#controller = controller;
|
|
2679
2960
|
this.#setStatus(RealtimeStatus.Connecting);
|
|
@@ -2703,8 +2984,7 @@ var ItdRealtime = class {
|
|
|
2703
2984
|
#handleEvent(name, data) {
|
|
2704
2985
|
this.#emitter.emit("message", { name, data });
|
|
2705
2986
|
if (name === "connected") {
|
|
2706
|
-
|
|
2707
|
-
this.#emitter.emit("ready", { userId });
|
|
2987
|
+
this.#emitter.emit("ready", { userId: pickString(data, "userId") });
|
|
2708
2988
|
return;
|
|
2709
2989
|
}
|
|
2710
2990
|
if (name === "notification") {
|
|
@@ -2792,13 +3072,22 @@ var ItdRealtime = class {
|
|
|
2792
3072
|
};
|
|
2793
3073
|
|
|
2794
3074
|
// src/core/pagination.ts
|
|
3075
|
+
var PaginationMode = Object.freeze({
|
|
3076
|
+
/** Следующая страница запрашивается непрозрачным курсором. */
|
|
3077
|
+
Cursor: "cursor",
|
|
3078
|
+
/** Следующая страница запрашивается номером. */
|
|
3079
|
+
Page: "page",
|
|
3080
|
+
/** Следующая страница запрашивается смещением от начала списка. */
|
|
3081
|
+
Offset: "offset"
|
|
3082
|
+
});
|
|
2795
3083
|
function readItems(body, fields) {
|
|
2796
3084
|
if (Array.isArray(body)) return body;
|
|
2797
3085
|
for (const field of fields) {
|
|
2798
3086
|
const items = pickArray(body, field);
|
|
2799
3087
|
if (items.length > 0) return items;
|
|
2800
3088
|
}
|
|
2801
|
-
|
|
3089
|
+
const primary = fields[0];
|
|
3090
|
+
return primary === void 0 ? [] : pickArray(body, primary);
|
|
2802
3091
|
}
|
|
2803
3092
|
function readCursor(body) {
|
|
2804
3093
|
const pagination = pickObject(body, "pagination");
|
|
@@ -2857,12 +3146,13 @@ function readOffsetPage(body, field, offset) {
|
|
|
2857
3146
|
var Paginator = class {
|
|
2858
3147
|
#options;
|
|
2859
3148
|
#maxPages;
|
|
2860
|
-
#state
|
|
3149
|
+
#state;
|
|
2861
3150
|
#finished = false;
|
|
2862
3151
|
#pagesLoaded = 0;
|
|
2863
3152
|
constructor(options) {
|
|
2864
3153
|
this.#options = options;
|
|
2865
3154
|
this.#maxPages = options.maxPages ?? 1e3;
|
|
3155
|
+
this.#state = options.start ?? {};
|
|
2866
3156
|
}
|
|
2867
3157
|
/**
|
|
2868
3158
|
* Загружает следующую страницу.
|
|
@@ -2928,7 +3218,7 @@ var Paginator = class {
|
|
|
2928
3218
|
this.#finished = true;
|
|
2929
3219
|
return previous;
|
|
2930
3220
|
}
|
|
2931
|
-
if (this.#options.mode ===
|
|
3221
|
+
if (this.#options.mode === PaginationMode.Cursor) {
|
|
2932
3222
|
const cursor = page.nextCursor ?? void 0;
|
|
2933
3223
|
if (!cursor || cursor === previous.cursor) {
|
|
2934
3224
|
this.#finished = true;
|
|
@@ -2936,7 +3226,7 @@ var Paginator = class {
|
|
|
2936
3226
|
}
|
|
2937
3227
|
return { cursor };
|
|
2938
3228
|
}
|
|
2939
|
-
if (this.#options.mode ===
|
|
3229
|
+
if (this.#options.mode === PaginationMode.Page) {
|
|
2940
3230
|
return { page: (previous.page ?? 1) + 1 };
|
|
2941
3231
|
}
|
|
2942
3232
|
return { offset: page.nextOffset ?? (previous.offset ?? 0) + page.items.length };
|
|
@@ -2965,13 +3255,15 @@ var BaseResource = class {
|
|
|
2965
3255
|
*
|
|
2966
3256
|
* @param mode схема пагинации эндпоинта
|
|
2967
3257
|
* @param load загружает одну страницу для указанной позиции
|
|
3258
|
+
* @param options `maxPages` и `signal`, а также `start` — позиция, с которой продолжить
|
|
2968
3259
|
*/
|
|
2969
3260
|
paginate(mode, load, options) {
|
|
2970
3261
|
return new Paginator({
|
|
2971
3262
|
mode,
|
|
2972
3263
|
load,
|
|
2973
3264
|
...options?.maxPages !== void 0 ? { maxPages: options.maxPages } : {},
|
|
2974
|
-
...options?.signal !== void 0 ? { signal: options.signal } : {}
|
|
3265
|
+
...options?.signal !== void 0 ? { signal: options.signal } : {},
|
|
3266
|
+
...options?.start !== void 0 ? { start: options.start } : {}
|
|
2975
3267
|
});
|
|
2976
3268
|
}
|
|
2977
3269
|
};
|
|
@@ -2985,6 +3277,14 @@ function withPageState(query, state) {
|
|
|
2985
3277
|
}
|
|
2986
3278
|
|
|
2987
3279
|
// src/resources/auth.ts
|
|
3280
|
+
var OAuthProvider = Object.freeze({
|
|
3281
|
+
Yandex: "yandex",
|
|
3282
|
+
Google: "google"
|
|
3283
|
+
});
|
|
3284
|
+
var SignInStatus = Object.freeze({
|
|
3285
|
+
Authenticated: "authenticated",
|
|
3286
|
+
OtpRequired: "otp_required"
|
|
3287
|
+
});
|
|
2988
3288
|
var AuthResource = class extends BaseResource {
|
|
2989
3289
|
#auth;
|
|
2990
3290
|
constructor(http, deps) {
|
|
@@ -3018,6 +3318,8 @@ var AuthResource = class extends BaseResource {
|
|
|
3018
3318
|
* тогда продолжайте через {@link verifyOtp} либо воспользуйтесь {@link signInWithOtp}.
|
|
3019
3319
|
*
|
|
3020
3320
|
* При успешном входе токен сохраняется в клиенте автоматически.
|
|
3321
|
+
*
|
|
3322
|
+
* @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
|
|
3021
3323
|
*/
|
|
3022
3324
|
async signIn(credentials, options = {}) {
|
|
3023
3325
|
const body = await this.http.request({
|
|
@@ -3031,9 +3333,9 @@ var AuthResource = class extends BaseResource {
|
|
|
3031
3333
|
const accessToken = pickString(body, "accessToken");
|
|
3032
3334
|
if (accessToken) {
|
|
3033
3335
|
await this.#auth.setAccessToken(accessToken);
|
|
3034
|
-
return { status:
|
|
3336
|
+
return { status: SignInStatus.Authenticated, accessToken };
|
|
3035
3337
|
}
|
|
3036
|
-
return { status:
|
|
3338
|
+
return { status: SignInStatus.OtpRequired, flowToken: pickString(body, "flowToken") };
|
|
3037
3339
|
}
|
|
3038
3340
|
/**
|
|
3039
3341
|
* Подтверждает вход кодом из письма.
|
|
@@ -3088,14 +3390,22 @@ var AuthResource = class extends BaseResource {
|
|
|
3088
3390
|
async signInWithOtp(input, options = {}) {
|
|
3089
3391
|
const { getOtp, ...credentials } = input;
|
|
3090
3392
|
const result = await this.signIn(credentials, options);
|
|
3091
|
-
if (result.status ===
|
|
3393
|
+
if (result.status === SignInStatus.Authenticated) return result.accessToken;
|
|
3092
3394
|
if (!result.flowToken) {
|
|
3093
3395
|
throw new ItdConfigError(
|
|
3094
3396
|
"\u0421\u0435\u0440\u0432\u0435\u0440 \u0437\u0430\u043F\u0440\u043E\u0441\u0438\u043B \u043A\u043E\u0434 \u043F\u043E\u0434\u0442\u0432\u0435\u0440\u0436\u0434\u0435\u043D\u0438\u044F, \u043D\u043E \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B flowToken \u2014 \u043F\u0440\u043E\u0434\u043E\u043B\u0436\u0438\u0442\u044C \u0432\u0445\u043E\u0434 \u043D\u0435\u0432\u043E\u0437\u043C\u043E\u0436\u043D\u043E"
|
|
3095
3397
|
);
|
|
3096
3398
|
}
|
|
3097
3399
|
const otp = await getOtp();
|
|
3098
|
-
return this.verifyOtp(
|
|
3400
|
+
return this.verifyOtp(
|
|
3401
|
+
{
|
|
3402
|
+
email: credentials.email,
|
|
3403
|
+
password: credentials.password,
|
|
3404
|
+
otp,
|
|
3405
|
+
flowToken: result.flowToken
|
|
3406
|
+
},
|
|
3407
|
+
options
|
|
3408
|
+
);
|
|
3099
3409
|
}
|
|
3100
3410
|
/**
|
|
3101
3411
|
* Обновляет токен доступа.
|
|
@@ -3109,9 +3419,18 @@ var AuthResource = class extends BaseResource {
|
|
|
3109
3419
|
/**
|
|
3110
3420
|
* Есть ли признак живой сессии обновления.
|
|
3111
3421
|
*
|
|
3112
|
-
* Проверяет cookie `is_auth`, которую сервер ставит рядом с refresh
|
|
3113
|
-
* не дёргать API у неавторизованного
|
|
3114
|
-
* cookie ведёт сама среда, и прочитать её
|
|
3422
|
+
* Проверяет cookie `is_auth`, которую сервер ставит рядом с refresh-токеном, а также
|
|
3423
|
+
* refresh-токен, переданный строкой. Позволяет не дёргать API у неавторизованного
|
|
3424
|
+
* пользователя. В браузере всегда `true`: cookie ведёт сама среда, и прочитать её
|
|
3425
|
+
* из JS нельзя.
|
|
3426
|
+
*
|
|
3427
|
+
* Читает {@link TokenStorage}, поэтому результат верен и до первого запроса.
|
|
3428
|
+
*
|
|
3429
|
+
* @example
|
|
3430
|
+
* ```ts
|
|
3431
|
+
* if (await itd.auth.hasRefreshSession()) await itd.auth.refresh();
|
|
3432
|
+
* else redirectToLogin();
|
|
3433
|
+
* ```
|
|
3115
3434
|
*/
|
|
3116
3435
|
hasRefreshSession() {
|
|
3117
3436
|
return this.#auth.hasRefreshSession();
|
|
@@ -3126,32 +3445,48 @@ var AuthResource = class extends BaseResource {
|
|
|
3126
3445
|
});
|
|
3127
3446
|
await this.#auth.clear();
|
|
3128
3447
|
}
|
|
3129
|
-
/**
|
|
3448
|
+
/**
|
|
3449
|
+
* Завершает все сессии пользователя и очищает локальную.
|
|
3450
|
+
*
|
|
3451
|
+
* Собран из двух запросов, потому что единого эндпоинта на сервере нет:
|
|
3452
|
+
* `POST /api/v1/auth/logout-all` отвечает `404`. Сначала отзываются все прочие сессии
|
|
3453
|
+
* (`DELETE /api/v1/auth/sessions`), затем завершается текущая — в обратном порядке
|
|
3454
|
+
* отзывать было бы уже нечем.
|
|
3455
|
+
*/
|
|
3130
3456
|
async logoutAll(options = {}) {
|
|
3131
|
-
await this.
|
|
3132
|
-
|
|
3133
|
-
path: "/api/v1/auth/logout-all",
|
|
3134
|
-
skipAuthRefresh: true,
|
|
3135
|
-
...this.requestOptions(options)
|
|
3136
|
-
});
|
|
3137
|
-
await this.#auth.clear();
|
|
3457
|
+
await this.revokeOtherSessions(options);
|
|
3458
|
+
await this.logout(options);
|
|
3138
3459
|
}
|
|
3139
3460
|
/** Забывает сессию локально, не обращаясь к серверу. */
|
|
3140
3461
|
signOut() {
|
|
3141
3462
|
return this.#auth.clear();
|
|
3142
3463
|
}
|
|
3143
|
-
/**
|
|
3144
|
-
|
|
3145
|
-
|
|
3464
|
+
/**
|
|
3465
|
+
* Запрашивает письмо с кодом для сброса пароля.
|
|
3466
|
+
*
|
|
3467
|
+
* @returns `flowToken`, который нужно передать в {@link resetPassword}
|
|
3468
|
+
*/
|
|
3469
|
+
async forgotPassword(input, options = {}) {
|
|
3470
|
+
const body = await this.http.request({
|
|
3146
3471
|
method: "POST",
|
|
3147
3472
|
path: "/api/v1/auth/forgot-password",
|
|
3148
|
-
body:
|
|
3473
|
+
body: input,
|
|
3149
3474
|
skipAuth: true,
|
|
3150
3475
|
skipAuthRefresh: true,
|
|
3151
3476
|
...this.requestOptions(options)
|
|
3152
3477
|
});
|
|
3478
|
+
const flowToken = pickString(body, "flowToken");
|
|
3479
|
+
if (!flowToken) {
|
|
3480
|
+
throw new ItdConfigError("\u0421\u0435\u0440\u0432\u0435\u0440 \u043D\u0435 \u0432\u0435\u0440\u043D\u0443\u043B flowToken \u043F\u0440\u0438 \u0437\u0430\u043F\u0440\u043E\u0441\u0435 \u0441\u0431\u0440\u043E\u0441\u0430 \u043F\u0430\u0440\u043E\u043B\u044F");
|
|
3481
|
+
}
|
|
3482
|
+
return flowToken;
|
|
3153
3483
|
}
|
|
3154
|
-
/**
|
|
3484
|
+
/**
|
|
3485
|
+
* Устанавливает новый пароль по коду из письма.
|
|
3486
|
+
*
|
|
3487
|
+
* Сервер ждёт все четыре поля сразу — `email`, `otp`, `flowToken` и `newPassword`;
|
|
3488
|
+
* при нехватке любого отвечает `422`.
|
|
3489
|
+
*/
|
|
3155
3490
|
resetPassword(input, options = {}) {
|
|
3156
3491
|
return this.http.request({
|
|
3157
3492
|
method: "POST",
|
|
@@ -3162,12 +3497,45 @@ var AuthResource = class extends BaseResource {
|
|
|
3162
3497
|
...this.requestOptions(options)
|
|
3163
3498
|
});
|
|
3164
3499
|
}
|
|
3165
|
-
/**
|
|
3500
|
+
/**
|
|
3501
|
+
* Полный сброс пароля с кодом из письма.
|
|
3502
|
+
*
|
|
3503
|
+
* Тот же приём, что и {@link signInWithOtp}: код запрашивается функцией `getOtp`,
|
|
3504
|
+
* остальное библиотека делает сама.
|
|
3505
|
+
*
|
|
3506
|
+
* @example
|
|
3507
|
+
* ```ts
|
|
3508
|
+
* await itd.auth.resetPasswordWithOtp({
|
|
3509
|
+
* email,
|
|
3510
|
+
* turnstileToken,
|
|
3511
|
+
* newPassword,
|
|
3512
|
+
* getOtp: () => rl.question('Код из письма: '),
|
|
3513
|
+
* });
|
|
3514
|
+
* ```
|
|
3515
|
+
*/
|
|
3516
|
+
async resetPasswordWithOtp(input, options = {}) {
|
|
3517
|
+
const flowToken = await this.forgotPassword(
|
|
3518
|
+
{ email: input.email, turnstileToken: input.turnstileToken },
|
|
3519
|
+
options
|
|
3520
|
+
);
|
|
3521
|
+
const otp = await input.getOtp();
|
|
3522
|
+
await this.resetPassword(
|
|
3523
|
+
{ email: input.email, otp, flowToken, newPassword: input.newPassword },
|
|
3524
|
+
options
|
|
3525
|
+
);
|
|
3526
|
+
}
|
|
3527
|
+
/**
|
|
3528
|
+
* Меняет пароль. Требует действующей сессии.
|
|
3529
|
+
*
|
|
3530
|
+
* При неверном текущем пароле сервер отвечает `ACCOUNT_CURRENT_PASSWORD_INCORRECT`.
|
|
3531
|
+
*/
|
|
3166
3532
|
changePassword(input, options = {}) {
|
|
3167
3533
|
return this.http.request({
|
|
3168
3534
|
method: "POST",
|
|
3169
3535
|
path: "/api/v1/auth/change-password",
|
|
3170
|
-
|
|
3536
|
+
// Текущий пароль уходит под двумя именами: какое из них ждёт сервер, снаружи
|
|
3537
|
+
// не проверить, а лишнее поле он игнорирует.
|
|
3538
|
+
body: { ...input, currentPassword: input.oldPassword },
|
|
3171
3539
|
...this.requestOptions(options)
|
|
3172
3540
|
});
|
|
3173
3541
|
}
|
|
@@ -3237,7 +3605,7 @@ var CommentsResource = class extends BaseResource {
|
|
|
3237
3605
|
iterateReplies(commentId, params = {}) {
|
|
3238
3606
|
const path = `/api/comments/${encodePathSegment(commentId, "commentId")}/replies`;
|
|
3239
3607
|
return this.paginate(
|
|
3240
|
-
|
|
3608
|
+
PaginationMode.Page,
|
|
3241
3609
|
async (state) => {
|
|
3242
3610
|
const body = await this.http.request({
|
|
3243
3611
|
method: "GET",
|
|
@@ -3247,7 +3615,7 @@ var CommentsResource = class extends BaseResource {
|
|
|
3247
3615
|
});
|
|
3248
3616
|
return readPagedPage(body, "replies");
|
|
3249
3617
|
},
|
|
3250
|
-
params
|
|
3618
|
+
{ ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
|
|
3251
3619
|
);
|
|
3252
3620
|
}
|
|
3253
3621
|
/**
|
|
@@ -3328,7 +3696,11 @@ var IMAGE_MIME_TYPES = Object.freeze([
|
|
|
3328
3696
|
"image/heic",
|
|
3329
3697
|
"image/heif"
|
|
3330
3698
|
]);
|
|
3331
|
-
var VIDEO_MIME_TYPES = Object.freeze([
|
|
3699
|
+
var VIDEO_MIME_TYPES = Object.freeze([
|
|
3700
|
+
"video/mp4",
|
|
3701
|
+
"video/webm",
|
|
3702
|
+
"video/quicktime"
|
|
3703
|
+
]);
|
|
3332
3704
|
var AUDIO_MIME_TYPES = Object.freeze(["audio/ogg"]);
|
|
3333
3705
|
var ALLOWED_MIME_TYPES = Object.freeze([
|
|
3334
3706
|
...IMAGE_MIME_TYPES,
|
|
@@ -3407,7 +3779,7 @@ var FilesResource = class extends BaseResource {
|
|
|
3407
3779
|
* ```
|
|
3408
3780
|
*/
|
|
3409
3781
|
async upload(input, options = {}) {
|
|
3410
|
-
const prepared = await this
|
|
3782
|
+
const prepared = await this.#prepare(input, options);
|
|
3411
3783
|
const form = new FormData();
|
|
3412
3784
|
form.set("file", prepared.blob, prepared.filename);
|
|
3413
3785
|
return this.http.request({
|
|
@@ -3458,11 +3830,11 @@ var FilesResource = class extends BaseResource {
|
|
|
3458
3830
|
});
|
|
3459
3831
|
}
|
|
3460
3832
|
/** Приводит любой поддерживаемый вход к `Blob` с именем и проверенным типом. */
|
|
3461
|
-
async prepare(input, options) {
|
|
3833
|
+
async #prepare(input, options) {
|
|
3462
3834
|
const { data, filename, contentType } = await this.#normalize(input, options);
|
|
3463
|
-
const type = contentType ?? ((data
|
|
3835
|
+
const type = contentType ?? ((isBlob(data) ? data.type : void 0) || mimeFromFilename(filename));
|
|
3464
3836
|
if (options.validateMime !== false) assertAllowedMime(type || void 0, filename);
|
|
3465
|
-
const blob = data
|
|
3837
|
+
const blob = isBlob(data) && (!type || data.type === type) ? data : new Blob([data], { type: type ?? "" });
|
|
3466
3838
|
return { blob, filename };
|
|
3467
3839
|
}
|
|
3468
3840
|
async #normalize(input, options) {
|
|
@@ -3479,8 +3851,8 @@ var FilesResource = class extends BaseResource {
|
|
|
3479
3851
|
...options.contentType ? { contentType: options.contentType } : {}
|
|
3480
3852
|
};
|
|
3481
3853
|
}
|
|
3482
|
-
if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || input
|
|
3483
|
-
const fallbackName = input
|
|
3854
|
+
if (input instanceof ArrayBuffer || ArrayBuffer.isView(input) || isBlob(input)) {
|
|
3855
|
+
const fallbackName = isFile(input) ? input.name : options.filename ?? this.#nameFromMime(options.contentType);
|
|
3484
3856
|
return {
|
|
3485
3857
|
data: input,
|
|
3486
3858
|
filename: options.filename ?? fallbackName,
|
|
@@ -3546,7 +3918,7 @@ var HashtagsResource = class extends BaseResource {
|
|
|
3546
3918
|
iteratePosts(tag, params = {}) {
|
|
3547
3919
|
const path = `/api/hashtags/${encodePathSegment(tag, "tag")}/posts`;
|
|
3548
3920
|
return this.paginate(
|
|
3549
|
-
|
|
3921
|
+
PaginationMode.Cursor,
|
|
3550
3922
|
async (state) => {
|
|
3551
3923
|
const body = await this.http.request({
|
|
3552
3924
|
method: "GET",
|
|
@@ -3556,7 +3928,7 @@ var HashtagsResource = class extends BaseResource {
|
|
|
3556
3928
|
});
|
|
3557
3929
|
return readCursorPage(body, "posts");
|
|
3558
3930
|
},
|
|
3559
|
-
params
|
|
3931
|
+
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
3560
3932
|
);
|
|
3561
3933
|
}
|
|
3562
3934
|
};
|
|
@@ -3787,8 +4159,11 @@ var NotificationsResource = class extends BaseResource {
|
|
|
3787
4159
|
* const next = await itd.notifications.list({ limit: 20, offset: page.nextOffset });
|
|
3788
4160
|
* ```
|
|
3789
4161
|
*/
|
|
3790
|
-
|
|
3791
|
-
|
|
4162
|
+
list(params = {}) {
|
|
4163
|
+
return this.#loadPage(params, params.offset ?? 0);
|
|
4164
|
+
}
|
|
4165
|
+
/** Общая загрузка страницы для {@link list} и {@link iterate}. */
|
|
4166
|
+
async #loadPage(params, offset) {
|
|
3792
4167
|
const body = await this.http.request({
|
|
3793
4168
|
method: "GET",
|
|
3794
4169
|
// Завершающий слэш обязателен: без него сервер отвечает ошибкой.
|
|
@@ -3811,19 +4186,9 @@ var NotificationsResource = class extends BaseResource {
|
|
|
3811
4186
|
*/
|
|
3812
4187
|
iterate(params = {}) {
|
|
3813
4188
|
return this.paginate(
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
const body = await this.http.request({
|
|
3818
|
-
method: "GET",
|
|
3819
|
-
path: "/api/notifications/",
|
|
3820
|
-
query: { limit: params.limit, offset },
|
|
3821
|
-
...this.requestOptions(params)
|
|
3822
|
-
});
|
|
3823
|
-
const page = readOffsetPage(body, "notifications", offset);
|
|
3824
|
-
return { ...page, items: page.items.map(normalizeNotification) };
|
|
3825
|
-
},
|
|
3826
|
-
params
|
|
4189
|
+
PaginationMode.Offset,
|
|
4190
|
+
(state) => this.#loadPage(params, state.offset ?? 0),
|
|
4191
|
+
{ ...params, ...params.offset !== void 0 ? { start: { offset: params.offset } } : {} }
|
|
3827
4192
|
);
|
|
3828
4193
|
}
|
|
3829
4194
|
/** Загружает число непрочитанных уведомлений. */
|
|
@@ -3955,7 +4320,7 @@ var PostsResource = class extends BaseResource {
|
|
|
3955
4320
|
*/
|
|
3956
4321
|
iterate(params = {}) {
|
|
3957
4322
|
return this.paginate(
|
|
3958
|
-
|
|
4323
|
+
PaginationMode.Cursor,
|
|
3959
4324
|
async (state) => {
|
|
3960
4325
|
const body = await this.http.request({
|
|
3961
4326
|
method: "GET",
|
|
@@ -3965,7 +4330,7 @@ var PostsResource = class extends BaseResource {
|
|
|
3965
4330
|
});
|
|
3966
4331
|
return readCursorPage(body, "posts");
|
|
3967
4332
|
},
|
|
3968
|
-
params
|
|
4333
|
+
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
3969
4334
|
);
|
|
3970
4335
|
}
|
|
3971
4336
|
/**
|
|
@@ -4129,7 +4494,7 @@ var PostsResource = class extends BaseResource {
|
|
|
4129
4494
|
iterateByUser(user, params = {}) {
|
|
4130
4495
|
const path = `/api/posts/user/${encodePathSegment(user, "user")}`;
|
|
4131
4496
|
return this.paginate(
|
|
4132
|
-
|
|
4497
|
+
PaginationMode.Cursor,
|
|
4133
4498
|
async (state) => {
|
|
4134
4499
|
const body = await this.http.request({
|
|
4135
4500
|
method: "GET",
|
|
@@ -4142,7 +4507,7 @@ var PostsResource = class extends BaseResource {
|
|
|
4142
4507
|
});
|
|
4143
4508
|
return readCursorPage(body, "posts");
|
|
4144
4509
|
},
|
|
4145
|
-
params
|
|
4510
|
+
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4146
4511
|
);
|
|
4147
4512
|
}
|
|
4148
4513
|
/** Загружает страницу постов, которые пользователь отметил реакцией. */
|
|
@@ -4159,7 +4524,7 @@ var PostsResource = class extends BaseResource {
|
|
|
4159
4524
|
iterateLikedByUser(user, params = {}) {
|
|
4160
4525
|
const path = `/api/posts/user/${encodePathSegment(user, "user")}/liked`;
|
|
4161
4526
|
return this.paginate(
|
|
4162
|
-
|
|
4527
|
+
PaginationMode.Cursor,
|
|
4163
4528
|
async (state) => {
|
|
4164
4529
|
const body = await this.http.request({
|
|
4165
4530
|
method: "GET",
|
|
@@ -4169,7 +4534,7 @@ var PostsResource = class extends BaseResource {
|
|
|
4169
4534
|
});
|
|
4170
4535
|
return readCursorPage(body, "posts");
|
|
4171
4536
|
},
|
|
4172
|
-
params
|
|
4537
|
+
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4173
4538
|
);
|
|
4174
4539
|
}
|
|
4175
4540
|
/**
|
|
@@ -4191,7 +4556,7 @@ var PostsResource = class extends BaseResource {
|
|
|
4191
4556
|
iterateComments(postId, params = {}) {
|
|
4192
4557
|
const path = `/api/posts/${encodePathSegment(postId, "postId")}/comments`;
|
|
4193
4558
|
return this.paginate(
|
|
4194
|
-
|
|
4559
|
+
PaginationMode.Cursor,
|
|
4195
4560
|
async (state) => {
|
|
4196
4561
|
const body = await this.http.request({
|
|
4197
4562
|
method: "GET",
|
|
@@ -4201,7 +4566,7 @@ var PostsResource = class extends BaseResource {
|
|
|
4201
4566
|
});
|
|
4202
4567
|
return readFlatCursorPage(body, "comments");
|
|
4203
4568
|
},
|
|
4204
|
-
params
|
|
4569
|
+
{ ...params, ...params.cursor ? { start: { cursor: params.cursor } } : {} }
|
|
4205
4570
|
);
|
|
4206
4571
|
}
|
|
4207
4572
|
/**
|
|
@@ -4479,28 +4844,33 @@ var UsersResource = class extends BaseResource {
|
|
|
4479
4844
|
...this.requestOptions(options)
|
|
4480
4845
|
});
|
|
4481
4846
|
}
|
|
4482
|
-
|
|
4847
|
+
/**
|
|
4848
|
+
* Загружает одну страницу списка пользователей.
|
|
4849
|
+
*
|
|
4850
|
+
* Имена полей перечислены с запасом: списки подписчиков и заблокированных приходят
|
|
4851
|
+
* под `users`, но альтернативное имя ничего не стоит и спасает, если эндпоинт назовёт
|
|
4852
|
+
* список по-своему.
|
|
4853
|
+
*/
|
|
4854
|
+
async #loadUserPage(path, params, state) {
|
|
4483
4855
|
const body = await this.http.request({
|
|
4484
4856
|
method: "GET",
|
|
4485
4857
|
path,
|
|
4486
|
-
query: { limit: params.limit
|
|
4858
|
+
query: withPageState({ limit: params.limit }, state),
|
|
4487
4859
|
...this.requestOptions(params)
|
|
4488
4860
|
});
|
|
4489
|
-
return readPagedPage(body, "users");
|
|
4861
|
+
return readPagedPage(body, "users", "followers", "following", "blocked");
|
|
4862
|
+
}
|
|
4863
|
+
#userPage(path, params) {
|
|
4864
|
+
return this.#loadUserPage(path, params, {
|
|
4865
|
+
...params.page !== void 0 ? { page: params.page } : {}
|
|
4866
|
+
});
|
|
4490
4867
|
}
|
|
4491
4868
|
#userPaginator(path, params) {
|
|
4492
4869
|
return this.paginate(
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
path,
|
|
4498
|
-
query: withPageState({ limit: params.limit }, state),
|
|
4499
|
-
...this.requestOptions(params)
|
|
4500
|
-
});
|
|
4501
|
-
return readPagedPage(body, "users");
|
|
4502
|
-
},
|
|
4503
|
-
params
|
|
4870
|
+
PaginationMode.Page,
|
|
4871
|
+
(state) => this.#loadUserPage(path, params, state),
|
|
4872
|
+
// Без `start` перебор начинался бы с первой страницы, молча игнорируя `page`.
|
|
4873
|
+
{ ...params, ...params.page !== void 0 ? { start: { page: params.page } } : {} }
|
|
4504
4874
|
);
|
|
4505
4875
|
}
|
|
4506
4876
|
};
|
|
@@ -4550,6 +4920,7 @@ var ItdClient = class {
|
|
|
4550
4920
|
this.#queue = this.#config.rateLimit ? new RequestQueue(this.#config.rateLimit) : void 0;
|
|
4551
4921
|
this.#http.setCollaborators({
|
|
4552
4922
|
getAuthHeaders: () => this.#authManager.getAuthHeaders(),
|
|
4923
|
+
getDeviceId: () => this.#authManager.getDeviceId(),
|
|
4553
4924
|
onUnauthorized: () => this.#authManager.onUnauthorized(),
|
|
4554
4925
|
getCookieHeader: (url) => this.#jar.getHeader(url),
|
|
4555
4926
|
saveCookies: (url, response) => this.#jar.setFromResponse(url, response),
|
|
@@ -4818,6 +5189,6 @@ function toDate(value) {
|
|
|
4818
5189
|
return Number.isFinite(date.getTime()) ? date : null;
|
|
4819
5190
|
}
|
|
4820
5191
|
|
|
4821
|
-
export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AttachmentType, CommentSort, DEFAULT_BASE_URL, DEFAULT_TIMEOUT, FeedTab, IMAGE_MIME_TYPES, ItdAbortError, ItdApiError, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LikesVisibility, LocalStorageTokenStorage, MAX_RECONNECT_ATTEMPTS, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, RealtimeStatus, ReportReason, ReportTargetType, STREAM_PATH, VIDEO_MIME_TYPES, WallAccess, canonicalNotificationType, comment, createClient, createTokenStorage, formatNotificationText, isBuilder, isItdApiError, isItdAuthError, isItdError, isItdRateLimitError, isItdValidationError, isKnownNotificationType, isMyProfile, normalizeNotification, poll, post, readNotificationEvent, readUnreadCountEvent, report, resolveNotificationUrl, toDate };
|
|
4822
|
-
//# sourceMappingURL=chunk-
|
|
4823
|
-
//# sourceMappingURL=chunk-
|
|
5192
|
+
export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AUTH_FLAG_COOKIE, AUTH_PATHS, AttachmentType, CommentSort, DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DEFAULT_USER_AGENT, DEVICE_ID_HEADER, DetectedRuntime, FeedTab, IMAGE_MIME_TYPES, ItdAbortError, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdRealtime, ItdServerError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, LocalStorageTokenStorage, MAX_RECONNECT_ATTEMPTS, MemoryTokenStorage, NOTIFICATION_TYPE_ALIASES, NotificationType, OAuthProvider, PaginationMode, Paginator, RECONNECT_BACKOFF, RECONNECT_JITTER, REFRESH_COOKIE, REFRESH_COOKIE_PATH, RealtimeStatus, RealtimeTransportKind, ReportReason, ReportTargetType, RuntimeMode, STREAM_PATH, SignInStatus, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, WallAccess, canonicalNotificationType, comment, createClient, createTokenStorage, formatNotificationText, isBuilder, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdValidationError, isKnownNotificationType, isMyProfile, normalizeNotification, poll, post, readNotificationEvent, readUnreadCountEvent, report, resolveNotificationUrl, toDate };
|
|
5193
|
+
//# sourceMappingURL=chunk-JV75JWOX.js.map
|
|
5194
|
+
//# sourceMappingURL=chunk-JV75JWOX.js.map
|