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