itd-api 0.7.1 → 0.8.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 +14 -14
- package/dist/events/index.cjs +53 -0
- package/dist/events/index.d.cts +3 -0
- package/dist/events/index.d.ts +3 -0
- package/dist/events/index.js +5 -0
- package/dist/index.cjs +269 -175
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +77 -56
- package/dist/index.d.ts +77 -56
- package/dist/index.js +251 -160
- package/dist/index.js.map +1 -1
- package/dist/node/index.cjs +4 -4
- package/dist/node/index.d.cts +1 -1
- package/dist/node/index.d.ts +1 -1
- package/dist/node/index.js +4 -4
- package/dist/rest/index.cjs +72 -14
- package/dist/rest/index.cjs.map +1 -1
- package/dist/rest/index.d.cts +11 -3
- package/dist/rest/index.d.ts +11 -3
- package/dist/rest/index.js +66 -8
- package/dist/rest/index.js.map +1 -1
- package/dist/shared/{errors-DfU8M5eS.cjs → errors-BmP3TKoW.cjs} +3 -3
- package/dist/shared/{errors-DfU8M5eS.cjs.map → errors-BmP3TKoW.cjs.map} +1 -1
- package/dist/shared/{errors-Bhrd2fJd.js → errors-GI10kZxk.js} +3 -3
- package/dist/shared/{errors-Bhrd2fJd.js.map → errors-GI10kZxk.js.map} +1 -1
- package/dist/shared/events-Br2r7coi.d.cts +698 -0
- package/dist/shared/events-QyhQZrnC.d.ts +698 -0
- package/dist/shared/{websocket-CbzB1Leq.js → events-TfhKh26b.js} +770 -490
- package/dist/shared/events-TfhKh26b.js.map +1 -0
- package/dist/shared/{websocket-sYlynrr0.cjs → events-iTteVsd_.cjs} +834 -518
- package/dist/shared/events-iTteVsd_.cjs.map +1 -0
- package/dist/shared/{multi-storage-CkvTUC5m.js → multi-storage-1AEZTczk.js} +4 -4
- package/dist/shared/{multi-storage-CkvTUC5m.js.map → multi-storage-1AEZTczk.js.map} +1 -1
- package/dist/shared/{multi-storage--yTEqiod.cjs → multi-storage-CCUgkqvI.cjs} +6 -6
- package/dist/shared/{multi-storage--yTEqiod.cjs.map → multi-storage-CCUgkqvI.cjs.map} +1 -1
- package/dist/shared/{options-DtATYdLr.js → options-BNP633A5.js} +8 -6
- package/dist/shared/options-BNP633A5.js.map +1 -0
- package/dist/shared/{options-Dg5N3r1V.cjs → options-BUfoHE9G.cjs} +8 -6
- package/dist/shared/options-BUfoHE9G.cjs.map +1 -0
- package/dist/shared/{cookies-DZwFq6kr.cjs → redact-DB9EMGJx.cjs} +301 -2
- package/dist/shared/redact-DB9EMGJx.cjs.map +1 -0
- package/dist/shared/{cookies-tX2sNwxb.js → redact-FNK29Fv-.js} +224 -3
- package/dist/shared/redact-FNK29Fv-.js.map +1 -0
- package/dist/shared/{render-DMp_3Nzk.d.cts → render-BszcKF7S.d.cts} +432 -254
- package/dist/shared/{render-vtLixIiU.js → render-CiL1VS0b.js} +1261 -621
- package/dist/shared/render-CiL1VS0b.js.map +1 -0
- package/dist/shared/{render-DyeHJNBw.cjs → render-DNTESBSl.cjs} +1335 -671
- package/dist/shared/render-DNTESBSl.cjs.map +1 -0
- package/dist/shared/{render-DZrxhC5_.d.ts → render-DVB7T8pm.d.ts} +432 -254
- package/dist/shared/{storage-BPJR_k4-.cjs → storage-BLmXmzKF.cjs} +2 -2
- package/dist/shared/{storage-BPJR_k4-.cjs.map → storage-BLmXmzKF.cjs.map} +1 -1
- package/dist/shared/{storage-D86edNCB.js → storage-CCxn0Kkv.js} +2 -2
- package/dist/shared/{storage-D86edNCB.js.map → storage-CCxn0Kkv.js.map} +1 -1
- package/dist/shared/{url-CYXgxqGx.js → url-B3ocFlHj.js} +491 -425
- package/dist/shared/url-B3ocFlHj.js.map +1 -0
- package/dist/shared/{url-B6-bXHKt.d.ts → url-Be9jlWRv.d.cts} +1233 -1195
- package/dist/shared/{url-B6-bXHKt.d.cts → url-Be9jlWRv.d.ts} +1233 -1195
- package/dist/shared/{url-yjl2c8Ie.cjs → url-CFN1sT9S.cjs} +562 -478
- package/dist/shared/url-CFN1sT9S.cjs.map +1 -0
- package/dist/web/index.cjs +2 -2
- package/dist/web/index.js +2 -2
- package/package.json +16 -15
- package/dist/realtime/index.cjs +0 -165
- package/dist/realtime/index.cjs.map +0 -1
- package/dist/realtime/index.d.cts +0 -51
- package/dist/realtime/index.d.ts +0 -51
- package/dist/realtime/index.js +0 -120
- package/dist/realtime/index.js.map +0 -1
- package/dist/shared/auth-provider-BfogACAb.js +0 -91
- package/dist/shared/auth-provider-BfogACAb.js.map +0 -1
- package/dist/shared/auth-provider-CTJkKfgy.cjs +0 -108
- package/dist/shared/auth-provider-CTJkKfgy.cjs.map +0 -1
- package/dist/shared/cookies-DZwFq6kr.cjs.map +0 -1
- package/dist/shared/cookies-tX2sNwxb.js.map +0 -1
- package/dist/shared/options-Dg5N3r1V.cjs.map +0 -1
- package/dist/shared/options-DtATYdLr.js.map +0 -1
- package/dist/shared/render-DyeHJNBw.cjs.map +0 -1
- package/dist/shared/render-vtLixIiU.js.map +0 -1
- package/dist/shared/url-CYXgxqGx.js.map +0 -1
- package/dist/shared/url-yjl2c8Ie.cjs.map +0 -1
- package/dist/shared/websocket-BMtihD56.d.ts +0 -562
- package/dist/shared/websocket-CbzB1Leq.js.map +0 -1
- package/dist/shared/websocket-D1p32SB2.d.cts +0 -562
- package/dist/shared/websocket-sYlynrr0.cjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"multi-storage--yTEqiod.cjs","names":["CookieJar","AUTH_FLAG_COOKIE","#store","MemoryKeyValueStore","copySession","ItdConfigError","createKeyValueStore","isEnumerableKeyValueStore","collectKeyValueStoreKeys"],"sources":["../../src/session/multi-storage.ts"],"sourcesContent":["import { AUTH_FLAG_COOKIE, CookieJar } from '../core/cookies.js';\nimport { ItdConfigError } from '../core/errors.js';\nimport {\n collectKeyValueStoreKeys,\n createKeyValueStore,\n type EnumerableKeyValueStore,\n isEnumerableKeyValueStore,\n MemoryKeyValueStore,\n} from '../core/key-value-store.js';\nimport { copySession, type ItdSession, type TokenStorage } from './storage.js';\n\n/**\n * Хранилище сессий нескольких аккаунтов.\n *\n * Отличается от {@link TokenStorage} тем, что каждый метод получает **имя аккаунта**:\n * так адаптер сам решает, как строить ключ, и Redis, БД или связка ключей получают то,\n * что им нужно. Имя приходит ровно тем, под которым аккаунт заведён в {@link ItdAccounts}:\n * библиотека его не нормализует и не экранирует — префиксы, экранирование и ограничения\n * на длину ключа остаются за адаптером.\n *\n * Все методы могут быть как синхронными, так и асинхронными.\n *\n * @example Хранилище поверх enumerable Redis backend\n * ```ts\n * const storage = createMultiTokenStorage(withNamespace(redisStore, 'itd'));\n * ```\n */\nexport interface MultiTokenStorage {\n /** Прочитать сессию аккаунта. `null`, если её нет. */\n get(account: string): ItdSession | null | Promise<ItdSession | null>;\n /** Сохранить сессию аккаунта целиком. */\n set(account: string, session: ItdSession): void | Promise<void>;\n /** Удалить сессию аккаунта. Вызывается при выходе и при неудачном обновлении токена. */\n clear(account: string): void | Promise<void>;\n /**\n * Имена сохранённых записей — по ним `ItdAccounts.restore()` находит кандидатов\n * после перезапуска процесса.\n *\n * Список ведёт сам адаптер: у файлового и памятного он виден из самой записи,\n * а хранилищу «ключ — значение» придётся держать множество имён рядом с сессиями.\n * Перед восстановлением контейнер читает каждую запись и пропускает оставшийся после\n * выхода одинокий `deviceId`: без токена или refresh-сессии авторизоваться невозможно.\n * Пустой список означает лишь то, что кандидатов нет, — сами записи при этом могут быть\n * доступны по имени.\n */\n accounts(): readonly string[] | Promise<readonly string[]>;\n}\n\n/**\n * Срез мультихранилища как обычное {@link TokenStorage} — в таком виде его получает\n * отдельный `ItdClient`, который про соседние аккаунты ничего не знает.\n */\nexport function scopedTokenStorage(storage: MultiTokenStorage, account: string): TokenStorage {\n return {\n get: () => storage.get(account),\n set: (session) => storage.set(account, session),\n clear: () => storage.clear(account),\n };\n}\n\n/**\n * Можно ли восстановить авторизованный аккаунт из записи.\n *\n * Один `deviceId` не считается сессией: он намеренно переживает выход, но не позволяет\n * выполнить ни одного авторизованного запроса.\n *\n * @internal\n */\nexport function isRestorableSession(session: ItdSession | null): session is ItdSession {\n if (!session) return false;\n if (session.accessToken || session.refreshToken) return true;\n\n const jar = new CookieJar();\n jar.deserialize(session.cookies);\n return jar.has(AUTH_FLAG_COOKIE);\n}\n\n/**\n * Управляемый срез хранилища для клиента аккаунта.\n *\n * После `revoke()` новые записи не доходят до общего хранилища, а `drain()` позволяет\n * дождаться уже начатых. Так удалённый клиент или завершившийся с опозданием refresh\n * не воскресят сессию после `removeAccount(..., { forget: true })`.\n *\n * @internal\n */\nexport interface ControlledTokenStorage {\n storage: TokenStorage;\n revoke(): void;\n drain(): Promise<void>;\n}\n\n/** @internal */\nexport function controlledTokenStorage(\n storage: MultiTokenStorage,\n account: string,\n): ControlledTokenStorage {\n let revoked = false;\n const pending = new Set<Promise<void>>();\n\n const mutate = (operation: () => void | Promise<void>): Promise<void> => {\n if (revoked) return Promise.resolve();\n\n const promise = Promise.resolve().then(operation);\n pending.add(promise);\n void promise.then(\n () => pending.delete(promise),\n () => pending.delete(promise),\n );\n return promise;\n };\n\n return {\n storage: {\n get: () => (revoked ? null : storage.get(account)),\n set: (session) => mutate(() => storage.set(account, session)),\n clear: () => mutate(() => storage.clear(account)),\n },\n revoke() {\n revoked = true;\n },\n async drain() {\n await Promise.all([...pending]);\n },\n };\n}\n\n/**\n * Мультихранилище в памяти процесса — вариант по умолчанию.\n *\n * Сессии теряются при перезапуске. Для долгоживущих ботов возьмите `FileMultiTokenStorage`\n * из `itd-api/node` либо соберите своё через {@link createMultiTokenStorage}.\n */\nexport class MemoryMultiTokenStorage implements MultiTokenStorage {\n readonly #store = new MemoryKeyValueStore<ItdSession>();\n\n constructor(initial?: Readonly<Record<string, ItdSession>> | null) {\n for (const [account, session] of Object.entries(initial ?? {})) {\n this.#store.set(accountKey(account), copySession(session));\n }\n }\n\n get(account: string): ItdSession | null {\n const session = this.#store.get(accountKey(account));\n return session ? copySession(session) : null;\n }\n\n set(account: string, session: ItdSession): void {\n this.#store.set(accountKey(account), copySession(session));\n }\n\n clear(account: string): void {\n this.#store.delete(accountKey(account));\n }\n\n accounts(): string[] {\n return this.#store\n .keys(ACCOUNT_KEY_PREFIX)\n .flatMap((key) => readAccountName(key.slice(ACCOUNT_KEY_PREFIX.length)) ?? []);\n }\n}\n\nconst ACCOUNT_KEY_PREFIX = 'accounts/';\n\nfunction accountKey(account: string): string {\n return `${ACCOUNT_KEY_PREFIX}${encodeURIComponent(account)}`;\n}\n\n/** Читает имя аккаунта из ключа. `undefined` — ключ не декодируется и запись пропускается. */\nfunction readAccountName(encoded: string): string | undefined {\n try {\n return decodeURIComponent(encoded);\n } catch {\n console.warn(\n `[itd-api] запись хранилища пропущена: ключ ${JSON.stringify(encoded)} не декодируется`,\n );\n return undefined;\n }\n}\n\n/** Настройки доменного адаптера нескольких сессий. */\nexport interface MultiTokenStorageAdapterOptions {\n /** Префикс ключей аккаунтов. По умолчанию `accounts/`. */\n prefix?: string | undefined;\n}\n\n/**\n * Создаёт доменное хранилище нескольких сессий поверх enumerable key-value backend.\n *\n * Отдельный индекс аккаунтов не используется: `accounts()` перечисляет ключи backend. Поэтому\n * запись сессии не может разойтись с индексом, но backend обязан эффективно поддерживать `keys`.\n */\nexport function createMultiTokenStorage(\n store: EnumerableKeyValueStore<ItdSession>,\n options: MultiTokenStorageAdapterOptions = {},\n): MultiTokenStorage {\n const prefix = options.prefix ?? ACCOUNT_KEY_PREFIX;\n if (typeof prefix !== 'string') {\n throw new ItdConfigError('prefix MultiTokenStorage должен быть строкой');\n }\n const backend = createKeyValueStore(store);\n if (!isEnumerableKeyValueStore(backend)) {\n throw new ItdConfigError('MultiTokenStorage требует KeyValueStore с методом keys()');\n }\n const key = (account: string) => `${prefix}${encodeURIComponent(account)}`;\n return {\n async get(account) {\n const session = await backend.get(key(account));\n return session ? copySession(session) : null;\n },\n set: (account, session) => backend.set(key(account), copySession(session)),\n clear: (account) => backend.delete(key(account)),\n async accounts() {\n const keys = await collectKeyValueStoreKeys(backend, prefix);\n return keys.flatMap((value) => readAccountName(value.slice(prefix.length)) ?? []);\n },\n };\n}\n"],"mappings":";;;;;;;;AAoDA,SAAgB,mBAAmB,SAA4B,SAA+B;CAC5F,OAAO;EACL,WAAW,QAAQ,IAAI,OAAO;EAC9B,MAAM,YAAY,QAAQ,IAAI,SAAS,OAAO;EAC9C,aAAa,QAAQ,MAAM,OAAO;CACpC;AACF;;;;;;;;;AAUA,SAAgB,oBAAoB,SAAmD;CACrF,IAAI,CAAC,SAAS,OAAO;CACrB,IAAI,QAAQ,eAAe,QAAQ,cAAc,OAAO;CAExD,MAAM,MAAM,IAAIA,gBAAAA,UAAU;CAC1B,IAAI,YAAY,QAAQ,OAAO;CAC/B,OAAO,IAAI,IAAIC,gBAAAA,gBAAgB;AACjC;;AAkBA,SAAgB,uBACd,SACA,SACwB;CACxB,IAAI,UAAU;CACd,MAAM,0BAAU,IAAI,IAAmB;CAEvC,MAAM,UAAU,cAAyD;EACvE,IAAI,SAAS,OAAO,QAAQ,QAAQ;EAEpC,MAAM,UAAU,QAAQ,QAAQ,CAAC,CAAC,KAAK,SAAS;EAChD,QAAQ,IAAI,OAAO;EACnB,QAAa,WACL,QAAQ,OAAO,OAAO,SACtB,QAAQ,OAAO,OAAO,CAC9B;EACA,OAAO;CACT;CAEA,OAAO;EACL,SAAS;GACP,WAAY,UAAU,OAAO,QAAQ,IAAI,OAAO;GAChD,MAAM,YAAY,aAAa,QAAQ,IAAI,SAAS,OAAO,CAAC;GAC5D,aAAa,aAAa,QAAQ,MAAM,OAAO,CAAC;EAClD;EACA,SAAS;GACP,UAAU;EACZ;EACA,MAAM,QAAQ;GACZ,MAAM,QAAQ,IAAI,CAAC,GAAG,OAAO,CAAC;EAChC;CACF;AACF;;;;;;;AAQA,IAAa,0BAAb,MAAkE;CAChE,SAAkB,IAAIE,gBAAAA,oBAAgC;CAEtD,YAAY,SAAuD;EACjE,KAAK,MAAM,CAAC,SAAS,YAAY,OAAO,QAAQ,WAAW,CAAC,CAAC,GAC3D,KAAKD,OAAO,IAAI,WAAW,OAAO,GAAGE,gBAAAA,YAAY,OAAO,CAAC;CAE7D;CAEA,IAAI,SAAoC;EACtC,MAAM,UAAU,KAAKF,OAAO,IAAI,WAAW,OAAO,CAAC;EACnD,OAAO,UAAUE,gBAAAA,YAAY,OAAO,IAAI;CAC1C;CAEA,IAAI,SAAiB,SAA2B;EAC9C,KAAKF,OAAO,IAAI,WAAW,OAAO,GAAGE,gBAAAA,YAAY,OAAO,CAAC;CAC3D;CAEA,MAAM,SAAuB;EAC3B,KAAKF,OAAO,OAAO,WAAW,OAAO,CAAC;CACxC;CAEA,WAAqB;EACnB,OAAO,KAAKA,OACT,KAAK,kBAAkB,CAAC,CACxB,SAAS,QAAQ,gBAAgB,IAAI,MAAM,mBAAmB,MAAM,CAAC,KAAK,CAAC,CAAC;CACjF;AACF;AAEA,MAAM,qBAAqB;AAE3B,SAAS,WAAW,SAAyB;CAC3C,OAAO,GAAG,qBAAqB,mBAAmB,OAAO;AAC3D;;AAGA,SAAS,gBAAgB,SAAqC;CAC5D,IAAI;EACF,OAAO,mBAAmB,OAAO;CACnC,QAAQ;EACN,QAAQ,KACN,8CAA8C,KAAK,UAAU,OAAO,EAAE,iBACxE;EACA;CACF;AACF;;;;;;;AAcA,SAAgB,wBACd,OACA,UAA2C,CAAC,GACzB;CACnB,MAAM,SAAS,QAAQ,UAAU;CACjC,IAAI,OAAO,WAAW,UACpB,MAAM,IAAIG,eAAAA,eAAe,8CAA8C;CAEzE,MAAM,UAAUC,gBAAAA,oBAAoB,KAAK;CACzC,IAAI,CAACC,gBAAAA,0BAA0B,OAAO,GACpC,MAAM,IAAIF,eAAAA,eAAe,0DAA0D;CAErF,MAAM,OAAO,YAAoB,GAAG,SAAS,mBAAmB,OAAO;CACvE,OAAO;EACL,MAAM,IAAI,SAAS;GACjB,MAAM,UAAU,MAAM,QAAQ,IAAI,IAAI,OAAO,CAAC;GAC9C,OAAO,UAAUD,gBAAAA,YAAY,OAAO,IAAI;EAC1C;EACA,MAAM,SAAS,YAAY,QAAQ,IAAI,IAAI,OAAO,GAAGA,gBAAAA,YAAY,OAAO,CAAC;EACzE,QAAQ,YAAY,QAAQ,OAAO,IAAI,OAAO,CAAC;EAC/C,MAAM,WAAW;GAEf,QAAO,MADYI,gBAAAA,yBAAyB,SAAS,MAAM,EAAA,CAC/C,SAAS,UAAU,gBAAgB,MAAM,MAAM,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;EAClF;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"multi-storage-CCUgkqvI.cjs","names":["CookieJar","AUTH_FLAG_COOKIE","#store","MemoryKeyValueStore","copySession","ItdConfigError","createKeyValueStore","isEnumerableKeyValueStore","collectKeyValueStoreKeys"],"sources":["../../src/session/multi-storage.ts"],"sourcesContent":["import { AUTH_FLAG_COOKIE, CookieJar } from '../core/cookies.js';\nimport { ItdConfigError } from '../core/errors.js';\nimport {\n collectKeyValueStoreKeys,\n createKeyValueStore,\n type EnumerableKeyValueStore,\n isEnumerableKeyValueStore,\n MemoryKeyValueStore,\n} from '../core/key-value-store.js';\nimport { copySession, type ItdSession, type TokenStorage } from './storage.js';\n\n/**\n * Хранилище сессий нескольких аккаунтов.\n *\n * Отличается от {@link TokenStorage} тем, что каждый метод получает **имя аккаунта**:\n * так адаптер сам решает, как строить ключ, и Redis, БД или связка ключей получают то,\n * что им нужно. Имя приходит ровно тем, под которым аккаунт заведён в {@link ItdAccounts}:\n * библиотека его не нормализует и не экранирует — префиксы, экранирование и ограничения\n * на длину ключа остаются за адаптером.\n *\n * Все методы могут быть как синхронными, так и асинхронными.\n *\n * @example Хранилище поверх enumerable Redis backend\n * ```ts\n * const storage = createMultiTokenStorage(withNamespace(redisStore, 'itd'));\n * ```\n */\nexport interface MultiTokenStorage {\n /** Прочитать сессию аккаунта. `null`, если её нет. */\n get(account: string): ItdSession | null | Promise<ItdSession | null>;\n /** Сохранить сессию аккаунта целиком. */\n set(account: string, session: ItdSession): void | Promise<void>;\n /** Удалить сессию аккаунта. Вызывается при выходе и при неудачном обновлении токена. */\n clear(account: string): void | Promise<void>;\n /**\n * Имена сохранённых записей — по ним `ItdAccounts.restore()` находит кандидатов\n * после перезапуска процесса.\n *\n * Список ведёт сам адаптер: у файлового и памятного он виден из самой записи,\n * а хранилищу «ключ — значение» придётся держать множество имён рядом с сессиями.\n * Перед восстановлением контейнер читает каждую запись и пропускает оставшийся после\n * выхода одинокий `deviceId`: без токена или refresh-сессии авторизоваться невозможно.\n * Пустой список означает лишь то, что кандидатов нет, — сами записи при этом могут быть\n * доступны по имени.\n */\n accounts(): readonly string[] | Promise<readonly string[]>;\n}\n\n/**\n * Срез мультихранилища как обычное {@link TokenStorage} — в таком виде его получает\n * отдельный `ItdClient`, который про соседние аккаунты ничего не знает.\n */\nexport function scopedTokenStorage(storage: MultiTokenStorage, account: string): TokenStorage {\n return {\n get: () => storage.get(account),\n set: (session) => storage.set(account, session),\n clear: () => storage.clear(account),\n };\n}\n\n/**\n * Можно ли восстановить авторизованный аккаунт из записи.\n *\n * Один `deviceId` не считается сессией: он намеренно переживает выход, но не позволяет\n * выполнить ни одного авторизованного запроса.\n *\n * @internal\n */\nexport function isRestorableSession(session: ItdSession | null): session is ItdSession {\n if (!session) return false;\n if (session.accessToken || session.refreshToken) return true;\n\n const jar = new CookieJar();\n jar.deserialize(session.cookies);\n return jar.has(AUTH_FLAG_COOKIE);\n}\n\n/**\n * Управляемый срез хранилища для клиента аккаунта.\n *\n * После `revoke()` новые записи не доходят до общего хранилища, а `drain()` позволяет\n * дождаться уже начатых. Так удалённый клиент или завершившийся с опозданием refresh\n * не воскресят сессию после `removeAccount(..., { forget: true })`.\n *\n * @internal\n */\nexport interface ControlledTokenStorage {\n storage: TokenStorage;\n revoke(): void;\n drain(): Promise<void>;\n}\n\n/** @internal */\nexport function controlledTokenStorage(\n storage: MultiTokenStorage,\n account: string,\n): ControlledTokenStorage {\n let revoked = false;\n const pending = new Set<Promise<void>>();\n\n const mutate = (operation: () => void | Promise<void>): Promise<void> => {\n if (revoked) return Promise.resolve();\n\n const promise = Promise.resolve().then(operation);\n pending.add(promise);\n void promise.then(\n () => pending.delete(promise),\n () => pending.delete(promise),\n );\n return promise;\n };\n\n return {\n storage: {\n get: () => (revoked ? null : storage.get(account)),\n set: (session) => mutate(() => storage.set(account, session)),\n clear: () => mutate(() => storage.clear(account)),\n },\n revoke() {\n revoked = true;\n },\n async drain() {\n await Promise.all([...pending]);\n },\n };\n}\n\n/**\n * Мультихранилище в памяти процесса — вариант по умолчанию.\n *\n * Сессии теряются при перезапуске. Для долгоживущих ботов возьмите `FileMultiTokenStorage`\n * из `itd-api/node` либо соберите своё через {@link createMultiTokenStorage}.\n */\nexport class MemoryMultiTokenStorage implements MultiTokenStorage {\n readonly #store = new MemoryKeyValueStore<ItdSession>();\n\n constructor(initial?: Readonly<Record<string, ItdSession>> | null) {\n for (const [account, session] of Object.entries(initial ?? {})) {\n this.#store.set(accountKey(account), copySession(session));\n }\n }\n\n get(account: string): ItdSession | null {\n const session = this.#store.get(accountKey(account));\n return session ? copySession(session) : null;\n }\n\n set(account: string, session: ItdSession): void {\n this.#store.set(accountKey(account), copySession(session));\n }\n\n clear(account: string): void {\n this.#store.delete(accountKey(account));\n }\n\n accounts(): string[] {\n return this.#store\n .keys(ACCOUNT_KEY_PREFIX)\n .flatMap((key) => readAccountName(key.slice(ACCOUNT_KEY_PREFIX.length)) ?? []);\n }\n}\n\nconst ACCOUNT_KEY_PREFIX = 'accounts/';\n\nfunction accountKey(account: string): string {\n return `${ACCOUNT_KEY_PREFIX}${encodeURIComponent(account)}`;\n}\n\n/** Читает имя аккаунта из ключа. `undefined` — ключ не декодируется и запись пропускается. */\nfunction readAccountName(encoded: string): string | undefined {\n try {\n return decodeURIComponent(encoded);\n } catch {\n console.warn(\n `[itd-api] запись хранилища пропущена: ключ ${JSON.stringify(encoded)} не декодируется`,\n );\n return undefined;\n }\n}\n\n/** Настройки доменного адаптера нескольких сессий. */\nexport interface MultiTokenStorageAdapterOptions {\n /** Префикс ключей аккаунтов. По умолчанию `accounts/`. */\n prefix?: string | undefined;\n}\n\n/**\n * Создаёт доменное хранилище нескольких сессий поверх enumerable key-value backend.\n *\n * Отдельный индекс аккаунтов не используется: `accounts()` перечисляет ключи backend. Поэтому\n * запись сессии не может разойтись с индексом, но backend обязан эффективно поддерживать `keys`.\n */\nexport function createMultiTokenStorage(\n store: EnumerableKeyValueStore<ItdSession>,\n options: MultiTokenStorageAdapterOptions = {},\n): MultiTokenStorage {\n const prefix = options.prefix ?? ACCOUNT_KEY_PREFIX;\n if (typeof prefix !== 'string') {\n throw new ItdConfigError('prefix MultiTokenStorage должен быть строкой');\n }\n const backend = createKeyValueStore(store);\n if (!isEnumerableKeyValueStore(backend)) {\n throw new ItdConfigError('MultiTokenStorage требует KeyValueStore с методом keys()');\n }\n const key = (account: string) => `${prefix}${encodeURIComponent(account)}`;\n return {\n async get(account) {\n const session = await backend.get(key(account));\n return session ? copySession(session) : null;\n },\n set: (account, session) => backend.set(key(account), copySession(session)),\n clear: (account) => backend.delete(key(account)),\n async accounts() {\n const keys = await collectKeyValueStoreKeys(backend, prefix);\n return keys.flatMap((value) => readAccountName(value.slice(prefix.length)) ?? []);\n },\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { a as ItdConfigError, l as ItdFileError, u as ItdFileErrorReason } from "./errors-
|
|
1
|
+
import { a as ItdConfigError, l as ItdFileError, u as ItdFileErrorReason } from "./errors-GI10kZxk.js";
|
|
2
|
+
import { a as redactUrl, r as redactErrorCause } from "./redact-FNK29Fv-.js";
|
|
2
3
|
//#region src/core/attachments/contracts.ts
|
|
3
4
|
/** Способ передачи содержимого вложения. */
|
|
4
5
|
const FileTransferMode = Object.freeze({
|
|
@@ -19,10 +20,11 @@ function optionalBytes(value, name) {
|
|
|
19
20
|
}
|
|
20
21
|
/** Создаёт типизированную ошибку превышения размера вложения. */
|
|
21
22
|
function fileTooLarge(url, limit, actual) {
|
|
22
|
-
const
|
|
23
|
+
const safeUrl = url ? redactUrl(url) : void 0;
|
|
24
|
+
const source = safeUrl ? ` по адресу ${safeUrl}` : "";
|
|
23
25
|
return new ItdFileError(`файл${source} больше предела в ${limit} байт: ${actual}`, {
|
|
24
26
|
reason: ItdFileErrorReason.TooLarge,
|
|
25
|
-
...
|
|
27
|
+
...safeUrl ? { url: safeUrl } : {},
|
|
26
28
|
limit,
|
|
27
29
|
actual
|
|
28
30
|
});
|
|
@@ -98,9 +100,9 @@ function boundedFileStream(source, options = {}) {
|
|
|
98
100
|
}
|
|
99
101
|
controller.error(new ItdFileError("не удалось прочитать поток вложения", {
|
|
100
102
|
reason: ItdFileErrorReason.Read,
|
|
101
|
-
...options.url ? { url: options.url } : {},
|
|
103
|
+
...options.url ? { url: redactUrl(options.url) } : {},
|
|
102
104
|
retryable: options.retryableRead ?? false,
|
|
103
|
-
cause: error
|
|
105
|
+
cause: redactErrorCause(error)
|
|
104
106
|
}));
|
|
105
107
|
}
|
|
106
108
|
},
|
|
@@ -139,4 +141,4 @@ function resolveFileStreamOptions(options, defaultMaxBytes) {
|
|
|
139
141
|
//#endregion
|
|
140
142
|
export { fileTooLarge as a, FileTransferMode as c, isReadableByteStream as i, boundedFileStream as n, optionalBytes as o, isBoundedFileStream as r, DEFAULT_URL_FILE_MAX_BYTES as s, resolveFileStreamOptions as t };
|
|
141
143
|
|
|
142
|
-
//# sourceMappingURL=options-
|
|
144
|
+
//# sourceMappingURL=options-BNP633A5.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options-BNP633A5.js","names":[],"sources":["../../src/core/attachments/contracts.ts","../../src/core/attachments/limits.ts","../../src/core/attachments/bounded-stream.ts","../../src/core/attachments/options.ts"],"sourcesContent":["/** Способ передачи содержимого вложения. */\nexport const FileTransferMode = Object.freeze({\n /** Сначала получить файл целиком, затем отправить его как `Blob`. */\n Buffer: 'buffer',\n /** Передавать данные по мере чтения, не собирая файл целиком в памяти. */\n Stream: 'stream',\n} as const);\nexport type FileTransferMode = (typeof FileTransferMode)[keyof typeof FileTransferMode];\n\n/** Размер очереди потокового вложения по умолчанию — 4 МиБ. */\nexport const DEFAULT_FILE_STREAM_BUFFER_BYTES = 4 * 1024 * 1024;\n\n/** Предел размера файла, скачиваемого по адресу, — 100 МиБ. */\nexport const DEFAULT_URL_FILE_MAX_BYTES = 100 * 1024 * 1024;\n\n/** Что доступно источнику при открытии файла. */\nexport interface FileContext {\n /** Общая отмена текущей попытки, включая её таймаут. */\n signal?: AbortSignal | undefined;\n /** `fetch` клиента — с переданными пользователем настройками. */\n fetch: typeof fetch;\n /** Номер попытки отправки, начиная с 1. */\n attempt: number;\n}\n\n/** Готовое содержимое файла. */\nexport interface FileContent {\n file: Blob | ArrayBuffer | Uint8Array;\n /** Имя влияет на определение MIME, если `contentType` не задан. */\n filename?: string | undefined;\n /** MIME без параметров. Если не указан, определяется по имени или `Blob`. */\n contentType?: string | undefined;\n}\n\n/** Открытый поток файла. */\nexport interface FileStreamContent {\n stream: ReadableStream<Uint8Array>;\n filename?: string | undefined;\n contentType?: string | undefined;\n /** Размер, если он известен до чтения. */\n size?: number | undefined;\n /** Освобождает внешний ресурс после завершения попытки. */\n close?: (() => void | Promise<void>) | undefined;\n}\n\n/** Буферный источник, который вычисляется непосредственно перед первой попыткой. */\nexport interface LazyFile {\n load: (context: FileContext) => FileContent | Promise<FileContent>;\n}\n\n/**\n * Повторяемый потоковый источник.\n *\n * `open` вызывается для каждой попытки отправки. Он обязан возвращать новый поток:\n * уже прочитанный `ReadableStream` повторно использовать нельзя.\n */\nexport interface StreamFile {\n open: (context: FileContext) => FileStreamContent | Promise<FileStreamContent>;\n}\n\n/** Общие настройки потоковой передачи. */\nexport interface FileStreamOptions {\n /** Буферный или потоковый режим. По умолчанию `'buffer'`. */\n mode?: FileTransferMode | undefined;\n /** Максимальный размер файла. Без значения размер не ограничивается. */\n maxBytes?: number | undefined;\n /**\n * Верхняя граница очереди, которой управляет библиотека. По умолчанию 4 МиБ.\n *\n * Сетевой рантайм может иметь собственные внутренние буферы сверх этого значения.\n */\n streamBufferBytes?: number | undefined;\n}\n\n/** Файл, который нужно получить по HTTP(S). */\nexport interface UrlFile extends UrlFileOptions {\n url: string;\n}\n\n/** Настройки файла по HTTP(S). */\nexport interface UrlFileOptions extends FileStreamOptions {\n filename?: string | undefined;\n /** По умолчанию берётся из `Content-Type` ответа. */\n contentType?: string | undefined;\n}\n\n/** Файл для загрузки. Голый поток не принимается, потому что его нельзя повторить. */\nexport type FileInput =\n | Blob\n | ArrayBuffer\n | Uint8Array\n | FileContent\n | UrlFile\n | LazyFile\n | StreamFile;\n\n/** Настройки фабрики пользовательского потока. */\nexport interface FromStreamOptions extends Omit<FileStreamOptions, 'mode'> {\n filename?: string | undefined;\n contentType?: string | undefined;\n /** Размер, если он известен заранее. */\n size?: number | undefined;\n}\n","import { ItdConfigError, ItdFileError, ItdFileErrorReason } from '../errors.js';\nimport { redactUrl } from '../redact.js';\n\n/** Проверяет числовую границу до обращения к источнику. */\nexport function optionalBytes(value: number | undefined, name: string): number | undefined {\n if (value === undefined) return undefined;\n if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value)) {\n throw new ItdConfigError(\n `${name} должен быть неотрицательным целым числом, получено: ${value}`,\n );\n }\n return value;\n}\n\n/** Создаёт типизированную ошибку превышения размера вложения. */\nexport function fileTooLarge(url: string | undefined, limit: number, actual: number): ItdFileError {\n const safeUrl = url ? redactUrl(url) : undefined;\n const source = safeUrl ? ` по адресу ${safeUrl}` : '';\n return new ItdFileError(`файл${source} больше предела в ${limit} байт: ${actual}`, {\n reason: ItdFileErrorReason.TooLarge,\n ...(safeUrl ? { url: safeUrl } : {}),\n limit,\n actual,\n });\n}\n","import { ItdConfigError, ItdFileError, ItdFileErrorReason } from '../errors.js';\nimport { redactErrorCause, redactUrl } from '../redact.js';\nimport { DEFAULT_FILE_STREAM_BUFFER_BYTES } from './contracts.js';\nimport { fileTooLarge, optionalBytes } from './limits.js';\n\n/** Потоки, на которые уже наложены счётчик размера и backpressure. */\nconst BOUNDED_FILE_STREAMS = new WeakSet<ReadableStream<Uint8Array>>();\n\n/** Настройки ограничивающей поток обёртки. */\nexport interface BoundedFileStreamOptions {\n maxBytes?: number | undefined;\n streamBufferBytes?: number | undefined;\n signal?: AbortSignal | undefined;\n url?: string | undefined;\n retryableRead?: boolean | undefined;\n}\n\n/** Приводит произвольный чанк потока к байтам. */\nfunction asBytes(value: Uint8Array): Uint8Array {\n return value instanceof Uint8Array ? value : new Uint8Array(value);\n}\n\n/** Проверяет значение без обращения к методам потенциально чужого объекта. */\nexport function isReadableByteStream(value: unknown): value is ReadableStream<Uint8Array> {\n return typeof ReadableStream !== 'undefined' && value instanceof ReadableStream;\n}\n\n/**\n * Ограничивает размер и очередь потока. Отмена результирующего потока отменяет исходный.\n *\n * @internal\n */\nexport function boundedFileStream(\n source: ReadableStream<Uint8Array>,\n options: BoundedFileStreamOptions = {},\n): ReadableStream<Uint8Array> {\n const maxBytes = optionalBytes(options.maxBytes, 'maxBytes');\n const streamBufferBytes =\n optionalBytes(\n options.streamBufferBytes ?? DEFAULT_FILE_STREAM_BUFFER_BYTES,\n 'streamBufferBytes',\n ) ?? DEFAULT_FILE_STREAM_BUFFER_BYTES;\n if (streamBufferBytes === 0) {\n throw new ItdConfigError('streamBufferBytes должен быть больше нуля');\n }\n\n const reader = source.getReader();\n let total = 0;\n let finished = false;\n let abortListener: (() => void) | undefined;\n\n const cleanup = () => {\n if (abortListener) options.signal?.removeEventListener('abort', abortListener);\n abortListener = undefined;\n };\n\n const stream = new ReadableStream<Uint8Array>(\n {\n start(controller) {\n if (!options.signal) return;\n abortListener = () => {\n finished = true;\n void reader.cancel(options.signal?.reason).catch(() => {});\n controller.error(options.signal?.reason ?? new DOMException('Aborted', 'AbortError'));\n cleanup();\n };\n if (options.signal.aborted) abortListener();\n else options.signal.addEventListener('abort', abortListener, { once: true });\n },\n async pull(controller) {\n if (finished) return;\n\n try {\n const next = await reader.read();\n if (finished) return;\n if (next.done) {\n finished = true;\n cleanup();\n controller.close();\n return;\n }\n\n const chunk = asBytes(next.value);\n total += chunk.byteLength;\n if (maxBytes !== undefined && total > maxBytes) {\n finished = true;\n await reader.cancel().catch(() => {});\n cleanup();\n controller.error(fileTooLarge(options.url, maxBytes, total));\n return;\n }\n\n controller.enqueue(chunk);\n } catch (error) {\n finished = true;\n cleanup();\n if (error instanceof ItdFileError) {\n controller.error(error);\n return;\n }\n controller.error(\n new ItdFileError('не удалось прочитать поток вложения', {\n reason: ItdFileErrorReason.Read,\n ...(options.url ? { url: redactUrl(options.url) } : {}),\n retryable: options.retryableRead ?? false,\n cause: redactErrorCause(error),\n }),\n );\n }\n },\n async cancel(reason) {\n finished = true;\n cleanup();\n await reader.cancel(reason).catch(() => {});\n },\n },\n {\n highWaterMark: streamBufferBytes,\n size: (chunk) => chunk.byteLength,\n },\n );\n BOUNDED_FILE_STREAMS.add(stream);\n return stream;\n}\n\n/** Наложены ли на поток ограничения библиотеки. @internal */\nexport function isBoundedFileStream(stream: ReadableStream<Uint8Array>): boolean {\n return BOUNDED_FILE_STREAMS.has(stream);\n}\n","import { ItdConfigError } from '../errors.js';\nimport {\n DEFAULT_FILE_STREAM_BUFFER_BYTES,\n type FileStreamOptions,\n FileTransferMode,\n} from './contracts.js';\nimport { optionalBytes } from './limits.js';\n\n/** Проверяет и дополняет настройки потока. @internal */\nexport function resolveFileStreamOptions(\n options: FileStreamOptions,\n defaultMaxBytes?: number,\n): {\n mode: FileTransferMode;\n maxBytes: number | undefined;\n streamBufferBytes: number;\n} {\n const mode = options.mode ?? FileTransferMode.Buffer;\n if (mode !== FileTransferMode.Buffer && mode !== FileTransferMode.Stream) {\n throw new ItdConfigError(`mode вложения должен быть 'buffer' или 'stream', получено: ${mode}`);\n }\n if (mode === FileTransferMode.Stream && typeof ReadableStream === 'undefined') {\n throw new ItdConfigError(\n \"эта среда не поддерживает ReadableStream; используйте mode: 'buffer'\",\n );\n }\n\n const maxBytes = optionalBytes(options.maxBytes ?? defaultMaxBytes, 'maxBytes');\n const streamBufferBytes =\n optionalBytes(\n options.streamBufferBytes ?? DEFAULT_FILE_STREAM_BUFFER_BYTES,\n 'streamBufferBytes',\n ) ?? DEFAULT_FILE_STREAM_BUFFER_BYTES;\n if (streamBufferBytes === 0) {\n throw new ItdConfigError('streamBufferBytes должен быть больше нуля');\n }\n\n return { mode, maxBytes, streamBufferBytes };\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
const require_errors = require("./errors-
|
|
1
|
+
const require_errors = require("./errors-BmP3TKoW.cjs");
|
|
2
|
+
const require_redact = require("./redact-DB9EMGJx.cjs");
|
|
2
3
|
//#region src/core/attachments/contracts.ts
|
|
3
4
|
/** Способ передачи содержимого вложения. */
|
|
4
5
|
const FileTransferMode = Object.freeze({
|
|
@@ -19,10 +20,11 @@ function optionalBytes(value, name) {
|
|
|
19
20
|
}
|
|
20
21
|
/** Создаёт типизированную ошибку превышения размера вложения. */
|
|
21
22
|
function fileTooLarge(url, limit, actual) {
|
|
22
|
-
const
|
|
23
|
+
const safeUrl = url ? require_redact.redactUrl(url) : void 0;
|
|
24
|
+
const source = safeUrl ? ` по адресу ${safeUrl}` : "";
|
|
23
25
|
return new require_errors.ItdFileError(`файл${source} больше предела в ${limit} байт: ${actual}`, {
|
|
24
26
|
reason: require_errors.ItdFileErrorReason.TooLarge,
|
|
25
|
-
...
|
|
27
|
+
...safeUrl ? { url: safeUrl } : {},
|
|
26
28
|
limit,
|
|
27
29
|
actual
|
|
28
30
|
});
|
|
@@ -98,9 +100,9 @@ function boundedFileStream(source, options = {}) {
|
|
|
98
100
|
}
|
|
99
101
|
controller.error(new require_errors.ItdFileError("не удалось прочитать поток вложения", {
|
|
100
102
|
reason: require_errors.ItdFileErrorReason.Read,
|
|
101
|
-
...options.url ? { url: options.url } : {},
|
|
103
|
+
...options.url ? { url: require_redact.redactUrl(options.url) } : {},
|
|
102
104
|
retryable: options.retryableRead ?? false,
|
|
103
|
-
cause: error
|
|
105
|
+
cause: require_redact.redactErrorCause(error)
|
|
104
106
|
}));
|
|
105
107
|
}
|
|
106
108
|
},
|
|
@@ -186,4 +188,4 @@ Object.defineProperty(exports, "resolveFileStreamOptions", {
|
|
|
186
188
|
}
|
|
187
189
|
});
|
|
188
190
|
|
|
189
|
-
//# sourceMappingURL=options-
|
|
191
|
+
//# sourceMappingURL=options-BUfoHE9G.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"options-BUfoHE9G.cjs","names":["ItdConfigError","redactUrl","ItdFileError","ItdFileErrorReason","ItdConfigError","ItdFileError","ItdFileErrorReason","redactUrl","redactErrorCause","ItdConfigError"],"sources":["../../src/core/attachments/contracts.ts","../../src/core/attachments/limits.ts","../../src/core/attachments/bounded-stream.ts","../../src/core/attachments/options.ts"],"sourcesContent":["/** Способ передачи содержимого вложения. */\nexport const FileTransferMode = Object.freeze({\n /** Сначала получить файл целиком, затем отправить его как `Blob`. */\n Buffer: 'buffer',\n /** Передавать данные по мере чтения, не собирая файл целиком в памяти. */\n Stream: 'stream',\n} as const);\nexport type FileTransferMode = (typeof FileTransferMode)[keyof typeof FileTransferMode];\n\n/** Размер очереди потокового вложения по умолчанию — 4 МиБ. */\nexport const DEFAULT_FILE_STREAM_BUFFER_BYTES = 4 * 1024 * 1024;\n\n/** Предел размера файла, скачиваемого по адресу, — 100 МиБ. */\nexport const DEFAULT_URL_FILE_MAX_BYTES = 100 * 1024 * 1024;\n\n/** Что доступно источнику при открытии файла. */\nexport interface FileContext {\n /** Общая отмена текущей попытки, включая её таймаут. */\n signal?: AbortSignal | undefined;\n /** `fetch` клиента — с переданными пользователем настройками. */\n fetch: typeof fetch;\n /** Номер попытки отправки, начиная с 1. */\n attempt: number;\n}\n\n/** Готовое содержимое файла. */\nexport interface FileContent {\n file: Blob | ArrayBuffer | Uint8Array;\n /** Имя влияет на определение MIME, если `contentType` не задан. */\n filename?: string | undefined;\n /** MIME без параметров. Если не указан, определяется по имени или `Blob`. */\n contentType?: string | undefined;\n}\n\n/** Открытый поток файла. */\nexport interface FileStreamContent {\n stream: ReadableStream<Uint8Array>;\n filename?: string | undefined;\n contentType?: string | undefined;\n /** Размер, если он известен до чтения. */\n size?: number | undefined;\n /** Освобождает внешний ресурс после завершения попытки. */\n close?: (() => void | Promise<void>) | undefined;\n}\n\n/** Буферный источник, который вычисляется непосредственно перед первой попыткой. */\nexport interface LazyFile {\n load: (context: FileContext) => FileContent | Promise<FileContent>;\n}\n\n/**\n * Повторяемый потоковый источник.\n *\n * `open` вызывается для каждой попытки отправки. Он обязан возвращать новый поток:\n * уже прочитанный `ReadableStream` повторно использовать нельзя.\n */\nexport interface StreamFile {\n open: (context: FileContext) => FileStreamContent | Promise<FileStreamContent>;\n}\n\n/** Общие настройки потоковой передачи. */\nexport interface FileStreamOptions {\n /** Буферный или потоковый режим. По умолчанию `'buffer'`. */\n mode?: FileTransferMode | undefined;\n /** Максимальный размер файла. Без значения размер не ограничивается. */\n maxBytes?: number | undefined;\n /**\n * Верхняя граница очереди, которой управляет библиотека. По умолчанию 4 МиБ.\n *\n * Сетевой рантайм может иметь собственные внутренние буферы сверх этого значения.\n */\n streamBufferBytes?: number | undefined;\n}\n\n/** Файл, который нужно получить по HTTP(S). */\nexport interface UrlFile extends UrlFileOptions {\n url: string;\n}\n\n/** Настройки файла по HTTP(S). */\nexport interface UrlFileOptions extends FileStreamOptions {\n filename?: string | undefined;\n /** По умолчанию берётся из `Content-Type` ответа. */\n contentType?: string | undefined;\n}\n\n/** Файл для загрузки. Голый поток не принимается, потому что его нельзя повторить. */\nexport type FileInput =\n | Blob\n | ArrayBuffer\n | Uint8Array\n | FileContent\n | UrlFile\n | LazyFile\n | StreamFile;\n\n/** Настройки фабрики пользовательского потока. */\nexport interface FromStreamOptions extends Omit<FileStreamOptions, 'mode'> {\n filename?: string | undefined;\n contentType?: string | undefined;\n /** Размер, если он известен заранее. */\n size?: number | undefined;\n}\n","import { ItdConfigError, ItdFileError, ItdFileErrorReason } from '../errors.js';\nimport { redactUrl } from '../redact.js';\n\n/** Проверяет числовую границу до обращения к источнику. */\nexport function optionalBytes(value: number | undefined, name: string): number | undefined {\n if (value === undefined) return undefined;\n if (!Number.isFinite(value) || value < 0 || !Number.isInteger(value)) {\n throw new ItdConfigError(\n `${name} должен быть неотрицательным целым числом, получено: ${value}`,\n );\n }\n return value;\n}\n\n/** Создаёт типизированную ошибку превышения размера вложения. */\nexport function fileTooLarge(url: string | undefined, limit: number, actual: number): ItdFileError {\n const safeUrl = url ? redactUrl(url) : undefined;\n const source = safeUrl ? ` по адресу ${safeUrl}` : '';\n return new ItdFileError(`файл${source} больше предела в ${limit} байт: ${actual}`, {\n reason: ItdFileErrorReason.TooLarge,\n ...(safeUrl ? { url: safeUrl } : {}),\n limit,\n actual,\n });\n}\n","import { ItdConfigError, ItdFileError, ItdFileErrorReason } from '../errors.js';\nimport { redactErrorCause, redactUrl } from '../redact.js';\nimport { DEFAULT_FILE_STREAM_BUFFER_BYTES } from './contracts.js';\nimport { fileTooLarge, optionalBytes } from './limits.js';\n\n/** Потоки, на которые уже наложены счётчик размера и backpressure. */\nconst BOUNDED_FILE_STREAMS = new WeakSet<ReadableStream<Uint8Array>>();\n\n/** Настройки ограничивающей поток обёртки. */\nexport interface BoundedFileStreamOptions {\n maxBytes?: number | undefined;\n streamBufferBytes?: number | undefined;\n signal?: AbortSignal | undefined;\n url?: string | undefined;\n retryableRead?: boolean | undefined;\n}\n\n/** Приводит произвольный чанк потока к байтам. */\nfunction asBytes(value: Uint8Array): Uint8Array {\n return value instanceof Uint8Array ? value : new Uint8Array(value);\n}\n\n/** Проверяет значение без обращения к методам потенциально чужого объекта. */\nexport function isReadableByteStream(value: unknown): value is ReadableStream<Uint8Array> {\n return typeof ReadableStream !== 'undefined' && value instanceof ReadableStream;\n}\n\n/**\n * Ограничивает размер и очередь потока. Отмена результирующего потока отменяет исходный.\n *\n * @internal\n */\nexport function boundedFileStream(\n source: ReadableStream<Uint8Array>,\n options: BoundedFileStreamOptions = {},\n): ReadableStream<Uint8Array> {\n const maxBytes = optionalBytes(options.maxBytes, 'maxBytes');\n const streamBufferBytes =\n optionalBytes(\n options.streamBufferBytes ?? DEFAULT_FILE_STREAM_BUFFER_BYTES,\n 'streamBufferBytes',\n ) ?? DEFAULT_FILE_STREAM_BUFFER_BYTES;\n if (streamBufferBytes === 0) {\n throw new ItdConfigError('streamBufferBytes должен быть больше нуля');\n }\n\n const reader = source.getReader();\n let total = 0;\n let finished = false;\n let abortListener: (() => void) | undefined;\n\n const cleanup = () => {\n if (abortListener) options.signal?.removeEventListener('abort', abortListener);\n abortListener = undefined;\n };\n\n const stream = new ReadableStream<Uint8Array>(\n {\n start(controller) {\n if (!options.signal) return;\n abortListener = () => {\n finished = true;\n void reader.cancel(options.signal?.reason).catch(() => {});\n controller.error(options.signal?.reason ?? new DOMException('Aborted', 'AbortError'));\n cleanup();\n };\n if (options.signal.aborted) abortListener();\n else options.signal.addEventListener('abort', abortListener, { once: true });\n },\n async pull(controller) {\n if (finished) return;\n\n try {\n const next = await reader.read();\n if (finished) return;\n if (next.done) {\n finished = true;\n cleanup();\n controller.close();\n return;\n }\n\n const chunk = asBytes(next.value);\n total += chunk.byteLength;\n if (maxBytes !== undefined && total > maxBytes) {\n finished = true;\n await reader.cancel().catch(() => {});\n cleanup();\n controller.error(fileTooLarge(options.url, maxBytes, total));\n return;\n }\n\n controller.enqueue(chunk);\n } catch (error) {\n finished = true;\n cleanup();\n if (error instanceof ItdFileError) {\n controller.error(error);\n return;\n }\n controller.error(\n new ItdFileError('не удалось прочитать поток вложения', {\n reason: ItdFileErrorReason.Read,\n ...(options.url ? { url: redactUrl(options.url) } : {}),\n retryable: options.retryableRead ?? false,\n cause: redactErrorCause(error),\n }),\n );\n }\n },\n async cancel(reason) {\n finished = true;\n cleanup();\n await reader.cancel(reason).catch(() => {});\n },\n },\n {\n highWaterMark: streamBufferBytes,\n size: (chunk) => chunk.byteLength,\n },\n );\n BOUNDED_FILE_STREAMS.add(stream);\n return stream;\n}\n\n/** Наложены ли на поток ограничения библиотеки. @internal */\nexport function isBoundedFileStream(stream: ReadableStream<Uint8Array>): boolean {\n return BOUNDED_FILE_STREAMS.has(stream);\n}\n","import { ItdConfigError } from '../errors.js';\nimport {\n DEFAULT_FILE_STREAM_BUFFER_BYTES,\n type FileStreamOptions,\n FileTransferMode,\n} from './contracts.js';\nimport { optionalBytes } from './limits.js';\n\n/** Проверяет и дополняет настройки потока. @internal */\nexport function resolveFileStreamOptions(\n options: FileStreamOptions,\n defaultMaxBytes?: number,\n): {\n mode: FileTransferMode;\n maxBytes: number | undefined;\n streamBufferBytes: number;\n} {\n const mode = options.mode ?? FileTransferMode.Buffer;\n if (mode !== FileTransferMode.Buffer && mode !== FileTransferMode.Stream) {\n throw new ItdConfigError(`mode вложения должен быть 'buffer' или 'stream', получено: ${mode}`);\n }\n if (mode === FileTransferMode.Stream && typeof ReadableStream === 'undefined') {\n throw new ItdConfigError(\n \"эта среда не поддерживает ReadableStream; используйте mode: 'buffer'\",\n );\n }\n\n const maxBytes = optionalBytes(options.maxBytes ?? defaultMaxBytes, 'maxBytes');\n const streamBufferBytes =\n optionalBytes(\n options.streamBufferBytes ?? DEFAULT_FILE_STREAM_BUFFER_BYTES,\n 'streamBufferBytes',\n ) ?? DEFAULT_FILE_STREAM_BUFFER_BYTES;\n if (streamBufferBytes === 0) {\n throw new ItdConfigError('streamBufferBytes должен быть больше нуля');\n }\n\n return { mode, maxBytes, streamBufferBytes };\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,118 @@
|
|
|
1
|
-
const require_errors = require("./errors-
|
|
1
|
+
const require_errors = require("./errors-BmP3TKoW.cjs");
|
|
2
|
+
//#region src/core/runtime.ts
|
|
3
|
+
/**
|
|
4
|
+
* Как библиотека обращается с cookie.
|
|
5
|
+
*
|
|
6
|
+
* - `browser` — cookie ведёт браузер, запросы уходят с `credentials: 'include'`;
|
|
7
|
+
* - `server` — cookie ведёт встроенный jar, заголовок `Cookie` подставляется вручную;
|
|
8
|
+
* - `auto` — определяется по среде исполнения (значение по умолчанию).
|
|
9
|
+
*/
|
|
10
|
+
const RuntimeMode = Object.freeze({
|
|
11
|
+
/** Определяется по среде исполнения. Значение по умолчанию. */
|
|
12
|
+
Auto: "auto",
|
|
13
|
+
/** Cookie ведёт браузер, запросы уходят с `credentials: 'include'`. */
|
|
14
|
+
Browser: "browser",
|
|
15
|
+
/** Cookie ведёт встроенный jar, заголовок `Cookie` подставляется вручную. */
|
|
16
|
+
Server: "server"
|
|
17
|
+
});
|
|
18
|
+
/** Распознанная среда исполнения. */
|
|
19
|
+
const DetectedRuntime = Object.freeze({
|
|
20
|
+
Browser: "browser",
|
|
21
|
+
/** Есть `window`, но нет `document`; cookie ведёт нативный сетевой слой. */
|
|
22
|
+
ReactNative: "react-native",
|
|
23
|
+
Server: "server"
|
|
24
|
+
});
|
|
25
|
+
/**
|
|
26
|
+
* Определяет среду исполнения.
|
|
27
|
+
*
|
|
28
|
+
* React Native выделен отдельно намеренно: там есть `window`, но нет `document`, а cookie
|
|
29
|
+
* хранит нативный сетевой слой (`NSHTTPCookieStorage` / `OkHttp`). Свой jar в этой среде
|
|
30
|
+
* привёл бы к отправке cookie дважды.
|
|
31
|
+
*/
|
|
32
|
+
function detectRuntime() {
|
|
33
|
+
if (globalThis.navigator?.product === "ReactNative") return DetectedRuntime.ReactNative;
|
|
34
|
+
if (typeof document !== "undefined") return DetectedRuntime.Browser;
|
|
35
|
+
return DetectedRuntime.Server;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Нужно ли вести собственный cookie-jar.
|
|
39
|
+
*
|
|
40
|
+
* Только в серверных средах: в браузере `Set-Cookie` из JS не читается и не нужен,
|
|
41
|
+
* в React Native cookie ведёт нативный слой.
|
|
42
|
+
*/
|
|
43
|
+
function shouldUseCookieJar(mode) {
|
|
44
|
+
if (mode === RuntimeMode.Browser) return false;
|
|
45
|
+
if (mode === RuntimeMode.Server) return true;
|
|
46
|
+
return detectRuntime() === DetectedRuntime.Server;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Нужно ли отправлять запросы с `credentials: 'include'`.
|
|
50
|
+
*
|
|
51
|
+
* Только в браузере: именно так refresh-cookie попадает в запрос `POST /api/v1/auth/refresh`.
|
|
52
|
+
* Требует настроенного CORS на стороне итд.com — если его нет, укажите свой прокси в `baseUrl`.
|
|
53
|
+
*/
|
|
54
|
+
function shouldSendCredentials(mode) {
|
|
55
|
+
if (mode === RuntimeMode.Browser) return true;
|
|
56
|
+
if (mode === RuntimeMode.Server) return false;
|
|
57
|
+
return detectRuntime() === DetectedRuntime.Browser;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Возвращает реализацию `fetch`.
|
|
61
|
+
*
|
|
62
|
+
* @param custom реализация из конфигурации клиента, если пользователь её передал
|
|
63
|
+
* @throws {ItdConfigError} если `fetch` недоступен — например, на Node ниже 18
|
|
64
|
+
*/
|
|
65
|
+
function resolveFetch(custom) {
|
|
66
|
+
if (custom !== void 0) {
|
|
67
|
+
if (typeof custom !== "function") throw new require_errors.ItdConfigError("fetch должен быть функцией");
|
|
68
|
+
return custom;
|
|
69
|
+
}
|
|
70
|
+
if (typeof globalThis.fetch === "function") return globalThis.fetch.bind(globalThis);
|
|
71
|
+
throw new require_errors.ItdConfigError("В этой среде нет глобального fetch. Обновитесь до Node 18+ либо передайте свою реализацию через опцию fetch.");
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Проверки бинарных типов, безопасные в любой среде.
|
|
75
|
+
*
|
|
76
|
+
* Обычный `instanceof` здесь недостаточен: конструктора может не быть вовсе, и тогда
|
|
77
|
+
* проверка падает с `ReferenceError`, а не возвращает `false`. `Blob` есть в Node с 18.0,
|
|
78
|
+
* но `File` стал глобальным только в Node 20 — при заявленной поддержке Node 18 голый
|
|
79
|
+
* `instanceof File` ронял бы любую загрузку.
|
|
80
|
+
*/
|
|
81
|
+
function isBlob(value) {
|
|
82
|
+
return typeof Blob !== "undefined" && value instanceof Blob;
|
|
83
|
+
}
|
|
84
|
+
/** @see {@link isBlob} */
|
|
85
|
+
function isFile(value) {
|
|
86
|
+
return typeof File !== "undefined" && value instanceof File;
|
|
87
|
+
}
|
|
88
|
+
/** Доступно ли потоковое чтение тела ответа — от этого зависит, сработает ли SSE. */
|
|
89
|
+
function supportsStreamingBody() {
|
|
90
|
+
return typeof ReadableStream !== "undefined" && typeof TextDecoder !== "undefined";
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Создаёт идентификатор устройства для заголовка `X-Device-Id` — UUID v4.
|
|
94
|
+
*
|
|
95
|
+
* `crypto.randomUUID` есть в Node 19+, браузерах и Deno, но отсутствует в Node 18 вне
|
|
96
|
+
* защищённого контекста, поэтому предусмотрен запасной путь.
|
|
97
|
+
*/
|
|
98
|
+
function createDeviceId() {
|
|
99
|
+
const webCrypto = globalThis.crypto;
|
|
100
|
+
if (typeof webCrypto?.randomUUID === "function") return webCrypto.randomUUID();
|
|
101
|
+
const bytes = /* @__PURE__ */ new Uint8Array(16);
|
|
102
|
+
if (typeof webCrypto?.getRandomValues === "function") webCrypto.getRandomValues(bytes);
|
|
103
|
+
else for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
104
|
+
bytes[6] = bytes[6] & 15 | 64;
|
|
105
|
+
bytes[8] = bytes[8] & 63 | 128;
|
|
106
|
+
const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
107
|
+
return [
|
|
108
|
+
hex.slice(0, 8),
|
|
109
|
+
hex.slice(8, 12),
|
|
110
|
+
hex.slice(12, 16),
|
|
111
|
+
hex.slice(16, 20),
|
|
112
|
+
hex.slice(20, 32)
|
|
113
|
+
].join("-");
|
|
114
|
+
}
|
|
115
|
+
//#endregion
|
|
2
116
|
//#region src/core/url.ts
|
|
3
117
|
/**
|
|
4
118
|
* Собирает строку запроса.
|
|
@@ -421,6 +535,113 @@ var CookieJar = class {
|
|
|
421
535
|
}
|
|
422
536
|
};
|
|
423
537
|
//#endregion
|
|
538
|
+
//#region src/core/redact.ts
|
|
539
|
+
/** Заголовки, значение которых нельзя писать в лог целиком. */
|
|
540
|
+
const SECRET_HEADERS = /* @__PURE__ */ new Set([
|
|
541
|
+
"authorization",
|
|
542
|
+
"cookie",
|
|
543
|
+
"set-cookie",
|
|
544
|
+
"x-api-key"
|
|
545
|
+
]);
|
|
546
|
+
/** Поля тела запроса, значение которых нельзя писать в лог. */
|
|
547
|
+
const SECRET_FIELDS = /* @__PURE__ */ new Set([
|
|
548
|
+
"password",
|
|
549
|
+
"oldpassword",
|
|
550
|
+
"newpassword",
|
|
551
|
+
"accesstoken",
|
|
552
|
+
"refreshtoken",
|
|
553
|
+
"currentpassword",
|
|
554
|
+
"flowtoken",
|
|
555
|
+
"token",
|
|
556
|
+
"turnstiletoken",
|
|
557
|
+
"otp"
|
|
558
|
+
]);
|
|
559
|
+
/** Query-параметры, содержащие секреты. */
|
|
560
|
+
const SECRET_QUERY_PARAMS = /* @__PURE__ */ new Set([
|
|
561
|
+
"token",
|
|
562
|
+
"access_token",
|
|
563
|
+
"c"
|
|
564
|
+
]);
|
|
565
|
+
/**
|
|
566
|
+
* Прячет середину секрета, оставляя концы для сопоставления.
|
|
567
|
+
*
|
|
568
|
+
* @example
|
|
569
|
+
* ```ts
|
|
570
|
+
* maskSecret('eyJhbGciOiJIUzI1NiJ9.abc'); // 'eyJh…(24)…abc'
|
|
571
|
+
* ```
|
|
572
|
+
*/
|
|
573
|
+
function maskSecret(value) {
|
|
574
|
+
if (value.length <= 8) return "…";
|
|
575
|
+
return `${value.slice(0, 4)}…(${value.length})…${value.slice(-3)}`;
|
|
576
|
+
}
|
|
577
|
+
/** Маскирует секретные query-параметры URL перед записью в логи и ошибки. */
|
|
578
|
+
function redactUrl(url) {
|
|
579
|
+
try {
|
|
580
|
+
const parsed = new URL(url);
|
|
581
|
+
const entries = [...parsed.searchParams.entries()];
|
|
582
|
+
parsed.search = "";
|
|
583
|
+
for (const [name, value] of entries) parsed.searchParams.append(name, SECRET_QUERY_PARAMS.has(name.toLowerCase()) ? maskSecret(value) : value);
|
|
584
|
+
return parsed.toString();
|
|
585
|
+
} catch {
|
|
586
|
+
return url.replace(/([?&](?:token|access_token|c)=)[^&#]*/gi, "$1[скрыто]");
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
/** Маскирует подписанные адреса внутри произвольного диагностического текста. */
|
|
590
|
+
function redactUrlsInText(text) {
|
|
591
|
+
return text.replace(/https?:\/\/[^\s"'<>]+/gi, (url) => redactUrl(url));
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* Создаёт безопасную причину ошибки для публичной цепочки `cause`.
|
|
595
|
+
*
|
|
596
|
+
* Чужие объекты не сохраняются: их поля и пользовательские методы нельзя надёжно
|
|
597
|
+
* сериализовать без риска вывести секрет. Для Error и строк остаются тип и сообщение,
|
|
598
|
+
* но все встреченные в сообщении HTTP(S)-адреса проходят маскирование.
|
|
599
|
+
*/
|
|
600
|
+
function redactErrorCause(error) {
|
|
601
|
+
const message = error instanceof Error ? error.message : typeof error === "string" ? error : void 0;
|
|
602
|
+
if (message === void 0) return void 0;
|
|
603
|
+
const safeMessage = redactUrlsInText(message);
|
|
604
|
+
const safe = error instanceof TypeError ? new TypeError(safeMessage) : new Error(safeMessage);
|
|
605
|
+
if (error instanceof Error && !(error instanceof TypeError)) safe.name = error.name;
|
|
606
|
+
return safe;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Готовит заголовки к записи в лог.
|
|
610
|
+
*
|
|
611
|
+
* `Authorization` и `Cookie` маскируются: логи часто уезжают в системы сбора,
|
|
612
|
+
* и токен доступа в них попадать не должен.
|
|
613
|
+
*/
|
|
614
|
+
function redactHeaders(headers) {
|
|
615
|
+
const result = {};
|
|
616
|
+
headers.forEach((value, name) => {
|
|
617
|
+
if (SECRET_HEADERS.has(name.toLowerCase())) {
|
|
618
|
+
const spaceAt = value.indexOf(" ");
|
|
619
|
+
result[name] = spaceAt > 0 ? `${value.slice(0, spaceAt)} ${maskSecret(value.slice(spaceAt + 1))}` : maskSecret(value);
|
|
620
|
+
return;
|
|
621
|
+
}
|
|
622
|
+
result[name] = value;
|
|
623
|
+
});
|
|
624
|
+
return result;
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* Готовит тело запроса к записи в лог: пароли, токены и коды OTP заменяются заглушкой.
|
|
628
|
+
*
|
|
629
|
+
* Обходит вложенные объекты и массивы. `FormData` и бинарные тела не раскрываются вовсе.
|
|
630
|
+
*/
|
|
631
|
+
function redactBody(body) {
|
|
632
|
+
if (body === null || body === void 0) return body;
|
|
633
|
+
if (typeof FormData !== "undefined" && body instanceof FormData) return "[FormData]";
|
|
634
|
+
if (isBlob(body)) return "[Blob]";
|
|
635
|
+
if (body instanceof ArrayBuffer || ArrayBuffer.isView(body)) return "[binary]";
|
|
636
|
+
if (Array.isArray(body)) return body.map(redactBody);
|
|
637
|
+
if (typeof body === "object") {
|
|
638
|
+
const result = {};
|
|
639
|
+
for (const [key, value] of Object.entries(body)) result[key] = SECRET_FIELDS.has(key.toLowerCase()) ? "[скрыто]" : redactBody(value);
|
|
640
|
+
return result;
|
|
641
|
+
}
|
|
642
|
+
return body;
|
|
643
|
+
}
|
|
644
|
+
//#endregion
|
|
424
645
|
Object.defineProperty(exports, "AUTH_FLAG_COOKIE", {
|
|
425
646
|
enumerable: true,
|
|
426
647
|
get: function() {
|
|
@@ -445,12 +666,24 @@ Object.defineProperty(exports, "REFRESH_COOKIE_PATH", {
|
|
|
445
666
|
return REFRESH_COOKIE_PATH;
|
|
446
667
|
}
|
|
447
668
|
});
|
|
669
|
+
Object.defineProperty(exports, "RuntimeMode", {
|
|
670
|
+
enumerable: true,
|
|
671
|
+
get: function() {
|
|
672
|
+
return RuntimeMode;
|
|
673
|
+
}
|
|
674
|
+
});
|
|
448
675
|
Object.defineProperty(exports, "buildQuery", {
|
|
449
676
|
enumerable: true,
|
|
450
677
|
get: function() {
|
|
451
678
|
return buildQuery;
|
|
452
679
|
}
|
|
453
680
|
});
|
|
681
|
+
Object.defineProperty(exports, "createDeviceId", {
|
|
682
|
+
enumerable: true,
|
|
683
|
+
get: function() {
|
|
684
|
+
return createDeviceId;
|
|
685
|
+
}
|
|
686
|
+
});
|
|
454
687
|
Object.defineProperty(exports, "encodePathSegment", {
|
|
455
688
|
enumerable: true,
|
|
456
689
|
get: function() {
|
|
@@ -463,6 +696,18 @@ Object.defineProperty(exports, "hostOf", {
|
|
|
463
696
|
return hostOf;
|
|
464
697
|
}
|
|
465
698
|
});
|
|
699
|
+
Object.defineProperty(exports, "isBlob", {
|
|
700
|
+
enumerable: true,
|
|
701
|
+
get: function() {
|
|
702
|
+
return isBlob;
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
Object.defineProperty(exports, "isFile", {
|
|
706
|
+
enumerable: true,
|
|
707
|
+
get: function() {
|
|
708
|
+
return isFile;
|
|
709
|
+
}
|
|
710
|
+
});
|
|
466
711
|
Object.defineProperty(exports, "isSameSite", {
|
|
467
712
|
enumerable: true,
|
|
468
713
|
get: function() {
|
|
@@ -475,6 +720,12 @@ Object.defineProperty(exports, "joinUrl", {
|
|
|
475
720
|
return joinUrl;
|
|
476
721
|
}
|
|
477
722
|
});
|
|
723
|
+
Object.defineProperty(exports, "maskSecret", {
|
|
724
|
+
enumerable: true,
|
|
725
|
+
get: function() {
|
|
726
|
+
return maskSecret;
|
|
727
|
+
}
|
|
728
|
+
});
|
|
478
729
|
Object.defineProperty(exports, "normalizeBaseUrl", {
|
|
479
730
|
enumerable: true,
|
|
480
731
|
get: function() {
|
|
@@ -487,5 +738,53 @@ Object.defineProperty(exports, "originOf", {
|
|
|
487
738
|
return originOf;
|
|
488
739
|
}
|
|
489
740
|
});
|
|
741
|
+
Object.defineProperty(exports, "redactBody", {
|
|
742
|
+
enumerable: true,
|
|
743
|
+
get: function() {
|
|
744
|
+
return redactBody;
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
Object.defineProperty(exports, "redactErrorCause", {
|
|
748
|
+
enumerable: true,
|
|
749
|
+
get: function() {
|
|
750
|
+
return redactErrorCause;
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
Object.defineProperty(exports, "redactHeaders", {
|
|
754
|
+
enumerable: true,
|
|
755
|
+
get: function() {
|
|
756
|
+
return redactHeaders;
|
|
757
|
+
}
|
|
758
|
+
});
|
|
759
|
+
Object.defineProperty(exports, "redactUrl", {
|
|
760
|
+
enumerable: true,
|
|
761
|
+
get: function() {
|
|
762
|
+
return redactUrl;
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
Object.defineProperty(exports, "resolveFetch", {
|
|
766
|
+
enumerable: true,
|
|
767
|
+
get: function() {
|
|
768
|
+
return resolveFetch;
|
|
769
|
+
}
|
|
770
|
+
});
|
|
771
|
+
Object.defineProperty(exports, "shouldSendCredentials", {
|
|
772
|
+
enumerable: true,
|
|
773
|
+
get: function() {
|
|
774
|
+
return shouldSendCredentials;
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
Object.defineProperty(exports, "shouldUseCookieJar", {
|
|
778
|
+
enumerable: true,
|
|
779
|
+
get: function() {
|
|
780
|
+
return shouldUseCookieJar;
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
Object.defineProperty(exports, "supportsStreamingBody", {
|
|
784
|
+
enumerable: true,
|
|
785
|
+
get: function() {
|
|
786
|
+
return supportsStreamingBody;
|
|
787
|
+
}
|
|
788
|
+
});
|
|
490
789
|
|
|
491
|
-
//# sourceMappingURL=
|
|
790
|
+
//# sourceMappingURL=redact-DB9EMGJx.cjs.map
|