commit 30bc85994863eef19210bf2d8e8d9132d03763a8 Author: cloudflare[bot] <> Date: Mon Jan 12 05:55:32 2026 +0000 source repo import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d1f5f56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Local Netlify folder +.netlify +.wrangler +node_modules/ +.vercel diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..516a6b3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Tech Shrimp(技术爬爬虾) + +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/README.md b/README.md new file mode 100644 index 0000000..38c44e4 --- /dev/null +++ b/README.md @@ -0,0 +1,186 @@ +# Gemini Balance Lite +# Gemini API 代理和负载均衡无服务器轻量版(边缘函数) + +### 作者:技术爬爬虾 +[B站](https://space.bilibili.com/316183842),[Youtube](https://www.youtube.com/@Tech_Shrimp),抖音,公众号 全网同名。转载请注明作者。 + + +## 项目简介 + +Gemini API 代理, 使用边缘函数把Gemini API免费中转到国内。还可以聚合多个Gemini API Key,随机选取API Key的使用实现负载均衡,使得Gemini API免费成倍增加。 + +## Vercel部署(推荐) +[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/tech-shrimp/gemini-balance-lite) + + +1. 点击部署按钮⬆️一键部署。 +2. 国内使用需要配置自定义域名 +
+ 配置自定义域名: + + ![image](/docs/images/5.png) +
+3. 去[AIStudio](https://aistudio.google.com)申请一个免费Gemini API Key +
将API Key与自定义的域名填入AI客户端即可使用,如果有多个API Key用逗号分隔 +
+ 以Cherry Studio为例: + + ![image](/docs/images/2.png) +
+ + + + +## Deno部署 + +1. [fork](https://github.com/tech-shrimp/gemini-balance-lite/fork)本项目 +2. 登录/注册 https://dash.deno.com/ +3. 创建项目 https://dash.deno.com/new_project +4. 选择此项目,填写项目名字(请仔细填写项目名字,关系到自动分配的域名) +5. Entrypoint 填写 `src/deno_index.ts` 其他字段留空 +
+ 如图 + + ![image](/docs/images/3.png) +
+6. 点击 Deploy Project +7. 部署成功后获得域名 +8. 国内使用需要配置自定义域名 +9. 去[AIStudio](https://aistudio.google.com)申请一个免费Gemini API Key +10. 将API Key与分配的域名填入AI客户端即可使用,如果有多个API Key用逗号分隔 + +
+以Cherry Studio为例: + +![image](/docs/images/2.png) +
+ + +## Cloudflare Worker 部署 +[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/tech-shrimp/gemini-balance-lite) + +0. CF Worker有可能会分配香港的CDN节点导致无法使用(Gemini不允许香港IP连接) +0. 广东地区不建议使用Cloudflare Worker 部署 +1. 点击部署按钮 +2. 登录Cloudflare账号 +3. 链接Github账户,部署 +4. 打开dash.cloudflare.com,查看部署后的worker +6. 国内使用需要配置自定义域名 +
+配置自定义域名: + +![image](/docs/images/4.png) +
+ + +## Netlify部署 +[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/tech-shrimp/gemini-balance-lite) +
点击部署按钮,登录Github账户即可 +
免费分配域名,国内可直连。 +
但是不稳定 + +
+将分配的域名复制下来,如图: + +![image](/docs/images/1.png) +
+ +去[AIStudio](https://aistudio.google.com)申请一个免费Gemini API Key +
将API Key与分配的域名填入AI客户端即可使用,如果有多个API Key用逗号分隔 + +
+以Cherry Studio为例: + +![image](/docs/images/2.png) +
+ + + +## 打赏 +#### 帮忙点点关注点点赞,谢谢啦~ +B站:[https://space.bilibili.com/316183842](https://space.bilibili.com/316183842)
+Youtube: [https://www.youtube.com/@Tech_Shrimp](https://www.youtube.com/@Tech_Shrimp) + + +## 本地调试 + +1. 安装NodeJs +2. npm install -g vercel +3. cd 项目根目录 +4. vercel dev + +## API 说明 + + +### Gemini 代理 + +可以使用 Gemini 的原生 API 格式进行代理请求。 +**Curl 示例:** +```bash +curl -X POST --location 'https:///v1beta/models/gemini-2.5-pro:generateContent' \ +--header 'Content-Type: application/json' \ +--header 'x-goog-api-key: ,' \ +--data '{ + "contents": [ + { + "role": "user", + "parts": [ + { + "text": "Hello" + } + ] + } + ] +}' +``` +**Curl 示例:(流式)** +```bash +curl -X POST --location 'https:///v1beta/models/gemini-2.5-pro:generateContent?alt=sse' \ +--header 'Content-Type: application/json' \ +--header 'x-goog-api-key: ,' \ +--data '{ + "contents": [ + { + "role": "user", + "parts": [ + { + "text": "Hello" + } + ] + } + ] +}' +``` +> 注意: 请将 `` 替换为你的部署域名,并将 `` 替换为你的 Gemini API Ke,如果有多个用逗号分隔 + + +### API Key 校验 + +可以通过向 `/verify` 端点发送请求来校验你的 API Key 是否有效。可以一次性校验多个 Key,用逗号隔开。 + +**Curl 示例:** +```bash +curl -X POST --location 'https:///verify' \ +--header 'x-goog-api-key: ,' +``` + +### OpenAI 格式 + +本项目兼容 OpenAI 的 API 格式,你可以通过 `/chat` 或 `/chat/completions` 端点来发送请求。 + +**Curl 示例:** +```bash +curl -X POST --location 'https:///chat/completions' \ +--header 'Content-Type: application/json' \ +--header 'Authorization: Bearer ' \ +--data '{ + "model": "gpt-3.5-turbo", + "messages": [ + { + "role": "user", + "content": "你好" + } + ] +}' +``` + diff --git a/api/vercel_index.js b/api/vercel_index.js new file mode 100644 index 0000000..454c8f9 --- /dev/null +++ b/api/vercel_index.js @@ -0,0 +1,9 @@ +import { handleRequest } from "../src/handle_request.js"; + +export const config = { + runtime: 'edge' //告诉 Vercel 这是 Edge Function +}; + +export default async function handler(req) { + return handleRequest(req); +} \ No newline at end of file diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..ad6dd16 --- /dev/null +++ b/deno.lock @@ -0,0 +1,18 @@ +{ + "version": "4", + "remote": { + "https://edge.netlify.com/": "fd941d61d88673d5f28aab283fb86fcc50f08a3bc80ee5470498fcfa88c65cfb", + "https://edge.netlify.com/bootstrap/config.ts": "19dcaa5c4480175295c4dffc9be11f7c280d9391b851d30409f3e6904c34a161", + "https://edge.netlify.com/bootstrap/context.ts": "c6e9de479234f3ac500d2fc1544dadf1925e335d0d5551b1a71b7ce14423a683", + "https://edge.netlify.com/bootstrap/cookie.ts": "8b0baae708989ca183c6f3b4ab3d029e6abcbc2e43f93edeb0ff447b3bbc3a05", + "https://edge.netlify.com/bootstrap/edge_function.ts": "b8253e86aa83c67341f5cfedeba5049d77fbf84dcab7eceff7566b7728ae9b39", + "https://edge.netlify.com/bootstrap/globals/types.ts": "eaa6148ded3121d8dee62dd91c86e7fe76601df0f3ca8d7962243a30f4c8935f" + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:wrangler@^4.23.0" + ] + } + } +} diff --git a/docs/images/1.png b/docs/images/1.png new file mode 100644 index 0000000..8c9cd5a Binary files /dev/null and b/docs/images/1.png differ diff --git a/docs/images/2.png b/docs/images/2.png new file mode 100644 index 0000000..42c4b30 Binary files /dev/null and b/docs/images/2.png differ diff --git a/docs/images/3.png b/docs/images/3.png new file mode 100644 index 0000000..2a39693 Binary files /dev/null and b/docs/images/3.png differ diff --git a/docs/images/4.png b/docs/images/4.png new file mode 100644 index 0000000..3442841 Binary files /dev/null and b/docs/images/4.png differ diff --git a/docs/images/5.png b/docs/images/5.png new file mode 100644 index 0000000..ea9a4d7 Binary files /dev/null and b/docs/images/5.png differ diff --git a/netlify.toml b/netlify.toml new file mode 100644 index 0000000..1a3e095 --- /dev/null +++ b/netlify.toml @@ -0,0 +1,4 @@ +[[redirects]] + from = "/" + to = "/.netlify/functions/api" + status = 200 \ No newline at end of file diff --git a/netlify/functions/api.js b/netlify/functions/api.js new file mode 100644 index 0000000..a30b9f9 --- /dev/null +++ b/netlify/functions/api.js @@ -0,0 +1,5 @@ +import { handleRequest } from "../../src/handle_request.js"; + +export default async (req, context) => { + return handleRequest(req); +}; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c431a10 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1528 @@ +{ + "name": "gemini-balance-lite", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "devDependencies": { + "wrangler": "^4.23.0" + } + }, + "node_modules/@cloudflare/kv-asset-handler": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.0.tgz", + "integrity": "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "mime": "^3.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@cloudflare/unenv-preset": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/@cloudflare/unenv-preset/-/unenv-preset-2.3.3.tgz", + "integrity": "sha512-/M3MEcj3V2WHIRSW1eAQBPRJ6JnGQHc6JKMAPLkDb7pLs3m6X9ES/+K3ceGqxI6TKeF32AWAi7ls0AYzVxCP0A==", + "dev": true, + "license": "MIT OR Apache-2.0", + "peerDependencies": { + "unenv": "2.0.0-rc.17", + "workerd": "^1.20250508.0" + }, + "peerDependenciesMeta": { + "workerd": { + "optional": true + } + } + }, + "node_modules/@cloudflare/workerd-darwin-64": { + "version": "1.20250617.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250617.0.tgz", + "integrity": "sha512-toG8JUKVLIks4oOJLe9FeuixE84pDpMZ32ip7mCpE7JaFc5BqGFvevk0YC/db3T71AQlialjRwioH3jS/dzItA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-darwin-arm64": { + "version": "1.20250617.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250617.0.tgz", + "integrity": "sha512-JTX0exbC9/ZtMmQQA8tDZEZFMXZrxOpTUj2hHnsUkErWYkr5SSZH04RBhPg6dU4VL8bXuB5/eJAh7+P9cZAp7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-64": { + "version": "1.20250617.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250617.0.tgz", + "integrity": "sha512-8jkSoVRJ+1bOx3tuWlZCGaGCV2ew7/jFMl6V3CPXOoEtERUHsZBQLVkQIGKcmC/LKSj7f/mpyBUeu2EPTo2HEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-linux-arm64": { + "version": "1.20250617.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250617.0.tgz", + "integrity": "sha512-YAzcOyu897z5dQKFzme1oujGWMGEJCR7/Wrrm1nSP6dqutxFPTubRADM8BHn2CV3ij//vaPnAeLmZE3jVwOwig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cloudflare/workerd-windows-64": { + "version": "1.20250617.0", + "resolved": "https://registry.npmjs.org/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250617.0.tgz", + "integrity": "sha512-XWM/6sagDrO0CYDKhXhPjM23qusvIN1ju9ZEml6gOQs8tNOFnq6Cn6X9FAmnyapRFCGUSEC3HZYJAm7zwVKaMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.4.tgz", + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.4.tgz", + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.4.tgz", + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.4.tgz", + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.4.tgz", + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.4.tgz", + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.4.tgz", + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.4.tgz", + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.4.tgz", + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.4.tgz", + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.4.tgz", + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.4.tgz", + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.4.tgz", + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.4.tgz", + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.4.tgz", + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.4.tgz", + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.4.tgz", + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.4.tgz", + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.4.tgz", + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.4.tgz", + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.4.tgz", + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.4.tgz", + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.4.tgz", + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.4.tgz", + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.4.tgz", + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.0.5" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.0.4" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.2.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/as-table": { + "version": "1.0.55", + "resolved": "https://registry.npmjs.org/as-table/-/as-table-1.0.55.tgz", + "integrity": "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "printable-characters": "^1.0.42" + } + }, + "node_modules/blake3-wasm": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz", + "integrity": "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==", + "dev": true, + "license": "MIT" + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", + "integrity": "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==", + "dev": true, + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", + "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/esbuild": { + "version": "0.25.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.4.tgz", + "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.4", + "@esbuild/android-arm": "0.25.4", + "@esbuild/android-arm64": "0.25.4", + "@esbuild/android-x64": "0.25.4", + "@esbuild/darwin-arm64": "0.25.4", + "@esbuild/darwin-x64": "0.25.4", + "@esbuild/freebsd-arm64": "0.25.4", + "@esbuild/freebsd-x64": "0.25.4", + "@esbuild/linux-arm": "0.25.4", + "@esbuild/linux-arm64": "0.25.4", + "@esbuild/linux-ia32": "0.25.4", + "@esbuild/linux-loong64": "0.25.4", + "@esbuild/linux-mips64el": "0.25.4", + "@esbuild/linux-ppc64": "0.25.4", + "@esbuild/linux-riscv64": "0.25.4", + "@esbuild/linux-s390x": "0.25.4", + "@esbuild/linux-x64": "0.25.4", + "@esbuild/netbsd-arm64": "0.25.4", + "@esbuild/netbsd-x64": "0.25.4", + "@esbuild/openbsd-arm64": "0.25.4", + "@esbuild/openbsd-x64": "0.25.4", + "@esbuild/sunos-x64": "0.25.4", + "@esbuild/win32-arm64": "0.25.4", + "@esbuild/win32-ia32": "0.25.4", + "@esbuild/win32-x64": "0.25.4" + } + }, + "node_modules/exit-hook": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-2.2.1.tgz", + "integrity": "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "dev": true, + "license": "MIT" + }, + "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/get-source": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/get-source/-/get-source-2.0.12.tgz", + "integrity": "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "data-uri-to-buffer": "^2.0.0", + "source-map": "^0.6.1" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/miniflare": { + "version": "4.20250617.5", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-4.20250617.5.tgz", + "integrity": "sha512-Qqn30jR6dCjXaKVizT6vH4KOb+GyLccoxLNOJEfu63yBPn8eoXa7PrdiSGTmjs2RY8/tr7eTO8Wu/Yr14k0xVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "acorn": "8.14.0", + "acorn-walk": "8.3.2", + "exit-hook": "2.2.1", + "glob-to-regexp": "0.4.1", + "sharp": "^0.33.5", + "stoppable": "1.1.0", + "undici": "^5.28.5", + "workerd": "1.20250617.0", + "ws": "8.18.0", + "youch": "3.3.4", + "zod": "3.22.3" + }, + "bin": { + "miniflare": "bootstrap.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/mustache": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/mustache/-/mustache-4.2.0.tgz", + "integrity": "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==", + "dev": true, + "license": "MIT", + "bin": { + "mustache": "bin/mustache" + } + }, + "node_modules/ohash": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", + "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/printable-characters": { + "version": "1.0.42", + "resolved": "https://registry.npmjs.org/printable-characters/-/printable-characters-1.0.42.tgz", + "integrity": "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==", + "dev": true, + "license": "Unlicense" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.6.3" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stacktracey": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/stacktracey/-/stacktracey-2.1.8.tgz", + "integrity": "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==", + "dev": true, + "license": "Unlicense", + "dependencies": { + "as-table": "^1.0.36", + "get-source": "^2.0.12" + } + }, + "node_modules/stoppable": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", + "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4", + "npm": ">=6" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/undici": { + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/unenv": { + "version": "2.0.0-rc.17", + "resolved": "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.17.tgz", + "integrity": "sha512-B06u0wXkEd+o5gOCMl/ZHl5cfpYbDZKAT+HWTL+Hws6jWu7dCiqBBXXXzMFcFVJb8D4ytAnYmxJA83uwOQRSsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "exsolve": "^1.0.4", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "ufo": "^1.6.1" + } + }, + "node_modules/workerd": { + "version": "1.20250617.0", + "resolved": "https://registry.npmjs.org/workerd/-/workerd-1.20250617.0.tgz", + "integrity": "sha512-Uv6p0PYUHp/W/aWfUPLkZVAoAjapisM27JJlwcX9wCPTfCfnuegGOxFMvvlYpmNaX4YCwEdLCwuNn3xkpSkuZw==", + "dev": true, + "hasInstallScript": true, + "license": "Apache-2.0", + "bin": { + "workerd": "bin/workerd" + }, + "engines": { + "node": ">=16" + }, + "optionalDependencies": { + "@cloudflare/workerd-darwin-64": "1.20250617.0", + "@cloudflare/workerd-darwin-arm64": "1.20250617.0", + "@cloudflare/workerd-linux-64": "1.20250617.0", + "@cloudflare/workerd-linux-arm64": "1.20250617.0", + "@cloudflare/workerd-windows-64": "1.20250617.0" + } + }, + "node_modules/wrangler": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/wrangler/-/wrangler-4.23.0.tgz", + "integrity": "sha512-JSeDt3IwA4TEmg/V3tRblImPjdxynBt9PUVO/acQJ83XGlMMSwswDKL1FuwvbFzgX6+JXc3GMHeu7r8AQIxw9w==", + "dev": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "@cloudflare/kv-asset-handler": "0.4.0", + "@cloudflare/unenv-preset": "2.3.3", + "blake3-wasm": "2.1.5", + "esbuild": "0.25.4", + "miniflare": "4.20250617.5", + "path-to-regexp": "6.3.0", + "unenv": "2.0.0-rc.17", + "workerd": "1.20250617.0" + }, + "bin": { + "wrangler": "bin/wrangler.js", + "wrangler2": "bin/wrangler.js" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@cloudflare/workers-types": "^4.20250617.0" + }, + "peerDependenciesMeta": { + "@cloudflare/workers-types": { + "optional": true + } + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/youch": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", + "integrity": "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cookie": "^0.7.1", + "mustache": "^4.2.0", + "stacktracey": "^2.1.8" + } + }, + "node_modules/zod": { + "version": "3.22.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.3.tgz", + "integrity": "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..9cfd25e --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "wrangler": "^4.23.0" + },"name": "gemini-balance-lite" +} diff --git a/src/deno_index.ts b/src/deno_index.ts new file mode 100644 index 0000000..7c254be --- /dev/null +++ b/src/deno_index.ts @@ -0,0 +1,9 @@ +import { handleRequest } from "./handle_request.js"; + +async function denoHandleRequest(req: Request): Promise { + const url = new URL(req.url); + console.log('Request URL:', req.url); + return handleRequest(req); +}; + +Deno.serve({ port: 80 },denoHandleRequest); \ No newline at end of file diff --git a/src/handle_request.js b/src/handle_request.js new file mode 100644 index 0000000..a1a008e --- /dev/null +++ b/src/handle_request.js @@ -0,0 +1,81 @@ +import { handleVerification } from './verify_keys.js'; +import openai from './openai.mjs'; + +export async function handleRequest(request) { + + const url = new URL(request.url); + const pathname = url.pathname; + const search = url.search; + + if (pathname === '/' || pathname === '/index.html') { + return new Response('Proxy is Running! More Details: https://github.com/tech-shrimp/gemini-balance-lite', { + status: 200, + headers: { 'Content-Type': 'text/html' } + }); + } + + if (pathname === '/verify' && request.method === 'POST') { + return handleVerification(request); + } + + // 处理OpenAI格式请求 + if (url.pathname.endsWith("/chat/completions") || url.pathname.endsWith("/completions") || url.pathname.endsWith("/embeddings") || url.pathname.endsWith("/models")) { + return openai.fetch(request); + } + + const targetUrl = `https://generativelanguage.googleapis.com${pathname}${search}`; + + try { + const headers = new Headers(); + for (const [key, value] of request.headers.entries()) { + if (key.trim().toLowerCase() === 'x-goog-api-key') { + const apiKeys = value.split(',').map(k => k.trim()).filter(k => k); + if (apiKeys.length > 0) { + const selectedKey = apiKeys[Math.floor(Math.random() * apiKeys.length)]; + console.log(`Gemini Selected API Key: ${selectedKey}`); + headers.set('x-goog-api-key', selectedKey); + } + } else { + if (key.trim().toLowerCase()==='content-type') + { + headers.set(key, value); + } + } + } + + console.log('Request Sending to Gemini') + console.log('targetUrl:'+targetUrl) + console.log(headers) + + const response = await fetch(targetUrl, { + method: request.method, + headers: headers, + body: request.body + }); + + console.log("Call Gemini Success") + + const responseHeaders = new Headers(response.headers); + + console.log('Header from Gemini:') + console.log(responseHeaders) + + responseHeaders.delete('transfer-encoding'); + responseHeaders.delete('connection'); + responseHeaders.delete('keep-alive'); + responseHeaders.delete('content-encoding'); + responseHeaders.set('Referrer-Policy', 'no-referrer'); + + return new Response(response.body, { + status: response.status, + headers: responseHeaders + }); + + } catch (error) { + console.error('Failed to fetch:', error); + return new Response('Internal Server Error\n' + error?.stack, { + status: 500, + headers: { 'Content-Type': 'text/plain' } + }); +} +}; diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..52cdae7 --- /dev/null +++ b/src/index.js @@ -0,0 +1,9 @@ + import { handleRequest } from "./handle_request.js"; + + export default { + async fetch (req, env, context) { + const url = new URL(req.url); + console.log('Request URL:', req.url); + return handleRequest(req); + } + } \ No newline at end of file diff --git a/src/openai.mjs b/src/openai.mjs new file mode 100644 index 0000000..fca7679 --- /dev/null +++ b/src/openai.mjs @@ -0,0 +1,681 @@ +//Author: PublicAffairs +//Project: https://github.com/PublicAffairs/openai-gemini +//MIT License : https://github.com/PublicAffairs/openai-gemini/blob/main/LICENSE + + +import { Buffer } from "node:buffer"; + +export default { + async fetch (request) { + if (request.method === "OPTIONS") { + return handleOPTIONS(); + } + const errHandler = (err) => { + console.error(err); + return new Response(err.message, fixCors({ status: err.status ?? 500 })); + }; + try { + const auth = request.headers.get("Authorization"); + let apiKey = auth?.split(" ")[1]; + if (apiKey && apiKey.includes(',')) { + const apiKeys = apiKey.split(',').map(k => k.trim()).filter(k => k); + apiKey = apiKeys[Math.floor(Math.random() * apiKeys.length)]; + console.log(`OpenAI Selected API Key: ${apiKey}`); + } + const assert = (success) => { + if (!success) { + throw new HttpError("The specified HTTP method is not allowed for the requested resource", 400); + } + }; + const { pathname } = new URL(request.url); + switch (true) { + case pathname.endsWith("/chat/completions"): + assert(request.method === "POST"); + return handleCompletions(await request.json(), apiKey) + .catch(errHandler); + case pathname.endsWith("/embeddings"): + assert(request.method === "POST"); + return handleEmbeddings(await request.json(), apiKey) + .catch(errHandler); + case pathname.endsWith("/models"): + assert(request.method === "GET"); + return handleModels(apiKey) + .catch(errHandler); + default: + throw new HttpError("404 Not Found", 404); + } + } catch (err) { + return errHandler(err); + } + } +}; + +class HttpError extends Error { + constructor(message, status) { + super(message); + this.name = this.constructor.name; + this.status = status; + } +} + +const fixCors = ({ headers, status, statusText }) => { + headers = new Headers(headers); + headers.set("Access-Control-Allow-Origin", "*"); + return { headers, status, statusText }; +}; + +const handleOPTIONS = async () => { + return new Response(null, { + headers: { + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Methods": "*", + "Access-Control-Allow-Headers": "*", + } + }); +}; + +const BASE_URL = "https://generativelanguage.googleapis.com"; +const API_VERSION = "v1beta"; + +// https://github.com/google-gemini/generative-ai-js/blob/cf223ff4a1ee5a2d944c53cddb8976136382bee6/src/requests/request.ts#L71 +const API_CLIENT = "genai-js/0.21.0"; // npm view @google/generative-ai version +const makeHeaders = (apiKey, more) => ({ + "x-goog-api-client": API_CLIENT, + ...(apiKey && { "x-goog-api-key": apiKey }), + ...more +}); + +async function handleModels (apiKey) { + const response = await fetch(`${BASE_URL}/${API_VERSION}/models`, { + headers: makeHeaders(apiKey), + }); + let { body } = response; + if (response.ok) { + const { models } = JSON.parse(await response.text()); + body = JSON.stringify({ + object: "list", + data: models.map(({ name }) => ({ + id: name.replace("models/", ""), + object: "model", + created: 0, + owned_by: "", + })), + }, null, " "); + } + return new Response(body, fixCors(response)); +} + +const DEFAULT_EMBEDDINGS_MODEL = "text-embedding-004"; +async function handleEmbeddings (req, apiKey) { + if (typeof req.model !== "string") { + throw new HttpError("model is not specified", 400); + } + let model; + if (req.model.startsWith("models/")) { + model = req.model; + } else { + if (!req.model.startsWith("gemini-")) { + req.model = DEFAULT_EMBEDDINGS_MODEL; + } + model = "models/" + req.model; + } + if (!Array.isArray(req.input)) { + req.input = [ req.input ]; + } + const response = await fetch(`${BASE_URL}/${API_VERSION}/${model}:batchEmbedContents`, { + method: "POST", + headers: makeHeaders(apiKey, { "Content-Type": "application/json" }), + body: JSON.stringify({ + "requests": req.input.map(text => ({ + model, + content: { parts: { text } }, + outputDimensionality: req.dimensions, + })) + }) + }); + let { body } = response; + if (response.ok) { + const { embeddings } = JSON.parse(await response.text()); + body = JSON.stringify({ + object: "list", + data: embeddings.map(({ values }, index) => ({ + object: "embedding", + index, + embedding: values, + })), + model: req.model, + }, null, " "); + } + return new Response(body, fixCors(response)); +} + +const DEFAULT_MODEL = "gemini-2.5-flash"; +async function handleCompletions (req, apiKey) { + let model = DEFAULT_MODEL; + switch (true) { + case typeof req.model !== "string": + break; + case req.model.startsWith("models/"): + model = req.model.substring(7); + break; + case req.model.startsWith("gemini-"): + case req.model.startsWith("gemma-"): + case req.model.startsWith("learnlm-"): + model = req.model; + } + let body = await transformRequest(req); + const extra = req.extra_body?.google + if (extra) { + if (extra.safety_settings) { + body.safetySettings = extra.safety_settings; + } + if (extra.cached_content) { + body.cachedContent = extra.cached_content; + } + if (extra.thinking_config) { + body.generationConfig.thinkingConfig = extra.thinking_config; + } + } + switch (true) { + case model.endsWith(":search"): + model = model.substring(0, model.length - 7); + // eslint-disable-next-line no-fallthrough + case req.model.endsWith("-search-preview"): + case req.tools?.some(tool => tool.function?.name === 'googleSearch'): + body.tools = body.tools || []; + body.tools.push({googleSearch: {}}); + } + console.log(body.tools) + const TASK = req.stream ? "streamGenerateContent" : "generateContent"; + let url = `${BASE_URL}/${API_VERSION}/models/${model}:${TASK}`; + if (req.stream) { url += "?alt=sse"; } + const response = await fetch(url, { + method: "POST", + headers: makeHeaders(apiKey, { "Content-Type": "application/json" }), + body: JSON.stringify(body), + }); + + body = response.body; + if (response.ok) { + let id = "chatcmpl-" + generateId(); //"chatcmpl-8pMMaqXMK68B3nyDBrapTDrhkHBQK"; + const shared = {}; + if (req.stream) { + body = response.body + .pipeThrough(new TextDecoderStream()) + .pipeThrough(new TransformStream({ + transform: parseStream, + flush: parseStreamFlush, + buffer: "", + shared, + })) + .pipeThrough(new TransformStream({ + transform: toOpenAiStream, + flush: toOpenAiStreamFlush, + streamIncludeUsage: req.stream_options?.include_usage, + model, id, last: [], + shared, + })) + .pipeThrough(new TextEncoderStream()); + } else { + body = await response.text(); + try { + body = JSON.parse(body); + if (!body.candidates) { + throw new Error("Invalid completion object"); + } + } catch (err) { + console.error("Error parsing response:", err); + return new Response(body, fixCors(response)); // output as is + } + body = processCompletionsResponse(body, model, id); + } + } + return new Response(body, fixCors(response)); +} + +const adjustProps = (schemaPart) => { + if (typeof schemaPart !== "object" || schemaPart === null) { + return; + } + if (Array.isArray(schemaPart)) { + schemaPart.forEach(adjustProps); + } else { + if (schemaPart.type === "object" && schemaPart.properties && schemaPart.additionalProperties === false) { + delete schemaPart.additionalProperties; + } + Object.values(schemaPart).forEach(adjustProps); + } +}; +const adjustSchema = (schema) => { + const obj = schema[schema.type]; + delete obj.strict; + return adjustProps(schema); +}; + +const harmCategory = [ + "HARM_CATEGORY_HATE_SPEECH", + "HARM_CATEGORY_SEXUALLY_EXPLICIT", + "HARM_CATEGORY_DANGEROUS_CONTENT", + "HARM_CATEGORY_HARASSMENT", + "HARM_CATEGORY_CIVIC_INTEGRITY", +]; +const safetySettings = harmCategory.map(category => ({ + category, + threshold: "BLOCK_NONE", +})); +const fieldsMap = { + frequency_penalty: "frequencyPenalty", + max_completion_tokens: "maxOutputTokens", + max_tokens: "maxOutputTokens", + n: "candidateCount", // not for streaming + presence_penalty: "presencePenalty", + seed: "seed", + stop: "stopSequences", + temperature: "temperature", + top_k: "topK", // non-standard + top_p: "topP", +}; +const thinkingBudgetMap = { + low: 1024, + medium: 8192, + high: 24576, +}; +const transformConfig = (req) => { + let cfg = {}; + //if (typeof req.stop === "string") { req.stop = [req.stop]; } // no need + for (let key in req) { + const matchedKey = fieldsMap[key]; + if (matchedKey) { + cfg[matchedKey] = req[key]; + } + } + if (req.response_format) { + switch (req.response_format.type) { + case "json_schema": + adjustSchema(req.response_format); + cfg.responseSchema = req.response_format.json_schema?.schema; + if (cfg.responseSchema && "enum" in cfg.responseSchema) { + cfg.responseMimeType = "text/x.enum"; + break; + } + // eslint-disable-next-line no-fallthrough + case "json_object": + cfg.responseMimeType = "application/json"; + break; + case "text": + cfg.responseMimeType = "text/plain"; + break; + default: + throw new HttpError("Unsupported response_format.type", 400); + } + } + if (req.reasoning_effort) { + cfg.thinkingConfig = { thinkingBudget: thinkingBudgetMap[req.reasoning_effort] }; + } + return cfg; +}; + +const parseImg = async (url) => { + let mimeType, data; + if (url.startsWith("http://") || url.startsWith("https://")) { + try { + const response = await fetch(url); + if (!response.ok) { + throw new Error(`${response.status} ${response.statusText} (${url})`); + } + mimeType = response.headers.get("content-type"); + data = Buffer.from(await response.arrayBuffer()).toString("base64"); + } catch (err) { + throw new Error("Error fetching image: " + err.toString()); + } + } else { + const match = url.match(/^data:(?.*?)(;base64)?,(?.*)$/); + if (!match) { + throw new HttpError("Invalid image data: " + url, 400); + } + ({ mimeType, data } = match.groups); + } + return { + inlineData: { + mimeType, + data, + }, + }; +}; + +const transformFnResponse = ({ content, tool_call_id }, parts) => { + if (!parts.calls) { + throw new HttpError("No function calls found in the previous message", 400); + } + let response; + try { + response = JSON.parse(content); + } catch (err) { + console.error("Error parsing function response content:", err); + throw new HttpError("Invalid function response: " + content, 400); + } + if (typeof response !== "object" || response === null || Array.isArray(response)) { + response = { result: response }; + } + if (!tool_call_id) { + throw new HttpError("tool_call_id not specified", 400); + } + const { i, name } = parts.calls[tool_call_id] ?? {}; + if (!name) { + throw new HttpError("Unknown tool_call_id: " + tool_call_id, 400); + } + if (parts[i]) { + throw new HttpError("Duplicated tool_call_id: " + tool_call_id, 400); + } + parts[i] = { + functionResponse: { + id: tool_call_id.startsWith("call_") ? null : tool_call_id, + name, + response, + } + }; +}; + +const transformFnCalls = ({ tool_calls }) => { + const calls = {}; + const parts = tool_calls.map(({ function: { arguments: argstr, name }, id, type }, i) => { + if (type !== "function") { + throw new HttpError(`Unsupported tool_call type: "${type}"`, 400); + } + let args; + try { + args = JSON.parse(argstr); + } catch (err) { + console.error("Error parsing function arguments:", err); + throw new HttpError("Invalid function arguments: " + argstr, 400); + } + calls[id] = {i, name}; + return { + functionCall: { + id: id.startsWith("call_") ? null : id, + name, + args, + } + }; + }); + parts.calls = calls; + return parts; +}; + +const transformMsg = async ({ content }) => { + const parts = []; + if (!Array.isArray(content)) { + // system, user: string + // assistant: string or null (Required unless tool_calls is specified.) + parts.push({ text: content }); + return parts; + } + // user: + // An array of content parts with a defined type. + // Supported options differ based on the model being used to generate the response. + // Can contain text, image, or audio inputs. + for (const item of content) { + switch (item.type) { + case "text": + parts.push({ text: item.text }); + break; + case "image_url": + parts.push(await parseImg(item.image_url.url)); + break; + case "input_audio": + parts.push({ + inlineData: { + mimeType: "audio/" + item.input_audio.format, + data: item.input_audio.data, + } + }); + break; + default: + throw new HttpError(`Unknown "content" item type: "${item.type}"`, 400); + } + } + if (content.every(item => item.type === "image_url")) { + parts.push({ text: "" }); // to avoid "Unable to submit request because it must have a text parameter" + } + return parts; +}; + +const transformMessages = async (messages) => { + if (!messages) { return; } + const contents = []; + let system_instruction; + for (const item of messages) { + switch (item.role) { + case "system": + system_instruction = { parts: await transformMsg(item) }; + continue; + case "tool": + // eslint-disable-next-line no-case-declarations + let { role, parts } = contents[contents.length - 1] ?? {}; + if (role !== "function") { + const calls = parts?.calls; + parts = []; parts.calls = calls; + contents.push({ + role: "function", // ignored + parts + }); + } + transformFnResponse(item, parts); + continue; + case "assistant": + item.role = "model"; + break; + case "user": + break; + default: + throw new HttpError(`Unknown message role: "${item.role}"`, 400); + } + contents.push({ + role: item.role, + parts: item.tool_calls ? transformFnCalls(item) : await transformMsg(item) + }); + } + if (system_instruction) { + if (!contents[0]?.parts.some(part => part.text)) { + contents.unshift({ role: "user", parts: { text: " " } }); + } + } + //console.info(JSON.stringify(contents, 2)); + return { system_instruction, contents }; +}; + +const transformTools = (req) => { + let tools, tool_config; + if (req.tools) { + const funcs = req.tools.filter(tool => tool.type === "function" && tool.function?.name !== 'googleSearch'); + if (funcs.length > 0) { + funcs.forEach(adjustSchema); + tools = [{ function_declarations: funcs.map(schema => schema.function) }]; + } + } + if (req.tool_choice) { + const allowed_function_names = req.tool_choice?.type === "function" ? [ req.tool_choice?.function?.name ] : undefined; + if (allowed_function_names || typeof req.tool_choice === "string") { + tool_config = { + function_calling_config: { + mode: allowed_function_names ? "ANY" : req.tool_choice.toUpperCase(), + allowed_function_names + } + }; + } + } + return { tools, tool_config }; +}; + +const transformRequest = async (req) => ({ + ...await transformMessages(req.messages), + safetySettings, + generationConfig: transformConfig(req), + ...transformTools(req), +}); + +const generateId = () => { + const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + const randomChar = () => characters[Math.floor(Math.random() * characters.length)]; + return Array.from({ length: 29 }, randomChar).join(""); +}; + +const reasonsMap = { //https://ai.google.dev/api/rest/v1/GenerateContentResponse#finishreason + //"FINISH_REASON_UNSPECIFIED": // Default value. This value is unused. + "STOP": "stop", + "MAX_TOKENS": "length", + "SAFETY": "content_filter", + "RECITATION": "content_filter", + //"OTHER": "OTHER", +}; +const SEP = "\n\n|>"; +const transformCandidates = (key, cand) => { + const message = { role: "assistant", content: [] }; + for (const part of cand.content?.parts ?? []) { + if (part.functionCall) { + const fc = part.functionCall; + message.tool_calls = message.tool_calls ?? []; + message.tool_calls.push({ + id: fc.id ?? "call_" + generateId(), + type: "function", + function: { + name: fc.name, + arguments: JSON.stringify(fc.args), + } + }); + } else { + message.content.push(part.text); + } + } + message.content = message.content.join(SEP) || null; + return { + index: cand.index || 0, // 0-index is absent in new -002 models response + [key]: message, + logprobs: null, + finish_reason: message.tool_calls ? "tool_calls" : reasonsMap[cand.finishReason] || cand.finishReason, + //original_finish_reason: cand.finishReason, + }; +}; +const transformCandidatesMessage = transformCandidates.bind(null, "message"); +const transformCandidatesDelta = transformCandidates.bind(null, "delta"); + +const transformUsage = (data) => ({ + completion_tokens: data.candidatesTokenCount, + prompt_tokens: data.promptTokenCount, + total_tokens: data.totalTokenCount +}); + +const checkPromptBlock = (choices, promptFeedback, key) => { + if (choices.length) { return; } + if (promptFeedback?.blockReason) { + console.log("Prompt block reason:", promptFeedback.blockReason); + if (promptFeedback.blockReason === "SAFETY") { + promptFeedback.safetyRatings + .filter(r => r.blocked) + .forEach(r => console.log(r)); + } + choices.push({ + index: 0, + [key]: null, + finish_reason: "content_filter", + //original_finish_reason: data.promptFeedback.blockReason, + }); + } + return true; +}; + +const processCompletionsResponse = (data, model, id) => { + const obj = { + id, + choices: data.candidates.map(transformCandidatesMessage), + created: Math.floor(Date.now()/1000), + model: data.modelVersion ?? model, + //system_fingerprint: "fp_69829325d0", + object: "chat.completion", + usage: data.usageMetadata && transformUsage(data.usageMetadata), + }; + if (obj.choices.length === 0 ) { + checkPromptBlock(obj.choices, data.promptFeedback, "message"); + } + return JSON.stringify(obj); +}; + +const responseLineRE = /^data: (.*)(?:\n\n|\r\r|\r\n\r\n)/; +function parseStream (chunk, controller) { + this.buffer += chunk; + do { + const match = this.buffer.match(responseLineRE); + if (!match) { break; } + controller.enqueue(match[1]); + this.buffer = this.buffer.substring(match[0].length); + } while (true); // eslint-disable-line no-constant-condition +} +function parseStreamFlush (controller) { + if (this.buffer) { + console.error("Invalid data:", this.buffer); + controller.enqueue(this.buffer); + this.shared.is_buffers_rest = true; + } +} + +const delimiter = "\n\n"; +const sseline = (obj) => { + obj.created = Math.floor(Date.now()/1000); + return "data: " + JSON.stringify(obj) + delimiter; +}; +function toOpenAiStream (line, controller) { + let data; + try { + data = JSON.parse(line); + if (!data.candidates) { + throw new Error("Invalid completion chunk object"); + } + } catch (err) { + console.error("Error parsing response:", err); + if (!this.shared.is_buffers_rest) { line =+ delimiter; } + controller.enqueue(line); // output as is + return; + } + const obj = { + id: this.id, + choices: data.candidates.map(transformCandidatesDelta), + //created: Math.floor(Date.now()/1000), + model: data.modelVersion ?? this.model, + //system_fingerprint: "fp_69829325d0", + object: "chat.completion.chunk", + usage: data.usageMetadata && this.streamIncludeUsage ? null : undefined, + }; + if (checkPromptBlock(obj.choices, data.promptFeedback, "delta")) { + controller.enqueue(sseline(obj)); + return; + } + console.assert(data.candidates.length === 1, "Unexpected candidates count: %d", data.candidates.length); + const cand = obj.choices[0]; + cand.index = cand.index || 0; // absent in new -002 models response + const finish_reason = cand.finish_reason; + cand.finish_reason = null; + if (!this.last[cand.index]) { // first + controller.enqueue(sseline({ + ...obj, + choices: [{ ...cand, tool_calls: undefined, delta: { role: "assistant", content: "" } }], + })); + } + delete cand.delta.role; + if ("content" in cand.delta) { // prevent empty data (e.g. when MAX_TOKENS) + controller.enqueue(sseline(obj)); + } + cand.finish_reason = finish_reason; + if (data.usageMetadata && this.streamIncludeUsage) { + obj.usage = transformUsage(data.usageMetadata); + } + cand.delta = {}; + this.last[cand.index] = obj; +} +function toOpenAiStreamFlush (controller) { + if (this.last.length > 0) { + for (const obj of this.last) { + controller.enqueue(sseline(obj)); + } + controller.enqueue("data: [DONE]" + delimiter); + } +} diff --git a/src/verify_keys.js b/src/verify_keys.js new file mode 100644 index 0000000..1ebc08b --- /dev/null +++ b/src/verify_keys.js @@ -0,0 +1,65 @@ +async function verifyKey(key, controller) { + const url = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent'; + const body = { + "contents": [{ + "role": "user", + "parts": [{ + "text": "Hello" + }] + }] + }; + let result; + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-goog-api-key': key, + }, + body: JSON.stringify(body), + }); + if (response.ok) { + await response.text(); // Consume body to release connection + result = { key: `${key.slice(0, 7)}......${key.slice(-7)}`, status: 'GOOD' }; + } else { + const errorData = await response.json().catch(() => ({ error: { message: 'Unknown error' } })); + result = { key: `${key.slice(0, 7)}......${key.slice(-7)}`, status: 'BAD', error: errorData.error.message }; + } + } catch (e) { + result = { key: `${key.slice(0, 7)}......${key.slice(-7)}`, status: 'ERROR', error: e.message }; + } + controller.enqueue(new TextEncoder().encode('data: ' + JSON.stringify(result) + '\n\n')); +} + +export async function handleVerification(request) { + try { + const authHeader = request.headers.get('x-goog-api-key'); + if (!authHeader) { + return new Response(JSON.stringify({ error: 'Missing x-goog-api-key header.' }), { + status: 400, + headers: { 'Content-Type': 'application/json' }, + }); + } + const keys = authHeader.split(',').map(k => k.trim()).filter(Boolean); + + const stream = new ReadableStream({ + async start(controller) { + const verificationPromises = keys.map(key => verifyKey(key, controller)); + await Promise.all(verificationPromises); + controller.close(); + } + }); + + return new Response(stream, { + status: 200, + headers: { + 'Content-Type': 'text/event-stream', + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + } + }); + + } catch (e) { + return new Response(JSON.stringify({ error: 'An unexpected error occurred: ' + e.message }), { status: 500, headers: { 'Content-Type': 'application/json' } }); + } +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..a81bf0a --- /dev/null +++ b/vercel.json @@ -0,0 +1,5 @@ +{ + "routes": [ + { "src": "/(.*)", "dest": "/api/vercel_index.js" } + ] +} \ No newline at end of file diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..f1a94ba --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,5 @@ +name = "gemini-balance-lite" +# 主入口 +main = "src/index.js" +compatibility_date = "2025-06-17" +compatibility_flags = ["nodejs_compat"]