From ade6eb82df984d1ab32edddeef90111e4264caa1 Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Sat, 24 Oct 2026 10:48:00 +0700 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B2=D0=B5=D0=B4=D0=B8=20=D0=BA?= =?UTF-8?q?=D0=B0=D1=80=D0=BA=D0=B0=D1=81=20=D0=BF=D0=B0=D0=BA=D0=B5=D1=82?= =?UTF-8?q?=D0=B0=20=D0=B8=20=D1=83=D1=82=D0=B8=D0=BB=D0=B8=D1=82=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D1=81=D0=BB=D0=BE=D0=B9=20MCP-=D1=81=D0=B5?= =?UTF-8?q?=D1=80=D0=B2=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit package.json, tsconfig, eslint, генерация типов из OpenAPI-спеки, модули config/redact/errors/money/sanitize/cache с тестами и обезличенные фикстуры живых ответов API openrusrouter.ru. Co-Authored-By: Claude Opus 5.5 --- .gitignore | 5 + .npmrc | 1 + LICENSE | 21 + eslint.config.js | 19 + package-lock.json | 3302 ++++++++++++++++ package.json | 55 + scripts/gen-types.mjs | 30 + src/cache.ts | 31 + src/config.ts | 85 + src/errors.ts | 47 + src/generated/openapi.d.ts | 6435 ++++++++++++++++++++++++++++++++ src/money.ts | 117 + src/redact.ts | 52 + src/sanitize.ts | 33 + test/cache.test.ts | 42 + test/config.test.ts | 72 + test/fixtures/api-balance.json | 1 + test/fixtures/api-keys.json | 43 + test/fixtures/api-models.json | 254 ++ test/fixtures/api-usage.json | 1 + test/fixtures/keys-models.json | 72 + test/fixtures/v1-auth-key.json | 1 + test/fixtures/v1-models.json | 436 +++ test/money.test.ts | 61 + test/redact.test.ts | 72 + test/sanitize.test.ts | 20 + tsconfig.build.json | 5 + tsconfig.json | 29 + tsconfig.test.json | 8 + 29 files changed, 11350 insertions(+) create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 LICENSE create mode 100644 eslint.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/gen-types.mjs create mode 100644 src/cache.ts create mode 100644 src/config.ts create mode 100644 src/errors.ts create mode 100644 src/generated/openapi.d.ts create mode 100644 src/money.ts create mode 100644 src/redact.ts create mode 100644 src/sanitize.ts create mode 100644 test/cache.test.ts create mode 100644 test/config.test.ts create mode 100644 test/fixtures/api-balance.json create mode 100644 test/fixtures/api-keys.json create mode 100644 test/fixtures/api-models.json create mode 100644 test/fixtures/api-usage.json create mode 100644 test/fixtures/keys-models.json create mode 100644 test/fixtures/v1-auth-key.json create mode 100644 test/fixtures/v1-models.json create mode 100644 test/money.test.ts create mode 100644 test/redact.test.ts create mode 100644 test/sanitize.test.ts create mode 100644 tsconfig.build.json create mode 100644 tsconfig.json create mode 100644 tsconfig.test.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2dc390c --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules +dist +*.tgz +.env +.DS_Store diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..b6f27f1 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..80286ef --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Realmanual + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..7ad3039 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,19 @@ +// @ts-check +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + { ignores: ["dist/**", "node_modules/**", "src/generated/**"] }, + js.configs.recommended, + ...tseslint.configs.strict, + { + rules: { + // stdout занят протоколом MCP (stdio): любая печать туда ломает сессию. + "no-console": ["error", { allow: ["error"] }], + }, + }, + { + files: ["scripts/**/*.mjs"], + languageOptions: { globals: { process: "readonly", console: "readonly", URL: "readonly" } }, + }, +); diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c68ecd6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,3302 @@ +{ + "name": "@openrusrouter/mcp", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@openrusrouter/mcp", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/sdk": "1.32.2", + "zod": "^4.6.7" + }, + "bin": { + "openrusrouter-mcp": "dist/index.js" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^20.19.43", + "eslint": "^10.13.0", + "openapi-typescript": "^7.13.0", + "tsx": "^4.23.15", + "typescript": "5.9.3", + "typescript-eslint": "^8.72.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/js": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@hono/node-server": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.5.tgz", + "integrity": "sha512-MgP/I06GdvsilDspMk/qmRzpk8LOUStP6jFAbSRi40W09DhoyUn+Mj7eyKICPqeYKybOjVFLVIAQzvu1bfIa7Q==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "hono": "^4 || ^5.0.0-0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.32.2", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.32.2.tgz", + "integrity": "sha512-SWsVtH/0vULSQStoZ6xxH2K09Li8fXYTMC64ZeogCdpZ3XSi1eCPOByV1OpuJxuI3OcmHhosDWYyii07tpPsLw==", + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9 || ^2.0.5", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.22", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.22.tgz", + "integrity": "sha512-dEyx4uHh6Pc6ZkdU9Xe8rA4h12Odq6QKU/fqseBn07xwVwskhHiF3KHhqmw5BR+jAiFTvKTmwCafFql/k1pSzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.3.2", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core/node_modules/brace-expansion": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@redocly/openapi-core/node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.72.0.tgz", + "integrity": "sha512-aysKgysPX9XOv/dk95UkvUDe++Q3n3dJPG9CEWT3RUccxfkPJ+Qn6nhKPDhZXU6Ofo9kgwGWwKyQCnZIXTgngg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/type-utils": "8.72.0", + "@typescript-eslint/utils": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.72.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.14.tgz", + "integrity": "sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.72.0.tgz", + "integrity": "sha512-0O+x3ynciD6zuXb1CKSu165haQV6ey53DKhHHUnujP7U+XlltJG+/keT0Xhd1HXCze+IHf7Tvt39ovyepX/LNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.72.0.tgz", + "integrity": "sha512-bzHngBbmIgzj6TsTjVUCntGx6FFLgE7mPjpO0PzHycOaZHXqS43XnrkFIHRS16LLr/xO6FHSXG3GJFo/F4bSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.72.0", + "@typescript-eslint/types": "^8.72.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.72.0.tgz", + "integrity": "sha512-JewPyJOYcXBSAS7PIgufscHOwZqhTGyBRiQJhhuDln66tMchxvvt7nJ90JKKIoOelmdHA3kAQbm9cEUHc4JSEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.72.0.tgz", + "integrity": "sha512-0vbxCf7xLVPSjcOdm6eQbgViH8EDJ8xnl1laLT9yoqSz2YmcSfSg5Vdl0JUvZf6rcuJ0ZivJIuMD4edTg8obaA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.72.0.tgz", + "integrity": "sha512-gEXxmmbSTSOVTC37IYYvremOM31v+UN2W772bJxmVYPhAOPSUxtacyb7wkm1EhkYKU6KzFJsdU44DCV4to0LWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/utils": "8.72.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.72.0.tgz", + "integrity": "sha512-Adfoy59kxNEjT5E5fnyrp7GpHYuWs8ONbdohu4ez6IU46qDq8LltG90tLmkucZB2uhYL8mqBjtXqGd8yhfjXqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.72.0.tgz", + "integrity": "sha512-gwLfFgWcw15tN2KXDCJvxEOQjPn+P3j/n3eSqkZ5sX4VpW9Cosodr+tWwBZRvJT3TcKx6HnPRGZMCNAiP1Xhug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.72.0", + "@typescript-eslint/tsconfig-utils": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/visitor-keys": "8.72.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.72.0.tgz", + "integrity": "sha512-0up3oArJZBYnzOaEXKo8DmI43QvMZyPoFEt9LWzRJcyl/RcJjfKKPebknN1VbTgXBf3M/KggUh5uUMdURLnseg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.72.0", + "@typescript-eslint/types": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.72.0.tgz", + "integrity": "sha512-E2mroviasaWecDzTGiGMtuEguuLVOeRggnrIn93CJuxYJzQBHbyymlT08aVQydbBal5538o+JkBKXh2sRuCyIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.72.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/body-parser": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.4.0.tgz", + "integrity": "sha512-Ifk1KcLrxaGt1occQagQNFNNOqRt1LFuehEnHHzB8Z6cRgIxRU1TBaiIsUiCdA+gy5PL7Y1IX31DNKkRmxtXjQ==", + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^2.1.0", + "debug": "^4.4.3", + "http-errors": "^2.0.1", + "iconv-lite": "^0.7.3", + "on-finished": "^2.4.1", + "qs": "^6.16.1", + "raw-body": "^3.0.2", + "type-is": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/body-parser/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/content-disposition": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-2.0.1.tgz", + "integrity": "sha512-e+H0ZXHSWYrENhQzw1LPuP4oF5MzVKmDU6d3hxlvaPEYLLg62MxtQNPRx4SYSuYJSBUgnQIG4HIN2tEtNv7Dog==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.6.tgz", + "integrity": "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.13.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.13.0.tgz", + "integrity": "sha512-UohDjD4zSzEwhf58M3O7vc/ycc8O1I7dNlhtD0WxE8Px8EHNJKtUwzBqrzeGhisRuv2ag4sAYswszCJRgKXYUg==", + "dev": true, + "license": "MIT", + "workspaces": [ + "packages/*" + ], + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "11.1.5 || >11.1.6 <12", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/express": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.3.0.tgz", + "integrity": "sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==", + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.3.0", + "content-disposition": "^2.0.1", + "content-type": "^2.0.0", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.8", + "qs": "^6.16.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.1.0", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.7.1", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.1.tgz", + "integrity": "sha512-2FXbqkygil3mZiXzEtM0IjE/UBmBNot0J1GEVrJT4JbrvwOSoEtqtO+wHbiMQELWe1eNvHoYRg2dkjOid+6JkA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "ip-address": "^10.2.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/express/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^6.1.23" + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz", + "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.13.13", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.13.tgz", + "integrity": "sha512-CQ46U0ZkAGmbT/4UxdzzGJpacP2IeKgY4a5/tOI9AABbpOMfK739wfDXmv1usCk+3RkKj1hQy4/fjhiwa2xlrA==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.3.tgz", + "integrity": "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.7.4", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.4.tgz", + "integrity": "sha512-DWBMKyeM25D70nrIiTkgQth59/G+naKOTep6huF9j+GmeK3DMD60Zvxg5d5qQnz+38AHgou6ean2LECX+lrKDg==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC" + }, + "node_modules/jose": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.13.tgz", + "integrity": "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/json-schema-typed/-/json-schema-typed-8.0.2.tgz", + "integrity": "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==", + "license": "BSD-2-Clause" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.1.tgz", + "integrity": "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/negotiator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.1.0.tgz", + "integrity": "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg==", + "license": "MIT", + "dependencies": { + "content-type": "^2.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/negotiator/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.2.tgz", + "integrity": "sha512-ATWUv387i+nLD49EiDoOft5scf32Vsdz3ktrQJG9bi7jRFmaXTssvyvj1EmeKVVgvesbqUtvQQvaS2TpTukAZQ==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/openapi-typescript": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.13.0.tgz", + "integrity": "sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.34.6", + "ansi-colors": "^4.1.3", + "change-case": "^5.4.4", + "parse-json": "^8.3.0", + "supports-color": "^10.2.2", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.4.2.tgz", + "integrity": "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.8.tgz", + "integrity": "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ==", + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.16.1", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.1.tgz", + "integrity": "sha512-5Px6J7rfHBAGUhNQ3es4YzcOdBBw0SDcVtThJml4RdnKw4tKs2epA0kPZ+K8X+WiHktT93RHVcQ/3ym5aTCgtg==", + "license": "BSD-3-Clause", + "dependencies": { + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.3.0.tgz", + "integrity": "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz", + "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==", + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", + "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==", + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsx": { + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.1.0.tgz", + "integrity": "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA==", + "license": "MIT", + "dependencies": { + "content-type": "^2.0.0", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/type-is/node_modules/content-type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-2.1.0.tgz", + "integrity": "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.72.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.72.0.tgz", + "integrity": "sha512-r26Fkc8As+mZvqp7SLsMmNFyYUup/6CjK7/eHBHmhOw6GH6n6YFxHVFrG0jN1c5CMpDzelLPxvgeCOzx7BjN+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.72.0", + "@typescript-eslint/parser": "8.72.0", + "@typescript-eslint/typescript-estree": "8.72.0", + "@typescript-eslint/utils": "8.72.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js-replace": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.2.tgz", + "integrity": "sha512-VeBBt/nMxedczht3KDL2yBBMJqIxpCQLMZp3UJ1otcWwUHdQW4IiyHpJivExzSK+FdxTowRfGL4FmwDqH9+S8Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "license": "ISC" + }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..876f749 --- /dev/null +++ b/package.json @@ -0,0 +1,55 @@ +{ + "name": "@openrusrouter/mcp", + "version": "0.1.0", + "description": "MCP-сервер (stdio) для площадки openrusrouter.ru: баланс, каталог моделей, ключи, расход", + "type": "module", + "bin": { + "openrusrouter-mcp": "dist/index.js" + }, + "files": [ + "dist", + "README.md", + "LICENSE", + "CHANGELOG.md" + ], + "engines": { + "node": ">=20" + }, + "publishConfig": { + "access": "public" + }, + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://git.realmanual.ru/pub/openrusrouter-mcp.git" + }, + "homepage": "https://git.realmanual.ru/pub/openrusrouter-mcp", + "keywords": [ + "mcp", + "openrusrouter", + "openrouter", + "llm" + ], + "scripts": { + "build": "tsc -p tsconfig.build.json", + "typecheck": "tsc --noEmit && tsc -p tsconfig.test.json", + "test": "node --import tsx --test \"test/**/*.test.ts\"", + "lint": "eslint .", + "gen:types": "node scripts/gen-types.mjs", + "prepublishOnly": "npm run typecheck && npm test && npm run build", + "pack:check": "npm pack --dry-run" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "1.32.2", + "zod": "^4.6.7" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@types/node": "^20.19.43", + "eslint": "^10.13.0", + "openapi-typescript": "^7.13.0", + "tsx": "^4.23.15", + "typescript": "5.9.3", + "typescript-eslint": "^8.72.0" + } +} diff --git a/scripts/gen-types.mjs b/scripts/gen-types.mjs new file mode 100644 index 0000000..4075e43 --- /dev/null +++ b/scripts/gen-types.mjs @@ -0,0 +1,30 @@ +#!/usr/bin/env node +// Генерирует типы TypeScript из публичной OpenAPI-спеки openrusrouter.ru. +// Источник переопределяется переменной SPEC_URL (например, локальный стенд). +// Результат коммитится: сборка пакета не должна зависеть от сети. +import { mkdirSync, writeFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import openapiTS, { astToString } from "openapi-typescript"; + +const DEFAULT_SPEC_URL = "https://openrusrouter.ru/docs/spec.json"; +const specUrl = new URL(process.env.SPEC_URL || DEFAULT_SPEC_URL); +if (specUrl.protocol !== "https:" && specUrl.protocol !== "file:" && + !(specUrl.protocol === "http:" && ["localhost", "127.0.0.1", "[::1]"].includes(specUrl.hostname))) { + console.error(`gen-types: недопустимый SPEC_URL (${specUrl.protocol}//${specUrl.host}) — нужен https или http://localhost`); + process.exit(1); +} + +const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const out = resolve(root, "src/generated/openapi.d.ts"); + +const ast = await openapiTS(specUrl); +const header = + "/**\n" + + " * Сгенерировано scripts/gen-types.mjs из OpenAPI-спеки openrusrouter.ru — не править руками.\n" + + " * Внимание: спека расходится с реальными ответами в части полей (см. test/fixtures);\n" + + " * источник правды о форме ответа — фикстуры живых ответов, а не эти типы.\n" + + " */\n"; +mkdirSync(dirname(out), { recursive: true }); +writeFileSync(out, header + astToString(ast)); +console.error(`gen-types: записано ${out}`); diff --git a/src/cache.ts b/src/cache.ts new file mode 100644 index 0000000..ba2a53a --- /dev/null +++ b/src/cache.ts @@ -0,0 +1,31 @@ +// Кэш одного значения с TTL и single-flight: параллельные вызовы до +// завершения загрузки ждут один и тот же промис, ошибка не кэшируется. + +export function createTtlCache( + ttlMs: number, + loader: () => Promise, + now: () => number = Date.now, +): () => Promise { + let value: { data: T; expiresAt: number } | undefined; + let inflight: Promise | undefined; + + return function get(): Promise { + if (value !== undefined && now() < value.expiresAt) { + return Promise.resolve(value.data); + } + if (inflight !== undefined) { + return inflight; + } + const p = (async () => { + try { + const data = await loader(); + value = { data, expiresAt: now() + ttlMs }; + return data; + } finally { + inflight = undefined; + } + })(); + inflight = p; + return p; + }; +} diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 0000000..85da590 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,85 @@ +// Конфигурация процесса MCP-сервера. Читается один раз на старте. +// +// Ключ клиента уезжает в заголовке Authorization на base URL, поэтому +// адрес проверяется строже, чем «лишь бы парсился»: незашифрованный http +// допустим только до локального стенда, а userinfo в адресе — отказ сразу +// (такой URL печатается в логах и сообщениях об ошибках целиком). + +export const DEFAULT_BASE_URL = "https://openrusrouter.ru"; + +const LOCAL_HOSTS = new Set(["localhost", "127.0.0.1", "::1", "[::1]"]); + +export interface Config { + readonly apiKey: string; + readonly baseUrl: string; +} + +/** Ошибка конфигурации. Сообщение НИКОГДА не содержит значения ключа. */ +export class ConfigError extends Error { + override name = "ConfigError"; +} + +export type Env = Readonly>; + +/** + * Чистый разбор конфигурации без побочных эффектов (без exit и stderr) — + * отдельно от loadConfig, чтобы его можно было проверить тестом. + */ +export function parseConfig(env: Env): Config { + const apiKey = (env.OPENRUSROUTER_API_KEY ?? "").trim(); + if (apiKey === "") { + throw new ConfigError( + "не задана переменная окружения OPENRUSROUTER_API_KEY — укажите API-ключ из личного кабинета openrusrouter.ru", + ); + } + const rawBase = (env.OPENRUSROUTER_BASE_URL ?? "").trim(); + const baseUrl = rawBase === "" ? DEFAULT_BASE_URL : normalizeBaseUrl(rawBase); + return Object.freeze({ apiKey, baseUrl }); +} + +function normalizeBaseUrl(raw: string): string { + let url: URL; + try { + url = new URL(raw); + } catch { + // Сам адрес в сообщение не кладём: в нём может оказаться что угодно. + throw new ConfigError("OPENRUSROUTER_BASE_URL не является корректным URL"); + } + if (url.username !== "" || url.password !== "") { + throw new ConfigError( + "OPENRUSROUTER_BASE_URL не должен содержать логин/пароль (user:pass@) — уберите их из адреса", + ); + } + if (url.protocol === "http:") { + if (!LOCAL_HOSTS.has(url.hostname)) { + throw new ConfigError( + `OPENRUSROUTER_BASE_URL: http разрешён только для localhost/127.0.0.1/::1, для ${url.hostname} нужен https`, + ); + } + } else if (url.protocol !== "https:") { + throw new ConfigError(`OPENRUSROUTER_BASE_URL: неподдерживаемый протокол ${url.protocol}`); + } + if (url.search !== "" || url.hash !== "") { + throw new ConfigError("OPENRUSROUTER_BASE_URL не должен содержать query-параметры или фрагмент"); + } + return `${url.origin}${url.pathname}`.replace(/\/+$/, ""); +} + +/** + * Читает конфигурацию из process.env. При ошибке пишет причину в stderr + * (stdout занят протоколом MCP) и завершает процесс с кодом 1. + */ +export function loadConfig(env: Env = process.env): Config { + let config: Config; + try { + config = parseConfig(env); + } catch (err) { + const message = err instanceof ConfigError ? err.message : "ошибка чтения конфигурации"; + console.error(`openrusrouter-mcp: ${message}`); + process.exit(1); + } + if (config.baseUrl !== DEFAULT_BASE_URL) { + console.error(`openrusrouter-mcp: ключ будет отправляться на ${new URL(config.baseUrl).host}`); + } + return config; +} diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..5d2ece4 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,47 @@ +import { redactError } from "./redact.js"; + +/** Неуспешный HTTP-ответ API площадки. */ +export class ApiError extends Error { + override name = "ApiError"; + readonly status: number; + readonly code?: string; + readonly path: string; + + constructor(status: number, path: string, message?: string, code?: string) { + super(message ?? `HTTP ${status}`); + this.status = status; + this.path = path; + if (code !== undefined) { + this.code = code; + } + } +} + +export interface ToolErrorResult { + [key: string]: unknown; + isError: true; + content: [{ type: "text"; text: string }]; +} + +function textForStatus(status: number | undefined, fallback: string): string { + if (status === 401) return "ключ недействителен или отозван"; + if (status === 403) return "ключу запрещено выполнять это действие"; + if (status === 429) return "превышен лимит частоты запросов, повторите позже"; + if (status !== undefined && status >= 500 && status <= 599) { + return "сервис openrusrouter.ru временно недоступен"; + } + return fallback; +} + +/** Ошибку любого вида превращает в результат инструмента MCP; текст всегда проходит redactError. */ +export function toToolErrorResult(err: unknown): ToolErrorResult { + const r = redactError(err); + let text = textForStatus(r.status, r.message); + const details = [r.status !== undefined ? `HTTP ${r.status}` : "", r.code ?? "", r.path ?? ""] + .filter((s) => s !== "") + .join(", "); + if (details !== "") { + text += ` (${details})`; + } + return { isError: true, content: [{ type: "text", text }] }; +} diff --git a/src/generated/openapi.d.ts b/src/generated/openapi.d.ts new file mode 100644 index 0000000..85b8173 --- /dev/null +++ b/src/generated/openapi.d.ts @@ -0,0 +1,6435 @@ +/** + * Сгенерировано scripts/gen-types.mjs из OpenAPI-спеки openrusrouter.ru — не править руками. + * Внимание: спека расходится с реальными ответами в части полей (см. test/fixtures); + * источник правды о форме ответа — фикстуры живых ответов, а не эти типы. + */ +export interface paths { + "/api/auth/config": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get login page configuration + * @description Публичный маршрут (сессии на странице входа ещё нет) — несекретная конфигурация экрана входа/регистрации. Секреты (TelegramBotToken, пароль T-Bank) сюда не попадают ни при каких условиях — только их несекретные производные. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Public configuration */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Показывать ли кнопку пополнения — эквайринг настроен на сервере. */ + payments_enabled?: boolean; + /** @description Скрыть ссылку регистрации — новые аккаунты временно не заводятся. */ + registration_disabled?: boolean; + /** @description Пусто — вход через Telegram не настроен, кнопку виджета показывать не нужно. */ + telegram_bot_username?: string; + /** @description Показывать ли кнопку тестового платежа (TBANK_TEST_ENABLED). На боевом терминале всегда false. */ + test_payment_enabled?: boolean; + /** @description Максимальная сумма пополнения, RUB. Присутствует только когда payments_enabled=true. */ + topup_max?: string; + /** @description Минимальная сумма пополнения, RUB. Присутствует только когда payments_enabled=true. */ + topup_min?: string; + /** @description Номер счётчика Яндекс.Метрики для SPA. Пусто — счётчик в кабинете не поднимается. */ + yandex_metrika_id?: string; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/forgot-password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Request password reset + * @description Всегда отвечает 200 с одним и тем же телом, существует адрес или нет, есть ли у аккаунта пароль (Telegram-only аккаунты письма не получают) и активен ли он — иначе форма стала бы перечислителем клиентов по коду/телу ответа или по времени. Если условия выполнены, на адрес уходит письмо со ссылкой вида /reset-password?token=<токен>, действующей час и одноразовой. Лимит частоты — по IP и отдельно по адресу получателя (`forgot_password_rate_limited`): второй уровень ловит распределённый flood по одной цели, который IP-лимит не видит. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** Format: email */ + email: string; + }; + }; + }; + responses: { + /** @description Request accepted (does not confirm whether an email was actually sent) */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @enum {string} */ + status?: "pending"; + }; + }; + }; + /** @description Invalid email (`invalid_email`) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded, by IP or by target email (`forgot_password_rate_limited`) */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/impersonation/stop": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Stop impersonation session + * @description Завершает сеанс просмотра кабинета клиента администратором и возвращает администратору его собственную сессию. Ответ restored=false означает, что вернуться не удалось (сессия администратора истекла) и требуется повторный вход. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Impersonation stopped */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + restored?: boolean; + }; + }; + }; + /** @description Impersonation session is not active */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Not authenticated */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/login": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Login to account + * @description Authenticate with email and password. Auth is **cookie-based**: on success the server sets `Set-Cookie: orr_session=...` — all subsequent requests automatically include this cookie. If TOTP is enabled, returns `totp_required: true` instead of a session; then call `/api/auth/login/totp` with the code to get the session cookie. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** Format: email */ + email: string; + password: string; + }; + }; + }; + responses: { + /** @description Login response — two possible outcomes: */ + 200: { + headers: { + /** @description Session cookie (orr_session=...) — set on success WITHOUT TOTP. Scalar auto-saves it via persistCookiesInLocalStorage and attaches to subsequent requests. */ + "Set-Cookie"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example ok */ + status?: string; + } | { + /** @example true */ + totp_required?: boolean; + }; + }; + }; + /** @description Invalid credentials */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Account blocked or email not verified */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/login/totp": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Submit TOTP code (second login step) + * @description Complete login after receiving `totp_required: true` from POST /api/auth/login. Requires the `orr_login_challenge` cookie set by that response. On success, the server sets `Set-Cookie: orr_session=...` — the **session cookie** that authorizes all subsequent API calls (no JWT in body). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * @description 6-digit TOTP code from authenticator app + * @example 123456 + */ + code: string; + }; + }; + }; + responses: { + /** @description TOTP accepted — session cookie (orr_session) set. You are now authenticated for all subsequent requests. */ + 200: { + headers: { + /** @description Session cookie (orr_session=...) — Scalar auto-saves and attaches to subsequent requests via persistCookiesInLocalStorage. */ + "Set-Cookie"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example true */ + ok?: boolean; + }; + }; + }; + /** @description Invalid TOTP code or challenge expired */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/logout": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Logout */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Logout successful */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Not authenticated */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/passkey/login/begin": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Begin passkey login (WebAuthn) + * @description Начинает discoverable-вход по ключу доступа: пользователь на этом шаге ещё не известен серверу, а фронтенд передаёт ответ дословно в navigator.credentials.get({publicKey: ...}). Публичный маршрут, без тела и без Content-Type. Смонтирован ТОЛЬКО когда контур passkey включён на сервере (passkey.Service собран) — иначе маршрут отсутствует вовсе и отвечает 404, тем же приёмом, что и раздел платежей. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description WebAuthn PublicKeyCredentialRequestOptions — передаётся в navigator.credentials.get() без переупаковки. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + /** @description cross_origin_rejected — недопустимый источник запроса */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Контур passkey выключен на сервере */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/passkey/login/finish": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Finish passkey login + * @description Завершает вход. Тело — ответ аутентификатора из navigator.credentials.get(), Content-Type: application/json обязателен. Тело ограничено 64 КиБ — attestation/assertion с полной цепочкой сертификатов заметно больше общего предела в 4 КиБ. Любой отказ (challenge истёк, подпись не сошлась, обнаружено клонирование ключа — счётчик подписей уменьшился по сравнению с сохранённым) отвечает ОДНИМ и тем же 401 без деталей — раскрывать нападающему причину незачем. Passkey засчитывается сразу за оба фактора аутентификации, отдельный шаг TOTP после него не требуется. Смонтирован только при включённом контуре passkey. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": Record; + }; + }; + responses: { + /** @description Вход выполнен — установлена cookie сессии orr_session. */ + 200: { + headers: { + /** @description Session cookie (orr_session=...). */ + "Set-Cookie"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example ok */ + status?: string; + }; + }; + }; + /** @description Ответ аутентификатора не подошёл — истёкший challenge, неверная подпись или обнаруженное клонирование ключа; причина не детализируется */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description cross_origin_rejected — недопустимый источник запроса */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Контур passkey выключен на сервере */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Тело больше 64 КиБ (request_too_large) */ + 413: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/register": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Register new account + * @description Create new account with email + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** Format: email */ + email: string; + password: string; + }; + }; + }; + responses: { + /** @description Registration successful */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid email or password */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Registration is disabled (`registration_disabled`) or the email domain is on the blocklist (`email_domain_blocked`) */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/reset-password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Reset password using a token from email + * @description Гасит одноразовый токен сброса пароля и записывает новый хеш. В отличие от подтверждения email, сессия НЕ выдаётся: здесь происходит смена секрета входа, а не однократное доказательство владения адресом, и TOTP/passkey (если включены) обязаны сработать на следующем обычном входе через POST /api/auth/login. Все существующие сессии и незавершённые попытки входа (login_challenges) отзываются, а счётчик неудачных попыток пароля сбрасывается. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Новый пароль, минимум 10 символов */ + password: string; + /** @description Одноразовый токен из ссылки в письме */ + token: string; + }; + }; + }; + responses: { + /** @description Password changed; no session is issued */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Token is invalid, expired or already used (`invalid_token`), or the new password is too short/long (`weak_password`) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/telegram": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Login via Telegram + * @description Второй способ входа — Telegram Login Widget, без пароля и без email вовсе. Тело — полный набор полей, которые отдаёт виджет (id, first_name, username, photo_url, auth_date, hash и всё, что Telegram добавит впоследствии), все строками: подпись HMAC-SHA256 проверяется по каждому присланному полю. Публичный маршрут (SEC-05): требует Content-Type: application/json и совпадающий Origin/Sec-Fetch-Site — иначе 403 cross_origin_rejected (login CSRF: чужая страница не должна суметь поставить cookie сессии в чужом браузере через no-cors fetch на этот маршрут). Общий IP-лимитер публичного auth-поддерева — 300 запросов в минуту. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + /** + * @example { + * "auth_date": "1700000000", + * "first_name": "Ivan", + * "hash": "abcdef0123456789...", + * "id": "123456789", + * "username": "ivan" + * } + */ + "application/json": { + [key: string]: string; + }; + }; + }; + responses: { + /** @description Вход выполнен — установлена cookie сессии orr_session. */ + 200: { + headers: { + /** @description Session cookie (orr_session=...). */ + "Set-Cookie"?: string; + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example ok */ + status?: string; + }; + }; + }; + /** @description Недействительная или просроченная подпись Telegram (telegram_auth_invalid) — не детализируется, какая именно, тем же приёмом, что и у POST /api/auth/login */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Регистрация новых аккаунтов отключена администратором (registration_disabled — существующий telegram-аккаунт продолжает входить как обычно), аккаунт заблокирован (account_blocked), либо запрос отклонён как cross-origin (cross_origin_rejected) */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Вход через Telegram не настроен на сервере (пустой TELEGRAM_BOT_TOKEN) */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/auth/verify-email": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Confirm email address + * @description Гасит одноразовый токен из письма и подтверждает адрес. При успехе СРАЗУ выдаёт сессию (cookie), потому что переход по ссылке уже доказал владение адресом: signed_in=true. Сессия не выдаётся, если у аккаунта включён второй фактор (TOTP или passkey) или он заблокирован — тогда signed_in=false, а почта всё равно подтверждена: отказ в автовходе не откатывает подтверждения. Способ входа записывается в журнал сессий как email_verify — отдельным значением, а не 'password': пароля на этом пути не вводили. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Одноразовый токен из ссылки в письме */ + token: string; + }; + }; + }; + responses: { + /** @description Email confirmed; session cookies set when signed_in is true */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Выдана ли сессия. false — почта подтверждена, но входить нужно обычным путём. */ + signed_in?: boolean; + /** @enum {string} */ + status?: "verified"; + }; + }; + }; + /** @description Token is invalid, expired or already used (`invalid_token`) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/balance": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get account balance */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Account balance */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Balance in rubles */ + balance?: number; + /** @example RUB */ + currency?: string; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/balance-history": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List balance history (card payments and non-card credits) + * @description Объединённая история пополнений текущего клиента одним списком: все попытки оплаты картой из payments в любом статусе (kind=payment) и зачисления без платежа из журнала — промокоды, переводы с реферального баланса, бонусы за отзыв (kind=ledger, status всегда confirmed). Строка журнала deposit сюда НЕ попадает: успешный платёж картой уже представлен своей строкой kind=payment, и одна операция не показывается дважды. Новые первыми; пагинация применяется к объединённому списку, total — число строк под фильтром. from/to — включительные границы; принимается дата YYYY-MM-DD (from — начало суток, to — конец суток) или RFC3339; без границ — вся история. from позже to — 400. limit по умолчанию 100, молча срезается сверху до 100 и возвращается к умолчанию при неположительном значении; offset по умолчанию 0, отрицательный молча прижимается к 0. + */ + get: { + parameters: { + query?: { + from?: string; + to?: string; + limit?: number; + offset?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Balance history page */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data?: { + /** @description Decimal string, RUB. */ + amount?: string; + /** Format: date-time */ + created_at?: string; + failure_reason?: string | null; + /** @enum {string} */ + kind?: "payment" | "ledger"; + /** @enum {string|null} */ + ledger_type?: "promo" | "referral_spent" | "review_reward" | null; + /** Format: uuid */ + payment_id?: string | null; + /** + * @description Для kind=payment — статус платежа; для kind=ledger всегда confirmed. + * @enum {string} + */ + status?: "pending" | "confirmed" | "rejected" | "expired" | "refunded" | "refund_pending" | "cancelled"; + }[]; + total?: number; + }; + }; + }; + /** @description from/to — не дата, from позже to, limit/offset не целое число (invalid_request) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/banner": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Active announcement banner + * @description Баннер объявлений, который показывается в кабинете прямо сейчас: активный И попавший в окно показа. Один и тот же для всех пользователей — скрытие живёт в браузере (localStorage по id баннера), а не на сервере. Нет опубликованного баннера, показ ещё не начался или уже закончился — `banner: null`, это штатное состояние, а не ошибка. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Active banner or null */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + banner?: { + /** + * Format: uuid + * @description По этому id фронтенд запоминает скрытие в браузере + */ + id?: string; + message?: string; + /** Format: date-time */ + show_from?: string | null; + /** Format: date-time */ + show_until?: string | null; + /** @enum {string} */ + type?: "info" | "notification" | "critical"; + } | null; + }; + }; + }; + /** @description No session */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/chat/completions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Chat completion (SSE) + * @description Отдаёт SSE (`Content-Type: text/event-stream`), НЕ JSON — даже если клиент прислал `stream:false`: сервер принудительно подставляет `stream:true` и `stream_options.include_usage:true` поверх присланного тела, это единственный режим, который умеет обработчик. Поток — те же строки `data: ...`, что отдаёт провайдер (OpenAI-совместимые чанки), плюс до двух служебных событий уже от самого сервера в конце: `data: {"type":"usage",...}` с суммой списания в рублях после успешного списания и `data: {"type":"warning",...}` с `topup_url`, если баланс после списания опустился до нуля или ниже. Только для авторизованных клиентов: аноним (нет или истекла сессия) получает 401 — страница /chat показывает ему предложение зарегистрироваться. Модели — только платные `available_in_chat`: бесплатная модель, названная напрямую, отвечает 404, как и не прокурированная под чат. Каждый запрос требует положительного баланса (иначе 402 `no_balance`, тело несёт `topup_url`/`hint`). `conversation_id` сохраняет сообщение в беседу клиента — чужой или несуществующий id отвечает 404, а не тихо игнорируется. Лимит запросов в минуту считается по user_id; при исчерпании — 429 `rate_limit_exceeded`. Мутация тарифицируется деньгами клиента, поэтому при наличии сессионной cookie обязателен заголовок `X-CSRF-Token` (double-submit, см. /api/auth/login). + */ + post: { + parameters: { + query?: never; + header?: { + /** @description Обязателен, если запрос идёт с сессионной cookie orr_session (у чата это единственный режим — аноним получает 401 до проверки CSRF). */ + "X-CSRF-Token"?: string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * Format: uuid + * @description Только для авторизованного клиента: сообщение сохраняется в историю его беседы. Чужой/несуществующий id — 404. + */ + conversation_id?: string; + messages: { + content?: string; + role?: string; + }[]; + /** @description ID модели из GET /api/chat/models */ + model: string; + }; + }; + }; + responses: { + /** @description SSE-поток чанков ответа (text/event-stream), НЕ JSON */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "text/event-stream": string; + }; + }; + /** @description Некорректное тело запроса (invalid_request) или модель не найдена/отключена (model_not_found) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Требуется вход: чат обслуживает только авторизованных клиентов (unauthorized) */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Недостаточно средств на счёте (no_balance, тело несёт topup_url и hint) */ + 402: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Модель недоступна в чате (не прокурирована под чат или бесплатная — чат только платный), либо conversation_id указывает на чужую/несуществующую беседу (not_found) */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Превышен лимит запросов в минуту (rate_limit_exceeded) или все upstream-ключи исчерпали лимит (rate_limited) */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Чат отключён администратором (chat_disabled) или нет доступных upstream-ключей (no_upstream) */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/chat/conversations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List own chat conversations + * @description АКТИВНЫЕ беседы клиента, от новых к старым: без параметров — только личные (project_id IS NULL), ?project_id (uuid) — беседы одного проекта. ?archived=1 отдаёт архивные беседы (все проекты вместе, с project_name). Гейт chat_enabled общий для /api/chat/*. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Conversation list */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data?: { + /** Format: date-time */ + created_at?: string; + /** Format: uuid */ + id?: string; + model?: string; + title?: string; + /** Format: date-time */ + updated_at?: string; + /** Format: uuid */ + user_id?: string; + }[]; + }; + }; + }; + /** @description No session (unauthorized) */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + /** + * Create a chat conversation + * @description Требует сессии и X-CSRF-Token (double-submit). title по умолчанию — «Новый чат». + */ + post: { + parameters: { + query?: never; + header: { + "X-CSRF-Token": string; + }; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + model: string; + /** + * Format: uuid + * @description Необязательно: беседа внутри RAG-проекта — ответы чата подмешивают фрагменты его готовых файлов. Чужой id — 404, как несуществующий. + */ + project_id?: string; + /** @description По умолчанию «Новый чат» */ + title?: string; + }; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** Format: uuid */ + id?: string; + }; + }; + }; + /** @description Поле model обязательно (invalid_request) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No session (unauthorized) */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/chat/conversations/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get a chat conversation with its messages + * @description Только своя беседа: чужая и несуществующая неотличимы, обе — 404 (not_found). + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Conversation with messages */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + conversation?: { + /** Format: date-time */ + created_at?: string; + /** Format: uuid */ + id?: string; + model?: string; + title?: string; + /** Format: date-time */ + updated_at?: string; + /** Format: uuid */ + user_id?: string; + }; + messages?: { + content?: string; + /** Format: uuid */ + conversation_id?: string; + /** Format: date-time */ + created_at?: string; + /** Format: int64 */ + id?: number; + /** @enum {string} */ + role?: "user" | "assistant"; + }[]; + }; + }; + }; + /** @description id не UUID (invalid_request) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No session (unauthorized) */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Чат не найден или принадлежит другому клиенту (not_found) */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + /** + * Delete a chat conversation + * @description Требует X-CSRF-Token. Только своя беседа — чужая и несуществующая неотличимы, обе 404. + */ + delete: { + parameters: { + query?: never; + header: { + "X-CSRF-Token": string; + }; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description id не UUID (invalid_request) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No session (unauthorized) */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Чат не найден или принадлежит другому клиенту (not_found) */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + options?: never; + head?: never; + /** + * Update conversation (project binding) + * @description Привязка беседы к RAG-проекту (project_id) или отвязка (пустое значение). Используется потоком «прикрепить файл к чату»: клиент создаёт проект и привязывает его к беседе. Чужая беседа/проект — 404, архивный проект — 409 project_archived. + */ + patch: { + parameters: { + query?: never; + header: { + "X-CSRF-Token": string; + }; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * Format: uuid + * @description Пустая строка/отсутствие — отвязать + */ + project_id?: string; + }; + }; + }; + responses: { + /** @description Привязано */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Чат или проект не найдены */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Проект в архиве (project_archived) */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + trace?: never; + }; + "/api/chat/conversations/{id}/archive": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Archive conversation + * @description Прячет беседу из рабочих списков чата. Полное удаление — DELETE, из архива. + */ + post: { + parameters: { + query?: never; + header: { + "X-CSRF-Token": string; + }; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description В архиве */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Чат не найден */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/chat/conversations/{id}/messages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Append a message to a chat conversation + * @description Ручное сохранение сообщения в историю — POST /api/chat/completions уже сохраняет сообщения user/assistant сам, эта ручка нужна фронту вне обычного цикла генерации. role принимает только user или assistant, system от клиента отклоняется 400 invalid_role. Требует X-CSRF-Token. + */ + post: { + parameters: { + query?: never; + header: { + "X-CSRF-Token": string; + }; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + content: string; + /** @enum {string} */ + role: "user" | "assistant"; + }; + }; + }; + responses: { + /** @description Saved */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Пустое содержимое (invalid_request) или недопустимая роль (invalid_role) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No session (unauthorized) */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Чат не найден или принадлежит другому клиенту (not_found) */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/chat/conversations/{id}/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Restore conversation from archive + * @description Возвращает беседу в рабочие списки (в её проект, если она проектная). + */ + post: { + parameters: { + query?: never; + header: { + "X-CSRF-Token": string; + }; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Восстановлена */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Чат не найден */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/chat/models": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List chat models + * @description Список моделей чата на лендинге. Только для авторизованных: аноним (нет или истекла сессия) получает 401 — страница /chat показывает ему предложение зарегистрироваться. Отдаются только ПЛАТНЫЕ модели с `available_in_chat` (`is_free=false`): каждый запрос чата тарифицируется, бесплатных моделей в чате не бывает. Гейт `chat_enabled` общий для всех `/api/chat/*`: при выключенном администратором чате любой маршрут отвечает 503 `chat_disabled`. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Model list */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data?: { + id?: string; + is_free?: boolean; + name?: string; + }[]; + }; + }; + }; + /** @description Чат отключён администратором (chat_disabled) */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/chat/projects": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List RAG projects + * @description АКТИВНЫЕ RAG-проекты чата текущего клиента с агрегатами файлов (files_total, files_ready — индексация закончена, когда они равны). ?archived=1 отдаёт архивные. Требует сессии (401 без неё). Гейт chat_enabled общий для /api/chat/*. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Project list */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data?: { + /** Format: date-time */ + created_at?: string; + files_ready?: number; + files_total?: number; + /** Format: uuid */ + id?: string; + name?: string; + /** Format: date-time */ + updated_at?: string; + }[]; + }; + }; + }; + }; + }; + put?: never; + /** + * Create RAG project + * @description Создаёт проект для загрузки документов. Не больше 20 проектов на клиента (409 too_many_projects). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + name: string; + }; + }; + }; + responses: { + /** @description Создан, тело {id} */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Достигнут предел проектов (too_many_projects) */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/chat/projects/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete RAG project + * @description Удаляет проект, его файлы и индекс (каскадом), объекты хранилища — по возможности. Беседы проекта остаются, отвязываясь от него (project_id → null). Чужой проект неотличим от несуществующего (404). + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Удалён */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Проект не найден */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + options?: never; + head?: never; + /** + * Rename RAG project + * @description Переименование проекта владельца. Чужой проект — 404. + */ + patch: { + parameters: { + query?: never; + header: { + "X-CSRF-Token": string; + }; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + name: string; + }; + }; + }; + responses: { + /** @description Переименован */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Проект не найден */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + trace?: never; + }; + "/api/chat/projects/{id}/archive": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Archive RAG project + * @description Прячет проект из рабочих списков; беседы и индекс остаются. Полное удаление — DELETE, из архива. + */ + post: { + parameters: { + query?: never; + header: { + "X-CSRF-Token": string; + }; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description В архиве */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Проект не найден */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/chat/projects/{id}/files": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List project files + * @description Файлы проекта со статусом индексации: pending (в очереди), processing, ready (chunks доступны чату), failed (error — причина для клиента). + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description File list */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Проект не найден */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + /** + * Upload project file + * @description multipart/form-data, поле file. Поддерживаются текстовые форматы, docx и pdf (400 unsupported_format иначе), не больше 10 МБ и 50 файлов на проект (409 too_many_files). Файл регистрируется со статусом pending и индексируется фоновой джобой (текст → чанки → эмбеддинги за счёт баланса клиента). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "multipart/form-data": { + /** Format: binary */ + file: string; + }; + }; + }; + responses: { + /** @description Файл зарегистрирован, тело — RagFile */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Поле file обязательно или формат не поддерживается (unsupported_format) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Проект не найден */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Достигнут предел файлов проекта (too_many_files) */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Файл больше 10 МБ (payload_too_large) */ + 413: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Хранилище файлов не настроено (storage_unavailable) или чат отключён (chat_disabled) */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/chat/projects/{id}/files/{fileID}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete project file + * @description Удаляет файл, его чанки из индекса и объект хранилища. Чужой файл неотличим от несуществующего (404). + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + fileID: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Удалён */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Файл не найден */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/chat/projects/{id}/restore": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Restore RAG project from archive + * @description Возвращает проект в рабочие списки. + */ + post: { + parameters: { + query?: never; + header: { + "X-CSRF-Token": string; + }; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Восстановлен */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Проект не найден */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/deposits": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List balance top-ups (deposits history) + * @description История зачислений на основной баланс текущего клиента: платежи картой (deposit), промокоды (promo) и переводы с реферального баланса (referral_spent) — витрина «откуда приходили деньги» на странице пополнений, без списаний и корректировок. from/to — включительные границы; принимается дата YYYY-MM-DD (from — начало суток, to — конец суток) или RFC3339; границы необязательны — без них возвращается вся история. from позже to — 400. limit по умолчанию 100, молча срезается сверху до 100 и возвращается к умолчанию при неположительном значении; offset по умолчанию 0, отрицательный молча прижимается к 0. + */ + get: { + parameters: { + query?: { + from?: string; + to?: string; + limit?: number; + offset?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Deposits page */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data?: { + /** @description Decimal string, RUB, положительное зачисление. */ + amount?: string; + /** @description Остаток счёта сразу после зачисления. */ + balance_after?: string; + comment?: string | null; + /** Format: date-time */ + created_at?: string; + /** @enum {string} */ + type?: "deposit" | "promo" | "referral_spent"; + }[]; + }; + }; + }; + /** @description from/to — не дата, from позже to, limit/offset не целое число (invalid_request) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/keys": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** List API keys */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of API keys */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIKey"][]; + }; + }; + }; + }; + put?: never; + /** Create new API key */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Key name for identification */ + name: string; + /** @description Optional per-key spend limit in RUB (decimal string). Must be set together with spend_window; omit/empty both for no limit. */ + spend_limit_rub?: string; + /** + * @description Optional spend window. Must be set together with spend_limit_rub; omit/empty both for no limit. + * @enum {string} + */ + spend_window?: "day" | "week"; + }; + }; + }; + responses: { + /** @description API key created */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["APIKey"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/keys/models": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List models for the key form + * @description Список ВКЛЮЧЁННЫХ моделей каталога (только id и name) — для чекбоксов формы создания/правки ключа. Тот же источник, что и у публичных витрин (только включённые модели), сортировка по id. Отдельная лёгкая ручка вместо полного GET /api/models: форме не нужны цена, модальность и прочие поля. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Список моделей */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + models?: { + id?: string; + name?: string; + }[]; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/keys/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Revoke API key + * @description Отзывает ключ. Владение проверяется в SQL (WHERE id AND user_id), а не здесь: 404 не различает для клиента чужой ключ и несуществующий — раскрывать существование чужого id незачем. Отозванный ключ хранится ещё 7 дней (см. purge_at в APIKey) и затем удаляется навсегда джобой уборки; история потребления по нему переживает удаление. + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Key revoked */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example revoked */ + status?: string; + }; + }; + }; + /** @description Key not found (unknown, foreign, or already revoked) */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + options?: never; + head?: never; + /** Update API key spend limit */ + patch: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description New spend limit in RUB (decimal string). Set together with spend_window; empty both to disable the limit. */ + spend_limit_rub?: string; + /** + * @description New spend window. Set together with spend_limit_rub; empty both to disable the limit. + * @enum {string} + */ + spend_window?: "day" | "week"; + }; + }; + }; + responses: { + /** @description Limit updated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + status?: string; + }; + }; + }; + /** @description Key not found (unknown, foreign, or revoked) */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + trace?: never; + }; + "/api/keys/{id}/models": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** + * Update key's allowed models + * @description Заменяет список моделей, разрешённых уже выпущенному ключу, — ПОЛНАЯ замена, не патч. Пустой массив или null снимают ограничение (снова доступны все модели каталога). Владение и «не отозван» проверяет сам SQL (WHERE id AND user_id AND revoked_at IS NULL) — 404 не различает чужой/несуществующий/отозванный ключ. Доступность в каталоге проверяется только у моделей, которых на ключе ещё нет: id, выбранный раньше и с тех пор выключенный администратором, разрешено оставить — обслуживания он не даёт, но и стирать выбор клиента при временном отключении модели нельзя. + */ + put: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Список id моделей. Пусто или null — без ограничений (все модели каталога). */ + allowed_models?: string[]; + }; + }; + }; + responses: { + /** @description Allowed models updated */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example updated */ + status?: string; + }; + }; + }; + /** @description В allowed_models есть неизвестный id модели (invalid_request) */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Key not found (unknown, foreign, or revoked) */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/keys/{id}/reissue": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Reissue API key (rotate its secret) + * @description Перевыпускает секрет СУЩЕСТВУЮЩЕГО ключа: строка остаётся той же (id, имя если не меняли, история расхода, накопленная трата окна лимита), но key_hash/key_prefix заменяются — старое значение перестаёт работать сразу. Тело и валидация те же, что у POST /api/keys. Полное новое значение приходит в ответе ровно один раз, как при создании. Доступность в каталоге проверяется только у моделей, которых на ключе ещё нет (carry-over, см. PUT /api/keys/{id}/models). + */ + post: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description id моделей каталога, разрешённых ключу. Отсутствие/пусто — все модели. */ + allowed_models?: string[]; + /** @description Key name for identification (as in create) */ + name: string; + /** @description Spend limit in RUB (decimal string). Must be set together with spend_window; omit/empty both for no limit. */ + spend_limit_rub?: string; + /** + * @description Spend window. Must be set together with spend_limit_rub; omit/empty both for no limit. + * @enum {string} + */ + spend_window?: "day" | "week"; + }; + }; + }; + responses: { + /** @description Key reissued: full new value returned exactly once */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** Format: uuid */ + id?: string; + /** @description Full key visible only in this response */ + key?: string; + name?: string; + prefix?: string; + warning?: string; + }; + }; + }; + /** @description Некорректное тело: пустое имя, лимит без окна, неизвестный id модели (invalid_request) */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Key not found (unknown, foreign, or revoked) */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/ledger": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List ledger entries (transaction log) + * @description Постраничный журнал движений по счёту: пополнения, списания за использование, ручные корректировки администратором, возвраты, промокоды и реферальные начисления/списания (см. enum поля type). limit по умолчанию 20, молча срезается сверху до 100; нечисловое значение — 400, а неположительное молча возвращается к умолчанию (в отличие от завышенного, которое просто зажимается сверху). offset по умолчанию 0, отрицательный молча зажимается в 0. + */ + get: { + parameters: { + query?: { + limit?: number; + offset?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Journal page */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data?: { + /** @description Decimal string, RUB. Знак зависит от типа записи. */ + amount?: string; + /** @description Остаток счёта сразу после этой записи. */ + balance_after?: string; + comment?: string | null; + /** Format: date-time */ + created_at?: string; + /** @enum {string} */ + type?: "deposit" | "usage" | "adjustment" | "refund" | "promo" | "referral_earned" | "referral_spent"; + }[]; + }; + }; + }; + /** @description limit или offset не является целым числом (invalid_request) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get current user profile */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description User profile */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["User"]; + }; + }; + /** @description Not authenticated */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/model-requests": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Request a model + * @description Заводит обращение в поддержку с заявкой на включение модели (tickets.source='model_request'). Ровно одно из полей: model_id — выбор из /api/models/requestable, проверяется по каталогу; model_name — свободный ввод, если нужной модели в каталоге нет вовсе. Тема и текст обращения формирует сервер, чтобы заявки выглядели одинаково в очереди саппорта. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Идентификатор выключенной модели из каталога */ + model_id?: string; + /** @description Название, введённое клиентом, если модели нет в каталоге */ + model_name?: string; + /** @description Необязательный комментарий: зачем нужна модель */ + note?: string; + }; + }; + }; + responses: { + /** @description Request created as a support ticket */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + status?: string; + /** Format: uuid */ + ticket_id?: string; + }; + }; + }; + /** @description Пустой запрос, оба поля разом, слишком длинное значение или неизвестная модель */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No session */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/models": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List models with ruble pricing + * @description Custom endpoint with ruble-based pricing and multipliers + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of models with pricing */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Model"][]; + }; + }; + /** @description Rate limit exceeded: rate_limit_exceeded (client's overall limit) */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/models/requestable": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Models available to request + * @description Модели, которые есть у провайдера, но выключены у нас, — то, что клиент может попросить включить кнопкой «Хочу модель» в кабинете. Снятые с продажи (deprecated) не отдаются: заявка на них заведомо не будет удовлетворена. Цен и служебных признаков каталога ответ не содержит. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Disabled models available for a request */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + models?: { + id?: string; + /** @enum {string} */ + modality?: "text" | "transcription" | "image" | "decision" | "embedding" | "speech"; + name?: string; + }[]; + }; + }; + }; + /** @description No session */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/payments": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List payments history + * @description Последние 50 платежей клиента, новые сначала. Раздел смонтирован только при настроенном эквайринге (404 иначе). + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description История платежей */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data?: components["schemas"]["Payment"][]; + }; + }; + }; + }; + }; + put?: never; + /** + * Create payment (top-up) + * @description Создаёт платёж T-Bank на пополнение баланса и возвращает ссылку на форму оплаты (payment_url). Раздел /api/payments/* монтируется ТОЛЬКО когда эквайринг настроен на сервере — иначе 404 у всех маршрутов этого раздела; GET /api/auth/config сообщает об этом заранее полем payments_enabled, не дожидаясь отказа здесь. Сумма — decimal-строка с точностью до копейки в границах [topup_min; topup_max] из GET /api/auth/config. email обязателен, только если у клиента ещё нет подтверждённого адреса в профиле (вход через Telegram) — на него уходит чек 54-ФЗ. Не более 5 одновременно НЕзавершённых (pending) платежей на клиента — иначе 400 too_many_pending_payments: каждый вызов этой ручки — настоящий Init в банке, а не бесплатная операция. Скорость создания тоже ограничена (по умолчанию 3 в минуту и 5 в час на клиента, настраивается PAYMENT_RATE_LIMIT_*): превышение — 429 payment_rate_limited. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Decimal string, RUB, с точностью до копейки. */ + amount: string; + /** + * Format: email + * @description Обязателен, если у клиента нет подтверждённого email в профиле — на него уйдёт чек 54-ФЗ. + */ + email?: string; + }; + }; + }; + responses: { + /** @description Payment created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Payment"]; + }; + }; + /** @description Сумма вне допустимых границ (amount_out_of_range), точность не до копейки (amount_precision), не указан email при его отсутствии в профиле (email_required), некорректный email (email_invalid), или уже 5 незавершённых платежей (too_many_pending_payments) */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Скоростной лимит создания платежей исчерпан (payment_rate_limited) — по умолчанию 3 в минуту и 5 в час на клиента; счётчик пишется на каждый вызов создания, независимо от оплаты */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Банк недоступен или отказал (payment_gateway_error) — сообщение банка не пробрасывается, в нём бывают внутренние коды */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/payments/test": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create test payment + * @description Настоящий платёж T-Bank на минимальную сумму с признаком is_test=true — тестовый контур банка проверяется тем же кодом, что и боевой, отдельной эмуляции нет. Монтируется, только когда явно включён флаг тестовых платежей на сервере (см. test_payment_enabled в GET /api/auth/config) — на боевом терминале маршрута нет вовсе (404). Тот же лимит незавершённых платежей (5), что у обычного создания. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Test payment created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Payment"]; + }; + }; + /** @description too_many_pending_payments */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description payment_gateway_error */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/payments/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get payment status + * @description Статус одного платежа. Пока он pending, ручка опрашивает банк, но не чаще раза в 3 секунды на платёж — несколько открытых вкладок клиента не должны превращаться в поток запросов к эквайрингу; недоступность банка не портит ответ (отдаётся последний известный статус, джоба сверки доведёт платёж до конца отдельно). Чужой платёж — тоже 404, как и несуществующий: посторонний не должен узнать сам факт существования id. Раздел смонтирован только при настроенном эквайринге. + */ + get: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Payment */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Payment"]; + }; + }; + /** @description Платёж не найден, id некорректен, либо принадлежит другому клиенту */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/profile/notifications": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get notification settings */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Notification settings */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["NotificationSettings"]; + }; + }; + }; + }; + /** Update notification settings */ + put: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["NotificationSettings"]; + }; + }; + responses: { + /** @description Settings saved */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + status?: string; + }; + }; + }; + /** @description Invalid channel, or channel requires an unavailable delivery method (email not verified / telegram not linked) */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/profile/notifications/test": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Send test notification + * @description Отправляет тестовое сообщение по каналу(ам), сохранённым настройками уведомлений (email/telegram/both), — проверка, что канал действительно работает, а не только сохранён. Частичный успех при channel=both (например, telegram доставился, а email не подтверждён) отвечает 200 с непустым errors; 400 — только если НИ один канал не сработал. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Отправлено хотя бы по одному каналу */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Причины отказа по остальным каналам — присутствуют, даже если общий статус успешен (частичная доставка при channel=both). */ + errors?: string[]; + /** @description Каналы, по которым сообщение реально ушло — из "email"/"telegram". */ + sent?: string[]; + /** @example ok */ + status?: string; + }; + }; + }; + /** @description Не удалось отправить ни по одному каналу (notification_failed) — email не подтверждён/не задан, telegram не привязан, либо telegram-уведомления не настроены на сервере */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/profile/password": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Change password + * @description Меняет пароль и обесценивает ВСЕ сессии пользователя, включая текущую (клиент увидит «нужен повторный вход»), а заодно гасит промежуточные токены входа (login_challenges, SEC-17): первый шаг входа мог быть пройден секунду назад ещё старым паролем, и без этого второй шаг (код TOTP) довершил бы вход уже после смены пароля. 400 no_password — у аккаунта нет пароля вовсе (вход только через Telegram); тогда пароль сначала задаётся отдельной кнопкой на странице входа, а не этим маршрутом. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Новый пароль. */ + new: string; + /** @description Текущий пароль. */ + old: string; + }; + }; + }; + responses: { + /** @description Пароль изменён */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + status?: string; + }; + }; + }; + /** @description У аккаунта нет пароля (no_password), либо новый пароль не проходит требования к сложности (invalid_password) */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Текущий пароль указан неверно (invalid_credentials) */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/profile/telegram/link": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Link Telegram account + * @description Привязывает Telegram к уже вошедшему аккаунту. Тело — тот же набор полей от Telegram Login Widget, что и при входе (POST /api/auth/telegram), подпись HMAC-SHA256 проверяется обязательно: без неё любой вошедший клиент мог бы приписать себе чужой telegram_id. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + [key: string]: string; + }; + }; + }; + responses: { + /** @description Привязан */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example linked */ + status?: string; + }; + }; + }; + /** @description Подпись Telegram не подтвердилась (telegram_auth_failed) */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description К аккаунту уже привязан другой Telegram (already_linked), либо этот telegram-аккаунт уже привязан к другому клиенту (telegram_already_linked) */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Вход через Telegram не настроен на сервере (telegram_not_configured) */ + 503: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/profile/telegram/unlink": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Unlink Telegram account + * @description Отвязывает Telegram и гасит только telegram-сессии — сессии по паролю/passkey остаются живыми. Требует другого способа входа: без пароля и passkey клиент потерял бы доступ к аккаунту вовсе (400 cannot_unlink). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Отвязан */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example unlinked */ + status?: string; + }; + }; + }; + /** @description У аккаунта нет ни пароля, ни passkey — отвязывать Telegram нечем (cannot_unlink) */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/promo/apply": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Redeem promo code + * @description Активирует промокод и зачисляет фиксированную сумму, обещанную в момент выпуска (сумма и сам код не меняются никогда — см. инвариант промокодов). Лимит 10 запросов в минуту НА ПОЛЬЗОВАТЕЛЯ (не на IP, SEC-11): «человеческие» админские коды короче автогенерированных и перебираемы словарём. 410 Gone — тремя разными кодами ошибки, но одним статусом: истёкший, исчерпанный и отключённый промокод одинаково больше недействителен. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + code: string; + }; + }; + }; + responses: { + /** @description Активирован */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + balance_after?: string; + credit_amount?: string; + }; + }; + }; + /** @description Пустой код (empty_code) */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Промокод не найден (promo_not_found) */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Клиент уже активировал этот промокод (promo_already_redeemed) */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Промокод больше недействителен: истёк срок (promo_expired), исчерпан лимит активаций (promo_exhausted) или отключён администратором (promo_disabled) */ + 410: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Превышен лимит попыток — 10 в минуту на пользователя (rate_limit_exceeded) */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/referral": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get referral program info + * @description Код, баланс, ставка и история начислений/приглашённых текущего клиента. 404, если реферальная программа выключена в настройках — неотличимо от несуществующего раздела. E-mail приглашённого в earnings[].referral_email уже замаскирован на стороне SQL (например j***@gmail.com, SEC-28) — полный адрес сюда не попадает никогда. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Referral info */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + earnings?: { + amount?: string; + created_at?: string; + /** @description Замаскированный адрес приглашённого. */ + referral_email?: string | null; + }[]; + referral_balance?: string; + /** @description Отсутствует, пока не выпущен POST /api/referral/generate-code. */ + referral_code?: string | null; + /** @description Текущая ставка начисления, проценты. */ + referral_percent?: string; + referrals?: { + created_at?: string; + email?: string | null; + }[]; + }; + }; + }; + /** @description Реферальная программа выключена (not_found) */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/referral/generate-code": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Generate referral code + * @description Выпускает персональный реферальный код клиента. 404 при выключенной программе — как у GET /api/referral. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Code issued */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + referral_code?: string; + }; + }; + }; + /** @description Реферальная программа выключена (not_found) */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/referral/use": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Move referral balance to main balance + * @description Списывает сумму с реферального баланса и зачисляет её на основной. Перевести можно весь остаток реферального баланса целиком — ограничение процентом (referral_max_use_percent) убрано миграцией 00022. 404 при выключенной программе. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Сумма к переводу, decimal-строка. Положительная и не выше текущего реферального баланса. */ + amount: string; + }; + }; + }; + responses: { + /** @description Перевод выполнен */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description Остаток реферального баланса после перевода. */ + referral_balance?: string; + }; + }; + }; + /** @description Некорректная или неположительная сумма (invalid_request), либо она превышает текущий реферальный остаток (insufficient_referral_balance) */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Реферальная программа выключена (not_found) */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/reviews": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Submit a review + * @description Отправляет отзыв о сервисе на модерацию. Допуск: роль client, подтверждённая почта, хоть один настоящий платёж или запрос к модели, нет активного отзыва (на модерации, опубликованного или скрытого). Отправка формы с consent=true — согласие на публикацию. Лимит частоты попыток: 3 в час, 5 в сутки. Только из сессии кабинета; под имперсонацией отклоняется. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + author_name: string; + body: string; + /** @description Согласие на публикацию, обязано быть true */ + consent: boolean; + rating: number; + }; + }; + }; + responses: { + /** @description Review created, awaiting moderation */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CabinetReview"]; + }; + }; + /** @description invalid_body, consent_required, invalid_rating, body_too_short, body_too_long, invalid_author_name */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description No session */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description review_not_allowed, reviews_disabled (приём новых отзывов выключен администратором), review_email_unverified, review_no_history, session_required или impersonation_read_only */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description review_exists — уже есть активный отзыв */ + 409: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description review_rate_limited */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/reviews/me": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * My review state + * @description Последний отзыв клиента (или null), вознаграждение за отзыв, допуск к отправке нового и границы полей формы. Вознаграждение выдаётся один раз на клиента за всё время: status=paid сохраняется и после удаления отзыва. Суммы — строками. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Review state */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + eligibility?: { + can_submit?: boolean; + /** @enum {string|null} */ + reason?: "email_not_verified" | "no_history" | "not_allowed" | "reviews_disabled" | "active_review_exists" | null; + }; + limits?: { + author_name_max?: number; + author_name_min?: number; + body_max?: number; + body_min?: number; + }; + review?: components["schemas"]["CabinetReview"]; + reward?: { + /** @description Текущая сумма вознаграждения из настроек, ₽ */ + amount?: string; + paid_amount?: string | null; + /** Format: date-time */ + paid_at?: string | null; + /** @enum {string} */ + status?: "none" | "pending" | "paid"; + }; + }; + }; + }; + /** @description No session */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + /** + * Withdraw my review + * @description Отзыв согласия на публикацию: удаляет все отзывы клиента. Выплаченное вознаграждение остаётся. Только из сессии кабинета; под имперсонацией отклоняется. + */ + delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Reviews deleted */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + deleted?: number; + }; + }; + }; + /** @description No session */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description session_required (API-ключ) или impersonation_read_only */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/security/passkeys": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List passkeys + * @description Список ключей доступа, привязанных к аккаунту. Секретные поля (CredentialID, PublicKey, SignCount) не отдаются никогда. Пустой массив, если ключей нет, — не null. 404, если контур passkey выключен на сервере. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Список */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data?: { + /** Format: date-time */ + created_at?: string; + /** Format: uuid */ + id?: string; + /** Format: date-time */ + last_used_at?: string | null; + name?: string; + }[]; + }; + }; + }; + /** @description Контур passkey выключен на сервере */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/security/passkeys/begin": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Begin passkey registration + * @description Начинает церемонию привязки НОВОГО ключа к уже вошедшему клиенту. Администратору требует, чтобы ТЕКУЩАЯ сессия сама была получена вторым фактором (403 second_factor_required) — иначе один пароль в обход обязательности 2FA позволил бы привязать passkey и получить auth_method=passkey, то есть полный доступ в админку. Смонтирован только при включённом контуре passkey. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description WebAuthn PublicKeyCredentialCreationOptions — передаётся в navigator.credentials.create() без переупаковки. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + /** @description second_factor_required — текущая сессия администратора не предъявляла второй фактор */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Контур passkey выключен на сервере */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/security/passkeys/finish": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Finish passkey registration + * @description Завершает привязку. Имя устройства и код TOTP передаются В ТЕЛЕ, а не в заголовках или query-string: ingress/nginx/CDN по умолчанию пишут в access-лог полный request URI, а разовый код второго фактора попадать в логи не должен (инвариант проекта). Код TOTP обязателен, если у аккаунта есть подтверждённая привязка (тем же приёмом, что и снятие TOTP), проверяется ДО церемонии WebAuthn — неверный код не должен сжигать challenge. Тело ограничено 64 КиБ. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Код TOTP — обязателен, только если у аккаунта есть подтверждённая привязка. */ + code?: string; + /** @description Ответ аутентификатора из navigator.credentials.create(), как есть. */ + credential: Record; + /** @description Имя устройства, до 100 символов. */ + name: string; + }; + }; + }; + responses: { + /** @description Ключ привязан */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example ok */ + status?: string; + }; + }; + }; + /** @description Пустое/слишком длинное имя устройства (invalid_request), неверный код TOTP (invalid_code), истёкший challenge (challenge_expired) или сбой церемонии WebAuthn (passkey_registration_failed) */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description second_factor_required */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Контур passkey выключен на сервере */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Тело больше 64 КиБ (request_too_large) */ + 413: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Слишком много попыток кода второго фактора: не больше 10 в минуту на клиента */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/security/passkeys/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Delete passkey + * @description Снятие фактора аутентификации обязано подтверждаться уже имеющимся вторым фактором (SEC-18): тело необязательно, но если у клиента есть подтверждённая привязка TOTP, без корректного code в теле вернётся 400 invalid_code. Администратору снять ПОСЛЕДНИЙ оставшийся фактор так нельзя (409) — клиенту можно: второй фактор для роли client не обязателен. Владение проверяется в SQL — чужой и несуществующий ключ неразличимы, оба 404. + */ + delete: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": { + /** @description Код TOTP — обязателен, только если у аккаунта есть подтверждённая привязка. */ + code?: string; + }; + }; + }; + responses: { + /** @description Удалён */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example revoked */ + status?: string; + }; + }; + }; + /** @description Неверный или отсутствующий код TOTP (invalid_code), когда он требуется */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Администратор без второго фактора в текущей сессии пытается снять ключ (second_factor_required) */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Ключ не найден (чужой, несуществующий), либо контур passkey выключен */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Это последний второй фактор администратора — снять из веба нельзя (last_second_factor) */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Слишком много попыток кода второго фактора: не больше 10 в минуту на клиента */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/security/status": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get security status + * @description Состояние второго фактора аккаунта и ТЕКУЩЕЙ СЕССИИ раздельно: totp_enabled/passkey_count/role — свойства учётной записи (что заведено и кем она является), second_factor — предъявлялся ли второй фактор именно на этом входе. Разница не косметическая: администратор с включённым TOTP, вошедший через Telegram или одним паролем, даёт totp_enabled=true при second_factor=false, и именно второе поле решает допуск в веб-админку. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Security status */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + passkey_count?: number; + /** @enum {string} */ + role?: "client" | "admin"; + /** @description Предъявлялся ли второй фактор на ТЕКУЩЕМ входе (не путать с totp_enabled). */ + second_factor?: boolean; + totp_enabled?: boolean; + }; + }; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/security/totp": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + post?: never; + /** + * Remove TOTP + * @description Снимает привязку и сразу понижает способ входа у всех живых сессий пользователя: сессия с auth_method=password+totp иначе продолжала бы предъявлять фактор, которого уже нет, до истечения 30-суточного TTL. Требует ТЕКУЩИЙ код в теле — угнанная сессия не может снять фактор без него. У НЕподтверждённой привязки код не требуется (она ничего не защищала). Администратор не может снять последний фактор (409) — клиент может остаться вовсе без второго фактора, он для роли client не обязателен. + */ + delete: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Не требуется для неподтверждённой привязки — тогда передайте пустую строку. */ + code: string; + }; + }; + }; + responses: { + /** @description Снято */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example true */ + ok?: boolean; + }; + }; + }; + /** @description Привязки нет (totp_not_enabled) или неверный код (invalid_code) */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description second_factor_required */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Это последний второй фактор администратора (last_second_factor) */ + 409: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description Слишком много попыток кода второго фактора: не больше 10 в минуту на клиента */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/security/totp/confirm": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Confirm TOTP enrollment + * @description Подтверждает секрет, выпущенный POST /api/security/totp/setup, шестизначным кодом. Занимает временной шаг сразу при успехе — тот же код нельзя предъявить повторно как код входа. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @example 123456 */ + code: string; + }; + }; + }; + responses: { + /** @description Подтверждено */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @example true */ + ok?: boolean; + }; + }; + }; + /** @description Привязка не начата (totp_not_started) или неверный код (invalid_code) */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description second_factor_required */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Слишком много попыток кода второго фактора: не больше 10 в минуту на клиента */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/security/totp/setup": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Start TOTP enrollment + * @description Создаёт неподтверждённый секрет и единственный раз отдаёт его вместе с otpauth-ссылкой для приложения-аутентификатора — прочитать секрет обратно нельзя больше никак: он хранится зашифрованным (UPSTREAM_KEY_ENCRYPTION_KEY). Повторный вызов до подтверждения затирает прежний неподтверждённый секрет. На УЖЕ подтверждённой привязке отвечает 400 — снятие второго фактора выполняется через DELETE /api/security/totp с текущим кодом, а не повторным setup. Администратору требует второй фактор в текущей сессии. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Секрет создан */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + otpauth_url?: string; + secret?: string; + }; + }; + }; + /** @description У аккаунта уже есть подтверждённая привязка TOTP (totp_already_enabled) */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description second_factor_required */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/tickets": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List own support tickets + * @description Newest first by updated_at, page-limited (SEC-32): limit defaults to 50 and is capped at 200 regardless of the value requested. Response carries total (all tickets under the filter, not just the current page) alongside tickets. + */ + get: { + parameters: { + query?: { + limit?: number; + offset?: number; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Page of the user's tickets */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + /** + * Create a support ticket + * @description multipart/form-data with fields: subject, message, and up to 3 image attachments (attachments[], each ≤10 MB, image/*). Notifies admins via Telegram. Лимит обращений на клиента: TICKET_RATE_LIMIT_PER_HOUR в час и TICKET_RATE_LIMIT_PER_DAY в сутки, сверх — 429 ticket_rate_limited. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ticket created */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid subject/message or attachment */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Тело запроса больше допустимого: не больше трёх файлов по 10 МБ */ + 413: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Превышен лимит обращений в час или сутки (ticket_rate_limited) */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/tickets/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get ticket with messages and attachments */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ticket detail with message thread */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Ticket not found or belongs to another user */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/tickets/{id}/attachments/{att_id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Download a ticket attachment + * @description Proxies the object from object storage; ownership of the ticket is verified before streaming. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Image bytes (inline) */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Attachment or ticket not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Object storage not configured */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/tickets/{id}/close": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Close own ticket */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Ticket closed */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Ticket not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/tickets/{id}/messages": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Reply to own ticket + * @description multipart/form-data with message and optional attachments[]. Moves ticket back to 'open'. Notifies admins via Telegram. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Reply added */ + 201: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Ticket not found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Тело запроса больше допустимого: не больше трёх файлов по 10 МБ */ + 413: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/api/usage": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get usage statistics */ + get: { + parameters: { + query?: { + /** @description Start date (RFC3339) */ + from?: string; + /** @description End date (RFC3339) */ + to?: string; + /** @description Aggregation dimension. Default: model. "key_model" is two-dimensional — it breaks each key down by the models called with it, so its rows carry BOTH key_name and model, and their per-key sums match the corresponding "key" row exactly. */ + group_by?: "day" | "model" | "key" | "key_model"; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Usage statistics */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data?: { + /** @description Money is a string, never a number. */ + amount_rub?: string; + /** @description Present for group_by=day. */ + day?: string; + /** @description Key identifier for group_by=key and key_model. Absent when the events carry no key (cabinet chat) or the key was deleted; those two cases are told apart by key_name. */ + key_id?: string; + /** @description Present for group_by=key and group_by=key_model. Not unique — use key_id to identify a row. */ + key_name?: string; + /** @description Present for group_by=model and group_by=key_model. */ + model?: string; + requests?: number; + /** Format: int64 */ + tokens_cached?: number; + /** Format: int64 */ + tokens_completion?: number; + /** Format: int64 */ + tokens_prompt?: number; + }[]; + /** @description Events whose cost reconciliation did not finish in time. Deliberately carries no ruble amount: a failed event has no trustworthy price, and showing zero would read as "free". */ + failed_events?: number; + /** Format: date-time */ + from?: string; + group_by?: string; + /** Format: date-time */ + to?: string; + }; + }; + }; + /** @description Unknown group_by, or from later than to */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/healthz": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Health check + * @description Returns 200 if service is healthy + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Service is healthy */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/audio/speech": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Synthesize speech + * @description OpenAI-compatible text-to-speech endpoint. Returns the synthesized audio as a binary body (audio/mpeg for mp3, audio/pcm for pcm). Streaming (stream: true) and input_references are not supported and are rejected with 400. Charged in rubles from the cost reported by the provider: the catalog does not publish a per-unit price for speech models because the provider does not state its unit (see the modality field of GET /api/models). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * @description Text to synthesize. Its length is limited by the service configuration; a longer text is rejected with 400. + * @example Привет, мир + */ + input: string; + /** @description Style instructions for the voice. For most providers they are passed as provider.options..instructions — see the model documentation. */ + instructions?: string; + /** @description Speech model id. Must be a model whose modality is "speech" in GET /api/models. */ + model: string; + /** @description Provider routing and provider-specific options, forwarded to the provider unchanged. */ + provider?: { + [key: string]: unknown; + }; + /** + * @description Audio format: "mp3" (audio/mpeg) or "pcm" (raw 16-bit little-endian PCM, audio/pcm). OpenRouter defaults to pcm; this service substitutes mp3 when the field is absent, for compatibility with the OpenAI SDKs. + * @default mp3 + * @enum {string} + */ + response_format?: "mp3" | "pcm"; + /** @description Playback speed multiplier, if the model supports it. */ + speed?: number; + /** + * @description Voice name. The set of voices depends on the model. + * @example alloy + */ + voice?: string; + }; + }; + }; + responses: { + /** @description Synthesized audio. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "audio/mpeg": string; + "audio/pcm": string; + }; + }; + /** @description invalid_request — the body is not readable JSON, model or input is missing, input exceeds the configured length, response_format is not mp3/pcm, or stream / input_references were sent (not supported). */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description invalid_api_key — missing or unknown API key. */ + 401: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description insufficient_balance — the account balance is at or below zero. Top up in the cabinet. */ + 402: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description account_blocked — the account is not active. model_not_allowed — the key has an allowed_models list and this model is not on it. */ + 403: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description model_not_found — the model is unknown, disabled in the catalog, or is not a speech model. */ + 404: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description request_too_large — the request body is above the size cap. */ + 413: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description rate_limit_exceeded — your overall request limit. spend_limit_exceeded — the spend limit of the API key in its current window. Free speech models additionally fall under the free-model limits. pending_settlement — previous speech synthesis requests have not been settled yet; retry after they are. */ + 429: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + /** @description upstream_error — every upstream key was tried and failed, or the provider returned an unusable response. */ + 502: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Error"]; + }; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/audio/transcriptions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Transcribe audio + * @description OpenAI-compatible speech-to-text endpoint. Accepts either a multipart/form-data file upload (what the OpenAI SDKs send) or a JSON body with base64-encoded audio in input_audio — use JSON for files above the 25 MB multipart limit. Streaming is not supported. Charged in rubles from the cost reported by the provider; note that for whisper-class models the provider prices by audio duration rather than by tokens, so the price_prompt_rub_1m field of GET /api/models does not apply to them (see the modality field there). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + input_audio: { + /** @description Base64-encoded audio. */ + data: string; + /** + * @description Audio container format. + * @example wav + */ + format: string; + }; + /** + * @description Language of the input audio (ISO-639-1). Auto-detected when omitted. + * @example ru + */ + language?: string; + /** + * @description Transcription model id. + * @example openai/whisper-large-v3 + */ + model: string; + /** @enum {string} */ + response_format?: "json" | "verbose_json"; + }; + "multipart/form-data": { + /** + * Format: binary + * @description Audio file. The format is derived from the filename extension or the part's content type. Up to 25 MB; send larger audio as base64 JSON. + */ + file: string; + /** + * @description Language of the input audio (ISO-639-1). Auto-detected when omitted. + * @example ru + */ + language?: string; + /** + * @description Transcription model id. Must be a model whose modality is "transcription" in GET /api/models. + * @example openai/whisper-large-v3 + */ + model: string; + /** + * @description "json" (default) returns { text, usage }; "verbose_json" additionally returns task, language, duration and segment timestamps. + * @enum {string} + */ + response_format?: "json" | "verbose_json"; + /** @description Sampling temperature. */ + temperature?: number; + }; + }; + }; + responses: { + /** @description Transcription result */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + /** @description The transcribed text. */ + text?: string; + usage?: { + cost?: number; + input_tokens?: number; + output_tokens?: number; + /** @description Duration of the input audio. */ + seconds?: number; + total_tokens?: number; + }; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Insufficient balance */ + 402: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Model not found, disabled, or not a transcription model */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Request body too large */ + 413: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Body is neither multipart/form-data nor application/json */ + 415: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded: rate_limit_exceeded (client's overall limit), free_rate_limit_exceeded (client's limit on free models), or free_quota_exhausted (installation-wide free-model budget exhausted) */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/auth/key": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Get key balance and spend limit + * @description How much can still be spent with the calling API key. The balance field is the MINIMUM of the account balance and the key's remaining spend limit — a key with a 100 ₽ daily limit that already spent 5 ₽ reports 95 ₽ even on a well-funded account, and reports the account balance when that is the smaller of the two. Compare balance with account_balance to see which one is binding. All money is returned as strings, never numbers. Authenticated by the API key only (a cabinet session has no key to report on). + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Key balance and limit */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data?: { + /** @description Whole account balance in rubles, not capped by the key limit. */ + account_balance?: string; + /** @description Spendable right now, in rubles: min(account_balance, limit_remaining). Never negative — an overspent limit or a negative account reports 0. */ + balance?: string; + /** @description Key name set by its owner. Never the key itself. */ + label?: string; + /** @description Spend limit of the key in rubles. Absent when no limit is set — absent, not zero, which would read as an exhausted limit. */ + limit?: string; + /** @description What is left of the limit in the current window, clamped at 0. Absent when no limit is set. */ + limit_remaining?: string; + /** + * Format: date-time + * @description When the window restarts and usage returns to zero. + */ + limit_resets_at?: string; + /** @enum {string} */ + limit_window?: "day" | "week"; + /** @description Already spent in the current window, in rubles. Reported as actually measured, so it can exceed limit: the last request of a window is allowed to cross it by its own cost. */ + usage?: string; + }; + }; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/chat/completions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create chat completion + * @description OpenAI-compatible chat completion endpoint. Returns charged completion with pricing in rubles. В usage ответа возвращается стоимость ЭТОГО запроса: cost_rub — сумма, списанная с баланса (та же, что в выписке), и cost — она же в долларах по курсу ЦБ, для клиентов, считающих расход по стандартному полю. Стоимость запроса у провайдера наружу не отдаётся: клиент платит по нашему прайсу, и это разные величины. При stream=true обе приходят в финальном чанке рядом с токенами. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["ChatCompletionRequest"]; + }; + }; + responses: { + /** @description Successful completion */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ChatCompletionResponse"]; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Insufficient balance */ + 402: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded: rate_limit_exceeded (client's overall limit), free_rate_limit_exceeded (client's limit on free models), or free_quota_exhausted (installation-wide free-model budget exhausted) */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/completions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create legacy completion + * @description OpenAI-совместимый legacy Completions API (prompt вместо messages) — тот же обработчик, что и у POST /v1/chat/completions и POST /v1/fim/completions, маршрутизируемый на путь /completions у OpenRouter. На этой стороне разбираются только model и stream (чтобы найти модель в каталоге и посчитать стоимость), остальное тело пересылается upstream как есть. Ответ провайдера возвращается без изменений байт в байт — включая форму choices (обычно choices[].text, а не .message, как у chat/completions). + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Maximum tokens in response. */ + max_tokens?: number; + /** + * @description Model ID from GET /v1/models. + * @example gpt-3.5-turbo-instruct + */ + model: string; + /** @description Text prompt to complete. */ + prompt: string; + /** + * @description Enable streaming responses. + * @default false + */ + stream?: boolean; + temperature?: number; + }; + }; + }; + responses: { + /** @description Successful completion — ответ провайдера в исходной форме, проброшен без изменений. */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": Record; + }; + }; + /** @description Malformed request body */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Insufficient balance */ + 402: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Model not in the key's allowed_models list (model_not_allowed) */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Model not found or disabled */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded: rate_limit_exceeded (client's overall limit), free_rate_limit_exceeded (client's limit on free models), or free_quota_exhausted (installation-wide free-model budget exhausted) */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/embeddings": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create embeddings + * @description OpenAI-compatible embeddings endpoint. Converts text into a numeric vector that measures semantic similarity — document search, RAG, clustering, recommendations. Served by OpenRouter only (no other provider offers embeddings), and the body is forwarded to the provider unchanged: input accepts a string or an array of strings (one vector per element, billed per input token), dimensions is understood only by models that declare it (e.g. openai/text-embedding-3-*). Streaming is not supported and is rejected with 400 — a vector arrives as a single JSON body. Charged in rubles from catalog prompt-token prices (completion tokens do not exist for embeddings); usage.cost in the response carries the charged amount (USD equivalent) and usage.cost_rub the exact ruble charge, same as chat completions. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** @description Requested vector size. Only models that support Matryoshka truncation accept it (openai/text-embedding-3-*); others reject it. */ + dimensions?: number; + /** + * @default float + * @enum {string} + */ + encoding_format?: "float" | "base64"; + /** + * @description Text to embed: a string or an array of strings. The response data[] array lines up with the input order. + * @example [ + * "first document", + * "second document" + * ] + */ + input: unknown; + /** + * @description Embedding model id. Must be a model whose modality is "embedding" in GET /api/models. + * @example openai/text-embedding-3-small + */ + model: string; + }; + }; + }; + responses: { + /** @description Vectors in OpenAI format: {object: "list", data: [{object: "embedding", index, embedding: [floats]}], model, usage: {prompt_tokens, total_tokens}}. usage.cost (USD) and usage.cost_rub (exact ruble charge, string) are added by this proxy. */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description invalid_request — the body is not readable JSON, or stream: true was requested (embeddings have no streaming). */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description invalid_api_key — missing or unknown API key. */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description insufficient_balance — the account balance is at or below zero. Top up in the cabinet. */ + 402: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description account_blocked — the account is not active. model_not_allowed — the key has an allowed_models list and this model is not on it. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description model_not_found — the model is unknown, disabled in the catalog, or is not an embedding model. A text model sent here fails with this code before the request leaves the proxy: there is no route for it at the provider, and forwarding it would surface as an upstream failure that never happened. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description rate_limit_exceeded — your overall request limit across all routes. spend_limit_exceeded — the spend limit of the API key in its current window. Free embedding models additionally fall under the free-model limits. upstream_rate_limited — the provider itself throttled the account; Retry-After is then set from the provider's own hint. */ + 429: { + headers: { + /** @description Seconds to wait, present on upstream_rate_limited. */ + "Retry-After"?: number; + [name: string]: unknown; + }; + content?: never; + }; + /** @description internal_error — a failure on our side (catalog, settings or exchange rate unreadable). Nothing was charged. */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description upstream_error — every upstream key was tried and failed, or the provider returned an unusable response (including a body without usage — billing would be blind). There is no fallback provider for this route: embeddings exist only at OpenRouter. */ + 502: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description upstream_unavailable — no upstream key could be selected for the provider at all. */ + 503: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description upstream_error — the provider did not answer in time. Nothing was charged: billing happens after the response is parsed. */ + 504: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/fim/completions": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Create FIM completion + * @description Fill-In-the-Middle endpoint for code autocomplete (e.g. mistralai/codestral-2508). The body carries prompt (code before the cursor) and an optional suffix (code after the cursor); the model returns the completion in between. Routed directly to the Mistral API — OpenRouter does not proxy FIM (its suffix field is stripped). Charged per token from the model's catalog price, in rubles. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * @description Model id with FIM support, e.g. "mistralai/codestral-2508". + * @example mistralai/codestral-2508 + */ + model: string; + /** + * @description Code text before the cursor. + * @example func main() { + */ + prompt: string; + /** + * @description Stream the response as SSE chunks. + * @example false + */ + stream?: boolean; + /** + * @description Optional code text after the cursor. When omitted, the model performs plain completion from the prompt. + * @example } + */ + suffix?: string; + }; + }; + }; + responses: { + /** @description Successful FIM completion */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ChatCompletionResponse"]; + }; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Insufficient balance */ + 402: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded: rate_limit_exceeded (client's overall limit), free_rate_limit_exceeded (client's limit on free models), or free_quota_exhausted (installation-wide free-model budget exhausted) */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/images": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Generate images (alias) + * @description Псевдоним POST /v1/images/generations — тот же обработчик, то же тело запроса и ответа (см. описание там). Путь используют клиенты, читавшие документацию OpenRouter напрямую, а не OpenAI SDK, у которых в ходу /v1/images/generations. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * @description Aspect ratio, e.g. "16:9", "1:1", "auto". Providers clamp to their supported subset. + * @example 16:9 + */ + aspect_ratio?: string; + /** @description Reference images for image-to-image generation, as base64 data URLs or HTTP(S) URLs. Billed by the provider as a separate line. */ + input_references?: Record[]; + /** + * @description Image generation model id. Must be a model whose modality is "image" in GET /api/models. + * @example bytedance-seed/seedream-5-0-pro + */ + model: string; + /** @description Upper bound on the number of images (1-10). Providers may return fewer; some reject n > 1. */ + n?: number; + /** @enum {string} */ + output_format?: "png" | "jpeg" | "webp" | "svg"; + /** + * @description Text description of the desired image. + * @example a red panda astronaut floating in space, studio lighting + */ + prompt: string; + /** @enum {string} */ + quality?: "auto" | "low" | "medium" | "high"; + /** + * @description Resolution tier. Replaces the OpenAI size field, which the provider does not understand. + * @enum {string} + */ + resolution?: "512" | "1K" | "2K" | "4K"; + }; + }; + }; + responses: { + /** @description Generated images */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + created?: number; + data?: { + /** @description Base64-encoded image bytes. */ + b64_json?: string; + /** @example image/png */ + media_type?: string; + }[]; + usage?: { + completion_tokens?: number; + cost?: number; + prompt_tokens?: number; + total_tokens?: number; + }; + }; + }; + }; + /** @description Malformed body, or stream: true (streaming is not supported) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Insufficient balance */ + 402: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Model not found, disabled, or not an image model */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded: rate_limit_exceeded (client's overall limit), free_rate_limit_exceeded (client's limit on free models), or free_quota_exhausted (installation-wide free-model budget exhausted) */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/images/generations": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Generate images + * @description OpenAI-compatible image generation endpoint. The same handler is also served at POST /v1/images, the path used by the OpenRouter documentation. The request body is forwarded to the provider unchanged: note that the OpenAI size field ("1024x1024") is not understood — use resolution ("512"/"1K"/"2K"/"4K") together with aspect_ratio instead. Streaming (stream: true) is not supported and is rejected with 400. Images are returned base64-encoded in data[].b64_json. Charged in rubles from the cost reported by the provider, which already accounts for n, high-resolution mode and any input references — the catalog price shown by GET /api/models is the base per-unit tariff, not the price of an arbitrary request. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": { + /** + * @description Aspect ratio, e.g. "16:9", "1:1", "auto". Providers clamp to their supported subset. + * @example 16:9 + */ + aspect_ratio?: string; + /** @description Reference images for image-to-image generation, as base64 data URLs or HTTP(S) URLs. Billed by the provider as a separate line. */ + input_references?: Record[]; + /** + * @description Image generation model id. Must be a model whose modality is "image" in GET /api/models. + * @example bytedance-seed/seedream-5-0-pro + */ + model: string; + /** @description Upper bound on the number of images (1-10). Providers may return fewer; some reject n > 1. */ + n?: number; + /** @enum {string} */ + output_format?: "png" | "jpeg" | "webp" | "svg"; + /** + * @description Text description of the desired image. + * @example a red panda astronaut floating in space, studio lighting + */ + prompt: string; + /** @enum {string} */ + quality?: "auto" | "low" | "medium" | "high"; + /** + * @description Resolution tier. Replaces the OpenAI size field, which the provider does not understand. + * @enum {string} + */ + resolution?: "512" | "1K" | "2K" | "4K"; + }; + }; + }; + responses: { + /** @description Generated images */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + created?: number; + data?: { + /** @description Base64-encoded image bytes. */ + b64_json?: string; + /** @example image/png */ + media_type?: string; + }[]; + usage?: { + completion_tokens?: number; + cost?: number; + prompt_tokens?: number; + total_tokens?: number; + }; + }; + }; + }; + /** @description Malformed body, or stream: true (streaming is not supported) */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Invalid or missing API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Insufficient balance */ + 402: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Model not found, disabled, or not an image model */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Rate limit exceeded: rate_limit_exceeded (client's overall limit), free_rate_limit_exceeded (client's limit on free models), or free_quota_exhausted (installation-wide free-model budget exhausted) */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/models": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * List available models + * @description Каталог доступных клиенту моделей. Форма объединяет два контракта: поля object/created/owned_by держат совместимость с OpenAI, остальные (name, context_length, architecture, pricing, top_provider) повторяют форму OpenRouter — по голому списку идентификаторов агентский клиент не может ни показать человеку размер контекста, ни посчитать стоимость сессии. Цены в pricing — НАШИ, с наценкой клиента, в долларах за один токен; они согласованы с usage.cost ответа. Ключ с ограничением allowed_models видит только разрешённые ему модели. + */ + get: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description List of available models */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": { + data?: components["schemas"]["ModelV1"][]; + /** @example list */ + object?: string; + }; + }; + }; + /** @description Rate limit exceeded: rate_limit_exceeded (client's overall limit) */ + 429: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/responses": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Responses API (не реализовано) + * @description OpenAI Responses API не реализован — это отдельный от Chat Completions протокол, который сторонние OpenAI-совместимые провайдеры (и OpenRouter) не поддерживают. Маршрут существует, чтобы вернуть внятную ошибку вместо голого 404 от ServeMux — свежие SDK/ноды по умолчанию ходят в /v1/responses и без явного 501 показывают запутывающий MODEL_NOT_FOUND. Аутентификация и биллинг не выполняются вовсе — запрос отклоняется до похода в БД. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Не реализовано (unsupported_endpoint). Тело — ошибка в формате OpenAI ({"error":{"message","type","code"}}, см. описание 4xx-ошибок /v1/chat/completions). Используйте POST /v1/chat/completions. */ + 501: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/v1/systemone": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Ask typed questions about a text + * @description Structured decisions: you give a state (a text, an object or an array) and a set of typed questions about it, and the model answers each one with calibrated probabilities. + * + * This is NOT an OpenAI-compatible endpoint. There is no messages field, no roles, no tools and no assistant text — the model emits probabilities, not prose, and a chat-shaped body is rejected. The path is fixed by the upstream SDK convention (base URL + "/v1/systemone"), so moving an existing integration over to this proxy means changing the base URL and nothing else. + * + * There is no streaming. The provider has no streaming mode at all, so there is no stream field to set and none is inspected — a body carrying one is forwarded as-is. + * + * Billing is by input tokens only: the amount in rubles is usage.input_tokens times the catalog prompt price of the model, times the markup, converted at the CBR rate. Output tokens are free BY CONSTRUCTION, not as a promotion — the model generates no text to charge for. The catalog id you asked for is what you are billed by, even when the response reports a different served version (see SystemOneResponse.model). + * + * The request body is forwarded to the provider unchanged except for the model field, which is translated from the catalog id to the provider's own model name. Body size is capped at 1 MiB and the number of questions at 64 — both are limits of this proxy. The shape of the questions themselves is NOT validated here: the provider's own limits (option and level counts) change without notice, and a stale copy of them would reject requests the provider would have accepted, so those come back as 422. + */ + post: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["SystemOneRequest"]; + }; + }; + responses: { + /** @description Answers, keyed by the ids from the request */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SystemOneResponse"]; + }; + }; + /** @description invalid_request — the body is not readable or not valid JSON. too_many_questions — more than 64 questions in one request; split it. The same code is also returned when the PROVIDER rejects the request (its status is passed through rather than masked as a 5xx: the provider is healthy and it is the request that needs fixing). The provider's own wording is not forwarded — it would name a third party — so check the question shapes against the schemas above. */ + 400: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description invalid_api_key — missing or unknown API key */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description insufficient_balance — the account balance is at or below zero. Top up in the cabinet. */ + 402: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description account_blocked — the account is not active. model_not_allowed — the key has an allowed_models list and this model is not on it. */ + 403: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description model_not_found — the model is unknown, disabled in the catalog, or is not a "decision" model. A text model sent here fails with this code before the request leaves the proxy: there is no route for it at the provider, and forwarding it would surface as an upstream failure that never happened. */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description request_too_large — the body is above the 1 MiB cap of this route. The cap is lower than the proxy-wide one on purpose: the model's context is 64k tokens, so a larger body cannot be served anyway and would only be read into memory to be rejected. */ + 413: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description The provider rejected the shape of a question — typically the option count of a choice question or the level count of a score question. This proxy deliberately does not pre-validate those limits (the provider changes them without notice), so this is where you learn about them. */ + 422: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Four different limits answer with this code, and they are NOT interchangeable. rate_limit_exceeded — your overall request limit across all routes. spend_limit_exceeded — the spend limit of the API key in its current window. typesafe_rate_limit_exceeded — your limit on THIS route specifically; slow down and it clears. typesafe_quota_exhausted — the installation-wide budget for this route is exhausted; this one is not about you and retrying faster only makes it worse. upstream_rate_limited — the provider itself throttled the account; Retry-After is then set from the provider's own hint and should be honoured rather than guessed at. */ + 429: { + headers: { + /** @description Seconds to wait, present on upstream_rate_limited. Taken from the provider's hint when it gave one. */ + "Retry-After"?: number; + [name: string]: unknown; + }; + content?: never; + }; + /** @description internal_error — a failure on our side (catalog, settings or exchange rate unreadable). Nothing was charged. */ + 500: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description upstream_error — every upstream key was tried and failed, or the provider returned an unusable response. There is no fallback provider for this route: the model is unique to it and does not exist in any other catalog. */ + 502: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description upstream_unavailable — no upstream key could be selected for the provider at all. upstream_busy — the provider reported itself overloaded; Retry-After carries how long to wait. */ + 503: { + headers: { + /** @description Seconds to wait, present on upstream_busy. */ + "Retry-After"?: number; + [name: string]: unknown; + }; + content?: never; + }; + /** @description upstream_error — the provider did not answer in time. Nothing was charged: billing happens after the response is parsed. */ + 504: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + APIKey: { + /** Format: date-time */ + created_at?: string; + id?: string; + /** @description Full key visible only on creation */ + key?: string; + /** @description Key prefix for identification */ + key_prefix?: string; + /** Format: date-time */ + last_used_at?: string | null; + name?: string; + /** + * Format: date-time + * @description When a revoked key will be permanently deleted (revoked_at + 7 days). Absent for active keys. Usage history survives deletion. + */ + purge_at?: string | null; + /** Format: date-time */ + revoked_at?: string | null; + /** @description Per-key spend limit in RUB (decimal string). 0 when no limit is set. */ + spend_limit_rub?: string; + /** + * Format: date-time + * @description When the current window resets (start of the next window). Absent when no limit is set. + */ + spend_resets_at?: string | null; + /** @description Amount spent in the current window so far (decimal string, RUB). */ + spend_used?: string; + /** @description Spend window: 'day' | 'week'. Empty when no limit is set. */ + spend_window?: string; + }; + /** @description Отзыв клиента в очереди модерации: автор, вознаграждение и признаки накрутки. Суммы — строками. */ + AdminReview: { + author_name?: string; + /** @description Текст для публикации (может быть поправлен модератором) */ + body?: string; + /** @description Текст клиента, не меняется никогда */ + body_original?: string; + /** Format: date-time */ + consent_at?: string; + /** Format: date-time */ + created_at?: string; + edited?: boolean; + /** Format: uuid */ + id?: string; + /** Format: date-time */ + moderated_at?: string | null; + moderated_by_email?: string | null; + /** Format: date-time */ + published_at?: string | null; + rating?: number; + reject_reason?: string | null; + /** @description Вознаграждение клиента — одно на клиента за всё время, review_id может указывать на прежний отзыв. */ + reward?: { + amount?: string | null; + /** Format: date-time */ + created_at?: string | null; + paid?: boolean; + /** Format: uuid */ + review_id?: string | null; + }; + risk?: { + confirmed_payments?: number; + email_verified?: boolean; + registration_ip?: string; + /** Format: int64 */ + requests?: number; + /** @description Сколько ДРУГИХ клиентов с тем же IP регистрации оставляли отзывы */ + same_ip_reviewers?: number; + }; + sort_order?: number; + source?: string; + /** @enum {string} */ + status?: "pending" | "published" | "rejected" | "hidden"; + /** Format: date-time */ + updated_at?: string; + user_email?: string | null; + /** Format: uuid */ + user_id?: string | null; + user_telegram?: string | null; + }; + /** @description Aggregated platform statistics for external analytics (GET /api/analytics/stats and GET /api/admin/stats). Monetary fields are decimal strings (NUMERIC, not float). */ + AnalyticsStats: { + /** + * Format: date-time + * @description Range start (RFC3339). Absent for period=all. + */ + from?: string; + /** @description Requested period: 7, 30, 90, or all */ + period?: string; + /** @description Profit summary. Profit = revenue_rub - cost_rub. */ + profit?: { + /** @description Upstream cost in RUB (cost_usd * fx_rate * (1+fx_buffer)) */ + cost_rub?: string; + profit_rub?: string; + revenue_rub?: string; + }; + /** @description Time series of client sign-ups (by day for 7/30/90, by month for all), ordered by bucket ascending. Sourced from the registration_events journal, which OUTLIVES account deletion — counting from the users table would silently drop sign-ups that never confirmed, since the unverified-cleanup worker job removes them. email_verified is keyed by the REGISTRATION date: 'of those who signed up that day, how many confirmed their email', not 'how many confirmations happened that day'. */ + registrations?: { + /** Format: date-time */ + bucket?: string; + /** @description Sign-ups via the email form in this bucket */ + email_registered?: number; + /** @description Of those, how many confirmed their email address */ + email_verified?: number; + /** @description Sign-ups via the Telegram login widget. Such clients have no email at all, so they are counted separately and never appear in email_verified. */ + telegram_registered?: number; + }[]; + /** @description Outcome of model requests within the period. succeeded counts requests that reached an answer (usage_events); failed counts PROVIDER refusals (upstream_request_errors) — 4xx/5xx from upstream, timeouts, or an exhausted key pool. Our own refusals (empty balance, spend limit, model not in catalog) are deliberately NOT counted here: they are explained by the client's account state, not by provider health. */ + requests?: { + failed?: number; + /** @description Refusal reasons, most frequent first. */ + reasons?: { + count?: number; + /** @enum {string} */ + reason?: "rate_limit" | "upstream_error" | "upstream_unavailable" | "timeout" | "bad_request" | "upstream_auth"; + /** @description How many DISTINCT clients this reason hit — tells a platform-wide outage apart from one client retrying in a loop. */ + users?: number; + }[]; + succeeded?: number; + }; + /** @description Money flows within the period. All fields are decimal-string RUB. */ + revenue?: { + /** @description Manual balance adjustments */ + adjustments?: string; + /** @description Confirmed non-test payments */ + deposits?: string; + refunds?: string; + /** @description SUM(amount_rub) of billed/reconciled usage */ + usage_revenue?: string; + }; + /** @description Time series of usage (by day for 7/30/90, by month for all). Ordered by bucket ascending. */ + series?: { + /** @description Revenue (amount_rub), decimal-string RUB */ + amount?: string; + /** Format: date-time */ + bucket?: string; + cost_rub?: string; + cost_usd?: string; + requests?: number; + }[]; + /** @description Point-in-time state of the system right now (unlike period-scoped fields above). Same SQL as the Prometheus gauges, so analytics and metrics never diverge. */ + snapshot?: { + /** @description Number of non-revoked API keys */ + api_keys_active?: number; + /** @description Number of accounts with negative balance */ + client_balance_negative_count?: number; + /** @description Sum of all client account balances, decimal-string RUB */ + client_balance_rub?: string; + /** @description Currently pending payments (not yet confirmed/expired). */ + payments_pending?: { + /** @description Sum of pending payment amounts, decimal-string RUB */ + amount?: string; + count?: number; + }; + /** @description Latest snapshot of each upstream provider/key balance (openrouter + deepseek). Empty until the worker upstream-balance job has ticked at least once. */ + upstream_balances?: { + /** @description Age of the snapshot. >1800 means the balance poller is not running. */ + age_seconds?: number; + /** @description AVAILABLE balance now (how much can still be spent), decimal-string USD. For openrouter = total_credits - total_usage. */ + balance?: string; + /** @description Human-readable key name; equals provider for account-global (openrouter /credits) */ + key?: string; + /** @description openrouter or deepseek */ + provider?: string; + /** @description Total ever credited (total_credits), decimal-string USD. 0 for deepseek — API does not return it. */ + total_credited?: string; + /** @description Cumulative spent (total_usage), decimal-string USD. 0 for deepseek. */ + used?: string; + }[]; + }; + /** + * Format: date-time + * @description Range end (RFC3339) + */ + to?: string; + /** @description Costs grouped by upstream key, ordered by cost_usd desc. */ + upstream?: { + cost_rub?: string; + cost_usd?: string; + /** @description Upstream key name, or 'без ключа' if upstream_key_id is null */ + name?: string; + requests?: number; + }[]; + users?: { + active?: number; + blocked?: number; + /** @description Clients registered within the period */ + registered?: number; + /** @description Total clients (role=client) */ + total?: number; + }; + }; + /** @description Баннер объявлений в кабинете. Активен одновременно может быть только один; окно показа проверяется на лету, status — производное от is_active и окна на момент ответа. */ + Banner: { + /** Format: date-time */ + created_at?: string; + /** Format: uuid */ + id?: string; + /** @description Создаётся false — публикация только через POST .../{id}/activate */ + is_active?: boolean; + message?: string; + /** + * Format: date-time + * @description Начало показа; null — сразу после публикации + */ + show_from?: string | null; + /** + * Format: date-time + * @description Конец показа; null — бессрочно + */ + show_until?: string | null; + /** @enum {string} */ + status?: "inactive" | "scheduled" | "showing" | "finished"; + /** + * @description информация (синий) / уведомление (жёлтый) / важный (красный) + * @enum {string} + */ + type?: "info" | "notification" | "critical"; + /** Format: date-time */ + updated_at?: string; + }; + CabinetReview: { + author_name?: string; + /** @description Текст в текущей редакции (модератор мог поправить) */ + body?: string; + /** Format: date-time */ + created_at?: string; + edited_by_moderator?: boolean; + /** Format: uuid */ + id?: string; + /** Format: date-time */ + published_at?: string | null; + rating?: number; + reject_reason?: string | null; + /** @enum {string} */ + status?: "pending" | "published" | "rejected" | "hidden"; + } | null; + /** @description Маркетинговая кампания жизненного цикла клиента (подпроект E): правило ('кому написать') + действие ('что сделать') + их параметры. */ + Campaign: { + action?: string; + /** @description Параметры действия */ + action_params?: Record; + cooldown_days?: number; + /** Format: date-time */ + created_at?: string; + /** @description Создаётся false — включение только через PATCH */ + enabled?: boolean; + /** Format: uuid */ + id?: string; + /** @description Признак рекламности КАМПАНИИ (не правила): от него зависит проверка согласия на рекламу и обязательность ссылки отписки. При создании по умолчанию берётся из вида выбранного правила, дальше меняется независимо через PATCH. */ + is_marketing?: boolean; + max_per_user?: number; + /** @description Параметры правила */ + params?: Record; + rule?: string; + title?: string; + /** Format: date-time */ + updated_at?: string; + }; + ChatCompletionRequest: { + /** @description Maximum tokens in response */ + max_tokens?: number; + /** @description List of messages in the conversation */ + messages: { + /** @description Message content */ + content: string; + /** + * @description Message role + * @enum {string} + */ + role: "system" | "user" | "assistant"; + }[]; + /** + * @description Model ID from /v1/models + * @example gpt-4o-mini + */ + model: string; + /** + * @description Enable streaming responses + * @default false + */ + stream: boolean; + /** @description Controls randomness (0-2) */ + temperature?: number; + }; + ChatCompletionResponse: { + choices?: { + /** @enum {string} */ + finish_reason?: "stop" | "length" | "tool_calls"; + index?: number; + message?: { + content?: string; + role?: string; + }; + }[]; + /** @description Unix timestamp */ + created?: number; + /** @description Unique completion ID */ + id?: string; + /** @description Model used */ + model?: string; + /** + * @description Response type + * @enum {string} + */ + object?: "chat.completion" | "chat.completion.chunk"; + usage?: { + completion_tokens?: number; + /** + * Format: double + * @description Та же сумма в долларах по курсу ЦБ на день запроса — для клиентов, считающих расход по стандартному полю usage.cost. Производная от cost_rub и округлена, поэтому источником правды служит cost_rub. ВНИМАНИЕ: это НЕ стоимость запроса у провайдера — она наша внутренняя величина и наружу не отдаётся. + */ + cost?: number; + /** @description Сумма, списанная с баланса за этот запрос, в рублях. Первична: именно она попадает в выписку и в статистику кабинета. Строка, а не число, — как и все денежные величины в этом API: разбор через double теряет последний знак, по которому клиент сверяет выписку. У потокового ответа приходит в том же финальном чанке, что и токены. */ + cost_rub?: string; + prompt_tokens?: number; + total_tokens?: number; + }; + }; + Error: { + code?: string; + message?: string; + }; + Model: { + context_length?: number; + description?: string; + /** @description Model identifier */ + id?: string; + /** @description What the model accepts on input ("text", "image", ...), as reported by the provider. A text model whose list contains "image" is a vision model: image_url content parts in POST /v1/chat/completions are billed as prompt tokens. Empty or missing — unknown (manually seeded rows). */ + input_modalities?: string[]; + is_free?: boolean; + /** + * @description Output modality. "text" — chat/completions models. "transcription" — speech-to-text models usable with POST /v1/audio/transcriptions. "image" — image generation models usable with POST /v1/images/generations; their tariff is in price_image_rub / price_image_unit, not in price_prompt_rub_1m. "decision" — structured decision models answering typed questions with calibrated probabilities; they are served by their own route, not by /v1/chat/completions, and are priced per input token (price_completion_rub_1m is zero by construction). "embedding" — vector models usable with POST /v1/embeddings, also priced per input token with no completion tokens by construction. "speech" — text-to-speech models usable with POST /v1/audio/speech; the provider does not state a unit for their price, so no per-token price applies to them and the charge follows the cost reported by the provider. + * @enum {string} + */ + modality?: "text" | "transcription" | "image" | "decision" | "embedding" | "speech"; + name?: string; + /** @example model */ + object?: string; + /** @description Model owner/provider */ + owned_by?: string; + /** @description Price per 1M completion tokens in rubles. See the note on price_prompt_rub_1m. For modalities "decision" and "embedding" it is zero BY CONSTRUCTION — decision models emit probabilities and embeddings emit a fixed-size vector, neither generates tokens — and not because the model is free. */ + price_completion_rub_1m?: string; + /** @description Image generation tariff in rubles PER UNIT named by price_image_unit. Present only for modality "image". */ + price_image_rub?: string; + /** + * @description Unit the image tariff is measured in: per generated image, per 1M output tokens (price_image_rub is already scaled to a million), or per megapixel. Absent when the price has not been fetched yet — distinguish that case by the missing unit, not by a zero price. + * @enum {string} + */ + price_image_unit?: "image" | "token" | "megapixel"; + /** @description Price per 1M prompt tokens in rubles. Not applicable to models with modality "transcription": for whisper-class models the provider prices by audio duration, not by tokens. For modalities "decision" and "embedding" this is the only meaningful price: such models are billed per INPUT token. */ + price_prompt_rub_1m?: string; + }; + /** @description Строка разбивки потребления. `model` пуст в разрезе производителей. Деньги — decimal-строки (NUMERIC, не float): JSON-число потеряло бы точность. `profit_rub` — разность выручки и себестоимости, посчитанная на слое представления, а не третьим запросом в БД. */ + ModelUsageRow: { + /** @description Заплачено upstream, пересчитано построчным курсом с резервом */ + cost_rub?: string; + model?: string; + profit_rub?: string; + requests?: number; + /** @description Списано с клиентов */ + revenue_rub?: string; + /** Format: int64 */ + tokens_cached?: number; + /** Format: int64 */ + tokens_completion?: number; + /** Format: int64 */ + tokens_prompt?: number; + vendor?: string; + }; + /** @description Модель каталога в форме, объединяющей два контракта: поля object/created/owned_by — совместимость с OpenAI, остальные — форма OpenRouter, без которой агентский клиент не может ни показать размер контекста, ни посчитать стоимость сессии. */ + ModelV1: { + /** @description Модальности модели. Вход у всех текстовый, кроме распознавания речи — там audio: клиент, фильтрующий каталог, обязан это видеть, иначе предложит отправить в whisper текст. */ + architecture?: { + input_modalities?: string[]; + /** @example text->text */ + modality?: string; + output_modalities?: string[]; + }; + context_length?: number; + /** @description Unix-время последней синхронизации записи каталога. */ + created?: number; + description?: string; + id?: string; + name?: string; + /** @example model */ + object?: string; + /** @description Вендор — часть идентификатора до слэша. */ + owned_by?: string; + /** @description Цена за ОДИН токен в долларах, строками (float64 для денег в проекте запрещён, а величины здесь порядка 1e-8 — double теряет на них значащие разряды). Цена НАША: с наценкой клиента и буфером курса, то есть tokens × prompt даёт ту же величину, что придёт в usage.cost ответа. Блок ОТСУТСТВУЕТ у моделей, чью цену нельзя выразить ценой за токен (transcription тарифицируется по длительности аудио, image — за изображение или мегапиксель) и при недоступном курсе ЦБ: нули там читались бы как «бесплатно». Цену таких моделей отдаёт GET /api/models вместе с единицей измерения. */ + pricing?: { + completion?: string; + /** @description Цена изображения на ВХОДЕ (vision). Отдельно не тарифицируется: такие токены приходят в prompt_tokens и считаются по цене промпта. */ + image?: string; + /** @description Цена чтения промпта из кэша за токен. Отсутствует, если провайдер её не объявил — ноль означал бы «кэш бесплатен», а тогда весь промпт считается по полной цене. Для агентских клиентов поле существенно: из кэша приходит почти весь промпт, и он дешевле обычного в разы. */ + input_cache_read?: string; + prompt?: string; + /** @description Плата за сам факт запроса. У нас её нет, поле всегда "0" — это факт, а не умолчание. */ + request?: string; + }; + top_provider?: { + context_length?: number; + /** @description Всегда false: сведений о модерации по каждой модели у нас нет, и false здесь — отсутствие сведений, а не утверждение «модерации нет». Поле присутствует, потому что часть клиентов падает на его отсутствии. */ + is_moderated?: boolean; + }; + }; + NotificationSettings: { + /** + * @description Delivery channel. "email" requires a verified email, "telegram" requires a linked account. + * @enum {string} + */ + channel?: "email" | "telegram" | "both"; + /** @description Notify when account balance drops below low_balance_threshold */ + low_balance_enabled?: boolean; + /** @description Threshold in RUB (decimal string) */ + low_balance_threshold?: string; + /** @description Consent to receive marketing/informational messages. GET always returns a concrete true/false (defaults to true for accounts that never saved this setting — see the unsubscribe link in every marketing message for an unauthenticated opt-out). On PUT this field is optional: omitting it (or sending null) leaves the current consent UNCHANGED — PUT otherwise replaces the whole settings object, and a client that doesn't send this field (an old cached form, a client following an older version of this spec) must not silently withdraw consent. Payment and support-reply notifications are sent regardless of this field — they are not marketing. */ + marketing_opt_in?: boolean | null; + }; + /** @description Один параметр правила или действия кампании — контракт, по которому форма настройки рисует поле ввода. */ + ParamSpec: { + /** @description Значение по умолчанию, тип соответствует полю type */ + default?: unknown; + /** @description Верхняя граница: значения для type=int, длины в символах для type=string. */ + max?: number | null; + /** @description Границы значения для type=int и границы ДЛИНЫ В СИМВОЛАХ для type=string. Параметр с min > 0 обязателен — пустым он не сохранится. */ + min?: number | null; + name?: string; + title?: string; + /** @enum {string} */ + type?: "int" | "string"; + }; + /** @description Платёж на пополнение баланса. Внутренние подробности эквайринга (PaymentId банка) не раскрываются — в поддержку клиент обращается по нашему id. */ + Payment: { + /** @description Decimal string, RUB. */ + amount?: string; + /** Format: date-time */ + confirmed_at?: string | null; + /** Format: date-time */ + created_at?: string; + failure_reason?: string | null; + /** Format: uuid */ + id?: string; + /** @description Создан через POST /api/payments/test — реальное списание с тестовой карты. */ + is_test?: boolean; + /** @description Ссылка на форму оплаты T-Bank. Присутствует, только пока status=pending — на любом другом статусе платёж уже решён или ссылка не работает. */ + payment_url?: string | null; + /** @enum {string} */ + status?: "pending" | "confirmed" | "rejected" | "expired" | "refunded" | "refund_pending"; + }; + /** @description Промокод на разовое начисление рублей клиенту. */ + PromoCode: { + code?: string; + /** Format: date-time */ + created_at?: string; + /** @description Рубли строкой (NUMERIC, не float) */ + credit_amount?: string; + /** @description Состоявшиеся активации. Ниже этого числа max_uses опустить нельзя */ + current_uses?: number; + /** @description Штатный способ закрыть промокод, не трогая уже состоявшиеся активации */ + disabled?: boolean; + /** Format: date-time */ + expires_at?: string; + /** Format: uuid */ + id?: string; + max_uses?: number; + note?: string; + }; + /** @description One answer. Three shapes matching the three shapes of question, told apart by the type field. */ + SystemOneAnswer: components["schemas"]["SystemOneNoulAnswer"] | components["schemas"]["SystemOneChoiceAnswer"] | components["schemas"]["SystemOneScoreAnswer"]; + /** @description Answer to a choice question. */ + SystemOneChoiceAnswer: { + /** @description The chosen option — one of the keys of the question's criteria. */ + choice?: string; + /** @description How sure the model is of the chosen option. */ + confidence?: number; + /** @description Probability of every option, keyed the same way as the question's criteria. Use it instead of choice when a near-tie should not be resolved silently. */ + probabilities?: { + [key: string]: number; + }; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "choice"; + }; + /** @description Pick one option out of a named set. The answer carries the chosen option plus the probability of every option. */ + SystemOneChoiceQuestion: { + /** + * @description The options: a map of option name to its description. The names are chosen by the caller and one of them comes back in the answer. The provider caps their number (255 at the time of writing) and rejects a violation with 422 — this proxy does not validate the cap, because the provider changes it without notice and a stale copy would reject requests the provider would have accepted. + * @example { + * "other": "Anything else", + * "refund": "The customer asks for their money back", + * "technical": "The customer reports something not working" + * } + */ + criteria: { + [key: string]: string; + }; + /** @description What to decide. A string, an object or an array. */ + instructions: string | Record | unknown[]; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "choice"; + }; + /** @description Answer to a noul question. */ + SystemOneNoulAnswer: { + /** @description Probability that the statement is true, calibrated: 0.5 means the model cannot tell, not that the answer is 'maybe'. */ + noul?: number; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "noul"; + }; + /** @description Yes/no question. The answer is a calibrated probability, not a verdict: 0.5 means the model genuinely cannot tell from the state given. */ + SystemOneNoulQuestion: { + /** @description Optional. What counts as true and what counts as false. Omitted, the model decides from the instructions alone. */ + criteria?: { + false?: string; + true?: string; + }; + /** + * @description What to decide. A string, an object or an array — the provider accepts all three and treats structure as part of the instruction. + * @example Is this message a refund request? + */ + instructions: string | Record | unknown[]; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "noul"; + }; + /** @description One question about the state. Three shapes, told apart by the type field. */ + SystemOneQuestion: components["schemas"]["SystemOneNoulQuestion"] | components["schemas"]["SystemOneChoiceQuestion"] | components["schemas"]["SystemOneScoreQuestion"]; + SystemOneRequest: { + /** + * @description Decision model id. Must be a model whose modality is "decision" in GET /api/models — a text model here is rejected with 404 before the request leaves this proxy. + * @example typesafe/jev-latest + */ + model: string; + /** @description The questions, keyed by an id of your choosing. The same ids come back in answers — that is the whole point of the map, and why the keys cannot be listed here. At most 64 questions per request (a proxy limit, answered with 400 too_many_questions); the shape of each question is validated by the provider, not here. */ + questions: { + [key: string]: components["schemas"]["SystemOneQuestion"]; + }; + /** @description The material the questions are asked about: a string, an object or an array. Text only — the model takes no images or audio. It is forwarded to the provider unchanged and never logged. */ + state: string | Record | unknown[]; + }; + SystemOneResponse: { + /** @description Answers under the same keys the questions were sent with. */ + answers?: { + [key: string]: components["schemas"]["SystemOneAnswer"]; + }; + /** @description The model version that actually served the request. It may differ from the id you sent when a floating alias was used. This is NOT the billing basis: the request is billed by the catalog id of the request, at the price GET /api/models shows for that id. */ + model?: string; + usage?: { + /** @description Tokens of state plus questions. The only billed quantity on this route. */ + input_tokens?: number; + /** @description Reported by the provider and never billed — see the route description. */ + output_tokens?: number; + }; + }; + /** @description Answer to a score question. */ + SystemOneScoreAnswer: { + /** @description How sure the model is of the level. */ + confidence?: number; + /** @description Level to its description, echoing the question's criteria — so the score can be rendered without keeping the request around. */ + legend?: { + [key: string]: string; + }; + /** @description Probability of every level. */ + probabilities?: { + [key: string]: number; + }; + /** @description The level on the scale, as an index into the question's ordered criteria. */ + score?: number; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "score"; + }; + /** @description Place the state on an ordered scale. The answer carries the numeric level plus the probability of every level. */ + SystemOneScoreQuestion: { + /** + * @description The levels of the scale, IN ORDER, from lowest to highest. The order is part of the contract and cannot be expressed by the schema itself: a shuffled array is valid JSON and valid against this schema, and the provider will happily score against it. Between 2 and 10 levels; the provider enforces the limits and rejects a violation with 422. + * @example [ + * "very negative", + * "negative", + * "neutral", + * "positive", + * "very positive" + * ] + */ + criteria: string[]; + /** @description What to decide. A string, an object or an array. */ + instructions: string | Record | unknown[]; + /** + * @description discriminator enum property added by openapi-typescript + * @enum {string} + */ + type: "score"; + }; + /** @description Профиль текущего клиента (GET /api/me). Секретов не содержит: ни пароля, ни его хеша, ни ключей — только факты их наличия. */ + User: { + /** @description Чат включён в настройках сервиса: по признаку меню кабинета показывает ссылку на /chat */ + chat_enabled?: boolean; + /** @description null у клиента, вошедшего только через Telegram */ + email?: string | null; + email_verified?: boolean; + /** @description У клиента задан пароль */ + has_credentials?: boolean; + /** + * Format: uuid + * @description User ID + */ + id?: string; + /** @description Присутствует, только когда кабинет открыт администратором от имени клиента — признак режима «только чтение». */ + impersonation?: { + email?: string | null; + /** Format: date-time */ + expires_at?: string; + }; + /** @description Незакрытые обращения клиента. По нему меню кабинета показывает отметку у «Техподдержки». */ + open_tickets?: number; + /** @description Реферальная программа включена в настройках сервиса */ + referral_enabled?: boolean; + /** @enum {string} */ + status?: "active" | "blocked"; + telegram_linked?: boolean; + /** @description Присутствует только при привязанном Telegram с заданным именем */ + telegram_username?: string; + /** @description Обращения, где ответил саппорт и ход за клиентом (статус waiting_user). Подмножество open_tickets; отметка в меню по нему выделяется. */ + tickets_awaiting_user?: number; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export type operations = Record; diff --git a/src/money.ts b/src/money.ts new file mode 100644 index 0000000..8c471ba --- /dev/null +++ b/src/money.ts @@ -0,0 +1,117 @@ +// Денежная арифметика без float. API отдаёт суммы decimal-строками +// (shopspring/decimal на бэкенде); здесь они живут как целое × 10^-scale. +// number для денег не используется нигде — ни на входе, ни внутри. + +export interface Decimal { + readonly int: bigint; + readonly scale: number; +} + +const DECIMAL_RE = /^(-?)(\d+)(?:\.(\d+))?$/; + +function assertScale(scale: number): void { + if (!Number.isSafeInteger(scale) || scale < 0) { + throw new RangeError(`недопустимый scale: ${String(scale)}`); + } +} + +function pow10(n: number): bigint { + assertScale(n); + return 10n ** BigInt(n); +} + +function make(int: bigint, scale: number): Decimal { + assertScale(scale); + return Object.freeze({ int, scale }); +} + +/** Строгий разбор decimal-строки: без экспоненты, без запятой, без пробелов, только string. */ +export function parseDecimal(s: unknown): Decimal { + if (typeof s !== "string") { + throw new TypeError(`ожидалась decimal-строка, получено ${typeof s}`); + } + const m = DECIMAL_RE.exec(s); + if (m === null) { + throw new SyntaxError(`некорректная decimal-строка: ${JSON.stringify(s.slice(0, 40))}`); + } + const [, sign = "", whole = "", frac = ""] = m; + const int = BigInt(whole + frac); + return make(sign === "-" ? -int : int, frac.length); +} + +export function fromBigInt(n: bigint): Decimal { + return make(n, 0); +} + +function rescale(a: Decimal, scale: number): bigint { + // Только повышение точности — без потерь. + return a.int * pow10(scale - a.scale); +} + +export function add(a: Decimal, b: Decimal): Decimal { + const scale = Math.max(a.scale, b.scale); + return make(rescale(a, scale) + rescale(b, scale), scale); +} + +export function mul(a: Decimal, b: Decimal): Decimal { + return make(a.int * b.int, a.scale + b.scale); +} + +export function mulByBigInt(a: Decimal, n: bigint): Decimal { + return make(a.int * n, a.scale); +} + +/** Деление на 10^p — точное, просто сдвиг scale. */ +export function divPow10(a: Decimal, p: number): Decimal { + assertScale(p); + return make(a.int, a.scale + p); +} + +/** ROUND HALF UP (половина — от нуля), как decimal.Round в shopspring. */ +export function round(a: Decimal, scale: number): Decimal { + assertScale(scale); + if (a.scale <= scale) { + return make(rescale(a, scale), scale); + } + const divisor = pow10(a.scale - scale); + const negative = a.int < 0n; + const abs = negative ? -a.int : a.int; + let q = abs / divisor; + const r = abs % divisor; + if (r * 2n >= divisor) { + q += 1n; + } + return make(negative ? -q : q, scale); +} + +export function compare(a: Decimal, b: Decimal): -1 | 0 | 1 { + const scale = Math.max(a.scale, b.scale); + const x = rescale(a, scale); + const y = rescale(b, scale); + return x < y ? -1 : x > y ? 1 : 0; +} + +function format(int: bigint, scale: number): string { + const negative = int < 0n; + const digits = (negative ? -int : int).toString().padStart(scale + 1, "0"); + const whole = digits.slice(0, digits.length - scale); + const frac = digits.slice(digits.length - scale); + const body = scale > 0 ? `${whole}.${frac}` : whole; + return negative ? `-${body}` : body; +} + +/** Канонический вид: без незначащих нулей справа; "0.00" и "-0" дают "0". */ +export function toString(a: Decimal): string { + let { int, scale } = a; + while (scale > 0 && int % 10n === 0n) { + int /= 10n; + scale -= 1; + } + return format(int, scale); +} + +/** Ровно scale знаков после точки, с округлением HALF UP. */ +export function toFixed(a: Decimal, scale: number): string { + const r = round(a, scale); + return format(r.int, r.scale); +} diff --git a/src/redact.ts b/src/redact.ts new file mode 100644 index 0000000..daa9787 --- /dev/null +++ b/src/redact.ts @@ -0,0 +1,52 @@ +// Вычистка секретов из текста и ошибок перед тем, как они уйдут в stderr +// или в ответ инструмента (а оттуда — в контекст модели и её логи). + +/** Ключи площадки: "orr_live_" + 32 символа [A-Za-z0-9] (backend/internal/apikey). */ +// Без \b слева: ключ, приклеенный к соседнему слову («keyorr_live_…»), тоже ключ. +const KEY_RE = /orr_[a-z]+_[A-Za-z0-9]+/g; +const BEARER_RE = /\bBearer\s+\S+/gi; + +export const REDACTED = "[REDACTED]"; + +export function redactString(s: string): string { + return s.replace(BEARER_RE, `Bearer ${REDACTED}`).replace(KEY_RE, REDACTED); +} + +export interface RedactedError { + name: string; + message: string; + status?: number; + code?: string; + path?: string; +} + +function isRecord(v: unknown): v is Record { + return typeof v === "object" && v !== null; +} + +/** + * Достаёт из произвольной ошибки только узкий набор полей. headers, config, + * request, cause и stack не копируются никогда: именно в них у http-клиентов + * лежит Authorization. + */ +export function redactError(err: unknown): RedactedError { + if (typeof err === "string") { + return { name: "Error", message: redactString(err) }; + } + if (!isRecord(err)) { + return { name: "Error", message: "неизвестная ошибка" }; + } + const name = typeof err.name === "string" && err.name !== "" ? err.name : "Error"; + const message = typeof err.message === "string" ? err.message : "неизвестная ошибка"; + const out: RedactedError = { name: redactString(name), message: redactString(message) }; + if (typeof err.status === "number" && Number.isInteger(err.status)) { + out.status = err.status; + } + if (typeof err.code === "string") { + out.code = redactString(err.code); + } + if (typeof err.path === "string") { + out.path = redactString(err.path); + } + return out; +} diff --git a/src/sanitize.ts b/src/sanitize.ts new file mode 100644 index 0000000..fff7bea --- /dev/null +++ b/src/sanitize.ts @@ -0,0 +1,33 @@ +// Очистка текста, пришедшего извне (названия и описания моделей, имена +// ключей), перед тем как он попадёт в ответ инструмента и в контекст модели: +// управляющие символы, bidi-override и zero-width позволяют прятать или +// переворачивать текст так, что человек и модель читают разное. + +// C0 (кроме \n), DEL и C1. +// eslint-disable-next-line no-control-regex -- ровно эти символы и вычищаем +const CONTROL_RE = /[\u0000-\u0009\u000B-\u001F\u007F-\u009F]/g; +// Bidi: LRM/RLM, ALM, embedding/override (U+202A–U+202E), isolates (U+2066–U+2069). +const BIDI_RE = /[\u200E\u200F\u061C\u202A-\u202E\u2066-\u2069]/g; +// Zero-width: ZWSP/ZWNJ/ZWJ, word joiner, BOM. +const ZERO_WIDTH_RE = /[\u200B-\u200D\u2060\uFEFF]/g; + +export function cleanText(s: string, max = 500): string { + const cleaned = s + .replace(/\r\n?|[\u2028\u2029]/g, "\n") + .replace(/\t/g, " ") + .replace(CONTROL_RE, "") + .replace(BIDI_RE, "") + .replace(ZERO_WIDTH_RE, "") + .replace(/[ \u00A0]+/g, " ") + .replace(/ *\n[ \n]*/g, "\n") + .trim(); + // Режем по кодовым точкам, а не по UTF-16, чтобы не разорвать суррогатную пару. + const chars = Array.from(cleaned); + if (chars.length <= max) { + return cleaned; + } + if (max <= 0) { + return ""; + } + return `${chars.slice(0, max - 1).join("").trimEnd()}…`; +} diff --git a/test/cache.test.ts b/test/cache.test.ts new file mode 100644 index 0000000..4474367 --- /dev/null +++ b/test/cache.test.ts @@ -0,0 +1,42 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { createTtlCache } from "../src/cache.js"; + +test("значение живёт ttl и потом перезагружается", async () => { + let t = 1_000; + let calls = 0; + const get = createTtlCache(100, async () => ++calls, () => t); + assert.equal(await get(), 1); + t += 99; + assert.equal(await get(), 1); + t += 1; + assert.equal(await get(), 2); + assert.equal(calls, 2); +}); + +test("single-flight: один loader на N параллельных get()", async () => { + let calls = 0; + let release!: (v: string) => void; + const get = createTtlCache(1_000, () => { + calls++; + return new Promise((r) => { + release = r; + }); + }); + const all = Promise.all(Array.from({ length: 10 }, () => get())); + release("ok"); + assert.deepEqual(await all, Array(10).fill("ok")); + assert.equal(calls, 1); +}); + +test("ошибка не кэшируется", async () => { + let calls = 0; + const get = createTtlCache(1_000, async () => { + calls++; + if (calls === 1) throw new Error("fail"); + return "ok"; + }); + await assert.rejects(get(), /fail/); + assert.equal(await get(), "ok"); + assert.equal(calls, 2); +}); diff --git a/test/config.test.ts b/test/config.test.ts new file mode 100644 index 0000000..4271e43 --- /dev/null +++ b/test/config.test.ts @@ -0,0 +1,72 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { ConfigError, DEFAULT_BASE_URL, parseConfig } from "../src/config.js"; + +const KEY = "orr_live_TestKey0123456789abcdefABCDEFGHIJ"; + +test("нет ключа — ConfigError без значений переменных", () => { + assert.throws( + () => parseConfig({ OPENRUSROUTER_BASE_URL: "https://secret-host.example/x" }), + (err: unknown) => { + assert.ok(err instanceof ConfigError); + assert.match(err.message, /OPENRUSROUTER_API_KEY/); + assert.ok(!err.message.includes("secret-host")); + assert.ok(!err.message.includes("${")); + return true; + }, + ); + assert.throws(() => parseConfig({ OPENRUSROUTER_API_KEY: " " }), ConfigError); +}); + +test("по умолчанию — прод, объект заморожен", () => { + const c = parseConfig({ OPENRUSROUTER_API_KEY: KEY }); + assert.equal(c.baseUrl, DEFAULT_BASE_URL); + assert.equal(c.apiKey, KEY); + assert.ok(Object.isFrozen(c)); +}); + +test("http на внешний хост — отказ", () => { + assert.throws( + () => parseConfig({ OPENRUSROUTER_API_KEY: KEY, OPENRUSROUTER_BASE_URL: "http://example.com" }), + ConfigError, + ); +}); + +test("userinfo в адресе — отказ, и пароль не попадает в сообщение", () => { + assert.throws( + () => + parseConfig({ + OPENRUSROUTER_API_KEY: KEY, + OPENRUSROUTER_BASE_URL: "https://user:hunter2@openrusrouter.ru", + }), + (err: unknown) => { + assert.ok(err instanceof ConfigError); + assert.ok(!err.message.includes("hunter2")); + return true; + }, + ); +}); + +test("мусор и чужие протоколы — отказ", () => { + for (const url of ["not a url", "ftp://openrusrouter.ru", "https://openrusrouter.ru/?k=1"]) { + assert.throws( + () => parseConfig({ OPENRUSROUTER_API_KEY: KEY, OPENRUSROUTER_BASE_URL: url }), + ConfigError, + url, + ); + } +}); + +test("https на любой хост и http на localhost — ок, хвостовой / срезан", () => { + const ok: Array<[string, string]> = [ + ["https://any-host.example/", "https://any-host.example"], + ["https://any-host.example/prefix/", "https://any-host.example/prefix"], + ["http://localhost:1234", "http://localhost:1234"], + ["http://127.0.0.1:8080/", "http://127.0.0.1:8080"], + ["http://[::1]:8080", "http://[::1]:8080"], + ]; + for (const [input, expected] of ok) { + const c = parseConfig({ OPENRUSROUTER_API_KEY: KEY, OPENRUSROUTER_BASE_URL: input }); + assert.equal(c.baseUrl, expected); + } +}); diff --git a/test/fixtures/api-balance.json b/test/fixtures/api-balance.json new file mode 100644 index 0000000..f0113bd --- /dev/null +++ b/test/fixtures/api-balance.json @@ -0,0 +1 @@ +{"balance":"1234.56789012","referral_balance":"0"} diff --git a/test/fixtures/api-keys.json b/test/fixtures/api-keys.json new file mode 100644 index 0000000..35b672f --- /dev/null +++ b/test/fixtures/api-keys.json @@ -0,0 +1,43 @@ +{ + "keys": [ + { + "id": "6f1c2a9e-4b7d-4e21-9a3c-1d2e3f405a6b", + "name": "my-agent", + "prefix": "orr_live_Ab3dEf", + "last_used_at": "2026-10-24T03:39:57.228771Z", + "created_at": "2026-10-23T02:00:45.11982Z", + "revoked_at": null, + "spend_window": "day", + "spend_limit_rub": "200", + "spend_used": "12.5784961", + "spend_resets_at": "2026-10-25T00:00:00Z", + "allowed_models": null + }, + { + "id": "a8b9c0d1-e2f3-4a5b-8c6d-7e8f90a1b2c3", + "name": "restricted-bot", + "prefix": "orr_live_Gh5iJk", + "last_used_at": "2026-10-20T01:36:06.837887Z", + "created_at": "2026-09-22T09:07:45.316979Z", + "revoked_at": null, + "spend_window": "day", + "spend_limit_rub": "100", + "spend_used": "0", + "spend_resets_at": "2026-10-25T00:00:00Z", + "allowed_models": ["openai/gpt-4o-mini"] + }, + { + "id": "0d9e8f7a-6b5c-4d3e-a2f1-0b9c8d7e6f5a", + "name": "ci-runner", + "prefix": "orr_live_Lm7nOp", + "last_used_at": "2026-10-02T13:11:04.90802Z", + "created_at": "2026-10-02T02:37:36.886285Z", + "revoked_at": null, + "spend_window": "day", + "spend_limit_rub": "1000", + "spend_used": "0", + "spend_resets_at": "2026-10-25T00:00:00Z", + "allowed_models": null + } + ] +} diff --git a/test/fixtures/api-models.json b/test/fixtures/api-models.json new file mode 100644 index 0000000..d4c966d --- /dev/null +++ b/test/fixtures/api-models.json @@ -0,0 +1,254 @@ +{ + "data": [ + { + "id": "aion-labs/aion-3.0-mini", + "name": "AionLabs: Aion-3.0-Mini", + "description": "Aion-3.0 Mini is a multi-model roleplaying and storytelling system from AionLabs, built on the DeepSeek family of models. It uses a collaborative generation process in which multiple specialized models each...", + "context_length": 131072, + "price_prompt_rub_1m": "79.91392973", + "price_completion_rub_1m": "159.82785945", + "is_free": false, + "modality": "text", + "input_modalities": [ + "text" + ], + "price_image_rub": "0" + }, + { + "id": "anthropic/claude-sonnet-5.5", + "name": "Anthropic: Claude Sonnet 5.5", + "description": "Claude Sonnet 5.5 is Anthropic's Sonnet-class model for well-scoped everyday work, succeeding Claude Sonnet 5 as a direct upgrade. It is especially strong at building features, fixing bugs, and producing...", + "context_length": 1000000, + "price_prompt_rub_1m": "228.3255135", + "price_completion_rub_1m": "1141.6275675", + "is_free": false, + "modality": "text", + "input_modalities": [ + "text", + "image", + "file" + ], + "price_image_rub": "0" + }, + { + "id": "assemblyai/universal-3-5-pro", + "name": "AssemblyAI: Universal-3.5 Pro", + "description": "Universal-3.5 Pro is AssemblyAI's speech-to-text model served through its Sync API, returning a complete transcript with word-level timestamps in a single synchronous response for audio up to 20 minutes. It...", + "context_length": 0, + "price_prompt_rub_1m": "9513.55925707", + "price_completion_rub_1m": "0", + "is_free": false, + "modality": "transcription", + "input_modalities": [ + "audio" + ], + "price_image_rub": "0" + }, + { + "id": "baai/bge-m3", + "name": "BAAI: bge-m3", + "description": "The bge-m3 embedding model encodes sentences, paragraphs, and long documents into a 1024-dimensional dense vector space, delivering high-quality semantic embeddings optimized for multilingual retrieval, semantic search, and large-context applications.", + "context_length": 8194, + "price_prompt_rub_1m": "1.14162757", + "price_completion_rub_1m": "0", + "is_free": false, + "modality": "embedding", + "input_modalities": [ + "text" + ], + "price_image_rub": "0" + }, + { + "id": "black-forest-labs/flux-3-image", + "name": "Black Forest Labs: FLUX 3 Image", + "description": "FLUX.3 Image is Black Forest Labs' flagship image generation and editing model. It handles text-to-image and multi-reference editing with up to 10 input images, and renders at fixed resolution tiers...", + "context_length": 0, + "price_prompt_rub_1m": "0", + "price_completion_rub_1m": "0", + "is_free": false, + "modality": "image", + "input_modalities": [ + "text", + "image" + ], + "price_image_rub": "0" + }, + { + "id": "bytedance-seed/seedream-4.5", + "name": "ByteDance Seed: Seedream 4.5", + "description": "Seedream 4.5 is the latest in-house image generation model developed by ByteDance. Compared with Seedream 4.0, it delivers comprehensive improvements, especially in editing consistency, including better preservation of subject details,...", + "context_length": 0, + "price_prompt_rub_1m": "0", + "price_completion_rub_1m": "0", + "is_free": false, + "modality": "image", + "input_modalities": [ + "image", + "text" + ], + "price_image_rub": "4.56651027", + "price_image_unit": "image" + }, + { + "id": "deepseek/deepseek-v4-flash", + "name": "DeepSeek: DeepSeek V4 Flash 0423", + "description": "DeepSeek V4 Flash is an efficiency-optimized Mixture-of-Experts model from DeepSeek with 284B total parameters and 13B activated parameters, supporting a 1M-token context window. It is designed for fast inference and...", + "context_length": 1048576, + "price_prompt_rub_1m": "10.38881086", + "price_completion_rub_1m": "20.77762173", + "is_free": false, + "modality": "text", + "input_modalities": [ + "text" + ], + "price_image_rub": "0" + }, + { + "id": "elevenlabs/eleven-flash-v2", + "name": "ElevenLabs: Eleven Flash v2", + "description": "Eleven Flash v2 is an English-only, ultra-low-latency text-to-speech model from ElevenLabs. It is suited for conversational and real-time English use cases, and has a 30,000-character request limit.", + "context_length": 0, + "price_prompt_rub_1m": "4566.51027", + "price_completion_rub_1m": "0", + "is_free": false, + "modality": "speech", + "input_modalities": [ + "text" + ], + "price_image_rub": "0" + }, + { + "id": "elevenlabs/eleven-multilingual-v2", + "name": "ElevenLabs: Eleven Multilingual v2", + "description": "Eleven Multilingual v2 is a text-to-speech model from ElevenLabs. It is suited for lifelike, consistent long-form narration such as voice-overs and audiobooks, supports 29 languages, and has a 10,000-character request...", + "context_length": 0, + "price_prompt_rub_1m": "9133.02054", + "price_completion_rub_1m": "0", + "is_free": false, + "modality": "speech", + "input_modalities": [ + "text" + ], + "price_image_rub": "0" + }, + { + "id": "google/gemini-2.5-flash-image", + "name": "Google: Nano Banana (Gemini 2.5 Flash Image)", + "description": "Gemini 2.5 Flash Image, a.k.a. \"Nano Banana,\" is now generally available. It is a state of the art image generation model with contextual understanding. It is capable of image generation,...", + "context_length": 0, + "price_prompt_rub_1m": "0", + "price_completion_rub_1m": "0", + "is_free": false, + "modality": "image", + "input_modalities": [ + "image", + "text" + ], + "price_image_rub": "3424.8827025", + "price_image_unit": "token" + }, + { + "id": "nvidia/nemotron-3.5-embed-1b:free", + "name": "NVIDIA: Nemotron 3.5 Embed 1B (free)", + "description": "Nemotron 3.5 Embed 1B is a multimodal embedding model from NVIDIA for multilingual retrieval over text and document page images (tables, charts, infographics). Queries are text and documents can be...", + "context_length": 32768, + "price_prompt_rub_1m": "0", + "price_completion_rub_1m": "0", + "is_free": true, + "modality": "embedding", + "input_modalities": [ + "text", + "image" + ], + "price_image_rub": "0" + }, + { + "id": "nvidia/nemotron-3.5-super-vl:free", + "name": "NVIDIA: Nemotron 3.5 Super VL (free)", + "description": "NVIDIA Nemotron 3.5 Super VL is an open multimodal mixture-of-experts model from NVIDIA, with 12B active parameters out of 120B total. It takes text, images, documents, GUI screenshots, and video...", + "context_length": 1000000, + "price_prompt_rub_1m": "0", + "price_completion_rub_1m": "0", + "is_free": true, + "modality": "text", + "input_modalities": [ + "text", + "image", + "video" + ], + "price_image_rub": "0" + }, + { + "id": "openai/gpt-6-luna", + "name": "OpenAI: GPT-6 Luna", + "description": "GPT-6 Luna is the fast, cost-efficient model in OpenAI's GPT-6 series, positioned below GPT-6 Sol. It is suited for high-volume and latency-sensitive workloads such as chat, classification, and lightweight agentic...", + "context_length": 1050000, + "price_prompt_rub_1m": "11.41627568", + "price_completion_rub_1m": "57.08137838", + "is_free": false, + "modality": "text", + "input_modalities": [ + "file", + "image", + "text" + ], + "price_image_rub": "0" + }, + { + "id": "openai/whisper-1", + "name": "OpenAI: Whisper 1", + "description": "Whisper is OpenAI's open-source automatic speech recognition model, available via API as `whisper-1`. It supports transcription and translation across 50+ languages from audio files up to 25 MB. Accepts formats...", + "context_length": 0, + "price_prompt_rub_1m": "11416.275675", + "price_completion_rub_1m": "0", + "is_free": false, + "modality": "transcription", + "input_modalities": [ + "audio" + ], + "price_image_rub": "0" + }, + { + "id": "qwen/qwen3-coder-30b-a3b-instruct", + "name": "Qwen: Qwen3 Coder 30B A3B Instruct", + "description": "Qwen3-Coder-30B-A3B-Instruct is a 30.5B parameter Mixture-of-Experts (MoE) model with 128 experts (8 active per forward pass), designed for advanced code generation, repository-scale understanding, and agentic tool use. Built on the...", + "context_length": 262144, + "price_prompt_rub_1m": "7.99139297", + "price_completion_rub_1m": "31.96557189", + "is_free": false, + "modality": "text", + "input_modalities": [ + "text" + ], + "price_image_rub": "0" + }, + { + "id": "typesafe/jev-1.13.0", + "name": "Jev 1.13", + "description": "Отвечает не текстом, а калиброванной вероятностью на типизированный вопрос о поданном тексте: да/нет, выбор из вариантов, оценка по шкале. Маршрут POST /v1/systemone — это не чат, поля messages у него нет. Основной язык модели — английский, остальные языки поддержаны заметно хуже: проверяйте качество на своей выборке. Известные ограничения провайдера: модель не считает, не сравнивает даты, не решает задач, требующих числовой точности, читает инструкции буквально и не генерирует текст.", + "context_length": 64000, + "price_prompt_rub_1m": "4.79483578", + "price_completion_rub_1m": "0", + "is_free": false, + "modality": "decision", + "input_modalities": [ + "text" + ], + "price_image_rub": "0" + }, + { + "id": "typesafe/jev-latest", + "name": "Jev (последняя версия)", + "description": "Плавающий алиас: провайдер обслуживает им текущую версию Jev. Цена совпадает с закреплённой версией. Отвечает не текстом, а калиброванной вероятностью на типизированный вопрос о поданном тексте: да/нет, выбор из вариантов, оценка по шкале. Маршрут POST /v1/systemone — это не чат, поля messages у него нет. Основной язык модели — английский, остальные языки поддержаны заметно хуже: проверяйте качество на своей выборке. Известные ограничения провайдера: модель не считает, не сравнивает даты, не решает задач, требующих числовой точности, читает инструкции буквально и не генерирует текст.", + "context_length": 64000, + "price_prompt_rub_1m": "4.79483578", + "price_completion_rub_1m": "0", + "is_free": false, + "modality": "decision", + "input_modalities": [ + "text" + ], + "price_image_rub": "0" + } + ] +} diff --git a/test/fixtures/api-usage.json b/test/fixtures/api-usage.json new file mode 100644 index 0000000..cfde7c6 --- /dev/null +++ b/test/fixtures/api-usage.json @@ -0,0 +1 @@ +{"data":[{"model":"deepseek/deepseek-v4-flash","requests":5,"tokens_prompt":51230,"tokens_cached":0,"tokens_completion":11874,"amount_rub":"0.8124417"},{"model":"elevenlabs/eleven-flash-v2","requests":3,"tokens_prompt":15,"tokens_cached":0,"tokens_completion":0,"amount_rub":"0.18412076"},{"model":"openai/whisper-1","requests":2,"tokens_prompt":0,"tokens_cached":0,"tokens_completion":0,"amount_rub":"0.24310558"},{"model":"openai/gpt-6-luna","requests":2,"tokens_prompt":1022,"tokens_cached":256,"tokens_completion":611,"amount_rub":"0.05120934"},{"model":"qwen/qwen3-coder-30b-a3b-instruct","requests":1,"tokens_prompt":301,"tokens_cached":0,"tokens_completion":38,"amount_rub":"0.00371284"}],"failed_events":0,"group_by":"model","from":"2026-10-17T03:39:57Z","to":"2026-10-24T03:39:57Z"} diff --git a/test/fixtures/keys-models.json b/test/fixtures/keys-models.json new file mode 100644 index 0000000..f2e60c3 --- /dev/null +++ b/test/fixtures/keys-models.json @@ -0,0 +1,72 @@ +{ + "models": [ + { + "id": "aion-labs/aion-3.0-mini", + "name": "AionLabs: Aion-3.0-Mini" + }, + { + "id": "anthropic/claude-sonnet-5.5", + "name": "Anthropic: Claude Sonnet 5.5" + }, + { + "id": "assemblyai/universal-3-5-pro", + "name": "AssemblyAI: Universal-3.5 Pro" + }, + { + "id": "baai/bge-m3", + "name": "BAAI: bge-m3" + }, + { + "id": "black-forest-labs/flux-3-image", + "name": "Black Forest Labs: FLUX 3 Image" + }, + { + "id": "bytedance-seed/seedream-4.5", + "name": "ByteDance Seed: Seedream 4.5" + }, + { + "id": "deepseek/deepseek-v4-flash", + "name": "DeepSeek: DeepSeek V4 Flash 0423" + }, + { + "id": "elevenlabs/eleven-flash-v2", + "name": "ElevenLabs: Eleven Flash v2" + }, + { + "id": "elevenlabs/eleven-multilingual-v2", + "name": "ElevenLabs: Eleven Multilingual v2" + }, + { + "id": "google/gemini-2.5-flash-image", + "name": "Google: Nano Banana (Gemini 2.5 Flash Image)" + }, + { + "id": "nvidia/nemotron-3.5-embed-1b:free", + "name": "NVIDIA: Nemotron 3.5 Embed 1B (free)" + }, + { + "id": "nvidia/nemotron-3.5-super-vl:free", + "name": "NVIDIA: Nemotron 3.5 Super VL (free)" + }, + { + "id": "openai/gpt-6-luna", + "name": "OpenAI: GPT-6 Luna" + }, + { + "id": "openai/whisper-1", + "name": "OpenAI: Whisper 1" + }, + { + "id": "qwen/qwen3-coder-30b-a3b-instruct", + "name": "Qwen: Qwen3 Coder 30B A3B Instruct" + }, + { + "id": "typesafe/jev-1.13.0", + "name": "Jev 1.13" + }, + { + "id": "typesafe/jev-latest", + "name": "Jev (последняя версия)" + } + ] +} diff --git a/test/fixtures/v1-auth-key.json b/test/fixtures/v1-auth-key.json new file mode 100644 index 0000000..081ed60 --- /dev/null +++ b/test/fixtures/v1-auth-key.json @@ -0,0 +1 @@ +{"data":{"label":"my-agent","balance":"187.42150390","account_balance":"1234.56789012","limit":"200","limit_remaining":"187.42150390","usage":"12.57849610","limit_window":"day","limit_resets_at":"2026-10-25T00:00:00Z"}} diff --git a/test/fixtures/v1-models.json b/test/fixtures/v1-models.json new file mode 100644 index 0000000..36b91b7 --- /dev/null +++ b/test/fixtures/v1-models.json @@ -0,0 +1,436 @@ +{ + "object": "list", + "data": [ + { + "id": "aion-labs/aion-3.0-mini", + "object": "model", + "created": 1792811344, + "owned_by": "aion-labs", + "name": "AionLabs: Aion-3.0-Mini", + "description": "Aion-3.0 Mini is a multi-model roleplaying and storytelling system from AionLabs, built on the DeepSeek family of models. It uses a collaborative generation process in which multiple specialized models each...", + "context_length": 131072, + "architecture": { + "input_modalities": [ + "text" + ], + "output_modalities": [ + "text" + ], + "modality": "text->text" + }, + "pricing": { + "prompt": "0.00000099225", + "completion": "0.0000019845", + "request": "0", + "image": "0", + "input_cache_read": "0.00000025515" + }, + "top_provider": { + "context_length": 131072, + "is_moderated": false + } + }, + { + "id": "anthropic/claude-sonnet-5.5", + "object": "model", + "created": 1792811344, + "owned_by": "anthropic", + "name": "Anthropic: Claude Sonnet 5.5", + "description": "Claude Sonnet 5.5 is Anthropic's Sonnet-class model for well-scoped everyday work, succeeding Claude Sonnet 5 as a direct upgrade. It is especially strong at building features, fixing bugs, and producing...", + "context_length": 1000000, + "architecture": { + "input_modalities": [ + "text", + "image", + "file" + ], + "output_modalities": [ + "text" + ], + "modality": "text+image+file->text" + }, + "pricing": { + "prompt": "0.000002835", + "completion": "0.000014175", + "request": "0", + "image": "0", + "input_cache_read": "0.00000014175" + }, + "top_provider": { + "context_length": 1000000, + "is_moderated": false + } + }, + { + "id": "assemblyai/universal-3-5-pro", + "object": "model", + "created": 1792811344, + "owned_by": "assemblyai", + "name": "AssemblyAI: Universal-3.5 Pro", + "description": "Universal-3.5 Pro is AssemblyAI's speech-to-text model served through its Sync API, returning a complete transcript with word-level timestamps in a single synchronous response for audio up to 20 minutes. It...", + "architecture": { + "input_modalities": [ + "audio" + ], + "output_modalities": [ + "text" + ], + "modality": "audio->text" + }, + "top_provider": { + "is_moderated": false + } + }, + { + "id": "baai/bge-m3", + "object": "model", + "created": 1792811344, + "owned_by": "baai", + "name": "BAAI: bge-m3", + "description": "The bge-m3 embedding model encodes sentences, paragraphs, and long documents into a 1024-dimensional dense vector space, delivering high-quality semantic embeddings optimized for multilingual retrieval, semantic search, and large-context applications.", + "context_length": 8194, + "architecture": { + "input_modalities": [ + "text" + ], + "output_modalities": [ + "embeddings" + ], + "modality": "text->embeddings" + }, + "pricing": { + "prompt": "0.000000014175", + "completion": "0", + "request": "0", + "image": "0" + }, + "top_provider": { + "context_length": 8194, + "is_moderated": false + } + }, + { + "id": "black-forest-labs/flux-3-image", + "object": "model", + "created": 1792811344, + "owned_by": "black-forest-labs", + "name": "Black Forest Labs: FLUX 3 Image", + "description": "FLUX.3 Image is Black Forest Labs' flagship image generation and editing model. It handles text-to-image and multi-reference editing with up to 10 input images, and renders at fixed resolution tiers...", + "architecture": { + "input_modalities": [ + "text" + ], + "output_modalities": [ + "image" + ], + "modality": "text->image" + }, + "top_provider": { + "is_moderated": false + } + }, + { + "id": "bytedance-seed/seedream-4.5", + "object": "model", + "created": 1792811344, + "owned_by": "bytedance-seed", + "name": "ByteDance Seed: Seedream 4.5", + "description": "Seedream 4.5 is the latest in-house image generation model developed by ByteDance. Compared with Seedream 4.0, it delivers comprehensive improvements, especially in editing consistency, including better preservation of subject details,...", + "architecture": { + "input_modalities": [ + "text" + ], + "output_modalities": [ + "image" + ], + "modality": "text->image" + }, + "top_provider": { + "is_moderated": false + } + }, + { + "id": "deepseek/deepseek-v4-flash", + "object": "model", + "created": 1792811344, + "owned_by": "deepseek", + "name": "DeepSeek: DeepSeek V4 Flash 0423", + "description": "DeepSeek V4 Flash is an efficiency-optimized Mixture-of-Experts model from DeepSeek with 284B total parameters and 13B activated parameters, supporting a 1M-token context window. It is designed for fast inference and...", + "context_length": 1048576, + "architecture": { + "input_modalities": [ + "text" + ], + "output_modalities": [ + "text" + ], + "modality": "text->text" + }, + "pricing": { + "prompt": "0.0000001289925", + "completion": "0.000000257985", + "request": "0", + "image": "0", + "input_cache_read": "0.0000000257985" + }, + "top_provider": { + "context_length": 1048576, + "is_moderated": false + } + }, + { + "id": "elevenlabs/eleven-flash-v2", + "object": "model", + "created": 1792811344, + "owned_by": "elevenlabs", + "name": "ElevenLabs: Eleven Flash v2", + "description": "Eleven Flash v2 is an English-only, ultra-low-latency text-to-speech model from ElevenLabs. It is suited for conversational and real-time English use cases, and has a 30,000-character request limit.", + "architecture": { + "input_modalities": [ + "text" + ], + "output_modalities": [ + "speech" + ], + "modality": "text->speech" + }, + "top_provider": { + "is_moderated": false + } + }, + { + "id": "elevenlabs/eleven-multilingual-v2", + "object": "model", + "created": 1792811344, + "owned_by": "elevenlabs", + "name": "ElevenLabs: Eleven Multilingual v2", + "description": "Eleven Multilingual v2 is a text-to-speech model from ElevenLabs. It is suited for lifelike, consistent long-form narration such as voice-overs and audiobooks, supports 29 languages, and has a 10,000-character request...", + "architecture": { + "input_modalities": [ + "text" + ], + "output_modalities": [ + "speech" + ], + "modality": "text->speech" + }, + "top_provider": { + "is_moderated": false + } + }, + { + "id": "google/gemini-2.5-flash-image", + "object": "model", + "created": 1792811344, + "owned_by": "google", + "name": "Google: Nano Banana (Gemini 2.5 Flash Image)", + "description": "Gemini 2.5 Flash Image, a.k.a. \"Nano Banana,\" is now generally available. It is a state of the art image generation model with contextual understanding. It is capable of image generation,...", + "architecture": { + "input_modalities": [ + "text" + ], + "output_modalities": [ + "image" + ], + "modality": "text->image" + }, + "top_provider": { + "is_moderated": false + } + }, + { + "id": "nvidia/nemotron-3.5-embed-1b:free", + "object": "model", + "created": 1792811344, + "owned_by": "nvidia", + "name": "NVIDIA: Nemotron 3.5 Embed 1B (free)", + "description": "Nemotron 3.5 Embed 1B is a multimodal embedding model from NVIDIA for multilingual retrieval over text and document page images (tables, charts, infographics). Queries are text and documents can be...", + "context_length": 32768, + "architecture": { + "input_modalities": [ + "text", + "image" + ], + "output_modalities": [ + "embeddings" + ], + "modality": "text+image->embeddings" + }, + "pricing": { + "prompt": "0", + "completion": "0", + "request": "0", + "image": "0" + }, + "top_provider": { + "context_length": 32768, + "is_moderated": false + } + }, + { + "id": "nvidia/nemotron-3.5-super-vl:free", + "object": "model", + "created": 1792811344, + "owned_by": "nvidia", + "name": "NVIDIA: Nemotron 3.5 Super VL (free)", + "description": "NVIDIA Nemotron 3.5 Super VL is an open multimodal mixture-of-experts model from NVIDIA, with 12B active parameters out of 120B total. It takes text, images, documents, GUI screenshots, and video...", + "context_length": 1000000, + "architecture": { + "input_modalities": [ + "text", + "image", + "video" + ], + "output_modalities": [ + "text" + ], + "modality": "text+image+video->text" + }, + "pricing": { + "prompt": "0", + "completion": "0", + "request": "0", + "image": "0" + }, + "top_provider": { + "context_length": 1000000, + "is_moderated": false + } + }, + { + "id": "openai/gpt-6-luna", + "object": "model", + "created": 1792811344, + "owned_by": "openai", + "name": "OpenAI: GPT-6 Luna", + "description": "GPT-6 Luna is the fast, cost-efficient model in OpenAI's GPT-6 series, positioned below GPT-6 Sol. It is suited for high-volume and latency-sensitive workloads such as chat, classification, and lightweight agentic...", + "context_length": 1050000, + "architecture": { + "input_modalities": [ + "file", + "image", + "text" + ], + "output_modalities": [ + "text" + ], + "modality": "file+image+text->text" + }, + "pricing": { + "prompt": "0.00000014175", + "completion": "0.00000070875", + "request": "0", + "image": "0", + "input_cache_read": "0.000000014175" + }, + "top_provider": { + "context_length": 1050000, + "is_moderated": false + } + }, + { + "id": "openai/whisper-1", + "object": "model", + "created": 1792811344, + "owned_by": "openai", + "name": "OpenAI: Whisper 1", + "description": "Whisper is OpenAI's open-source automatic speech recognition model, available via API as `whisper-1`. It supports transcription and translation across 50+ languages from audio files up to 25 MB. Accepts formats...", + "architecture": { + "input_modalities": [ + "audio" + ], + "output_modalities": [ + "text" + ], + "modality": "audio->text" + }, + "top_provider": { + "is_moderated": false + } + }, + { + "id": "qwen/qwen3-coder-30b-a3b-instruct", + "object": "model", + "created": 1792811344, + "owned_by": "qwen", + "name": "Qwen: Qwen3 Coder 30B A3B Instruct", + "description": "Qwen3-Coder-30B-A3B-Instruct is a 30.5B parameter Mixture-of-Experts (MoE) model with 128 experts (8 active per forward pass), designed for advanced code generation, repository-scale understanding, and agentic tool use. Built on the...", + "context_length": 262144, + "architecture": { + "input_modalities": [ + "text" + ], + "output_modalities": [ + "text" + ], + "modality": "text->text" + }, + "pricing": { + "prompt": "0.000000099225", + "completion": "0.0000003969", + "request": "0", + "image": "0" + }, + "top_provider": { + "context_length": 262144, + "is_moderated": false + } + }, + { + "id": "typesafe/jev-1.13.0", + "object": "model", + "created": 1789984193, + "owned_by": "typesafe", + "name": "Jev 1.13", + "description": "Отвечает не текстом, а калиброванной вероятностью на типизированный вопрос о поданном тексте: да/нет, выбор из вариантов, оценка по шкале. Маршрут POST /v1/systemone — это не чат, поля messages у него нет. Основной язык модели — английский, остальные языки поддержаны заметно хуже: проверяйте качество на своей выборке. Известные ограничения провайдера: модель не считает, не сравнивает даты, не решает задач, требующих числовой точности, читает инструкции буквально и не генерирует текст.", + "context_length": 64000, + "architecture": { + "input_modalities": [ + "text" + ], + "output_modalities": [ + "text" + ], + "modality": "text->text" + }, + "pricing": { + "prompt": "0.000000059535", + "completion": "0", + "request": "0", + "image": "0" + }, + "top_provider": { + "context_length": 64000, + "is_moderated": false + } + }, + { + "id": "typesafe/jev-latest", + "object": "model", + "created": 1789984193, + "owned_by": "typesafe", + "name": "Jev (последняя версия)", + "description": "Плавающий алиас: провайдер обслуживает им текущую версию Jev. Цена совпадает с закреплённой версией. Отвечает не текстом, а калиброванной вероятностью на типизированный вопрос о поданном тексте: да/нет, выбор из вариантов, оценка по шкале. Маршрут POST /v1/systemone — это не чат, поля messages у него нет. Основной язык модели — английский, остальные языки поддержаны заметно хуже: проверяйте качество на своей выборке. Известные ограничения провайдера: модель не считает, не сравнивает даты, не решает задач, требующих числовой точности, читает инструкции буквально и не генерирует текст.", + "context_length": 64000, + "architecture": { + "input_modalities": [ + "text" + ], + "output_modalities": [ + "text" + ], + "modality": "text->text" + }, + "pricing": { + "prompt": "0.000000059535", + "completion": "0", + "request": "0", + "image": "0" + }, + "top_provider": { + "context_length": 64000, + "is_moderated": false + } + } + ] +} diff --git a/test/money.test.ts b/test/money.test.ts new file mode 100644 index 0000000..c437e59 --- /dev/null +++ b/test/money.test.ts @@ -0,0 +1,61 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { + add, + compare, + divPow10, + mul, + mulByBigInt, + parseDecimal, + round, + toFixed, + toString, +} from "../src/money.js"; + +const d = parseDecimal; + +test("0.1 + 0.2 = 0.3", () => { + assert.equal(toString(add(d("0.1"), d("0.2"))), "0.3"); +}); + +test("мелкая цена × много токенов без потери точности", () => { + assert.equal(toString(mulByBigInt(d("0.00000012"), 50_000_000n)), "6"); + assert.equal(toString(mulByBigInt(d("0.0000001289925"), 123_456_789n)), "15.9249998550825"); + // цена за 1M токенов + assert.equal(toString(divPow10(d("79.91392973"), 6)), "0.00007991392973"); + assert.equal(toString(mul(d("1.5"), d("0.25"))), "0.375"); + assert.equal(toString(mul(d("-1.5"), d("0.2"))), "-0.3"); +}); + +test("round HALF UP — от нуля на .5", () => { + assert.equal(toString(round(d("0.125"), 2)), "0.13"); + assert.equal(toString(round(d("-0.125"), 2)), "-0.13"); + assert.equal(toString(round(d("0.124999"), 2)), "0.12"); + assert.equal(toString(round(d("2.5"), 0)), "3"); + assert.equal(toString(round(d("-2.5"), 0)), "-3"); + assert.equal(toString(round(d("0.0005"), 3)), "0.001"); + assert.equal(toString(round(d("-0.0004"), 3)), "0"); + assert.equal(toFixed(d("1.005"), 2), "1.01"); + assert.equal(toFixed(d("7"), 2), "7.00"); + assert.equal(toFixed(d("-0.001"), 2), "0.00"); +}); + +test("parseDecimal строгий", () => { + assert.throws(() => parseDecimal(0.1), TypeError); + for (const bad of ["1e-7", "", "1,5", " 1", "1.", ".5", "+1", "--1", "NaN"]) { + assert.throws(() => parseDecimal(bad), SyntaxError, bad); + } + assert.equal(toString(d("-12.3400")), "-12.34"); +}); + +test("compare и канонический toString для нулей и отрицательных", () => { + assert.equal(compare(d("0"), d("0.0")), 0); + assert.equal(compare(d("0.00"), d("-0")), 0); + for (const z of ["0", "0.0", "0.00", "-0", "-0.000"]) { + assert.equal(toString(d(z)), "0", z); + } + assert.equal(compare(d("-1.5"), d("-1.49")), -1); + assert.equal(compare(d("10"), d("9.999")), 1); + assert.equal(toString(d("-0.050")), "-0.05"); + assert.equal(toString(d("100")), "100"); +}); diff --git a/test/redact.test.ts b/test/redact.test.ts new file mode 100644 index 0000000..13ea5fb --- /dev/null +++ b/test/redact.test.ts @@ -0,0 +1,72 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { redactError, redactString } from "../src/redact.js"; +import { ApiError, toToolErrorResult } from "../src/errors.js"; + +const KEY = "orr_live_AbCdEf0123456789GhIjKlMnOpQrStUv"; + +function assertClean(serialized: string): void { + assert.ok(!serialized.includes(KEY), serialized); + assert.ok(!serialized.includes(`Bearer ${KEY}`), serialized); + assert.ok(!serialized.includes(KEY.slice(9)), serialized); +} + +test("redactString маскирует Bearer и ключи", () => { + const out = redactString(`Authorization: Bearer ${KEY}; ключ=${KEY}; x${KEY}`); + assertClean(out); + assert.match(out, /Bearer \[REDACTED\]/); +}); + +test("ключ в message", () => { + const e = new Error(`request failed for ${KEY}`); + assertClean(JSON.stringify(redactError(e))); +}); + +test("ключ в cause.message не протекает", () => { + const e = new Error("outer", { cause: new Error(`inner Bearer ${KEY}`) }); + const r = redactError(e); + assertClean(JSON.stringify(r)); + assert.equal(r.message, "outer"); +}); + +test("headers/config/request/stack не копируются", () => { + const e = Object.assign(new Error("boom"), { + status: 401, + code: "invalid_key", + path: "/api/balance", + headers: { Authorization: `Bearer ${KEY}` }, + config: { headers: { Authorization: `Bearer ${KEY}` } }, + request: { auth: KEY }, + }); + e.stack = `Error: ${KEY}\n at x`; + const r = redactError(e); + assertClean(JSON.stringify(r)); + assert.deepEqual(Object.keys(r).sort(), ["code", "message", "name", "path", "status"]); + assert.equal(r.status, 401); +}); + +test("произвольные значения", () => { + assertClean(JSON.stringify(redactError(`string ${KEY}`))); + assertClean(JSON.stringify(redactError({ message: `obj ${KEY}`, headers: { a: KEY } }))); + assert.equal(redactError(null).message, "неизвестная ошибка"); + assert.equal(redactError(42).message, "неизвестная ошибка"); +}); + +test("toToolErrorResult маппит статусы и редактирует текст", () => { + const cases: Array<[number, RegExp]> = [ + [401, /недействителен или отозван/], + [403, /запрещено/], + [429, /лимит частоты/], + [503, /временно недоступен/], + ]; + for (const [status, re] of cases) { + const r = toToolErrorResult(new ApiError(status, "/api/balance", `oops ${KEY}`)); + assert.equal(r.isError, true); + assert.match(r.content[0].text, re); + assertClean(JSON.stringify(r)); + } + const other = toToolErrorResult(new ApiError(400, "/api/usage", `bad Bearer ${KEY}`)); + assert.match(other.content[0].text, /bad Bearer \[REDACTED\]/); + assertClean(JSON.stringify(other)); + assertClean(JSON.stringify(toToolErrorResult(new Error(`plain ${KEY}`)))); +}); diff --git a/test/sanitize.test.ts b/test/sanitize.test.ts new file mode 100644 index 0000000..ccac0eb --- /dev/null +++ b/test/sanitize.test.ts @@ -0,0 +1,20 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { cleanText } from "../src/sanitize.js"; + +test("убирает управляющие, bidi и zero-width символы", () => { + assert.equal(cleanText("a\u0000b\u0007c\u009Fd\u007Fe"), "abcde"); + assert.equal(cleanText("safe\u202Etxt.exe\u202C"), "safetxt.exe"); + assert.equal(cleanText("x\u2066y\u2069z\u200Bw\uFEFF"), "xyzw"); +}); + +test("схлопывает пробелы и переводы строк", () => { + assert.equal(cleanText(" a \t b\r\n\r\n\n c "), "a b\nc"); +}); + +test("обрезает по max с многоточием, не рвёт суррогатные пары", () => { + assert.equal(cleanText("abcdef", 4), "abc…"); + assert.equal(cleanText("abcd", 4), "abcd"); + assert.equal(cleanText("😀😀😀😀😀", 3), "😀😀…"); + assert.equal(Array.from(cleanText("x".repeat(1000))).length, 500); +}); diff --git a/tsconfig.build.json b/tsconfig.build.json new file mode 100644 index 0000000..fa56182 --- /dev/null +++ b/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*.ts"], + "exclude": ["test/**", "node_modules", "dist"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..b4abe49 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": [ + "ES2022" + ], + "types": [ + "node" + ], + "strict": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "outDir": "dist", + "rootDir": "src", + "declaration": false, + "sourceMap": true + }, + "include": [ + "src/**/*.ts" + ] +} diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 0000000..c8c5ea7 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "noEmit": true + }, + "include": ["src/**/*.ts", "test/**/*.ts"] +}