itd-api 0.9.3 → 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 +405 -42
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +181 -29
- package/dist/index.d.ts +181 -29
- package/dist/index.js +401 -44
- 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-BzjI6pn5.js → events-B0Wtn4cF.js} +3 -3
- package/dist/shared/{events-BzjI6pn5.js.map → events-B0Wtn4cF.js.map} +1 -1
- package/dist/shared/{events-C1FudmSe.cjs → events-BB791aPk.cjs} +8 -2
- package/dist/shared/{events-C1FudmSe.cjs.map → events-BB791aPk.cjs.map} +1 -1
- package/dist/shared/{events-BPxBF0gQ.d.cts → events-BNGjJVI3.d.cts} +2 -2
- package/dist/shared/{events-DnwfcLxS.d.ts → events-CJO6q93Z.d.ts} +2 -2
- package/dist/shared/{render-Omy1drH1.cjs → render-BimHjOhw.cjs} +2 -2
- package/dist/shared/{render-Omy1drH1.cjs.map → render-BimHjOhw.cjs.map} +1 -1
- package/dist/shared/{render-C1F_QWYD.d.cts → render-C1A57W-5.d.cts} +2 -2
- package/dist/shared/{render-CtOpp9Pm.js → render-CkhvH_yh.js} +2 -2
- package/dist/shared/{render-CtOpp9Pm.js.map → render-CkhvH_yh.js.map} +1 -1
- package/dist/shared/{render-uFW2kDxp.d.ts → render-DC5nvqcP.d.ts} +2 -2
- package/dist/shared/{url-3Ts6EEHQ.js → url-BA5hEwj2.js} +65 -3
- package/dist/shared/url-BA5hEwj2.js.map +1 -0
- package/dist/shared/{url-D6wsXm1T.cjs → url-CDVgMIq5.cjs} +100 -2
- package/dist/shared/url-CDVgMIq5.cjs.map +1 -0
- package/dist/shared/{url-DubjkRR9.d.ts → url-DkMueFAT.d.cts} +70 -3
- package/dist/shared/{url-DubjkRR9.d.cts → url-DkMueFAT.d.ts} +70 -3
- package/package.json +2 -1
- package/dist/shared/url-3Ts6EEHQ.js.map +0 -1
- package/dist/shared/url-D6wsXm1T.cjs.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
|
/**
|
|
@@ -131,11 +234,10 @@ function validateAuth(auth) {
|
|
|
131
234
|
return { ...auth };
|
|
132
235
|
}
|
|
133
236
|
if ("email" in auth || "password" in auth) {
|
|
134
|
-
const { email, password,
|
|
237
|
+
const { email, password, captcha } = auth;
|
|
135
238
|
if (typeof email !== "string" || email.trim() === "") throw new require_errors.ItdConfigError("auth.email должен быть непустой строкой");
|
|
136
239
|
if (typeof password !== "string" || password === "") throw new require_errors.ItdConfigError("auth.password должен быть непустой строкой");
|
|
137
|
-
|
|
138
|
-
if (turnstileToken !== void 0 && (typeof turnstileToken !== "string" || turnstileToken.trim() === "")) throw new require_errors.ItdConfigError("auth.turnstileToken должен быть непустой строкой");
|
|
240
|
+
validateCaptchaOptions(captcha);
|
|
139
241
|
return { ...auth };
|
|
140
242
|
}
|
|
141
243
|
throw new require_errors.ItdConfigError("auth не распознан. Ожидается строка с accessToken либо объект { accessToken }, { email, password } или { getToken }");
|
|
@@ -182,6 +284,7 @@ function createItdAuth(options, deps) {
|
|
|
182
284
|
}
|
|
183
285
|
/** Пути эндпоинтов авторизации. */
|
|
184
286
|
const AUTH_PATHS = {
|
|
287
|
+
captchaProvider: "/api/v1/auth/captcha/provider",
|
|
185
288
|
signUp: "/api/v1/auth/sign-up",
|
|
186
289
|
signIn: "/api/v1/auth/sign-in",
|
|
187
290
|
verifyOtp: "/api/v1/auth/verify-otp",
|
|
@@ -191,7 +294,10 @@ const AUTH_PATHS = {
|
|
|
191
294
|
forgotPassword: "/api/v1/auth/forgot-password",
|
|
192
295
|
resetPassword: "/api/v1/auth/reset-password",
|
|
193
296
|
changePassword: "/api/v1/auth/change-password",
|
|
194
|
-
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"
|
|
195
301
|
};
|
|
196
302
|
/**
|
|
197
303
|
* Публичный ключ Cloudflare Turnstile платформы итд.com.
|
|
@@ -201,14 +307,15 @@ const AUTH_PATHS = {
|
|
|
201
307
|
*
|
|
202
308
|
* Ключ привязан к домену: на чужом origin Cloudflare отказывает виджету с кодом `110200`.
|
|
203
309
|
* Поэтому отрисовать его может только код, выполняемый на самом итд.com. Остальным
|
|
204
|
-
* подходит `@itd-api/
|
|
310
|
+
* подходит `@itd-api/captcha`, готовый токен из другого источника или вовсе вход
|
|
205
311
|
* без капчи — по сохранённой сессии либо по токенам, взятым в браузере.
|
|
206
312
|
*
|
|
207
313
|
* @example
|
|
208
314
|
* ```ts
|
|
209
315
|
* turnstile.render('#captcha', {
|
|
210
316
|
* sitekey: TURNSTILE_SITE_KEY,
|
|
211
|
-
* callback: (
|
|
317
|
+
* callback: (token) =>
|
|
318
|
+
* itd.auth.signIn({ email, password, captcha: { type: CaptchaType.Cloudflare, token } }),
|
|
212
319
|
* });
|
|
213
320
|
* ```
|
|
214
321
|
*/
|
|
@@ -626,6 +733,13 @@ var AuthManager = class {
|
|
|
626
733
|
}
|
|
627
734
|
/** Сохраняет токен, полученный извне, — например после подтверждения OTP. */
|
|
628
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) {
|
|
629
743
|
this.#assertActive();
|
|
630
744
|
if (typeof accessToken !== "string" || accessToken.trim() === "") throw new require_errors.ItdConfigError("accessToken должен быть непустой строкой");
|
|
631
745
|
await this.#loadSession();
|
|
@@ -635,7 +749,7 @@ var AuthManager = class {
|
|
|
635
749
|
this.#invalidateInFlight();
|
|
636
750
|
this.#transitionAuth(accessToken);
|
|
637
751
|
const saved = this.#saveSession({
|
|
638
|
-
...this.#session ?? {},
|
|
752
|
+
...replaceSession ? {} : this.#session ?? {},
|
|
639
753
|
accessToken,
|
|
640
754
|
obtainedAt: this.#config.clock.now()
|
|
641
755
|
});
|
|
@@ -850,29 +964,23 @@ var AuthManager = class {
|
|
|
850
964
|
this.#signingIn = promise;
|
|
851
965
|
return promise;
|
|
852
966
|
}
|
|
853
|
-
/**
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
if (credentials.getTurnstileToken) {
|
|
861
|
-
const token = await credentials.getTurnstileToken();
|
|
862
|
-
if (token) return token;
|
|
863
|
-
}
|
|
864
|
-
if (credentials.turnstileToken) return credentials.turnstileToken;
|
|
865
|
-
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
|
+
}));
|
|
866
974
|
}
|
|
867
975
|
async #performSignIn(credentials, revision) {
|
|
868
|
-
const
|
|
976
|
+
const captcha = await resolveCaptchaBody(credentials.captcha, () => this.#resolveCaptchaProvider());
|
|
869
977
|
const flowCookies = new require_redact.CookieJar();
|
|
870
978
|
const result = await this.#http.execute(AUTH_SIGN_IN, {
|
|
871
979
|
path: AUTH_PATHS.signIn,
|
|
872
980
|
body: {
|
|
873
981
|
email: credentials.email,
|
|
874
982
|
password: credentials.password,
|
|
875
|
-
|
|
983
|
+
...captcha
|
|
876
984
|
},
|
|
877
985
|
skipAuth: true,
|
|
878
986
|
skipAuthRefresh: true,
|
|
@@ -896,6 +1004,125 @@ var AuthManager = class {
|
|
|
896
1004
|
}
|
|
897
1005
|
};
|
|
898
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
|
|
899
1126
|
//#region src/resources/auth.ts
|
|
900
1127
|
/**
|
|
901
1128
|
* Результат входа.
|
|
@@ -910,9 +1137,35 @@ var AuthManager = class {
|
|
|
910
1137
|
*/
|
|
911
1138
|
var AuthResource = class extends require_render.BaseResource {
|
|
912
1139
|
#auth;
|
|
1140
|
+
#connection;
|
|
1141
|
+
#qrFlows = /* @__PURE__ */ new Map();
|
|
913
1142
|
constructor(http, deps) {
|
|
914
1143
|
super(http);
|
|
915
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);
|
|
916
1169
|
}
|
|
917
1170
|
/**
|
|
918
1171
|
* Проверяет состояние авторизации и возвращает текущего пользователя.
|
|
@@ -926,14 +1179,106 @@ var AuthResource = class extends require_render.BaseResource {
|
|
|
926
1179
|
});
|
|
927
1180
|
}
|
|
928
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
|
+
/**
|
|
929
1270
|
* Регистрирует аккаунт и запускает подтверждение по коду.
|
|
930
1271
|
*
|
|
931
1272
|
* @returns `flowToken`, который нужно передать в {@link verifyOtp}
|
|
932
1273
|
*/
|
|
933
1274
|
signUp(credentials, options = {}) {
|
|
1275
|
+
const { captcha, ...rest } = credentials;
|
|
934
1276
|
return this.http.execute(AUTH_SIGN_UP, {
|
|
935
1277
|
path: AUTH_PATHS.signUp,
|
|
936
|
-
body:
|
|
1278
|
+
body: {
|
|
1279
|
+
...rest,
|
|
1280
|
+
...captchaBody(captcha)
|
|
1281
|
+
},
|
|
937
1282
|
skipAuth: true,
|
|
938
1283
|
skipAuthRefresh: true,
|
|
939
1284
|
...options
|
|
@@ -950,17 +1295,21 @@ var AuthResource = class extends require_render.BaseResource {
|
|
|
950
1295
|
* @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
|
|
951
1296
|
*/
|
|
952
1297
|
async signIn(credentials, options = {}) {
|
|
1298
|
+
const { captcha, ...rest } = credentials;
|
|
953
1299
|
const revision = this.#auth.revision();
|
|
954
1300
|
const cookies = this.#auth.createCookieFlow(false);
|
|
955
1301
|
const result = await this.http.execute(AUTH_SIGN_IN, {
|
|
956
1302
|
path: AUTH_PATHS.signIn,
|
|
957
|
-
body:
|
|
1303
|
+
body: {
|
|
1304
|
+
...rest,
|
|
1305
|
+
...captchaBody(captcha)
|
|
1306
|
+
},
|
|
958
1307
|
skipAuth: true,
|
|
959
1308
|
skipAuthRefresh: true,
|
|
960
1309
|
cookieJar: cookies,
|
|
961
1310
|
...options
|
|
962
1311
|
});
|
|
963
|
-
if (result.status === SignInStatus.Authenticated) await this.#auth.
|
|
1312
|
+
if (result.status === SignInStatus.Authenticated) await this.#auth.commitAuthFlow(result.accessToken, cookies, revision);
|
|
964
1313
|
else this.#auth.commitCookieFlow(cookies, revision);
|
|
965
1314
|
return result;
|
|
966
1315
|
}
|
|
@@ -980,7 +1329,7 @@ var AuthResource = class extends require_render.BaseResource {
|
|
|
980
1329
|
cookieJar: cookies,
|
|
981
1330
|
...options
|
|
982
1331
|
});
|
|
983
|
-
await this.#auth.
|
|
1332
|
+
await this.#auth.commitAuthFlow(accessToken, cookies, revision);
|
|
984
1333
|
return accessToken;
|
|
985
1334
|
}
|
|
986
1335
|
/** Отправляет код подтверждения повторно. */
|
|
@@ -1085,7 +1434,10 @@ var AuthResource = class extends require_render.BaseResource {
|
|
|
1085
1434
|
forgotPassword(input, options = {}) {
|
|
1086
1435
|
return this.http.execute(AUTH_FORGOT_PASSWORD, {
|
|
1087
1436
|
path: AUTH_PATHS.forgotPassword,
|
|
1088
|
-
body:
|
|
1437
|
+
body: {
|
|
1438
|
+
email: input.email,
|
|
1439
|
+
...captchaBody(input.captcha)
|
|
1440
|
+
},
|
|
1089
1441
|
skipAuth: true,
|
|
1090
1442
|
skipAuthRefresh: true,
|
|
1091
1443
|
...options
|
|
@@ -1116,23 +1468,21 @@ var AuthResource = class extends require_render.BaseResource {
|
|
|
1116
1468
|
* ```ts
|
|
1117
1469
|
* await itd.auth.resetPasswordWithOtp({
|
|
1118
1470
|
* email,
|
|
1119
|
-
*
|
|
1471
|
+
* captcha: { type: provider, token, field },
|
|
1120
1472
|
* newPassword,
|
|
1121
1473
|
* getOtp: () => rl.question('Код из письма: '),
|
|
1122
1474
|
* });
|
|
1123
1475
|
* ```
|
|
1124
1476
|
*/
|
|
1125
1477
|
async resetPasswordWithOtp(input, options = {}) {
|
|
1126
|
-
const
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
}, options);
|
|
1130
|
-
const otp = await input.getOtp();
|
|
1478
|
+
const { getOtp, newPassword, ...request } = input;
|
|
1479
|
+
const flowToken = await this.forgotPassword(request, options);
|
|
1480
|
+
const otp = await getOtp();
|
|
1131
1481
|
await this.resetPassword({
|
|
1132
1482
|
email: input.email,
|
|
1133
1483
|
otp,
|
|
1134
1484
|
flowToken,
|
|
1135
|
-
newPassword
|
|
1485
|
+
newPassword
|
|
1136
1486
|
}, options);
|
|
1137
1487
|
}
|
|
1138
1488
|
/**
|
|
@@ -1221,8 +1571,8 @@ function createManagedClient(options, internals) {
|
|
|
1221
1571
|
* const itd = new ItdClient({
|
|
1222
1572
|
* // `auth` не обязателен: когда хранилище уже содержит сессию, токен берётся оттуда,
|
|
1223
1573
|
* // а истёкший продлевается сам. Здесь он нужен на первый запуск.
|
|
1224
|
-
* // Вход по паролю требует токена капчи — см.
|
|
1225
|
-
* auth: { email, password,
|
|
1574
|
+
* // Вход по паролю требует токена капчи — см. CaptchaOptions.
|
|
1575
|
+
* auth: { email, password, captcha: createCaptchaSolver() },
|
|
1226
1576
|
* storage: new FileTokenStorage('./.itd-session.json'),
|
|
1227
1577
|
* rateLimit: { concurrency: 4, rps: 8 },
|
|
1228
1578
|
* });
|
|
@@ -1243,7 +1593,10 @@ var ItdClient = class ItdClient {
|
|
|
1243
1593
|
#auth;
|
|
1244
1594
|
/** Авторизация, сессии и пароли. */
|
|
1245
1595
|
get auth() {
|
|
1246
|
-
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
|
+
});
|
|
1247
1600
|
return this.#auth;
|
|
1248
1601
|
}
|
|
1249
1602
|
/** Профили, подписки, блокировки, приватность. */
|
|
@@ -1740,7 +2093,9 @@ function assertAccountsActive(accounts, action) {
|
|
|
1740
2093
|
* await accounts.restore();
|
|
1741
2094
|
*
|
|
1742
2095
|
* if (!accounts.has('kiow')) {
|
|
1743
|
-
* accounts.addAccount('kiow', {
|
|
2096
|
+
* accounts.addAccount('kiow', {
|
|
2097
|
+
* auth: { email, password, captcha: createCaptchaSolver() },
|
|
2098
|
+
* });
|
|
1744
2099
|
* }
|
|
1745
2100
|
*
|
|
1746
2101
|
* await accounts.account('kiow').posts.create({ content: 'привет' });
|
|
@@ -1819,7 +2174,9 @@ var ItdAccounts = class ItdAccounts {
|
|
|
1819
2174
|
*
|
|
1820
2175
|
* @example
|
|
1821
2176
|
* ```ts
|
|
1822
|
-
* accounts.addAccount('bot', {
|
|
2177
|
+
* accounts.addAccount('bot', {
|
|
2178
|
+
* auth: { email, password, captcha: createCaptchaSolver() },
|
|
2179
|
+
* });
|
|
1823
2180
|
* accounts.addAccount('reader', { auth: '<accessToken>' });
|
|
1824
2181
|
* accounts.addAccount('через-прокси', { fetch: proxyFetch('socks5://…') });
|
|
1825
2182
|
* ```
|
|
@@ -2157,6 +2514,10 @@ exports.AUDIO_MIME_TYPES = require_render.AUDIO_MIME_TYPES;
|
|
|
2157
2514
|
exports.AccessType = require_url.AccessType;
|
|
2158
2515
|
exports.AttachmentType = require_url.AttachmentType;
|
|
2159
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;
|
|
2160
2521
|
exports.CommentSort = require_url.CommentSort;
|
|
2161
2522
|
exports.DEFAULT_BASE_URL = require_url.DEFAULT_BASE_URL;
|
|
2162
2523
|
exports.DEFAULT_RATE_LIMIT_BUCKET = require_url.DEFAULT_RATE_LIMIT_BUCKET;
|
|
@@ -2206,6 +2567,8 @@ exports.NotificationUpdateType = require_events.NotificationUpdateType;
|
|
|
2206
2567
|
exports.OPERATIONS = require_url.OPERATIONS;
|
|
2207
2568
|
exports.PaginationMode = require_render.PaginationMode;
|
|
2208
2569
|
exports.Paginator = require_render.Paginator;
|
|
2570
|
+
exports.QrLoginStatus = QrLoginStatus;
|
|
2571
|
+
exports.QrLoginStreamStatus = QrLoginStreamStatus;
|
|
2209
2572
|
exports.RateLimitPacing = require_url.RateLimitPacing;
|
|
2210
2573
|
exports.ReportReason = require_url.ReportReason;
|
|
2211
2574
|
exports.ReportTargetType = require_url.ReportTargetType;
|