itd-api 0.9.1 → 0.9.3

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.
Files changed (63) hide show
  1. package/dist/events/index.cjs +3 -4
  2. package/dist/events/index.d.cts +3 -3
  3. package/dist/events/index.d.ts +3 -3
  4. package/dist/events/index.js +4 -4
  5. package/dist/index.cjs +283 -90
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +16 -72
  8. package/dist/index.d.ts +16 -72
  9. package/dist/index.js +284 -90
  10. package/dist/index.js.map +1 -1
  11. package/dist/node/index.cjs +3 -3
  12. package/dist/node/index.js +3 -3
  13. package/dist/rest/index.cjs +4 -5
  14. package/dist/rest/index.d.cts +3 -3
  15. package/dist/rest/index.d.ts +3 -3
  16. package/dist/rest/index.js +5 -5
  17. package/dist/shared/{events-BDdJyH2p.d.ts → events-BPxBF0gQ.d.cts} +3 -5
  18. package/dist/shared/{events-D6y0Hxyz.js → events-BzjI6pn5.js} +4 -4
  19. package/dist/shared/{events-D6y0Hxyz.js.map → events-BzjI6pn5.js.map} +1 -1
  20. package/dist/shared/{events-BqFkwwTw.cjs → events-C1FudmSe.cjs} +3 -9
  21. package/dist/shared/{events-BqFkwwTw.cjs.map → events-C1FudmSe.cjs.map} +1 -1
  22. package/dist/shared/{events-BQyH5-S0.d.cts → events-DnwfcLxS.d.ts} +3 -5
  23. package/dist/shared/{multi-storage-SoW05uzO.js → multi-storage-C3Ngtop1.js} +13 -8
  24. package/dist/shared/multi-storage-C3Ngtop1.js.map +1 -0
  25. package/dist/shared/{multi-storage-CDKLa956.cjs → multi-storage-s-fPtjoy.cjs} +13 -8
  26. package/dist/shared/multi-storage-s-fPtjoy.cjs.map +1 -0
  27. package/dist/shared/{options-DOJYtoti.cjs → options-DjEtbVaf.cjs} +2 -2
  28. package/dist/shared/{options-DOJYtoti.cjs.map → options-DjEtbVaf.cjs.map} +1 -1
  29. package/dist/shared/{options-DhgGg7Ms.js → options-i4LrKfqx.js} +2 -2
  30. package/dist/shared/{options-DhgGg7Ms.js.map → options-i4LrKfqx.js.map} +1 -1
  31. package/dist/shared/{redact-Ba5iSVYZ.js → redact-DJV0XM3n.js} +40 -15
  32. package/dist/shared/redact-DJV0XM3n.js.map +1 -0
  33. package/dist/shared/{redact-BP00URmQ.cjs → redact-lAmck1Z6.cjs} +45 -14
  34. package/dist/shared/redact-lAmck1Z6.cjs.map +1 -0
  35. package/dist/shared/{render-BU1_wex8.d.cts → render-C1F_QWYD.d.cts} +82 -19
  36. package/dist/shared/{render-sb1VJhUA.js → render-CtOpp9Pm.js} +127 -124
  37. package/dist/shared/render-CtOpp9Pm.js.map +1 -0
  38. package/dist/shared/{render-DAEeCMk9.cjs → render-Omy1drH1.cjs} +125 -128
  39. package/dist/shared/render-Omy1drH1.cjs.map +1 -0
  40. package/dist/shared/{render-CZ_-F2Tl.d.ts → render-uFW2kDxp.d.ts} +82 -19
  41. package/dist/shared/{storage-QNVxzpDB.cjs → storage-CZJZoaz9.cjs} +4 -2
  42. package/dist/shared/storage-CZJZoaz9.cjs.map +1 -0
  43. package/dist/shared/{storage-CfHKzVHf.js → storage-DiDe0PZM.js} +4 -2
  44. package/dist/shared/storage-DiDe0PZM.js.map +1 -0
  45. package/dist/shared/{url-Dsr4c5r5.js → url-3Ts6EEHQ.js} +301 -96
  46. package/dist/shared/url-3Ts6EEHQ.js.map +1 -0
  47. package/dist/shared/{url-CzHqE-Fg.cjs → url-D6wsXm1T.cjs} +301 -96
  48. package/dist/shared/url-D6wsXm1T.cjs.map +1 -0
  49. package/dist/shared/{url-CZZcyPI0.d.cts → url-DubjkRR9.d.cts} +10 -16
  50. package/dist/shared/{url-CZZcyPI0.d.ts → url-DubjkRR9.d.ts} +10 -16
  51. package/dist/web/index.cjs +1 -1
  52. package/dist/web/index.js +1 -1
  53. package/package.json +5 -5
  54. package/dist/shared/multi-storage-CDKLa956.cjs.map +0 -1
  55. package/dist/shared/multi-storage-SoW05uzO.js.map +0 -1
  56. package/dist/shared/redact-BP00URmQ.cjs.map +0 -1
  57. package/dist/shared/redact-Ba5iSVYZ.js.map +0 -1
  58. package/dist/shared/render-DAEeCMk9.cjs.map +0 -1
  59. package/dist/shared/render-sb1VJhUA.js.map +0 -1
  60. package/dist/shared/storage-CfHKzVHf.js.map +0 -1
  61. package/dist/shared/storage-QNVxzpDB.cjs.map +0 -1
  62. package/dist/shared/url-CzHqE-Fg.cjs.map +0 -1
  63. package/dist/shared/url-Dsr4c5r5.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,13 +1,13 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_url = require("./shared/url-CzHqE-Fg.cjs");
2
+ const require_url = require("./shared/url-D6wsXm1T.cjs");
3
3
  const require_errors = require("./shared/errors-BmP3TKoW.cjs");
4
- const require_redact = require("./shared/redact-BP00URmQ.cjs");
5
- const require_render = require("./shared/render-DAEeCMk9.cjs");
6
- const require_options = require("./shared/options-DOJYtoti.cjs");
7
- const require_events = require("./shared/events-BqFkwwTw.cjs");
4
+ const require_redact = require("./shared/redact-lAmck1Z6.cjs");
5
+ const require_render = require("./shared/render-Omy1drH1.cjs");
6
+ const require_options = require("./shared/options-DjEtbVaf.cjs");
7
+ const require_events = require("./shared/events-C1FudmSe.cjs");
8
8
  const require_key_value_store = require("./shared/key-value-store-B5GYVEYZ.cjs");
9
- const require_storage = require("./shared/storage-QNVxzpDB.cjs");
10
- const require_multi_storage = require("./shared/multi-storage-CDKLa956.cjs");
9
+ const require_storage = require("./shared/storage-CZJZoaz9.cjs");
10
+ const require_multi_storage = require("./shared/multi-storage-s-fPtjoy.cjs");
11
11
  //#region src/notifications-api.ts
12
12
  var ClientNotifications = class extends require_render.NotificationsResource {
13
13
  #createEvents;
@@ -50,7 +50,7 @@ const AUTH_SIGN_IN = require_url.defineBuiltInOperation("auth.signIn", (body) =>
50
50
  };
51
51
  });
52
52
  const AUTH_VERIFY_OTP = require_url.defineBuiltInOperation("auth.verifyOtp", (body) => requireString(body, "accessToken", "Сервер не вернул accessToken после подтверждения кода"));
53
- const AUTH_REFRESH = require_url.defineBuiltInOperation("auth.refresh", (body) => require_url.pickString(body, "accessToken"));
53
+ const AUTH_REFRESH = require_url.defineBuiltInOperation("auth.refresh", (body) => requireString(body, "accessToken", "Сервер не вернул accessToken при обновлении сессии"));
54
54
  const AUTH_RESEND_OTP = require_url.voidOperation("auth.resendOtp");
55
55
  const AUTH_LOGOUT = require_url.voidOperation("auth.logout");
56
56
  const AUTH_FORGOT_PASSWORD = require_url.defineBuiltInOperation("auth.forgotPassword", (body) => requireString(body, "flowToken", "Сервер не вернул flowToken при запросе сброса пароля"));
@@ -75,38 +75,34 @@ function decodeBase64Url(segment) {
75
75
  const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0));
76
76
  return new TextDecoder().decode(bytes);
77
77
  }
78
- /**
79
- * Читает `sub` и `sid` из полезной нагрузки JWT.
80
- *
81
- * Подпись не проверяется — ключа для проверки у клиента нет. Прочитанные значения служат
82
- * лишь метками для разделения локального состояния и не дают доступа: его определяет сервер.
83
- */
84
- function readTokenIdentity(token) {
78
+ /** Читает полезную нагрузку JWT без проверки подписи. */
79
+ function readTokenPayload(token) {
85
80
  try {
86
81
  const payload = token.split(".")[1];
87
- if (!payload) return {};
82
+ if (!payload) return void 0;
88
83
  const parsed = JSON.parse(decodeBase64Url(payload));
89
- if (typeof parsed !== "object" || parsed === null) return {};
90
- const { sub, sid } = parsed;
91
- return {
92
- ...typeof sub === "string" && sub.length > 0 ? { subject: sub } : {},
93
- ...typeof sid === "string" && sid.length > 0 ? { sessionId: sid } : {}
94
- };
84
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return void 0;
85
+ return parsed;
95
86
  } catch {
96
- return {};
87
+ return;
97
88
  }
98
89
  }
99
- /**
100
- * Читает `sub` из полезной нагрузки JWT.
101
- *
102
- * @returns идентификатор владельца токена; `undefined`, если токен не JWT, повреждён
103
- * или поля `sub` в нём нет
104
- */
105
- function readTokenSubject(token) {
106
- return readTokenIdentity(token).subject;
90
+ /** Читает используемые клиентом поля JWT за один раз. */
91
+ function readTokenMetadata(token) {
92
+ const payload = readTokenPayload(token);
93
+ if (!payload) return {};
94
+ const { sub, sid, exp } = payload;
95
+ const expiresAt = typeof exp === "number" && Number.isFinite(exp) ? exp * 1e3 : void 0;
96
+ return {
97
+ ...typeof sub === "string" && sub.length > 0 ? { subject: sub } : {},
98
+ ...typeof sid === "string" && sid.length > 0 ? { sessionId: sid } : {},
99
+ ...expiresAt !== void 0 && Number.isFinite(expiresAt) ? { expiresAt } : {}
100
+ };
107
101
  }
108
102
  //#endregion
109
103
  //#region src/session/auth.ts
104
+ /** Запас до истечения токена на очередь и выполнение запроса. */
105
+ const ACCESS_TOKEN_REFRESH_LEEWAY_MS = 3e4;
110
106
  /**
111
107
  * Проверяет форму `auth` и сообщает о типичных ошибках понятным текстом.
112
108
  *
@@ -120,12 +116,18 @@ function validateAuth(auth) {
120
116
  return auth;
121
117
  }
122
118
  if (typeof auth !== "object" || auth === null) throw new require_errors.ItdConfigError(`auth должен быть строкой с токеном или объектом, получено: ${typeof auth}`);
119
+ if ([
120
+ "getToken" in auth,
121
+ "accessToken" in auth || "refreshToken" in auth,
122
+ "email" in auth || "password" in auth
123
+ ].filter(Boolean).length > 1) throw new require_errors.ItdConfigError("auth должен описывать ровно один способ авторизации: getToken, accessToken или email/password");
123
124
  if ("getToken" in auth) {
124
125
  if (typeof auth.getToken !== "function") throw new require_errors.ItdConfigError("auth.getToken должен быть функцией");
125
126
  return { ...auth };
126
127
  }
127
128
  if ("accessToken" in auth) {
128
129
  if (typeof auth.accessToken !== "string" || auth.accessToken.trim() === "") throw new require_errors.ItdConfigError("auth.accessToken должен быть непустой строкой");
130
+ if (auth.refreshToken !== void 0 && (typeof auth.refreshToken !== "string" || auth.refreshToken.trim() === "")) throw new require_errors.ItdConfigError("auth.refreshToken должен быть непустой строкой");
129
131
  return { ...auth };
130
132
  }
131
133
  if ("email" in auth || "password" in auth) {
@@ -213,10 +215,67 @@ const AUTH_PATHS = {
213
215
  const TURNSTILE_SITE_KEY = "0x4AAAAAACHhxczw6fJGwPBg";
214
216
  /** Убирает поле, которое сохраняли промежуточные версии с поддержкой `getUserId()`. */
215
217
  function withoutLegacyUserId(session) {
216
- const clean = { ...session };
218
+ const clean = require_storage.copySession(session);
217
219
  delete clean.userId;
218
220
  return clean;
219
221
  }
222
+ function normalizeSessionValue(value, source, mode = "strict", logger) {
223
+ const tolerant = mode === "stored";
224
+ if (!require_url.isRecord$1(value)) {
225
+ if (tolerant) {
226
+ logger?.warn(`${source} вернула не объект сессии; сохранённая запись пропущена`);
227
+ return null;
228
+ }
229
+ throw new require_errors.ItdConfigError(`${source} должна быть объектом сессии`);
230
+ }
231
+ const record = value;
232
+ const normalized = { ...record };
233
+ delete normalized.userId;
234
+ const normalizeString = (field) => {
235
+ const item = record[field];
236
+ if (item === void 0) {
237
+ delete normalized[field];
238
+ return;
239
+ }
240
+ if (item !== void 0 && (typeof item !== "string" || item.trim() === "")) {
241
+ if (tolerant) {
242
+ logger?.warn(`${source}.${field} повреждена; поле сохранённой сессии пропущено`);
243
+ delete normalized[field];
244
+ return;
245
+ }
246
+ throw new require_errors.ItdConfigError(`${source}.${field} должна быть непустой строкой`);
247
+ }
248
+ normalized[field] = item;
249
+ };
250
+ normalizeString("accessToken");
251
+ normalizeString("refreshToken");
252
+ normalizeString("deviceId");
253
+ if (record.obtainedAt === void 0) delete normalized.obtainedAt;
254
+ else if (typeof record.obtainedAt !== "number" || !Number.isFinite(record.obtainedAt)) {
255
+ if (!tolerant) throw new require_errors.ItdConfigError(`${source}.obtainedAt должна быть конечным числом`);
256
+ else {
257
+ logger?.warn(`${source}.obtainedAt повреждена; поле сохранённой сессии пропущено`);
258
+ delete normalized.obtainedAt;
259
+ }
260
+ }
261
+ if (record.cookies === void 0) delete normalized.cookies;
262
+ else if (!Array.isArray(record.cookies)) {
263
+ if (tolerant) {
264
+ logger?.warn(`${source}.cookies повреждена; поле сохранённой сессии пропущено`);
265
+ delete normalized.cookies;
266
+ } else throw new require_errors.ItdConfigError(`${source}.cookies должна быть массивом`);
267
+ } else {
268
+ const valid = record.cookies.filter((cookie) => typeof cookie === "string" && require_redact.isSerializedCookieEntry(cookie));
269
+ if (valid.length > 0) normalized.cookies = valid;
270
+ else delete normalized.cookies;
271
+ }
272
+ return normalized;
273
+ }
274
+ function normalizeSession(value, source) {
275
+ const normalized = normalizeSessionValue(value, source);
276
+ if (!normalized) throw new require_errors.ItdConfigError(`${source} должна быть объектом сессии`);
277
+ return normalized;
278
+ }
220
279
  let authScopeSequence = 0;
221
280
  function nextAuthScope() {
222
281
  authScopeSequence += 1;
@@ -243,7 +302,7 @@ var AuthManager = class {
243
302
  * старте читают хранилище один раз и не заводят каждый свой `deviceId`.
244
303
  */
245
304
  #loading = null;
246
- /** Общий промис обновления: к нему присоединяются все, кто получил 401. */
305
+ /** Текущее обновление: к нему присоединяются запросы той же ревизии. */
247
306
  #refreshing = null;
248
307
  /** Общий промис входа по логину и паролю. */
249
308
  #signingIn = null;
@@ -253,6 +312,7 @@ var AuthManager = class {
253
312
  #externalToken;
254
313
  /** Идентификаторы последнего токена внешнего источника для синхронных потребителей. */
255
314
  #externalIdentity;
315
+ #tokenMetadata;
256
316
  /**
257
317
  * Идентификатор устройства.
258
318
  *
@@ -266,11 +326,12 @@ var AuthManager = class {
266
326
  * Счётчик смен владельца авторизации.
267
327
  *
268
328
  * Растёт при каждой операции, которая заменяет или очищает сессию извне: `clear`,
269
- * `setSession`, `setAccessToken`, вход. `#performRefresh` снимает его значение перед
270
- * сетевым запросом и сверяет перед записью — иначе запоздавший ответ обновления мог бы
271
- * воскресить уже очищенную сессию поверх `signOut`.
329
+ * `setSession`, `setAccessToken`, вход. Refresh сверяет снимок до и после сетевого запроса.
272
330
  */
273
331
  #authEpoch = 0;
332
+ #externalReadGeneration = 0;
333
+ #disposed = false;
334
+ #persistence = Promise.resolve();
274
335
  constructor(config, http, jar, hooks = {}) {
275
336
  this.#config = config;
276
337
  this.#http = http;
@@ -280,15 +341,25 @@ var AuthManager = class {
280
341
  }
281
342
  /** Подписка на события авторизации. */
282
343
  get on() {
283
- return this.#emitter.on.bind(this.#emitter);
344
+ return ((...args) => {
345
+ this.#assertActive();
346
+ return this.#emitter.on(...args);
347
+ });
284
348
  }
285
349
  /** Подписка на одно срабатывание. */
286
350
  get once() {
287
- return this.#emitter.once.bind(this.#emitter);
351
+ return ((...args) => {
352
+ this.#assertActive();
353
+ return this.#emitter.once(...args);
354
+ });
288
355
  }
289
356
  /** Снимает служебные подписки при окончательном освобождении владельца. @internal */
290
357
  dispose() {
358
+ if (this.#disposed) return;
359
+ this.#disposed = true;
360
+ this.#externalReadGeneration += 1;
291
361
  this.#invalidateInFlight();
362
+ this.#tokenMetadata = void 0;
292
363
  this.#emitter.removeAllListeners();
293
364
  }
294
365
  /**
@@ -302,6 +373,7 @@ var AuthManager = class {
302
373
  }
303
374
  /** Загружает сессию и возвращает идентификаторы аккаунта и сессии для плагинов. */
304
375
  async getAuthIdentity() {
376
+ this.#assertActive();
305
377
  const session = await this.#loadSession();
306
378
  if (session?.accessToken) return this.#identityForToken(session.accessToken);
307
379
  const auth = this.#config.auth;
@@ -322,16 +394,57 @@ var AuthManager = class {
322
394
  /** Отмечает смену владельца авторизации — обесценивает результат идущего обновления. */
323
395
  #invalidateInFlight() {
324
396
  this.#authEpoch += 1;
397
+ this.#externalReadGeneration += 1;
398
+ }
399
+ /** Ревизия владельца сессии для условного commit внешнего auth-flow. @internal */
400
+ revision() {
401
+ this.#assertActive();
402
+ return this.#authEpoch;
403
+ }
404
+ /** Создаёт изолированный jar для сетевого auth-flow. @internal */
405
+ createCookieFlow(inherit = true) {
406
+ this.#assertActive();
407
+ return inherit ? this.#jar.clone() : new require_redact.CookieJar();
408
+ }
409
+ /** Применяет cookies только если владелец сессии не сменился. @internal */
410
+ commitCookieFlow(cookies, expectedRevision) {
411
+ this.#assertActive();
412
+ if (this.#authEpoch !== expectedRevision) return false;
413
+ this.#jar.replaceWith(cookies);
414
+ return true;
415
+ }
416
+ #assertActive() {
417
+ if (this.#disposed) throw new require_errors.ItdStateError("Менеджер авторизации уже освобождён");
418
+ }
419
+ #currentAccessToken() {
420
+ return this.#session?.accessToken ?? null;
325
421
  }
326
422
  #identityForToken(accessToken) {
327
- const token = accessToken ? readTokenIdentity(accessToken) : {};
423
+ const token = this.#metadataForToken(accessToken);
328
424
  return {
329
425
  ...token.subject ? { userId: token.subject } : {},
330
426
  ...token.sessionId ? { sessionId: token.sessionId } : {}
331
427
  };
332
428
  }
429
+ #metadataForToken(accessToken) {
430
+ if (!accessToken) {
431
+ this.#tokenMetadata = void 0;
432
+ return {};
433
+ }
434
+ if (this.#tokenMetadata?.accessToken === accessToken) return this.#tokenMetadata.value;
435
+ const value = readTokenMetadata(accessToken);
436
+ this.#tokenMetadata = {
437
+ accessToken,
438
+ value
439
+ };
440
+ return value;
441
+ }
333
442
  async #readExternalToken(getToken) {
443
+ this.#assertActive();
444
+ const generation = ++this.#externalReadGeneration;
334
445
  const token = await getToken() ?? null;
446
+ this.#assertActive();
447
+ if (generation !== this.#externalReadGeneration) return token;
335
448
  const current = this.#identityForToken(token ?? void 0);
336
449
  if (this.#externalToken !== void 0 && this.#externalToken !== token) {
337
450
  this.#rotateAuthScope();
@@ -362,6 +475,7 @@ var AuthManager = class {
362
475
  * ответ был бы `false` даже при полностью рабочей сохранённой сессии.
363
476
  */
364
477
  async hasRefreshSession() {
478
+ this.#assertActive();
365
479
  await this.#loadSession();
366
480
  return this.#hasRefreshSession();
367
481
  }
@@ -371,14 +485,33 @@ var AuthManager = class {
371
485
  if (this.#jar.has("is_auth", this.#config.baseUrl)) return true;
372
486
  return Boolean(this.#session?.refreshToken);
373
487
  }
374
- /**
375
- * Готовит состояние авторизации до входа запроса в очередь. Стадия `auth_preparation`.
376
- *
377
- * Загрузка хранилища, внешний источник токена и отложенный вход асинхронны, поэтому
378
- * обязаны завершиться до захвата слота очереди.
379
- */
488
+ /** Готовит внутреннюю сессию до retry исходного запроса. */
489
+ async preflight(allowRefresh) {
490
+ this.#assertActive();
491
+ let accessToken = (await this.#loadSession())?.accessToken;
492
+ if (!accessToken) {
493
+ const auth = this.#config.auth;
494
+ if (typeof auth === "object" && auth !== null && "email" in auth) accessToken = await this.#signInWithCredentials(auth) ?? void 0;
495
+ }
496
+ if (!accessToken || !allowRefresh || !this.#config.autoRefresh) return;
497
+ const expiresAt = this.#metadataForToken(accessToken).expiresAt;
498
+ if (expiresAt === void 0 || expiresAt > this.#config.clock.now() + ACCESS_TOKEN_REFRESH_LEEWAY_MS || !this.#hasRefreshSession()) return;
499
+ try {
500
+ await this.#refreshDeduplicated({
501
+ epoch: this.#authEpoch,
502
+ accessToken
503
+ });
504
+ } catch (error) {
505
+ this.#emitter.emit("authError", { error });
506
+ throw error;
507
+ }
508
+ }
509
+ /** Перед каждой попыткой перечитывает внешний источник токена. */
380
510
  async prepare() {
381
- await this.token();
511
+ this.#assertActive();
512
+ if ((await this.#loadSession())?.accessToken) return;
513
+ const auth = this.#config.auth;
514
+ if (typeof auth === "object" && auth !== null && "getToken" in auth) await this.#readExternalToken(() => auth.getToken());
382
515
  }
383
516
  /**
384
517
  * Заголовки уже подготовленной авторизации без чтения storage или вызова внешнего источника.
@@ -397,6 +530,7 @@ var AuthManager = class {
397
530
  * через `itd.auth.refresh()`, а библиотека просто пробрасывает {@link ItdAuthError}.
398
531
  */
399
532
  recover() {
533
+ this.#assertActive();
400
534
  return this.#config.autoRefresh ? this.onUnauthorized() : Promise.resolve(false);
401
535
  }
402
536
  /**
@@ -407,6 +541,7 @@ var AuthManager = class {
407
541
  * по новой сессии на каждый старт.
408
542
  */
409
543
  deviceId() {
544
+ this.#assertActive();
410
545
  if (this.#deviceId) return Promise.resolve(this.#deviceId);
411
546
  this.#deviceIdLoading ??= this.#resolveDeviceId().finally(() => {
412
547
  this.#deviceIdLoading = null;
@@ -414,9 +549,12 @@ var AuthManager = class {
414
549
  return this.#deviceIdLoading;
415
550
  }
416
551
  async #resolveDeviceId() {
552
+ const revision = this.#authEpoch;
417
553
  const session = await this.#loadSession();
554
+ this.#assertActive();
418
555
  const deviceId = this.#config.deviceId ?? session?.deviceId ?? require_redact.createDeviceId();
419
556
  this.#deviceId = deviceId;
557
+ if (this.#authEpoch !== revision) return deviceId;
420
558
  if (session?.deviceId !== deviceId) await this.#saveSession({
421
559
  ...session ?? {},
422
560
  deviceId
@@ -430,6 +568,7 @@ var AuthManager = class {
430
568
  * и пароль, первый же запрос сам заведёт сессию.
431
569
  */
432
570
  async token() {
571
+ this.#assertActive();
433
572
  const session = await this.#loadSession();
434
573
  if (session?.accessToken) return session.accessToken;
435
574
  const auth = this.#config.auth;
@@ -444,6 +583,7 @@ var AuthManager = class {
444
583
  * @returns `true`, если токен обновлён и запрос имеет смысл повторить
445
584
  */
446
585
  async onUnauthorized() {
586
+ this.#assertActive();
447
587
  try {
448
588
  if (await this.#refreshDeduplicated() !== null) return true;
449
589
  this.#emitter.emit("authError", { error: this.#noRefreshSessionError() });
@@ -479,24 +619,33 @@ var AuthManager = class {
479
619
  * @throws {ItdAuthError} если обновить сессию не удалось
480
620
  */
481
621
  async refresh() {
622
+ this.#assertActive();
482
623
  const token = await this.#refreshDeduplicated();
483
624
  if (token === null) throw this.#noRefreshSessionError();
484
625
  return token;
485
626
  }
486
627
  /** Сохраняет токен, полученный извне, — например после подтверждения OTP. */
487
- async setAccessToken(accessToken) {
628
+ async setAccessToken(accessToken, expectedRevision = this.#authEpoch, cookies) {
629
+ this.#assertActive();
630
+ if (typeof accessToken !== "string" || accessToken.trim() === "") throw new require_errors.ItdConfigError("accessToken должен быть непустой строкой");
488
631
  await this.#loadSession();
632
+ this.#assertActive();
633
+ if (this.#authEpoch !== expectedRevision) return false;
634
+ if (cookies) this.#jar.replaceWith(cookies);
489
635
  this.#invalidateInFlight();
490
636
  this.#transitionAuth(accessToken);
491
- await this.#saveSession({
637
+ const saved = this.#saveSession({
492
638
  ...this.#session ?? {},
493
639
  accessToken,
494
640
  obtainedAt: this.#config.clock.now()
495
641
  });
496
642
  this.#emitter.emit("tokens", { accessToken });
643
+ await saved;
644
+ return true;
497
645
  }
498
646
  /** Текущая сессия целиком. Полезно, чтобы сохранить её самому. */
499
647
  async getSession() {
648
+ this.#assertActive();
500
649
  const session = await this.#loadSession();
501
650
  return session ? require_storage.copySession(session) : null;
502
651
  }
@@ -507,20 +656,24 @@ var AuthManager = class {
507
656
  * токена идентификатор прежнего владельца остаться не может.
508
657
  */
509
658
  async getUserId() {
659
+ this.#assertActive();
510
660
  const session = await this.#loadSession();
511
- return session?.accessToken ? readTokenSubject(session.accessToken) : void 0;
661
+ return session?.accessToken ? this.#identityForToken(session.accessToken).userId : void 0;
512
662
  }
513
663
  /** Заменяет сессию и связанные с ней cookie целиком. */
514
664
  async setSession(session) {
665
+ this.#assertActive();
666
+ const normalized = normalizeSession(session, "session");
515
667
  await this.#loadSession();
668
+ this.#assertActive();
516
669
  this.#invalidateInFlight();
517
- this.#transitionAuth(session.accessToken);
670
+ this.#transitionAuth(normalized.accessToken);
518
671
  this.#jar.clear();
519
- this.#jar.deserialize(session.cookies);
520
- if (session.deviceId) this.#deviceId = session.deviceId;
521
- this.#session = session;
672
+ this.#jar.deserialize(normalized.cookies);
673
+ if (normalized.deviceId) this.#deviceId = normalized.deviceId;
674
+ this.#session = normalized;
522
675
  this.#seedRefreshCookie();
523
- await this.#saveSession(session);
676
+ await this.#saveSession(normalized);
524
677
  }
525
678
  /**
526
679
  * Забывает сессию и cookie. Сетевой запрос не выполняется.
@@ -529,14 +682,19 @@ var AuthManager = class {
529
682
  * новую запись в списке сессий.
530
683
  */
531
684
  async clear() {
685
+ this.#assertActive();
532
686
  await this.#loadSession();
687
+ this.#assertActive();
533
688
  this.#invalidateInFlight();
534
689
  this.#transitionAuth(void 0);
535
- this.#session = null;
536
690
  this.#jar.clear();
537
- await this.#config.storage.clear();
538
- if (this.#deviceId) await this.#saveSession({ deviceId: this.#deviceId });
691
+ const retained = this.#deviceId ? { deviceId: this.#deviceId } : null;
692
+ this.#session = retained;
539
693
  this.#emitter.emit("signOut", void 0);
694
+ await this.#enqueuePersistence(async () => {
695
+ await this.#config.storage.clear();
696
+ if (retained) await this.#config.storage.set(require_storage.copySession(retained));
697
+ });
540
698
  }
541
699
  #loadSession() {
542
700
  if (this.#session !== void 0) return Promise.resolve(this.#session);
@@ -547,7 +705,8 @@ var AuthManager = class {
547
705
  }
548
706
  async #performLoad() {
549
707
  const loaded = await this.#config.storage.get() ?? null;
550
- const stored = loaded ? withoutLegacyUserId(loaded) : null;
708
+ this.#assertActive();
709
+ const stored = loaded === null ? null : normalizeSessionValue(loaded, "storage.get()", "stored", this.#config.logger);
551
710
  if (stored?.cookies) this.#jar.deserialize(stored.cookies);
552
711
  const fromConfig = this.#sessionFromConfig(this.#config.auth);
553
712
  this.#session = stored && fromConfig ? {
@@ -569,7 +728,7 @@ var AuthManager = class {
569
728
  if (!this.#config.useCookieJar) return;
570
729
  const refreshToken = this.#session?.refreshToken;
571
730
  if (!refreshToken) return;
572
- if (this.#jar.has("refresh_token")) return;
731
+ if (this.#jar.has("refresh_token", this.#config.baseUrl + "/api/v1/auth")) return;
573
732
  this.#jar.set(this.#config.baseUrl, require_redact.REFRESH_COOKIE, refreshToken, require_redact.REFRESH_COOKIE_PATH);
574
733
  }
575
734
  #sessionFromConfig(auth) {
@@ -588,56 +747,76 @@ var AuthManager = class {
588
747
  async #saveSession(session) {
589
748
  const cookies = this.#config.useCookieJar ? this.#jar.serialize() : void 0;
590
749
  const deviceId = session.deviceId ?? this.#deviceId;
750
+ const { cookies: _staleCookies, ...withoutCookies } = require_storage.copySession(session);
591
751
  const next = withoutLegacyUserId({
592
- ...session,
752
+ ...withoutCookies,
593
753
  ...cookies?.length ? { cookies } : {},
594
754
  ...deviceId ? { deviceId } : {}
595
755
  });
596
756
  this.#session = next;
597
- await this.#config.storage.set(next);
598
- }
599
- /**
600
- * Обновление с дедупликацией.
601
- *
602
- * Все, кто пришёл, пока обновление уже идёт, получают его результат, а не запускают своё.
603
- */
604
- #refreshDeduplicated() {
605
- if (this.#refreshing) return this.#refreshing;
606
- const promise = this.#performRefresh().finally(() => {
607
- this.#refreshing = null;
757
+ await this.#enqueuePersistence(() => this.#config.storage.set(require_storage.copySession(next)));
758
+ }
759
+ #enqueuePersistence(operation) {
760
+ const pending = this.#persistence.then(operation, operation);
761
+ this.#persistence = pending.catch(() => {});
762
+ return pending;
763
+ }
764
+ /** Дедуплицирует refresh одной ревизии; следующая ждёт завершения предыдущей. */
765
+ #refreshDeduplicated(expectation = { epoch: this.#authEpoch }) {
766
+ const current = this.#refreshing;
767
+ if (current) {
768
+ if (current.epoch === expectation.epoch) return current.promise;
769
+ return current.promise.then(() => this.#refreshDeduplicated(expectation), () => this.#refreshDeduplicated(expectation));
770
+ }
771
+ const promise = this.#performRefresh(expectation).finally(() => {
772
+ if (this.#refreshing?.promise === promise) this.#refreshing = null;
608
773
  });
609
- this.#refreshing = promise;
774
+ this.#refreshing = {
775
+ epoch: expectation.epoch,
776
+ promise
777
+ };
610
778
  return promise;
611
779
  }
612
- async #performRefresh() {
780
+ #matchesRefreshExpectation(expectation) {
781
+ return this.#authEpoch === expectation.epoch && (expectation.accessToken === void 0 || this.#session?.accessToken === expectation.accessToken);
782
+ }
783
+ async #performRefresh(expectation) {
784
+ this.#assertActive();
613
785
  await this.#loadSession();
786
+ if (!this.#matchesRefreshExpectation(expectation)) return this.#currentAccessToken();
614
787
  if (!this.#hasRefreshSession()) return this.#reloginOrNull();
615
- const epoch = this.#authEpoch;
788
+ const flowCookies = this.#jar.clone();
616
789
  try {
617
790
  const accessToken = await this.#http.execute(AUTH_REFRESH, {
618
791
  path: AUTH_PATHS.refresh,
619
792
  skipAuth: true,
620
- skipAuthRefresh: true
793
+ skipAuthRefresh: true,
794
+ cookieJar: flowCookies
621
795
  });
622
- if (!accessToken) return this.#reloginOrNull();
623
- if (this.#authEpoch !== epoch) return this.#session?.accessToken ?? null;
796
+ if (!this.#matchesRefreshExpectation(expectation)) return this.#session?.accessToken ?? null;
797
+ this.#jar.replaceWith(flowCookies);
624
798
  const rotated = this.#jar.getValue(require_redact.REFRESH_COOKIE, this.#config.baseUrl + require_redact.REFRESH_COOKIE_PATH);
625
799
  this.#transitionAuth(accessToken, false);
626
- await this.#saveSession({
800
+ const saved = this.#saveSession({
627
801
  ...this.#session ?? {},
628
802
  accessToken,
629
803
  ...rotated ? { refreshToken: rotated } : {},
630
804
  obtainedAt: this.#config.clock.now()
631
805
  });
632
806
  this.#emitter.emit("tokens", { accessToken });
807
+ await saved;
808
+ if (this.#authEpoch !== expectation.epoch) return this.#currentAccessToken();
633
809
  return accessToken;
634
810
  } catch (error) {
635
- if (error instanceof require_errors.ItdApiError) {
636
- if (this.#authEpoch !== epoch) return this.#session?.accessToken ?? null;
811
+ if (this.#authEpoch !== expectation.epoch) return this.#currentAccessToken();
812
+ if (error instanceof require_errors.ItdAuthError) {
813
+ this.#invalidateInFlight();
814
+ const clearedRevision = this.#authEpoch;
637
815
  this.#transitionAuth(void 0);
638
816
  this.#session = null;
639
817
  this.#jar.clear();
640
- await this.#config.storage.clear();
818
+ await this.#enqueuePersistence(() => this.#config.storage.clear());
819
+ if (this.#authEpoch !== clearedRevision) return this.#currentAccessToken();
641
820
  const relogged = await this.#reloginOrNull();
642
821
  if (relogged !== null) return relogged;
643
822
  throw error;
@@ -651,7 +830,8 @@ var AuthManager = class {
651
830
  if (!this.#config.reloginOnRefreshFailure || !auth || typeof auth !== "object" || !("email" in auth)) return null;
652
831
  try {
653
832
  return await this.#signInWithCredentials(auth);
654
- } catch {
833
+ } catch (error) {
834
+ this.#config.logger?.warn("Автоматический повторный вход не удался", error);
655
835
  return null;
656
836
  }
657
837
  }
@@ -663,7 +843,8 @@ var AuthManager = class {
663
843
  */
664
844
  #signInWithCredentials(credentials) {
665
845
  if (this.#signingIn) return this.#signingIn;
666
- const promise = this.#performSignIn(credentials).finally(() => {
846
+ const revision = this.#authEpoch;
847
+ const promise = this.#performSignIn(credentials, revision).finally(() => {
667
848
  this.#signingIn = null;
668
849
  });
669
850
  this.#signingIn = promise;
@@ -683,8 +864,9 @@ var AuthManager = class {
683
864
  if (credentials.turnstileToken) return credentials.turnstileToken;
684
865
  throw new require_errors.ItdConfigError("Вход по email и паролю требует токен капчи Cloudflare Turnstile: без него сервер отвечает 422. Передайте auth.getTurnstileToken (источник свежего токена) либо разовый auth.turnstileToken. Ключ виджета — TURNSTILE_SITE_KEY. В Node токен умеет добывать отдельный пакет: npm i @itd-api/turnstile, затем getTurnstileToken: createTurnstileSolver().");
685
866
  }
686
- async #performSignIn(credentials) {
867
+ async #performSignIn(credentials, revision) {
687
868
  const turnstileToken = await this.#resolveTurnstileToken(credentials);
869
+ const flowCookies = new require_redact.CookieJar();
688
870
  const result = await this.#http.execute(AUTH_SIGN_IN, {
689
871
  path: AUTH_PATHS.signIn,
690
872
  body: {
@@ -693,18 +875,23 @@ var AuthManager = class {
693
875
  turnstileToken
694
876
  },
695
877
  skipAuth: true,
696
- skipAuthRefresh: true
878
+ skipAuthRefresh: true,
879
+ cookieJar: flowCookies
697
880
  });
698
881
  if (result.status !== SignInStatus.Authenticated) throw new require_errors.ItdConfigError("Вход по email и паролю требует подтверждения кодом из письма. Автоматический вход невозможен: воспользуйтесь itd.auth.signInWithOtp() и передайте полученный accessToken в конфигурацию клиента.");
699
882
  const accessToken = result.accessToken;
883
+ this.#assertActive();
884
+ if (this.#authEpoch !== revision) return this.#session?.accessToken ?? null;
885
+ this.#jar.replaceWith(flowCookies);
700
886
  this.#invalidateInFlight();
701
887
  this.#transitionAuth(accessToken);
702
- await this.#saveSession({
888
+ const saved = this.#saveSession({
703
889
  accessToken,
704
890
  obtainedAt: this.#config.clock.now()
705
891
  });
706
892
  this.#emitter.emit("tokens", { accessToken });
707
893
  this.#emitter.emit("signIn", { accessToken });
894
+ await saved;
708
895
  return accessToken;
709
896
  }
710
897
  };
@@ -763,14 +950,18 @@ var AuthResource = class extends require_render.BaseResource {
763
950
  * @param credentials email, пароль и обязательный токен капчи — см. {@link CaptchaCredentials}
764
951
  */
765
952
  async signIn(credentials, options = {}) {
953
+ const revision = this.#auth.revision();
954
+ const cookies = this.#auth.createCookieFlow(false);
766
955
  const result = await this.http.execute(AUTH_SIGN_IN, {
767
956
  path: AUTH_PATHS.signIn,
768
957
  body: credentials,
769
958
  skipAuth: true,
770
959
  skipAuthRefresh: true,
960
+ cookieJar: cookies,
771
961
  ...options
772
962
  });
773
- if (result.status === SignInStatus.Authenticated) await this.#auth.setAccessToken(result.accessToken);
963
+ if (result.status === SignInStatus.Authenticated) await this.#auth.setAccessToken(result.accessToken, revision, cookies);
964
+ else this.#auth.commitCookieFlow(cookies, revision);
774
965
  return result;
775
966
  }
776
967
  /**
@@ -779,14 +970,17 @@ var AuthResource = class extends require_render.BaseResource {
779
970
  * Полученный токен сохраняется в клиенте автоматически.
780
971
  */
781
972
  async verifyOtp(input, options = {}) {
973
+ const revision = this.#auth.revision();
974
+ const cookies = this.#auth.createCookieFlow();
782
975
  const accessToken = await this.http.execute(AUTH_VERIFY_OTP, {
783
976
  path: AUTH_PATHS.verifyOtp,
784
977
  body: input,
785
978
  skipAuth: true,
786
979
  skipAuthRefresh: true,
980
+ cookieJar: cookies,
787
981
  ...options
788
982
  });
789
- await this.#auth.setAccessToken(accessToken);
983
+ await this.#auth.setAccessToken(accessToken, revision, cookies);
790
984
  return accessToken;
791
985
  }
792
986
  /** Отправляет код подтверждения повторно. */
@@ -1705,7 +1899,7 @@ var ItdAccounts = class ItdAccounts {
1705
1899
  })));
1706
1900
  const added = [];
1707
1901
  for (const { name, session } of sessions) {
1708
- if (!require_multi_storage.isRestorableSession(session)) continue;
1902
+ if (!require_multi_storage.isRestorableSession(session, this.#base.baseUrl ?? "https://xn--d1ah4a.com")) continue;
1709
1903
  if (this.#clients.has(name) || this.#removing.has(name)) continue;
1710
1904
  this.addAccount(name);
1711
1905
  added.push(name);
@@ -2039,7 +2233,6 @@ exports.comment = require_render.comment;
2039
2233
  exports.createAccounts = createAccounts;
2040
2234
  exports.createClient = createClient;
2041
2235
  exports.createKeyValueStore = require_key_value_store.createKeyValueStore;
2042
- exports.createMemoryShopOrderAccessStorage = require_render.createMemoryShopOrderAccessStorage;
2043
2236
  exports.createMultiTokenStorage = require_multi_storage.createMultiTokenStorage;
2044
2237
  exports.createNotificationEventsClient = require_events.createNotificationEventsClient;
2045
2238
  exports.createRecordKeyValueStore = require_key_value_store.createRecordKeyValueStore;