itd-api 0.9.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -8
- package/dist/events/index.cjs +2 -2
- package/dist/events/index.d.cts +2 -2
- package/dist/events/index.d.ts +2 -2
- package/dist/events/index.js +2 -2
- package/dist/index.cjs +489 -92
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +186 -36
- package/dist/index.d.ts +186 -36
- package/dist/index.js +485 -94
- package/dist/index.js.map +1 -1
- package/dist/rest/index.cjs +2 -2
- package/dist/rest/index.d.cts +2 -2
- package/dist/rest/index.d.ts +2 -2
- package/dist/rest/index.js +2 -2
- package/dist/shared/{events-BIBO2Jif.js → events-B0Wtn4cF.js} +3 -3
- package/dist/shared/{events-BIBO2Jif.js.map → events-B0Wtn4cF.js.map} +1 -1
- package/dist/shared/{events-BsRm_Fba.cjs → events-BB791aPk.cjs} +8 -2
- package/dist/shared/{events-BsRm_Fba.cjs.map → events-BB791aPk.cjs.map} +1 -1
- package/dist/shared/{events-BCn5LRnT.d.cts → events-BNGjJVI3.d.cts} +2 -2
- package/dist/shared/{events-t1uGgpkf.d.ts → events-CJO6q93Z.d.ts} +2 -2
- package/dist/shared/{render-DqVzx68r.cjs → render-BimHjOhw.cjs} +2 -30
- package/dist/shared/render-BimHjOhw.cjs.map +1 -0
- package/dist/shared/{render-DA83gS0f.d.cts → render-C1A57W-5.d.cts} +2 -14
- package/dist/shared/{render--ZXmYEf-.js → render-CkhvH_yh.js} +2 -30
- package/dist/shared/render-CkhvH_yh.js.map +1 -0
- package/dist/shared/{render-BdnBV-aT.d.ts → render-DC5nvqcP.d.ts} +2 -14
- package/dist/shared/{url-i2f-X1hX.js → url-BA5hEwj2.js} +95 -16
- package/dist/shared/url-BA5hEwj2.js.map +1 -0
- package/dist/shared/{url-Doc0XXW_.cjs → url-CDVgMIq5.cjs} +130 -15
- package/dist/shared/url-CDVgMIq5.cjs.map +1 -0
- package/dist/shared/{url-Dbj6V6fk.d.cts → url-DkMueFAT.d.cts} +76 -17
- package/dist/shared/{url-Dbj6V6fk.d.ts → url-DkMueFAT.d.ts} +76 -17
- package/package.json +4 -3
- package/dist/shared/render--ZXmYEf-.js.map +0 -1
- package/dist/shared/render-DqVzx68r.cjs.map +0 -1
- package/dist/shared/url-Doc0XXW_.cjs.map +0 -1
- package/dist/shared/url-i2f-X1hX.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_url = require("./shared/url-
|
|
2
|
+
const require_url = require("./shared/url-CDVgMIq5.cjs");
|
|
3
3
|
const require_errors = require("./shared/errors-BmP3TKoW.cjs");
|
|
4
4
|
const require_redact = require("./shared/redact-lAmck1Z6.cjs");
|
|
5
|
-
const require_render = require("./shared/render-
|
|
5
|
+
const require_render = require("./shared/render-BimHjOhw.cjs");
|
|
6
6
|
const require_options = require("./shared/options-DjEtbVaf.cjs");
|
|
7
|
-
const require_events = require("./shared/events-
|
|
7
|
+
const require_events = require("./shared/events-BB791aPk.cjs");
|
|
8
8
|
const require_key_value_store = require("./shared/key-value-store-B5GYVEYZ.cjs");
|
|
9
9
|
const require_storage = require("./shared/storage-CZJZoaz9.cjs");
|
|
10
10
|
const require_multi_storage = require("./shared/multi-storage-s-fPtjoy.cjs");
|
|
@@ -32,12 +32,37 @@ const SignInStatus = Object.freeze({
|
|
|
32
32
|
Authenticated: "authenticated",
|
|
33
33
|
OtpRequired: "otp_required"
|
|
34
34
|
});
|
|
35
|
+
/**
|
|
36
|
+
* Поле тела запроса по умолчанию для известных провайдеров.
|
|
37
|
+
*
|
|
38
|
+
* Используется, когда провайдера не спрашивали. Ответ `captchaProvider()` важнее этой таблицы.
|
|
39
|
+
*/
|
|
40
|
+
const CAPTCHA_FIELDS = Object.freeze({
|
|
41
|
+
[require_url.CaptchaType.Itd]: require_url.CaptchaField.Itd,
|
|
42
|
+
[require_url.CaptchaType.Cloudflare]: require_url.CaptchaField.Cloudflare
|
|
43
|
+
});
|
|
44
|
+
/** Состояния, которые может вернуть проверка QR-входа. */
|
|
45
|
+
const QrLoginStatus = Object.freeze({
|
|
46
|
+
Pending: "pending",
|
|
47
|
+
Scanned: "scanned",
|
|
48
|
+
CaptchaRequired: "captcha_required",
|
|
49
|
+
Authorized: "authorized",
|
|
50
|
+
Rejected: "rejected"
|
|
51
|
+
});
|
|
52
|
+
/** Состояния, приходящие из потокового наблюдения за QR-входом. */
|
|
53
|
+
const QrLoginStreamStatus = Object.freeze({
|
|
54
|
+
Pending: "pending",
|
|
55
|
+
Scanned: "scanned",
|
|
56
|
+
Approved: "approved",
|
|
57
|
+
Rejected: "rejected"
|
|
58
|
+
});
|
|
35
59
|
function requireString(body, key, message) {
|
|
36
60
|
const value = require_url.pickString(body, key);
|
|
37
61
|
if (!value) throw new require_errors.ItdConfigError(message);
|
|
38
62
|
return value;
|
|
39
63
|
}
|
|
40
64
|
const AUTH_CHECK = require_url.passthroughOperation("auth.check");
|
|
65
|
+
const AUTH_CAPTCHA_PROVIDER = require_url.passthroughOperation("auth.captchaProvider");
|
|
41
66
|
const AUTH_SIGN_UP = require_url.defineBuiltInOperation("auth.signUp", (body) => requireString(body, "flowToken", "Сервер не вернул flowToken при регистрации"));
|
|
42
67
|
const AUTH_SIGN_IN = require_url.defineBuiltInOperation("auth.signIn", (body) => {
|
|
43
68
|
const accessToken = require_url.pickString(body, "accessToken");
|
|
@@ -59,6 +84,84 @@ const AUTH_CHANGE_PASSWORD = require_url.voidOperation("auth.changePassword");
|
|
|
59
84
|
const AUTH_SESSIONS = require_url.defineBuiltInOperation("auth.sessions", (body) => require_url.pickArray(body, "sessions"));
|
|
60
85
|
const AUTH_REVOKE_SESSION = require_url.voidOperation("auth.revokeSession");
|
|
61
86
|
const AUTH_REVOKE_OTHER_SESSIONS = require_url.voidOperation("auth.revokeOtherSessions");
|
|
87
|
+
const AUTH_QR_START = require_url.passthroughOperation("auth.qrStart");
|
|
88
|
+
const AUTH_QR_CLAIM = require_url.passthroughOperation("auth.qrClaim");
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/session/captcha.ts
|
|
91
|
+
function requireCaptchaText(value, field) {
|
|
92
|
+
if (value !== void 0 && (typeof value !== "string" || value.trim() === "")) throw new require_errors.ItdConfigError(`${field} должен быть непустой строкой`);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Проверяет форму блока `auth.captcha`.
|
|
96
|
+
*
|
|
97
|
+
* Отсутствие источника токена ошибкой не считается: до входа по паролю дело может и не
|
|
98
|
+
* дойти. О нехватке сообщит {@link resolveCaptchaBody} в момент входа.
|
|
99
|
+
*
|
|
100
|
+
* @throws {ItdConfigError} при некорректных значениях
|
|
101
|
+
*/
|
|
102
|
+
function validateCaptchaOptions(captcha) {
|
|
103
|
+
if (captcha === void 0) return;
|
|
104
|
+
if (!require_url.isRecord$1(captcha)) throw new require_errors.ItdConfigError("auth.captcha должен быть объектом");
|
|
105
|
+
const { type, token, getToken, field } = captcha;
|
|
106
|
+
requireCaptchaText(type, "auth.captcha.type");
|
|
107
|
+
requireCaptchaText(token, "auth.captcha.token");
|
|
108
|
+
requireCaptchaText(field, "auth.captcha.field");
|
|
109
|
+
if (getToken !== void 0 && typeof getToken !== "function") throw new require_errors.ItdConfigError("auth.captcha.getToken должен быть функцией");
|
|
110
|
+
}
|
|
111
|
+
/** Разбирает ответ `GET /api/v1/auth/captcha/provider` для автоматического входа. */
|
|
112
|
+
function readCaptchaProvider(body) {
|
|
113
|
+
const config = require_url.isRecord$1(body) ? body : void 0;
|
|
114
|
+
const provider = config?.provider;
|
|
115
|
+
const field = config?.field;
|
|
116
|
+
if (typeof provider !== "string" || provider.trim() === "" || typeof field !== "string" || field.trim() === "") throw new require_errors.ItdConfigError("Сервер вернул неподдерживаемую конфигурацию капчи");
|
|
117
|
+
return {
|
|
118
|
+
provider,
|
|
119
|
+
field
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** Куда класть токен: настройка, иначе умолчание провайдера. */
|
|
123
|
+
function resolveField(type, configured) {
|
|
124
|
+
const field = configured ?? CAPTCHA_FIELDS[type];
|
|
125
|
+
if (field) return field;
|
|
126
|
+
throw new require_errors.ItdConfigError(`Неизвестно, в каком поле сервер ждёт токен капчи «${type}». Передайте auth.captcha.field либо оставьте auth.captcha.type = ${require_url.CaptchaChoice.Auto}, чтобы поле назвал сервер.`);
|
|
127
|
+
}
|
|
128
|
+
/** Спрашивает свежий токен, затем берёт разовый. */
|
|
129
|
+
async function resolveToken(captcha, type) {
|
|
130
|
+
const token = await captcha.getToken?.(type) || captcha.token;
|
|
131
|
+
if (token) return token;
|
|
132
|
+
throw new require_errors.ItdConfigError(`Источник капчи не вернул токен для провайдера «${type}». Проверьте auth.captcha.getToken.`);
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Готовит фрагмент тела запроса с токеном капчи.
|
|
136
|
+
*
|
|
137
|
+
* При `CaptchaChoice.Auto` провайдер спрашивается у сервера — тогда и тип виджета,
|
|
138
|
+
* и имя поля приходят оттуда. Явно названный тип решается без этого запроса.
|
|
139
|
+
*
|
|
140
|
+
* @param askProvider как спросить активного провайдера; вызывается, только если это нужно
|
|
141
|
+
* @throws {ItdConfigError} если капча не настроена или токен не получен
|
|
142
|
+
*/
|
|
143
|
+
async function resolveCaptchaBody(captcha, askProvider) {
|
|
144
|
+
if (!captcha?.token && !captcha?.getToken) throw new require_errors.ItdConfigError("Вход по email и паролю требует токен капчи. Передайте auth.captcha.getToken — источник свежего токена — либо разовый auth.captcha.token. В Node токен получает пакет @itd-api/captcha.");
|
|
145
|
+
const choice = captcha.type ?? require_url.CaptchaChoice.Auto;
|
|
146
|
+
if (choice === require_url.CaptchaChoice.Auto) {
|
|
147
|
+
const { provider, field } = await askProvider();
|
|
148
|
+
return { [field]: await resolveToken(captcha, provider) };
|
|
149
|
+
}
|
|
150
|
+
return { [resolveField(choice, captcha.field)]: await resolveToken(captcha, choice) };
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Готовит фрагмент тела для разового вызова с готовым доказательством.
|
|
154
|
+
*
|
|
155
|
+
* Поле берётся из самого доказательства либо из умолчания провайдера; запроса к серверу нет.
|
|
156
|
+
*
|
|
157
|
+
* @throws {ItdConfigError} если доказательство неполное
|
|
158
|
+
*/
|
|
159
|
+
function captchaBody(captcha) {
|
|
160
|
+
if (!require_url.isRecord$1(captcha)) throw new require_errors.ItdConfigError("captcha должен быть объектом { type, token }");
|
|
161
|
+
if (typeof captcha.token !== "string" || captcha.token.trim() === "") throw new require_errors.ItdConfigError("captcha.token должен быть непустой строкой");
|
|
162
|
+
if (typeof captcha.type !== "string" || captcha.type.trim() === "") throw new require_errors.ItdConfigError("captcha.type должен быть непустой строкой");
|
|
163
|
+
return { [resolveField(captcha.type, captcha.field)]: captcha.token };
|
|
164
|
+
}
|
|
62
165
|
//#endregion
|
|
63
166
|
//#region src/session/jwt.ts
|
|
64
167
|
/**
|
|
@@ -75,38 +178,34 @@ function decodeBase64Url(segment) {
|
|
|
75
178
|
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
76
179
|
return new TextDecoder().decode(bytes);
|
|
77
180
|
}
|
|
78
|
-
/**
|
|
79
|
-
|
|
80
|
-
*
|
|
81
|
-
* Подпись не проверяется — ключа для проверки у клиента нет. Прочитанные значения служат
|
|
82
|
-
* лишь метками для разделения локального состояния и не дают доступа: его определяет сервер.
|
|
83
|
-
*/
|
|
84
|
-
function readTokenIdentity(token) {
|
|
181
|
+
/** Читает полезную нагрузку JWT без проверки подписи. */
|
|
182
|
+
function readTokenPayload(token) {
|
|
85
183
|
try {
|
|
86
184
|
const payload = token.split(".")[1];
|
|
87
|
-
if (!payload) return
|
|
185
|
+
if (!payload) return void 0;
|
|
88
186
|
const parsed = JSON.parse(decodeBase64Url(payload));
|
|
89
|
-
if (typeof parsed !== "object" || parsed === null) return
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
...typeof sub === "string" && sub.length > 0 ? { subject: sub } : {},
|
|
93
|
-
...typeof sid === "string" && sid.length > 0 ? { sessionId: sid } : {}
|
|
94
|
-
};
|
|
187
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
188
|
+
return parsed;
|
|
95
189
|
} catch {
|
|
96
|
-
return
|
|
190
|
+
return;
|
|
97
191
|
}
|
|
98
192
|
}
|
|
99
|
-
/**
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
193
|
+
/** Читает используемые клиентом поля JWT за один раз. */
|
|
194
|
+
function readTokenMetadata(token) {
|
|
195
|
+
const payload = readTokenPayload(token);
|
|
196
|
+
if (!payload) return {};
|
|
197
|
+
const { sub, sid, exp } = payload;
|
|
198
|
+
const expiresAt = typeof exp === "number" && Number.isFinite(exp) ? exp * 1e3 : void 0;
|
|
199
|
+
return {
|
|
200
|
+
...typeof sub === "string" && sub.length > 0 ? { subject: sub } : {},
|
|
201
|
+
...typeof sid === "string" && sid.length > 0 ? { sessionId: sid } : {},
|
|
202
|
+
...expiresAt !== void 0 && Number.isFinite(expiresAt) ? { expiresAt } : {}
|
|
203
|
+
};
|
|
107
204
|
}
|
|
108
205
|
//#endregion
|
|
109
206
|
//#region src/session/auth.ts
|
|
207
|
+
/** Запас до истечения токена на очередь и выполнение запроса. */
|
|
208
|
+
const ACCESS_TOKEN_REFRESH_LEEWAY_MS = 3e4;
|
|
110
209
|
/**
|
|
111
210
|
* Проверяет форму `auth` и сообщает о типичных ошибках понятным текстом.
|
|
112
211
|
*
|
|
@@ -135,11 +234,10 @@ function validateAuth(auth) {
|
|
|
135
234
|
return { ...auth };
|
|
136
235
|
}
|
|
137
236
|
if ("email" in auth || "password" in auth) {
|
|
138
|
-
const { email, password,
|
|
237
|
+
const { email, password, captcha } = auth;
|
|
139
238
|
if (typeof email !== "string" || email.trim() === "") throw new require_errors.ItdConfigError("auth.email должен быть непустой строкой");
|
|
140
239
|
if (typeof password !== "string" || password === "") throw new require_errors.ItdConfigError("auth.password должен быть непустой строкой");
|
|
141
|
-
|
|
142
|
-
if (turnstileToken !== void 0 && (typeof turnstileToken !== "string" || turnstileToken.trim() === "")) throw new require_errors.ItdConfigError("auth.turnstileToken должен быть непустой строкой");
|
|
240
|
+
validateCaptchaOptions(captcha);
|
|
143
241
|
return { ...auth };
|
|
144
242
|
}
|
|
145
243
|
throw new require_errors.ItdConfigError("auth не распознан. Ожидается строка с accessToken либо объект { accessToken }, { email, password } или { getToken }");
|
|
@@ -186,6 +284,7 @@ function createItdAuth(options, deps) {
|
|
|
186
284
|
}
|
|
187
285
|
/** Пути эндпоинтов авторизации. */
|
|
188
286
|
const AUTH_PATHS = {
|
|
287
|
+
captchaProvider: "/api/v1/auth/captcha/provider",
|
|
189
288
|
signUp: "/api/v1/auth/sign-up",
|
|
190
289
|
signIn: "/api/v1/auth/sign-in",
|
|
191
290
|
verifyOtp: "/api/v1/auth/verify-otp",
|
|
@@ -195,7 +294,10 @@ const AUTH_PATHS = {
|
|
|
195
294
|
forgotPassword: "/api/v1/auth/forgot-password",
|
|
196
295
|
resetPassword: "/api/v1/auth/reset-password",
|
|
197
296
|
changePassword: "/api/v1/auth/change-password",
|
|
198
|
-
sessions: "/api/v1/auth/sessions"
|
|
297
|
+
sessions: "/api/v1/auth/sessions",
|
|
298
|
+
qrStart: "/api/v1/auth/qr/start",
|
|
299
|
+
qrClaim: "/api/v1/auth/qr/claim",
|
|
300
|
+
qrStream: "/api/v1/auth/qr/stream"
|
|
199
301
|
};
|
|
200
302
|
/**
|
|
201
303
|
* Публичный ключ Cloudflare Turnstile платформы итд.com.
|
|
@@ -205,14 +307,15 @@ const AUTH_PATHS = {
|
|
|
205
307
|
*
|
|
206
308
|
* Ключ привязан к домену: на чужом origin Cloudflare отказывает виджету с кодом `110200`.
|
|
207
309
|
* Поэтому отрисовать его может только код, выполняемый на самом итд.com. Остальным
|
|
208
|
-
* подходит `@itd-api/
|
|
310
|
+
* подходит `@itd-api/captcha`, готовый токен из другого источника или вовсе вход
|
|
209
311
|
* без капчи — по сохранённой сессии либо по токенам, взятым в браузере.
|
|
210
312
|
*
|
|
211
313
|
* @example
|
|
212
314
|
* ```ts
|
|
213
315
|
* turnstile.render('#captcha', {
|
|
214
316
|
* sitekey: TURNSTILE_SITE_KEY,
|
|
215
|
-
* callback: (
|
|
317
|
+
* callback: (token) =>
|
|
318
|
+
* itd.auth.signIn({ email, password, captcha: { type: CaptchaType.Cloudflare, token } }),
|
|
216
319
|
* });
|
|
217
320
|
* ```
|
|
218
321
|
*/
|
|
@@ -306,7 +409,7 @@ var AuthManager = class {
|
|
|
306
409
|
* старте читают хранилище один раз и не заводят каждый свой `deviceId`.
|
|
307
410
|
*/
|
|
308
411
|
#loading = null;
|
|
309
|
-
/**
|
|
412
|
+
/** Текущее обновление: к нему присоединяются запросы той же ревизии. */
|
|
310
413
|
#refreshing = null;
|
|
311
414
|
/** Общий промис входа по логину и паролю. */
|
|
312
415
|
#signingIn = null;
|
|
@@ -316,6 +419,7 @@ var AuthManager = class {
|
|
|
316
419
|
#externalToken;
|
|
317
420
|
/** Идентификаторы последнего токена внешнего источника для синхронных потребителей. */
|
|
318
421
|
#externalIdentity;
|
|
422
|
+
#tokenMetadata;
|
|
319
423
|
/**
|
|
320
424
|
* Идентификатор устройства.
|
|
321
425
|
*
|
|
@@ -329,9 +433,7 @@ var AuthManager = class {
|
|
|
329
433
|
* Счётчик смен владельца авторизации.
|
|
330
434
|
*
|
|
331
435
|
* Растёт при каждой операции, которая заменяет или очищает сессию извне: `clear`,
|
|
332
|
-
* `setSession`, `setAccessToken`, вход.
|
|
333
|
-
* сетевым запросом и сверяет перед записью — иначе запоздавший ответ обновления мог бы
|
|
334
|
-
* воскресить уже очищенную сессию поверх `signOut`.
|
|
436
|
+
* `setSession`, `setAccessToken`, вход. Refresh сверяет снимок до и после сетевого запроса.
|
|
335
437
|
*/
|
|
336
438
|
#authEpoch = 0;
|
|
337
439
|
#externalReadGeneration = 0;
|
|
@@ -364,6 +466,7 @@ var AuthManager = class {
|
|
|
364
466
|
this.#disposed = true;
|
|
365
467
|
this.#externalReadGeneration += 1;
|
|
366
468
|
this.#invalidateInFlight();
|
|
469
|
+
this.#tokenMetadata = void 0;
|
|
367
470
|
this.#emitter.removeAllListeners();
|
|
368
471
|
}
|
|
369
472
|
/**
|
|
@@ -424,12 +527,25 @@ var AuthManager = class {
|
|
|
424
527
|
return this.#session?.accessToken ?? null;
|
|
425
528
|
}
|
|
426
529
|
#identityForToken(accessToken) {
|
|
427
|
-
const token =
|
|
530
|
+
const token = this.#metadataForToken(accessToken);
|
|
428
531
|
return {
|
|
429
532
|
...token.subject ? { userId: token.subject } : {},
|
|
430
533
|
...token.sessionId ? { sessionId: token.sessionId } : {}
|
|
431
534
|
};
|
|
432
535
|
}
|
|
536
|
+
#metadataForToken(accessToken) {
|
|
537
|
+
if (!accessToken) {
|
|
538
|
+
this.#tokenMetadata = void 0;
|
|
539
|
+
return {};
|
|
540
|
+
}
|
|
541
|
+
if (this.#tokenMetadata?.accessToken === accessToken) return this.#tokenMetadata.value;
|
|
542
|
+
const value = readTokenMetadata(accessToken);
|
|
543
|
+
this.#tokenMetadata = {
|
|
544
|
+
accessToken,
|
|
545
|
+
value
|
|
546
|
+
};
|
|
547
|
+
return value;
|
|
548
|
+
}
|
|
433
549
|
async #readExternalToken(getToken) {
|
|
434
550
|
this.#assertActive();
|
|
435
551
|
const generation = ++this.#externalReadGeneration;
|
|
@@ -476,15 +592,33 @@ var AuthManager = class {
|
|
|
476
592
|
if (this.#jar.has("is_auth", this.#config.baseUrl)) return true;
|
|
477
593
|
return Boolean(this.#session?.refreshToken);
|
|
478
594
|
}
|
|
479
|
-
/**
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
595
|
+
/** Готовит внутреннюю сессию до retry исходного запроса. */
|
|
596
|
+
async preflight(allowRefresh) {
|
|
597
|
+
this.#assertActive();
|
|
598
|
+
let accessToken = (await this.#loadSession())?.accessToken;
|
|
599
|
+
if (!accessToken) {
|
|
600
|
+
const auth = this.#config.auth;
|
|
601
|
+
if (typeof auth === "object" && auth !== null && "email" in auth) accessToken = await this.#signInWithCredentials(auth) ?? void 0;
|
|
602
|
+
}
|
|
603
|
+
if (!accessToken || !allowRefresh || !this.#config.autoRefresh) return;
|
|
604
|
+
const expiresAt = this.#metadataForToken(accessToken).expiresAt;
|
|
605
|
+
if (expiresAt === void 0 || expiresAt > this.#config.clock.now() + ACCESS_TOKEN_REFRESH_LEEWAY_MS || !this.#hasRefreshSession()) return;
|
|
606
|
+
try {
|
|
607
|
+
await this.#refreshDeduplicated({
|
|
608
|
+
epoch: this.#authEpoch,
|
|
609
|
+
accessToken
|
|
610
|
+
});
|
|
611
|
+
} catch (error) {
|
|
612
|
+
this.#emitter.emit("authError", { error });
|
|
613
|
+
throw error;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
/** Перед каждой попыткой перечитывает внешний источник токена. */
|
|
485
617
|
async prepare() {
|
|
486
618
|
this.#assertActive();
|
|
487
|
-
await this
|
|
619
|
+
if ((await this.#loadSession())?.accessToken) return;
|
|
620
|
+
const auth = this.#config.auth;
|
|
621
|
+
if (typeof auth === "object" && auth !== null && "getToken" in auth) await this.#readExternalToken(() => auth.getToken());
|
|
488
622
|
}
|
|
489
623
|
/**
|
|
490
624
|
* Заголовки уже подготовленной авторизации без чтения storage или вызова внешнего источника.
|
|
@@ -599,6 +733,13 @@ var AuthManager = class {
|
|
|
599
733
|
}
|
|
600
734
|
/** Сохраняет токен, полученный извне, — например после подтверждения OTP. */
|
|
601
735
|
async setAccessToken(accessToken, expectedRevision = this.#authEpoch, cookies) {
|
|
736
|
+
return this.#storeAccessToken(accessToken, expectedRevision, cookies, false);
|
|
737
|
+
}
|
|
738
|
+
/** Атомарно принимает токен и cookie завершённого входа как новую сессию. @internal */
|
|
739
|
+
async commitAuthFlow(accessToken, cookies, expectedRevision = this.#authEpoch) {
|
|
740
|
+
return this.#storeAccessToken(accessToken, expectedRevision, cookies, true);
|
|
741
|
+
}
|
|
742
|
+
async #storeAccessToken(accessToken, expectedRevision, cookies, replaceSession) {
|
|
602
743
|
this.#assertActive();
|
|
603
744
|
if (typeof accessToken !== "string" || accessToken.trim() === "") throw new require_errors.ItdConfigError("accessToken должен быть непустой строкой");
|
|
604
745
|
await this.#loadSession();
|
|
@@ -608,7 +749,7 @@ var AuthManager = class {
|
|
|
608
749
|
this.#invalidateInFlight();
|
|
609
750
|
this.#transitionAuth(accessToken);
|
|
610
751
|
const saved = this.#saveSession({
|
|
611
|
-
...this.#session ?? {},
|
|
752
|
+
...replaceSession ? {} : this.#session ?? {},
|
|
612
753
|
accessToken,
|
|
613
754
|
obtainedAt: this.#config.clock.now()
|
|
614
755
|
});
|
|
@@ -631,7 +772,7 @@ var AuthManager = class {
|
|
|
631
772
|
async getUserId() {
|
|
632
773
|
this.#assertActive();
|
|
633
774
|
const session = await this.#loadSession();
|
|
634
|
-
return session?.accessToken ?
|
|
775
|
+
return session?.accessToken ? this.#identityForToken(session.accessToken).userId : void 0;
|
|
635
776
|
}
|
|
636
777
|
/** Заменяет сессию и связанные с ней cookie целиком. */
|
|
637
778
|
async setSession(session) {
|
|
@@ -734,24 +875,30 @@ var AuthManager = class {
|
|
|
734
875
|
this.#persistence = pending.catch(() => {});
|
|
735
876
|
return pending;
|
|
736
877
|
}
|
|
737
|
-
/**
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
const promise = this.#performRefresh().finally(() => {
|
|
745
|
-
this.#refreshing = null;
|
|
878
|
+
/** Дедуплицирует refresh одной ревизии; следующая ждёт завершения предыдущей. */
|
|
879
|
+
#refreshDeduplicated(expectation = { epoch: this.#authEpoch }) {
|
|
880
|
+
const current = this.#refreshing;
|
|
881
|
+
if (current) {
|
|
882
|
+
if (current.epoch === expectation.epoch) return current.promise;
|
|
883
|
+
return current.promise.then(() => this.#refreshDeduplicated(expectation), () => this.#refreshDeduplicated(expectation));
|
|
884
|
+
}
|
|
885
|
+
const promise = this.#performRefresh(expectation).finally(() => {
|
|
886
|
+
if (this.#refreshing?.promise === promise) this.#refreshing = null;
|
|
746
887
|
});
|
|
747
|
-
this.#refreshing =
|
|
888
|
+
this.#refreshing = {
|
|
889
|
+
epoch: expectation.epoch,
|
|
890
|
+
promise
|
|
891
|
+
};
|
|
748
892
|
return promise;
|
|
749
893
|
}
|
|
750
|
-
|
|
894
|
+
#matchesRefreshExpectation(expectation) {
|
|
895
|
+
return this.#authEpoch === expectation.epoch && (expectation.accessToken === void 0 || this.#session?.accessToken === expectation.accessToken);
|
|
896
|
+
}
|
|
897
|
+
async #performRefresh(expectation) {
|
|
751
898
|
this.#assertActive();
|
|
752
899
|
await this.#loadSession();
|
|
900
|
+
if (!this.#matchesRefreshExpectation(expectation)) return this.#currentAccessToken();
|
|
753
901
|
if (!this.#hasRefreshSession()) return this.#reloginOrNull();
|
|
754
|
-
const epoch = this.#authEpoch;
|
|
755
902
|
const flowCookies = this.#jar.clone();
|
|
756
903
|
try {
|
|
757
904
|
const accessToken = await this.#http.execute(AUTH_REFRESH, {
|
|
@@ -760,7 +907,7 @@ var AuthManager = class {
|
|
|
760
907
|
skipAuthRefresh: true,
|
|
761
908
|
cookieJar: flowCookies
|
|
762
909
|
});
|
|
763
|
-
if (this.#
|
|
910
|
+
if (!this.#matchesRefreshExpectation(expectation)) return this.#session?.accessToken ?? null;
|
|
764
911
|
this.#jar.replaceWith(flowCookies);
|
|
765
912
|
const rotated = this.#jar.getValue(require_redact.REFRESH_COOKIE, this.#config.baseUrl + require_redact.REFRESH_COOKIE_PATH);
|
|
766
913
|
this.#transitionAuth(accessToken, false);
|
|
@@ -772,10 +919,11 @@ var AuthManager = class {
|
|
|
772
919
|
});
|
|
773
920
|
this.#emitter.emit("tokens", { accessToken });
|
|
774
921
|
await saved;
|
|
922
|
+
if (this.#authEpoch !== expectation.epoch) return this.#currentAccessToken();
|
|
775
923
|
return accessToken;
|
|
776
924
|
} catch (error) {
|
|
925
|
+
if (this.#authEpoch !== expectation.epoch) return this.#currentAccessToken();
|
|
777
926
|
if (error instanceof require_errors.ItdAuthError) {
|
|
778
|
-
if (this.#authEpoch !== epoch) return this.#session?.accessToken ?? null;
|
|
779
927
|
this.#invalidateInFlight();
|
|
780
928
|
const clearedRevision = this.#authEpoch;
|
|
781
929
|
this.#transitionAuth(void 0);
|
|
@@ -816,29 +964,23 @@ var AuthManager = class {
|
|
|
816
964
|
this.#signingIn = promise;
|
|
817
965
|
return promise;
|
|
818
966
|
}
|
|
819
|
-
/**
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
if (credentials.getTurnstileToken) {
|
|
827
|
-
const token = await credentials.getTurnstileToken();
|
|
828
|
-
if (token) return token;
|
|
829
|
-
}
|
|
830
|
-
if (credentials.turnstileToken) return credentials.turnstileToken;
|
|
831
|
-
throw new require_errors.ItdConfigError("Вход по email и паролю требует токен капчи Cloudflare Turnstile: без него сервер отвечает 422. Передайте auth.getTurnstileToken (источник свежего токена) либо разовый auth.turnstileToken. Ключ виджета — TURNSTILE_SITE_KEY. В Node токен умеет добывать отдельный пакет: npm i @itd-api/turnstile, затем getTurnstileToken: createTurnstileSolver().");
|
|
967
|
+
/** Спрашивает у сервера активного провайдера и поле, в котором он ждёт токен. */
|
|
968
|
+
async #resolveCaptchaProvider() {
|
|
969
|
+
return readCaptchaProvider(await this.#http.execute(AUTH_CAPTCHA_PROVIDER, {
|
|
970
|
+
path: AUTH_PATHS.captchaProvider,
|
|
971
|
+
skipAuth: true,
|
|
972
|
+
skipAuthRefresh: true
|
|
973
|
+
}));
|
|
832
974
|
}
|
|
833
975
|
async #performSignIn(credentials, revision) {
|
|
834
|
-
const
|
|
976
|
+
const captcha = await resolveCaptchaBody(credentials.captcha, () => this.#resolveCaptchaProvider());
|
|
835
977
|
const flowCookies = new require_redact.CookieJar();
|
|
836
978
|
const result = await this.#http.execute(AUTH_SIGN_IN, {
|
|
837
979
|
path: AUTH_PATHS.signIn,
|
|
838
980
|
body: {
|
|
839
981
|
email: credentials.email,
|
|
840
982
|
password: credentials.password,
|
|
841
|
-
|
|
983
|
+
...captcha
|
|
842
984
|
},
|
|
843
985
|
skipAuth: true,
|
|
844
986
|
skipAuthRefresh: true,
|
|
@@ -862,6 +1004,125 @@ var AuthManager = class {
|
|
|
862
1004
|
}
|
|
863
1005
|
};
|
|
864
1006
|
//#endregion
|
|
1007
|
+
//#region src/session/qr-stream.ts
|
|
1008
|
+
const QR_STREAM_STATUSES = new Set(Object.values(QrLoginStreamStatus));
|
|
1009
|
+
function readQrLoginStreamEvent(data) {
|
|
1010
|
+
let value;
|
|
1011
|
+
try {
|
|
1012
|
+
value = JSON.parse(data);
|
|
1013
|
+
} catch {
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
1016
|
+
if (!require_url.isRecord$1(value)) return void 0;
|
|
1017
|
+
const event = value;
|
|
1018
|
+
if (typeof event.status !== "string" || !QR_STREAM_STATUSES.has(event.status)) return;
|
|
1019
|
+
return {
|
|
1020
|
+
status: event.status,
|
|
1021
|
+
...typeof event.expiresIn === "number" && Number.isFinite(event.expiresIn) ? { expiresIn: event.expiresIn } : {}
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
/** Читает короткоживущий SSE-поток QR-входа и последовательно доставляет его события. */
|
|
1025
|
+
async function consumeQrLoginStream(body, onEvent, signal) {
|
|
1026
|
+
const reader = body.getReader();
|
|
1027
|
+
const decoder = new TextDecoder();
|
|
1028
|
+
let delivery = Promise.resolve();
|
|
1029
|
+
let completed = false;
|
|
1030
|
+
const cancel = () => {
|
|
1031
|
+
reader.cancel(signal?.reason).catch(() => {});
|
|
1032
|
+
};
|
|
1033
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
1034
|
+
const parser = require_events.createParser({ onEvent(message) {
|
|
1035
|
+
const event = readQrLoginStreamEvent(message.data);
|
|
1036
|
+
if (!event) return;
|
|
1037
|
+
delivery = delivery.then(() => onEvent(event));
|
|
1038
|
+
} });
|
|
1039
|
+
try {
|
|
1040
|
+
for (;;) {
|
|
1041
|
+
const { done, value } = await reader.read();
|
|
1042
|
+
if (done) break;
|
|
1043
|
+
parser.feed(decoder.decode(value, { stream: true }));
|
|
1044
|
+
await delivery;
|
|
1045
|
+
}
|
|
1046
|
+
parser.feed(decoder.decode());
|
|
1047
|
+
await delivery;
|
|
1048
|
+
if (signal?.aborted) throw signal.reason;
|
|
1049
|
+
completed = true;
|
|
1050
|
+
} finally {
|
|
1051
|
+
signal?.removeEventListener("abort", cancel);
|
|
1052
|
+
if (!completed) await reader.cancel().catch(() => {});
|
|
1053
|
+
reader.releaseLock?.();
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/** Открывает изолированный HTTP/SSE-канал QR-входа. @internal */
|
|
1057
|
+
async function openQrLoginStream(connection, cookies, input, onEvent, signal) {
|
|
1058
|
+
connection.assertActive?.("открыть QR-поток");
|
|
1059
|
+
const path = AUTH_PATHS.qrStream;
|
|
1060
|
+
const method = "POST";
|
|
1061
|
+
const url = require_redact.joinUrl(connection.baseUrl, path);
|
|
1062
|
+
const abort = require_url.createRequestAbortScope(signal, connection.signal, 0, connection.clock);
|
|
1063
|
+
try {
|
|
1064
|
+
const headers = await connection.baseHeaders(url);
|
|
1065
|
+
headers.delete("Authorization");
|
|
1066
|
+
headers.delete("Cookie");
|
|
1067
|
+
headers.set("Accept", "text/event-stream");
|
|
1068
|
+
headers.set("Content-Type", "application/json");
|
|
1069
|
+
const cookie = cookies.getHeader(url);
|
|
1070
|
+
if (cookie) headers.set("Cookie", cookie);
|
|
1071
|
+
let response;
|
|
1072
|
+
try {
|
|
1073
|
+
response = await connection.fetch(url, {
|
|
1074
|
+
method,
|
|
1075
|
+
headers,
|
|
1076
|
+
body: JSON.stringify(input),
|
|
1077
|
+
credentials: "include",
|
|
1078
|
+
signal: abort.signal
|
|
1079
|
+
});
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
const failure = new require_errors.ItdNetworkError(`Не удалось открыть QR-поток: ${String(error)}`, {
|
|
1082
|
+
method,
|
|
1083
|
+
path,
|
|
1084
|
+
cause: error
|
|
1085
|
+
});
|
|
1086
|
+
throw require_url.requestAbortError(abort, {
|
|
1087
|
+
timeout: 0,
|
|
1088
|
+
method,
|
|
1089
|
+
path
|
|
1090
|
+
}, failure);
|
|
1091
|
+
}
|
|
1092
|
+
cookies.setFromResponse(response.url || url, response);
|
|
1093
|
+
if (!response.ok) {
|
|
1094
|
+
const text = await response.text();
|
|
1095
|
+
let body = text;
|
|
1096
|
+
try {
|
|
1097
|
+
body = JSON.parse(text);
|
|
1098
|
+
} catch {}
|
|
1099
|
+
throw require_url.createApiError({
|
|
1100
|
+
method,
|
|
1101
|
+
path,
|
|
1102
|
+
status: response.status,
|
|
1103
|
+
statusText: response.statusText,
|
|
1104
|
+
headers: response.headers,
|
|
1105
|
+
response,
|
|
1106
|
+
body,
|
|
1107
|
+
now: connection.clock.now()
|
|
1108
|
+
});
|
|
1109
|
+
}
|
|
1110
|
+
if (!response.body) throw new require_errors.ItdNetworkError("Сервер не вернул тело QR-потока", {
|
|
1111
|
+
method,
|
|
1112
|
+
path
|
|
1113
|
+
});
|
|
1114
|
+
await consumeQrLoginStream(response.body, onEvent, abort.signal);
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
throw require_url.requestAbortError(abort, {
|
|
1117
|
+
timeout: 0,
|
|
1118
|
+
method,
|
|
1119
|
+
path
|
|
1120
|
+
}, error);
|
|
1121
|
+
} finally {
|
|
1122
|
+
abort.cleanup();
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
//#endregion
|
|
865
1126
|
//#region src/resources/auth.ts
|
|
866
1127
|
/**
|
|
867
1128
|
* Результат входа.
|
|
@@ -876,9 +1137,35 @@ var AuthManager = class {
|
|
|
876
1137
|
*/
|
|
877
1138
|
var AuthResource = class extends require_render.BaseResource {
|
|
878
1139
|
#auth;
|
|
1140
|
+
#connection;
|
|
1141
|
+
#qrFlows = /* @__PURE__ */ new Map();
|
|
879
1142
|
constructor(http, deps) {
|
|
880
1143
|
super(http);
|
|
881
1144
|
this.#auth = deps.auth;
|
|
1145
|
+
this.#connection = deps.connection;
|
|
1146
|
+
}
|
|
1147
|
+
#pruneQrFlows() {
|
|
1148
|
+
const now = this.#connection.clock.now();
|
|
1149
|
+
for (const [qrId, flow] of this.#qrFlows) if (flow.expiresAt !== void 0 && flow.expiresAt <= now) this.#qrFlows.delete(qrId);
|
|
1150
|
+
}
|
|
1151
|
+
#qrFlow(qrId) {
|
|
1152
|
+
this.#pruneQrFlows();
|
|
1153
|
+
const existing = this.#qrFlows.get(qrId);
|
|
1154
|
+
if (existing) return existing;
|
|
1155
|
+
const flow = {
|
|
1156
|
+
cookies: this.#auth.createCookieFlow(false),
|
|
1157
|
+
expiresAt: void 0
|
|
1158
|
+
};
|
|
1159
|
+
this.#qrFlows.set(qrId, flow);
|
|
1160
|
+
return flow;
|
|
1161
|
+
}
|
|
1162
|
+
#touchQrFlow(flow, expiresIn) {
|
|
1163
|
+
if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn)) return;
|
|
1164
|
+
const expiresAt = this.#connection.clock.now() + Math.max(0, expiresIn) * 1e3;
|
|
1165
|
+
flow.expiresAt = flow.expiresAt === void 0 ? expiresAt : Math.min(flow.expiresAt, expiresAt);
|
|
1166
|
+
}
|
|
1167
|
+
#forgetTerminalQrError(qrId, error) {
|
|
1168
|
+
if (require_errors.isItdApiError(error) && error.hasCode("QR_EXPIRED", "QR_ALREADY_USED", "QR_TOKEN_MISMATCH")) this.#qrFlows.delete(qrId);
|
|
882
1169
|
}
|
|
883
1170
|
/**
|
|
884
1171
|
* Проверяет состояние авторизации и возвращает текущего пользователя.
|
|
@@ -892,14 +1179,106 @@ var AuthResource = class extends require_render.BaseResource {
|
|
|
892
1179
|
});
|
|
893
1180
|
}
|
|
894
1181
|
/**
|
|
1182
|
+
* Узнаёт активного провайдера капчи и поле токена.
|
|
1183
|
+
*
|
|
1184
|
+
* Сервер может переключить провайдера или переименовать поле без выпуска новой версии SDK.
|
|
1185
|
+
* Автоматическому входу этот вызов не нужен — клиент делает его сам.
|
|
1186
|
+
*/
|
|
1187
|
+
captchaProvider(options = {}) {
|
|
1188
|
+
return this.http.execute(AUTH_CAPTCHA_PROVIDER, {
|
|
1189
|
+
path: AUTH_PATHS.captchaProvider,
|
|
1190
|
+
skipAuth: true,
|
|
1191
|
+
skipAuthRefresh: true,
|
|
1192
|
+
...options
|
|
1193
|
+
});
|
|
1194
|
+
}
|
|
1195
|
+
/** Создаёт короткоживущую сессию QR-входа. */
|
|
1196
|
+
async startQrLogin(options = {}) {
|
|
1197
|
+
const cookies = this.#auth.createCookieFlow(false);
|
|
1198
|
+
const result = await this.http.execute(AUTH_QR_START, {
|
|
1199
|
+
path: AUTH_PATHS.qrStart,
|
|
1200
|
+
skipAuth: true,
|
|
1201
|
+
skipAuthRefresh: true,
|
|
1202
|
+
cookieJar: cookies,
|
|
1203
|
+
...options
|
|
1204
|
+
});
|
|
1205
|
+
if (typeof result.qrId === "string" && result.qrId !== "") {
|
|
1206
|
+
const flow = {
|
|
1207
|
+
cookies,
|
|
1208
|
+
expiresAt: void 0
|
|
1209
|
+
};
|
|
1210
|
+
this.#touchQrFlow(flow, result.expiresIn);
|
|
1211
|
+
this.#qrFlows.set(result.qrId, flow);
|
|
1212
|
+
}
|
|
1213
|
+
return result;
|
|
1214
|
+
}
|
|
1215
|
+
/**
|
|
1216
|
+
* Проверяет QR-сессию и завершает вход после подтверждения на другом устройстве.
|
|
1217
|
+
*
|
|
1218
|
+
* До подтверждения возвращает промежуточный статус. Если сервер потребовал капчу,
|
|
1219
|
+
* повторите вызов с `captcha` — тип берётся из {@link captchaProvider}.
|
|
1220
|
+
* Полученный `accessToken` автоматически сохраняется в клиенте.
|
|
1221
|
+
*/
|
|
1222
|
+
async claimQrLogin(input, options = {}) {
|
|
1223
|
+
const revision = this.#auth.revision();
|
|
1224
|
+
const flow = this.#qrFlow(input.qrId);
|
|
1225
|
+
try {
|
|
1226
|
+
const { captcha, ...rest } = input;
|
|
1227
|
+
const result = await this.http.execute(AUTH_QR_CLAIM, {
|
|
1228
|
+
path: AUTH_PATHS.qrClaim,
|
|
1229
|
+
body: {
|
|
1230
|
+
...rest,
|
|
1231
|
+
...captcha ? captchaBody(captcha) : {}
|
|
1232
|
+
},
|
|
1233
|
+
skipAuth: true,
|
|
1234
|
+
skipAuthRefresh: true,
|
|
1235
|
+
cookieJar: flow.cookies,
|
|
1236
|
+
...options
|
|
1237
|
+
});
|
|
1238
|
+
this.#touchQrFlow(flow, result.expiresIn);
|
|
1239
|
+
if (result.status === QrLoginStatus.Authorized) {
|
|
1240
|
+
await this.#auth.commitAuthFlow(result.accessToken, flow.cookies, revision);
|
|
1241
|
+
this.#qrFlows.delete(input.qrId);
|
|
1242
|
+
} else if (result.status === QrLoginStatus.Rejected) this.#qrFlows.delete(input.qrId);
|
|
1243
|
+
return result;
|
|
1244
|
+
} catch (error) {
|
|
1245
|
+
this.#forgetTerminalQrError(input.qrId, error);
|
|
1246
|
+
throw error;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
/**
|
|
1250
|
+
* Слушает состояния QR-входа до закрытия ответа или отмены `signal`.
|
|
1251
|
+
*
|
|
1252
|
+
* Событие `approved` означает, что подтверждение завершено: после него вызовите
|
|
1253
|
+
* {@link claimQrLogin}, чтобы получить и сохранить access token.
|
|
1254
|
+
*/
|
|
1255
|
+
async streamQrLogin(input, onEvent, options = {}) {
|
|
1256
|
+
if (typeof onEvent !== "function") throw new require_errors.ItdConfigError("onEvent QR-потока должен быть функцией");
|
|
1257
|
+
const flow = this.#qrFlow(input.qrId);
|
|
1258
|
+
try {
|
|
1259
|
+
await openQrLoginStream(this.#connection, flow.cookies, input, async (event) => {
|
|
1260
|
+
this.#touchQrFlow(flow, event.expiresIn);
|
|
1261
|
+
if (event.status === QrLoginStreamStatus.Rejected) this.#qrFlows.delete(input.qrId);
|
|
1262
|
+
await onEvent(event);
|
|
1263
|
+
}, options.signal);
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
this.#forgetTerminalQrError(input.qrId, error);
|
|
1266
|
+
throw error;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
/**
|
|
895
1270
|
* Регистрирует аккаунт и запускает подтверждение по коду.
|
|
896
1271
|
*
|
|
897
1272
|
* @returns `flowToken`, который нужно передать в {@link verifyOtp}
|
|
898
1273
|
*/
|
|
899
1274
|
signUp(credentials, options = {}) {
|
|
1275
|
+
const { captcha, ...rest } = credentials;
|
|
900
1276
|
return this.http.execute(AUTH_SIGN_UP, {
|
|
901
1277
|
path: AUTH_PATHS.signUp,
|
|
902
|
-
body:
|
|
1278
|
+
body: {
|
|
1279
|
+
...rest,
|
|
1280
|
+
...captchaBody(captcha)
|
|
1281
|
+
},
|
|
903
1282
|
skipAuth: true,
|
|
904
1283
|
skipAuthRefresh: true,
|
|
905
1284
|
...options
|
|
@@ -916,17 +1295,21 @@ var AuthResource = class extends require_render.BaseResource {
|
|
|
916
1295
|
* @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
|
|
917
1296
|
*/
|
|
918
1297
|
async signIn(credentials, options = {}) {
|
|
1298
|
+
const { captcha, ...rest } = credentials;
|
|
919
1299
|
const revision = this.#auth.revision();
|
|
920
1300
|
const cookies = this.#auth.createCookieFlow(false);
|
|
921
1301
|
const result = await this.http.execute(AUTH_SIGN_IN, {
|
|
922
1302
|
path: AUTH_PATHS.signIn,
|
|
923
|
-
body:
|
|
1303
|
+
body: {
|
|
1304
|
+
...rest,
|
|
1305
|
+
...captchaBody(captcha)
|
|
1306
|
+
},
|
|
924
1307
|
skipAuth: true,
|
|
925
1308
|
skipAuthRefresh: true,
|
|
926
1309
|
cookieJar: cookies,
|
|
927
1310
|
...options
|
|
928
1311
|
});
|
|
929
|
-
if (result.status === SignInStatus.Authenticated) await this.#auth.
|
|
1312
|
+
if (result.status === SignInStatus.Authenticated) await this.#auth.commitAuthFlow(result.accessToken, cookies, revision);
|
|
930
1313
|
else this.#auth.commitCookieFlow(cookies, revision);
|
|
931
1314
|
return result;
|
|
932
1315
|
}
|
|
@@ -946,7 +1329,7 @@ var AuthResource = class extends require_render.BaseResource {
|
|
|
946
1329
|
cookieJar: cookies,
|
|
947
1330
|
...options
|
|
948
1331
|
});
|
|
949
|
-
await this.#auth.
|
|
1332
|
+
await this.#auth.commitAuthFlow(accessToken, cookies, revision);
|
|
950
1333
|
return accessToken;
|
|
951
1334
|
}
|
|
952
1335
|
/** Отправляет код подтверждения повторно. */
|
|
@@ -1051,7 +1434,10 @@ var AuthResource = class extends require_render.BaseResource {
|
|
|
1051
1434
|
forgotPassword(input, options = {}) {
|
|
1052
1435
|
return this.http.execute(AUTH_FORGOT_PASSWORD, {
|
|
1053
1436
|
path: AUTH_PATHS.forgotPassword,
|
|
1054
|
-
body:
|
|
1437
|
+
body: {
|
|
1438
|
+
email: input.email,
|
|
1439
|
+
...captchaBody(input.captcha)
|
|
1440
|
+
},
|
|
1055
1441
|
skipAuth: true,
|
|
1056
1442
|
skipAuthRefresh: true,
|
|
1057
1443
|
...options
|
|
@@ -1082,23 +1468,21 @@ var AuthResource = class extends require_render.BaseResource {
|
|
|
1082
1468
|
* ```ts
|
|
1083
1469
|
* await itd.auth.resetPasswordWithOtp({
|
|
1084
1470
|
* email,
|
|
1085
|
-
*
|
|
1471
|
+
* captcha: { type: provider, token, field },
|
|
1086
1472
|
* newPassword,
|
|
1087
1473
|
* getOtp: () => rl.question('Код из письма: '),
|
|
1088
1474
|
* });
|
|
1089
1475
|
* ```
|
|
1090
1476
|
*/
|
|
1091
1477
|
async resetPasswordWithOtp(input, options = {}) {
|
|
1092
|
-
const
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
}, options);
|
|
1096
|
-
const otp = await input.getOtp();
|
|
1478
|
+
const { getOtp, newPassword, ...request } = input;
|
|
1479
|
+
const flowToken = await this.forgotPassword(request, options);
|
|
1480
|
+
const otp = await getOtp();
|
|
1097
1481
|
await this.resetPassword({
|
|
1098
1482
|
email: input.email,
|
|
1099
1483
|
otp,
|
|
1100
1484
|
flowToken,
|
|
1101
|
-
newPassword
|
|
1485
|
+
newPassword
|
|
1102
1486
|
}, options);
|
|
1103
1487
|
}
|
|
1104
1488
|
/**
|
|
@@ -1187,8 +1571,8 @@ function createManagedClient(options, internals) {
|
|
|
1187
1571
|
* const itd = new ItdClient({
|
|
1188
1572
|
* // `auth` не обязателен: когда хранилище уже содержит сессию, токен берётся оттуда,
|
|
1189
1573
|
* // а истёкший продлевается сам. Здесь он нужен на первый запуск.
|
|
1190
|
-
* // Вход по паролю требует токена капчи — см.
|
|
1191
|
-
* auth: { email, password,
|
|
1574
|
+
* // Вход по паролю требует токена капчи — см. CaptchaOptions.
|
|
1575
|
+
* auth: { email, password, captcha: createCaptchaSolver() },
|
|
1192
1576
|
* storage: new FileTokenStorage('./.itd-session.json'),
|
|
1193
1577
|
* rateLimit: { concurrency: 4, rps: 8 },
|
|
1194
1578
|
* });
|
|
@@ -1209,7 +1593,10 @@ var ItdClient = class ItdClient {
|
|
|
1209
1593
|
#auth;
|
|
1210
1594
|
/** Авторизация, сессии и пароли. */
|
|
1211
1595
|
get auth() {
|
|
1212
|
-
this.#auth ??= new AuthResource(this.#runtime.http, {
|
|
1596
|
+
this.#auth ??= new AuthResource(this.#runtime.http, {
|
|
1597
|
+
auth: this.#runtime.auth,
|
|
1598
|
+
connection: this.#runtime.connection()
|
|
1599
|
+
});
|
|
1213
1600
|
return this.#auth;
|
|
1214
1601
|
}
|
|
1215
1602
|
/** Профили, подписки, блокировки, приватность. */
|
|
@@ -1706,7 +2093,9 @@ function assertAccountsActive(accounts, action) {
|
|
|
1706
2093
|
* await accounts.restore();
|
|
1707
2094
|
*
|
|
1708
2095
|
* if (!accounts.has('kiow')) {
|
|
1709
|
-
* accounts.addAccount('kiow', {
|
|
2096
|
+
* accounts.addAccount('kiow', {
|
|
2097
|
+
* auth: { email, password, captcha: createCaptchaSolver() },
|
|
2098
|
+
* });
|
|
1710
2099
|
* }
|
|
1711
2100
|
*
|
|
1712
2101
|
* await accounts.account('kiow').posts.create({ content: 'привет' });
|
|
@@ -1785,7 +2174,9 @@ var ItdAccounts = class ItdAccounts {
|
|
|
1785
2174
|
*
|
|
1786
2175
|
* @example
|
|
1787
2176
|
* ```ts
|
|
1788
|
-
* accounts.addAccount('bot', {
|
|
2177
|
+
* accounts.addAccount('bot', {
|
|
2178
|
+
* auth: { email, password, captcha: createCaptchaSolver() },
|
|
2179
|
+
* });
|
|
1789
2180
|
* accounts.addAccount('reader', { auth: '<accessToken>' });
|
|
1790
2181
|
* accounts.addAccount('через-прокси', { fetch: proxyFetch('socks5://…') });
|
|
1791
2182
|
* ```
|
|
@@ -2123,6 +2514,10 @@ exports.AUDIO_MIME_TYPES = require_render.AUDIO_MIME_TYPES;
|
|
|
2123
2514
|
exports.AccessType = require_url.AccessType;
|
|
2124
2515
|
exports.AttachmentType = require_url.AttachmentType;
|
|
2125
2516
|
exports.BUCKET_LIMITS = require_url.BUCKET_LIMITS;
|
|
2517
|
+
exports.CAPTCHA_FIELDS = CAPTCHA_FIELDS;
|
|
2518
|
+
exports.CaptchaChoice = require_url.CaptchaChoice;
|
|
2519
|
+
exports.CaptchaField = require_url.CaptchaField;
|
|
2520
|
+
exports.CaptchaType = require_url.CaptchaType;
|
|
2126
2521
|
exports.CommentSort = require_url.CommentSort;
|
|
2127
2522
|
exports.DEFAULT_BASE_URL = require_url.DEFAULT_BASE_URL;
|
|
2128
2523
|
exports.DEFAULT_RATE_LIMIT_BUCKET = require_url.DEFAULT_RATE_LIMIT_BUCKET;
|
|
@@ -2172,6 +2567,8 @@ exports.NotificationUpdateType = require_events.NotificationUpdateType;
|
|
|
2172
2567
|
exports.OPERATIONS = require_url.OPERATIONS;
|
|
2173
2568
|
exports.PaginationMode = require_render.PaginationMode;
|
|
2174
2569
|
exports.Paginator = require_render.Paginator;
|
|
2570
|
+
exports.QrLoginStatus = QrLoginStatus;
|
|
2571
|
+
exports.QrLoginStreamStatus = QrLoginStreamStatus;
|
|
2175
2572
|
exports.RateLimitPacing = require_url.RateLimitPacing;
|
|
2176
2573
|
exports.ReportReason = require_url.ReportReason;
|
|
2177
2574
|
exports.ReportTargetType = require_url.ReportTargetType;
|