@torrow/sdk-auth 0.0.0-stage → 0.7.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/dist/index.js ADDED
@@ -0,0 +1,1317 @@
1
+ // ../../src/auth-session/app-credentials.ts
2
+ var DEFAULT_APP_CREDENTIALS = {
3
+ clientId: "sdk.torrow.net",
4
+ clientSecret: "4YM1qYq87faSXaFqXTBcnJrn/jj0AOXl6tTsfubLyCE="
5
+ };
6
+ function resolveAppCredentials(input) {
7
+ return input ?? DEFAULT_APP_CREDENTIALS;
8
+ }
9
+ function appCredentialsToAuthIdentityBasic(appCredentials) {
10
+ const clientId = appCredentials?.clientId?.trim();
11
+ const clientSecret = appCredentials?.clientSecret?.trim();
12
+ if (!clientId || !clientSecret) return void 0;
13
+ return { username: clientId, password: clientSecret };
14
+ }
15
+
16
+ // ../../src/auth-session/otp-flow.ts
17
+ import {
18
+ isAuthCaptchaType,
19
+ isDeviceType,
20
+ isSendCodeAccountType,
21
+ isVerificationMethod,
22
+ TokenStatusValues as TokenStatusValues2,
23
+ VerificationMethodValues
24
+ } from "@torrow/sdk-client";
25
+
26
+ // ../../src/auth-session/identity-client.ts
27
+ import { TorrowClient } from "@torrow/sdk-client";
28
+ function createIdentityClient(options) {
29
+ const authIdentityBasic = appCredentialsToAuthIdentityBasic(
30
+ resolveAppCredentials(options.appCredentials)
31
+ );
32
+ if (!authIdentityBasic) {
33
+ throw new Error(
34
+ "TokenSession: \u043D\u0435\u043F\u043E\u043B\u043D\u0430\u044F appCredentials (\u043D\u0443\u0436\u043D\u044B \u043D\u0435\u043F\u0443\u0441\u0442\u044B\u0435 clientId \u0438 clientSecret). \u0411\u0435\u0437 \u043E\u043F\u0446\u0438\u0438 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F DEFAULT_APP_CREDENTIALS."
35
+ );
36
+ }
37
+ return new TorrowClient({
38
+ baseUrl: options.baseUrl,
39
+ fetch: options.fetch,
40
+ defaultHeaders: options.defaultHeaders,
41
+ userAgent: options.userAgent,
42
+ authIdentityBasic
43
+ });
44
+ }
45
+
46
+ // ../../src/auth-session/auth-protocol.ts
47
+ import {
48
+ DefaultAuthScopes,
49
+ isTokenStatus,
50
+ TokenStatusValues
51
+ } from "@torrow/sdk-client";
52
+ function buildOtpSendRequest(input) {
53
+ const accountName = input.accountName.trim();
54
+ if (!accountName) throw new Error("runOtpLogin: accountName is empty");
55
+ return {
56
+ accountName,
57
+ type: input.type,
58
+ language: input.language?.trim() || "ru",
59
+ regionStorageId: typeof input.regionStorageId === "number" && Number.isFinite(input.regionStorageId) ? input.regionStorageId : 70,
60
+ ...input.captcha?.trim() ? { captcha: input.captcha.trim() } : {},
61
+ ...input.captchaType ? { captchaType: input.captchaType } : {},
62
+ ...input.inviteSessionId?.trim() ? { inviteSessionId: input.inviteSessionId.trim() } : {},
63
+ ...input.preferredMethod ? { preferredMethod: input.preferredMethod } : {},
64
+ ...input.supportedMethods?.length ? { supportedMethods: input.supportedMethods } : {}
65
+ };
66
+ }
67
+ function sendOtpCode(client, input, signal) {
68
+ return client.auth.sendCode(buildOtpSendRequest(input), { signal });
69
+ }
70
+ function requestAuthToken(client, input, signal) {
71
+ return client.auth.requestToken(
72
+ {
73
+ sessionId: input.sessionId,
74
+ code: input.code,
75
+ deviceInfo: input.deviceInfo,
76
+ scopes: input.scopes?.length ? input.scopes : [...DefaultAuthScopes],
77
+ ...input.splitTokens !== void 0 ? { splitTokens: input.splitTokens } : {}
78
+ },
79
+ { signal }
80
+ );
81
+ }
82
+ function tokenTransition(status, legacy = false) {
83
+ if (legacy && status == null) return TokenStatusValues.TokenReady;
84
+ return typeof status === "string" && isTokenStatus(status) ? status : void 0;
85
+ }
86
+ function tokenResponseFields(o) {
87
+ return {
88
+ access: o.accessToken ?? o.AccessToken ?? o.access_token,
89
+ refresh: o.refreshToken ?? o.RefreshToken ?? o.refresh_token,
90
+ expiration: o.expiration ?? o.Expiration ?? o.expires_in,
91
+ status: o.status ?? o.Status
92
+ };
93
+ }
94
+
95
+ // ../../src/auth-session/otp-flow.ts
96
+ var OtpPhaseValues = /* @__PURE__ */ ((OtpPhaseValues2) => {
97
+ OtpPhaseValues2["AwaitingCode"] = "AwaitingCode";
98
+ OtpPhaseValues2["Pending"] = "Pending";
99
+ return OtpPhaseValues2;
100
+ })(OtpPhaseValues || {});
101
+ var OtpFlowErrorCodeValues = /* @__PURE__ */ ((OtpFlowErrorCodeValues2) => {
102
+ OtpFlowErrorCodeValues2["InvalidArguments"] = "InvalidArguments";
103
+ OtpFlowErrorCodeValues2["InvalidChallenge"] = "InvalidChallenge";
104
+ OtpFlowErrorCodeValues2["InvalidResponse"] = "InvalidResponse";
105
+ OtpFlowErrorCodeValues2["UnsupportedMethod"] = "UnsupportedMethod";
106
+ return OtpFlowErrorCodeValues2;
107
+ })(OtpFlowErrorCodeValues || {});
108
+ var OtpFlowError = class extends Error {
109
+ constructor(code, cleanup) {
110
+ super(`OTP flow: ${code}`);
111
+ this.code = code;
112
+ this.#cleanup = cleanup ? Object.freeze({ ...cleanup }) : void 0;
113
+ }
114
+ code;
115
+ name = "OtpFlowError";
116
+ #cleanup;
117
+ /** Секрет доступен явно; не является перечисляемым полем исключения. */
118
+ get cleanup() {
119
+ return this.#cleanup;
120
+ }
121
+ };
122
+ function createOtpFlow(options) {
123
+ if (!isRecord(options) || !isRecord(options.deviceInfo) || typeof options.deviceInfo.deviceId !== "string" || !options.deviceInfo.deviceId.trim() || typeof options.deviceInfo.deviceType !== "string" || !isDeviceType(options.deviceInfo.deviceType) || options.scopes !== void 0 && (!Array.isArray(options.scopes) || !options.scopes.every(
124
+ (scope) => typeof scope === "string" && scope.trim()
125
+ )) || options.splitTokens !== void 0 && typeof options.splitTokens !== "boolean")
126
+ throw new OtpFlowError("InvalidArguments" /* InvalidArguments */);
127
+ const client = createIdentityClient(options);
128
+ const deviceInfo = { ...options.deviceInfo };
129
+ const scopes = options.scopes ? [...options.scopes] : void 0;
130
+ const splitTokens = options.splitTokens;
131
+ return {
132
+ async start(input, requestOptions) {
133
+ validateStartInput(input);
134
+ requestOptions?.signal?.throwIfAborted();
135
+ const startedAt = Date.now();
136
+ const response = await sendOtpCode(
137
+ client,
138
+ {
139
+ ...input,
140
+ accountName: input.account.accountName,
141
+ type: input.account.accountType
142
+ },
143
+ requestOptions?.signal
144
+ );
145
+ if (!isRecord(response) || typeof response.sessionId !== "string" || !response.sessionId.trim())
146
+ throw new OtpFlowError("InvalidResponse" /* InvalidResponse */);
147
+ if (!supportedMethod(response.usedMethod, input.account.accountType))
148
+ throw new OtpFlowError("UnsupportedMethod" /* UnsupportedMethod */);
149
+ const ttl = response.sessionLifeTimeInSeconds ?? 180;
150
+ if (!positive(ttl)) throw new OtpFlowError("InvalidResponse" /* InvalidResponse */);
151
+ const expiresAtMs = startedAt + ttl * 1e3;
152
+ if (!timestamp(expiresAtMs) || expiresAtMs <= Date.now())
153
+ throw new OtpFlowError("InvalidResponse" /* InvalidResponse */);
154
+ const mobile = response.usedMethod === VerificationMethodValues.MtsMobileId;
155
+ const nextSendAtMs = response.nextAvailableUsageTime instanceof Date ? response.nextAvailableUsageTime.getTime() : typeof response.nextAvailableUsageTime === "string" ? Date.parse(response.nextAvailableUsageTime) : void 0;
156
+ return {
157
+ sessionId: response.sessionId.trim(),
158
+ accountType: input.account.accountType,
159
+ usedMethod: response.usedMethod,
160
+ phase: mobile ? "Pending" /* Pending */ : "AwaitingCode" /* AwaitingCode */,
161
+ expiresAtMs,
162
+ codeLength: codeLength(response.codeLength) ? response.codeLength : input.account.accountType === "Email" && !mobile ? 6 : 4,
163
+ ...mobile ? {
164
+ pollIntervalSeconds: positive(response.pollIntervalSeconds) ? response.pollIntervalSeconds : 3
165
+ } : {},
166
+ ...attempts(response.attemptCountToUseSession) ? { attemptsRemaining: response.attemptCountToUseSession } : {},
167
+ ...timestamp(nextSendAtMs) ? { nextSendAtMs } : {},
168
+ ...methods(response.availableMethods) ? { availableMethods: [...response.availableMethods] } : {}
169
+ };
170
+ },
171
+ async complete(input, requestOptions) {
172
+ if (!isRecord(input)) throw new OtpFlowError("InvalidArguments" /* InvalidArguments */);
173
+ requestOptions?.signal?.throwIfAborted();
174
+ const continuation = validateChallenge(input.challenge);
175
+ if (continuation.phase === "Pending" /* Pending */ && input.code !== void 0 || continuation.phase === "AwaitingCode" /* AwaitingCode */ && (typeof input.code !== "string" || !input.code.trim()))
176
+ throw new OtpFlowError("InvalidArguments" /* InvalidArguments */);
177
+ const raw = await requestAuthToken(
178
+ client,
179
+ {
180
+ sessionId: continuation.sessionId,
181
+ code: input.code?.trim() ?? "",
182
+ deviceInfo,
183
+ scopes,
184
+ splitTokens
185
+ },
186
+ requestOptions?.signal
187
+ );
188
+ if (!isRecord(raw)) throw new OtpFlowError("InvalidResponse" /* InvalidResponse */);
189
+ const fields = tokenResponseFields(raw);
190
+ const refreshToken = typeof fields.refresh === "string" && fields.refresh.trim() ? fields.refresh.trim() : void 0;
191
+ const cleanup = refreshToken ? { refreshToken } : void 0;
192
+ const status = tokenTransition(
193
+ typeof fields.status === "string" ? fields.status.trim() : fields.status
194
+ );
195
+ if (!status || fields.access != null && typeof fields.access !== "string" || fields.refresh != null && typeof fields.refresh !== "string" || fields.expiration != null && (typeof fields.expiration !== "number" || !Number.isFinite(fields.expiration) || fields.expiration < 0))
196
+ throw new OtpFlowError("InvalidResponse" /* InvalidResponse */, cleanup);
197
+ if (status === TokenStatusValues2.Pending)
198
+ return {
199
+ status,
200
+ cleanup,
201
+ challenge: {
202
+ ...continuation,
203
+ phase: "Pending" /* Pending */,
204
+ usedMethod: VerificationMethodValues.MtsMobileId,
205
+ pollIntervalSeconds: continuation.pollIntervalSeconds ?? 3
206
+ }
207
+ };
208
+ if (status === TokenStatusValues2.AwaitingSmsCode) {
209
+ const { pollIntervalSeconds: _interval, ...challenge } = continuation;
210
+ return {
211
+ status,
212
+ cleanup,
213
+ challenge: {
214
+ ...challenge,
215
+ phase: "AwaitingCode" /* AwaitingCode */,
216
+ usedMethod: VerificationMethodValues.Sms,
217
+ codeLength: challenge.codeLength || 4
218
+ }
219
+ };
220
+ }
221
+ if (status === TokenStatusValues2.Failed) return { status, cleanup };
222
+ if (typeof fields.access !== "string" || !fields.access.trim())
223
+ throw new OtpFlowError("InvalidResponse" /* InvalidResponse */, cleanup);
224
+ return {
225
+ status,
226
+ tokens: {
227
+ status,
228
+ accessToken: fields.access.trim(),
229
+ ...refreshToken ? { refreshToken } : {},
230
+ ...typeof fields.expiration === "number" ? { expiration: fields.expiration } : {}
231
+ }
232
+ };
233
+ }
234
+ };
235
+ }
236
+ function isRecord(value) {
237
+ return value !== null && typeof value === "object" && !Array.isArray(value);
238
+ }
239
+ function positive(value) {
240
+ return typeof value === "number" && Number.isFinite(value) && value > 0;
241
+ }
242
+ function codeLength(value) {
243
+ return positive(value) && Number.isInteger(value) && value <= 32;
244
+ }
245
+ function attempts(value) {
246
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
247
+ }
248
+ function timestamp(value) {
249
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && value <= 864e13;
250
+ }
251
+ function methods(value) {
252
+ return Array.isArray(value) && value.every(
253
+ (method) => typeof method === "string" && isVerificationMethod(method)
254
+ );
255
+ }
256
+ function supportedMethod(value, accountType) {
257
+ return typeof value === "string" && isVerificationMethod(value) && (value !== VerificationMethodValues.UnknownMethod || accountType === "Email");
258
+ }
259
+ function validateStartInput(input) {
260
+ if (!isRecord(input) || !isRecord(input.account) || typeof input.account.accountType !== "string" || !isSendCodeAccountType(input.account.accountType) || typeof input.account.accountName !== "string" || !input.account.accountName.trim())
261
+ throw new OtpFlowError("InvalidArguments" /* InvalidArguments */);
262
+ for (const field of ["language", "captcha", "inviteSessionId"]) {
263
+ if (input[field] !== void 0 && typeof input[field] !== "string")
264
+ throw new OtpFlowError("InvalidArguments" /* InvalidArguments */);
265
+ }
266
+ if (input.regionStorageId !== void 0 && !attempts(input.regionStorageId) || input.captchaType !== void 0 && (typeof input.captchaType !== "string" || !isAuthCaptchaType(input.captchaType)) || input.preferredMethod !== void 0 && (typeof input.preferredMethod !== "string" || !isVerificationMethod(input.preferredMethod)) || input.supportedMethods !== void 0 && !methods(input.supportedMethods))
267
+ throw new OtpFlowError("InvalidArguments" /* InvalidArguments */);
268
+ }
269
+ function validateChallenge(value) {
270
+ if (!isRecord(value) || typeof value.sessionId !== "string" || !value.sessionId.trim() || typeof value.accountType !== "string" || !isSendCodeAccountType(value.accountType) || !supportedMethod(value.usedMethod, value.accountType) || value.phase !== "Pending" /* Pending */ && value.phase !== "AwaitingCode" /* AwaitingCode */ || !timestamp(value.expiresAtMs) || value.expiresAtMs <= Date.now() || value.phase === "Pending" /* Pending */ !== (value.usedMethod === VerificationMethodValues.MtsMobileId))
271
+ throw new OtpFlowError("InvalidChallenge" /* InvalidChallenge */);
272
+ if (value.codeLength !== void 0 && !codeLength(value.codeLength) || value.pollIntervalSeconds !== void 0 && !positive(value.pollIntervalSeconds) || value.attemptsRemaining !== void 0 && !attempts(value.attemptsRemaining) || value.nextSendAtMs !== void 0 && !timestamp(value.nextSendAtMs) || value.availableMethods !== void 0 && !methods(value.availableMethods))
273
+ throw new OtpFlowError("InvalidChallenge" /* InvalidChallenge */);
274
+ return {
275
+ sessionId: value.sessionId.trim(),
276
+ accountType: value.accountType,
277
+ usedMethod: value.usedMethod,
278
+ phase: value.phase,
279
+ expiresAtMs: value.expiresAtMs,
280
+ ...codeLength(value.codeLength) ? { codeLength: value.codeLength } : {},
281
+ ...positive(value.pollIntervalSeconds) ? { pollIntervalSeconds: value.pollIntervalSeconds } : {},
282
+ ...attempts(value.attemptsRemaining) ? { attemptsRemaining: value.attemptsRemaining } : {},
283
+ ...timestamp(value.nextSendAtMs) ? { nextSendAtMs: value.nextSendAtMs } : {},
284
+ ...methods(value.availableMethods) ? { availableMethods: [...value.availableMethods] } : {}
285
+ };
286
+ }
287
+
288
+ // ../../src/auth-session/token-storage.ts
289
+ var MemoryTokenStorage = class {
290
+ snapshot;
291
+ constructor(initial) {
292
+ this.snapshot = { ...initial ?? {} };
293
+ }
294
+ async load() {
295
+ return { ...this.snapshot };
296
+ }
297
+ async save(snapshot) {
298
+ this.snapshot = {
299
+ accessToken: snapshot.accessToken?.trim() || void 0,
300
+ refreshToken: snapshot.refreshToken?.trim() || void 0,
301
+ accessExpiresAtMs: snapshot.accessExpiresAtMs || void 0,
302
+ deviceId: snapshot.deviceId?.trim() || void 0
303
+ };
304
+ }
305
+ };
306
+
307
+ // ../../src/auth-session/local-storage-token-storage.ts
308
+ function resolveLocalStorage() {
309
+ const g = globalThis;
310
+ if (!g.localStorage || typeof g.localStorage.getItem !== "function") {
311
+ throw new Error(
312
+ "LocalStorageTokenStorage: globalThis.localStorage is not available in this environment"
313
+ );
314
+ }
315
+ return g.localStorage;
316
+ }
317
+ var LocalStorageTokenStorage = class {
318
+ constructor(key, store) {
319
+ this.key = key;
320
+ const trimmed = key.trim();
321
+ if (!trimmed) {
322
+ throw new Error("LocalStorageTokenStorage: empty key");
323
+ }
324
+ this.key = trimmed;
325
+ this.store = store ?? resolveLocalStorage();
326
+ }
327
+ key;
328
+ store;
329
+ async load() {
330
+ try {
331
+ const raw = this.store.getItem(this.key);
332
+ if (!raw?.trim()) return {};
333
+ const parsed = JSON.parse(raw);
334
+ if (parsed === null || typeof parsed !== "object") return {};
335
+ const o = parsed;
336
+ return {
337
+ accessToken: typeof o.accessToken === "string" ? o.accessToken : void 0,
338
+ refreshToken: typeof o.refreshToken === "string" ? o.refreshToken : void 0,
339
+ accessExpiresAtMs: typeof o.accessExpiresAtMs === "number" ? o.accessExpiresAtMs : void 0,
340
+ deviceId: typeof o.deviceId === "string" ? o.deviceId : void 0
341
+ };
342
+ } catch {
343
+ return {};
344
+ }
345
+ }
346
+ async save(snapshot) {
347
+ const toSave = {
348
+ accessToken: snapshot.accessToken?.trim() || void 0,
349
+ refreshToken: snapshot.refreshToken?.trim() || void 0,
350
+ accessExpiresAtMs: snapshot.accessExpiresAtMs || void 0,
351
+ deviceId: snapshot.deviceId?.trim() || void 0
352
+ };
353
+ this.store.setItem(this.key, JSON.stringify(toSave));
354
+ }
355
+ };
356
+
357
+ // ../../src/auth-session/auth-session-not-ready.ts
358
+ import { TorrowHttpError } from "@torrow/sdk-client";
359
+ var AuthSessionNotReadyError = class extends Error {
360
+ name = "AuthSessionNotReadyError";
361
+ cause;
362
+ constructor(message = "Auth session is not ready yet", cause) {
363
+ super(message);
364
+ this.cause = cause;
365
+ }
366
+ };
367
+ function isAuthSessionNotReadyYet(e) {
368
+ if (e instanceof AuthSessionNotReadyError) return true;
369
+ if (!(e instanceof TorrowHttpError)) return false;
370
+ const text = [
371
+ e.message,
372
+ e.apiErrorName,
373
+ typeof e.body === "string" ? e.body : JSON.stringify(e.body ?? "")
374
+ ].join(" ").toLowerCase();
375
+ return text.includes("session is not ready") || text.includes("unauthorized_client");
376
+ }
377
+
378
+ // ../../src/auth-session/TokenSession.ts
379
+ import {
380
+ DefaultAuthScopes as DefaultAuthScopes2,
381
+ TorrowClient as TorrowClient2,
382
+ TorrowHttpError as TorrowHttpError2,
383
+ DeviceTypeValues,
384
+ TokenStatusValues as TokenStatusValues3
385
+ } from "@torrow/sdk-client";
386
+
387
+ // ../../src/auth-session/fetch-with-401-retry.ts
388
+ function requestUrl(input) {
389
+ if (typeof input === "string") return input;
390
+ if (input instanceof URL) return input.href;
391
+ return input.url;
392
+ }
393
+ function isAuthIdentityUrl(url) {
394
+ return url.includes("/api/v1/auth/");
395
+ }
396
+ function createFetchWith401Retry(options) {
397
+ const baseFetch = options.fetch ?? globalThis.fetch.bind(globalThis);
398
+ const wrapped = async (input, init) => {
399
+ const first = await baseFetch(input, init);
400
+ if (first.status !== 401) return first;
401
+ const url = requestUrl(input);
402
+ if (isAuthIdentityUrl(url)) return first;
403
+ await options.refresh();
404
+ const token = await options.getAccessToken();
405
+ const headers = new Headers(init?.headers);
406
+ if (token) {
407
+ headers.set("Authorization", `Bearer ${token}`);
408
+ } else {
409
+ headers.delete("Authorization");
410
+ }
411
+ return baseFetch(input, { ...init, headers });
412
+ };
413
+ return wrapped;
414
+ }
415
+
416
+ // ../../src/auth-session/merge-tokens.ts
417
+ import { isTokenRespReadyStatus } from "@torrow/sdk-client";
418
+ function normalizeTokenResp(raw) {
419
+ if (raw === null || raw === void 0) return {};
420
+ if (typeof raw === "string") {
421
+ const t = raw.trim();
422
+ return t ? { accessToken: t } : {};
423
+ }
424
+ if (typeof raw !== "object") return {};
425
+ const o = raw;
426
+ const { access, refresh, expiration: expRaw, status: statusRaw } = tokenResponseFields(o);
427
+ const out = {};
428
+ if (typeof access === "string" && access.trim()) out.accessToken = access.trim();
429
+ if (typeof refresh === "string" && refresh.trim()) out.refreshToken = refresh.trim();
430
+ if (typeof expRaw === "number" && Number.isFinite(expRaw)) out.expiration = expRaw;
431
+ if (typeof statusRaw === "string" && statusRaw.trim()) {
432
+ out.status = statusRaw.trim();
433
+ }
434
+ return out;
435
+ }
436
+ function decodeJwtJsonSegment(segment) {
437
+ try {
438
+ const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
439
+ const pad = b64.length % 4 === 0 ? "" : "=".repeat(4 - b64.length % 4);
440
+ const binary = atob(b64 + pad);
441
+ const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
442
+ return JSON.parse(new TextDecoder().decode(bytes));
443
+ } catch {
444
+ return void 0;
445
+ }
446
+ }
447
+ function accessExpiresAtMsFromJwt(accessToken) {
448
+ const parts = accessToken.split(".");
449
+ if (parts.length < 2) return void 0;
450
+ if (decodeJwtJsonSegment(parts[0]) === void 0) return void 0;
451
+ const payload = decodeJwtJsonSegment(parts[1]);
452
+ if (payload === null || typeof payload !== "object") return void 0;
453
+ const exp = payload.exp;
454
+ if (typeof exp !== "number" || !Number.isFinite(exp)) return void 0;
455
+ return exp * 1e3;
456
+ }
457
+ function accessTokenLooksLikeJwt(accessToken) {
458
+ return accessToken.split(".").length >= 3;
459
+ }
460
+ function mergeAuthTokens(prev, tokens, opts) {
461
+ const replace = opts?.replace === true;
462
+ const nextAccess = (replace ? tokens.accessToken?.trim() || "" : tokens.accessToken?.trim() || prev.accessToken?.trim() || "").trim();
463
+ const refreshToken = replace ? tokens.refreshToken?.trim() || "" : tokens.refreshToken?.trim() || prev.refreshToken?.trim() || "";
464
+ const ttlSec = tokens.expiration ?? 0;
465
+ let accessExpiresAtMs;
466
+ if (ttlSec > 0) {
467
+ accessExpiresAtMs = Date.now() + ttlSec * 1e3;
468
+ } else {
469
+ const keepAfter = opts?.keepPrevTtlIfExpiresAfterMs ?? Date.now();
470
+ const jwtExp = nextAccess ? accessExpiresAtMsFromJwt(nextAccess) : void 0;
471
+ if (typeof jwtExp === "number") {
472
+ accessExpiresAtMs = jwtExp;
473
+ } else if (!replace && typeof prev.accessExpiresAtMs === "number" && prev.accessExpiresAtMs > keepAfter) {
474
+ accessExpiresAtMs = prev.accessExpiresAtMs;
475
+ } else {
476
+ accessExpiresAtMs = void 0;
477
+ }
478
+ }
479
+ return {
480
+ ...prev,
481
+ accessToken: nextAccess || void 0,
482
+ refreshToken: refreshToken || void 0,
483
+ accessExpiresAtMs
484
+ };
485
+ }
486
+ function assertTokenRespReadyForStorage(tokens, context) {
487
+ const status = tokens.status;
488
+ if (!isTokenRespReadyStatus(status)) {
489
+ throw new Error(
490
+ `${context}: TokenResp.status=${status} (\u043E\u0436\u0438\u0434\u0430\u043B\u0441\u044F TokenReady). \u0422\u043E\u043A\u0435\u043D\u044B \u043D\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u0435\u043C.`
491
+ );
492
+ }
493
+ if (!tokens.accessToken?.trim() && !tokens.refreshToken?.trim()) {
494
+ throw new Error(`${context}: TokenResp \u0431\u0435\u0437 accessToken \u0438 refreshToken.`);
495
+ }
496
+ return tokens;
497
+ }
498
+
499
+ // ../../src/auth-session/TokenSession.ts
500
+ var DEFAULT_SKEW_MS = 3e4;
501
+ var SESSION_CLOSED_MESSAGE = "TokenSession: session closed (logout)";
502
+ function defaultOnRefreshFailure(err) {
503
+ if (!(err instanceof TorrowHttpError2)) return "keep";
504
+ const s = err.status;
505
+ if (s === 429 || s === 502 || s === 503 || s === 504) return "keep";
506
+ return "clear";
507
+ }
508
+ var TokenSession = class _TokenSession {
509
+ constructor(opts) {
510
+ this.opts = opts;
511
+ }
512
+ opts;
513
+ tokenSnapshot = {};
514
+ ensureFreshMutex = Promise.resolve();
515
+ _client;
516
+ statusListeners = /* @__PURE__ */ new Set();
517
+ closed = false;
518
+ /** Set in refreshWithFailurePolicy catch; not cleared on success (see public refresh). */
519
+ lastRefreshError;
520
+ static async open(opts) {
521
+ const session = new _TokenSession(opts);
522
+ session.tokenSnapshot = await opts.tokenStorage.load();
523
+ if (!session.tokenSnapshot.deviceId?.trim()) {
524
+ const di = session.resolveDeviceInfo();
525
+ const id = di.deviceId?.trim();
526
+ if (id) {
527
+ session.tokenSnapshot = { ...session.tokenSnapshot, deviceId: id };
528
+ await session.persist(false);
529
+ }
530
+ }
531
+ session._client = session.buildClient();
532
+ if (!opts.skipEnsureFreshOnOpen) {
533
+ await session.ensureFresh();
534
+ }
535
+ return session;
536
+ }
537
+ /** Persist, переданный в `TokenSession.open` / `TorrowAuth.*`. */
538
+ get tokenStorage() {
539
+ return this.opts.tokenStorage;
540
+ }
541
+ get client() {
542
+ if (!this._client) {
543
+ throw new Error("TokenSession: client not initialized (call TokenSession.open)");
544
+ }
545
+ return this._client;
546
+ }
547
+ /** Текущий статус сессии. */
548
+ get status() {
549
+ return this.computeStatus();
550
+ }
551
+ /** Подписка на смену статуса; возвращает unsubscribe. Сразу вызывает listener с текущим статусом. */
552
+ onStatusChange(listener) {
553
+ this.statusListeners.add(listener);
554
+ try {
555
+ listener(this.computeStatus());
556
+ } catch {
557
+ }
558
+ return () => {
559
+ this.statusListeners.delete(listener);
560
+ };
561
+ }
562
+ /** Текущий снимок токенов сессии (копия {@link AuthTokenSnapshot}). */
563
+ getTokenSnapshot() {
564
+ return { ...this.tokenSnapshot };
565
+ }
566
+ /** `deviceInfo` из options open или fallback Desktop + deviceId из tokenSnapshot. */
567
+ getDeviceInfo() {
568
+ return this.resolveDeviceInfo();
569
+ }
570
+ async getAccessToken() {
571
+ if (this.closed) return void 0;
572
+ await this.ensureFresh();
573
+ return this.tokenSnapshot.accessToken?.trim() || void 0;
574
+ }
575
+ /**
576
+ * Сохранить ответ `requestToken` / `refresh` (только TokenReady).
577
+ * Сериализован с `ensureFresh` / `logout` через mutex. Дефолт merge намеренно
578
+ * (refresh-shaped); логин — `{ replace: true }`. После `logout` — новая сессия.
579
+ */
580
+ async applyTokenResp(tokens, opts) {
581
+ await this.runExclusive(() => this.applyTokenRespUnlocked(tokens, opts));
582
+ }
583
+ async clearTokens() {
584
+ await this.runExclusive(() => this.clearTokensUnlocked());
585
+ }
586
+ /**
587
+ * Revoke refresh (если есть) + очистка storage.
588
+ * Единственное публичное имя выхода из сессии.
589
+ */
590
+ async logout() {
591
+ await this.runExclusive(async () => {
592
+ this.closed = true;
593
+ const refresh = this.tokenSnapshot.refreshToken?.trim();
594
+ if (refresh) {
595
+ try {
596
+ await this.identityClient().auth.revoke({ token: refresh });
597
+ } catch {
598
+ }
599
+ }
600
+ await this.clearTokensUnlocked();
601
+ });
602
+ }
603
+ /**
604
+ * Получить токены по **id auth-сессии** (`sessionId` из `sendCode` / `createSession`)
605
+ * и **OTP-коду доступа**, затем сохранить их в storage сессии.
606
+ *
607
+ * Один вызов `POST /api/v1/auth/token` (`requestToken`) с `{ sessionId, code }`.
608
+ * При успехе (`TokenReady`) — persist; если сессия ещё не готова —
609
+ * {@link AuthSessionNotReadyError} (поллинг для deep-link — внутри `TorrowAuth.loginWithLink`).
610
+ *
611
+ * HTTP вне mutex; persist сериализован через {@link applyTokenResp} (не оборачивать
612
+ * весь метод в `runExclusive` — вложенный mutex с публичным `applyTokenResp`).
613
+ */
614
+ async requestTokenWithCode(args) {
615
+ this.assertNotClosedForWrite();
616
+ const sessionId = args.sessionId.trim();
617
+ const code = args.code.trim();
618
+ if (!sessionId || !code) {
619
+ throw new Error("TokenSession.requestTokenWithCode: sessionId and code are required");
620
+ }
621
+ const scopes = args.scopes && args.scopes.length > 0 ? args.scopes : [...DefaultAuthScopes2];
622
+ const deviceInfo = this.resolveDeviceInfo();
623
+ let resp;
624
+ try {
625
+ resp = normalizeTokenResp(
626
+ await requestAuthToken(
627
+ this.identityClient(),
628
+ { sessionId, code, scopes, deviceInfo },
629
+ args.signal
630
+ )
631
+ );
632
+ } catch (e) {
633
+ if (isAuthSessionNotReadyYet(e)) {
634
+ throw new AuthSessionNotReadyError("Auth session is not ready yet", e);
635
+ }
636
+ throw e;
637
+ }
638
+ if (tokenTransition(resp.status, true) !== TokenStatusValues3.TokenReady) {
639
+ throw new AuthSessionNotReadyError(
640
+ `TokenResp.status=${resp.status} (\u043E\u0436\u0438\u0434\u0430\u043B\u0441\u044F TokenReady)`
641
+ );
642
+ }
643
+ const ready = assertTokenRespReadyForStorage(resp, "TokenSession.requestTokenWithCode");
644
+ await this.applyTokenResp(ready, { replace: true });
645
+ return ready;
646
+ }
647
+ /**
648
+ * Явный refresh (single-flight через общий mutex сессии).
649
+ * Сериализован с `logout` / `ensureFresh` / `applyTokenResp`.
650
+ * При гонке с `logout()`: promise может resolve с `TokenResp`, но snapshot уже очищен —
651
+ * не используй return value; смотри `session.status` / `getTokenSnapshot()`.
652
+ */
653
+ async refresh() {
654
+ return this.runExclusive(async () => {
655
+ this.assertNotClosedForWrite();
656
+ if (!this.tokenSnapshot.refreshToken?.trim()) {
657
+ throw new Error("TokenSession.refresh: \u043D\u0435\u0442 refreshToken \u0432 storage");
658
+ }
659
+ const result = await this.refreshWithFailurePolicy();
660
+ if (result.outcome === "refreshed") {
661
+ return result.resp;
662
+ }
663
+ if (this.lastRefreshError !== void 0) {
664
+ throw this.lastRefreshError;
665
+ }
666
+ throw new Error("TokenSession.refresh: refresh failed");
667
+ });
668
+ }
669
+ async ensureFresh() {
670
+ await this.runExclusive(() => this.ensureFreshImpl());
671
+ }
672
+ assertNotClosedForWrite() {
673
+ if (this.closed) {
674
+ throw new Error(SESSION_CLOSED_MESSAGE);
675
+ }
676
+ }
677
+ /** Caller must hold `runExclusive`. Do not call public `applyTokenResp` from here. */
678
+ async applyTokenRespUnlocked(tokens, opts) {
679
+ this.assertNotClosedForWrite();
680
+ const normalized = normalizeTokenResp(tokens);
681
+ assertTokenRespReadyForStorage(normalized, "TokenSession.applyTokenResp");
682
+ this.tokenSnapshot = mergeAuthTokens(this.tokenSnapshot, normalized, {
683
+ ...opts,
684
+ // Host TTL survives refresh without expiration only while still fresh (incl. skew); else cleared → opaque until 401.
685
+ keepPrevTtlIfExpiresAfterMs: Date.now() + (this.opts.accessTokenSkewMs ?? DEFAULT_SKEW_MS)
686
+ });
687
+ await this.persist(true);
688
+ }
689
+ async clearTokensUnlocked() {
690
+ this.tokenSnapshot = {
691
+ ...this.tokenSnapshot,
692
+ accessToken: void 0,
693
+ refreshToken: void 0,
694
+ accessExpiresAtMs: void 0
695
+ };
696
+ await this.persist(true);
697
+ }
698
+ /** Caller must hold `runExclusive`. `refreshWithFailurePolicy` skips when no refreshToken; guard here is intentional defense-in-depth. */
699
+ async refreshUnlocked() {
700
+ const refresh = this.tokenSnapshot.refreshToken?.trim();
701
+ if (!refresh) {
702
+ throw new Error("TokenSession.refresh: \u043D\u0435\u0442 refreshToken \u0432 storage");
703
+ }
704
+ const deviceInfo = this.resolveDeviceInfo();
705
+ const resp = await this.identityClient().auth.refresh({
706
+ refreshToken: refresh,
707
+ deviceInfo
708
+ });
709
+ await this.applyTokenRespUnlocked(resp);
710
+ return resp;
711
+ }
712
+ /** Caller must hold `runExclusive`. */
713
+ async refreshWithFailurePolicy() {
714
+ if (this.closed) {
715
+ return { outcome: "unchanged" };
716
+ }
717
+ const refresh = this.tokenSnapshot.refreshToken?.trim();
718
+ if (!refresh) {
719
+ return { outcome: "unchanged" };
720
+ }
721
+ try {
722
+ const resp = await this.refreshUnlocked();
723
+ this.lastRefreshError = void 0;
724
+ return { outcome: "refreshed", resp };
725
+ } catch (e) {
726
+ this.lastRefreshError = e;
727
+ const action = await this.opts.onRefreshFailure?.(e) ?? defaultOnRefreshFailure(e);
728
+ if (action === "throw") throw e;
729
+ if (action === "keep") {
730
+ return { outcome: "unchanged" };
731
+ }
732
+ await this.clearTokensUnlocked();
733
+ return { outcome: "cleared" };
734
+ }
735
+ }
736
+ async runExclusive(fn) {
737
+ const prev = this.ensureFreshMutex;
738
+ let release;
739
+ this.ensureFreshMutex = new Promise((resolve) => {
740
+ release = resolve;
741
+ });
742
+ await prev;
743
+ try {
744
+ return await fn();
745
+ } finally {
746
+ release();
747
+ }
748
+ }
749
+ async ensureFreshImpl() {
750
+ if (this.closed) return;
751
+ const skew = this.opts.accessTokenSkewMs ?? DEFAULT_SKEW_MS;
752
+ const access = this.tokenSnapshot.accessToken?.trim() ?? "";
753
+ const expires = this.tokenSnapshot.accessExpiresAtMs ?? 0;
754
+ if (access && this.isAccessStillFresh(access, expires, skew)) {
755
+ return;
756
+ }
757
+ const refresh = this.tokenSnapshot.refreshToken?.trim() ?? "";
758
+ if (refresh) {
759
+ await this.refreshWithFailurePolicy();
760
+ }
761
+ }
762
+ isAccessStillFresh(access, expiresAtMs, accessTokenSkewMs) {
763
+ const now = Date.now();
764
+ const jwtExpMs = accessExpiresAtMsFromJwt(access);
765
+ if (jwtExpMs !== void 0) {
766
+ const jwtFresh = now < jwtExpMs - accessTokenSkewMs;
767
+ const snapFresh = !(expiresAtMs > 0) || now < expiresAtMs - accessTokenSkewMs;
768
+ return jwtFresh && snapFresh;
769
+ }
770
+ if (accessTokenLooksLikeJwt(access)) {
771
+ return false;
772
+ }
773
+ if (!(expiresAtMs > 0)) {
774
+ return true;
775
+ }
776
+ return now < expiresAtMs - accessTokenSkewMs;
777
+ }
778
+ computeStatus() {
779
+ const skew = this.opts.accessTokenSkewMs ?? DEFAULT_SKEW_MS;
780
+ const access = this.tokenSnapshot.accessToken?.trim() ?? "";
781
+ const expires = this.tokenSnapshot.accessExpiresAtMs ?? 0;
782
+ if (access && this.isAccessStillFresh(access, expires, skew)) {
783
+ return "authenticated";
784
+ }
785
+ const hasAny = Boolean(access) || Boolean(this.tokenSnapshot.refreshToken?.trim()) || typeof expires === "number" && expires > 0;
786
+ if (!hasAny) return "unauthenticated";
787
+ return "expired";
788
+ }
789
+ emitStatusChange() {
790
+ const status = this.computeStatus();
791
+ for (const listener of this.statusListeners) {
792
+ try {
793
+ listener(status);
794
+ } catch {
795
+ }
796
+ }
797
+ }
798
+ resolveDeviceInfo() {
799
+ if (this.opts.deviceInfo) {
800
+ return { ...this.opts.deviceInfo };
801
+ }
802
+ const deviceId = this.tokenSnapshot.deviceId?.trim();
803
+ return {
804
+ ...deviceId ? { deviceId } : {},
805
+ deviceType: DeviceTypeValues.Desktop
806
+ };
807
+ }
808
+ /** Identity-клиент с Basic приложения, без Bearer пользователя. */
809
+ identityClient() {
810
+ return createIdentityClient(this.opts);
811
+ }
812
+ buildClient() {
813
+ let fetchImpl = this.opts.fetch;
814
+ if (this.opts.refreshOnUnauthorized !== false) {
815
+ fetchImpl = createFetchWith401Retry({
816
+ fetch: this.opts.fetch,
817
+ getAccessToken: async () => {
818
+ if (this.closed) return void 0;
819
+ return this.tokenSnapshot.accessToken?.trim() || void 0;
820
+ },
821
+ refresh: async () => {
822
+ await this.runExclusive(async () => {
823
+ if (this.closed) {
824
+ throw new Error(SESSION_CLOSED_MESSAGE);
825
+ }
826
+ const result = await this.refreshWithFailurePolicy();
827
+ if (result.outcome !== "refreshed") {
828
+ throw this.lastRefreshError ?? new Error("TokenSession 401-retry: refresh failed");
829
+ }
830
+ });
831
+ }
832
+ });
833
+ }
834
+ return new TorrowClient2({
835
+ baseUrl: this.opts.baseUrl,
836
+ fetch: fetchImpl,
837
+ defaultHeaders: this.opts.defaultHeaders,
838
+ userAgent: this.opts.userAgent,
839
+ authIdentityBasic: appCredentialsToAuthIdentityBasic(
840
+ resolveAppCredentials(this.opts.appCredentials)
841
+ ),
842
+ getAccessToken: async () => this.getAccessToken()
843
+ });
844
+ }
845
+ async persist(emitStatus) {
846
+ await this.opts.tokenStorage.save(this.tokenSnapshot);
847
+ if (emitStatus) this.emitStatusChange();
848
+ }
849
+ };
850
+
851
+ // ../../src/auth-session/torrow-auth.ts
852
+ import {
853
+ TorrowClient as TorrowClient3
854
+ } from "@torrow/sdk-client";
855
+
856
+ // ../../src/auth-session/link-login.ts
857
+ import {
858
+ AccountTypeValues,
859
+ DefaultAuthScopes as DefaultAuthScopes3
860
+ } from "@torrow/sdk-client";
861
+
862
+ // ../../src/auth-session/sleep.ts
863
+ function sleepMs(ms, signal) {
864
+ return new Promise((resolve, reject) => {
865
+ if (signal?.aborted) {
866
+ reject(signal.reason ?? new Error("Aborted"));
867
+ return;
868
+ }
869
+ const onAbort = () => {
870
+ clearTimeout(t);
871
+ signal?.removeEventListener("abort", onAbort);
872
+ reject(signal?.reason ?? new Error("Aborted"));
873
+ };
874
+ const t = setTimeout(() => {
875
+ signal?.removeEventListener("abort", onAbort);
876
+ resolve();
877
+ }, ms);
878
+ signal?.addEventListener("abort", onAbort, { once: true });
879
+ });
880
+ }
881
+
882
+ // ../../src/auth-session/link-login.ts
883
+ var DEFAULT_POLL_INTERVAL_MS = 2e3;
884
+ var DEFAULT_POLL_TIMEOUT_SEC = 180;
885
+ var DEFAULT_LANGUAGE = "ru";
886
+ var DEFAULT_REGION = 70;
887
+ async function runLinkLogin(session, options) {
888
+ const client = session.identityClient();
889
+ const accountType = options.accountType ?? AccountTypeValues.Telegram;
890
+ const body = {
891
+ accountType,
892
+ language: options.language?.trim() || DEFAULT_LANGUAGE,
893
+ regionStorageId: typeof options.regionStorageId === "number" && Number.isFinite(options.regionStorageId) ? options.regionStorageId : DEFAULT_REGION,
894
+ ...options.inviteSessionId?.trim() ? { inviteSessionId: options.inviteSessionId.trim() } : {},
895
+ ...options.captcha?.trim() ? { captcha: options.captcha.trim() } : {},
896
+ ...options.deviceInfo.deviceId?.trim() ? { deviceId: options.deviceInfo.deviceId.trim() } : {}
897
+ };
898
+ const createResp = await client.auth.createSession(body, { signal: options.signal });
899
+ const sessionId = createResp.sessionId?.trim();
900
+ const code = createResp.code?.trim();
901
+ const authUrl = createResp.authUrl?.trim();
902
+ if (!sessionId || !code || !authUrl) {
903
+ throw new Error(
904
+ "runLinkLogin: createSession response incomplete (need sessionId, code, authUrl)"
905
+ );
906
+ }
907
+ await options.onLink(authUrl);
908
+ const scopes = options.scopes && options.scopes.length > 0 ? options.scopes : [...DefaultAuthScopes3];
909
+ const intervalMs = typeof options.pollIntervalMs === "number" && options.pollIntervalMs > 0 ? options.pollIntervalMs : DEFAULT_POLL_INTERVAL_MS;
910
+ const timeoutSec = typeof options.pollTimeoutSec === "number" && options.pollTimeoutSec > 0 ? options.pollTimeoutSec : typeof createResp.sessionLifeTimeInSeconds === "number" && createResp.sessionLifeTimeInSeconds > 0 ? createResp.sessionLifeTimeInSeconds : DEFAULT_POLL_TIMEOUT_SEC;
911
+ const deadline = Date.now() + timeoutSec * 1e3;
912
+ for (; ; ) {
913
+ options.signal?.throwIfAborted?.();
914
+ try {
915
+ return await session.requestTokenWithCode({
916
+ sessionId,
917
+ code,
918
+ scopes,
919
+ signal: options.signal
920
+ });
921
+ } catch (e) {
922
+ if (!isAuthSessionNotReadyYet(e)) throw e;
923
+ if (Date.now() >= deadline) {
924
+ throw new Error(
925
+ `runLinkLogin: timeout ~${timeoutSec}s waiting for auth session to become ready`
926
+ );
927
+ }
928
+ await sleepMs(intervalMs, options.signal);
929
+ }
930
+ }
931
+ }
932
+
933
+ // ../../src/auth-session/otp-login.ts
934
+ import {
935
+ DefaultAuthScopes as DefaultAuthScopes5,
936
+ SendCodeAccountTypeValues,
937
+ TorrowHttpError as TorrowHttpError3,
938
+ VerificationMethodValues as VerificationMethodValues2,
939
+ TokenStatusValues as TokenStatusValues5
940
+ } from "@torrow/sdk-client";
941
+
942
+ // ../../src/auth-session/mobile-id.ts
943
+ import {
944
+ DefaultAuthScopes as DefaultAuthScopes4,
945
+ TokenStatusValues as TokenStatusValues4
946
+ } from "@torrow/sdk-client";
947
+ var DEFAULT_POLL_INTERVAL_SEC = 3;
948
+ var DEFAULT_MAX_WAIT_SEC = 180;
949
+ function scopesOrDefault(scopes) {
950
+ return scopes && scopes.length > 0 ? scopes : [...DefaultAuthScopes4];
951
+ }
952
+ async function pollMobileIdOnce(client, args) {
953
+ const sessionId = args.sessionId.trim();
954
+ if (!sessionId) {
955
+ throw new Error("pollMobileIdOnce: sessionId is empty");
956
+ }
957
+ const body = {
958
+ sessionId,
959
+ // Empty code = plain poll (server accepts it for Mobile ID); value = SMS-OTP fallback.
960
+ code: args.code ?? "",
961
+ scopes: scopesOrDefault(args.scopes),
962
+ deviceInfo: args.deviceInfo
963
+ };
964
+ return normalizeTokenResp(await requestAuthToken(client, body, args.signal));
965
+ }
966
+ async function pollMobileIdUntilReady(client, options) {
967
+ const sessionId = options.sessionId.trim();
968
+ if (!sessionId) {
969
+ throw new Error("pollMobileIdUntilReady: sessionId is empty");
970
+ }
971
+ const intervalSec = typeof options.pollIntervalSeconds === "number" && options.pollIntervalSeconds > 0 ? options.pollIntervalSeconds : DEFAULT_POLL_INTERVAL_SEC;
972
+ const maxWaitSec = typeof options.maxWaitSeconds === "number" && options.maxWaitSeconds > 0 ? options.maxWaitSeconds : DEFAULT_MAX_WAIT_SEC;
973
+ const deadline = Date.now() + maxWaitSec * 1e3;
974
+ const scopes = scopesOrDefault(options.scopes);
975
+ for (; ; ) {
976
+ if (options.signal?.aborted) {
977
+ throw options.signal.reason ?? new Error("Aborted");
978
+ }
979
+ const resp = await pollMobileIdOnce(client, {
980
+ sessionId,
981
+ deviceInfo: options.deviceInfo,
982
+ scopes,
983
+ signal: options.signal
984
+ });
985
+ const status = resp.status;
986
+ if (tokenTransition(status, true) === TokenStatusValues4.TokenReady) {
987
+ return assertTokenRespReadyForStorage(resp, "pollMobileIdUntilReady");
988
+ }
989
+ if (status === TokenStatusValues4.Failed) {
990
+ throw new Error(
991
+ "pollMobileIdUntilReady: TokenResp.status=Failed \u2014 \u043D\u0443\u0436\u0435\u043D \u043D\u043E\u0432\u044B\u0439 sendCode."
992
+ );
993
+ }
994
+ if (status === TokenStatusValues4.AwaitingSmsCode) {
995
+ const obtain = options.obtainSmsCode;
996
+ if (!obtain) {
997
+ throw new Error(
998
+ "pollMobileIdUntilReady: AwaitingSmsCode, \u043D\u043E obtainSmsCode \u043D\u0435 \u0437\u0430\u0434\u0430\u043D."
999
+ );
1000
+ }
1001
+ const code = (await obtain({
1002
+ codeLength: options.smsCodeLength
1003
+ })).trim();
1004
+ if (!code) {
1005
+ throw new Error("pollMobileIdUntilReady: \u043F\u0443\u0441\u0442\u043E\u0439 SMS-\u043A\u043E\u0434 \u043F\u043E\u0441\u043B\u0435 AwaitingSmsCode.");
1006
+ }
1007
+ const afterSms = await pollMobileIdOnce(client, {
1008
+ sessionId,
1009
+ deviceInfo: options.deviceInfo,
1010
+ scopes,
1011
+ code,
1012
+ signal: options.signal
1013
+ });
1014
+ if (tokenTransition(afterSms.status, true) === TokenStatusValues4.TokenReady) {
1015
+ return assertTokenRespReadyForStorage(afterSms, "pollMobileIdUntilReady (SMS)");
1016
+ }
1017
+ if (afterSms.status === TokenStatusValues4.Failed) {
1018
+ throw new Error(
1019
+ "pollMobileIdUntilReady: TokenResp.status=Failed \u043F\u043E\u0441\u043B\u0435 SMS \u2014 \u043D\u0443\u0436\u0435\u043D \u043D\u043E\u0432\u044B\u0439 sendCode."
1020
+ );
1021
+ }
1022
+ if (afterSms.status === TokenStatusValues4.Pending) {
1023
+ continue;
1024
+ }
1025
+ if (afterSms.status === TokenStatusValues4.AwaitingSmsCode) {
1026
+ continue;
1027
+ }
1028
+ throw new Error(
1029
+ `pollMobileIdUntilReady: \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u044B\u0439 TokenResp.status=${String(afterSms.status)} \u043F\u043E\u0441\u043B\u0435 SMS.`
1030
+ );
1031
+ }
1032
+ if (status !== TokenStatusValues4.Pending) {
1033
+ throw new Error(
1034
+ `pollMobileIdUntilReady: \u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u044B\u0439 TokenResp.status=${String(status)}.`
1035
+ );
1036
+ }
1037
+ if (Date.now() >= deadline) {
1038
+ throw new Error(
1039
+ `pollMobileIdUntilReady: timeout ~${maxWaitSec}\u0441 (poll \u043A\u0430\u0436\u0434\u044B\u0435 ${intervalSec}\u0441) \u2014 \u0441\u0442\u0430\u0442\u0443\u0441 Pending.`
1040
+ );
1041
+ }
1042
+ await sleepMs(intervalSec * 1e3, options.signal);
1043
+ }
1044
+ }
1045
+
1046
+ // ../../src/auth-session/otp-login.ts
1047
+ var MOBILE_ID_SMS_CODE_LENGTH = 4;
1048
+ var OtpCodeRequestLimitExceededError = class extends Error {
1049
+ name = "OtpCodeRequestLimitExceededError";
1050
+ minutes;
1051
+ seconds;
1052
+ traceUrl;
1053
+ apiMessage;
1054
+ body;
1055
+ constructor(body) {
1056
+ const parsed = parseRateLimitBody(body);
1057
+ const wait = parsed.min || parsed.sec ? `\u041F\u043E\u0432\u0442\u043E\u0440 \u043F\u0440\u0438\u043C\u0435\u0440\u043D\u043E \u0447\u0435\u0440\u0435\u0437 ${[parsed.min && `${parsed.min} \u043C\u0438\u043D.`, parsed.sec && `${parsed.sec} \u0441\u0435\u043A.`].filter(Boolean).join(" ")}.` : "\u0422\u043E\u0447\u043D\u043E\u0435 \u0432\u0440\u0435\u043C\u044F \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u044F \u0441\u0435\u0440\u0432\u0435\u0440 \u043D\u0435 \u043F\u0435\u0440\u0435\u0434\u0430\u043B.";
1058
+ super(`sendCode: CODE_REQUEST_LIMIT_EXCEEDED. ${wait}`);
1059
+ this.minutes = parsed.min;
1060
+ this.seconds = parsed.sec;
1061
+ this.traceUrl = parsed.trace;
1062
+ this.apiMessage = parsed.message;
1063
+ this.body = body;
1064
+ }
1065
+ };
1066
+ var OtpAuthHeaderMissingError = class extends Error {
1067
+ name = "OtpAuthHeaderMissingError";
1068
+ body;
1069
+ systemMessage;
1070
+ constructor(body) {
1071
+ const sys = body !== null && typeof body === "object" && typeof body.systemMessage === "string" ? body.systemMessage.trim() : void 0;
1072
+ super(
1073
+ [
1074
+ "requestToken: AUTH_HEADER_MISSING \u2014 \u0437\u0430\u0434\u0430\u0439 appCredentials: { clientId, clientSecret }.",
1075
+ sys ? `\u0421\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u0430: ${sys}` : ""
1076
+ ].filter(Boolean).join(" ")
1077
+ );
1078
+ this.body = body;
1079
+ this.systemMessage = sys;
1080
+ }
1081
+ };
1082
+ function parseRateLimitBody(body) {
1083
+ if (body === null || typeof body !== "object") return {};
1084
+ const o = body;
1085
+ const p = o.parameters ?? {};
1086
+ return {
1087
+ min: p.minutes != null && String(p.minutes) !== "" ? String(p.minutes) : void 0,
1088
+ sec: p.seconds != null && String(p.seconds) !== "" ? String(p.seconds) : void 0,
1089
+ message: typeof o.message === "string" && o.message.trim() ? o.message.trim() : void 0,
1090
+ trace: typeof o.traceUrl === "string" && o.traceUrl.trim() ? o.traceUrl.trim() : void 0
1091
+ };
1092
+ }
1093
+ function isSendCodeRateLimitedBody(body) {
1094
+ if (body === null || typeof body !== "object") return false;
1095
+ return body.name === "CODE_REQUEST_LIMIT_EXCEEDED";
1096
+ }
1097
+ function isAuthHeaderMissingBody(body) {
1098
+ if (body === null || typeof body !== "object") return false;
1099
+ return body.name === "AUTH_HEADER_MISSING";
1100
+ }
1101
+ function httpErrorBody(e) {
1102
+ if (e instanceof TorrowHttpError3) return e.body;
1103
+ if (e !== null && typeof e === "object" && "body" in e) {
1104
+ return e.body;
1105
+ }
1106
+ return void 0;
1107
+ }
1108
+ function scopesOrDefault2(scopes) {
1109
+ return scopes && scopes.length > 0 ? scopes : [...DefaultAuthScopes5];
1110
+ }
1111
+ function effectiveCodeLength(raw, fallback) {
1112
+ if (typeof raw === "number" && raw > 0) return raw;
1113
+ return fallback > 0 ? fallback : void 0;
1114
+ }
1115
+ async function requestTokenOrThrow(client, body, signal) {
1116
+ try {
1117
+ return normalizeTokenResp(await requestAuthToken(client, body, signal));
1118
+ } catch (e) {
1119
+ const errBody = httpErrorBody(e);
1120
+ if (errBody !== void 0 && isAuthHeaderMissingBody(errBody)) {
1121
+ throw new OtpAuthHeaderMissingError(errBody);
1122
+ }
1123
+ throw e;
1124
+ }
1125
+ }
1126
+ async function runOtpLogin(client, options) {
1127
+ const accountName = options.accountName.trim();
1128
+ const accountType = options.type;
1129
+ const scopes = scopesOrDefault2(options.scopes);
1130
+ const signal = options.signal;
1131
+ let sendResp;
1132
+ try {
1133
+ sendResp = await sendOtpCode(client, options, signal);
1134
+ } catch (e) {
1135
+ const errBody = httpErrorBody(e);
1136
+ if (errBody !== void 0 && isSendCodeRateLimitedBody(errBody)) {
1137
+ throw new OtpCodeRequestLimitExceededError(errBody);
1138
+ }
1139
+ throw e;
1140
+ }
1141
+ const sessionId = sendResp.sessionId?.trim();
1142
+ if (!sessionId) {
1143
+ throw new Error("runOtpLogin: sendCode response had no sessionId");
1144
+ }
1145
+ const result = {
1146
+ accountName,
1147
+ accountType,
1148
+ sessionId,
1149
+ usedMethod: sendResp.usedMethod,
1150
+ codeLength: sendResp.codeLength,
1151
+ pollIntervalSeconds: sendResp.pollIntervalSeconds,
1152
+ availableMethods: sendResp.availableMethods,
1153
+ sessionLifeTimeInSeconds: sendResp.sessionLifeTimeInSeconds,
1154
+ nextAvailableUsageTime: sendResp.nextAvailableUsageTime ?? null
1155
+ };
1156
+ await options.onSendCode(result);
1157
+ if (sendResp.usedMethod === VerificationMethodValues2.MtsMobileId) {
1158
+ try {
1159
+ return await pollMobileIdUntilReady(client, {
1160
+ sessionId,
1161
+ deviceInfo: options.deviceInfo,
1162
+ scopes,
1163
+ pollIntervalSeconds: sendResp.pollIntervalSeconds,
1164
+ maxWaitSeconds: sendResp.sessionLifeTimeInSeconds,
1165
+ signal,
1166
+ smsCodeLength: typeof sendResp.codeLength === "number" && sendResp.codeLength > 0 ? sendResp.codeLength : MOBILE_ID_SMS_CODE_LENGTH,
1167
+ obtainSmsCode: async (ctx) => {
1168
+ const codeLength2 = effectiveCodeLength(ctx.codeLength, MOBILE_ID_SMS_CODE_LENGTH);
1169
+ return (await options.getCode({
1170
+ sessionId,
1171
+ codeLength: codeLength2,
1172
+ usedMethod: sendResp.usedMethod
1173
+ })).trim();
1174
+ }
1175
+ });
1176
+ } catch (e) {
1177
+ const errBody = httpErrorBody(e);
1178
+ if (errBody !== void 0 && isAuthHeaderMissingBody(errBody)) {
1179
+ throw new OtpAuthHeaderMissingError(errBody);
1180
+ }
1181
+ throw e;
1182
+ }
1183
+ }
1184
+ const classicLength = effectiveCodeLength(
1185
+ sendResp.codeLength,
1186
+ accountType === SendCodeAccountTypeValues.Email ? 6 : 4
1187
+ );
1188
+ const code = (await options.getCode({
1189
+ sessionId,
1190
+ codeLength: classicLength,
1191
+ usedMethod: sendResp.usedMethod
1192
+ })).trim();
1193
+ if (!code) {
1194
+ throw new Error("runOtpLogin: empty OTP code");
1195
+ }
1196
+ const resp = await requestTokenOrThrow(
1197
+ client,
1198
+ {
1199
+ sessionId,
1200
+ code,
1201
+ scopes,
1202
+ deviceInfo: options.deviceInfo
1203
+ },
1204
+ signal
1205
+ );
1206
+ if (tokenTransition(resp.status, true) !== TokenStatusValues5.TokenReady) {
1207
+ throw new Error(`runOtpLogin: TokenResp.status=${resp.status} (\u043E\u0436\u0438\u0434\u0430\u043B\u0441\u044F TokenReady)`);
1208
+ }
1209
+ return assertTokenRespReadyForStorage(resp, "runOtpLogin");
1210
+ }
1211
+
1212
+ // ../../src/auth-session/torrow-auth.ts
1213
+ function sessionOpenOpts(base) {
1214
+ return {
1215
+ appCredentials: base.appCredentials,
1216
+ tokenStorage: base.tokenStorage,
1217
+ deviceInfo: base.deviceInfo,
1218
+ baseUrl: base.baseUrl,
1219
+ fetch: base.fetch,
1220
+ defaultHeaders: base.defaultHeaders,
1221
+ userAgent: base.userAgent,
1222
+ accessTokenSkewMs: base.accessTokenSkewMs,
1223
+ refreshOnUnauthorized: base.refreshOnUnauthorized,
1224
+ onRefreshFailure: base.onRefreshFailure
1225
+ };
1226
+ }
1227
+ function sessionOpenOptsForLogin(base) {
1228
+ return { ...sessionOpenOpts(base), skipEnsureFreshOnOpen: true };
1229
+ }
1230
+ var TorrowAuth = {
1231
+ /** Пошаговый OTP без storage, polling и refresh. */
1232
+ createOtpFlow,
1233
+ async connect(options) {
1234
+ const session = await TokenSession.open(sessionOpenOpts(options));
1235
+ return { client: session.client, session };
1236
+ },
1237
+ async loginWithOtp(options) {
1238
+ const accountName = options.account.accountName.trim();
1239
+ if (!accountName) {
1240
+ throw new Error("TorrowAuth.loginWithOtp: account.accountName is empty");
1241
+ }
1242
+ const session = await TokenSession.open(sessionOpenOptsForLogin(options));
1243
+ const deviceInfo = session.getDeviceInfo();
1244
+ const ready = await runOtpLogin(session.identityClient(), {
1245
+ accountName,
1246
+ type: options.account.accountType,
1247
+ deviceInfo,
1248
+ onSendCode: options.onSendCode,
1249
+ getCode: options.getCode,
1250
+ scopes: options.scopes,
1251
+ language: options.language,
1252
+ regionStorageId: options.regionStorageId,
1253
+ captcha: options.captcha,
1254
+ captchaType: options.captchaType,
1255
+ inviteSessionId: options.inviteSessionId,
1256
+ preferredMethod: options.preferredMethod,
1257
+ supportedMethods: options.supportedMethods,
1258
+ signal: options.signal
1259
+ });
1260
+ await session.applyTokenResp(ready, { replace: true });
1261
+ return { client: session.client, session };
1262
+ },
1263
+ async loginWithLink(options) {
1264
+ const session = await TokenSession.open(sessionOpenOptsForLogin(options));
1265
+ await runLinkLogin(session, {
1266
+ deviceInfo: session.getDeviceInfo(),
1267
+ onLink: options.onLink,
1268
+ accountType: options.accountType,
1269
+ language: options.language,
1270
+ regionStorageId: options.regionStorageId,
1271
+ captcha: options.captcha,
1272
+ inviteSessionId: options.inviteSessionId,
1273
+ scopes: options.scopes,
1274
+ pollIntervalMs: options.pollIntervalMs,
1275
+ pollTimeoutSec: options.pollTimeoutSec,
1276
+ signal: options.signal
1277
+ });
1278
+ return { client: session.client, session };
1279
+ },
1280
+ async fromTokens(options) {
1281
+ const accessToken = options.accessToken.trim();
1282
+ if (!accessToken) {
1283
+ throw new Error("TorrowAuth.fromTokens: accessToken is empty");
1284
+ }
1285
+ const refreshToken = options.refreshToken?.trim() || void 0;
1286
+ const prev = await options.tokenStorage.load();
1287
+ const explicitTtl = options.accessExpiresAtMs;
1288
+ const accessExpiresAtMs = typeof explicitTtl === "number" && Number.isFinite(explicitTtl) && explicitTtl > 0 ? explicitTtl : accessExpiresAtMsFromJwt(accessToken);
1289
+ await options.tokenStorage.save({
1290
+ accessToken,
1291
+ refreshToken,
1292
+ ...typeof accessExpiresAtMs === "number" && accessExpiresAtMs > 0 ? { accessExpiresAtMs } : { accessExpiresAtMs: void 0 },
1293
+ deviceId: options.deviceInfo.deviceId?.trim() || prev.deviceId?.trim() || void 0
1294
+ });
1295
+ return TorrowAuth.connect(options);
1296
+ },
1297
+ anonymous(config = {}) {
1298
+ return new TorrowClient3({
1299
+ ...config,
1300
+ getAccessToken: void 0
1301
+ });
1302
+ }
1303
+ };
1304
+ export {
1305
+ AuthSessionNotReadyError,
1306
+ DEFAULT_APP_CREDENTIALS,
1307
+ LocalStorageTokenStorage,
1308
+ MemoryTokenStorage,
1309
+ OtpAuthHeaderMissingError,
1310
+ OtpCodeRequestLimitExceededError,
1311
+ OtpFlowError,
1312
+ OtpFlowErrorCodeValues,
1313
+ OtpPhaseValues,
1314
+ TokenSession,
1315
+ TorrowAuth
1316
+ };
1317
+ //# sourceMappingURL=index.js.map