itd-api 0.4.0 → 0.6.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 +6 -4
- package/dist/index.cjs +3030 -1579
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +950 -568
- package/dist/index.d.ts +950 -568
- package/dist/index.js +2961 -1503
- package/dist/index.js.map +1 -1
- package/dist/{multi-storage-D4h6QA-w.js → multi-storage-BUZaLAPO.js} +132 -84
- package/dist/multi-storage-BUZaLAPO.js.map +1 -0
- package/dist/{multi-storage-DwaIh24p.d.ts → multi-storage-BUoEoW8f.d.cts} +13 -43
- package/dist/{multi-storage-C8R5Crwo.cjs → multi-storage-Bf84xiO8.cjs} +169 -91
- package/dist/multi-storage-Bf84xiO8.cjs.map +1 -0
- package/dist/{multi-storage-DP_ujJw1.d.cts → multi-storage-CQSlI_kn.d.ts} +13 -43
- package/dist/node.cjs +62 -53
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +19 -5
- package/dist/node.d.ts +19 -5
- package/dist/node.js +62 -54
- package/dist/node.js.map +1 -1
- package/dist/storage-BqMxs76Y.d.cts +152 -0
- package/dist/storage-BqMxs76Y.d.ts +152 -0
- package/dist/{storage-ycBqLBRB.cjs → storage-DnzZPS_9.cjs} +263 -17
- package/dist/storage-DnzZPS_9.cjs.map +1 -0
- package/dist/{storage-D9tfHx7Z.js → storage-IHdXw52v.js} +210 -18
- package/dist/storage-IHdXw52v.js.map +1 -0
- package/dist/web.cjs +143 -40
- package/dist/web.cjs.map +1 -1
- package/dist/web.d.cts +40 -3
- package/dist/web.d.ts +40 -3
- package/dist/web.js +141 -41
- package/dist/web.js.map +1 -1
- package/package.json +3 -3
- package/dist/multi-storage-C8R5Crwo.cjs.map +0 -1
- package/dist/multi-storage-D4h6QA-w.js.map +0 -1
- package/dist/runtime-CFEsf-jD.cjs +0 -185
- package/dist/runtime-CFEsf-jD.cjs.map +0 -1
- package/dist/runtime-DHxDn8gf.js +0 -126
- package/dist/runtime-DHxDn8gf.js.map +0 -1
- package/dist/storage-BjNRlkbE.d.cts +0 -82
- package/dist/storage-BjNRlkbE.d.ts +0 -82
- package/dist/storage-D9tfHx7Z.js.map +0 -1
- package/dist/storage-ycBqLBRB.cjs.map +0 -1
package/dist/node.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_multi_storage = require("./multi-storage-
|
|
3
|
-
const require_storage = require("./storage-
|
|
2
|
+
const require_multi_storage = require("./multi-storage-Bf84xiO8.cjs");
|
|
3
|
+
const require_storage = require("./storage-DnzZPS_9.cjs");
|
|
4
4
|
//#region src/node.ts
|
|
5
5
|
/**
|
|
6
6
|
* `itd-api/node` — файловая система для Node, Bun и Deno.
|
|
@@ -42,11 +42,12 @@ function fromPath(path, options = {}) {
|
|
|
42
42
|
actual: info.size
|
|
43
43
|
});
|
|
44
44
|
const file = createReadStream(path, {
|
|
45
|
-
highWaterMark: Math.min(resolved.streamBufferBytes,
|
|
45
|
+
highWaterMark: Math.min(resolved.streamBufferBytes, 1048576),
|
|
46
46
|
...signal ? { signal } : {}
|
|
47
47
|
});
|
|
48
|
+
const source = Readable.toWeb(file);
|
|
48
49
|
return {
|
|
49
|
-
stream: require_multi_storage.boundedFileStream(
|
|
50
|
+
stream: require_multi_storage.boundedFileStream(source, {
|
|
50
51
|
...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
|
|
51
52
|
streamBufferBytes: resolved.streamBufferBytes,
|
|
52
53
|
...signal ? { signal } : {}
|
|
@@ -85,25 +86,22 @@ function fromPath(path, options = {}) {
|
|
|
85
86
|
/**
|
|
86
87
|
* Читает и разбирает JSON-файл.
|
|
87
88
|
*
|
|
88
|
-
* Отсутствующий файл означает `
|
|
89
|
-
*
|
|
90
|
-
* а мультихранилище требует строгого разбора: молча затереть файл с несколькими токенами
|
|
91
|
-
* особенно опасно.
|
|
89
|
+
* Отсутствующий файл означает `undefined`. Повреждённый JSON не считается пустым хранилищем:
|
|
90
|
+
* иначе следующая запись могла бы незаметно затереть данные.
|
|
92
91
|
*/
|
|
93
|
-
async function readJsonFile(path
|
|
92
|
+
async function readJsonFile(path) {
|
|
94
93
|
let raw;
|
|
95
94
|
try {
|
|
96
95
|
const { readFile } = await import("node:fs/promises");
|
|
97
96
|
raw = await readFile(path, "utf8");
|
|
98
97
|
} catch (error) {
|
|
99
|
-
if (hasErrorCode(error, "ENOENT")) return
|
|
98
|
+
if (hasErrorCode(error, "ENOENT")) return void 0;
|
|
100
99
|
throw error;
|
|
101
100
|
}
|
|
102
101
|
try {
|
|
103
102
|
return JSON.parse(raw);
|
|
104
103
|
} catch {
|
|
105
|
-
|
|
106
|
-
throw new require_storage.ItdConfigError(`Файл ${path} повреждён: ожидался JSON с сессиями аккаунтов. Исправьте файл или перенесите его перед следующим сохранением.`);
|
|
104
|
+
throw new require_storage.ItdConfigError(`Файл ${path} повреждён: ожидался JSON key-value хранилища. Исправьте файл или перенесите его перед следующим сохранением.`);
|
|
107
105
|
}
|
|
108
106
|
}
|
|
109
107
|
/**
|
|
@@ -131,6 +129,49 @@ async function removeFile(path) {
|
|
|
131
129
|
const { rm } = await import("node:fs/promises");
|
|
132
130
|
await rm(path, { force: true });
|
|
133
131
|
}
|
|
132
|
+
/** Версия общего формата файлового key-value backend. */
|
|
133
|
+
const FILE_STORE_VERSION = 1;
|
|
134
|
+
/**
|
|
135
|
+
* Key-value backend в одном JSON-файле.
|
|
136
|
+
*
|
|
137
|
+
* Изменения сериализуются внутри экземпляра и записываются атомарным переименованием. Несколько
|
|
138
|
+
* экземпляров или процессов, направленных на один путь, требуют внешней синхронизации.
|
|
139
|
+
*/
|
|
140
|
+
var FileKeyValueStore = class {
|
|
141
|
+
#path;
|
|
142
|
+
#inner;
|
|
143
|
+
/** @param path путь к JSON-файлу. Для секретов добавьте его в `.gitignore`. */
|
|
144
|
+
constructor(path) {
|
|
145
|
+
this.#path = path;
|
|
146
|
+
this.#inner = require_storage.createRecordKeyValueStore({
|
|
147
|
+
read: () => this.#read(),
|
|
148
|
+
write: (values) => writeJsonAtomic(path, {
|
|
149
|
+
version: FILE_STORE_VERSION,
|
|
150
|
+
values
|
|
151
|
+
}),
|
|
152
|
+
delete: () => removeFile(path)
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
get(key) {
|
|
156
|
+
return Promise.resolve(this.#inner.get(key));
|
|
157
|
+
}
|
|
158
|
+
set(key, value) {
|
|
159
|
+
return Promise.resolve(this.#inner.set(key, value));
|
|
160
|
+
}
|
|
161
|
+
delete(key) {
|
|
162
|
+
return Promise.resolve(this.#inner.delete(key));
|
|
163
|
+
}
|
|
164
|
+
keys(prefix) {
|
|
165
|
+
return Promise.resolve(this.#inner.keys(prefix));
|
|
166
|
+
}
|
|
167
|
+
async #read() {
|
|
168
|
+
const parsed = await readJsonFile(this.#path);
|
|
169
|
+
if (parsed === void 0) return void 0;
|
|
170
|
+
const envelope = typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
171
|
+
if (envelope?.version !== FILE_STORE_VERSION || typeof envelope.values !== "object" || envelope.values === null || Array.isArray(envelope.values)) throw new require_storage.ItdConfigError(`Файл ${this.#path} имеет неподдерживаемый формат: ожидается { version: ${FILE_STORE_VERSION}, values }. Возьмите другой путь, чтобы не перезаписать посторонние данные.`);
|
|
172
|
+
return envelope.values;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
134
175
|
/**
|
|
135
176
|
* Хранит сессию в файле.
|
|
136
177
|
*
|
|
@@ -148,36 +189,21 @@ async function removeFile(path) {
|
|
|
148
189
|
* ```
|
|
149
190
|
*/
|
|
150
191
|
var FileTokenStorage = class {
|
|
151
|
-
#
|
|
152
|
-
/**
|
|
153
|
-
* Цепочка операций с файлом. Запись и удаление выполняются последовательно
|
|
154
|
-
* в порядке вызова; ошибка одной операции не останавливает следующие.
|
|
155
|
-
*/
|
|
156
|
-
#writing = Promise.resolve();
|
|
192
|
+
#inner;
|
|
157
193
|
/** @param path путь к файлу сессии. Добавьте его в `.gitignore`. */
|
|
158
194
|
constructor(path) {
|
|
159
|
-
this.#
|
|
195
|
+
this.#inner = require_storage.createTokenStorage(new FileKeyValueStore(path));
|
|
160
196
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const parsed = await readJsonFile(this.#path);
|
|
164
|
-
return typeof parsed === "object" && parsed !== null ? require_storage.copySession(parsed) : null;
|
|
197
|
+
get() {
|
|
198
|
+
return Promise.resolve(this.#inner.get());
|
|
165
199
|
}
|
|
166
200
|
set(session) {
|
|
167
|
-
|
|
168
|
-
return this.#enqueue(() => writeJsonAtomic(this.#path, snapshot));
|
|
201
|
+
return Promise.resolve(this.#inner.set(session));
|
|
169
202
|
}
|
|
170
203
|
clear() {
|
|
171
|
-
return this.#
|
|
172
|
-
}
|
|
173
|
-
/** Добавляет файловую операцию в очередь. */
|
|
174
|
-
#enqueue(operation) {
|
|
175
|
-
this.#writing = this.#writing.then(operation, operation);
|
|
176
|
-
return this.#writing;
|
|
204
|
+
return Promise.resolve(this.#inner.clear());
|
|
177
205
|
}
|
|
178
206
|
};
|
|
179
|
-
/** Версия формата файла с сессиями нескольких аккаунтов. */
|
|
180
|
-
const SESSIONS_FILE_VERSION = 1;
|
|
181
207
|
/**
|
|
182
208
|
* Хранит сессии нескольких аккаунтов в одном файле.
|
|
183
209
|
*
|
|
@@ -185,8 +211,7 @@ const SESSIONS_FILE_VERSION = 1;
|
|
|
185
211
|
* и без общего слепка с очередью записей они теряли бы сессии друг друга. Как и
|
|
186
212
|
* {@link FileTokenStorage}, пишет через временный файл с правами `0600`.
|
|
187
213
|
*
|
|
188
|
-
*
|
|
189
|
-
* не спутать с однопользовательским, и чужой не будет молча перезаписан.
|
|
214
|
+
* Использует тот же версионированный {@link FileKeyValueStore}, что и одиночное хранилище.
|
|
190
215
|
*
|
|
191
216
|
* @example
|
|
192
217
|
* ```ts
|
|
@@ -197,19 +222,10 @@ const SESSIONS_FILE_VERSION = 1;
|
|
|
197
222
|
* ```
|
|
198
223
|
*/
|
|
199
224
|
var FileMultiTokenStorage = class {
|
|
200
|
-
#path;
|
|
201
225
|
#inner;
|
|
202
226
|
/** @param path путь к файлу сессий. Добавьте его в `.gitignore`. */
|
|
203
227
|
constructor(path) {
|
|
204
|
-
this.#
|
|
205
|
-
this.#inner = require_multi_storage.createRecordMultiStorage({
|
|
206
|
-
read: () => this.#read(),
|
|
207
|
-
write: (accounts) => writeJsonAtomic(path, {
|
|
208
|
-
version: SESSIONS_FILE_VERSION,
|
|
209
|
-
accounts
|
|
210
|
-
}),
|
|
211
|
-
remove: () => removeFile(path)
|
|
212
|
-
});
|
|
228
|
+
this.#inner = require_multi_storage.createMultiTokenStorage(new FileKeyValueStore(path));
|
|
213
229
|
}
|
|
214
230
|
get(account) {
|
|
215
231
|
return Promise.resolve(this.#inner.get(account));
|
|
@@ -223,16 +239,9 @@ var FileMultiTokenStorage = class {
|
|
|
223
239
|
accounts() {
|
|
224
240
|
return Promise.resolve(this.#inner.accounts());
|
|
225
241
|
}
|
|
226
|
-
async #read() {
|
|
227
|
-
const parsed = await readJsonFile(this.#path, true);
|
|
228
|
-
if (parsed === null) return null;
|
|
229
|
-
const envelope = typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
230
|
-
const accounts = envelope?.accounts;
|
|
231
|
-
if (envelope?.version !== SESSIONS_FILE_VERSION || typeof accounts !== "object" || accounts === null || Array.isArray(accounts)) throw new require_storage.ItdConfigError(`Файл ${this.#path} имеет неподдерживаемый формат: ожидается { version: ${SESSIONS_FILE_VERSION}, accounts }. Возможно, это файл одиночной сессии или файл другой версии — возьмите отдельный путь, чтобы не перезаписать его.`);
|
|
232
|
-
return accounts;
|
|
233
|
-
}
|
|
234
242
|
};
|
|
235
243
|
//#endregion
|
|
244
|
+
exports.FileKeyValueStore = FileKeyValueStore;
|
|
236
245
|
exports.FileMultiTokenStorage = FileMultiTokenStorage;
|
|
237
246
|
exports.FileTokenStorage = FileTokenStorage;
|
|
238
247
|
exports.fromPath = fromPath;
|
package/dist/node.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"node.cjs","names":["resolveFileStreamOptions","FileTransferMode","ItdFileError","ItdFileErrorReason","boundedFileStream","ItdConfigError","#path","#writing","copySession","#enqueue","#inner","createRecordMultiStorage","#read"],"sources":["../src/node.ts"],"sourcesContent":["/**\n * `itd-api/node` — файловая система для Node, Bun и Deno.\n *\n * Здесь лежит только то, что требует `node:fs` и потому не может попасть в основной бандл:\n * иначе браузерные сборщики пытались бы разрешить этот модуль. Клиент, аккаунты, билдеры,\n * типы и всё остальное берутся из `itd-api` — одинаково на всех платформах.\n *\n * @example\n * ```ts\n * import { ItdClient } from 'itd-api';\n * import { FileTokenStorage, fromPath } from 'itd-api/node';\n *\n * // Когда сессия уже сохранена, `auth` не нужен — токен возьмётся из хранилища.\n * const itd = new ItdClient({ storage: new FileTokenStorage('./.itd-session.json') });\n *\n * await itd.posts.create((p) => p.content('привет').attach(fromPath('./photo.jpg')));\n * ```\n *\n * @packageDocumentation\n */\n\nimport { boundedFileStream } from './core/attachments/bounded-stream.js';\nimport {\n type FileStreamOptions,\n FileTransferMode,\n type LazyFile,\n type StreamFile,\n} from './core/attachments/contracts.js';\nimport { resolveFileStreamOptions } from './core/attachments/options.js';\nimport { ItdConfigError, ItdFileError, ItdFileErrorReason } from './core/errors.js';\nimport { createRecordMultiStorage, type MultiTokenStorage } from './core/multi-storage.js';\nimport { copySession, type ItdSession, type TokenStorage } from './core/storage.js';\n\n/** Проверяет код системной ошибки без привязки к типам конкретного рантайма. */\nfunction hasErrorCode(error: unknown, code: string): boolean {\n return typeof error === 'object' && error !== null && 'code' in error && error.code === code;\n}\n\n/** Что можно уточнить у файла с диска. */\nexport interface PathFileOptions extends FileStreamOptions {\n /** Имя файла. По умолчанию — имя из пути. */\n filename?: string | undefined;\n /** MIME-тип. По умолчанию определяется по расширению. */\n contentType?: string | undefined;\n}\n\n/**\n * Вложение из файла на диске.\n *\n * Клиент настраивать не нужно: чтение диска приезжает вместе со значением, а не через\n * опцию, поэтому один и тот же клиент принимает и файлы с диска, и всё остальное.\n *\n * По умолчанию файл читается целиком непосредственно перед отправкой. Режим\n * `{ mode: 'stream' }` передаёт его частями и открывает заново при повторной попытке.\n * Модуль `node:fs` подключается динамически.\n *\n * @example\n * ```ts\n * import { ItdClient } from 'itd-api';\n * import { fromPath } from 'itd-api/node';\n *\n * const itd = new ItdClient({ auth });\n *\n * await itd.posts.create((p) =>\n * p.content('смотрите').attach(fromPath('./photo.jpg')),\n * );\n * ```\n */\nexport function fromPath(\n path: string,\n options: PathFileOptions & { mode: typeof FileTransferMode.Stream },\n): StreamFile;\nexport function fromPath(\n path: string,\n options?: PathFileOptions & { mode?: typeof FileTransferMode.Buffer },\n): LazyFile;\nexport function fromPath(path: string, options: PathFileOptions): LazyFile | StreamFile;\nexport function fromPath(path: string, options: PathFileOptions = {}): LazyFile | StreamFile {\n const resolved = resolveFileStreamOptions(options);\n\n if (resolved.mode === FileTransferMode.Stream) {\n return {\n open: async ({ signal }) => {\n const [{ createReadStream }, { stat }, { basename }, { Readable }] = await Promise.all([\n import('node:fs'),\n import('node:fs/promises'),\n import('node:path'),\n import('node:stream'),\n ]);\n const info = await stat(path);\n if (resolved.maxBytes !== undefined && info.size > resolved.maxBytes) {\n throw new ItdFileError(\n `файл ${path} больше предела в ${resolved.maxBytes} байт: ${info.size}`,\n {\n reason: ItdFileErrorReason.TooLarge,\n limit: resolved.maxBytes,\n actual: info.size,\n },\n );\n }\n\n const file = createReadStream(path, {\n highWaterMark: Math.min(resolved.streamBufferBytes, 1024 * 1024),\n ...(signal ? { signal } : {}),\n });\n const source = Readable.toWeb(file) as ReadableStream<Uint8Array>;\n return {\n stream: boundedFileStream(source, {\n ...(resolved.maxBytes !== undefined ? { maxBytes: resolved.maxBytes } : {}),\n streamBufferBytes: resolved.streamBufferBytes,\n ...(signal ? { signal } : {}),\n }),\n filename: options.filename ?? basename(path),\n ...(options.contentType ? { contentType: options.contentType } : {}),\n size: info.size,\n close: () => {\n file.destroy();\n },\n };\n },\n };\n }\n\n return {\n load: async ({ signal }) => {\n const [{ readFile, stat }, { basename }] = await Promise.all([\n import('node:fs/promises'),\n import('node:path'),\n ]);\n if (resolved.maxBytes !== undefined) {\n const info = await stat(path);\n if (info.size > resolved.maxBytes) {\n throw new ItdFileError(\n `файл ${path} больше предела в ${resolved.maxBytes} байт: ${info.size}`,\n {\n reason: ItdFileErrorReason.TooLarge,\n limit: resolved.maxBytes,\n actual: info.size,\n },\n );\n }\n }\n\n const file = new Uint8Array(await readFile(path, signal ? { signal } : undefined));\n if (resolved.maxBytes !== undefined && file.byteLength > resolved.maxBytes) {\n throw new ItdFileError(\n `файл ${path} больше предела в ${resolved.maxBytes} байт: ${file.byteLength}`,\n {\n reason: ItdFileErrorReason.TooLarge,\n limit: resolved.maxBytes,\n actual: file.byteLength,\n },\n );\n }\n\n return {\n file,\n filename: options.filename ?? basename(path),\n ...(options.contentType ? { contentType: options.contentType } : {}),\n };\n },\n };\n}\n\n/**\n * Читает и разбирает JSON-файл.\n *\n * Отсутствующий файл означает `null`. Остальные ошибки файловой системы не скрываются.\n * Одиночное хранилище ради обратной совместимости считает повреждённый JSON пустым,\n * а мультихранилище требует строгого разбора: молча затереть файл с несколькими токенами\n * особенно опасно.\n */\nasync function readJsonFile(path: string, strict = false): Promise<unknown> {\n let raw: string;\n try {\n const { readFile } = await import('node:fs/promises');\n raw = await readFile(path, 'utf8');\n } catch (error) {\n if (hasErrorCode(error, 'ENOENT')) return null;\n throw error;\n }\n\n try {\n return JSON.parse(raw) as unknown;\n } catch {\n if (!strict) return null;\n throw new ItdConfigError(\n `Файл ${path} повреждён: ожидался JSON с сессиями аккаунтов. ` +\n 'Исправьте файл или перенесите его перед следующим сохранением.',\n );\n }\n}\n\n/**\n * Записывает JSON во временный файл и переименовывает его поверх целевого.\n *\n * Права `0600` (чтение и запись только владельцу) — в файле лежат токены. Переименование\n * атомарно, поэтому падение процесса посреди сохранения не оставит повреждённого файла.\n */\nasync function writeJsonAtomic(path: string, value: unknown): Promise<void> {\n const { writeFile, rename, unlink } = await import('node:fs/promises');\n\n // Временный файл уникален на процесс и вызов — иначе параллельные записи (в том числе\n // из разных процессов) делят один `.tmp`.\n const pid = typeof process !== 'undefined' ? process.pid : 0;\n const temporary = `${path}.${pid}.${Math.random().toString(36).slice(2)}.tmp`;\n\n try {\n await writeFile(temporary, JSON.stringify(value, null, 2), { encoding: 'utf8', mode: 0o600 });\n await rename(temporary, path);\n } catch (error) {\n await unlink(temporary).catch(() => {});\n throw error;\n }\n}\n\n/** Удаляет файл. Благодаря `force` отсутствие файла ошибкой не считается. */\nasync function removeFile(path: string): Promise<void> {\n const { rm } = await import('node:fs/promises');\n await rm(path, { force: true });\n}\n\n/**\n * Хранит сессию в файле.\n *\n * Нужна долгоживущим процессам: без неё бот при каждом запуске входит заново — а вход\n * требует решённой капчи, да и серия входов подряд может привести к временной блокировке\n * аккаунта. С сохранённой сессией опция `auth` не нужна вовсе.\n *\n * Файл создаётся с правами `0600` (чтение и запись только владельцу) — в нём лежат токены.\n * Запись идёт через временный файл с последующим переименованием, поэтому падение процесса\n * посреди сохранения не оставит повреждённую сессию.\n *\n * @example\n * ```ts\n * const itd = new ItdClient({ storage: new FileTokenStorage('./.itd-session.json') });\n * ```\n */\nexport class FileTokenStorage implements TokenStorage {\n readonly #path: string;\n /**\n * Цепочка операций с файлом. Запись и удаление выполняются последовательно\n * в порядке вызова; ошибка одной операции не останавливает следующие.\n */\n #writing: Promise<void> = Promise.resolve();\n\n /** @param path путь к файлу сессии. Добавьте его в `.gitignore`. */\n constructor(path: string) {\n this.#path = path;\n }\n\n async get(): Promise<ItdSession | null> {\n // Чтение, начатое сразу после `set()`/`clear()`, должно видеть результат более ранней\n // операции, даже если вызывающий код не сохранил и не дождался её промиса.\n await this.#writing.then(\n () => undefined,\n () => undefined,\n );\n const parsed = await readJsonFile(this.#path);\n return typeof parsed === 'object' && parsed !== null ? copySession(parsed as ItdSession) : null;\n }\n\n set(session: ItdSession): Promise<void> {\n // Пользователь может изменить переданный объект сразу после вызова `set()`.\n // В очередь должен попасть снимок на момент вызова, а не живая ссылка.\n const snapshot = copySession(session);\n return this.#enqueue(() => writeJsonAtomic(this.#path, snapshot));\n }\n\n clear(): Promise<void> {\n return this.#enqueue(() => removeFile(this.#path));\n }\n\n /** Добавляет файловую операцию в очередь. */\n #enqueue(operation: () => Promise<void>): Promise<void> {\n this.#writing = this.#writing.then(operation, operation);\n return this.#writing;\n }\n}\n\n/** Версия формата файла с сессиями нескольких аккаунтов. */\nconst SESSIONS_FILE_VERSION = 1;\n\n/**\n * Хранит сессии нескольких аккаунтов в одном файле.\n *\n * Разбирается с гонкой «прочитать, изменить, записать»: десять аккаунтов пишут в один файл,\n * и без общего слепка с очередью записей они теряли бы сессии друг друга. Как и\n * {@link FileTokenStorage}, пишет через временный файл с правами `0600`.\n *\n * Формат — конверт `{ version, accounts }`, а не голая карта: так файл нескольких аккаунтов\n * не спутать с однопользовательским, и чужой не будет молча перезаписан.\n *\n * @example\n * ```ts\n * const accounts = new ItdAccounts({\n * storage: new FileMultiTokenStorage('./.itd-sessions.json'),\n * });\n * await accounts.restore();\n * ```\n */\nexport class FileMultiTokenStorage implements MultiTokenStorage {\n readonly #path: string;\n readonly #inner: MultiTokenStorage;\n\n /** @param path путь к файлу сессий. Добавьте его в `.gitignore`. */\n constructor(path: string) {\n this.#path = path;\n this.#inner = createRecordMultiStorage({\n read: () => this.#read(),\n write: (accounts) => writeJsonAtomic(path, { version: SESSIONS_FILE_VERSION, accounts }),\n // Последний аккаунт ушёл — файл с пустой картой выглядел бы мусором.\n remove: () => removeFile(path),\n });\n }\n\n get(account: string): Promise<ItdSession | null> {\n return Promise.resolve(this.#inner.get(account));\n }\n\n set(account: string, session: ItdSession): Promise<void> {\n return Promise.resolve(this.#inner.set(account, session));\n }\n\n clear(account: string): Promise<void> {\n return Promise.resolve(this.#inner.clear(account));\n }\n\n accounts(): Promise<readonly string[]> {\n return Promise.resolve(this.#inner.accounts());\n }\n\n async #read(): Promise<Record<string, ItdSession> | null> {\n const parsed = await readJsonFile(this.#path, true);\n if (parsed === null) return null;\n\n const envelope =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { version?: unknown; accounts?: unknown })\n : undefined;\n const accounts = envelope?.accounts;\n\n if (\n envelope?.version !== SESSIONS_FILE_VERSION ||\n typeof accounts !== 'object' ||\n accounts === null ||\n Array.isArray(accounts)\n ) {\n throw new ItdConfigError(\n `Файл ${this.#path} имеет неподдерживаемый формат: ожидается ` +\n `{ version: ${SESSIONS_FILE_VERSION}, accounts }. ` +\n 'Возможно, это файл одиночной сессии или файл другой версии — ' +\n 'возьмите отдельный путь, чтобы не перезаписать его.',\n );\n }\n\n return accounts as Record<string, ItdSession>;\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"node.cjs","names":["resolveFileStreamOptions","FileTransferMode","ItdFileError","ItdFileErrorReason","boundedFileStream","ItdConfigError","#path","#inner","createRecordKeyValueStore","#read","createTokenStorage","createMultiTokenStorage"],"sources":["../src/node.ts"],"sourcesContent":["/**\n * `itd-api/node` — файловая система для Node, Bun и Deno.\n *\n * Здесь лежит только то, что требует `node:fs` и потому не может попасть в основной бандл:\n * иначе браузерные сборщики пытались бы разрешить этот модуль. Клиент, аккаунты, билдеры,\n * типы и всё остальное берутся из `itd-api` — одинаково на всех платформах.\n *\n * @example\n * ```ts\n * import { ItdClient } from 'itd-api';\n * import { FileTokenStorage, fromPath } from 'itd-api/node';\n *\n * // Когда сессия уже сохранена, `auth` не нужен — токен возьмётся из хранилища.\n * const itd = new ItdClient({ storage: new FileTokenStorage('./.itd-session.json') });\n *\n * await itd.posts.create((p) => p.content('привет').attach(fromPath('./photo.jpg')));\n * ```\n *\n * @packageDocumentation\n */\n\nimport { boundedFileStream } from './core/attachments/bounded-stream.js';\nimport {\n type FileStreamOptions,\n FileTransferMode,\n type LazyFile,\n type StreamFile,\n} from './core/attachments/contracts.js';\nimport { resolveFileStreamOptions } from './core/attachments/options.js';\nimport { ItdConfigError, ItdFileError, ItdFileErrorReason } from './core/errors.js';\nimport { createRecordKeyValueStore, type EnumerableKeyValueStore } from './core/key-value-store.js';\nimport { createMultiTokenStorage, type MultiTokenStorage } from './core/multi-storage.js';\nimport { createTokenStorage, type ItdSession, type TokenStorage } from './core/storage.js';\n\n/** Проверяет код системной ошибки без привязки к типам конкретного рантайма. */\nfunction hasErrorCode(error: unknown, code: string): boolean {\n return typeof error === 'object' && error !== null && 'code' in error && error.code === code;\n}\n\n/** Что можно уточнить у файла с диска. */\nexport interface PathFileOptions extends FileStreamOptions {\n /** Имя файла. По умолчанию — имя из пути. */\n filename?: string | undefined;\n /** MIME-тип. По умолчанию определяется по расширению. */\n contentType?: string | undefined;\n}\n\n/**\n * Вложение из файла на диске.\n *\n * Клиент настраивать не нужно: чтение диска приезжает вместе со значением, а не через\n * опцию, поэтому один и тот же клиент принимает и файлы с диска, и всё остальное.\n *\n * По умолчанию файл читается целиком непосредственно перед отправкой. Режим\n * `{ mode: 'stream' }` передаёт его частями и открывает заново при повторной попытке.\n * Модуль `node:fs` подключается динамически.\n *\n * @example\n * ```ts\n * import { ItdClient } from 'itd-api';\n * import { fromPath } from 'itd-api/node';\n *\n * const itd = new ItdClient({ auth });\n *\n * await itd.posts.create((p) =>\n * p.content('смотрите').attach(fromPath('./photo.jpg')),\n * );\n * ```\n */\nexport function fromPath(\n path: string,\n options: PathFileOptions & { mode: typeof FileTransferMode.Stream },\n): StreamFile;\nexport function fromPath(\n path: string,\n options?: PathFileOptions & { mode?: typeof FileTransferMode.Buffer },\n): LazyFile;\nexport function fromPath(path: string, options: PathFileOptions): LazyFile | StreamFile;\nexport function fromPath(path: string, options: PathFileOptions = {}): LazyFile | StreamFile {\n const resolved = resolveFileStreamOptions(options);\n\n if (resolved.mode === FileTransferMode.Stream) {\n return {\n open: async ({ signal }) => {\n const [{ createReadStream }, { stat }, { basename }, { Readable }] = await Promise.all([\n import('node:fs'),\n import('node:fs/promises'),\n import('node:path'),\n import('node:stream'),\n ]);\n const info = await stat(path);\n if (resolved.maxBytes !== undefined && info.size > resolved.maxBytes) {\n throw new ItdFileError(\n `файл ${path} больше предела в ${resolved.maxBytes} байт: ${info.size}`,\n {\n reason: ItdFileErrorReason.TooLarge,\n limit: resolved.maxBytes,\n actual: info.size,\n },\n );\n }\n\n const file = createReadStream(path, {\n highWaterMark: Math.min(resolved.streamBufferBytes, 1024 * 1024),\n ...(signal ? { signal } : {}),\n });\n const source = Readable.toWeb(file) as ReadableStream<Uint8Array>;\n return {\n stream: boundedFileStream(source, {\n ...(resolved.maxBytes !== undefined ? { maxBytes: resolved.maxBytes } : {}),\n streamBufferBytes: resolved.streamBufferBytes,\n ...(signal ? { signal } : {}),\n }),\n filename: options.filename ?? basename(path),\n ...(options.contentType ? { contentType: options.contentType } : {}),\n size: info.size,\n close: () => {\n file.destroy();\n },\n };\n },\n };\n }\n\n return {\n load: async ({ signal }) => {\n const [{ readFile, stat }, { basename }] = await Promise.all([\n import('node:fs/promises'),\n import('node:path'),\n ]);\n if (resolved.maxBytes !== undefined) {\n const info = await stat(path);\n if (info.size > resolved.maxBytes) {\n throw new ItdFileError(\n `файл ${path} больше предела в ${resolved.maxBytes} байт: ${info.size}`,\n {\n reason: ItdFileErrorReason.TooLarge,\n limit: resolved.maxBytes,\n actual: info.size,\n },\n );\n }\n }\n\n const file = new Uint8Array(await readFile(path, signal ? { signal } : undefined));\n if (resolved.maxBytes !== undefined && file.byteLength > resolved.maxBytes) {\n throw new ItdFileError(\n `файл ${path} больше предела в ${resolved.maxBytes} байт: ${file.byteLength}`,\n {\n reason: ItdFileErrorReason.TooLarge,\n limit: resolved.maxBytes,\n actual: file.byteLength,\n },\n );\n }\n\n return {\n file,\n filename: options.filename ?? basename(path),\n ...(options.contentType ? { contentType: options.contentType } : {}),\n };\n },\n };\n}\n\n/**\n * Читает и разбирает JSON-файл.\n *\n * Отсутствующий файл означает `undefined`. Повреждённый JSON не считается пустым хранилищем:\n * иначе следующая запись могла бы незаметно затереть данные.\n */\nasync function readJsonFile(path: string): Promise<unknown> {\n let raw: string;\n try {\n const { readFile } = await import('node:fs/promises');\n raw = await readFile(path, 'utf8');\n } catch (error) {\n if (hasErrorCode(error, 'ENOENT')) return undefined;\n throw error;\n }\n\n try {\n return JSON.parse(raw) as unknown;\n } catch {\n throw new ItdConfigError(\n `Файл ${path} повреждён: ожидался JSON key-value хранилища. ` +\n 'Исправьте файл или перенесите его перед следующим сохранением.',\n );\n }\n}\n\n/**\n * Записывает JSON во временный файл и переименовывает его поверх целевого.\n *\n * Права `0600` (чтение и запись только владельцу) — в файле лежат токены. Переименование\n * атомарно, поэтому падение процесса посреди сохранения не оставит повреждённого файла.\n */\nasync function writeJsonAtomic(path: string, value: unknown): Promise<void> {\n const { writeFile, rename, unlink } = await import('node:fs/promises');\n\n // Временный файл уникален на процесс и вызов — иначе параллельные записи (в том числе\n // из разных процессов) делят один `.tmp`.\n const pid = typeof process !== 'undefined' ? process.pid : 0;\n const temporary = `${path}.${pid}.${Math.random().toString(36).slice(2)}.tmp`;\n\n try {\n await writeFile(temporary, JSON.stringify(value, null, 2), { encoding: 'utf8', mode: 0o600 });\n await rename(temporary, path);\n } catch (error) {\n await unlink(temporary).catch(() => {});\n throw error;\n }\n}\n\n/** Удаляет файл. Благодаря `force` отсутствие файла ошибкой не считается. */\nasync function removeFile(path: string): Promise<void> {\n const { rm } = await import('node:fs/promises');\n await rm(path, { force: true });\n}\n\n/** Версия общего формата файлового key-value backend. */\nconst FILE_STORE_VERSION = 1;\n\n/**\n * Key-value backend в одном JSON-файле.\n *\n * Изменения сериализуются внутри экземпляра и записываются атомарным переименованием. Несколько\n * экземпляров или процессов, направленных на один путь, требуют внешней синхронизации.\n */\nexport class FileKeyValueStore<T> {\n readonly #path: string;\n readonly #inner: EnumerableKeyValueStore<T>;\n\n /** @param path путь к JSON-файлу. Для секретов добавьте его в `.gitignore`. */\n constructor(path: string) {\n this.#path = path;\n this.#inner = createRecordKeyValueStore<T>({\n read: () => this.#read(),\n write: (values) => writeJsonAtomic(path, { version: FILE_STORE_VERSION, values }),\n delete: () => removeFile(path),\n });\n }\n\n get(key: string): Promise<T | undefined> {\n return Promise.resolve(this.#inner.get(key));\n }\n\n set(key: string, value: T): Promise<void> {\n return Promise.resolve(this.#inner.set(key, value));\n }\n\n delete(key: string): Promise<void> {\n return Promise.resolve(this.#inner.delete(key));\n }\n\n keys(prefix?: string): Promise<Iterable<string> | AsyncIterable<string>> {\n return Promise.resolve(this.#inner.keys(prefix));\n }\n\n async #read(): Promise<Readonly<Record<string, T>> | undefined> {\n const parsed = await readJsonFile(this.#path);\n if (parsed === undefined) return undefined;\n const envelope =\n typeof parsed === 'object' && parsed !== null\n ? (parsed as { version?: unknown; values?: unknown })\n : undefined;\n if (\n envelope?.version !== FILE_STORE_VERSION ||\n typeof envelope.values !== 'object' ||\n envelope.values === null ||\n Array.isArray(envelope.values)\n ) {\n throw new ItdConfigError(\n `Файл ${this.#path} имеет неподдерживаемый формат: ожидается ` +\n `{ version: ${FILE_STORE_VERSION}, values }. Возьмите другой путь, чтобы не ` +\n 'перезаписать посторонние данные.',\n );\n }\n return envelope.values as Readonly<Record<string, T>>;\n }\n}\n\n/**\n * Хранит сессию в файле.\n *\n * Нужна долгоживущим процессам: без неё бот при каждом запуске входит заново — а вход\n * требует решённой капчи, да и серия входов подряд может привести к временной блокировке\n * аккаунта. С сохранённой сессией опция `auth` не нужна вовсе.\n *\n * Файл создаётся с правами `0600` (чтение и запись только владельцу) — в нём лежат токены.\n * Запись идёт через временный файл с последующим переименованием, поэтому падение процесса\n * посреди сохранения не оставит повреждённую сессию.\n *\n * @example\n * ```ts\n * const itd = new ItdClient({ storage: new FileTokenStorage('./.itd-session.json') });\n * ```\n */\nexport class FileTokenStorage implements TokenStorage {\n readonly #inner: TokenStorage;\n\n /** @param path путь к файлу сессии. Добавьте его в `.gitignore`. */\n constructor(path: string) {\n this.#inner = createTokenStorage(new FileKeyValueStore<ItdSession>(path));\n }\n\n get(): Promise<ItdSession | null> {\n return Promise.resolve(this.#inner.get());\n }\n\n set(session: ItdSession): Promise<void> {\n return Promise.resolve(this.#inner.set(session));\n }\n\n clear(): Promise<void> {\n return Promise.resolve(this.#inner.clear());\n }\n}\n\n/**\n * Хранит сессии нескольких аккаунтов в одном файле.\n *\n * Разбирается с гонкой «прочитать, изменить, записать»: десять аккаунтов пишут в один файл,\n * и без общего слепка с очередью записей они теряли бы сессии друг друга. Как и\n * {@link FileTokenStorage}, пишет через временный файл с правами `0600`.\n *\n * Использует тот же версионированный {@link FileKeyValueStore}, что и одиночное хранилище.\n *\n * @example\n * ```ts\n * const accounts = new ItdAccounts({\n * storage: new FileMultiTokenStorage('./.itd-sessions.json'),\n * });\n * await accounts.restore();\n * ```\n */\nexport class FileMultiTokenStorage implements MultiTokenStorage {\n readonly #inner: MultiTokenStorage;\n\n /** @param path путь к файлу сессий. Добавьте его в `.gitignore`. */\n constructor(path: string) {\n this.#inner = createMultiTokenStorage(new FileKeyValueStore<ItdSession>(path));\n }\n\n get(account: string): Promise<ItdSession | null> {\n return Promise.resolve(this.#inner.get(account));\n }\n\n set(account: string, session: ItdSession): Promise<void> {\n return Promise.resolve(this.#inner.set(account, session));\n }\n\n clear(account: string): Promise<void> {\n return Promise.resolve(this.#inner.clear(account));\n }\n\n accounts(): Promise<readonly string[]> {\n return Promise.resolve(this.#inner.accounts());\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAS,aAAa,OAAgB,MAAuB;CAC3D,OAAO,OAAO,UAAU,YAAY,UAAU,QAAQ,UAAU,SAAS,MAAM,SAAS;AAC1F;AAyCA,SAAgB,SAAS,MAAc,UAA2B,CAAC,GAA0B;CAC3F,MAAM,WAAWA,sBAAAA,yBAAyB,OAAO;CAEjD,IAAI,SAAS,SAASC,sBAAAA,iBAAiB,QACrC,OAAO,EACL,MAAM,OAAO,EAAE,aAAa;EAC1B,MAAM,CAAC,EAAE,oBAAoB,EAAE,QAAQ,EAAE,YAAY,EAAE,cAAc,MAAM,QAAQ,IAAI;GACrF,OAAO;GACP,OAAO;GACP,OAAO;GACP,OAAO;EACT,CAAC;EACD,MAAM,OAAO,MAAM,KAAK,IAAI;EAC5B,IAAI,SAAS,aAAa,KAAA,KAAa,KAAK,OAAO,SAAS,UAC1D,MAAM,IAAIC,gBAAAA,aACR,QAAQ,KAAK,oBAAoB,SAAS,SAAS,SAAS,KAAK,QACjE;GACE,QAAQC,gBAAAA,mBAAmB;GAC3B,OAAO,SAAS;GAChB,QAAQ,KAAK;EACf,CACF;EAGF,MAAM,OAAO,iBAAiB,MAAM;GAClC,eAAe,KAAK,IAAI,SAAS,mBAAmB,OAAW;GAC/D,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;EAC7B,CAAC;EACD,MAAM,SAAS,SAAS,MAAM,IAAI;EAClC,OAAO;GACL,QAAQC,sBAAAA,kBAAkB,QAAQ;IAChC,GAAI,SAAS,aAAa,KAAA,IAAY,EAAE,UAAU,SAAS,SAAS,IAAI,CAAC;IACzE,mBAAmB,SAAS;IAC5B,GAAI,SAAS,EAAE,OAAO,IAAI,CAAC;GAC7B,CAAC;GACD,UAAU,QAAQ,YAAY,SAAS,IAAI;GAC3C,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,YAAY,IAAI,CAAC;GAClE,MAAM,KAAK;GACX,aAAa;IACX,KAAK,QAAQ;GACf;EACF;CACF,EACF;CAGF,OAAO,EACL,MAAM,OAAO,EAAE,aAAa;EAC1B,MAAM,CAAC,EAAE,UAAU,QAAQ,EAAE,cAAc,MAAM,QAAQ,IAAI,CAC3D,OAAO,qBACP,OAAO,YACT,CAAC;EACD,IAAI,SAAS,aAAa,KAAA,GAAW;GACnC,MAAM,OAAO,MAAM,KAAK,IAAI;GAC5B,IAAI,KAAK,OAAO,SAAS,UACvB,MAAM,IAAIF,gBAAAA,aACR,QAAQ,KAAK,oBAAoB,SAAS,SAAS,SAAS,KAAK,QACjE;IACE,QAAQC,gBAAAA,mBAAmB;IAC3B,OAAO,SAAS;IAChB,QAAQ,KAAK;GACf,CACF;EAEJ;EAEA,MAAM,OAAO,IAAI,WAAW,MAAM,SAAS,MAAM,SAAS,EAAE,OAAO,IAAI,KAAA,CAAS,CAAC;EACjF,IAAI,SAAS,aAAa,KAAA,KAAa,KAAK,aAAa,SAAS,UAChE,MAAM,IAAID,gBAAAA,aACR,QAAQ,KAAK,oBAAoB,SAAS,SAAS,SAAS,KAAK,cACjE;GACE,QAAQC,gBAAAA,mBAAmB;GAC3B,OAAO,SAAS;GAChB,QAAQ,KAAK;EACf,CACF;EAGF,OAAO;GACL;GACA,UAAU,QAAQ,YAAY,SAAS,IAAI;GAC3C,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,YAAY,IAAI,CAAC;EACpE;CACF,EACF;AACF;;;;;;;AAQA,eAAe,aAAa,MAAgC;CAC1D,IAAI;CACJ,IAAI;EACF,MAAM,EAAE,aAAa,MAAM,OAAO;EAClC,MAAM,MAAM,SAAS,MAAM,MAAM;CACnC,SAAS,OAAO;EACd,IAAI,aAAa,OAAO,QAAQ,GAAG,OAAO,KAAA;EAC1C,MAAM;CACR;CAEA,IAAI;EACF,OAAO,KAAK,MAAM,GAAG;CACvB,QAAQ;EACN,MAAM,IAAIE,gBAAAA,eACR,QAAQ,KAAK,8GAEf;CACF;AACF;;;;;;;AAQA,eAAe,gBAAgB,MAAc,OAA+B;CAC1E,MAAM,EAAE,WAAW,QAAQ,WAAW,MAAM,OAAO;CAKnD,MAAM,YAAY,GAAG,KAAK,GADd,OAAO,YAAY,cAAc,QAAQ,MAAM,EAC1B,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE;CAExE,IAAI;EACF,MAAM,UAAU,WAAW,KAAK,UAAU,OAAO,MAAM,CAAC,GAAG;GAAE,UAAU;GAAQ,MAAM;EAAM,CAAC;EAC5F,MAAM,OAAO,WAAW,IAAI;CAC9B,SAAS,OAAO;EACd,MAAM,OAAO,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;EACtC,MAAM;CACR;AACF;;AAGA,eAAe,WAAW,MAA6B;CACrD,MAAM,EAAE,OAAO,MAAM,OAAO;CAC5B,MAAM,GAAG,MAAM,EAAE,OAAO,KAAK,CAAC;AAChC;;AAGA,MAAM,qBAAqB;;;;;;;AAQ3B,IAAa,oBAAb,MAAkC;CAChC;CACA;;CAGA,YAAY,MAAc;EACxB,KAAKC,QAAQ;EACb,KAAKC,SAASC,gBAAAA,0BAA6B;GACzC,YAAY,KAAKC,MAAM;GACvB,QAAQ,WAAW,gBAAgB,MAAM;IAAE,SAAS;IAAoB;GAAO,CAAC;GAChF,cAAc,WAAW,IAAI;EAC/B,CAAC;CACH;CAEA,IAAI,KAAqC;EACvC,OAAO,QAAQ,QAAQ,KAAKF,OAAO,IAAI,GAAG,CAAC;CAC7C;CAEA,IAAI,KAAa,OAAyB;EACxC,OAAO,QAAQ,QAAQ,KAAKA,OAAO,IAAI,KAAK,KAAK,CAAC;CACpD;CAEA,OAAO,KAA4B;EACjC,OAAO,QAAQ,QAAQ,KAAKA,OAAO,OAAO,GAAG,CAAC;CAChD;CAEA,KAAK,QAAoE;EACvE,OAAO,QAAQ,QAAQ,KAAKA,OAAO,KAAK,MAAM,CAAC;CACjD;CAEA,MAAME,QAA0D;EAC9D,MAAM,SAAS,MAAM,aAAa,KAAKH,KAAK;EAC5C,IAAI,WAAW,KAAA,GAAW,OAAO,KAAA;EACjC,MAAM,WACJ,OAAO,WAAW,YAAY,WAAW,OACpC,SACD,KAAA;EACN,IACE,UAAU,YAAY,sBACtB,OAAO,SAAS,WAAW,YAC3B,SAAS,WAAW,QACpB,MAAM,QAAQ,SAAS,MAAM,GAE7B,MAAM,IAAID,gBAAAA,eACR,QAAQ,KAAKC,MAAM,uDACH,mBAAmB,4EAErC;EAEF,OAAO,SAAS;CAClB;AACF;;;;;;;;;;;;;;;;;AAkBA,IAAa,mBAAb,MAAsD;CACpD;;CAGA,YAAY,MAAc;EACxB,KAAKC,SAASG,gBAAAA,mBAAmB,IAAI,kBAA8B,IAAI,CAAC;CAC1E;CAEA,MAAkC;EAChC,OAAO,QAAQ,QAAQ,KAAKH,OAAO,IAAI,CAAC;CAC1C;CAEA,IAAI,SAAoC;EACtC,OAAO,QAAQ,QAAQ,KAAKA,OAAO,IAAI,OAAO,CAAC;CACjD;CAEA,QAAuB;EACrB,OAAO,QAAQ,QAAQ,KAAKA,OAAO,MAAM,CAAC;CAC5C;AACF;;;;;;;;;;;;;;;;;;AAmBA,IAAa,wBAAb,MAAgE;CAC9D;;CAGA,YAAY,MAAc;EACxB,KAAKA,SAASI,sBAAAA,wBAAwB,IAAI,kBAA8B,IAAI,CAAC;CAC/E;CAEA,IAAI,SAA6C;EAC/C,OAAO,QAAQ,QAAQ,KAAKJ,OAAO,IAAI,OAAO,CAAC;CACjD;CAEA,IAAI,SAAiB,SAAoC;EACvD,OAAO,QAAQ,QAAQ,KAAKA,OAAO,IAAI,SAAS,OAAO,CAAC;CAC1D;CAEA,MAAM,SAAgC;EACpC,OAAO,QAAQ,QAAQ,KAAKA,OAAO,MAAM,OAAO,CAAC;CACnD;CAEA,WAAuC;EACrC,OAAO,QAAQ,QAAQ,KAAKA,OAAO,SAAS,CAAC;CAC/C;AACF"}
|
package/dist/node.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { r as TokenStorage, t as ItdSession } from "./storage-
|
|
2
|
-
import {
|
|
1
|
+
import { r as TokenStorage, t as ItdSession } from "./storage-BqMxs76Y.cjs";
|
|
2
|
+
import { d as FileTransferMode, m as StreamFile, n as MultiTokenStorage, p as LazyFile, u as FileStreamOptions } from "./multi-storage-BUoEoW8f.cjs";
|
|
3
3
|
//#region src/node.d.ts
|
|
4
4
|
/** Что можно уточнить у файла с диска. */
|
|
5
5
|
interface PathFileOptions extends FileStreamOptions {
|
|
@@ -37,6 +37,21 @@ declare function fromPath(path: string, options?: PathFileOptions & {
|
|
|
37
37
|
mode?: typeof FileTransferMode.Buffer;
|
|
38
38
|
}): LazyFile;
|
|
39
39
|
declare function fromPath(path: string, options: PathFileOptions): LazyFile | StreamFile;
|
|
40
|
+
/**
|
|
41
|
+
* Key-value backend в одном JSON-файле.
|
|
42
|
+
*
|
|
43
|
+
* Изменения сериализуются внутри экземпляра и записываются атомарным переименованием. Несколько
|
|
44
|
+
* экземпляров или процессов, направленных на один путь, требуют внешней синхронизации.
|
|
45
|
+
*/
|
|
46
|
+
declare class FileKeyValueStore<T> {
|
|
47
|
+
#private;
|
|
48
|
+
/** @param path путь к JSON-файлу. Для секретов добавьте его в `.gitignore`. */
|
|
49
|
+
constructor(path: string);
|
|
50
|
+
get(key: string): Promise<T | undefined>;
|
|
51
|
+
set(key: string, value: T): Promise<void>;
|
|
52
|
+
delete(key: string): Promise<void>;
|
|
53
|
+
keys(prefix?: string): Promise<Iterable<string> | AsyncIterable<string>>;
|
|
54
|
+
}
|
|
40
55
|
/**
|
|
41
56
|
* Хранит сессию в файле.
|
|
42
57
|
*
|
|
@@ -68,8 +83,7 @@ declare class FileTokenStorage implements TokenStorage {
|
|
|
68
83
|
* и без общего слепка с очередью записей они теряли бы сессии друг друга. Как и
|
|
69
84
|
* {@link FileTokenStorage}, пишет через временный файл с правами `0600`.
|
|
70
85
|
*
|
|
71
|
-
*
|
|
72
|
-
* не спутать с однопользовательским, и чужой не будет молча перезаписан.
|
|
86
|
+
* Использует тот же версионированный {@link FileKeyValueStore}, что и одиночное хранилище.
|
|
73
87
|
*
|
|
74
88
|
* @example
|
|
75
89
|
* ```ts
|
|
@@ -89,5 +103,5 @@ declare class FileMultiTokenStorage implements MultiTokenStorage {
|
|
|
89
103
|
accounts(): Promise<readonly string[]>;
|
|
90
104
|
}
|
|
91
105
|
//#endregion
|
|
92
|
-
export { FileMultiTokenStorage, FileTokenStorage, PathFileOptions, fromPath };
|
|
106
|
+
export { FileKeyValueStore, FileMultiTokenStorage, FileTokenStorage, PathFileOptions, fromPath };
|
|
93
107
|
//# sourceMappingURL=node.d.cts.map
|
package/dist/node.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { r as TokenStorage, t as ItdSession } from "./storage-
|
|
2
|
-
import {
|
|
1
|
+
import { r as TokenStorage, t as ItdSession } from "./storage-BqMxs76Y.js";
|
|
2
|
+
import { d as FileTransferMode, m as StreamFile, n as MultiTokenStorage, p as LazyFile, u as FileStreamOptions } from "./multi-storage-CQSlI_kn.js";
|
|
3
3
|
//#region src/node.d.ts
|
|
4
4
|
/** Что можно уточнить у файла с диска. */
|
|
5
5
|
interface PathFileOptions extends FileStreamOptions {
|
|
@@ -37,6 +37,21 @@ declare function fromPath(path: string, options?: PathFileOptions & {
|
|
|
37
37
|
mode?: typeof FileTransferMode.Buffer;
|
|
38
38
|
}): LazyFile;
|
|
39
39
|
declare function fromPath(path: string, options: PathFileOptions): LazyFile | StreamFile;
|
|
40
|
+
/**
|
|
41
|
+
* Key-value backend в одном JSON-файле.
|
|
42
|
+
*
|
|
43
|
+
* Изменения сериализуются внутри экземпляра и записываются атомарным переименованием. Несколько
|
|
44
|
+
* экземпляров или процессов, направленных на один путь, требуют внешней синхронизации.
|
|
45
|
+
*/
|
|
46
|
+
declare class FileKeyValueStore<T> {
|
|
47
|
+
#private;
|
|
48
|
+
/** @param path путь к JSON-файлу. Для секретов добавьте его в `.gitignore`. */
|
|
49
|
+
constructor(path: string);
|
|
50
|
+
get(key: string): Promise<T | undefined>;
|
|
51
|
+
set(key: string, value: T): Promise<void>;
|
|
52
|
+
delete(key: string): Promise<void>;
|
|
53
|
+
keys(prefix?: string): Promise<Iterable<string> | AsyncIterable<string>>;
|
|
54
|
+
}
|
|
40
55
|
/**
|
|
41
56
|
* Хранит сессию в файле.
|
|
42
57
|
*
|
|
@@ -68,8 +83,7 @@ declare class FileTokenStorage implements TokenStorage {
|
|
|
68
83
|
* и без общего слепка с очередью записей они теряли бы сессии друг друга. Как и
|
|
69
84
|
* {@link FileTokenStorage}, пишет через временный файл с правами `0600`.
|
|
70
85
|
*
|
|
71
|
-
*
|
|
72
|
-
* не спутать с однопользовательским, и чужой не будет молча перезаписан.
|
|
86
|
+
* Использует тот же версионированный {@link FileKeyValueStore}, что и одиночное хранилище.
|
|
73
87
|
*
|
|
74
88
|
* @example
|
|
75
89
|
* ```ts
|
|
@@ -89,5 +103,5 @@ declare class FileMultiTokenStorage implements MultiTokenStorage {
|
|
|
89
103
|
accounts(): Promise<readonly string[]>;
|
|
90
104
|
}
|
|
91
105
|
//#endregion
|
|
92
|
-
export { FileMultiTokenStorage, FileTokenStorage, PathFileOptions, fromPath };
|
|
106
|
+
export { FileKeyValueStore, FileMultiTokenStorage, FileTokenStorage, PathFileOptions, fromPath };
|
|
93
107
|
//# sourceMappingURL=node.d.ts.map
|
package/dist/node.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { o as resolveFileStreamOptions, p as FileTransferMode, r as createMultiTokenStorage, s as boundedFileStream } from "./multi-storage-BUZaLAPO.js";
|
|
2
|
+
import { b as ItdFileErrorReason, h as ItdConfigError, r as createTokenStorage, s as createRecordKeyValueStore, y as ItdFileError } from "./storage-IHdXw52v.js";
|
|
3
3
|
//#region src/node.ts
|
|
4
4
|
/**
|
|
5
5
|
* `itd-api/node` — файловая система для Node, Bun и Deno.
|
|
@@ -41,11 +41,12 @@ function fromPath(path, options = {}) {
|
|
|
41
41
|
actual: info.size
|
|
42
42
|
});
|
|
43
43
|
const file = createReadStream(path, {
|
|
44
|
-
highWaterMark: Math.min(resolved.streamBufferBytes,
|
|
44
|
+
highWaterMark: Math.min(resolved.streamBufferBytes, 1048576),
|
|
45
45
|
...signal ? { signal } : {}
|
|
46
46
|
});
|
|
47
|
+
const source = Readable.toWeb(file);
|
|
47
48
|
return {
|
|
48
|
-
stream: boundedFileStream(
|
|
49
|
+
stream: boundedFileStream(source, {
|
|
49
50
|
...resolved.maxBytes !== void 0 ? { maxBytes: resolved.maxBytes } : {},
|
|
50
51
|
streamBufferBytes: resolved.streamBufferBytes,
|
|
51
52
|
...signal ? { signal } : {}
|
|
@@ -84,25 +85,22 @@ function fromPath(path, options = {}) {
|
|
|
84
85
|
/**
|
|
85
86
|
* Читает и разбирает JSON-файл.
|
|
86
87
|
*
|
|
87
|
-
* Отсутствующий файл означает `
|
|
88
|
-
*
|
|
89
|
-
* а мультихранилище требует строгого разбора: молча затереть файл с несколькими токенами
|
|
90
|
-
* особенно опасно.
|
|
88
|
+
* Отсутствующий файл означает `undefined`. Повреждённый JSON не считается пустым хранилищем:
|
|
89
|
+
* иначе следующая запись могла бы незаметно затереть данные.
|
|
91
90
|
*/
|
|
92
|
-
async function readJsonFile(path
|
|
91
|
+
async function readJsonFile(path) {
|
|
93
92
|
let raw;
|
|
94
93
|
try {
|
|
95
94
|
const { readFile } = await import("node:fs/promises");
|
|
96
95
|
raw = await readFile(path, "utf8");
|
|
97
96
|
} catch (error) {
|
|
98
|
-
if (hasErrorCode(error, "ENOENT")) return
|
|
97
|
+
if (hasErrorCode(error, "ENOENT")) return void 0;
|
|
99
98
|
throw error;
|
|
100
99
|
}
|
|
101
100
|
try {
|
|
102
101
|
return JSON.parse(raw);
|
|
103
102
|
} catch {
|
|
104
|
-
|
|
105
|
-
throw new ItdConfigError(`Файл ${path} повреждён: ожидался JSON с сессиями аккаунтов. Исправьте файл или перенесите его перед следующим сохранением.`);
|
|
103
|
+
throw new ItdConfigError(`Файл ${path} повреждён: ожидался JSON key-value хранилища. Исправьте файл или перенесите его перед следующим сохранением.`);
|
|
106
104
|
}
|
|
107
105
|
}
|
|
108
106
|
/**
|
|
@@ -130,6 +128,49 @@ async function removeFile(path) {
|
|
|
130
128
|
const { rm } = await import("node:fs/promises");
|
|
131
129
|
await rm(path, { force: true });
|
|
132
130
|
}
|
|
131
|
+
/** Версия общего формата файлового key-value backend. */
|
|
132
|
+
const FILE_STORE_VERSION = 1;
|
|
133
|
+
/**
|
|
134
|
+
* Key-value backend в одном JSON-файле.
|
|
135
|
+
*
|
|
136
|
+
* Изменения сериализуются внутри экземпляра и записываются атомарным переименованием. Несколько
|
|
137
|
+
* экземпляров или процессов, направленных на один путь, требуют внешней синхронизации.
|
|
138
|
+
*/
|
|
139
|
+
var FileKeyValueStore = class {
|
|
140
|
+
#path;
|
|
141
|
+
#inner;
|
|
142
|
+
/** @param path путь к JSON-файлу. Для секретов добавьте его в `.gitignore`. */
|
|
143
|
+
constructor(path) {
|
|
144
|
+
this.#path = path;
|
|
145
|
+
this.#inner = createRecordKeyValueStore({
|
|
146
|
+
read: () => this.#read(),
|
|
147
|
+
write: (values) => writeJsonAtomic(path, {
|
|
148
|
+
version: FILE_STORE_VERSION,
|
|
149
|
+
values
|
|
150
|
+
}),
|
|
151
|
+
delete: () => removeFile(path)
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
get(key) {
|
|
155
|
+
return Promise.resolve(this.#inner.get(key));
|
|
156
|
+
}
|
|
157
|
+
set(key, value) {
|
|
158
|
+
return Promise.resolve(this.#inner.set(key, value));
|
|
159
|
+
}
|
|
160
|
+
delete(key) {
|
|
161
|
+
return Promise.resolve(this.#inner.delete(key));
|
|
162
|
+
}
|
|
163
|
+
keys(prefix) {
|
|
164
|
+
return Promise.resolve(this.#inner.keys(prefix));
|
|
165
|
+
}
|
|
166
|
+
async #read() {
|
|
167
|
+
const parsed = await readJsonFile(this.#path);
|
|
168
|
+
if (parsed === void 0) return void 0;
|
|
169
|
+
const envelope = typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
170
|
+
if (envelope?.version !== FILE_STORE_VERSION || typeof envelope.values !== "object" || envelope.values === null || Array.isArray(envelope.values)) throw new ItdConfigError(`Файл ${this.#path} имеет неподдерживаемый формат: ожидается { version: ${FILE_STORE_VERSION}, values }. Возьмите другой путь, чтобы не перезаписать посторонние данные.`);
|
|
171
|
+
return envelope.values;
|
|
172
|
+
}
|
|
173
|
+
};
|
|
133
174
|
/**
|
|
134
175
|
* Хранит сессию в файле.
|
|
135
176
|
*
|
|
@@ -147,36 +188,21 @@ async function removeFile(path) {
|
|
|
147
188
|
* ```
|
|
148
189
|
*/
|
|
149
190
|
var FileTokenStorage = class {
|
|
150
|
-
#
|
|
151
|
-
/**
|
|
152
|
-
* Цепочка операций с файлом. Запись и удаление выполняются последовательно
|
|
153
|
-
* в порядке вызова; ошибка одной операции не останавливает следующие.
|
|
154
|
-
*/
|
|
155
|
-
#writing = Promise.resolve();
|
|
191
|
+
#inner;
|
|
156
192
|
/** @param path путь к файлу сессии. Добавьте его в `.gitignore`. */
|
|
157
193
|
constructor(path) {
|
|
158
|
-
this.#
|
|
194
|
+
this.#inner = createTokenStorage(new FileKeyValueStore(path));
|
|
159
195
|
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
const parsed = await readJsonFile(this.#path);
|
|
163
|
-
return typeof parsed === "object" && parsed !== null ? copySession(parsed) : null;
|
|
196
|
+
get() {
|
|
197
|
+
return Promise.resolve(this.#inner.get());
|
|
164
198
|
}
|
|
165
199
|
set(session) {
|
|
166
|
-
|
|
167
|
-
return this.#enqueue(() => writeJsonAtomic(this.#path, snapshot));
|
|
200
|
+
return Promise.resolve(this.#inner.set(session));
|
|
168
201
|
}
|
|
169
202
|
clear() {
|
|
170
|
-
return this.#
|
|
171
|
-
}
|
|
172
|
-
/** Добавляет файловую операцию в очередь. */
|
|
173
|
-
#enqueue(operation) {
|
|
174
|
-
this.#writing = this.#writing.then(operation, operation);
|
|
175
|
-
return this.#writing;
|
|
203
|
+
return Promise.resolve(this.#inner.clear());
|
|
176
204
|
}
|
|
177
205
|
};
|
|
178
|
-
/** Версия формата файла с сессиями нескольких аккаунтов. */
|
|
179
|
-
const SESSIONS_FILE_VERSION = 1;
|
|
180
206
|
/**
|
|
181
207
|
* Хранит сессии нескольких аккаунтов в одном файле.
|
|
182
208
|
*
|
|
@@ -184,8 +210,7 @@ const SESSIONS_FILE_VERSION = 1;
|
|
|
184
210
|
* и без общего слепка с очередью записей они теряли бы сессии друг друга. Как и
|
|
185
211
|
* {@link FileTokenStorage}, пишет через временный файл с правами `0600`.
|
|
186
212
|
*
|
|
187
|
-
*
|
|
188
|
-
* не спутать с однопользовательским, и чужой не будет молча перезаписан.
|
|
213
|
+
* Использует тот же версионированный {@link FileKeyValueStore}, что и одиночное хранилище.
|
|
189
214
|
*
|
|
190
215
|
* @example
|
|
191
216
|
* ```ts
|
|
@@ -196,19 +221,10 @@ const SESSIONS_FILE_VERSION = 1;
|
|
|
196
221
|
* ```
|
|
197
222
|
*/
|
|
198
223
|
var FileMultiTokenStorage = class {
|
|
199
|
-
#path;
|
|
200
224
|
#inner;
|
|
201
225
|
/** @param path путь к файлу сессий. Добавьте его в `.gitignore`. */
|
|
202
226
|
constructor(path) {
|
|
203
|
-
this.#
|
|
204
|
-
this.#inner = createRecordMultiStorage({
|
|
205
|
-
read: () => this.#read(),
|
|
206
|
-
write: (accounts) => writeJsonAtomic(path, {
|
|
207
|
-
version: SESSIONS_FILE_VERSION,
|
|
208
|
-
accounts
|
|
209
|
-
}),
|
|
210
|
-
remove: () => removeFile(path)
|
|
211
|
-
});
|
|
227
|
+
this.#inner = createMultiTokenStorage(new FileKeyValueStore(path));
|
|
212
228
|
}
|
|
213
229
|
get(account) {
|
|
214
230
|
return Promise.resolve(this.#inner.get(account));
|
|
@@ -222,16 +238,8 @@ var FileMultiTokenStorage = class {
|
|
|
222
238
|
accounts() {
|
|
223
239
|
return Promise.resolve(this.#inner.accounts());
|
|
224
240
|
}
|
|
225
|
-
async #read() {
|
|
226
|
-
const parsed = await readJsonFile(this.#path, true);
|
|
227
|
-
if (parsed === null) return null;
|
|
228
|
-
const envelope = typeof parsed === "object" && parsed !== null ? parsed : void 0;
|
|
229
|
-
const accounts = envelope?.accounts;
|
|
230
|
-
if (envelope?.version !== SESSIONS_FILE_VERSION || typeof accounts !== "object" || accounts === null || Array.isArray(accounts)) throw new ItdConfigError(`Файл ${this.#path} имеет неподдерживаемый формат: ожидается { version: ${SESSIONS_FILE_VERSION}, accounts }. Возможно, это файл одиночной сессии или файл другой версии — возьмите отдельный путь, чтобы не перезаписать его.`);
|
|
231
|
-
return accounts;
|
|
232
|
-
}
|
|
233
241
|
};
|
|
234
242
|
//#endregion
|
|
235
|
-
export { FileMultiTokenStorage, FileTokenStorage, fromPath };
|
|
243
|
+
export { FileKeyValueStore, FileMultiTokenStorage, FileTokenStorage, fromPath };
|
|
236
244
|
|
|
237
245
|
//# sourceMappingURL=node.js.map
|