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.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { A as ViewReason, B as BUCKET_LIMITS, C as ItdErrorCode, D as ReportTargetType, E as ReportReason, F as defineBuiltInOperation, G as pickArray, H as createClientRuntime, I as isBuiltInOperationId, L as operationBucket, M as WallAccess, N as ITD_CATALOG, O as ServiceState, P as OPERATIONS, R as operationMethod, S as InteractionType, T as NotificationType, U as createApiError, V as DEFAULT_RATE_LIMIT_BUCKET, X as RetrySafety, Y as pickString, _ as CaptchaType, at as orderPluginDefinitions, b as FeedTab, c as normalizeNotification, ct as STATUS_SERVICE, d as canonicalNotificationType, dt as isRecord, et as createRequestAbortScope, f as isKnownNotificationType, ft as requireOptionalBoolean, g as CaptchaField, gt as installAsyncDisposeFallback, h as CaptchaChoice, ht as systemClock, it as assertPluginRemovable, j as ViewSource, k as SpanType, lt as resolveRateLimit, m as AttachmentType, n as formatNotificationText, o as passthroughOperation, ot as DEFAULT_BASE_URL, p as AccessType, pt as RateLimitPacing, rt as RequestQueuePool, s as voidOperation, t as resolveNotificationUrl, tt as requestAbortError, ut as LIBRARY_VERSION, v as CommentSort, w as LikesVisibility, x as IncidentKind, y as EventChannelStatus, z as operationRetrySafety } from "./shared/url-BA5hEwj2.js";
|
|
2
2
|
import { A as isItdStateError, C as isItdError, D as isItdPhoneVerificationError, E as isItdNotFoundError, O as isItdRateLimitError, S as isItdConflictError, T as isItdForbiddenError, _ as ItdStateError, a as ItdConfigError, b as isItdApiError, c as ItdErrorKind, d as ItdForbiddenError, f as ItdNetworkError, g as ItdServerError, h as ItdRateLimitError, i as ItdAuthError, j as isItdValidationError, k as isItdServerError, l as ItdFileError, m as ItdPhoneVerificationError, n as ItdApiError, o as ItdConflictError, p as ItdNotFoundError, r as ItdApiErrorKind, s as ItdError, t as ItdAbortError, u as ItdFileErrorReason, v as ItdTimeoutError, w as isItdFileError, x as isItdAuthError, y as ItdValidationError } from "./shared/errors-GI10kZxk.js";
|
|
3
|
-
import { c as REFRESH_COOKIE, l as REFRESH_COOKIE_PATH, s as CookieJar, u as isSerializedCookieEntry, v as RuntimeMode, y as createDeviceId } from "./shared/redact-DJV0XM3n.js";
|
|
4
|
-
import { A as utcStampToIso, C as AUDIO_MIME_TYPES, D as isBuilder, E as comment, F as mapPage, I as createFeatureHost, L as ExtensibleOperationCatalog, M as BaseResource, N as PaginationMode, O as createStatusFeature, P as Paginator, S as ALLOWED_MIME_TYPES, T as VIDEO_MIME_TYPES, _ as poll, a as createShopOrderAccessStorage, b as parseHtml, c as ShopOrderStatus, d as isMyProfile, f as fromStream, g as post, h as report, i as MemoryShopOrderAccessStorage, j as NotificationsResource, k as toDate, l as ShopProductCategory, m as createResources, n as renderSpans, o as statusDays, p as fromUrl, r as createShopFeature, s as ShopConsentKind, t as SpanRenderFormat, u as ShopProductStatus, v as autoSpans, w as IMAGE_MIME_TYPES, x as parseMarkdown, y as markup } from "./shared/render
|
|
3
|
+
import { c as REFRESH_COOKIE, h as joinUrl, l as REFRESH_COOKIE_PATH, s as CookieJar, u as isSerializedCookieEntry, v as RuntimeMode, y as createDeviceId } from "./shared/redact-DJV0XM3n.js";
|
|
4
|
+
import { A as utcStampToIso, C as AUDIO_MIME_TYPES, D as isBuilder, E as comment, F as mapPage, I as createFeatureHost, L as ExtensibleOperationCatalog, M as BaseResource, N as PaginationMode, O as createStatusFeature, P as Paginator, S as ALLOWED_MIME_TYPES, T as VIDEO_MIME_TYPES, _ as poll, a as createShopOrderAccessStorage, b as parseHtml, c as ShopOrderStatus, d as isMyProfile, f as fromStream, g as post, h as report, i as MemoryShopOrderAccessStorage, j as NotificationsResource, k as toDate, l as ShopProductCategory, m as createResources, n as renderSpans, o as statusDays, p as fromUrl, r as createShopFeature, s as ShopConsentKind, t as SpanRenderFormat, u as ShopProductStatus, v as autoSpans, w as IMAGE_MIME_TYPES, x as parseMarkdown, y as markup } from "./shared/render-CkhvH_yh.js";
|
|
5
5
|
import { c as FileTransferMode } from "./shared/options-i4LrKfqx.js";
|
|
6
|
-
import { _ as
|
|
6
|
+
import { _ as Emitter, a as WebSocketTransport, c as NotificationEventsTransport, d as NotificationUpdateOrigin, f as NotificationUpdateType, g as runEventMiddleware, h as UnauthorizedStreamError, i as createNotificationEventsClient, l as resolveNotificationEventsOptions, m as EventChannel, n as EventRouter, o as EVENT_OPERATIONS, p as createParser, r as NotificationEventsClient, s as NotificationEvents, t as EventComposer, u as setNotificationEventsConnectGuard, v as reportListenerError } from "./shared/events-B0Wtn4cF.js";
|
|
7
7
|
import { a as isEnumerableKeyValueStore, i as createRecordKeyValueStore, o as withCodec, r as createKeyValueStore, s as withNamespace, t as MemoryKeyValueStore } from "./shared/key-value-store-Bt2PSMxY.js";
|
|
8
8
|
import { n as copySession, r as createTokenStorage, t as MemoryTokenStorage } from "./shared/storage-DiDe0PZM.js";
|
|
9
9
|
import { a as scopedTokenStorage, i as isRestorableSession, n as controlledTokenStorage, r as createMultiTokenStorage, t as MemoryMultiTokenStorage } from "./shared/multi-storage-C3Ngtop1.js";
|
|
@@ -31,12 +31,37 @@ const SignInStatus = Object.freeze({
|
|
|
31
31
|
Authenticated: "authenticated",
|
|
32
32
|
OtpRequired: "otp_required"
|
|
33
33
|
});
|
|
34
|
+
/**
|
|
35
|
+
* Поле тела запроса по умолчанию для известных провайдеров.
|
|
36
|
+
*
|
|
37
|
+
* Используется, когда провайдера не спрашивали. Ответ `captchaProvider()` важнее этой таблицы.
|
|
38
|
+
*/
|
|
39
|
+
const CAPTCHA_FIELDS = Object.freeze({
|
|
40
|
+
[CaptchaType.Itd]: CaptchaField.Itd,
|
|
41
|
+
[CaptchaType.Cloudflare]: CaptchaField.Cloudflare
|
|
42
|
+
});
|
|
43
|
+
/** Состояния, которые может вернуть проверка QR-входа. */
|
|
44
|
+
const QrLoginStatus = Object.freeze({
|
|
45
|
+
Pending: "pending",
|
|
46
|
+
Scanned: "scanned",
|
|
47
|
+
CaptchaRequired: "captcha_required",
|
|
48
|
+
Authorized: "authorized",
|
|
49
|
+
Rejected: "rejected"
|
|
50
|
+
});
|
|
51
|
+
/** Состояния, приходящие из потокового наблюдения за QR-входом. */
|
|
52
|
+
const QrLoginStreamStatus = Object.freeze({
|
|
53
|
+
Pending: "pending",
|
|
54
|
+
Scanned: "scanned",
|
|
55
|
+
Approved: "approved",
|
|
56
|
+
Rejected: "rejected"
|
|
57
|
+
});
|
|
34
58
|
function requireString(body, key, message) {
|
|
35
59
|
const value = pickString(body, key);
|
|
36
60
|
if (!value) throw new ItdConfigError(message);
|
|
37
61
|
return value;
|
|
38
62
|
}
|
|
39
63
|
const AUTH_CHECK = passthroughOperation("auth.check");
|
|
64
|
+
const AUTH_CAPTCHA_PROVIDER = passthroughOperation("auth.captchaProvider");
|
|
40
65
|
const AUTH_SIGN_UP = defineBuiltInOperation("auth.signUp", (body) => requireString(body, "flowToken", "Сервер не вернул flowToken при регистрации"));
|
|
41
66
|
const AUTH_SIGN_IN = defineBuiltInOperation("auth.signIn", (body) => {
|
|
42
67
|
const accessToken = pickString(body, "accessToken");
|
|
@@ -58,6 +83,84 @@ const AUTH_CHANGE_PASSWORD = voidOperation("auth.changePassword");
|
|
|
58
83
|
const AUTH_SESSIONS = defineBuiltInOperation("auth.sessions", (body) => pickArray(body, "sessions"));
|
|
59
84
|
const AUTH_REVOKE_SESSION = voidOperation("auth.revokeSession");
|
|
60
85
|
const AUTH_REVOKE_OTHER_SESSIONS = voidOperation("auth.revokeOtherSessions");
|
|
86
|
+
const AUTH_QR_START = passthroughOperation("auth.qrStart");
|
|
87
|
+
const AUTH_QR_CLAIM = passthroughOperation("auth.qrClaim");
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/session/captcha.ts
|
|
90
|
+
function requireCaptchaText(value, field) {
|
|
91
|
+
if (value !== void 0 && (typeof value !== "string" || value.trim() === "")) throw new ItdConfigError(`${field} должен быть непустой строкой`);
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Проверяет форму блока `auth.captcha`.
|
|
95
|
+
*
|
|
96
|
+
* Отсутствие источника токена ошибкой не считается: до входа по паролю дело может и не
|
|
97
|
+
* дойти. О нехватке сообщит {@link resolveCaptchaBody} в момент входа.
|
|
98
|
+
*
|
|
99
|
+
* @throws {ItdConfigError} при некорректных значениях
|
|
100
|
+
*/
|
|
101
|
+
function validateCaptchaOptions(captcha) {
|
|
102
|
+
if (captcha === void 0) return;
|
|
103
|
+
if (!isRecord(captcha)) throw new ItdConfigError("auth.captcha должен быть объектом");
|
|
104
|
+
const { type, token, getToken, field } = captcha;
|
|
105
|
+
requireCaptchaText(type, "auth.captcha.type");
|
|
106
|
+
requireCaptchaText(token, "auth.captcha.token");
|
|
107
|
+
requireCaptchaText(field, "auth.captcha.field");
|
|
108
|
+
if (getToken !== void 0 && typeof getToken !== "function") throw new ItdConfigError("auth.captcha.getToken должен быть функцией");
|
|
109
|
+
}
|
|
110
|
+
/** Разбирает ответ `GET /api/v1/auth/captcha/provider` для автоматического входа. */
|
|
111
|
+
function readCaptchaProvider(body) {
|
|
112
|
+
const config = isRecord(body) ? body : void 0;
|
|
113
|
+
const provider = config?.provider;
|
|
114
|
+
const field = config?.field;
|
|
115
|
+
if (typeof provider !== "string" || provider.trim() === "" || typeof field !== "string" || field.trim() === "") throw new ItdConfigError("Сервер вернул неподдерживаемую конфигурацию капчи");
|
|
116
|
+
return {
|
|
117
|
+
provider,
|
|
118
|
+
field
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
/** Куда класть токен: настройка, иначе умолчание провайдера. */
|
|
122
|
+
function resolveField(type, configured) {
|
|
123
|
+
const field = configured ?? CAPTCHA_FIELDS[type];
|
|
124
|
+
if (field) return field;
|
|
125
|
+
throw new ItdConfigError(`Неизвестно, в каком поле сервер ждёт токен капчи «${type}». Передайте auth.captcha.field либо оставьте auth.captcha.type = ${CaptchaChoice.Auto}, чтобы поле назвал сервер.`);
|
|
126
|
+
}
|
|
127
|
+
/** Спрашивает свежий токен, затем берёт разовый. */
|
|
128
|
+
async function resolveToken(captcha, type) {
|
|
129
|
+
const token = await captcha.getToken?.(type) || captcha.token;
|
|
130
|
+
if (token) return token;
|
|
131
|
+
throw new ItdConfigError(`Источник капчи не вернул токен для провайдера «${type}». Проверьте auth.captcha.getToken.`);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Готовит фрагмент тела запроса с токеном капчи.
|
|
135
|
+
*
|
|
136
|
+
* При `CaptchaChoice.Auto` провайдер спрашивается у сервера — тогда и тип виджета,
|
|
137
|
+
* и имя поля приходят оттуда. Явно названный тип решается без этого запроса.
|
|
138
|
+
*
|
|
139
|
+
* @param askProvider как спросить активного провайдера; вызывается, только если это нужно
|
|
140
|
+
* @throws {ItdConfigError} если капча не настроена или токен не получен
|
|
141
|
+
*/
|
|
142
|
+
async function resolveCaptchaBody(captcha, askProvider) {
|
|
143
|
+
if (!captcha?.token && !captcha?.getToken) throw new ItdConfigError("Вход по email и паролю требует токен капчи. Передайте auth.captcha.getToken — источник свежего токена — либо разовый auth.captcha.token. В Node токен получает пакет @itd-api/captcha.");
|
|
144
|
+
const choice = captcha.type ?? CaptchaChoice.Auto;
|
|
145
|
+
if (choice === CaptchaChoice.Auto) {
|
|
146
|
+
const { provider, field } = await askProvider();
|
|
147
|
+
return { [field]: await resolveToken(captcha, provider) };
|
|
148
|
+
}
|
|
149
|
+
return { [resolveField(choice, captcha.field)]: await resolveToken(captcha, choice) };
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Готовит фрагмент тела для разового вызова с готовым доказательством.
|
|
153
|
+
*
|
|
154
|
+
* Поле берётся из самого доказательства либо из умолчания провайдера; запроса к серверу нет.
|
|
155
|
+
*
|
|
156
|
+
* @throws {ItdConfigError} если доказательство неполное
|
|
157
|
+
*/
|
|
158
|
+
function captchaBody(captcha) {
|
|
159
|
+
if (!isRecord(captcha)) throw new ItdConfigError("captcha должен быть объектом { type, token }");
|
|
160
|
+
if (typeof captcha.token !== "string" || captcha.token.trim() === "") throw new ItdConfigError("captcha.token должен быть непустой строкой");
|
|
161
|
+
if (typeof captcha.type !== "string" || captcha.type.trim() === "") throw new ItdConfigError("captcha.type должен быть непустой строкой");
|
|
162
|
+
return { [resolveField(captcha.type, captcha.field)]: captcha.token };
|
|
163
|
+
}
|
|
61
164
|
//#endregion
|
|
62
165
|
//#region src/session/jwt.ts
|
|
63
166
|
/**
|
|
@@ -74,38 +177,34 @@ function decodeBase64Url(segment) {
|
|
|
74
177
|
const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
|
|
75
178
|
return new TextDecoder().decode(bytes);
|
|
76
179
|
}
|
|
77
|
-
/**
|
|
78
|
-
|
|
79
|
-
*
|
|
80
|
-
* Подпись не проверяется — ключа для проверки у клиента нет. Прочитанные значения служат
|
|
81
|
-
* лишь метками для разделения локального состояния и не дают доступа: его определяет сервер.
|
|
82
|
-
*/
|
|
83
|
-
function readTokenIdentity(token) {
|
|
180
|
+
/** Читает полезную нагрузку JWT без проверки подписи. */
|
|
181
|
+
function readTokenPayload(token) {
|
|
84
182
|
try {
|
|
85
183
|
const payload = token.split(".")[1];
|
|
86
|
-
if (!payload) return
|
|
184
|
+
if (!payload) return void 0;
|
|
87
185
|
const parsed = JSON.parse(decodeBase64Url(payload));
|
|
88
|
-
if (typeof parsed !== "object" || parsed === null) return
|
|
89
|
-
|
|
90
|
-
return {
|
|
91
|
-
...typeof sub === "string" && sub.length > 0 ? { subject: sub } : {},
|
|
92
|
-
...typeof sid === "string" && sid.length > 0 ? { sessionId: sid } : {}
|
|
93
|
-
};
|
|
186
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
|
|
187
|
+
return parsed;
|
|
94
188
|
} catch {
|
|
95
|
-
return
|
|
189
|
+
return;
|
|
96
190
|
}
|
|
97
191
|
}
|
|
98
|
-
/**
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
192
|
+
/** Читает используемые клиентом поля JWT за один раз. */
|
|
193
|
+
function readTokenMetadata(token) {
|
|
194
|
+
const payload = readTokenPayload(token);
|
|
195
|
+
if (!payload) return {};
|
|
196
|
+
const { sub, sid, exp } = payload;
|
|
197
|
+
const expiresAt = typeof exp === "number" && Number.isFinite(exp) ? exp * 1e3 : void 0;
|
|
198
|
+
return {
|
|
199
|
+
...typeof sub === "string" && sub.length > 0 ? { subject: sub } : {},
|
|
200
|
+
...typeof sid === "string" && sid.length > 0 ? { sessionId: sid } : {},
|
|
201
|
+
...expiresAt !== void 0 && Number.isFinite(expiresAt) ? { expiresAt } : {}
|
|
202
|
+
};
|
|
106
203
|
}
|
|
107
204
|
//#endregion
|
|
108
205
|
//#region src/session/auth.ts
|
|
206
|
+
/** Запас до истечения токена на очередь и выполнение запроса. */
|
|
207
|
+
const ACCESS_TOKEN_REFRESH_LEEWAY_MS = 3e4;
|
|
109
208
|
/**
|
|
110
209
|
* Проверяет форму `auth` и сообщает о типичных ошибках понятным текстом.
|
|
111
210
|
*
|
|
@@ -134,11 +233,10 @@ function validateAuth(auth) {
|
|
|
134
233
|
return { ...auth };
|
|
135
234
|
}
|
|
136
235
|
if ("email" in auth || "password" in auth) {
|
|
137
|
-
const { email, password,
|
|
236
|
+
const { email, password, captcha } = auth;
|
|
138
237
|
if (typeof email !== "string" || email.trim() === "") throw new ItdConfigError("auth.email должен быть непустой строкой");
|
|
139
238
|
if (typeof password !== "string" || password === "") throw new ItdConfigError("auth.password должен быть непустой строкой");
|
|
140
|
-
|
|
141
|
-
if (turnstileToken !== void 0 && (typeof turnstileToken !== "string" || turnstileToken.trim() === "")) throw new ItdConfigError("auth.turnstileToken должен быть непустой строкой");
|
|
239
|
+
validateCaptchaOptions(captcha);
|
|
142
240
|
return { ...auth };
|
|
143
241
|
}
|
|
144
242
|
throw new ItdConfigError("auth не распознан. Ожидается строка с accessToken либо объект { accessToken }, { email, password } или { getToken }");
|
|
@@ -185,6 +283,7 @@ function createItdAuth(options, deps) {
|
|
|
185
283
|
}
|
|
186
284
|
/** Пути эндпоинтов авторизации. */
|
|
187
285
|
const AUTH_PATHS = {
|
|
286
|
+
captchaProvider: "/api/v1/auth/captcha/provider",
|
|
188
287
|
signUp: "/api/v1/auth/sign-up",
|
|
189
288
|
signIn: "/api/v1/auth/sign-in",
|
|
190
289
|
verifyOtp: "/api/v1/auth/verify-otp",
|
|
@@ -194,7 +293,10 @@ const AUTH_PATHS = {
|
|
|
194
293
|
forgotPassword: "/api/v1/auth/forgot-password",
|
|
195
294
|
resetPassword: "/api/v1/auth/reset-password",
|
|
196
295
|
changePassword: "/api/v1/auth/change-password",
|
|
197
|
-
sessions: "/api/v1/auth/sessions"
|
|
296
|
+
sessions: "/api/v1/auth/sessions",
|
|
297
|
+
qrStart: "/api/v1/auth/qr/start",
|
|
298
|
+
qrClaim: "/api/v1/auth/qr/claim",
|
|
299
|
+
qrStream: "/api/v1/auth/qr/stream"
|
|
198
300
|
};
|
|
199
301
|
/**
|
|
200
302
|
* Публичный ключ Cloudflare Turnstile платформы итд.com.
|
|
@@ -204,14 +306,15 @@ const AUTH_PATHS = {
|
|
|
204
306
|
*
|
|
205
307
|
* Ключ привязан к домену: на чужом origin Cloudflare отказывает виджету с кодом `110200`.
|
|
206
308
|
* Поэтому отрисовать его может только код, выполняемый на самом итд.com. Остальным
|
|
207
|
-
* подходит `@itd-api/
|
|
309
|
+
* подходит `@itd-api/captcha`, готовый токен из другого источника или вовсе вход
|
|
208
310
|
* без капчи — по сохранённой сессии либо по токенам, взятым в браузере.
|
|
209
311
|
*
|
|
210
312
|
* @example
|
|
211
313
|
* ```ts
|
|
212
314
|
* turnstile.render('#captcha', {
|
|
213
315
|
* sitekey: TURNSTILE_SITE_KEY,
|
|
214
|
-
* callback: (
|
|
316
|
+
* callback: (token) =>
|
|
317
|
+
* itd.auth.signIn({ email, password, captcha: { type: CaptchaType.Cloudflare, token } }),
|
|
215
318
|
* });
|
|
216
319
|
* ```
|
|
217
320
|
*/
|
|
@@ -305,7 +408,7 @@ var AuthManager = class {
|
|
|
305
408
|
* старте читают хранилище один раз и не заводят каждый свой `deviceId`.
|
|
306
409
|
*/
|
|
307
410
|
#loading = null;
|
|
308
|
-
/**
|
|
411
|
+
/** Текущее обновление: к нему присоединяются запросы той же ревизии. */
|
|
309
412
|
#refreshing = null;
|
|
310
413
|
/** Общий промис входа по логину и паролю. */
|
|
311
414
|
#signingIn = null;
|
|
@@ -315,6 +418,7 @@ var AuthManager = class {
|
|
|
315
418
|
#externalToken;
|
|
316
419
|
/** Идентификаторы последнего токена внешнего источника для синхронных потребителей. */
|
|
317
420
|
#externalIdentity;
|
|
421
|
+
#tokenMetadata;
|
|
318
422
|
/**
|
|
319
423
|
* Идентификатор устройства.
|
|
320
424
|
*
|
|
@@ -328,9 +432,7 @@ var AuthManager = class {
|
|
|
328
432
|
* Счётчик смен владельца авторизации.
|
|
329
433
|
*
|
|
330
434
|
* Растёт при каждой операции, которая заменяет или очищает сессию извне: `clear`,
|
|
331
|
-
* `setSession`, `setAccessToken`, вход.
|
|
332
|
-
* сетевым запросом и сверяет перед записью — иначе запоздавший ответ обновления мог бы
|
|
333
|
-
* воскресить уже очищенную сессию поверх `signOut`.
|
|
435
|
+
* `setSession`, `setAccessToken`, вход. Refresh сверяет снимок до и после сетевого запроса.
|
|
334
436
|
*/
|
|
335
437
|
#authEpoch = 0;
|
|
336
438
|
#externalReadGeneration = 0;
|
|
@@ -363,6 +465,7 @@ var AuthManager = class {
|
|
|
363
465
|
this.#disposed = true;
|
|
364
466
|
this.#externalReadGeneration += 1;
|
|
365
467
|
this.#invalidateInFlight();
|
|
468
|
+
this.#tokenMetadata = void 0;
|
|
366
469
|
this.#emitter.removeAllListeners();
|
|
367
470
|
}
|
|
368
471
|
/**
|
|
@@ -423,12 +526,25 @@ var AuthManager = class {
|
|
|
423
526
|
return this.#session?.accessToken ?? null;
|
|
424
527
|
}
|
|
425
528
|
#identityForToken(accessToken) {
|
|
426
|
-
const token =
|
|
529
|
+
const token = this.#metadataForToken(accessToken);
|
|
427
530
|
return {
|
|
428
531
|
...token.subject ? { userId: token.subject } : {},
|
|
429
532
|
...token.sessionId ? { sessionId: token.sessionId } : {}
|
|
430
533
|
};
|
|
431
534
|
}
|
|
535
|
+
#metadataForToken(accessToken) {
|
|
536
|
+
if (!accessToken) {
|
|
537
|
+
this.#tokenMetadata = void 0;
|
|
538
|
+
return {};
|
|
539
|
+
}
|
|
540
|
+
if (this.#tokenMetadata?.accessToken === accessToken) return this.#tokenMetadata.value;
|
|
541
|
+
const value = readTokenMetadata(accessToken);
|
|
542
|
+
this.#tokenMetadata = {
|
|
543
|
+
accessToken,
|
|
544
|
+
value
|
|
545
|
+
};
|
|
546
|
+
return value;
|
|
547
|
+
}
|
|
432
548
|
async #readExternalToken(getToken) {
|
|
433
549
|
this.#assertActive();
|
|
434
550
|
const generation = ++this.#externalReadGeneration;
|
|
@@ -475,15 +591,33 @@ var AuthManager = class {
|
|
|
475
591
|
if (this.#jar.has("is_auth", this.#config.baseUrl)) return true;
|
|
476
592
|
return Boolean(this.#session?.refreshToken);
|
|
477
593
|
}
|
|
478
|
-
/**
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
594
|
+
/** Готовит внутреннюю сессию до retry исходного запроса. */
|
|
595
|
+
async preflight(allowRefresh) {
|
|
596
|
+
this.#assertActive();
|
|
597
|
+
let accessToken = (await this.#loadSession())?.accessToken;
|
|
598
|
+
if (!accessToken) {
|
|
599
|
+
const auth = this.#config.auth;
|
|
600
|
+
if (typeof auth === "object" && auth !== null && "email" in auth) accessToken = await this.#signInWithCredentials(auth) ?? void 0;
|
|
601
|
+
}
|
|
602
|
+
if (!accessToken || !allowRefresh || !this.#config.autoRefresh) return;
|
|
603
|
+
const expiresAt = this.#metadataForToken(accessToken).expiresAt;
|
|
604
|
+
if (expiresAt === void 0 || expiresAt > this.#config.clock.now() + ACCESS_TOKEN_REFRESH_LEEWAY_MS || !this.#hasRefreshSession()) return;
|
|
605
|
+
try {
|
|
606
|
+
await this.#refreshDeduplicated({
|
|
607
|
+
epoch: this.#authEpoch,
|
|
608
|
+
accessToken
|
|
609
|
+
});
|
|
610
|
+
} catch (error) {
|
|
611
|
+
this.#emitter.emit("authError", { error });
|
|
612
|
+
throw error;
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
/** Перед каждой попыткой перечитывает внешний источник токена. */
|
|
484
616
|
async prepare() {
|
|
485
617
|
this.#assertActive();
|
|
486
|
-
await this
|
|
618
|
+
if ((await this.#loadSession())?.accessToken) return;
|
|
619
|
+
const auth = this.#config.auth;
|
|
620
|
+
if (typeof auth === "object" && auth !== null && "getToken" in auth) await this.#readExternalToken(() => auth.getToken());
|
|
487
621
|
}
|
|
488
622
|
/**
|
|
489
623
|
* Заголовки уже подготовленной авторизации без чтения storage или вызова внешнего источника.
|
|
@@ -598,6 +732,13 @@ var AuthManager = class {
|
|
|
598
732
|
}
|
|
599
733
|
/** Сохраняет токен, полученный извне, — например после подтверждения OTP. */
|
|
600
734
|
async setAccessToken(accessToken, expectedRevision = this.#authEpoch, cookies) {
|
|
735
|
+
return this.#storeAccessToken(accessToken, expectedRevision, cookies, false);
|
|
736
|
+
}
|
|
737
|
+
/** Атомарно принимает токен и cookie завершённого входа как новую сессию. @internal */
|
|
738
|
+
async commitAuthFlow(accessToken, cookies, expectedRevision = this.#authEpoch) {
|
|
739
|
+
return this.#storeAccessToken(accessToken, expectedRevision, cookies, true);
|
|
740
|
+
}
|
|
741
|
+
async #storeAccessToken(accessToken, expectedRevision, cookies, replaceSession) {
|
|
601
742
|
this.#assertActive();
|
|
602
743
|
if (typeof accessToken !== "string" || accessToken.trim() === "") throw new ItdConfigError("accessToken должен быть непустой строкой");
|
|
603
744
|
await this.#loadSession();
|
|
@@ -607,7 +748,7 @@ var AuthManager = class {
|
|
|
607
748
|
this.#invalidateInFlight();
|
|
608
749
|
this.#transitionAuth(accessToken);
|
|
609
750
|
const saved = this.#saveSession({
|
|
610
|
-
...this.#session ?? {},
|
|
751
|
+
...replaceSession ? {} : this.#session ?? {},
|
|
611
752
|
accessToken,
|
|
612
753
|
obtainedAt: this.#config.clock.now()
|
|
613
754
|
});
|
|
@@ -630,7 +771,7 @@ var AuthManager = class {
|
|
|
630
771
|
async getUserId() {
|
|
631
772
|
this.#assertActive();
|
|
632
773
|
const session = await this.#loadSession();
|
|
633
|
-
return session?.accessToken ?
|
|
774
|
+
return session?.accessToken ? this.#identityForToken(session.accessToken).userId : void 0;
|
|
634
775
|
}
|
|
635
776
|
/** Заменяет сессию и связанные с ней cookie целиком. */
|
|
636
777
|
async setSession(session) {
|
|
@@ -733,24 +874,30 @@ var AuthManager = class {
|
|
|
733
874
|
this.#persistence = pending.catch(() => {});
|
|
734
875
|
return pending;
|
|
735
876
|
}
|
|
736
|
-
/**
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const promise = this.#performRefresh().finally(() => {
|
|
744
|
-
this.#refreshing = null;
|
|
877
|
+
/** Дедуплицирует refresh одной ревизии; следующая ждёт завершения предыдущей. */
|
|
878
|
+
#refreshDeduplicated(expectation = { epoch: this.#authEpoch }) {
|
|
879
|
+
const current = this.#refreshing;
|
|
880
|
+
if (current) {
|
|
881
|
+
if (current.epoch === expectation.epoch) return current.promise;
|
|
882
|
+
return current.promise.then(() => this.#refreshDeduplicated(expectation), () => this.#refreshDeduplicated(expectation));
|
|
883
|
+
}
|
|
884
|
+
const promise = this.#performRefresh(expectation).finally(() => {
|
|
885
|
+
if (this.#refreshing?.promise === promise) this.#refreshing = null;
|
|
745
886
|
});
|
|
746
|
-
this.#refreshing =
|
|
887
|
+
this.#refreshing = {
|
|
888
|
+
epoch: expectation.epoch,
|
|
889
|
+
promise
|
|
890
|
+
};
|
|
747
891
|
return promise;
|
|
748
892
|
}
|
|
749
|
-
|
|
893
|
+
#matchesRefreshExpectation(expectation) {
|
|
894
|
+
return this.#authEpoch === expectation.epoch && (expectation.accessToken === void 0 || this.#session?.accessToken === expectation.accessToken);
|
|
895
|
+
}
|
|
896
|
+
async #performRefresh(expectation) {
|
|
750
897
|
this.#assertActive();
|
|
751
898
|
await this.#loadSession();
|
|
899
|
+
if (!this.#matchesRefreshExpectation(expectation)) return this.#currentAccessToken();
|
|
752
900
|
if (!this.#hasRefreshSession()) return this.#reloginOrNull();
|
|
753
|
-
const epoch = this.#authEpoch;
|
|
754
901
|
const flowCookies = this.#jar.clone();
|
|
755
902
|
try {
|
|
756
903
|
const accessToken = await this.#http.execute(AUTH_REFRESH, {
|
|
@@ -759,7 +906,7 @@ var AuthManager = class {
|
|
|
759
906
|
skipAuthRefresh: true,
|
|
760
907
|
cookieJar: flowCookies
|
|
761
908
|
});
|
|
762
|
-
if (this.#
|
|
909
|
+
if (!this.#matchesRefreshExpectation(expectation)) return this.#session?.accessToken ?? null;
|
|
763
910
|
this.#jar.replaceWith(flowCookies);
|
|
764
911
|
const rotated = this.#jar.getValue(REFRESH_COOKIE, this.#config.baseUrl + REFRESH_COOKIE_PATH);
|
|
765
912
|
this.#transitionAuth(accessToken, false);
|
|
@@ -771,10 +918,11 @@ var AuthManager = class {
|
|
|
771
918
|
});
|
|
772
919
|
this.#emitter.emit("tokens", { accessToken });
|
|
773
920
|
await saved;
|
|
921
|
+
if (this.#authEpoch !== expectation.epoch) return this.#currentAccessToken();
|
|
774
922
|
return accessToken;
|
|
775
923
|
} catch (error) {
|
|
924
|
+
if (this.#authEpoch !== expectation.epoch) return this.#currentAccessToken();
|
|
776
925
|
if (error instanceof ItdAuthError) {
|
|
777
|
-
if (this.#authEpoch !== epoch) return this.#session?.accessToken ?? null;
|
|
778
926
|
this.#invalidateInFlight();
|
|
779
927
|
const clearedRevision = this.#authEpoch;
|
|
780
928
|
this.#transitionAuth(void 0);
|
|
@@ -815,29 +963,23 @@ var AuthManager = class {
|
|
|
815
963
|
this.#signingIn = promise;
|
|
816
964
|
return promise;
|
|
817
965
|
}
|
|
818
|
-
/**
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
if (credentials.getTurnstileToken) {
|
|
826
|
-
const token = await credentials.getTurnstileToken();
|
|
827
|
-
if (token) return token;
|
|
828
|
-
}
|
|
829
|
-
if (credentials.turnstileToken) return credentials.turnstileToken;
|
|
830
|
-
throw new ItdConfigError("Вход по email и паролю требует токен капчи Cloudflare Turnstile: без него сервер отвечает 422. Передайте auth.getTurnstileToken (источник свежего токена) либо разовый auth.turnstileToken. Ключ виджета — TURNSTILE_SITE_KEY. В Node токен умеет добывать отдельный пакет: npm i @itd-api/turnstile, затем getTurnstileToken: createTurnstileSolver().");
|
|
966
|
+
/** Спрашивает у сервера активного провайдера и поле, в котором он ждёт токен. */
|
|
967
|
+
async #resolveCaptchaProvider() {
|
|
968
|
+
return readCaptchaProvider(await this.#http.execute(AUTH_CAPTCHA_PROVIDER, {
|
|
969
|
+
path: AUTH_PATHS.captchaProvider,
|
|
970
|
+
skipAuth: true,
|
|
971
|
+
skipAuthRefresh: true
|
|
972
|
+
}));
|
|
831
973
|
}
|
|
832
974
|
async #performSignIn(credentials, revision) {
|
|
833
|
-
const
|
|
975
|
+
const captcha = await resolveCaptchaBody(credentials.captcha, () => this.#resolveCaptchaProvider());
|
|
834
976
|
const flowCookies = new CookieJar();
|
|
835
977
|
const result = await this.#http.execute(AUTH_SIGN_IN, {
|
|
836
978
|
path: AUTH_PATHS.signIn,
|
|
837
979
|
body: {
|
|
838
980
|
email: credentials.email,
|
|
839
981
|
password: credentials.password,
|
|
840
|
-
|
|
982
|
+
...captcha
|
|
841
983
|
},
|
|
842
984
|
skipAuth: true,
|
|
843
985
|
skipAuthRefresh: true,
|
|
@@ -861,6 +1003,125 @@ var AuthManager = class {
|
|
|
861
1003
|
}
|
|
862
1004
|
};
|
|
863
1005
|
//#endregion
|
|
1006
|
+
//#region src/session/qr-stream.ts
|
|
1007
|
+
const QR_STREAM_STATUSES = new Set(Object.values(QrLoginStreamStatus));
|
|
1008
|
+
function readQrLoginStreamEvent(data) {
|
|
1009
|
+
let value;
|
|
1010
|
+
try {
|
|
1011
|
+
value = JSON.parse(data);
|
|
1012
|
+
} catch {
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
if (!isRecord(value)) return void 0;
|
|
1016
|
+
const event = value;
|
|
1017
|
+
if (typeof event.status !== "string" || !QR_STREAM_STATUSES.has(event.status)) return;
|
|
1018
|
+
return {
|
|
1019
|
+
status: event.status,
|
|
1020
|
+
...typeof event.expiresIn === "number" && Number.isFinite(event.expiresIn) ? { expiresIn: event.expiresIn } : {}
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
/** Читает короткоживущий SSE-поток QR-входа и последовательно доставляет его события. */
|
|
1024
|
+
async function consumeQrLoginStream(body, onEvent, signal) {
|
|
1025
|
+
const reader = body.getReader();
|
|
1026
|
+
const decoder = new TextDecoder();
|
|
1027
|
+
let delivery = Promise.resolve();
|
|
1028
|
+
let completed = false;
|
|
1029
|
+
const cancel = () => {
|
|
1030
|
+
reader.cancel(signal?.reason).catch(() => {});
|
|
1031
|
+
};
|
|
1032
|
+
signal?.addEventListener("abort", cancel, { once: true });
|
|
1033
|
+
const parser = createParser({ onEvent(message) {
|
|
1034
|
+
const event = readQrLoginStreamEvent(message.data);
|
|
1035
|
+
if (!event) return;
|
|
1036
|
+
delivery = delivery.then(() => onEvent(event));
|
|
1037
|
+
} });
|
|
1038
|
+
try {
|
|
1039
|
+
for (;;) {
|
|
1040
|
+
const { done, value } = await reader.read();
|
|
1041
|
+
if (done) break;
|
|
1042
|
+
parser.feed(decoder.decode(value, { stream: true }));
|
|
1043
|
+
await delivery;
|
|
1044
|
+
}
|
|
1045
|
+
parser.feed(decoder.decode());
|
|
1046
|
+
await delivery;
|
|
1047
|
+
if (signal?.aborted) throw signal.reason;
|
|
1048
|
+
completed = true;
|
|
1049
|
+
} finally {
|
|
1050
|
+
signal?.removeEventListener("abort", cancel);
|
|
1051
|
+
if (!completed) await reader.cancel().catch(() => {});
|
|
1052
|
+
reader.releaseLock?.();
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
/** Открывает изолированный HTTP/SSE-канал QR-входа. @internal */
|
|
1056
|
+
async function openQrLoginStream(connection, cookies, input, onEvent, signal) {
|
|
1057
|
+
connection.assertActive?.("открыть QR-поток");
|
|
1058
|
+
const path = AUTH_PATHS.qrStream;
|
|
1059
|
+
const method = "POST";
|
|
1060
|
+
const url = joinUrl(connection.baseUrl, path);
|
|
1061
|
+
const abort = createRequestAbortScope(signal, connection.signal, 0, connection.clock);
|
|
1062
|
+
try {
|
|
1063
|
+
const headers = await connection.baseHeaders(url);
|
|
1064
|
+
headers.delete("Authorization");
|
|
1065
|
+
headers.delete("Cookie");
|
|
1066
|
+
headers.set("Accept", "text/event-stream");
|
|
1067
|
+
headers.set("Content-Type", "application/json");
|
|
1068
|
+
const cookie = cookies.getHeader(url);
|
|
1069
|
+
if (cookie) headers.set("Cookie", cookie);
|
|
1070
|
+
let response;
|
|
1071
|
+
try {
|
|
1072
|
+
response = await connection.fetch(url, {
|
|
1073
|
+
method,
|
|
1074
|
+
headers,
|
|
1075
|
+
body: JSON.stringify(input),
|
|
1076
|
+
credentials: "include",
|
|
1077
|
+
signal: abort.signal
|
|
1078
|
+
});
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
const failure = new ItdNetworkError(`Не удалось открыть QR-поток: ${String(error)}`, {
|
|
1081
|
+
method,
|
|
1082
|
+
path,
|
|
1083
|
+
cause: error
|
|
1084
|
+
});
|
|
1085
|
+
throw requestAbortError(abort, {
|
|
1086
|
+
timeout: 0,
|
|
1087
|
+
method,
|
|
1088
|
+
path
|
|
1089
|
+
}, failure);
|
|
1090
|
+
}
|
|
1091
|
+
cookies.setFromResponse(response.url || url, response);
|
|
1092
|
+
if (!response.ok) {
|
|
1093
|
+
const text = await response.text();
|
|
1094
|
+
let body = text;
|
|
1095
|
+
try {
|
|
1096
|
+
body = JSON.parse(text);
|
|
1097
|
+
} catch {}
|
|
1098
|
+
throw createApiError({
|
|
1099
|
+
method,
|
|
1100
|
+
path,
|
|
1101
|
+
status: response.status,
|
|
1102
|
+
statusText: response.statusText,
|
|
1103
|
+
headers: response.headers,
|
|
1104
|
+
response,
|
|
1105
|
+
body,
|
|
1106
|
+
now: connection.clock.now()
|
|
1107
|
+
});
|
|
1108
|
+
}
|
|
1109
|
+
if (!response.body) throw new ItdNetworkError("Сервер не вернул тело QR-потока", {
|
|
1110
|
+
method,
|
|
1111
|
+
path
|
|
1112
|
+
});
|
|
1113
|
+
await consumeQrLoginStream(response.body, onEvent, abort.signal);
|
|
1114
|
+
} catch (error) {
|
|
1115
|
+
throw requestAbortError(abort, {
|
|
1116
|
+
timeout: 0,
|
|
1117
|
+
method,
|
|
1118
|
+
path
|
|
1119
|
+
}, error);
|
|
1120
|
+
} finally {
|
|
1121
|
+
abort.cleanup();
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
//#endregion
|
|
864
1125
|
//#region src/resources/auth.ts
|
|
865
1126
|
/**
|
|
866
1127
|
* Результат входа.
|
|
@@ -875,9 +1136,35 @@ var AuthManager = class {
|
|
|
875
1136
|
*/
|
|
876
1137
|
var AuthResource = class extends BaseResource {
|
|
877
1138
|
#auth;
|
|
1139
|
+
#connection;
|
|
1140
|
+
#qrFlows = /* @__PURE__ */ new Map();
|
|
878
1141
|
constructor(http, deps) {
|
|
879
1142
|
super(http);
|
|
880
1143
|
this.#auth = deps.auth;
|
|
1144
|
+
this.#connection = deps.connection;
|
|
1145
|
+
}
|
|
1146
|
+
#pruneQrFlows() {
|
|
1147
|
+
const now = this.#connection.clock.now();
|
|
1148
|
+
for (const [qrId, flow] of this.#qrFlows) if (flow.expiresAt !== void 0 && flow.expiresAt <= now) this.#qrFlows.delete(qrId);
|
|
1149
|
+
}
|
|
1150
|
+
#qrFlow(qrId) {
|
|
1151
|
+
this.#pruneQrFlows();
|
|
1152
|
+
const existing = this.#qrFlows.get(qrId);
|
|
1153
|
+
if (existing) return existing;
|
|
1154
|
+
const flow = {
|
|
1155
|
+
cookies: this.#auth.createCookieFlow(false),
|
|
1156
|
+
expiresAt: void 0
|
|
1157
|
+
};
|
|
1158
|
+
this.#qrFlows.set(qrId, flow);
|
|
1159
|
+
return flow;
|
|
1160
|
+
}
|
|
1161
|
+
#touchQrFlow(flow, expiresIn) {
|
|
1162
|
+
if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn)) return;
|
|
1163
|
+
const expiresAt = this.#connection.clock.now() + Math.max(0, expiresIn) * 1e3;
|
|
1164
|
+
flow.expiresAt = flow.expiresAt === void 0 ? expiresAt : Math.min(flow.expiresAt, expiresAt);
|
|
1165
|
+
}
|
|
1166
|
+
#forgetTerminalQrError(qrId, error) {
|
|
1167
|
+
if (isItdApiError(error) && error.hasCode("QR_EXPIRED", "QR_ALREADY_USED", "QR_TOKEN_MISMATCH")) this.#qrFlows.delete(qrId);
|
|
881
1168
|
}
|
|
882
1169
|
/**
|
|
883
1170
|
* Проверяет состояние авторизации и возвращает текущего пользователя.
|
|
@@ -891,14 +1178,106 @@ var AuthResource = class extends BaseResource {
|
|
|
891
1178
|
});
|
|
892
1179
|
}
|
|
893
1180
|
/**
|
|
1181
|
+
* Узнаёт активного провайдера капчи и поле токена.
|
|
1182
|
+
*
|
|
1183
|
+
* Сервер может переключить провайдера или переименовать поле без выпуска новой версии SDK.
|
|
1184
|
+
* Автоматическому входу этот вызов не нужен — клиент делает его сам.
|
|
1185
|
+
*/
|
|
1186
|
+
captchaProvider(options = {}) {
|
|
1187
|
+
return this.http.execute(AUTH_CAPTCHA_PROVIDER, {
|
|
1188
|
+
path: AUTH_PATHS.captchaProvider,
|
|
1189
|
+
skipAuth: true,
|
|
1190
|
+
skipAuthRefresh: true,
|
|
1191
|
+
...options
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
/** Создаёт короткоживущую сессию QR-входа. */
|
|
1195
|
+
async startQrLogin(options = {}) {
|
|
1196
|
+
const cookies = this.#auth.createCookieFlow(false);
|
|
1197
|
+
const result = await this.http.execute(AUTH_QR_START, {
|
|
1198
|
+
path: AUTH_PATHS.qrStart,
|
|
1199
|
+
skipAuth: true,
|
|
1200
|
+
skipAuthRefresh: true,
|
|
1201
|
+
cookieJar: cookies,
|
|
1202
|
+
...options
|
|
1203
|
+
});
|
|
1204
|
+
if (typeof result.qrId === "string" && result.qrId !== "") {
|
|
1205
|
+
const flow = {
|
|
1206
|
+
cookies,
|
|
1207
|
+
expiresAt: void 0
|
|
1208
|
+
};
|
|
1209
|
+
this.#touchQrFlow(flow, result.expiresIn);
|
|
1210
|
+
this.#qrFlows.set(result.qrId, flow);
|
|
1211
|
+
}
|
|
1212
|
+
return result;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* Проверяет QR-сессию и завершает вход после подтверждения на другом устройстве.
|
|
1216
|
+
*
|
|
1217
|
+
* До подтверждения возвращает промежуточный статус. Если сервер потребовал капчу,
|
|
1218
|
+
* повторите вызов с `captcha` — тип берётся из {@link captchaProvider}.
|
|
1219
|
+
* Полученный `accessToken` автоматически сохраняется в клиенте.
|
|
1220
|
+
*/
|
|
1221
|
+
async claimQrLogin(input, options = {}) {
|
|
1222
|
+
const revision = this.#auth.revision();
|
|
1223
|
+
const flow = this.#qrFlow(input.qrId);
|
|
1224
|
+
try {
|
|
1225
|
+
const { captcha, ...rest } = input;
|
|
1226
|
+
const result = await this.http.execute(AUTH_QR_CLAIM, {
|
|
1227
|
+
path: AUTH_PATHS.qrClaim,
|
|
1228
|
+
body: {
|
|
1229
|
+
...rest,
|
|
1230
|
+
...captcha ? captchaBody(captcha) : {}
|
|
1231
|
+
},
|
|
1232
|
+
skipAuth: true,
|
|
1233
|
+
skipAuthRefresh: true,
|
|
1234
|
+
cookieJar: flow.cookies,
|
|
1235
|
+
...options
|
|
1236
|
+
});
|
|
1237
|
+
this.#touchQrFlow(flow, result.expiresIn);
|
|
1238
|
+
if (result.status === QrLoginStatus.Authorized) {
|
|
1239
|
+
await this.#auth.commitAuthFlow(result.accessToken, flow.cookies, revision);
|
|
1240
|
+
this.#qrFlows.delete(input.qrId);
|
|
1241
|
+
} else if (result.status === QrLoginStatus.Rejected) this.#qrFlows.delete(input.qrId);
|
|
1242
|
+
return result;
|
|
1243
|
+
} catch (error) {
|
|
1244
|
+
this.#forgetTerminalQrError(input.qrId, error);
|
|
1245
|
+
throw error;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Слушает состояния QR-входа до закрытия ответа или отмены `signal`.
|
|
1250
|
+
*
|
|
1251
|
+
* Событие `approved` означает, что подтверждение завершено: после него вызовите
|
|
1252
|
+
* {@link claimQrLogin}, чтобы получить и сохранить access token.
|
|
1253
|
+
*/
|
|
1254
|
+
async streamQrLogin(input, onEvent, options = {}) {
|
|
1255
|
+
if (typeof onEvent !== "function") throw new ItdConfigError("onEvent QR-потока должен быть функцией");
|
|
1256
|
+
const flow = this.#qrFlow(input.qrId);
|
|
1257
|
+
try {
|
|
1258
|
+
await openQrLoginStream(this.#connection, flow.cookies, input, async (event) => {
|
|
1259
|
+
this.#touchQrFlow(flow, event.expiresIn);
|
|
1260
|
+
if (event.status === QrLoginStreamStatus.Rejected) this.#qrFlows.delete(input.qrId);
|
|
1261
|
+
await onEvent(event);
|
|
1262
|
+
}, options.signal);
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
this.#forgetTerminalQrError(input.qrId, error);
|
|
1265
|
+
throw error;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
894
1269
|
* Регистрирует аккаунт и запускает подтверждение по коду.
|
|
895
1270
|
*
|
|
896
1271
|
* @returns `flowToken`, который нужно передать в {@link verifyOtp}
|
|
897
1272
|
*/
|
|
898
1273
|
signUp(credentials, options = {}) {
|
|
1274
|
+
const { captcha, ...rest } = credentials;
|
|
899
1275
|
return this.http.execute(AUTH_SIGN_UP, {
|
|
900
1276
|
path: AUTH_PATHS.signUp,
|
|
901
|
-
body:
|
|
1277
|
+
body: {
|
|
1278
|
+
...rest,
|
|
1279
|
+
...captchaBody(captcha)
|
|
1280
|
+
},
|
|
902
1281
|
skipAuth: true,
|
|
903
1282
|
skipAuthRefresh: true,
|
|
904
1283
|
...options
|
|
@@ -915,17 +1294,21 @@ var AuthResource = class extends BaseResource {
|
|
|
915
1294
|
* @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
|
|
916
1295
|
*/
|
|
917
1296
|
async signIn(credentials, options = {}) {
|
|
1297
|
+
const { captcha, ...rest } = credentials;
|
|
918
1298
|
const revision = this.#auth.revision();
|
|
919
1299
|
const cookies = this.#auth.createCookieFlow(false);
|
|
920
1300
|
const result = await this.http.execute(AUTH_SIGN_IN, {
|
|
921
1301
|
path: AUTH_PATHS.signIn,
|
|
922
|
-
body:
|
|
1302
|
+
body: {
|
|
1303
|
+
...rest,
|
|
1304
|
+
...captchaBody(captcha)
|
|
1305
|
+
},
|
|
923
1306
|
skipAuth: true,
|
|
924
1307
|
skipAuthRefresh: true,
|
|
925
1308
|
cookieJar: cookies,
|
|
926
1309
|
...options
|
|
927
1310
|
});
|
|
928
|
-
if (result.status === SignInStatus.Authenticated) await this.#auth.
|
|
1311
|
+
if (result.status === SignInStatus.Authenticated) await this.#auth.commitAuthFlow(result.accessToken, cookies, revision);
|
|
929
1312
|
else this.#auth.commitCookieFlow(cookies, revision);
|
|
930
1313
|
return result;
|
|
931
1314
|
}
|
|
@@ -945,7 +1328,7 @@ var AuthResource = class extends BaseResource {
|
|
|
945
1328
|
cookieJar: cookies,
|
|
946
1329
|
...options
|
|
947
1330
|
});
|
|
948
|
-
await this.#auth.
|
|
1331
|
+
await this.#auth.commitAuthFlow(accessToken, cookies, revision);
|
|
949
1332
|
return accessToken;
|
|
950
1333
|
}
|
|
951
1334
|
/** Отправляет код подтверждения повторно. */
|
|
@@ -1050,7 +1433,10 @@ var AuthResource = class extends BaseResource {
|
|
|
1050
1433
|
forgotPassword(input, options = {}) {
|
|
1051
1434
|
return this.http.execute(AUTH_FORGOT_PASSWORD, {
|
|
1052
1435
|
path: AUTH_PATHS.forgotPassword,
|
|
1053
|
-
body:
|
|
1436
|
+
body: {
|
|
1437
|
+
email: input.email,
|
|
1438
|
+
...captchaBody(input.captcha)
|
|
1439
|
+
},
|
|
1054
1440
|
skipAuth: true,
|
|
1055
1441
|
skipAuthRefresh: true,
|
|
1056
1442
|
...options
|
|
@@ -1081,23 +1467,21 @@ var AuthResource = class extends BaseResource {
|
|
|
1081
1467
|
* ```ts
|
|
1082
1468
|
* await itd.auth.resetPasswordWithOtp({
|
|
1083
1469
|
* email,
|
|
1084
|
-
*
|
|
1470
|
+
* captcha: { type: provider, token, field },
|
|
1085
1471
|
* newPassword,
|
|
1086
1472
|
* getOtp: () => rl.question('Код из письма: '),
|
|
1087
1473
|
* });
|
|
1088
1474
|
* ```
|
|
1089
1475
|
*/
|
|
1090
1476
|
async resetPasswordWithOtp(input, options = {}) {
|
|
1091
|
-
const
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
}, options);
|
|
1095
|
-
const otp = await input.getOtp();
|
|
1477
|
+
const { getOtp, newPassword, ...request } = input;
|
|
1478
|
+
const flowToken = await this.forgotPassword(request, options);
|
|
1479
|
+
const otp = await getOtp();
|
|
1096
1480
|
await this.resetPassword({
|
|
1097
1481
|
email: input.email,
|
|
1098
1482
|
otp,
|
|
1099
1483
|
flowToken,
|
|
1100
|
-
newPassword
|
|
1484
|
+
newPassword
|
|
1101
1485
|
}, options);
|
|
1102
1486
|
}
|
|
1103
1487
|
/**
|
|
@@ -1186,8 +1570,8 @@ function createManagedClient(options, internals) {
|
|
|
1186
1570
|
* const itd = new ItdClient({
|
|
1187
1571
|
* // `auth` не обязателен: когда хранилище уже содержит сессию, токен берётся оттуда,
|
|
1188
1572
|
* // а истёкший продлевается сам. Здесь он нужен на первый запуск.
|
|
1189
|
-
* // Вход по паролю требует токена капчи — см.
|
|
1190
|
-
* auth: { email, password,
|
|
1573
|
+
* // Вход по паролю требует токена капчи — см. CaptchaOptions.
|
|
1574
|
+
* auth: { email, password, captcha: createCaptchaSolver() },
|
|
1191
1575
|
* storage: new FileTokenStorage('./.itd-session.json'),
|
|
1192
1576
|
* rateLimit: { concurrency: 4, rps: 8 },
|
|
1193
1577
|
* });
|
|
@@ -1208,7 +1592,10 @@ var ItdClient = class ItdClient {
|
|
|
1208
1592
|
#auth;
|
|
1209
1593
|
/** Авторизация, сессии и пароли. */
|
|
1210
1594
|
get auth() {
|
|
1211
|
-
this.#auth ??= new AuthResource(this.#runtime.http, {
|
|
1595
|
+
this.#auth ??= new AuthResource(this.#runtime.http, {
|
|
1596
|
+
auth: this.#runtime.auth,
|
|
1597
|
+
connection: this.#runtime.connection()
|
|
1598
|
+
});
|
|
1212
1599
|
return this.#auth;
|
|
1213
1600
|
}
|
|
1214
1601
|
/** Профили, подписки, блокировки, приватность. */
|
|
@@ -1705,7 +2092,9 @@ function assertAccountsActive(accounts, action) {
|
|
|
1705
2092
|
* await accounts.restore();
|
|
1706
2093
|
*
|
|
1707
2094
|
* if (!accounts.has('kiow')) {
|
|
1708
|
-
* accounts.addAccount('kiow', {
|
|
2095
|
+
* accounts.addAccount('kiow', {
|
|
2096
|
+
* auth: { email, password, captcha: createCaptchaSolver() },
|
|
2097
|
+
* });
|
|
1709
2098
|
* }
|
|
1710
2099
|
*
|
|
1711
2100
|
* await accounts.account('kiow').posts.create({ content: 'привет' });
|
|
@@ -1784,7 +2173,9 @@ var ItdAccounts = class ItdAccounts {
|
|
|
1784
2173
|
*
|
|
1785
2174
|
* @example
|
|
1786
2175
|
* ```ts
|
|
1787
|
-
* accounts.addAccount('bot', {
|
|
2176
|
+
* accounts.addAccount('bot', {
|
|
2177
|
+
* auth: { email, password, captcha: createCaptchaSolver() },
|
|
2178
|
+
* });
|
|
1788
2179
|
* accounts.addAccount('reader', { auth: '<accessToken>' });
|
|
1789
2180
|
* accounts.addAccount('через-прокси', { fetch: proxyFetch('socks5://…') });
|
|
1790
2181
|
* ```
|
|
@@ -2117,6 +2508,6 @@ function createAccounts(options = {}) {
|
|
|
2117
2508
|
return new ItdAccounts(options);
|
|
2118
2509
|
}
|
|
2119
2510
|
//#endregion
|
|
2120
|
-
export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AccessType, AttachmentType, BUCKET_LIMITS, CommentSort, DEFAULT_BASE_URL, DEFAULT_RATE_LIMIT_BUCKET, EventChannel, EventChannelStatus, EventComposer, EventRouter, FeedTab, FileTransferMode, IMAGE_MIME_TYPES, IncidentKind, InteractionType, ItdAbortError, ItdAccounts, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdFileError, ItdFileErrorReason, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdServerError, ItdStateError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, MemoryKeyValueStore, MemoryMultiTokenStorage, MemoryShopOrderAccessStorage, MemoryTokenStorage, NotificationEvents, NotificationEventsClient, NotificationEventsTransport, NotificationType, NotificationUpdateOrigin, NotificationUpdateType, OPERATIONS, PaginationMode, Paginator, RateLimitPacing, ReportReason, ReportTargetType, RetrySafety, RuntimeMode, STATUS_SERVICE, ServiceState, ShopConsentKind, ShopOrderStatus, ShopProductCategory, ShopProductStatus, SignInStatus, SpanRenderFormat, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, WebSocketTransport, autoSpans, canonicalNotificationType, comment, createAccounts, createClient, createKeyValueStore, createMultiTokenStorage, createNotificationEventsClient, createRecordKeyValueStore, createShopFeature, createShopOrderAccessStorage, createTokenStorage, formatNotificationText, fromStream, fromUrl, isBuilder, isBuiltInOperationId, isEnumerableKeyValueStore, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdFileError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdStateError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, markup, normalizeNotification, operationBucket, operationMethod, operationRetrySafety, parseHtml, parseMarkdown, poll, post, renderSpans, report, resolveNotificationUrl, runEventMiddleware, scopedTokenStorage, statusDays, systemClock, toDate, utcStampToIso, withCodec, withNamespace };
|
|
2511
|
+
export { ALLOWED_MIME_TYPES, AUDIO_MIME_TYPES, AccessType, AttachmentType, BUCKET_LIMITS, CAPTCHA_FIELDS, CaptchaChoice, CaptchaField, CaptchaType, CommentSort, DEFAULT_BASE_URL, DEFAULT_RATE_LIMIT_BUCKET, EventChannel, EventChannelStatus, EventComposer, EventRouter, FeedTab, FileTransferMode, IMAGE_MIME_TYPES, IncidentKind, InteractionType, ItdAbortError, ItdAccounts, ItdApiError, ItdApiErrorKind, ItdAuthError, ItdClient, ItdConfigError, ItdConflictError, ItdError, ItdErrorCode, ItdErrorKind, ItdFileError, ItdFileErrorReason, ItdForbiddenError, ItdNetworkError, ItdNotFoundError, ItdPhoneVerificationError, ItdRateLimitError, ItdServerError, ItdStateError, ItdTimeoutError, ItdValidationError, LIBRARY_VERSION, LikesVisibility, MemoryKeyValueStore, MemoryMultiTokenStorage, MemoryShopOrderAccessStorage, MemoryTokenStorage, NotificationEvents, NotificationEventsClient, NotificationEventsTransport, NotificationType, NotificationUpdateOrigin, NotificationUpdateType, OPERATIONS, PaginationMode, Paginator, QrLoginStatus, QrLoginStreamStatus, RateLimitPacing, ReportReason, ReportTargetType, RetrySafety, RuntimeMode, STATUS_SERVICE, ServiceState, ShopConsentKind, ShopOrderStatus, ShopProductCategory, ShopProductStatus, SignInStatus, SpanRenderFormat, SpanType, TURNSTILE_SITE_KEY, UnauthorizedStreamError, VIDEO_MIME_TYPES, ViewReason, ViewSource, WallAccess, WebSocketTransport, autoSpans, canonicalNotificationType, comment, createAccounts, createClient, createKeyValueStore, createMultiTokenStorage, createNotificationEventsClient, createRecordKeyValueStore, createShopFeature, createShopOrderAccessStorage, createTokenStorage, formatNotificationText, fromStream, fromUrl, isBuilder, isBuiltInOperationId, isEnumerableKeyValueStore, isItdApiError, isItdAuthError, isItdConflictError, isItdError, isItdFileError, isItdForbiddenError, isItdNotFoundError, isItdPhoneVerificationError, isItdRateLimitError, isItdServerError, isItdStateError, isItdValidationError, isKnownNotificationType, isMyProfile, mapPage, markup, normalizeNotification, operationBucket, operationMethod, operationRetrySafety, parseHtml, parseMarkdown, poll, post, renderSpans, report, resolveNotificationUrl, runEventMiddleware, scopedTokenStorage, statusDays, systemClock, toDate, utcStampToIso, withCodec, withNamespace };
|
|
2121
2512
|
|
|
2122
2513
|
//# sourceMappingURL=index.js.map
|