itd-api 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +114 -489
- package/dist/index.cjs +8781 -415
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4990 -1
- package/dist/index.d.ts +4990 -1
- package/dist/index.js +8688 -2
- package/dist/index.js.map +1 -1
- package/dist/multi-storage-BhcA2Izn.d.ts +198 -0
- package/dist/multi-storage-CyMe404l.js +805 -0
- package/dist/multi-storage-CyMe404l.js.map +1 -0
- package/dist/multi-storage-D1keK2Op.cjs +930 -0
- package/dist/multi-storage-D1keK2Op.cjs.map +1 -0
- package/dist/multi-storage-NDqzRQcD.d.cts +198 -0
- package/dist/node.cjs +225 -548
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +46 -59
- package/dist/node.d.ts +46 -59
- package/dist/node.js +223 -126
- package/dist/node.js.map +1 -1
- package/dist/runtime-CFEsf-jD.cjs +185 -0
- package/dist/runtime-CFEsf-jD.cjs.map +1 -0
- package/dist/runtime-DHxDn8gf.js +126 -0
- package/dist/runtime-DHxDn8gf.js.map +1 -0
- package/dist/storage-BjNRlkbE.d.cts +82 -0
- package/dist/storage-BjNRlkbE.d.ts +82 -0
- package/dist/storage-D9tfHx7Z.js +424 -0
- package/dist/storage-D9tfHx7Z.js.map +1 -0
- package/dist/storage-ycBqLBRB.cjs +615 -0
- package/dist/storage-ycBqLBRB.cjs.map +1 -0
- package/dist/web.cjs +87 -0
- package/dist/web.cjs.map +1 -0
- package/dist/web.d.cts +27 -0
- package/dist/web.d.ts +27 -0
- package/dist/web.js +86 -0
- package/dist/web.js.map +1 -0
- package/package.json +34 -14
- package/dist/chunk-6FB4HTKH.js +0 -7763
- package/dist/chunk-6FB4HTKH.js.map +0 -1
- package/dist/chunk-73CISRBG.cjs +0 -7873
- package/dist/chunk-73CISRBG.cjs.map +0 -1
- package/dist/index-BZF4K90s.d.cts +0 -4961
- package/dist/index-BZF4K90s.d.ts +0 -4961
- package/guides/README.md +0 -24
- package/guides/authentication/README.md +0 -176
- package/guides/authentication/examples/bot-with-session.mjs +0 -98
- package/guides/authentication/examples/turnstile-login.mjs +0 -56
- package/guides/integrations/README.md +0 -62
- package/guides/integrations/examples/proxy.mjs +0 -26
- package/guides/multi-accounts/README.md +0 -143
- package/guides/multi-accounts/examples/multi-accounts.mjs +0 -71
- package/guides/plugins/README.md +0 -253
- package/guides/plugins/examples/cache.mjs +0 -33
- package/guides/plugins/examples/crypto.mjs +0 -54
- package/guides/quickstart/README.md +0 -124
- package/guides/quickstart/examples/quick-start.mjs +0 -44
- package/guides/quickstart/examples/typescript.ts +0 -90
- package/guides/realtime/README.md +0 -109
- package/guides/realtime/examples/notifications.mjs +0 -62
- package/guides/reference/README.md +0 -67
- package/guides/reference/accounts.md +0 -101
- package/guides/reference/auth.md +0 -141
- package/guides/reference/builders.md +0 -135
- package/guides/reference/client.md +0 -184
- package/guides/reference/comments.md +0 -58
- package/guides/reference/discovery.md +0 -81
- package/guides/reference/enums.md +0 -103
- package/guides/reference/errors.md +0 -107
- package/guides/reference/files.md +0 -73
- package/guides/reference/models.md +0 -448
- package/guides/reference/notifications.md +0 -77
- package/guides/reference/pagination.md +0 -82
- package/guides/reference/platform.md +0 -47
- package/guides/reference/posts.md +0 -157
- package/guides/reference/realtime.md +0 -78
- package/guides/reference/reports.md +0 -28
- package/guides/reference/subscription.md +0 -41
- package/guides/reference/users.md +0 -146
- package/guides/reference/verification.md +0 -24
- package/guides/text-markup/README.md +0 -214
- package/guides/text-markup/examples/create-post.mjs +0 -64
|
@@ -0,0 +1,930 @@
|
|
|
1
|
+
const require_storage = require("./storage-ycBqLBRB.cjs");
|
|
2
|
+
//#region node_modules/set-cookie-parser/lib/set-cookie.js
|
|
3
|
+
var defaultParseOptions = {
|
|
4
|
+
decodeValues: true,
|
|
5
|
+
map: false,
|
|
6
|
+
silent: false,
|
|
7
|
+
split: "auto"
|
|
8
|
+
};
|
|
9
|
+
function isForbiddenKey(key) {
|
|
10
|
+
return typeof key !== "string" || key in {};
|
|
11
|
+
}
|
|
12
|
+
function createNullObj() {
|
|
13
|
+
return Object.create(null);
|
|
14
|
+
}
|
|
15
|
+
function isNonEmptyString(str) {
|
|
16
|
+
return typeof str === "string" && !!str.trim();
|
|
17
|
+
}
|
|
18
|
+
function parseString(setCookieValue, options) {
|
|
19
|
+
var parts = setCookieValue.split(";").filter(isNonEmptyString);
|
|
20
|
+
var nameValuePairStr = parts.shift();
|
|
21
|
+
if (!nameValuePairStr) return null;
|
|
22
|
+
var parsed = parseNameValuePair(nameValuePairStr);
|
|
23
|
+
var name = parsed.name;
|
|
24
|
+
var value = parsed.value;
|
|
25
|
+
options = options ? Object.assign({}, defaultParseOptions, options) : defaultParseOptions;
|
|
26
|
+
if (isForbiddenKey(name)) return null;
|
|
27
|
+
try {
|
|
28
|
+
value = options.decodeValues ? decodeURIComponent(value) : value;
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.error("set-cookie-parser: failed to decode cookie value. Set options.decodeValues=false to disable decoding.", e);
|
|
31
|
+
}
|
|
32
|
+
var cookie = createNullObj();
|
|
33
|
+
cookie.name = name;
|
|
34
|
+
cookie.value = value;
|
|
35
|
+
parts.forEach(function(part) {
|
|
36
|
+
var sides = part.split("=");
|
|
37
|
+
var key = sides.shift().trim().toLowerCase();
|
|
38
|
+
if (isForbiddenKey(key)) return;
|
|
39
|
+
var value = sides.join("=").trim();
|
|
40
|
+
if (key === "expires") cookie.expires = new Date(value);
|
|
41
|
+
else if (key === "max-age") {
|
|
42
|
+
var n = parseInt(value, 10);
|
|
43
|
+
if (!Number.isNaN(n)) cookie.maxAge = n;
|
|
44
|
+
} else if (key === "secure") cookie.secure = true;
|
|
45
|
+
else if (key === "httponly") cookie.httpOnly = true;
|
|
46
|
+
else if (key === "samesite") cookie.sameSite = value;
|
|
47
|
+
else if (key === "partitioned") cookie.partitioned = true;
|
|
48
|
+
else if (key) cookie[key] = value;
|
|
49
|
+
});
|
|
50
|
+
return cookie;
|
|
51
|
+
}
|
|
52
|
+
function parseNameValuePair(nameValuePairStr) {
|
|
53
|
+
var name = "";
|
|
54
|
+
var value = "";
|
|
55
|
+
var nameValueArr = nameValuePairStr.split("=");
|
|
56
|
+
if (nameValueArr.length > 1) {
|
|
57
|
+
name = nameValueArr.shift();
|
|
58
|
+
value = nameValueArr.join("=");
|
|
59
|
+
} else value = nameValuePairStr;
|
|
60
|
+
return {
|
|
61
|
+
name,
|
|
62
|
+
value
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function parseSetCookie(input, options) {
|
|
66
|
+
options = options ? Object.assign({}, defaultParseOptions, options) : defaultParseOptions;
|
|
67
|
+
if (!input) if (!options.map) return [];
|
|
68
|
+
else return createNullObj();
|
|
69
|
+
if (input.headers) if (typeof input.headers.getSetCookie === "function") input = input.headers.getSetCookie();
|
|
70
|
+
else if (input.headers["set-cookie"]) input = input.headers["set-cookie"];
|
|
71
|
+
else {
|
|
72
|
+
var sch = input.headers[Object.keys(input.headers).find(function(key) {
|
|
73
|
+
return key.toLowerCase() === "set-cookie";
|
|
74
|
+
})];
|
|
75
|
+
if (!sch && input.headers.cookie && !options.silent) console.warn("Warning: set-cookie-parser appears to have been called on a request object. It is designed to parse Set-Cookie headers from responses, not Cookie headers from requests. Set the option {silent: true} to suppress this warning.");
|
|
76
|
+
input = sch;
|
|
77
|
+
}
|
|
78
|
+
var split = options.split;
|
|
79
|
+
var isArray = Array.isArray(input);
|
|
80
|
+
if (split === "auto") split = !isArray;
|
|
81
|
+
if (!isArray) input = [input];
|
|
82
|
+
input = input.filter(isNonEmptyString);
|
|
83
|
+
if (split) input = input.map(splitCookiesString).flat();
|
|
84
|
+
if (!options.map) return input.map(function(str) {
|
|
85
|
+
return parseString(str, options);
|
|
86
|
+
}).filter(Boolean);
|
|
87
|
+
else {
|
|
88
|
+
var cookies = createNullObj();
|
|
89
|
+
return input.reduce(function(cookies, str) {
|
|
90
|
+
var cookie = parseString(str, options);
|
|
91
|
+
if (cookie && !isForbiddenKey(cookie.name)) cookies[cookie.name] = cookie;
|
|
92
|
+
return cookies;
|
|
93
|
+
}, cookies);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function splitCookiesString(cookiesString) {
|
|
97
|
+
if (Array.isArray(cookiesString)) return cookiesString;
|
|
98
|
+
if (typeof cookiesString !== "string") return [];
|
|
99
|
+
var cookiesStrings = [];
|
|
100
|
+
var pos = 0;
|
|
101
|
+
var start;
|
|
102
|
+
var ch;
|
|
103
|
+
var lastComma;
|
|
104
|
+
var nextStart;
|
|
105
|
+
var cookiesSeparatorFound;
|
|
106
|
+
function skipWhitespace() {
|
|
107
|
+
while (pos < cookiesString.length && /\s/.test(cookiesString.charAt(pos))) pos += 1;
|
|
108
|
+
return pos < cookiesString.length;
|
|
109
|
+
}
|
|
110
|
+
function notSpecialChar() {
|
|
111
|
+
ch = cookiesString.charAt(pos);
|
|
112
|
+
return ch !== "=" && ch !== ";" && ch !== ",";
|
|
113
|
+
}
|
|
114
|
+
while (pos < cookiesString.length) {
|
|
115
|
+
start = pos;
|
|
116
|
+
cookiesSeparatorFound = false;
|
|
117
|
+
while (skipWhitespace()) {
|
|
118
|
+
ch = cookiesString.charAt(pos);
|
|
119
|
+
if (ch === ",") {
|
|
120
|
+
lastComma = pos;
|
|
121
|
+
pos += 1;
|
|
122
|
+
skipWhitespace();
|
|
123
|
+
nextStart = pos;
|
|
124
|
+
while (pos < cookiesString.length && notSpecialChar()) pos += 1;
|
|
125
|
+
if (pos < cookiesString.length && cookiesString.charAt(pos) === "=") {
|
|
126
|
+
cookiesSeparatorFound = true;
|
|
127
|
+
pos = nextStart;
|
|
128
|
+
cookiesStrings.push(cookiesString.substring(start, lastComma));
|
|
129
|
+
start = pos;
|
|
130
|
+
} else pos = lastComma + 1;
|
|
131
|
+
} else pos += 1;
|
|
132
|
+
}
|
|
133
|
+
if (!cookiesSeparatorFound || pos >= cookiesString.length) cookiesStrings.push(cookiesString.substring(start, cookiesString.length));
|
|
134
|
+
}
|
|
135
|
+
return cookiesStrings;
|
|
136
|
+
}
|
|
137
|
+
parseSetCookie.parseSetCookie = parseSetCookie;
|
|
138
|
+
parseSetCookie.parse = parseSetCookie;
|
|
139
|
+
parseSetCookie.parseString = parseString;
|
|
140
|
+
parseSetCookie.splitCookiesString = splitCookiesString;
|
|
141
|
+
//#endregion
|
|
142
|
+
//#region src/core/cookies.ts
|
|
143
|
+
/** Имя cookie-флага «есть refresh-сессия». Ставится сайтом итд.com рядом с refresh-токеном. */
|
|
144
|
+
const AUTH_FLAG_COOKIE = "is_auth";
|
|
145
|
+
/**
|
|
146
|
+
* Имя cookie с refresh-токеном.
|
|
147
|
+
*
|
|
148
|
+
* `POST /api/v1/auth/refresh` читает токен **только отсюда** — тело запроса сервер игнорирует.
|
|
149
|
+
* Cookie помечена `HttpOnly`, поэтому в браузере её не прочитать и не выставить; вне браузера
|
|
150
|
+
* она проходит через jar, и туда же кладётся токен, переданный строкой.
|
|
151
|
+
*/
|
|
152
|
+
const REFRESH_COOKIE = "refresh_token";
|
|
153
|
+
/** Путь, которым сервер ограничивает {@link REFRESH_COOKIE}. */
|
|
154
|
+
const REFRESH_COOKIE_PATH = "/api/v1/auth";
|
|
155
|
+
/** Разделитель origin и содержимого cookie при сериализации. В origin пробелов не бывает. */
|
|
156
|
+
const SERIALIZED_SEPARATOR = " ";
|
|
157
|
+
function originOf(url) {
|
|
158
|
+
try {
|
|
159
|
+
return new URL(url).origin;
|
|
160
|
+
} catch {
|
|
161
|
+
return "";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Дата в миллисекунды. Некорректная дата считается отсутствующей, а не `NaN`. */
|
|
165
|
+
function toTimestamp(date) {
|
|
166
|
+
if (!date) return void 0;
|
|
167
|
+
const time = date.getTime();
|
|
168
|
+
return Number.isFinite(time) ? time : void 0;
|
|
169
|
+
}
|
|
170
|
+
function pathOf(url) {
|
|
171
|
+
try {
|
|
172
|
+
return new URL(url).pathname || "/";
|
|
173
|
+
} catch {
|
|
174
|
+
return "/";
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Подходит ли путь cookie запросу.
|
|
179
|
+
*
|
|
180
|
+
* Правило из RFC 6265: путь cookie должен быть префиксом пути запроса, причём граница
|
|
181
|
+
* должна проходить по сегменту — `/api` не подходит запросу `/apiary`.
|
|
182
|
+
*/
|
|
183
|
+
function pathMatches(cookiePath, requestPath) {
|
|
184
|
+
if (cookiePath === "/" || cookiePath === requestPath) return true;
|
|
185
|
+
if (!requestPath.startsWith(cookiePath)) return false;
|
|
186
|
+
return cookiePath.endsWith("/") || requestPath[cookiePath.length] === "/";
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Минимальное хранилище cookie для сред без своего.
|
|
190
|
+
*
|
|
191
|
+
* Refresh-токен итд.com приходит в `Set-Cookie`, а `fetch` вне браузера cookie не хранит —
|
|
192
|
+
* без jar сессию не продлить. В браузере и React Native не используется: там cookie ведёт
|
|
193
|
+
* сама среда.
|
|
194
|
+
*
|
|
195
|
+
* Реализована практическая часть RFC 6265: origin, путь, срок жизни, флаг `Secure`.
|
|
196
|
+
* Доменные cookie для поддоменов намеренно не поддерживаются — API работает с одного хоста.
|
|
197
|
+
*/
|
|
198
|
+
var CookieJar = class {
|
|
199
|
+
#byOrigin = /* @__PURE__ */ new Map();
|
|
200
|
+
/**
|
|
201
|
+
* Забирает `Set-Cookie` из ответа.
|
|
202
|
+
*
|
|
203
|
+
* Использует `Headers.getSetCookie()`, где он есть (Node 20+, undici). В остальных средах
|
|
204
|
+
* заголовки склеены в одну строку, и её нельзя резать по запятой напрямую: запятая есть
|
|
205
|
+
* внутри `Expires=Wed, 09 Jun 2027 …`. Разделением занимается `set-cookie-parser`.
|
|
206
|
+
*/
|
|
207
|
+
setFromResponse(url, response) {
|
|
208
|
+
const headers = response.headers;
|
|
209
|
+
const raw = typeof headers.getSetCookie === "function" ? headers.getSetCookie() : splitCookiesString(headers.get("set-cookie") ?? "");
|
|
210
|
+
if (raw.length > 0) this.setFromStrings(url, raw);
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Кладёт cookie напрямую, минуя `Set-Cookie`.
|
|
214
|
+
*
|
|
215
|
+
* Нужно ровно в одном случае: пользователь передал refresh-токен строкой, а сервер читает
|
|
216
|
+
* его только из cookie. Значение не кодируется — оно уходит в заголовок как есть.
|
|
217
|
+
*/
|
|
218
|
+
set(url, name, value, path = "/") {
|
|
219
|
+
this.setFromStrings(url, [`${name}=${value}; Path=${path}`]);
|
|
220
|
+
}
|
|
221
|
+
/** Сохраняет cookie из готовых строк `Set-Cookie`. */
|
|
222
|
+
setFromStrings(url, setCookieStrings) {
|
|
223
|
+
const origin = originOf(url);
|
|
224
|
+
if (!origin) return;
|
|
225
|
+
const jar = this.#byOrigin.get(origin) ?? /* @__PURE__ */ new Map();
|
|
226
|
+
for (const parsed of parseSetCookie(setCookieStrings)) {
|
|
227
|
+
const expires = toTimestamp(parsed.expires);
|
|
228
|
+
const expiresAt = (typeof parsed.maxAge === "number" && Number.isFinite(parsed.maxAge) ? Date.now() + parsed.maxAge * 1e3 : void 0) ?? expires;
|
|
229
|
+
if (expiresAt !== void 0 && expiresAt <= Date.now()) {
|
|
230
|
+
jar.delete(parsed.name);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
jar.set(parsed.name, {
|
|
234
|
+
name: parsed.name,
|
|
235
|
+
value: parsed.value,
|
|
236
|
+
path: parsed.path ?? "/",
|
|
237
|
+
expires: expiresAt,
|
|
238
|
+
secure: parsed.secure ?? false
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
this.#byOrigin.set(origin, jar);
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Собирает значение заголовка `Cookie` для запроса.
|
|
245
|
+
*
|
|
246
|
+
* @returns строка вида `a=1; b=2` либо `undefined`, если подходящих cookie нет
|
|
247
|
+
*/
|
|
248
|
+
getHeader(url) {
|
|
249
|
+
const cookies = this.#matching(url);
|
|
250
|
+
if (cookies.length === 0) return void 0;
|
|
251
|
+
return cookies.map((cookie) => `${cookie.name}=${cookie.value}`).join("; ");
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* Значение действующей cookie.
|
|
255
|
+
*
|
|
256
|
+
* Нужно, чтобы забрать обновлённый refresh-токен: сервер ротирует его при каждом
|
|
257
|
+
* продлении сессии, и сохранять надо именно новое значение.
|
|
258
|
+
*
|
|
259
|
+
* @param url если указан, учитываются origin, путь и флаг `Secure`
|
|
260
|
+
*/
|
|
261
|
+
getValue(name, url) {
|
|
262
|
+
if (url) return this.#matching(url).find((cookie) => cookie.name === name)?.value;
|
|
263
|
+
const now = Date.now();
|
|
264
|
+
for (const jar of this.#byOrigin.values()) {
|
|
265
|
+
const cookie = jar.get(name);
|
|
266
|
+
if (cookie && (cookie.expires === void 0 || cookie.expires > now)) return cookie.value;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Есть ли действующая cookie с таким именем.
|
|
271
|
+
*
|
|
272
|
+
* Используется для проверки флага {@link AUTH_FLAG_COOKIE} перед запросом обновления
|
|
273
|
+
* токена: у анонима refresh-сессии нет, и дёргать API незачем.
|
|
274
|
+
*/
|
|
275
|
+
has(name, url) {
|
|
276
|
+
if (url) return this.#matching(url).some((cookie) => cookie.name === name);
|
|
277
|
+
const now = Date.now();
|
|
278
|
+
for (const jar of this.#byOrigin.values()) {
|
|
279
|
+
const cookie = jar.get(name);
|
|
280
|
+
if (cookie && (cookie.expires === void 0 || cookie.expires > now)) return true;
|
|
281
|
+
}
|
|
282
|
+
return false;
|
|
283
|
+
}
|
|
284
|
+
/** Сохраняет содержимое jar для записи в {@link TokenStorage}. */
|
|
285
|
+
serialize() {
|
|
286
|
+
const result = [];
|
|
287
|
+
const now = Date.now();
|
|
288
|
+
for (const [origin, jar] of this.#byOrigin) for (const cookie of jar.values()) {
|
|
289
|
+
if (cookie.expires !== void 0 && cookie.expires <= now) continue;
|
|
290
|
+
const parts = [`${cookie.name}=${cookie.value}`, `Path=${cookie.path}`];
|
|
291
|
+
if (cookie.expires !== void 0) parts.push(`Expires=${new Date(cookie.expires).toUTCString()}`);
|
|
292
|
+
if (cookie.secure) parts.push("Secure");
|
|
293
|
+
result.push(`${origin}${SERIALIZED_SEPARATOR}${parts.join("; ")}`);
|
|
294
|
+
}
|
|
295
|
+
return result;
|
|
296
|
+
}
|
|
297
|
+
/** Восстанавливает jar из результата {@link serialize}. Некорректные записи молча пропускаются. */
|
|
298
|
+
deserialize(entries) {
|
|
299
|
+
if (!entries) return;
|
|
300
|
+
for (const entry of entries) {
|
|
301
|
+
const separatorAt = entry.indexOf(SERIALIZED_SEPARATOR);
|
|
302
|
+
if (separatorAt <= 0) continue;
|
|
303
|
+
const origin = entry.slice(0, separatorAt);
|
|
304
|
+
const setCookie = entry.slice(separatorAt + 1);
|
|
305
|
+
if (!originOf(origin)) continue;
|
|
306
|
+
this.setFromStrings(origin, [setCookie]);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/** Удаляет все cookie. */
|
|
310
|
+
clear() {
|
|
311
|
+
this.#byOrigin.clear();
|
|
312
|
+
}
|
|
313
|
+
/** Действующие cookie, подходящие запросу: тот же origin, подходящий путь, не истёкшие. */
|
|
314
|
+
#matching(url) {
|
|
315
|
+
const origin = originOf(url);
|
|
316
|
+
const jar = this.#byOrigin.get(origin);
|
|
317
|
+
if (!jar) return [];
|
|
318
|
+
const isSecureRequest = origin.startsWith("https:");
|
|
319
|
+
const requestPath = pathOf(url);
|
|
320
|
+
const now = Date.now();
|
|
321
|
+
const result = [];
|
|
322
|
+
for (const cookie of jar.values()) {
|
|
323
|
+
if (cookie.expires !== void 0 && cookie.expires <= now) {
|
|
324
|
+
jar.delete(cookie.name);
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (cookie.secure && !isSecureRequest) continue;
|
|
328
|
+
if (!pathMatches(cookie.path, requestPath)) continue;
|
|
329
|
+
result.push(cookie);
|
|
330
|
+
}
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/core/attachments.ts
|
|
336
|
+
/** Способ передачи содержимого вложения. */
|
|
337
|
+
const FileTransferMode = Object.freeze({
|
|
338
|
+
/** Сначала получить файл целиком, затем отправить его как `Blob`. */
|
|
339
|
+
Buffer: "buffer",
|
|
340
|
+
/** Передавать данные по мере чтения, не собирая файл целиком в памяти. */
|
|
341
|
+
Stream: "stream"
|
|
342
|
+
});
|
|
343
|
+
/** Размер очереди потокового вложения по умолчанию — 4 МиБ. */
|
|
344
|
+
const DEFAULT_FILE_STREAM_BUFFER_BYTES = 4 * 1024 * 1024;
|
|
345
|
+
/** Предел размера файла, скачиваемого по адресу, — 100 МиБ. */
|
|
346
|
+
const DEFAULT_URL_FILE_MAX_BYTES = 100 * 1024 * 1024;
|
|
347
|
+
/** Потоки, на которые уже наложены счётчик размера и backpressure. */
|
|
348
|
+
const BOUNDED_FILE_STREAMS = /* @__PURE__ */ new WeakSet();
|
|
349
|
+
/** Проверяет числовую границу до обращения к источнику. */
|
|
350
|
+
function optionalBytes(value, name) {
|
|
351
|
+
if (value === void 0) return void 0;
|
|
352
|
+
if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value)) throw new require_storage.ItdConfigError(`${name} должен быть неотрицательным целым числом, получено: ${value}`);
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
/** Проверяет и дополняет настройки потока. @internal */
|
|
356
|
+
function resolveFileStreamOptions(options, defaultMaxBytes) {
|
|
357
|
+
const mode = options.mode ?? FileTransferMode.Buffer;
|
|
358
|
+
if (mode !== FileTransferMode.Buffer && mode !== FileTransferMode.Stream) throw new require_storage.ItdConfigError(`mode вложения должен быть 'buffer' или 'stream', получено: ${mode}`);
|
|
359
|
+
if (mode === FileTransferMode.Stream && typeof ReadableStream === "undefined") throw new require_storage.ItdConfigError("эта среда не поддерживает ReadableStream; используйте mode: 'buffer'");
|
|
360
|
+
const maxBytes = optionalBytes(options.maxBytes ?? defaultMaxBytes, "maxBytes");
|
|
361
|
+
const streamBufferBytes = optionalBytes(options.streamBufferBytes ?? 4194304, "streamBufferBytes") ?? 4194304;
|
|
362
|
+
if (streamBufferBytes === 0) throw new require_storage.ItdConfigError("streamBufferBytes должен быть больше нуля");
|
|
363
|
+
return {
|
|
364
|
+
mode,
|
|
365
|
+
maxBytes,
|
|
366
|
+
streamBufferBytes
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
/** Убирает параметры MIME и приводит его к форме для сравнения. */
|
|
370
|
+
function normalizeMimeType(contentType) {
|
|
371
|
+
return contentType?.split(";", 1)[0]?.trim().toLowerCase() || void 0;
|
|
372
|
+
}
|
|
373
|
+
/** Достаёт имя файла из пути URL. */
|
|
374
|
+
function filenameFromUrl(url) {
|
|
375
|
+
const last = url.pathname.split("/").pop();
|
|
376
|
+
if (!last) return void 0;
|
|
377
|
+
try {
|
|
378
|
+
return decodeURIComponent(last) || void 0;
|
|
379
|
+
} catch {
|
|
380
|
+
return last;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/** Создаёт ошибку превышения размера. */
|
|
384
|
+
function tooLarge(url, limit, actual) {
|
|
385
|
+
return new require_storage.ItdFileError(`файл${url ? ` по адресу ${url}` : ""} больше предела в ${limit} байт: ${actual}`, {
|
|
386
|
+
reason: require_storage.ItdFileErrorReason.TooLarge,
|
|
387
|
+
...url ? { url } : {},
|
|
388
|
+
limit,
|
|
389
|
+
actual
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
/** Проверяет объявленный размер и возвращает его, если заголовок корректен. */
|
|
393
|
+
function declaredSize(response, maxBytes, url) {
|
|
394
|
+
const header = response.headers.get("content-length");
|
|
395
|
+
if (header === null) return void 0;
|
|
396
|
+
const size = Number(header);
|
|
397
|
+
if (!Number.isFinite(size) || size < 0 || !Number.isInteger(size)) return void 0;
|
|
398
|
+
if (maxBytes !== void 0 && size > maxBytes) throw tooLarge(url, maxBytes, size);
|
|
399
|
+
return size;
|
|
400
|
+
}
|
|
401
|
+
/** Получает HTTP-ответ источника и проверяет его статус. */
|
|
402
|
+
async function fetchFile(target, options, context) {
|
|
403
|
+
let requested;
|
|
404
|
+
try {
|
|
405
|
+
requested = new URL(target);
|
|
406
|
+
} catch {
|
|
407
|
+
throw new require_storage.ItdConfigError(`«${target}» не разбирается как адрес`);
|
|
408
|
+
}
|
|
409
|
+
if (requested.protocol !== "http:" && requested.protocol !== "https:") throw new require_storage.ItdConfigError(`вложение по адресу поддерживает только http и https, получено: ${requested.protocol}`);
|
|
410
|
+
let response;
|
|
411
|
+
try {
|
|
412
|
+
response = await context.fetch(requested, { ...context.signal ? { signal: context.signal } : {} });
|
|
413
|
+
} catch (error) {
|
|
414
|
+
if (context.signal?.aborted || error instanceof Error && error.name === "AbortError") throw error;
|
|
415
|
+
throw new require_storage.ItdFileError(`не удалось получить файл по адресу ${requested.href}`, {
|
|
416
|
+
reason: require_storage.ItdFileErrorReason.Network,
|
|
417
|
+
url: requested.href,
|
|
418
|
+
retryable: true,
|
|
419
|
+
cause: error
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
const finalUrl = response.url ? new URL(response.url) : requested;
|
|
423
|
+
if (!response.ok) {
|
|
424
|
+
await response.body?.cancel().catch(() => {});
|
|
425
|
+
throw new require_storage.ItdFileError(`источник ${finalUrl.href} ответил статусом ${response.status}`, {
|
|
426
|
+
reason: require_storage.ItdFileErrorReason.Http,
|
|
427
|
+
url: finalUrl.href,
|
|
428
|
+
status: response.status,
|
|
429
|
+
retryable: response.status === 408 || response.status === 429 || response.status >= 500
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
const { maxBytes } = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
|
|
433
|
+
let size;
|
|
434
|
+
try {
|
|
435
|
+
size = declaredSize(response, maxBytes, finalUrl.href);
|
|
436
|
+
} catch (error) {
|
|
437
|
+
await response.body?.cancel().catch(() => {});
|
|
438
|
+
throw error;
|
|
439
|
+
}
|
|
440
|
+
return {
|
|
441
|
+
response,
|
|
442
|
+
url: finalUrl,
|
|
443
|
+
size
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/** Приводит произвольный чанк потока к байтам. */
|
|
447
|
+
function asBytes(value) {
|
|
448
|
+
return value instanceof Uint8Array ? value : new Uint8Array(value);
|
|
449
|
+
}
|
|
450
|
+
function isReadableByteStream(value) {
|
|
451
|
+
return typeof ReadableStream !== "undefined" && value instanceof ReadableStream;
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Ограничивает размер и очередь потока. Отмена результирующего потока отменяет исходный.
|
|
455
|
+
*
|
|
456
|
+
* @internal
|
|
457
|
+
*/
|
|
458
|
+
function boundedFileStream(source, options = {}) {
|
|
459
|
+
const maxBytes = optionalBytes(options.maxBytes, "maxBytes");
|
|
460
|
+
const streamBufferBytes = optionalBytes(options.streamBufferBytes ?? 4194304, "streamBufferBytes") ?? 4194304;
|
|
461
|
+
if (streamBufferBytes === 0) throw new require_storage.ItdConfigError("streamBufferBytes должен быть больше нуля");
|
|
462
|
+
const reader = source.getReader();
|
|
463
|
+
let total = 0;
|
|
464
|
+
let finished = false;
|
|
465
|
+
let abortListener;
|
|
466
|
+
const cleanup = () => {
|
|
467
|
+
if (abortListener) options.signal?.removeEventListener("abort", abortListener);
|
|
468
|
+
abortListener = void 0;
|
|
469
|
+
};
|
|
470
|
+
const stream = new ReadableStream({
|
|
471
|
+
start(controller) {
|
|
472
|
+
if (!options.signal) return;
|
|
473
|
+
abortListener = () => {
|
|
474
|
+
finished = true;
|
|
475
|
+
reader.cancel(options.signal?.reason).catch(() => {});
|
|
476
|
+
controller.error(options.signal?.reason ?? new DOMException("Aborted", "AbortError"));
|
|
477
|
+
cleanup();
|
|
478
|
+
};
|
|
479
|
+
if (options.signal.aborted) abortListener();
|
|
480
|
+
else options.signal.addEventListener("abort", abortListener, { once: true });
|
|
481
|
+
},
|
|
482
|
+
async pull(controller) {
|
|
483
|
+
if (finished) return;
|
|
484
|
+
try {
|
|
485
|
+
const next = await reader.read();
|
|
486
|
+
if (finished) return;
|
|
487
|
+
if (next.done) {
|
|
488
|
+
finished = true;
|
|
489
|
+
cleanup();
|
|
490
|
+
controller.close();
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
const chunk = asBytes(next.value);
|
|
494
|
+
total += chunk.byteLength;
|
|
495
|
+
if (maxBytes !== void 0 && total > maxBytes) {
|
|
496
|
+
finished = true;
|
|
497
|
+
await reader.cancel().catch(() => {});
|
|
498
|
+
cleanup();
|
|
499
|
+
controller.error(tooLarge(options.url, maxBytes, total));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
controller.enqueue(chunk);
|
|
503
|
+
} catch (error) {
|
|
504
|
+
finished = true;
|
|
505
|
+
cleanup();
|
|
506
|
+
if (error instanceof require_storage.ItdFileError) {
|
|
507
|
+
controller.error(error);
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
controller.error(new require_storage.ItdFileError("не удалось прочитать поток вложения", {
|
|
511
|
+
reason: require_storage.ItdFileErrorReason.Read,
|
|
512
|
+
...options.url ? { url: options.url } : {},
|
|
513
|
+
retryable: options.retryableRead ?? false,
|
|
514
|
+
cause: error
|
|
515
|
+
}));
|
|
516
|
+
}
|
|
517
|
+
},
|
|
518
|
+
async cancel(reason) {
|
|
519
|
+
finished = true;
|
|
520
|
+
cleanup();
|
|
521
|
+
await reader.cancel(reason).catch(() => {});
|
|
522
|
+
}
|
|
523
|
+
}, {
|
|
524
|
+
highWaterMark: streamBufferBytes,
|
|
525
|
+
size: (chunk) => chunk.byteLength
|
|
526
|
+
});
|
|
527
|
+
BOUNDED_FILE_STREAMS.add(stream);
|
|
528
|
+
return stream;
|
|
529
|
+
}
|
|
530
|
+
/** Наложены ли на поток ограничения библиотеки. @internal */
|
|
531
|
+
function isBoundedFileStream(stream) {
|
|
532
|
+
return BOUNDED_FILE_STREAMS.has(stream);
|
|
533
|
+
}
|
|
534
|
+
/** Читает ответ с контролем размера до создания итогового `Blob`. */
|
|
535
|
+
async function responseBlob(response, url, maxBytes, streamBufferBytes, signal) {
|
|
536
|
+
if (!response.body) {
|
|
537
|
+
const blob = await response.blob();
|
|
538
|
+
if (maxBytes !== void 0 && blob.size > maxBytes) throw tooLarge(url, maxBytes, blob.size);
|
|
539
|
+
return blob;
|
|
540
|
+
}
|
|
541
|
+
const chunks = [];
|
|
542
|
+
const reader = boundedFileStream(response.body, {
|
|
543
|
+
...maxBytes !== void 0 ? { maxBytes } : {},
|
|
544
|
+
streamBufferBytes,
|
|
545
|
+
...signal ? { signal } : {},
|
|
546
|
+
url,
|
|
547
|
+
retryableRead: true
|
|
548
|
+
}).getReader();
|
|
549
|
+
try {
|
|
550
|
+
for (;;) {
|
|
551
|
+
const next = await reader.read();
|
|
552
|
+
if (next.done) break;
|
|
553
|
+
chunks.push(next.value);
|
|
554
|
+
}
|
|
555
|
+
} finally {
|
|
556
|
+
reader.releaseLock();
|
|
557
|
+
}
|
|
558
|
+
return new Blob(chunks.map((chunk) => Uint8Array.from(chunk).buffer));
|
|
559
|
+
}
|
|
560
|
+
/** Скачивает файл целиком с ограничением размера. @internal */
|
|
561
|
+
async function downloadFile(target, options, context) {
|
|
562
|
+
const resolved = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
|
|
563
|
+
const { response, url } = await fetchFile(target, options, context);
|
|
564
|
+
const blob = await responseBlob(response, url.href, resolved.maxBytes, resolved.streamBufferBytes, context.signal);
|
|
565
|
+
const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
|
|
566
|
+
const filename = options.filename ?? filenameFromUrl(url);
|
|
567
|
+
return {
|
|
568
|
+
file: new Blob([blob], { type: contentType ?? "" }),
|
|
569
|
+
...filename ? { filename } : {},
|
|
570
|
+
...contentType ? { contentType } : {}
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
/** Открывает HTTP-ответ как ограниченный поток. @internal */
|
|
574
|
+
async function openUrlFile(target, options, context) {
|
|
575
|
+
const resolved = resolveFileStreamOptions(options, DEFAULT_URL_FILE_MAX_BYTES);
|
|
576
|
+
const { response, url, size } = await fetchFile(target, options, context);
|
|
577
|
+
if (!response.body) throw new require_storage.ItdFileError(`источник ${url.href} не предоставил потоковое тело`, {
|
|
578
|
+
reason: require_storage.ItdFileErrorReason.StreamUnavailable,
|
|
579
|
+
url: url.href
|
|
580
|
+
});
|
|
581
|
+
const stream = boundedFileStream(response.body, {
|
|
582
|
+
...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
|
|
583
|
+
streamBufferBytes: resolved.streamBufferBytes,
|
|
584
|
+
...context.signal ? { signal: context.signal } : {},
|
|
585
|
+
url: url.href,
|
|
586
|
+
retryableRead: true
|
|
587
|
+
});
|
|
588
|
+
const filename = options.filename ?? filenameFromUrl(url);
|
|
589
|
+
const contentType = normalizeMimeType(options.contentType ?? response.headers.get("content-type") ?? void 0);
|
|
590
|
+
return {
|
|
591
|
+
stream,
|
|
592
|
+
...filename ? { filename } : {},
|
|
593
|
+
...contentType ? { contentType } : {},
|
|
594
|
+
...size !== void 0 ? { size } : {},
|
|
595
|
+
close: () => stream.cancel().catch(() => {})
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
function fromUrl(url, options = {}) {
|
|
599
|
+
if (resolveFileStreamOptions(options, 104857600).mode === FileTransferMode.Stream) return { open: (context) => openUrlFile(url, options, context) };
|
|
600
|
+
return { load: (context) => downloadFile(url, options, context) };
|
|
601
|
+
}
|
|
602
|
+
/**
|
|
603
|
+
* Создаёт повторяемый пользовательский поток.
|
|
604
|
+
*
|
|
605
|
+
* Фабрика вызывается заново для каждой попытки; возвращать один и тот же поток нельзя.
|
|
606
|
+
*/
|
|
607
|
+
function fromStream(factory, options = {}) {
|
|
608
|
+
const resolved = resolveFileStreamOptions({
|
|
609
|
+
...options,
|
|
610
|
+
mode: FileTransferMode.Stream
|
|
611
|
+
}, void 0);
|
|
612
|
+
optionalBytes(options.size, "size");
|
|
613
|
+
return { open: async (context) => {
|
|
614
|
+
let opened;
|
|
615
|
+
try {
|
|
616
|
+
opened = await factory(context);
|
|
617
|
+
} catch (error) {
|
|
618
|
+
if (error instanceof require_storage.ItdFileError || error instanceof require_storage.ItdConfigError || context.signal?.aborted) throw error;
|
|
619
|
+
throw new require_storage.ItdFileError("не удалось открыть поток вложения", {
|
|
620
|
+
reason: require_storage.ItdFileErrorReason.Read,
|
|
621
|
+
retryable: true,
|
|
622
|
+
cause: error
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
const content = isReadableByteStream(opened) ? { stream: opened } : opened;
|
|
626
|
+
if (!content || !isReadableByteStream(content.stream)) throw new require_storage.ItdConfigError("fromStream должен вернуть ReadableStream или { stream }");
|
|
627
|
+
const size = options.size ?? content.size;
|
|
628
|
+
optionalBytes(size, "size");
|
|
629
|
+
if (resolved.maxBytes !== void 0 && size !== void 0 && size > resolved.maxBytes) {
|
|
630
|
+
await content.close?.();
|
|
631
|
+
throw tooLarge(void 0, resolved.maxBytes, size);
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
stream: boundedFileStream(content.stream, {
|
|
635
|
+
...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
|
|
636
|
+
streamBufferBytes: resolved.streamBufferBytes,
|
|
637
|
+
...context.signal ? { signal: context.signal } : {},
|
|
638
|
+
retryableRead: true
|
|
639
|
+
}),
|
|
640
|
+
...options.filename ?? content.filename ? { filename: options.filename ?? content.filename } : {},
|
|
641
|
+
...normalizeMimeType(options.contentType ?? content.contentType) ? { contentType: normalizeMimeType(options.contentType ?? content.contentType) } : {},
|
|
642
|
+
...size !== void 0 ? { size } : {},
|
|
643
|
+
...content.close ? { close: content.close } : {}
|
|
644
|
+
};
|
|
645
|
+
} };
|
|
646
|
+
}
|
|
647
|
+
//#endregion
|
|
648
|
+
//#region src/core/multi-storage.ts
|
|
649
|
+
/**
|
|
650
|
+
* Срез мультихранилища как обычное {@link TokenStorage} — в таком виде его получает
|
|
651
|
+
* отдельный `ItdClient`, который про соседние аккаунты ничего не знает.
|
|
652
|
+
*/
|
|
653
|
+
function scopedTokenStorage(storage, account) {
|
|
654
|
+
return {
|
|
655
|
+
get: () => storage.get(account),
|
|
656
|
+
set: (session) => storage.set(account, session),
|
|
657
|
+
clear: () => storage.clear(account)
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Можно ли восстановить авторизованный аккаунт из записи.
|
|
662
|
+
*
|
|
663
|
+
* Один `deviceId` не считается сессией: он намеренно переживает выход, но не позволяет
|
|
664
|
+
* выполнить ни одного авторизованного запроса.
|
|
665
|
+
*
|
|
666
|
+
* @internal
|
|
667
|
+
*/
|
|
668
|
+
function isRestorableSession(session) {
|
|
669
|
+
if (!session) return false;
|
|
670
|
+
if (session.accessToken || session.refreshToken) return true;
|
|
671
|
+
const jar = new CookieJar();
|
|
672
|
+
jar.deserialize(session.cookies);
|
|
673
|
+
return jar.has(AUTH_FLAG_COOKIE);
|
|
674
|
+
}
|
|
675
|
+
/** @internal */
|
|
676
|
+
function controlledTokenStorage(storage, account) {
|
|
677
|
+
let revoked = false;
|
|
678
|
+
const pending = /* @__PURE__ */ new Set();
|
|
679
|
+
const mutate = (operation) => {
|
|
680
|
+
if (revoked) return Promise.resolve();
|
|
681
|
+
const promise = Promise.resolve().then(operation);
|
|
682
|
+
pending.add(promise);
|
|
683
|
+
promise.then(() => pending.delete(promise), () => pending.delete(promise));
|
|
684
|
+
return promise;
|
|
685
|
+
};
|
|
686
|
+
return {
|
|
687
|
+
storage: {
|
|
688
|
+
get: () => revoked ? null : storage.get(account),
|
|
689
|
+
set: (session) => mutate(() => storage.set(account, session)),
|
|
690
|
+
clear: () => mutate(() => storage.clear(account))
|
|
691
|
+
},
|
|
692
|
+
revoke() {
|
|
693
|
+
revoked = true;
|
|
694
|
+
},
|
|
695
|
+
async drain() {
|
|
696
|
+
await Promise.all([...pending]);
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
/**
|
|
701
|
+
* Мультихранилище в памяти процесса — вариант по умолчанию.
|
|
702
|
+
*
|
|
703
|
+
* Сессии теряются при перезапуске. Для долгоживущих ботов возьмите `FileMultiTokenStorage`
|
|
704
|
+
* из `itd-api/node` либо соберите своё через {@link createMultiTokenStorage}.
|
|
705
|
+
*/
|
|
706
|
+
var MemoryMultiTokenStorage = class {
|
|
707
|
+
#sessions = /* @__PURE__ */ new Map();
|
|
708
|
+
constructor(initial) {
|
|
709
|
+
for (const [account, session] of Object.entries(initial ?? {})) this.#sessions.set(account, require_storage.copySession(session));
|
|
710
|
+
}
|
|
711
|
+
get(account) {
|
|
712
|
+
const session = this.#sessions.get(account);
|
|
713
|
+
return session ? require_storage.copySession(session) : null;
|
|
714
|
+
}
|
|
715
|
+
set(account, session) {
|
|
716
|
+
this.#sessions.set(account, require_storage.copySession(session));
|
|
717
|
+
}
|
|
718
|
+
clear(account) {
|
|
719
|
+
this.#sessions.delete(account);
|
|
720
|
+
}
|
|
721
|
+
accounts() {
|
|
722
|
+
return [...this.#sessions.keys()];
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
/**
|
|
726
|
+
* Собирает {@link MultiTokenStorage} из четырёх функций — когда заводить класс избыточно.
|
|
727
|
+
* Аналог `createTokenStorage` для нескольких аккаунтов.
|
|
728
|
+
*/
|
|
729
|
+
function createMultiTokenStorage(handlers) {
|
|
730
|
+
return handlers;
|
|
731
|
+
}
|
|
732
|
+
/** Создаёт запись без прототипа: имена `__proto__` и `constructor` остаются обычными ключами. */
|
|
733
|
+
function emptySessionRecord() {
|
|
734
|
+
return Object.create(null);
|
|
735
|
+
}
|
|
736
|
+
/** Копирует внешний снимок в запись без унаследованных свойств. */
|
|
737
|
+
function normalizeSessionRecord(record) {
|
|
738
|
+
const normalized = emptySessionRecord();
|
|
739
|
+
for (const [account, session] of Object.entries(record ?? {})) normalized[account] = require_storage.copySession(session);
|
|
740
|
+
return normalized;
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Мультихранилище поверх источника, который читается и пишется целиком.
|
|
744
|
+
*
|
|
745
|
+
* Решает главную проблему такого способа хранения — **гонку «прочитать, изменить,
|
|
746
|
+
* записать»**: десять аккаунтов пишут в одну запись, и наивная реализация теряла бы
|
|
747
|
+
* чужие сессии. Источник читается один раз, дальше слепок живёт в памяти, а записи
|
|
748
|
+
* выстраиваются в цепочку и идут по очереди.
|
|
749
|
+
*
|
|
750
|
+
* Внутри процесса этого достаточно. Несколько процессов, пишущих в одну запись,
|
|
751
|
+
* по-прежнему затирают друг друга — как и несколько экземпляров этого адаптера,
|
|
752
|
+
* направленных на один источник в одном процессе.
|
|
753
|
+
*/
|
|
754
|
+
function createRecordMultiStorage(source) {
|
|
755
|
+
/** Слепок записи. `undefined` — источник ещё не читался. */
|
|
756
|
+
let snapshot;
|
|
757
|
+
/** Общий промис чтения: параллельные вызовы на холодном старте читают источник один раз. */
|
|
758
|
+
let loading = null;
|
|
759
|
+
/** Цепочка записей. Ошибка одной не останавливает следующие. */
|
|
760
|
+
let writing = Promise.resolve();
|
|
761
|
+
const load = async () => {
|
|
762
|
+
if (snapshot !== void 0) return snapshot;
|
|
763
|
+
loading ??= source.read().then((value) => {
|
|
764
|
+
snapshot = normalizeSessionRecord(value);
|
|
765
|
+
return snapshot;
|
|
766
|
+
}).finally(() => {
|
|
767
|
+
loading = null;
|
|
768
|
+
});
|
|
769
|
+
return loading;
|
|
770
|
+
};
|
|
771
|
+
const flush = () => {
|
|
772
|
+
const current = normalizeSessionRecord(snapshot ?? null);
|
|
773
|
+
const operation = async () => {
|
|
774
|
+
if (source.remove && Object.keys(current).length === 0) await source.remove();
|
|
775
|
+
else await source.write(current);
|
|
776
|
+
};
|
|
777
|
+
writing = writing.then(operation, operation);
|
|
778
|
+
return writing;
|
|
779
|
+
};
|
|
780
|
+
return {
|
|
781
|
+
async get(account) {
|
|
782
|
+
const current = await load();
|
|
783
|
+
const session = Object.hasOwn(current, account) ? current[account] : void 0;
|
|
784
|
+
return session ? require_storage.copySession(session) : null;
|
|
785
|
+
},
|
|
786
|
+
async set(account, session) {
|
|
787
|
+
const current = await load();
|
|
788
|
+
current[account] = require_storage.copySession(session);
|
|
789
|
+
await flush();
|
|
790
|
+
},
|
|
791
|
+
async clear(account) {
|
|
792
|
+
const current = await load();
|
|
793
|
+
if (!Object.hasOwn(current, account)) return;
|
|
794
|
+
delete current[account];
|
|
795
|
+
await flush();
|
|
796
|
+
},
|
|
797
|
+
async accounts() {
|
|
798
|
+
return Object.keys(await load());
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
//#endregion
|
|
803
|
+
Object.defineProperty(exports, "AUTH_FLAG_COOKIE", {
|
|
804
|
+
enumerable: true,
|
|
805
|
+
get: function() {
|
|
806
|
+
return AUTH_FLAG_COOKIE;
|
|
807
|
+
}
|
|
808
|
+
});
|
|
809
|
+
Object.defineProperty(exports, "CookieJar", {
|
|
810
|
+
enumerable: true,
|
|
811
|
+
get: function() {
|
|
812
|
+
return CookieJar;
|
|
813
|
+
}
|
|
814
|
+
});
|
|
815
|
+
Object.defineProperty(exports, "DEFAULT_FILE_STREAM_BUFFER_BYTES", {
|
|
816
|
+
enumerable: true,
|
|
817
|
+
get: function() {
|
|
818
|
+
return DEFAULT_FILE_STREAM_BUFFER_BYTES;
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
Object.defineProperty(exports, "DEFAULT_URL_FILE_MAX_BYTES", {
|
|
822
|
+
enumerable: true,
|
|
823
|
+
get: function() {
|
|
824
|
+
return DEFAULT_URL_FILE_MAX_BYTES;
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
Object.defineProperty(exports, "FileTransferMode", {
|
|
828
|
+
enumerable: true,
|
|
829
|
+
get: function() {
|
|
830
|
+
return FileTransferMode;
|
|
831
|
+
}
|
|
832
|
+
});
|
|
833
|
+
Object.defineProperty(exports, "MemoryMultiTokenStorage", {
|
|
834
|
+
enumerable: true,
|
|
835
|
+
get: function() {
|
|
836
|
+
return MemoryMultiTokenStorage;
|
|
837
|
+
}
|
|
838
|
+
});
|
|
839
|
+
Object.defineProperty(exports, "REFRESH_COOKIE", {
|
|
840
|
+
enumerable: true,
|
|
841
|
+
get: function() {
|
|
842
|
+
return REFRESH_COOKIE;
|
|
843
|
+
}
|
|
844
|
+
});
|
|
845
|
+
Object.defineProperty(exports, "REFRESH_COOKIE_PATH", {
|
|
846
|
+
enumerable: true,
|
|
847
|
+
get: function() {
|
|
848
|
+
return REFRESH_COOKIE_PATH;
|
|
849
|
+
}
|
|
850
|
+
});
|
|
851
|
+
Object.defineProperty(exports, "boundedFileStream", {
|
|
852
|
+
enumerable: true,
|
|
853
|
+
get: function() {
|
|
854
|
+
return boundedFileStream;
|
|
855
|
+
}
|
|
856
|
+
});
|
|
857
|
+
Object.defineProperty(exports, "controlledTokenStorage", {
|
|
858
|
+
enumerable: true,
|
|
859
|
+
get: function() {
|
|
860
|
+
return controlledTokenStorage;
|
|
861
|
+
}
|
|
862
|
+
});
|
|
863
|
+
Object.defineProperty(exports, "createMultiTokenStorage", {
|
|
864
|
+
enumerable: true,
|
|
865
|
+
get: function() {
|
|
866
|
+
return createMultiTokenStorage;
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
Object.defineProperty(exports, "createRecordMultiStorage", {
|
|
870
|
+
enumerable: true,
|
|
871
|
+
get: function() {
|
|
872
|
+
return createRecordMultiStorage;
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
Object.defineProperty(exports, "downloadFile", {
|
|
876
|
+
enumerable: true,
|
|
877
|
+
get: function() {
|
|
878
|
+
return downloadFile;
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
Object.defineProperty(exports, "fromStream", {
|
|
882
|
+
enumerable: true,
|
|
883
|
+
get: function() {
|
|
884
|
+
return fromStream;
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
Object.defineProperty(exports, "fromUrl", {
|
|
888
|
+
enumerable: true,
|
|
889
|
+
get: function() {
|
|
890
|
+
return fromUrl;
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
Object.defineProperty(exports, "isBoundedFileStream", {
|
|
894
|
+
enumerable: true,
|
|
895
|
+
get: function() {
|
|
896
|
+
return isBoundedFileStream;
|
|
897
|
+
}
|
|
898
|
+
});
|
|
899
|
+
Object.defineProperty(exports, "isRestorableSession", {
|
|
900
|
+
enumerable: true,
|
|
901
|
+
get: function() {
|
|
902
|
+
return isRestorableSession;
|
|
903
|
+
}
|
|
904
|
+
});
|
|
905
|
+
Object.defineProperty(exports, "normalizeMimeType", {
|
|
906
|
+
enumerable: true,
|
|
907
|
+
get: function() {
|
|
908
|
+
return normalizeMimeType;
|
|
909
|
+
}
|
|
910
|
+
});
|
|
911
|
+
Object.defineProperty(exports, "openUrlFile", {
|
|
912
|
+
enumerable: true,
|
|
913
|
+
get: function() {
|
|
914
|
+
return openUrlFile;
|
|
915
|
+
}
|
|
916
|
+
});
|
|
917
|
+
Object.defineProperty(exports, "resolveFileStreamOptions", {
|
|
918
|
+
enumerable: true,
|
|
919
|
+
get: function() {
|
|
920
|
+
return resolveFileStreamOptions;
|
|
921
|
+
}
|
|
922
|
+
});
|
|
923
|
+
Object.defineProperty(exports, "scopedTokenStorage", {
|
|
924
|
+
enumerable: true,
|
|
925
|
+
get: function() {
|
|
926
|
+
return scopedTokenStorage;
|
|
927
|
+
}
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
//# sourceMappingURL=multi-storage-D1keK2Op.cjs.map
|