diff --git a/.github/workflows/servers_ts.yaml b/.github/workflows/servers_ts.yaml index 40181a5..72d3a87 100644 --- a/.github/workflows/servers_ts.yaml +++ b/.github/workflows/servers_ts.yaml @@ -29,6 +29,7 @@ jobs: - youtube - x-scraper - linkedin-scraper + - telegram-bot defaults: run: working-directory: servers/${{ matrix.server }} diff --git a/servers/README.md b/servers/README.md index c31a732..b125ccb 100644 --- a/servers/README.md +++ b/servers/README.md @@ -35,4 +35,5 @@ OpenAPI→MCP connectors also land here as fleet servers - design in |--------|---------|--------|-----------| | [`image-host/`](./image-host) | TS · Cloudflare Worker + R2 | built + tested (unit + workerd integration) | `bearer` | | [`reddit/`](./reddit) | TS · Cloudflare Worker (Apify-backed) | built + tested (unit + workerd integration) | `edison-jwt` | +| [`telegram-bot/`](./telegram-bot) | TS · Cloudflare Worker (stateless, Bot API) | built + tested (unit + workerd integration) | `edison-jwt` + user's bot token header | | [`adb/`](./adb) | TS · stdio (npx `@sealgate/android-adb-mcp`) | built + tested (unit) | `none` | diff --git a/servers/telegram-bot/README.md b/servers/telegram-bot/README.md new file mode 100644 index 0000000..0737971 --- /dev/null +++ b/servers/telegram-bot/README.md @@ -0,0 +1,88 @@ +# `telegram-bot` - Edison first-party MCP server + +Drive your own Telegram bot through the Bot API: read the messages it receives, +look up chats, and send, edit, forward, react to, pin, and delete messages. + +- **Runtime:** TypeScript on a Cloudflare Worker, **stateless** (no Durable Object). +- **Transport:** streamable HTTP at `/mcp`, JSON responses, no session id. +- **Credential:** each user's own bot token from [@BotFather](https://t.me/BotFather), + sent as the `X-Telegram-Bot-Token` header. SealGate stores it in its + zero-knowledge template values (catalog `headers` + `template_fields`, beside + the `edison-jwt` gate) and injects it on + every call. +- **Fleet auth:** `edison-jwt` in production, like the other hosted connectors. + SealGate mints a per-user JWT for every call, so the public endpoint is not an + open relay and usage is attributable. The bot token rides beside it; the JWT + owns `Authorization`, the token has its own header. Self-hosters can use + `bearer` or `open` instead. + +``` + AI client ──► SealGate (policies, trifecta tracking, encrypted token) + │ X-Telegram-Bot-Token: + ▼ + telegram.sealgate.ai/mcp (one McpServer per request, then dropped) + │ + ▼ + api.telegram.org/bot/ +``` + +## Why stateless + +`McpAgent` persists its `props` into Durable Object storage, so passing the +token that way would write a live credential to disk. Instead each request +builds its own `McpServer` closed over that request's token and discards it +after responding. The Bot API is itself stateless, so nothing is lost. The token +is checked for shape (`:`, which also stops it reshaping the URL +it is placed in), validated with `getMe` on `initialize` (a rejected token is a +401 at install time), and never logged or echoed in an error. + +## Tools + +| tool | Bot API | notes | +|------|---------|-------| +| `telegram_get_me` | `getMe` | Which bot is connected. | +| `telegram_get_updates` | `getUpdates` | Unacknowledged messages/events (Telegram keeps 24h). Returns `next_offset`; pass it back as `offset` to acknowledge. Short poll (`timeout: 0`). | +| `telegram_get_chat` | `getChat` + `getChatMemberCount` | Type, title, username, description, member count, pinned message. | +| `telegram_send_message` | `sendMessage` | Text up to 4096 chars, optional `parse_mode`, reply, forum topic, silent, no link preview. | +| `telegram_send_photo` | `sendPhoto` | By `https://` URL (Telegram fetches it) or `file_id`. | +| `telegram_send_document` | `sendDocument` | Same sources as photo. | +| `telegram_forward_message` | `forwardMessage` | Between chats the bot can see. | +| `telegram_edit_message_text` | `editMessageText` | The bot's own messages. | +| `telegram_delete_message` | `deleteMessage` | Own messages, or others' as a group admin (48h). | +| `telegram_set_reaction` | `setMessageReaction` | One emoji, or omit to clear. | +| `telegram_pin_message` | `pinChatMessage` | Needs pin rights in groups. | +| `telegram_unpin_message` | `unpinChatMessage` | Same rights as pin. | + +`chat_id` is an integer id (negative for groups/channels) or a public +`@username`; it is validated in the input schema, so a bad value never reaches +Telegram. The text and caption caps (4096 / 1024) apply to the raw string; with +`parse_mode` set Telegram counts after parsing the markup, so a message near the +limit with heavy markup may need trimming. Messages come back normalized: `message_id`, ISO `date`, `chat`, +`from`, `text` (or caption), `reply_to_message_id`, `message_thread_id`, and +`media` (`kind`, `file_id`, `file_name`, `mime_type`). + +## Limits of a bot + +- **No history.** Bots only see messages sent after they joined, through + `getUpdates`. Older chat history is not readable with the Bot API. +- **One consumer.** Telegram delivers each update once. `getUpdates` fails with + 409 while the bot has a webhook, or while another process polls it. Use a + dedicated bot, never SealGate's own notification bot. +- **Groups:** with privacy mode on (the default) a bot only receives commands and + replies to it. Turn it off in @BotFather (`/setprivacy`) to see all messages. + +## Develop + +```bash +bun install +bun run test # unit (offline) +bun run typecheck +bun run test:integration # workerd, Telegram mocked with fetchMock +bun run dev # wrangler dev +``` + +## Deploy + +`servers_deploy.yaml` deploys every connector with a `wrangler.jsonc` on merge. +There are no secrets to set. The custom domain `telegram.sealgate.ai` must be on +a zone in the same Cloudflare account. diff --git a/servers/telegram-bot/bun.lock b/servers/telegram-bot/bun.lock new file mode 100644 index 0000000..9353987 --- /dev/null +++ b/servers/telegram-bot/bun.lock @@ -0,0 +1,784 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "@edison/mcp-telegram-bot", + "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.0", + "zod": "^3.24.2", + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "0.8.19", + "@cloudflare/workers-types": "^4.20260101.0", + "@types/bun": "^1.3.14", + "@vitest/runner": "3.1.4", + "@vitest/snapshot": "3.1.4", + "typescript": "^5.7.3", + "vitest": "3.1.4", + "wrangler": "^4.20.0", + }, + }, + }, + "packages": { + "@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.5.0", "", {}, "sha512-jxQYkj8dSIzc0cD6cMMNdOc1UVjqSqu8BZdor5s8cGjW2I8BjODt/kWPVdY+u9zj3ms75Q5qaZgnxUad83+eAg=="], + + "@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.16.2", "", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": ">1.20260305.0 <2.0.0-0" }, "optionalPeers": ["workerd"] }, "sha512-JBP1+Z7ZSNG/d4mRP+y8VC5dka3tZVMLEZRvS+rzQ4DGV1EoxRFQckcJTTkXbHSQiTj0DtNI01Zwb/V2fX0mvQ=="], + + "@cloudflare/vitest-pool-workers": ["@cloudflare/vitest-pool-workers@0.8.19", "", { "dependencies": { "birpc": "0.2.14", "cjs-module-lexer": "^1.2.3", "devalue": "^4.3.0", "miniflare": "4.20250417.0", "semver": "^7.7.1", "wrangler": "4.12.1", "zod": "^3.22.3" }, "peerDependencies": { "@vitest/runner": "2.0.x - 3.1.x", "@vitest/snapshot": "2.0.x - 3.1.x", "vitest": "2.0.x - 3.1.x" } }, "sha512-V2E5IEevit8xSvDM0XZTC4JWfDa22YOWRwr7FdoCHMb1hh5rFc6MVYL1wDC3V+aUwJ5toqI69L4h7xxgR+7Wjg=="], + + "@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260925.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-GFDjFo5jfmg98iiy73DOZjHRV+lPWaeGeJ6MbtUIVyostkh9L69KliaAWVytBHCSp36o3NNZOQ0CN8VrqO3+8A=="], + + "@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260925.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-b3BLoP9NkF6b2QOQFs8wFfOt9XSH3jVtacdGn9lcvkUCdPq7F6CfDlleHqyQEnDfZ32lH5jgMRTdaMHUPf/XbA=="], + + "@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260925.1", "", { "os": "linux", "cpu": "x64" }, "sha512-LczXd6uEMqEmBxBJPFwplRIwKmUHqj5t8hilIWOtbX5i0TFp7/iMYBXbAiIhvZnev+8yiTyA7uWBBjDvfPh5Ug=="], + + "@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260925.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-hziOugQbLuva85w8z9bGJbEtABKzSip653x0NgvxgP/Q3n1TpRqRyIJRBufbSUZRkJlfH2se2nQaTx2OU0TT3g=="], + + "@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260925.1", "", { "os": "win32", "cpu": "x64" }, "sha512-mG0JYuF6HVCgXSsNeB5os8Vtdprk5jHPysviKNNMHJSLA3Q1djFjSmK4+pnQR/zOAgzNo0HJ+Tpcz+tZj1Q7fQ=="], + + "@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260702.1", "", {}, "sha512-mOhf5TUEB1m2vPrxtqoIGfz0fUC9xyxRDx5gWHy5s+OCo6dcV+g7wI1R7gYCMFohhqF/2y2xeKVwMwCJjfn/WA=="], + + "@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="], + + "@emnapi/runtime": ["@emnapi/runtime@1.11.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], + + "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], + + "@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="], + + "@img/colour": ["@img/colour@1.1.0", "", {}, "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ=="], + + "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="], + + "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="], + + "@img/sharp-freebsd-wasm32": ["@img/sharp-freebsd-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "os": "freebsd" }, "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA=="], + + "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="], + + "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="], + + "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="], + + "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="], + + "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.3.3", "", { "os": "linux", "cpu": "ppc64" }, "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w=="], + + "@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.3.3", "", { "os": "linux", "cpu": "none" }, "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ=="], + + "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="], + + "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="], + + "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="], + + "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="], + + "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="], + + "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="], + + "@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.3.3" }, "os": "linux", "cpu": "ppc64" }, "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ=="], + + "@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.3.3" }, "os": "linux", "cpu": "none" }, "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA=="], + + "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="], + + "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="], + + "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="], + + "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="], + + "@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="], + + "@img/sharp-webcontainers-wasm32": ["@img/sharp-webcontainers-wasm32@0.35.4", "", { "dependencies": { "@img/sharp-wasm32": "0.35.4" }, "cpu": "none" }, "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA=="], + + "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.35.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw=="], + + "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="], + + "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="], + + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.1", "", { "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" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-H2HxLvC3HDNybePJaLdSrU1hhUK5iQw+WvV1b01myFyI7sdVGe1u/IPTE5D9fGCiJDVtgMV/lmFkQXLmQyIFYA=="], + + "@napi-rs/lzma-linux-x64-gnu": ["@napi-rs/lzma-linux-x64-gnu@1.5.1", "", { "os": "linux", "cpu": "x64" }, "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ=="], + + "@poppinss/colors": ["@poppinss/colors@4.1.6", "", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="], + + "@poppinss/dumper": ["@poppinss/dumper@0.6.5", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="], + + "@poppinss/exception": ["@poppinss/exception@1.2.3", "", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="], + + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.63.5", "", { "os": "android", "cpu": "arm" }, "sha512-J25QJU+B78T4FhhBsNpLJyVWOi31mwtpcMwywHmOKH65Q9IWGA81gPj+dnwlhU8wktVriYE+tFAaQgrnJRzAZg=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.63.5", "", { "os": "android", "cpu": "arm64" }, "sha512-LDopB3zuZM5Ux9TT2luNEBJW/tYbGU2g1d+VpKk6I+gSKDb+/7sYE6M225gRQt4RbMX6MSwMsVR/phdjVUgRLg=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.63.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wlJEERGfeuHeBavCL2qVnNacOK43NDoZM4sjkeRPymd04OAE9T1zBqDJgmZ+CIsPTYKwdzpUC8vmOw84dwY4Tg=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.63.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-4nJJGg5jbo2wwPP4JP+LfEBA3bvP8rU9CLuhp7jWvq9sxEyhjQFTFdrqi+/dHEin/pd8jpT0vcehIpnZtmEdcQ=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.63.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-DrZbyCDF1hneuO6jRbvZ2D7+PIBM6yIwYnJpg2vIk58T+wuFpiaGZrfUr59lDWw45bg+IrpTGLPiNi/Fk4w3Cg=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.63.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-gqfUVMJMB3mehqywxp6hTBFfgtMQykZY19+cfiaYP0toIJLb/1DZRJHVkQQGP13W4TAwfZDWeg1qBcheTRioXQ=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.63.5", "", { "os": "linux", "cpu": "arm" }, "sha512-CFmhpvAwzSaWMlN3VN7UtmoTihlZNzoP0juQib5TQRnYUyDV8dXeWOp29sobWAT6gXl/hQgAClLlEiYozQG3OQ=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.63.5", "", { "os": "linux", "cpu": "arm" }, "sha512-Uc9H8eXCOayV6JLTH5bXKMId6qbhNHa818/BgYjm4jrlq3vZquC9cqyvHBw17xy5Mnj5f+I3gFK5JcEf3hSqrw=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.63.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-VcPr/szv/1BFw112Kt//fxulXt/JPqzzidU84iW68L2DdjnOO8QFUv2zTSYBEPHD6movBD4z+bbr5y60GYM7Jw=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.63.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-BnxtJ5/91BrIHYIkGrmjz/lbMhqEHt1dPFqIxIFR+jPn0xVc/oUSCtIT089zfp5ufwGDlYz2UC+Fe1SRBpYFbQ=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-LrYcHZwF+fAMNKHYTOQ5osWM4AZF7YF6D+XtsjDyEvljtt11twc+zHVXBLNEjxVSUnKYsOhvVz4Z213eW02COQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-nj7QKQePAAUpCpJHtg0pR0W/b92A9NO17JS3BAQmHDn/yhmkir2p8llrKY9TOhleKIaSzy1JhxS3T9FVld6coA=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.63.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-5ylkX6dWMeBKge9nTU+Rxfb+ZfaCIJ9lRqIFaK0eAMcWp7OJbYnLveLgXmm0VrvuLKb8qIK+mHyH0qu88RM+iA=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.63.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-oHK4ZHYFDKjZviK34I+NwgfbGxgI7ztrNxj2hPTSSNFgeq1a/lEd7dHV2fdGAuTH4Iym3RHJg+vAbWaWG4B7Zg=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-UcetmHZ6XOXuUByiKZyQmb55ZPr0LABr3Ec/HB9wKZn6CEAFWZkE+hsJErJ9hbPBC7nI0dKuELx7CoV6IM7TMg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.63.5", "", { "os": "linux", "cpu": "none" }, "sha512-C5CmDPQBtvjVo8cgQsBs+w6WB0JLkiixhgi6hVLV11hERWdn/p0XcPU2OUcZzac9BPOFq7SbaHFa8r3SWEysCQ=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.63.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-lHVQHJFKsuuxLMi3MQO9XVL8Tje3JR82CzB+QDKC5NWBcsIWuwsn9uIM5e3lBhI+fF1/s63qnyYqsg65+8rV/w=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.63.5", "", { "os": "linux", "cpu": "x64" }, "sha512-3W9bTFcQNJn71cSJVM9RKIiZOy8DO/XLDii8Uv/Pm6WKqDRj7JV3ZfuXIEfyuy5LXpIzAbB/1M4Ukp9GKNa7nA=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.63.5", "", { "os": "linux", "cpu": "x64" }, "sha512-VDC7rRJlee/scpki96GZ27Omf6yU87s1YXwVTpjE5841faVlDYYT565rgfmoR1U0sqL7z5ivQSDjcsF6VRXyBA=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.63.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-z86Ok2p4pTdv5xqCKZsTooO7yBEiaJR/HzU3Wx8RmWsPoLppnMKROhJusQob8B3IE1ghC343kUW9rC2r+Wf3ig=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.63.5", "", { "os": "none", "cpu": "arm64" }, "sha512-IzQmj+xXwQFGhMAMKMQVXkMwMZN3TqkJgAE0nSsqvVwWWciP4AIPMmWRqOQ2GfX7TUDZr+xqGFcBS36CRPGw0g=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.63.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-F6qpTaPc9bwBH85kjy0/BLmLSW1uv7AoOXCoRIkg2arlgCYlWYcAbiMkvZuAcaWk9TpCRG//okznLAqLGshkMw=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.63.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-igoDsTFhhwECBeGbUuLeIk7t8Y1apa+cs6mDWpx2EZ0ch7oEQgzHbFUXN9euoHekCAQzXdXApAGkV6jznS7tWw=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.63.5", "", { "os": "win32", "cpu": "x64" }, "sha512-U3teMeMbXFmaM5D+OTJpsOXd+wV/qftIeYF9kBKL4v73641qyJmoXFtA28DQLsnmlyayEsTe72xpLHrArq6vHw=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.63.5", "", { "os": "win32", "cpu": "x64" }, "sha512-ypfC34F3RKXvCXBglGqGMsUSMKlgwd1HX9AOAlx9RoZZ6GaI42YHVeKpzg3JG+wpBUJYTG+NNZhqbDWL8tBZkw=="], + + "@sindresorhus/is": ["@sindresorhus/is@7.2.0", "", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="], + + "@speed-highlight/core": ["@speed-highlight/core@1.2.24", "", {}, "sha512-qeW2e1l78afw8VhRPfPQ1Gjj+KU5XFQ/OFV5ti6eTa9bruO7mJyZtA4vw0ofqmA3tKCkROE9xLk3VZoeRc98nw=="], + + "@types/bun": ["@types/bun@1.4.2", "", { "dependencies": { "bun-types": "1.4.2" } }, "sha512-GimotNn7+ZV0uVArItBbriZsR1oNf0+WTzPkdcFrzShI7k2norL0uzEaJT8T33dWr7O/c9ZDuAFQrctKCi72oQ=="], + + "@types/estree": ["@types/estree@1.0.9", "", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="], + + "@types/node": ["@types/node@26.6.3", "", { "dependencies": { "undici-types": "~8.9.0" } }, "sha512-dsqMQQoeTLqu9wynDD00q573mNzso3IdQOAfHRJqLCcmCFPoGo9A1bDpUcv/9tnKpErQWv9uKeGfl37EIS02Yg=="], + + "@vitest/expect": ["@vitest/expect@3.1.4", "", { "dependencies": { "@vitest/spy": "3.1.4", "@vitest/utils": "3.1.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA=="], + + "@vitest/mocker": ["@vitest/mocker@3.1.4", "", { "dependencies": { "@vitest/spy": "3.1.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@3.1.4", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg=="], + + "@vitest/runner": ["@vitest/runner@3.1.4", "", { "dependencies": { "@vitest/utils": "3.1.4", "pathe": "^2.0.3" } }, "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@3.1.4", "", { "dependencies": { "@vitest/pretty-format": "3.1.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg=="], + + "@vitest/spy": ["@vitest/spy@3.1.4", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg=="], + + "@vitest/utils": ["@vitest/utils@3.1.4", "", { "dependencies": { "@vitest/pretty-format": "3.1.4", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" } }, "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg=="], + + "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + + "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], + + "acorn-walk": ["acorn-walk@8.3.2", "", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="], + + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "as-table": ["as-table@1.0.55", "", { "dependencies": { "printable-characters": "^1.0.42" } }, "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ=="], + + "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "birpc": ["birpc@0.2.14", "", {}, "sha512-37FHE8rqsYM5JEKCnXFyHpBCzvgHEExwVVTq+nUmloInU7l8ezD1TpOhKpS8oe1DTYFqEK27rFZVKG43oTqXRA=="], + + "blake3-wasm": ["blake3-wasm@2.1.5", "", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="], + + "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], + + "bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "chai": ["chai@5.3.3", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + + "check-error": ["check-error@2.1.3", "", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + + "cjs-module-lexer": ["cjs-module-lexer@1.4.3", "", {}, "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q=="], + + "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="], + + "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + + "defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "devalue": ["devalue@4.3.3", "", {}, "sha512-UH8EL6H2ifcY8TbD2QsxwCC/pr5xSwPvv85LrLXVihmHVC3T3YqTCIwnR5ak0yO1KYqlxrPVOA/JVZJYPy2ATg=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "error-stack-parser-es": ["error-stack-parser-es@1.0.5", "", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + + "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "eventsource": ["eventsource@3.0.7", "", { "dependencies": { "eventsource-parser": "^3.0.1" } }, "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA=="], + + "eventsource-parser": ["eventsource-parser@3.1.1", "", {}, "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ=="], + + "exit-hook": ["exit-hook@2.2.1", "", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="], + + "expect-type": ["expect-type@1.4.0", "", {}, "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA=="], + + "express": ["express@5.2.1", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "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.7", "qs": "^6.14.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.0.1", "vary": "^1.1.2" } }, "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw=="], + + "express-rate-limit": ["express-rate-limit@8.7.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g=="], + + "exsolve": ["exsolve@1.1.1", "", {}, "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.8", "", {}, "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg=="], + + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + + "finalhandler": ["finalhandler@2.1.1", "", { "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" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], + + "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "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" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-source": ["get-source@2.0.12", "", { "dependencies": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" } }, "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w=="], + + "glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], + + "hono": ["hono@4.13.9", "", {}, "sha512-7dMkQmZoC4E6F7AtaQSPhlWAdnBti+j7rreMZl8QB4jFiEhP9TWbGWUMi8WYzBCgmgulxuvLQupKqo+Co6Omyg=="], + + "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ip-address": ["ip-address@10.7.2", "", {}, "sha512-7H/2gFSIitxc0hG3nOI1glS8QLo/EHBFFLk8vEUjXY/xu0AdL8jZ9U1IzO2PUm0d2D/ofQcAifb0g6OBkt8U7w=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-arrayish": ["is-arrayish@0.3.4", "", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="], + + "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jose": ["jose@6.2.12", "", {}, "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + + "kleur": ["kleur@4.1.5", "", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="], + + "loupe": ["loupe@3.2.1", "", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + + "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], + + "merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="], + + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "miniflare": ["miniflare@4.20250417.0", "", { "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", "stoppable": "1.1.0", "undici": "^5.28.5", "workerd": "1.20250417.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-bROKLQKr4CoS93tnGuw5e08VaNwM3VowTL3Z2Cps1HzY6a4Bq8uNtggQ7WogriMq77jcHn6kbz64bvWyF//Jkw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mustache": ["mustache@4.2.0", "", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="], + + "nanoid": ["nanoid@3.3.19", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug=="], + + "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "ohash": ["ohash@2.0.12", "", {}, "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + + "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pathval": ["pathval@2.0.1", "", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], + + "pkce-challenge": ["pkce-challenge@5.0.1", "", {}, "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ=="], + + "postcss": ["postcss@8.5.28", "", { "dependencies": { "nanoid": "^3.3.18", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A=="], + + "printable-characters": ["printable-characters@1.0.42", "", {}, "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ=="], + + "proxy-addr": ["proxy-addr@2.0.8", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-5nnx0yGyVUcY6t9RnWcARWtwT9F1D8O9rt08htPvnd49W1IgZtmLkhu9WfMzQj1cFxjHIO6connUNVW5k7AVyQ=="], + + "qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="], + + "range-parser": ["range-parser@1.3.0", "", {}, "sha512-hek2mFQpPuI4E1BBKrSto+BU3e3x4xuarsbiwr3+lf7p44juvFMV0XFWQAP3xUyqXA4RrXLIoaSUGbSt056ZMw=="], + + "raw-body": ["raw-body@3.0.2", "", { "dependencies": { "bytes": "~3.1.2", "http-errors": "~2.0.1", "iconv-lite": "~0.7.0", "unpipe": "~1.0.0" } }, "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "rollup": ["rollup@4.63.5", "", { "dependencies": { "@types/estree": "1.0.9" }, "optionalDependencies": { "@napi-rs/lzma-linux-x64-gnu": "1.5.1", "@rollup/rollup-android-arm-eabi": "4.63.5", "@rollup/rollup-android-arm64": "4.63.5", "@rollup/rollup-darwin-arm64": "4.63.5", "@rollup/rollup-darwin-x64": "4.63.5", "@rollup/rollup-freebsd-arm64": "4.63.5", "@rollup/rollup-freebsd-x64": "4.63.5", "@rollup/rollup-linux-arm-gnueabihf": "4.63.5", "@rollup/rollup-linux-arm-musleabihf": "4.63.5", "@rollup/rollup-linux-arm64-gnu": "4.63.5", "@rollup/rollup-linux-arm64-musl": "4.63.5", "@rollup/rollup-linux-loong64-gnu": "4.63.5", "@rollup/rollup-linux-loong64-musl": "4.63.5", "@rollup/rollup-linux-ppc64-gnu": "4.63.5", "@rollup/rollup-linux-ppc64-musl": "4.63.5", "@rollup/rollup-linux-riscv64-gnu": "4.63.5", "@rollup/rollup-linux-riscv64-musl": "4.63.5", "@rollup/rollup-linux-s390x-gnu": "4.63.5", "@rollup/rollup-linux-x64-gnu": "4.63.5", "@rollup/rollup-linux-x64-musl": "4.63.5", "@rollup/rollup-openbsd-x64": "4.63.5", "@rollup/rollup-openharmony-arm64": "4.63.5", "@rollup/rollup-win32-arm64-msvc": "4.63.5", "@rollup/rollup-win32-ia32-msvc": "4.63.5", "@rollup/rollup-win32-x64-gnu": "4.63.5", "@rollup/rollup-win32-x64-msvc": "4.63.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-KRWwmNLlPw5M7HcdYfm15oBv9n9LPtjzpzCIxS/phwqvPyxHSoKX6Y2YU3pxSPfy0CLquVgsx/j/hBi6OvH1Nw=="], + + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], + + "send": ["send@1.2.1", "", { "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" } }, "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ=="], + + "serve-static": ["serve-static@2.2.1", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "sharp": ["sharp@0.33.5", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "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" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "side-channel": ["side-channel@1.1.1", "", { "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" } }, "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ=="], + + "side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "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" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "simple-swizzle": ["simple-swizzle@0.2.4", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + + "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "stacktracey": ["stacktracey@2.2.0", "", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-ETyQEz+CzXiLjEbyJqpbp+/T79RQD/6wqFucRBIlVNZfYq2Ay7wbretD4cxpbymZlaPWx58aIhPEY1Cr8DlVvg=="], + + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "stoppable": ["stoppable@1.1.0", "", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="], + + "supports-color": ["supports-color@10.2.2", "", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="], + + "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + + "tinypool": ["tinypool@1.1.1", "", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "ufo": ["ufo@1.6.4", "", {}, "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA=="], + + "undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + + "undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="], + + "unenv": ["unenv@2.0.0-rc.24", "", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "vite": ["vite@6.4.3", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A=="], + + "vite-node": ["vite-node@3.1.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA=="], + + "vitest": ["vitest@3.1.4", "", { "dependencies": { "@vitest/expect": "3.1.4", "@vitest/mocker": "3.1.4", "@vitest/pretty-format": "^3.1.4", "@vitest/runner": "3.1.4", "@vitest/snapshot": "3.1.4", "@vitest/spy": "3.1.4", "@vitest/utils": "3.1.4", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.13", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.1.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.1.4", "@vitest/ui": "3.1.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + + "workerd": ["workerd@1.20260925.1", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260925.1", "@cloudflare/workerd-darwin-arm64": "1.20260925.1", "@cloudflare/workerd-linux-64": "1.20260925.1", "@cloudflare/workerd-linux-arm64": "1.20260925.1", "@cloudflare/workerd-windows-64": "1.20260925.1" }, "bin": { "workerd": "bin/workerd" } }, "sha512-cl7Mrvhm+xEC9brMBIWIHyfYl7y5dGH3CjQog6CglWhgRwAq+/b+2gHBXNQb4hsjLXQVCuCYExJRYCjEaZmAlA=="], + + "wrangler": ["wrangler@4.141.0", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.5.0", "@cloudflare/unenv-preset": "2.16.2", "blake3-wasm": "2.1.5", "esbuild": "0.28.1", "miniflare": "5.20260925.0-alpha", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260925.1" }, "optionalDependencies": { "fsevents": "2.3.3" }, "peerDependencies": { "@cloudflare/workers-types": "^5.20260925.1" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js", "cf-wrangler": "bin/cf-wrangler.js" } }, "sha512-8+LRZEcynBMfVYgO4N378z5XyuajaILt2IzsdWRLCHrRWz7mmGWv4igdVjqbRQMJtE0S5bSmyz0W2XpA6UM/ow=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "ws": ["ws@8.18.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + + "youch": ["youch@3.3.4", "", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="], + + "youch-core": ["youch-core@0.3.3", "", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + + "@cloudflare/vitest-pool-workers/wrangler": ["wrangler@4.12.1", "", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.0", "@cloudflare/unenv-preset": "2.3.1", "blake3-wasm": "2.1.5", "esbuild": "0.25.2", "miniflare": "4.20250417.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.15", "workerd": "1.20250417.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250417.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-jYrz8y2ffhsRqvQLO2dXFi9HLvPUJk3jn7U71GWfBBCHm0I6r2ik7Vs9ajpRcTGlbNw1RY0uIHVJBVR/7bEN5A=="], + + "@img/sharp-freebsd-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.4", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA=="], + + "@img/sharp-webcontainers-wasm32/@img/sharp-wasm32": ["@img/sharp-wasm32@0.35.4", "", { "dependencies": { "@emnapi/runtime": "^1.11.3" } }, "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA=="], + + "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + + "miniflare/workerd": ["workerd@1.20250417.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250417.0", "@cloudflare/workerd-darwin-arm64": "1.20250417.0", "@cloudflare/workerd-linux-64": "1.20250417.0", "@cloudflare/workerd-linux-arm64": "1.20250417.0", "@cloudflare/workerd-windows-64": "1.20250417.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-naz6oJiVODd3/Lkp9l3vtc56HKOOvx+AWDvEsTa5eSfi5SI9V0HYpLYSPblAwrfazbQ4ff1Vl3jkTl/5JxqCAA=="], + + "miniflare/zod": ["zod@3.22.3", "", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="], + + "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + + "router/path-to-regexp": ["path-to-regexp@8.4.2", "", {}, "sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA=="], + + "type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + + "vite/esbuild": ["esbuild@0.25.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.12", "@esbuild/android-arm": "0.25.12", "@esbuild/android-arm64": "0.25.12", "@esbuild/android-x64": "0.25.12", "@esbuild/darwin-arm64": "0.25.12", "@esbuild/darwin-x64": "0.25.12", "@esbuild/freebsd-arm64": "0.25.12", "@esbuild/freebsd-x64": "0.25.12", "@esbuild/linux-arm": "0.25.12", "@esbuild/linux-arm64": "0.25.12", "@esbuild/linux-ia32": "0.25.12", "@esbuild/linux-loong64": "0.25.12", "@esbuild/linux-mips64el": "0.25.12", "@esbuild/linux-ppc64": "0.25.12", "@esbuild/linux-riscv64": "0.25.12", "@esbuild/linux-s390x": "0.25.12", "@esbuild/linux-x64": "0.25.12", "@esbuild/netbsd-arm64": "0.25.12", "@esbuild/netbsd-x64": "0.25.12", "@esbuild/openbsd-arm64": "0.25.12", "@esbuild/openbsd-x64": "0.25.12", "@esbuild/openharmony-arm64": "0.25.12", "@esbuild/sunos-x64": "0.25.12", "@esbuild/win32-arm64": "0.25.12", "@esbuild/win32-ia32": "0.25.12", "@esbuild/win32-x64": "0.25.12" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg=="], + + "vitest/@vitest/pretty-format": ["@vitest/pretty-format@3.2.7", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA=="], + + "wrangler/miniflare": ["miniflare@5.20260925.0-alpha", "", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "0.35.4", "undici": "7.29.0", "workerd": "1.20260925.1", "ws": "8.21.0", "youch": "4.1.0-beta.10" } }, "sha512-ly2ygNOfuouMiPcyTNEUW6f3+zgndfC45e1nQZ2LSciOSFfrih8V7ciaZ01I/64Ccp3F6fWraxh5VuHPsP3AAw=="], + + "@cloudflare/vitest-pool-workers/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.0", "", { "dependencies": { "mime": "^3.0.0" } }, "sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA=="], + + "@cloudflare/vitest-pool-workers/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.3.1", "", { "peerDependencies": { "unenv": "2.0.0-rc.15", "workerd": "^1.20250320.0" }, "optionalPeers": ["workerd"] }, "sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild": ["esbuild@0.25.2", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.2", "@esbuild/android-arm": "0.25.2", "@esbuild/android-arm64": "0.25.2", "@esbuild/android-x64": "0.25.2", "@esbuild/darwin-arm64": "0.25.2", "@esbuild/darwin-x64": "0.25.2", "@esbuild/freebsd-arm64": "0.25.2", "@esbuild/freebsd-x64": "0.25.2", "@esbuild/linux-arm": "0.25.2", "@esbuild/linux-arm64": "0.25.2", "@esbuild/linux-ia32": "0.25.2", "@esbuild/linux-loong64": "0.25.2", "@esbuild/linux-mips64el": "0.25.2", "@esbuild/linux-ppc64": "0.25.2", "@esbuild/linux-riscv64": "0.25.2", "@esbuild/linux-s390x": "0.25.2", "@esbuild/linux-x64": "0.25.2", "@esbuild/netbsd-arm64": "0.25.2", "@esbuild/netbsd-x64": "0.25.2", "@esbuild/openbsd-arm64": "0.25.2", "@esbuild/openbsd-x64": "0.25.2", "@esbuild/sunos-x64": "0.25.2", "@esbuild/win32-arm64": "0.25.2", "@esbuild/win32-ia32": "0.25.2", "@esbuild/win32-x64": "0.25.2" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ=="], + + "@cloudflare/vitest-pool-workers/wrangler/unenv": ["unenv@2.0.0-rc.15", "", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.4", "ohash": "^2.0.11", "pathe": "^2.0.3", "ufo": "^1.5.4" } }, "sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA=="], + + "@cloudflare/vitest-pool-workers/wrangler/workerd": ["workerd@1.20250417.0", "", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250417.0", "@cloudflare/workerd-darwin-arm64": "1.20250417.0", "@cloudflare/workerd-linux-64": "1.20250417.0", "@cloudflare/workerd-linux-arm64": "1.20250417.0", "@cloudflare/workerd-windows-64": "1.20250417.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-naz6oJiVODd3/Lkp9l3vtc56HKOOvx+AWDvEsTa5eSfi5SI9V0HYpLYSPblAwrfazbQ4ff1Vl3jkTl/5JxqCAA=="], + + "miniflare/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250417.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-4Adfl92aKepjxb8e6af2d+xpD2sBOADgHqvkyXsFmoLb80weMEDDRGJi1p1m5q1M78/oVnGcpdmuRCAathanRg=="], + + "miniflare/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250417.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dSlk18F4i3T1OTzFBxx3pKpXRMP6w2xZ26+oIV32BFWrCi/HxGzUd6gVA0q37oLGqITRt8xU693J4Gl1CwC/Ag=="], + + "miniflare/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250417.0", "", { "os": "linux", "cpu": "x64" }, "sha512-27MVzOa/lENcqewC2L9EcqstXW843UhjBMcwV1umDfsjwLyZOEv6Gtm/6j5r0L0gASvkRTam3fAmtPk/gt48TA=="], + + "miniflare/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250417.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-34qBk0htAXmUneOTQxW6/g6pjNVR91r0vJzz2FID84cAIOYVl4hZLijkjmVl+MMDU6boXUs+yDwhItdg06YvAg=="], + + "miniflare/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250417.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PDwATFioff+geVHfgTzSWsxgwjgotrdXStb0EL0lMyMT5zNmHArAnOx83CbDtud63Uv9rVX1BAfPP4tyD1O+5A=="], + + "vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA=="], + + "vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.12", "", { "os": "android", "cpu": "arm" }, "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg=="], + + "vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.12", "", { "os": "android", "cpu": "arm64" }, "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg=="], + + "vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.12", "", { "os": "android", "cpu": "x64" }, "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg=="], + + "vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg=="], + + "vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA=="], + + "vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg=="], + + "vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ=="], + + "vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.12", "", { "os": "linux", "cpu": "arm" }, "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw=="], + + "vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ=="], + + "vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA=="], + + "vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng=="], + + "vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw=="], + + "vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA=="], + + "vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.12", "", { "os": "linux", "cpu": "none" }, "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w=="], + + "vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg=="], + + "vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.12", "", { "os": "linux", "cpu": "x64" }, "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw=="], + + "vite/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg=="], + + "vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.12", "", { "os": "none", "cpu": "x64" }, "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ=="], + + "vite/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.12", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A=="], + + "vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw=="], + + "vite/esbuild/@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.12", "", { "os": "none", "cpu": "arm64" }, "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg=="], + + "vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w=="], + + "vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg=="], + + "vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ=="], + + "vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="], + + "wrangler/miniflare/sharp": ["sharp@0.35.4", "", { "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", "semver": "^7.8.5" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.35.4", "@img/sharp-darwin-x64": "0.35.4", "@img/sharp-freebsd-wasm32": "0.35.4", "@img/sharp-libvips-darwin-arm64": "1.3.3", "@img/sharp-libvips-darwin-x64": "1.3.3", "@img/sharp-libvips-linux-arm": "1.3.3", "@img/sharp-libvips-linux-arm64": "1.3.3", "@img/sharp-libvips-linux-ppc64": "1.3.3", "@img/sharp-libvips-linux-riscv64": "1.3.3", "@img/sharp-libvips-linux-s390x": "1.3.3", "@img/sharp-libvips-linux-x64": "1.3.3", "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", "@img/sharp-libvips-linuxmusl-x64": "1.3.3", "@img/sharp-linux-arm": "0.35.4", "@img/sharp-linux-arm64": "0.35.4", "@img/sharp-linux-ppc64": "0.35.4", "@img/sharp-linux-riscv64": "0.35.4", "@img/sharp-linux-s390x": "0.35.4", "@img/sharp-linux-x64": "0.35.4", "@img/sharp-linuxmusl-arm64": "0.35.4", "@img/sharp-linuxmusl-x64": "0.35.4", "@img/sharp-webcontainers-wasm32": "0.35.4", "@img/sharp-win32-arm64": "0.35.4", "@img/sharp-win32-ia32": "0.35.4", "@img/sharp-win32-x64": "0.35.4" } }, "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA=="], + + "wrangler/miniflare/undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="], + + "wrangler/miniflare/ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], + + "wrangler/miniflare/youch": ["youch@4.1.0-beta.10", "", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.25.2", "", { "os": "android", "cpu": "arm" }, "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.2", "", { "os": "android", "cpu": "arm64" }, "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.25.2", "", { "os": "android", "cpu": "x64" }, "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.2", "", { "os": "linux", "cpu": "arm" }, "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.2", "", { "os": "linux", "cpu": "ia32" }, "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.2", "", { "os": "linux", "cpu": "none" }, "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.2", "", { "os": "linux", "cpu": "x64" }, "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.2", "", { "os": "none", "cpu": "arm64" }, "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.2", "", { "os": "none", "cpu": "x64" }, "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.2", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.2", "", { "os": "openbsd", "cpu": "x64" }, "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.2", "", { "os": "sunos", "cpu": "x64" }, "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg=="], + + "@cloudflare/vitest-pool-workers/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.2", "", { "os": "win32", "cpu": "x64" }, "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA=="], + + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250417.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-4Adfl92aKepjxb8e6af2d+xpD2sBOADgHqvkyXsFmoLb80weMEDDRGJi1p1m5q1M78/oVnGcpdmuRCAathanRg=="], + + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250417.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-dSlk18F4i3T1OTzFBxx3pKpXRMP6w2xZ26+oIV32BFWrCi/HxGzUd6gVA0q37oLGqITRt8xU693J4Gl1CwC/Ag=="], + + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250417.0", "", { "os": "linux", "cpu": "x64" }, "sha512-27MVzOa/lENcqewC2L9EcqstXW843UhjBMcwV1umDfsjwLyZOEv6Gtm/6j5r0L0gASvkRTam3fAmtPk/gt48TA=="], + + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250417.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-34qBk0htAXmUneOTQxW6/g6pjNVR91r0vJzz2FID84cAIOYVl4hZLijkjmVl+MMDU6boXUs+yDwhItdg06YvAg=="], + + "@cloudflare/vitest-pool-workers/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250417.0", "", { "os": "win32", "cpu": "x64" }, "sha512-PDwATFioff+geVHfgTzSWsxgwjgotrdXStb0EL0lMyMT5zNmHArAnOx83CbDtud63Uv9rVX1BAfPP4tyD1O+5A=="], + + "wrangler/miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.3.3" }, "os": "darwin", "cpu": "arm64" }, "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA=="], + + "wrangler/miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.3.3" }, "os": "darwin", "cpu": "x64" }, "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw=="], + + "wrangler/miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg=="], + + "wrangler/miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw=="], + + "wrangler/miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.3.3", "", { "os": "linux", "cpu": "arm" }, "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA=="], + + "wrangler/miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A=="], + + "wrangler/miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.3.3", "", { "os": "linux", "cpu": "s390x" }, "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q=="], + + "wrangler/miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA=="], + + "wrangler/miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw=="], + + "wrangler/miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw=="], + + "wrangler/miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.3.3" }, "os": "linux", "cpu": "arm" }, "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A=="], + + "wrangler/miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ=="], + + "wrangler/miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.3.3" }, "os": "linux", "cpu": "s390x" }, "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ=="], + + "wrangler/miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA=="], + + "wrangler/miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" }, "os": "linux", "cpu": "arm64" }, "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA=="], + + "wrangler/miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.35.4", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.3.3" }, "os": "linux", "cpu": "x64" }, "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg=="], + + "wrangler/miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.35.4", "", { "os": "win32", "cpu": "ia32" }, "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw=="], + + "wrangler/miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.35.4", "", { "os": "win32", "cpu": "x64" }, "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg=="], + + "wrangler/miniflare/youch/cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + } +} diff --git a/servers/telegram-bot/bunfig.toml b/servers/telegram-bot/bunfig.toml new file mode 100644 index 0000000..90deb8b --- /dev/null +++ b/servers/telegram-bot/bunfig.toml @@ -0,0 +1,5 @@ +# Keep bare `bun test` on the offline unit tier only. The integration tier +# (test/integration/*.spec.ts) imports `cloudflare:test` and must run under +# vitest + workerd via `bun run test:integration`, not the bun test runner. +[test] +root = "test/unit" diff --git a/servers/telegram-bot/catalog-entry.json b/servers/telegram-bot/catalog-entry.json new file mode 100644 index 0000000..25c491a --- /dev/null +++ b/servers/telegram-bot/catalog-entry.json @@ -0,0 +1,102 @@ +{ + "id": "telegram-bot", + "displayName": "Telegram Bot", + "description": "Drive your own Telegram bot: read the messages it receives, look up chats, and send, edit, forward, react to, pin, and delete messages. Bring a bot token from @BotFather.", + "author": "SealGate", + "edison_hosted": true, + "category": "communication", + "tags": [ + "telegram", + "bot", + "messaging", + "chat" + ], + "url": "https://telegram.sealgate.ai/mcp", + "auth": "edison-jwt", + "headers": { + "X-Telegram-Bot-Token": "{TELEGRAM_BOT_TOKEN}" + }, + "template_fields": { + "env": { + "TELEGRAM_BOT_TOKEN": { + "description": "Bot token from @BotFather (/newbot). Use a bot no other service polls or webhooks, since Telegram delivers each update to one consumer.", + "example": "123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsawE" + } + } + }, + "icon": "telegram-bot.svg", + "tools_configurations": { + "telegram_get_me": { + "write_operation": false, + "read_private_data": false, + "read_untrusted_public_data": false, + "acl": "PUBLIC" + }, + "telegram_get_updates": { + "write_operation": false, + "read_private_data": true, + "read_untrusted_public_data": true, + "acl": "PRIVATE" + }, + "telegram_get_chat": { + "write_operation": false, + "read_private_data": true, + "read_untrusted_public_data": true, + "acl": "PRIVATE" + }, + "telegram_send_message": { + "write_operation": true, + "read_private_data": false, + "read_untrusted_public_data": false, + "acl": "SECRET" + }, + "telegram_send_photo": { + "write_operation": true, + "read_private_data": false, + "read_untrusted_public_data": false, + "acl": "SECRET" + }, + "telegram_send_document": { + "write_operation": true, + "read_private_data": false, + "read_untrusted_public_data": false, + "acl": "SECRET" + }, + "telegram_forward_message": { + "write_operation": true, + "read_private_data": false, + "read_untrusted_public_data": false, + "acl": "SECRET" + }, + "telegram_edit_message_text": { + "write_operation": true, + "read_private_data": false, + "read_untrusted_public_data": false, + "acl": "SECRET" + }, + "telegram_delete_message": { + "write_operation": true, + "read_private_data": false, + "read_untrusted_public_data": false, + "acl": "PRIVATE" + }, + "telegram_set_reaction": { + "write_operation": true, + "read_private_data": false, + "read_untrusted_public_data": false, + "acl": "PRIVATE" + }, + "telegram_pin_message": { + "write_operation": true, + "read_private_data": false, + "read_untrusted_public_data": false, + "acl": "PRIVATE" + }, + "telegram_unpin_message": { + "write_operation": true, + "read_private_data": false, + "read_untrusted_public_data": false, + "acl": "PRIVATE" + } + } +} diff --git a/servers/telegram-bot/package.json b/servers/telegram-bot/package.json new file mode 100644 index 0000000..363ae47 --- /dev/null +++ b/servers/telegram-bot/package.json @@ -0,0 +1,28 @@ +{ + "name": "@edison/mcp-telegram-bot", + "private": true, + "description": "Edison first-party MCP server: drive your own Telegram bot through the Bot API (read updates, send/edit/forward/react/pin/delete). Stateless Cloudflare Worker.", + "type": "module", + "scripts": { + "dev": "wrangler dev", + "deploy": "wrangler deploy", + "typecheck": "tsc --noEmit", + "test": "bun test test/unit", + "test:integration": "vitest run", + "cf-typegen": "wrangler types" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.30.0", + "zod": "^3.24.2" + }, + "devDependencies": { + "@cloudflare/vitest-pool-workers": "0.8.19", + "@cloudflare/workers-types": "^4.20260101.0", + "@types/bun": "^1.3.14", + "@vitest/runner": "3.1.4", + "@vitest/snapshot": "3.1.4", + "typescript": "^5.7.3", + "vitest": "3.1.4", + "wrangler": "^4.20.0" + } +} diff --git a/servers/telegram-bot/src/auth.ts b/servers/telegram-bot/src/auth.ts new file mode 100644 index 0000000..fb95847 --- /dev/null +++ b/servers/telegram-bot/src/auth.ts @@ -0,0 +1,4 @@ +// The fleet auth contract lives once in shared/auth/ts. This connector just +// re-exports it so src/index.ts and the unit tests import a stable local path +// while the verify logic stays a single source of truth across every server. +export * from "../../../shared/auth/ts/auth"; diff --git a/servers/telegram-bot/src/index.ts b/servers/telegram-bot/src/index.ts new file mode 100644 index 0000000..faac0e5 --- /dev/null +++ b/servers/telegram-bot/src/index.ts @@ -0,0 +1,418 @@ +/** + * telegram-bot - an Edison first-party MCP server. + * + * Drive a Telegram bot through the Bot API: read the updates it receives, look + * up chats, and send, edit, forward, react to, pin and delete messages. Each + * user brings their own bot from @BotFather; SealGate stores the token in its + * zero-knowledge template values and injects it per request as the + * `X-Telegram-Bot-Token` header (catalog `headers` + `template_fields`, beside + * the `edison-jwt` gate). + * + * Transport: streamable HTTP at `/mcp`, **stateless**. Unlike the Durable + * Object connectors, every request builds its own McpServer closed over that + * request's token and discards it after responding. That keeps the token out + * of any storage: McpAgent persists its `props` into Durable Object storage, + * which is fine for a JWT subject and not for a live bot credential. The Bot + * API itself is stateless, so there is no per-bot session worth keeping. + * + * Auth: two credentials. The fleet gate (./auth) runs first and is edison-jwt + * in production, like the other hosted connectors: SealGate mints a per-user + * JWT, so the public endpoint is no open relay and every call is attributable. + * The caller's own bot token then authorizes the Telegram work. A bad bot token + * is a 401 without an OAuth challenge, which SealGate reports as rejected + * credentials rather than starting OAuth. + */ +import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js"; +import { z } from "zod"; + +import { checkAuth } from "./auth"; +import { + ALLOWED_UPDATE_KINDS, + BOT_TOKEN_HEADER, + CHAT_ID_RE, + MAX_CAPTION_LEN, + MAX_TEXT_LEN, + MAX_UPDATES_LIMIT, + isRecord, + isValidBotToken, + nextOffset, + normalizeChatInfo, + normalizeMessage, + normalizeUpdate, + normalizeUser, + telegramClient, + validateFileRef, + type TelegramCall, +} from "./telegram"; + +export interface Env { + // Optional override of https://api.telegram.org (tests, a local Bot API server). + TELEGRAM_API_BASE?: string; + // Fleet auth (see ./auth, ./jwt). + AUTH_TOKEN?: string; + AUTH_MODE?: string; + EDISON_JWKS_URL?: string; + EDISON_JWT_ISSUER?: string; + EDISON_JWT_AUDIENCE?: string; +} + +const SERVICE = "telegram-bot"; + +// --- input schemas ------------------------------------------------------------ +// Validation and normalization live in the schemas: the SDK hands handlers the +// parsed (transformed) value, and a failure never reaches Telegram. + +const chatId = z + .union([z.number().int(), z.string()]) + .transform((v, ctx) => { + const id = String(v).trim(); + if (CHAT_ID_RE.test(id)) return id; + ctx.addIssue({ code: z.ZodIssueCode.custom, message: "chat_id must be an integer id or a public '@username'" }); + return z.NEVER; + }) + .describe("Chat id (integer, negative for groups/channels) or a public '@channelusername'."); + +const fileRef = (kind: string) => + z + .string() + .max(2048) + .transform((v, ctx) => { + const ref = validateFileRef(v); + if (ref) return ref; + ctx.addIssue({ code: z.ZodIssueCode.custom, message: `${kind} must be an https:// URL without credentials, or a Telegram file_id` }); + return z.NEVER; + }) + .describe("An https:// URL (Telegram fetches it) or the file_id of a file the bot has seen."); + +const messageId = z.number().int().positive().describe("Message id within the chat."); +const parseMode = z + .enum(["MarkdownV2", "HTML"]) + .optional() + .describe("Telegram formatting mode. Omit for plain text. Length limits apply after Telegram parses the markup."); +const text = z.string().min(1).max(MAX_TEXT_LEN).describe(`Message text (1-${MAX_TEXT_LEN} characters).`); +const disableNotification = z.boolean().optional().describe("Deliver silently (no sound)."); + +/** Fields shared by every tool that sends a new message into a chat. */ +const sendFields = { + chat_id: chatId, + reply_to_message_id: z.number().int().positive().optional().describe("Reply to this message id in the same chat."), + message_thread_id: z.number().int().positive().optional().describe("Forum topic id (supergroups with topics)."), + disable_notification: disableNotification, +}; +const fileFields = { + ...sendFields, + caption: z.string().max(MAX_CAPTION_LEN).optional().describe(`Caption (max ${MAX_CAPTION_LEN}).`), + parse_mode: parseMode, +}; + +type SendArgs = { + chat_id: string; + reply_to_message_id?: number; + message_thread_id?: number; + disable_notification?: boolean; +}; + +/** Bot API params for `sendFields`; allow_sending_without_reply keeps a deleted target from failing the send. */ +function sendParams({ reply_to_message_id, ...rest }: SendArgs): Record { + return reply_to_message_id === undefined + ? rest + : { ...rest, reply_parameters: { message_id: reply_to_message_id, allow_sending_without_reply: true } }; +} + +// --- results -------------------------------------------------------------------- + +function textError(message: string) { + return { isError: true as const, content: [{ type: "text" as const, text: `Error: ${message}` }] }; +} + +function ok(summary: string, data: Record) { + return { + content: [{ type: "text" as const, text: `${summary}\n${JSON.stringify(data, null, 2)}` }], + structuredContent: data, + }; +} + +type Render = (result: T) => [summary: string, data: Record]; + +/** Call one Bot API method and turn the outcome into a tool result. */ +function runner(call: TelegramCall) { + return async (method: string, params: Record, render: Render) => { + const res = await call(method, params); + return res.ok ? ok(...render(res.result)) : textError(res.error); + }; +} + +const sentMessage = + (what: string): Render => + (raw) => { + const message = normalizeMessage(raw); + return [`Sent ${what} as message ${message?.message_id ?? "?"}`, { message }]; + }; + +// --- server --------------------------------------------------------------------- + +/** Build one request's MCP server over that request's Telegram client. */ +export function buildServer(call: TelegramCall): McpServer { + const server = new McpServer({ name: SERVICE, version: "0.1.0" }); + const run = runner(call); + + server.registerTool( + "telegram_get_me", + { description: "Return the bot's own identity (id, username, name). Use it to confirm which bot is connected." }, + () => + run("getMe", {}, (raw) => { + const bot = normalizeUser(raw); + return [`Connected as @${bot?.username ?? "unknown"}`, { bot }]; + }), + ); + + server.registerTool( + "telegram_get_updates", + { + description: + "Fetch messages and events the bot has received and not yet acknowledged (Telegram keeps them " + + "for 24 hours). Bots cannot read older chat history. Pass the returned `next_offset` as `offset` " + + "on the next call to acknowledge what you have read, or updates will be returned again. Fails if " + + "the bot has a webhook set.", + inputSchema: { + offset: z + .number() + .int() + .optional() + .describe("Return updates from this id on; acknowledges all earlier ones. Use the previous next_offset."), + limit: z + .number() + .int() + .min(1) + .max(MAX_UPDATES_LIMIT) + .default(50) + .describe(`Max updates to return (1-${MAX_UPDATES_LIMIT}, default 50).`), + allowed_updates: z + .array(z.enum(ALLOWED_UPDATE_KINDS)) + .max(ALLOWED_UPDATE_KINDS.length) + .optional() + .describe("Only return these update kinds. Omit for the bot's current setting."), + }, + }, + // timeout 0 = short poll: a Worker request must not hang waiting for new messages. + (args) => + run("getUpdates", { ...args, timeout: 0 }, (raw) => { + const updates = Array.isArray(raw) ? raw.map(normalizeUpdate) : []; + return [ + `${updates.length} update(s)`, + { count: updates.length, next_offset: nextOffset(updates), updates }, + ]; + }), + ); + + server.registerTool( + "telegram_get_chat", + { + description: + "Look up a chat the bot can see: type, title, username, description, member count, pinned message. " + + "Private chats are only visible after that user has messaged the bot.", + inputSchema: { chat_id: chatId }, + }, + async ({ chat_id }) => { + const [chat, count] = await Promise.all([ + call("getChat", { chat_id }), + call("getChatMemberCount", { chat_id }), + ]); + if (!chat.ok) return textError(chat.error); + return ok(`Chat ${chat_id}`, { chat: normalizeChatInfo(chat.result, count.ok ? count.result : null) }); + }, + ); + + server.registerTool( + "telegram_send_message", + { + description: "Send a text message from the bot to a chat, optionally as a reply or into a forum topic.", + inputSchema: { + ...sendFields, + text, + parse_mode: parseMode, + disable_link_preview: z.boolean().optional().describe("Do not render a preview for links in the text."), + }, + }, + ({ text, parse_mode, disable_link_preview, ...send }) => + run( + "sendMessage", + { + ...sendParams(send), + text, + parse_mode, + link_preview_options: disable_link_preview ? { is_disabled: true } : undefined, + }, + sentMessage("text"), + ), + ); + + server.registerTool( + "telegram_send_photo", + { description: "Send a photo to a chat.", inputSchema: { ...fileFields, photo: fileRef("photo") } }, + ({ photo, caption, parse_mode, ...send }) => + run("sendPhoto", { ...sendParams(send), photo, caption, parse_mode }, sentMessage("photo")), + ); + + server.registerTool( + "telegram_send_document", + { description: "Send a file to a chat.", inputSchema: { ...fileFields, document: fileRef("document") } }, + ({ document, caption, parse_mode, ...send }) => + run("sendDocument", { ...sendParams(send), document, caption, parse_mode }, sentMessage("document")), + ); + + server.registerTool( + "telegram_forward_message", + { + description: "Forward a message from one chat the bot can see into another.", + inputSchema: { + chat_id: chatId.describe("Destination chat id or '@username'."), + from_chat_id: chatId.describe("Source chat id or '@username'."), + message_id: messageId, + disable_notification: disableNotification, + }, + }, + (args) => run("forwardMessage", args, sentMessage("forward")), + ); + + server.registerTool( + "telegram_edit_message_text", + { + description: "Replace the text of a message the bot sent earlier.", + inputSchema: { chat_id: chatId, message_id: messageId, text, parse_mode: parseMode }, + }, + (args) => + run("editMessageText", args, (raw) => [ + `Edited message ${args.message_id}`, + { message: normalizeMessage(raw) }, + ]), + ); + + server.registerTool( + "telegram_delete_message", + { + description: + "Delete a message. Bots can delete their own messages, and others' in groups where they are an admin " + + "with delete rights (within 48 hours).", + inputSchema: { chat_id: chatId, message_id: messageId }, + }, + (args) => run("deleteMessage", args, () => [`Deleted message ${args.message_id}`, { deleted: true }]), + ); + + server.registerTool( + "telegram_set_reaction", + { + description: "Set the bot's emoji reaction on a message, or clear it by omitting `emoji`.", + inputSchema: { + chat_id: chatId, + message_id: messageId, + emoji: z + .string() + .min(1) + .max(16) + .optional() + .describe("One emoji from Telegram's allowed reaction set, e.g. '👍'. Omit to remove the reaction."), + }, + }, + ({ emoji, ...target }) => + run("setMessageReaction", { ...target, reaction: emoji ? [{ type: "emoji", emoji }] : [] }, () => [ + emoji ? `Reacted ${emoji}` : "Reaction cleared", + { reaction: emoji ?? null }, + ]), + ); + + server.registerTool( + "telegram_pin_message", + { + description: "Pin a message in a chat. Needs pin rights in groups.", + inputSchema: { chat_id: chatId, message_id: messageId, disable_notification: disableNotification }, + }, + (args) => run("pinChatMessage", args, () => [`Pinned message ${args.message_id}`, { pinned: true }]), + ); + + server.registerTool( + "telegram_unpin_message", + { + description: "Unpin a pinned message in a chat. Needs pin rights in groups.", + inputSchema: { chat_id: chatId, message_id: messageId }, + }, + (args) => run("unpinChatMessage", args, () => [`Unpinned message ${args.message_id}`, { pinned: false }]), + ); + + return server; +} + +// --- HTTP ----------------------------------------------------------------------- + +function jsonError(status: number, message: string, extra: Record = {}): Response { + return new Response(JSON.stringify({ error: message }), { + status, + headers: { "content-type": "application/json", ...extra }, + }); +} + +function isInitialize(body: unknown): boolean { + return (Array.isArray(body) ? body : [body]).some((m) => isRecord(m) && m.method === "initialize"); +} + +async function handleMcp(request: Request, env: Env): Promise { + // Stateless transport: no session, so GET (server-initiated SSE) and DELETE + // (session teardown) have nothing to act on. + if (request.method !== "POST") { + return jsonError(405, "method not allowed: this server is stateless, POST JSON-RPC to /mcp", { allow: "POST" }); + } + + const auth = await checkAuth(request, env); + if (!auth.ok) { + const extra: Record = auth.status === 401 ? { "www-authenticate": `Bearer realm="${SERVICE}"` } : {}; + return jsonError(auth.status, auth.message, extra); + } + + const token = request.headers.get(BOT_TOKEN_HEADER)?.trim(); + if (!token) return jsonError(401, `missing ${BOT_TOKEN_HEADER} header (a @BotFather bot token)`); + if (!isValidBotToken(token)) return jsonError(401, `malformed ${BOT_TOKEN_HEADER}: expected ':'`); + + let body: unknown; + try { + body = await request.json(); + } catch { + return jsonError(400, "request body must be JSON-RPC"); + } + + const call = telegramClient(token, { base: env.TELEGRAM_API_BASE?.trim() }); + + // Check the token once per connection, on initialize, so a wrong token fails + // at install time instead of on the first tool call. + if (isInitialize(body)) { + const me = await call("getMe", {}, 10_000); + if (!me.ok) return jsonError(me.status === 401 ? 401 : 502, me.error); + } + + const server = buildServer(call); + const transport = new WebStandardStreamableHTTPServerTransport({ + sessionIdGenerator: undefined, + enableJsonResponse: true, + }); + await server.connect(transport); + try { + return await transport.handleRequest(request, { parsedBody: body }); + } finally { + // JSON-response mode resolves only once every reply is ready, so the + // per-request server can be dropped straight away. + await server.close(); + } +} + +export default { + async fetch(request: Request, env: Env): Promise { + const url = new URL(request.url); + if (url.pathname === "/health") { + return Response.json({ ok: true, service: SERVICE }); + } + if (url.pathname === "/mcp") { + return handleMcp(request, env); + } + return new Response("Not found", { status: 404 }); + }, +}; diff --git a/servers/telegram-bot/src/jwt.ts b/servers/telegram-bot/src/jwt.ts new file mode 100644 index 0000000..53b8ab0 --- /dev/null +++ b/servers/telegram-bot/src/jwt.ts @@ -0,0 +1,3 @@ +// Edison-JWT verifier: re-exported from the shared fleet auth module so the +// signature/JWKS logic is defined once (see shared/auth/ts/jwt.ts). +export * from "../../../shared/auth/ts/jwt"; diff --git a/servers/telegram-bot/src/telegram.ts b/servers/telegram-bot/src/telegram.ts new file mode 100644 index 0000000..d778fa8 --- /dev/null +++ b/servers/telegram-bot/src/telegram.ts @@ -0,0 +1,315 @@ +/** + * Pure Telegram Bot API helpers for the telegram-bot connector: token, chat-id + * and file-ref validation, the per-request API client, and the normalization + * that turns raw Bot API objects into a compact, stable shape. + * + * Nothing here holds state. The bot token is only ever placed in the request + * URL path (the Bot API has no header form), never in an error message or log. + */ + +export const TELEGRAM_API_BASE = "https://api.telegram.org"; + +/** Header SealGate injects from the user's encrypted TELEGRAM_BOT_TOKEN field. */ +export const BOT_TOKEN_HEADER = "x-telegram-bot-token"; + +// Bot API limits (https://core.telegram.org/bots/api#sendmessage and friends). +export const MAX_TEXT_LEN = 4096; +export const MAX_CAPTION_LEN = 1024; +export const MAX_UPDATES_LIMIT = 100; + +/** Update kinds that carry a whole message. */ +export const MESSAGE_UPDATE_KINDS = [ + "message", + "edited_message", + "channel_post", + "edited_channel_post", + "business_message", + "edited_business_message", +] as const; +export type MessageUpdateKind = (typeof MESSAGE_UPDATE_KINDS)[number]; + +/** Update kinds a caller may ask `getUpdates` for (the ones normalizeUpdate understands). */ +export const ALLOWED_UPDATE_KINDS = [...MESSAGE_UPDATE_KINDS, "message_reaction", "callback_query"] as const; + +/** + * `:` as issued by @BotFather. The token is interpolated into + * the URL path, so the charset check is also what stops a caller-supplied value + * from reshaping the request URL (no `/`, `?`, `#`, `..`). + */ +const BOT_TOKEN_RE = /^\d{3,20}:[A-Za-z0-9_-]{30,60}$/; + +export function isValidBotToken(token: string | null | undefined): token is string { + return typeof token === "string" && BOT_TOKEN_RE.test(token); +} + +/** Integer chat id (negative for groups/channels) or a public `@username` (5-32 characters). */ +export const CHAT_ID_RE = /^(-?\d{1,20}|@[A-Za-z][A-Za-z0-9_]{4,31})$/; + +/** File sources a caller may hand to sendPhoto/sendDocument: an https URL or a Telegram file_id. */ +export function validateFileRef(value: string): string | null { + const v = value.trim(); + if (!v) return null; + if (/^https:\/\//i.test(v)) { + try { + const u = new URL(v); + // No embedded credentials: they would be forwarded to Telegram's fetcher. + if (u.username || u.password) return null; + return u.toString(); + } catch { + return null; + } + } + return /^[A-Za-z0-9_-]{10,300}$/.test(v) ? v : null; +} + +// --- client ----------------------------------------------------------------- + +export type CallResult = { ok: true; result: T } | { ok: false; status: number; error: string }; +export type TelegramCall = ( + method: string, + params: Record, + timeoutMs?: number, +) => Promise>; + +/** + * Bind a token to a Bot API client. Every failure folds into `{ ok: false }` + * with a message that never contains the token: network errors report only the + * error class (a fetch error can echo the URL), and Telegram's own + * `description` never includes it. + */ +export function telegramClient( + token: string, + opts: { base?: string; fetchImpl?: typeof fetch } = {}, +): TelegramCall { + const base = (opts.base || TELEGRAM_API_BASE).replace(/\/+$/, ""); + const doFetch = opts.fetchImpl ?? fetch; + return async (method: string, params: Record, timeoutMs = 30_000) => { + let res: Response; + try { + res = await doFetch(`${base}/bot${token}/${method}`, { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(params), + signal: AbortSignal.timeout(timeoutMs), + }); + } catch (err) { + const name = err instanceof Error ? err.constructor.name : "Error"; + return { ok: false, status: 502, error: `could not reach Telegram: ${name}` }; + } + + let json: unknown; + try { + json = await res.json(); + } catch { + return { ok: false, status: 502, error: `Telegram returned ${res.status} with a non-JSON body` }; + } + if (isRecord(json) && json.ok === true) { + return { ok: true, result: json.result as T }; + } + const description = isRecord(json) ? (str(json.description) ?? "unknown error") : "unknown error"; + return { ok: false, status: res.status, error: explainError(res.status, description) }; + }; +} + +/** Turn the Bot API errors a user can act on into a next step. */ +export function explainError(status: number, description: string): string { + const base = `Telegram ${status}: ${description.slice(0, 300)}`; + if (status === 401) return `${base}. The bot token was rejected; re-issue it with @BotFather.`; + if (status === 409 && /webhook/i.test(description)) { + return ( + `${base}. This bot has a webhook set, so updates cannot be polled. Use a bot that no ` + + "other service consumes, or remove the webhook with deleteWebhook." + ); + } + if (status === 409) { + return `${base}. Another process is polling this bot; only one consumer can read its updates.`; + } + if (status === 403) return `${base}. The bot is not a member of that chat or was blocked by the user.`; + return base; +} + +// --- normalization ---------------------------------------------------------- + +export function isRecord(v: unknown): v is Record { + return typeof v === "object" && v !== null && !Array.isArray(v); +} + +function num(v: unknown): number | null { + return typeof v === "number" && Number.isFinite(v) ? v : null; +} + +function str(v: unknown): string | null { + return typeof v === "string" ? v : null; +} + +/** Bot API unix-seconds timestamp to ISO8601. */ +export function isoDate(v: unknown): string | null { + const seconds = num(v); + return seconds === null ? null : new Date(seconds * 1000).toISOString(); +} + +/** "First Last" from a User or private Chat, or null when neither is set. */ +export function fullName(raw: Record): string | null { + return [str(raw.first_name), str(raw.last_name)].filter(Boolean).join(" ") || null; +} + +export interface NormalizedChat { + id: number | null; + type: string | null; + title: string | null; + username: string | null; +} + +export interface NormalizedChatInfo extends NormalizedChat { + description: string | null; + member_count: number | null; + pinned_message: NormalizedMessage | null; +} + +export interface NormalizedUser { + id: number | null; + is_bot: boolean | null; + username: string | null; + name: string | null; +} + +export interface NormalizedMedia { + kind: string; + file_id: string | null; + file_name: string | null; + mime_type: string | null; +} + +export interface NormalizedMessage { + message_id: number | null; + date: string | null; + chat: NormalizedChat | null; + from: NormalizedUser | null; + text: string | null; + reply_to_message_id: number | null; + message_thread_id: number | null; + media: NormalizedMedia[]; +} + +/** One update, discriminated on `type`; kinds normalizeUpdate does not model arrive as `other`. */ +export type NormalizedUpdate = { update_id: number | null } & ( + | { type: MessageUpdateKind; message: NormalizedMessage | null } + | { + type: "message_reaction"; + chat: NormalizedChat | null; + from: NormalizedUser | null; + message_id: number | null; + date: string | null; + emoji: string[]; + } + | { type: "callback_query"; from: NormalizedUser | null; data: string | null; message: NormalizedMessage | null } + | { type: "other"; kind: string } +); + +// Media fields whose value is a single object carrying a file_id. +const SINGLE_FILE_MEDIA = ["document", "audio", "video", "voice", "video_note", "animation", "sticker"] as const; + +export function normalizeChat(raw: unknown): NormalizedChat | null { + if (!isRecord(raw)) return null; + return { + id: num(raw.id), + type: str(raw.type), + title: str(raw.title) ?? fullName(raw), + username: str(raw.username), + }; +} + +/** A `getChat` ChatFullInfo plus its member count, on top of the basic chat shape. */ +export function normalizeChatInfo(raw: unknown, memberCount: number | null): NormalizedChatInfo | null { + const chat = normalizeChat(raw); + if (!chat || !isRecord(raw)) return null; + return { + ...chat, + description: str(raw.description) ?? str(raw.bio), + member_count: memberCount, + pinned_message: normalizeMessage(raw.pinned_message), + }; +} + +export function normalizeUser(raw: unknown): NormalizedUser | null { + if (!isRecord(raw)) return null; + return { + id: num(raw.id), + is_bot: typeof raw.is_bot === "boolean" ? raw.is_bot : null, + username: str(raw.username), + name: fullName(raw), + }; +} + +function mediaOf(raw: Record): NormalizedMedia[] { + const media: NormalizedMedia[] = []; + // `photo` is an array of sizes; the last is the largest. + if (Array.isArray(raw.photo) && raw.photo.length > 0) { + const largest = raw.photo[raw.photo.length - 1]; + media.push({ kind: "photo", file_id: isRecord(largest) ? str(largest.file_id) : null, file_name: null, mime_type: null }); + } + for (const kind of SINGLE_FILE_MEDIA) { + const m = raw[kind]; + if (isRecord(m)) { + media.push({ kind, file_id: str(m.file_id), file_name: str(m.file_name), mime_type: str(m.mime_type) }); + } + } + return media; +} + +export function normalizeMessage(raw: unknown): NormalizedMessage | null { + if (!isRecord(raw)) return null; + return { + message_id: num(raw.message_id), + date: isoDate(raw.date), + chat: normalizeChat(raw.chat), + from: normalizeUser(raw.from) ?? normalizeUser(raw.sender_chat), + text: str(raw.text) ?? str(raw.caption), + reply_to_message_id: isRecord(raw.reply_to_message) ? num(raw.reply_to_message.message_id) : null, + message_thread_id: num(raw.message_thread_id), + media: mediaOf(raw), + }; +} + +export function normalizeUpdate(raw: unknown): NormalizedUpdate { + const rec = isRecord(raw) ? raw : {}; + const update_id = num(rec.update_id); + for (const type of MESSAGE_UPDATE_KINDS) { + if (isRecord(rec[type])) return { update_id, type, message: normalizeMessage(rec[type]) }; + } + if (isRecord(rec.message_reaction)) { + const r = rec.message_reaction; + const emoji = Array.isArray(r.new_reaction) + ? r.new_reaction.flatMap((x) => (isRecord(x) && typeof x.emoji === "string" ? [x.emoji] : [])) + : []; + return { + update_id, + type: "message_reaction", + chat: normalizeChat(r.chat), + from: normalizeUser(r.user) ?? normalizeUser(r.actor_chat), + message_id: num(r.message_id), + date: isoDate(r.date), + emoji, + }; + } + if (isRecord(rec.callback_query)) { + const q = rec.callback_query; + return { + update_id, + type: "callback_query", + from: normalizeUser(q.from), + data: str(q.data), + message: normalizeMessage(q.message), + }; + } + // Any other kind (poll, chat_member, ...): name it so the caller knows it + // exists and can advance the offset past it. + return { update_id, type: "other", kind: Object.keys(rec).find((k) => k !== "update_id") ?? "unknown" }; +} + +/** Offset that acknowledges every update in `updates` (Bot API: last id + 1). */ +export function nextOffset(updates: NormalizedUpdate[]): number | null { + let max: number | null = null; + for (const u of updates) if (u.update_id !== null && (max === null || u.update_id > max)) max = u.update_id; + return max === null ? null : max + 1; +} diff --git a/servers/telegram-bot/telegram-bot.svg b/servers/telegram-bot/telegram-bot.svg new file mode 100644 index 0000000..b637cb5 --- /dev/null +++ b/servers/telegram-bot/telegram-bot.svg @@ -0,0 +1 @@ +Telegram \ No newline at end of file diff --git a/servers/telegram-bot/test/integration/env.d.ts b/servers/telegram-bot/test/integration/env.d.ts new file mode 100644 index 0000000..de75433 --- /dev/null +++ b/servers/telegram-bot/test/integration/env.d.ts @@ -0,0 +1,7 @@ +// Types the `env` provided by cloudflare:test with this worker's bindings. +// (Excluded from `tsc --noEmit`; consumed by the editor / vitest.) +import type { Env } from "../../src/index"; + +declare module "cloudflare:test" { + interface ProvidedEnv extends Env {} +} diff --git a/servers/telegram-bot/test/integration/worker.spec.ts b/servers/telegram-bot/test/integration/worker.spec.ts new file mode 100644 index 0000000..07a07a5 --- /dev/null +++ b/servers/telegram-bot/test/integration/worker.spec.ts @@ -0,0 +1,272 @@ +import { fetchMock, SELF } from "cloudflare:test"; +import { afterEach, beforeAll, describe, expect, it } from "vitest"; + +const ORIGIN = "https://example.com"; +const TOKEN = "123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsawE"; +const TG = "https://api.telegram.org"; +const HEADERS = { + "content-type": "application/json", + accept: "application/json, text/event-stream", + "x-telegram-bot-token": TOKEN, + authorization: "Bearer test-token", +}; + +const INIT = { + jsonrpc: "2.0", + id: 1, + method: "initialize", + params: { protocolVersion: "2025-06-18", capabilities: {}, clientInfo: { name: "test", version: "1.0" } }, +}; + +const EXPECTED_TOOLS = [ + "telegram_delete_message", + "telegram_edit_message_text", + "telegram_forward_message", + "telegram_get_chat", + "telegram_get_me", + "telegram_get_updates", + "telegram_pin_message", + "telegram_send_document", + "telegram_send_message", + "telegram_send_photo", + "telegram_set_reaction", + "telegram_unpin_message", +]; + +function mockTelegram(method: string, status: number, body: unknown, onBody?: (b: unknown) => void) { + fetchMock + .get(TG) + .intercept({ path: `/bot${TOKEN}/${method}`, method: "POST" }) + .reply(status, (opts) => { + onBody?.(JSON.parse(String(opts.body))); + return JSON.stringify(body); + }); +} + +async function rpc(body: unknown, headers: Record = HEADERS) { + return SELF.fetch(`${ORIGIN}/mcp`, { method: "POST", headers, body: JSON.stringify(body) }); +} + +async function callTool(name: string, args: Record) { + const res = await rpc({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name, arguments: args } }); + expect(res.status).toBe(200); + const msg = (await res.json()) as { result: Record }; + return msg.result; +} + +beforeAll(() => { + fetchMock.activate(); + fetchMock.disableNetConnect(); +}); + +afterEach(() => { + fetchMock.assertNoPendingInterceptors(); +}); + +describe("routing", () => { + it("GET /health returns ok", async () => { + const res = await SELF.fetch(`${ORIGIN}/health`); + expect(res.status).toBe(200); + expect(await res.json()).toMatchObject({ ok: true, service: "telegram-bot" }); + }); + + it("unknown path is 404", async () => { + expect((await SELF.fetch(`${ORIGIN}/nope`)).status).toBe(404); + }); +}); + +describe("fleet auth gate on /mcp", () => { + it("401s without the gateway credential, even with a valid bot token", async () => { + const { authorization: _, ...rest } = HEADERS; + const res = await rpc(INIT, rest); + expect(res.status).toBe(401); + expect(res.headers.get("www-authenticate")).toContain("Bearer"); + }); + + it("401s a wrong gateway credential", async () => { + const res = await rpc(INIT, { ...HEADERS, authorization: "Bearer nope" }); + expect(res.status).toBe(401); + }); +}); + +describe("bot token gate on /mcp", () => { + it("401s without the token header", async () => { + const { "x-telegram-bot-token": _, ...rest } = HEADERS; + const res = await rpc(INIT, rest); + expect(res.status).toBe(401); + expect(await res.text()).toContain("x-telegram-bot-token"); + }); + + it("401s a malformed token without calling Telegram", async () => { + const res = await rpc(INIT, { ...HEADERS, "x-telegram-bot-token": `${TOKEN}/../x` }); + expect(res.status).toBe(401); + }); + + it("401s on initialize when Telegram rejects the token", async () => { + mockTelegram("getMe", 401, { ok: false, error_code: 401, description: "Unauthorized" }); + const res = await rpc(INIT); + expect(res.status).toBe(401); + const body = await res.text(); + expect(body).toContain("@BotFather"); + expect(body).not.toContain(TOKEN); + }); + + it("405s a GET, even without a token: the server is stateless", async () => { + const { "x-telegram-bot-token": _, ...rest } = HEADERS; + const res = await SELF.fetch(`${ORIGIN}/mcp`, { method: "GET", headers: rest }); + expect(res.status).toBe(405); + }); +}); + +describe("MCP over the stateless transport", () => { + it("initializes with a valid token and issues no session id", async () => { + mockTelegram("getMe", 200, { ok: true, result: { id: 123456789, is_bot: true, first_name: "B", username: "b_bot" } }); + const res = await rpc(INIT); + expect(res.status).toBe(200); + expect(res.headers.get("mcp-session-id")).toBeNull(); + const msg = (await res.json()) as { result: { serverInfo: { name: string } } }; + expect(msg.result.serverInfo.name).toBe("telegram-bot"); + }); + + it("lists every tool", async () => { + const res = await rpc({ jsonrpc: "2.0", id: 2, method: "tools/list" }); + const msg = (await res.json()) as { result: { tools: { name: string }[] } }; + expect(msg.result.tools.map((t) => t.name).sort()).toEqual(EXPECTED_TOOLS); + }); + + it("surfaces the webhook conflict on get_updates", async () => { + mockTelegram("getUpdates", 409, { + ok: false, + error_code: 409, + description: "Conflict: can't use getUpdates method while webhook is active", + }); + const result = await callTool("telegram_get_updates", {}); + expect(result.isError).toBe(true); + expect(result.content[0].text).toContain("deleteWebhook"); + }); + + it("short-polls for updates and returns the offset that acknowledges them", async () => { + let sent: unknown; + mockTelegram("getUpdates", 200, { + ok: true, + result: [ + { update_id: 100, message: { message_id: 1, date: 1_700_000_000, chat: { id: 5, type: "private" }, text: "a" } }, + { update_id: 101, message: { message_id: 2, date: 1_700_000_001, chat: { id: 5, type: "private" }, text: "b" } }, + ], + }, (b) => (sent = b)); + const result = await callTool("telegram_get_updates", { limit: 10 }); + expect(sent).toEqual({ limit: 10, timeout: 0 }); + expect(result.structuredContent).toMatchObject({ count: 2, next_offset: 102 }); + }); + + it("rejects a bad chat id in the schema, before calling Telegram", async () => { + const result = await callTool("telegram_send_message", { chat_id: "general", text: "hi" }); + expect(result.isError).toBe(true); + expect(JSON.stringify(result.content)).toContain("chat_id"); + }); + + it("rejects a non-https photo in the schema, before calling Telegram", async () => { + const result = await callTool("telegram_send_photo", { chat_id: 5, photo: "http://example.com/a.png" }); + expect(result.isError).toBe(true); + // The schema's own message, not the 502 a blocked Telegram call would produce. + expect(JSON.stringify(result.content)).toContain("photo must be an https:// URL"); + }); + + it("returns chat info with a null member count when that call fails", async () => { + mockTelegram("getChat", 200, { ok: true, result: { id: -100, type: "supergroup", title: "T", description: "d" } }); + mockTelegram("getChatMemberCount", 400, { ok: false, error_code: 400, description: "Bad Request" }); + const result = await callTool("telegram_get_chat", { chat_id: -100 }); + expect(result.structuredContent.chat).toMatchObject({ id: -100, title: "T", description: "d", member_count: null }); + }); +}); + +// Every tool that maps to one Bot API method: call it, and assert both the exact +// body sent to Telegram and that the tool reports success. +const SENT = { message_id: 42, date: 1_700_000_000, chat: { id: 555, type: "private" }, text: "x" }; +const REPLY = { + message_id: 7, + allow_sending_without_reply: true, +}; +const TOOL_CASES: { tool: string; args: Record; method: string; body: unknown; result: unknown }[] = [ + { tool: "telegram_get_me", args: {}, method: "getMe", body: {}, result: { id: 1, is_bot: true, username: "b_bot" } }, + { + tool: "telegram_send_message", + args: { chat_id: 555, text: "hi", reply_to_message_id: 7, disable_link_preview: true }, + method: "sendMessage", + body: { chat_id: "555", text: "hi", reply_parameters: REPLY, link_preview_options: { is_disabled: true } }, + result: SENT, + }, + { + tool: "telegram_send_photo", + args: { chat_id: "@my_channel", photo: "https://example.com/a.png", caption: "c" }, + method: "sendPhoto", + body: { chat_id: "@my_channel", photo: "https://example.com/a.png", caption: "c" }, + result: SENT, + }, + { + tool: "telegram_send_document", + args: { chat_id: 555, document: "BQACAgQAAxkBAAIB", message_thread_id: 3 }, + method: "sendDocument", + body: { chat_id: "555", document: "BQACAgQAAxkBAAIB", message_thread_id: 3 }, + result: SENT, + }, + { + tool: "telegram_forward_message", + args: { chat_id: 1, from_chat_id: -100, message_id: 9 }, + method: "forwardMessage", + body: { chat_id: "1", from_chat_id: "-100", message_id: 9 }, + result: SENT, + }, + { + tool: "telegram_edit_message_text", + args: { chat_id: 555, message_id: 42, text: "new", parse_mode: "HTML" }, + method: "editMessageText", + body: { chat_id: "555", message_id: 42, text: "new", parse_mode: "HTML" }, + result: SENT, + }, + { + tool: "telegram_delete_message", + args: { chat_id: 555, message_id: 42 }, + method: "deleteMessage", + body: { chat_id: "555", message_id: 42 }, + result: true, + }, + { + tool: "telegram_set_reaction", + args: { chat_id: 555, message_id: 42, emoji: "👍" }, + method: "setMessageReaction", + body: { chat_id: "555", message_id: 42, reaction: [{ type: "emoji", emoji: "👍" }] }, + result: true, + }, + { + tool: "telegram_set_reaction", + args: { chat_id: 555, message_id: 42 }, + method: "setMessageReaction", + body: { chat_id: "555", message_id: 42, reaction: [] }, + result: true, + }, + { + tool: "telegram_pin_message", + args: { chat_id: 555, message_id: 42, disable_notification: true }, + method: "pinChatMessage", + body: { chat_id: "555", message_id: 42, disable_notification: true }, + result: true, + }, + { + tool: "telegram_unpin_message", + args: { chat_id: 555, message_id: 42 }, + method: "unpinChatMessage", + body: { chat_id: "555", message_id: 42 }, + result: true, + }, +]; + +describe("each tool sends the right Bot API call", () => { + it.each(TOOL_CASES)("$tool -> $method", async ({ tool, args, method, body, result }) => { + let sent: unknown; + mockTelegram(method, 200, { ok: true, result }, (b) => (sent = b)); + const out = await callTool(tool, args); + expect(out.isError).toBeUndefined(); + expect(sent).toEqual(body); + }); +}); diff --git a/servers/telegram-bot/test/unit/auth.test.ts b/servers/telegram-bot/test/unit/auth.test.ts new file mode 100644 index 0000000..d32f8d5 --- /dev/null +++ b/servers/telegram-bot/test/unit/auth.test.ts @@ -0,0 +1,125 @@ +import { describe, expect, test } from "bun:test"; + +import { checkAuth, extractBearer, resolveAuthMode, timingSafeEqual } from "../../src/auth"; + +function req(headers: Record = {}) { + return { headers: new Headers(headers) }; +} + +describe("resolveAuthMode", () => { + test("defaults to open with no token, bearer with a token", () => { + expect(resolveAuthMode({})).toBe("open"); + expect(resolveAuthMode({ AUTH_TOKEN: "x" })).toBe("bearer"); + }); + + test("honors an explicit mode", () => { + expect(resolveAuthMode({ AUTH_MODE: "open", AUTH_TOKEN: "x" })).toBe("open"); + expect(resolveAuthMode({ AUTH_MODE: "edison-jwt" })).toBe("edison-jwt"); + }); + + test("passes an unknown mode through verbatim (checkAuth rejects it)", () => { + expect(resolveAuthMode({ AUTH_MODE: "totally-made-up" })).toBe("totally-made-up"); + }); + + test("a defined-but-empty token stays bearer (never silently open)", () => { + // AUTH_TOKEN="" is a misconfig, not a request for open mode: keep it in + // bearer so checkAuth 500s rather than serving unauthenticated traffic. + expect(resolveAuthMode({ AUTH_TOKEN: "" })).toBe("bearer"); + expect(resolveAuthMode({ AUTH_TOKEN: " " })).toBe("bearer"); + }); + + test("an explicitly empty/whitespace AUTH_MODE is invalid, never open", () => { + // A set-but-empty AUTH_MODE is a misconfig: it must fail closed, not fall + // through to `open` (which would serve the tools unauthenticated). + expect(resolveAuthMode({ AUTH_MODE: "" })).toBe("invalid-auth-mode"); + expect(resolveAuthMode({ AUTH_MODE: " " })).toBe("invalid-auth-mode"); + // ...even with no AUTH_TOKEN present at all. + expect(resolveAuthMode({ AUTH_MODE: " " })).toBe("invalid-auth-mode"); + }); +}); + +describe("extractBearer", () => { + test("parses a bearer header case-insensitively", () => { + expect(extractBearer("Bearer abc123")).toBe("abc123"); + expect(extractBearer("bearer spaced ")).toBe("spaced"); + }); + + test("returns null for missing or malformed headers", () => { + expect(extractBearer(null)).toBeNull(); + expect(extractBearer("Basic abc")).toBeNull(); + }); +}); + +describe("timingSafeEqual", () => { + test("matches equal strings and rejects unequal / different-length", () => { + expect(timingSafeEqual("secret", "secret")).toBe(true); + expect(timingSafeEqual("secret", "secre7")).toBe(false); + expect(timingSafeEqual("secret", "secretx")).toBe(false); + }); +}); + +describe("checkAuth", () => { + test("open mode admits everyone", async () => { + expect(await checkAuth(req(), {})).toMatchObject({ ok: true, mode: "open", subject: "anonymous" }); + }); + + test("bearer mode: missing, wrong, and correct tokens", async () => { + const env = { AUTH_TOKEN: "s3cret" }; + expect(await checkAuth(req(), env)).toMatchObject({ ok: false, status: 401 }); + expect(await checkAuth(req({ authorization: "Bearer nope" }), env)).toMatchObject({ + ok: false, + status: 401, + }); + expect(await checkAuth(req({ authorization: "Bearer s3cret" }), env)).toMatchObject({ + ok: true, + subject: "bearer", + }); + }); + + test("bearer mode with no configured token is a 500 misconfig", async () => { + expect(await checkAuth(req({ authorization: "Bearer x" }), { AUTH_MODE: "bearer" })).toMatchObject({ + ok: false, + status: 500, + }); + }); + + test("a defined-but-empty token 500s (misconfig), never admits as open", async () => { + // No AUTH_MODE + AUTH_TOKEN="" must resolve to bearer and fail closed. + expect(await checkAuth(req({ authorization: "Bearer anything" }), { AUTH_TOKEN: "" })).toMatchObject({ + ok: false, + status: 500, + }); + expect(await checkAuth(req({ authorization: "Bearer anything" }), { AUTH_TOKEN: " " })).toMatchObject({ + ok: false, + status: 500, + }); + }); + + test("edison-jwt with incomplete config fails closed (500), never admits", async () => { + // Missing JWKS/issuer/audience must not fall through to allowing traffic. + expect(await checkAuth(req({ authorization: "Bearer x" }), { AUTH_MODE: "edison-jwt" })).toMatchObject( + { ok: false, status: 500 }, + ); + }); + + test("edison-jwt with full config but no token is a 401", async () => { + const env = { + AUTH_MODE: "edison-jwt", + EDISON_JWKS_URL: "https://edison.example/.well-known/jwks.json", + EDISON_JWT_ISSUER: "https://edison.example", + EDISON_JWT_AUDIENCE: "reddit", + }; + expect(await checkAuth(req(), env)).toMatchObject({ ok: false, status: 401 }); + }); + + test("an unknown mode is rejected, never admitted", async () => { + const result = await checkAuth(req(), { AUTH_MODE: "totally-made-up" }); + expect(result.ok).toBe(false); + }); + + test("an explicitly empty AUTH_MODE fails closed (500), never admits as open", async () => { + // AUTH_MODE="" must not resolve to `open` and serve unauthenticated traffic. + const result = await checkAuth(req({ authorization: "Bearer x" }), { AUTH_MODE: "" }); + expect(result).toMatchObject({ ok: false, status: 500 }); + }); +}); diff --git a/servers/telegram-bot/test/unit/jwt.test.ts b/servers/telegram-bot/test/unit/jwt.test.ts new file mode 100644 index 0000000..2a6a99a --- /dev/null +++ b/servers/telegram-bot/test/unit/jwt.test.ts @@ -0,0 +1,254 @@ +import { beforeAll, describe, expect, test } from "bun:test"; + +import { + __resetJwksCacheForTest, + type EdisonJwtConfig, + type Jwks, + verifyEdisonJwt, + verifyJwtWithJwks, +} from "../../src/jwt"; + +// Deterministic clock so exp/nbf checks don't depend on wall time. +const NOW = 1_000_000_000; +const KID = "test-key-1"; +const CONFIG: EdisonJwtConfig = { + jwksUrl: "https://edison.example/.well-known/jwks.json", + issuer: "https://edison.example", + audience: "reddit", +}; + +// Every JWKS fetch must go to the configured endpoint, so a regression that +// ignores or hardcodes config.jwksUrl fails the fetch-counting tests below. +function expectJwksUrl(input: unknown): void { + const url = input instanceof Request ? input.url : String(input); + expect(new URL(url).href).toBe(CONFIG.jwksUrl); +} + + +function b64url(bytes: Uint8Array): string { + let bin = ""; + for (const b of bytes) bin += String.fromCharCode(b); + return btoa(bin).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, ""); +} +function b64urlStr(s: string): string { + return b64url(new TextEncoder().encode(s)); +} + +let privateKey: CryptoKey; +let jwks: Jwks; + +async function sign( + claims: Record, + opts: { kid?: string | null; alg?: string } = {}, +): Promise { + // opts.kid === null omits the kid header entirely (to test rejection). + const headerObj: Record = { alg: opts.alg ?? "RS256", typ: "JWT" }; + if (opts.kid !== null) headerObj.kid = opts.kid ?? KID; + const header = b64urlStr(JSON.stringify(headerObj)); + const payload = b64urlStr(JSON.stringify(claims)); + const signingInput = `${header}.${payload}`; + const sig = await crypto.subtle.sign( + { name: "RSASSA-PKCS1-v1_5" }, + privateKey, + new TextEncoder().encode(signingInput), + ); + return `${signingInput}.${b64url(new Uint8Array(sig))}`; +} + +function validClaims(overrides: Record = {}): Record { + return { iss: CONFIG.issuer, aud: CONFIG.audience, sub: "user-42", iat: NOW, exp: NOW + 300, ...overrides }; +} + +beforeAll(async () => { + const pair = (await crypto.subtle.generateKey( + { name: "RSASSA-PKCS1-v1_5", modulusLength: 2048, publicExponent: new Uint8Array([1, 0, 1]), hash: "SHA-256" }, + true, + ["sign", "verify"], + )) as CryptoKeyPair; + privateKey = pair.privateKey; + const jwk = (await crypto.subtle.exportKey("jwk", pair.publicKey)) as JsonWebKey; + jwks = { keys: [{ ...jwk, kid: KID, use: "sig", alg: "RS256" } as Jwks["keys"][number]] }; +}); + +describe("verifyJwtWithJwks", () => { + test("accepts a well-formed, correctly signed token and returns sub", async () => { + const token = await sign(validClaims()); + expect(await verifyJwtWithJwks(token, jwks, CONFIG, NOW)).toEqual({ ok: true, sub: "user-42" }); + }); + + test("accepts an audience array that includes this server", async () => { + const token = await sign(validClaims({ aud: ["other", CONFIG.audience] })); + expect(await verifyJwtWithJwks(token, jwks, CONFIG, NOW)).toMatchObject({ ok: true }); + }); + + test("rejects an expired token", async () => { + const token = await sign(validClaims({ exp: NOW - 120 })); + expect(await verifyJwtWithJwks(token, jwks, CONFIG, NOW)).toMatchObject({ ok: false, status: 401 }); + }); + + test("rejects a wrong issuer", async () => { + const token = await sign(validClaims({ iss: "https://evil.example" })); + expect(await verifyJwtWithJwks(token, jwks, CONFIG, NOW)).toMatchObject({ ok: false, status: 401 }); + }); + + test("rejects a wrong audience", async () => { + const token = await sign(validClaims({ aud: "some-other-server" })); + expect(await verifyJwtWithJwks(token, jwks, CONFIG, NOW)).toMatchObject({ ok: false, status: 401 }); + }); + + test("rejects a missing sub", async () => { + const token = await sign(validClaims({ sub: undefined })); + expect(await verifyJwtWithJwks(token, jwks, CONFIG, NOW)).toMatchObject({ ok: false, status: 401 }); + }); + + test("rejects a non-RS256 alg without trusting the signature", async () => { + const token = await sign(validClaims(), { alg: "HS256" }); + expect(await verifyJwtWithJwks(token, jwks, CONFIG, NOW)).toMatchObject({ ok: false, status: 401 }); + }); + + test("rejects when no JWKS key matches the kid", async () => { + const token = await sign(validClaims(), { kid: "unknown-kid" }); + expect(await verifyJwtWithJwks(token, jwks, CONFIG, NOW)).toMatchObject({ ok: false, status: 401 }); + }); + + test("rejects a token with no kid (Edison always sets one)", async () => { + const token = await sign(validClaims(), { kid: null }); + expect(await verifyJwtWithJwks(token, jwks, CONFIG, NOW)).toMatchObject({ ok: false, status: 401 }); + }); + + test("rejects a tampered signature", async () => { + const token = await sign(validClaims()); + const tampered = token.slice(0, -3) + (token.endsWith("AAA") ? "BBB" : "AAA"); + expect(await verifyJwtWithJwks(tampered, jwks, CONFIG, NOW)).toMatchObject({ ok: false, status: 401 }); + }); + + test("rejects a structurally malformed token", async () => { + expect(await verifyJwtWithJwks("not.a.jwt.at.all", jwks, CONFIG, NOW)).toMatchObject({ ok: false }); + expect(await verifyJwtWithJwks("onlyonepart", jwks, CONFIG, NOW)).toMatchObject({ ok: false }); + }); + + test("rejects a header/claims that decode to a non-object (null/array) without throwing", async () => { + // JSON `null` parses fine but would throw on the `header.alg`/`claims.iss` + // reads - must come back as a 401, never an uncaught 500. + const goodHeader = b64urlStr(JSON.stringify({ alg: "RS256", kid: KID, typ: "JWT" })); + const goodPayload = b64urlStr(JSON.stringify(validClaims())); + const nullPart = b64urlStr("null"); + const arrPart = b64urlStr("[]"); + expect(await verifyJwtWithJwks(`${nullPart}.${goodPayload}.AAAA`, jwks, CONFIG, NOW)).toMatchObject({ + ok: false, + status: 401, + }); + // A non-object claims is rejected before the signature check even runs. + expect(await verifyJwtWithJwks(`${goodHeader}.${nullPart}.AAAA`, jwks, CONFIG, NOW)).toMatchObject({ + ok: false, + status: 401, + }); + expect(await verifyJwtWithJwks(`${arrPart}.${goodPayload}.AAAA`, jwks, CONFIG, NOW)).toMatchObject({ + ok: false, + status: 401, + }); + }); +}); + +describe("verifyEdisonJwt (JWKS fetch + refetch cooldown)", () => { + // Claims must be valid under the real clock - verifyEdisonJwt uses Date.now(). + function realClaims(overrides: Record = {}): Record { + const now = Math.floor(Date.now() / 1000); + return { iss: CONFIG.issuer, aud: CONFIG.audience, sub: "user-42", iat: now, exp: now + 300, ...overrides }; + } + + test("fetches JWKS once, then rate-limits attacker-driven kid-miss refetches", async () => { + __resetJwksCacheForTest(); + let fetches = 0; + const realFetch = globalThis.fetch; + globalThis.fetch = (async (input: unknown) => { + expectJwksUrl(input); + fetches++; + return new Response(JSON.stringify(jwks), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as unknown as typeof fetch; + try { + const ok = await verifyEdisonJwt(await sign(realClaims()), CONFIG); + expect(ok).toMatchObject({ ok: true, sub: "user-42" }); + expect(fetches).toBe(1); + + // A burst of unknown, attacker-chosen kids must not each hit the network. + for (let i = 0; i < 5; i++) { + await verifyEdisonJwt(await sign(realClaims(), { kid: `attacker-${i}` }), CONFIG); + } + expect(fetches).toBe(1); + } finally { + globalThis.fetch = realFetch; + } + }); + + test("coalesces a concurrent cold-start burst into a single JWKS fetch", async () => { + __resetJwksCacheForTest(); + let fetches = 0; + const realFetch = globalThis.fetch; + // A deliberately slow fetch so all requests overlap the same in-flight GET. + globalThis.fetch = (async (input: unknown) => { + expectJwksUrl(input); + fetches++; + await new Promise((r) => setTimeout(r, 20)); + return new Response(JSON.stringify(jwks), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as unknown as typeof fetch; + try { + const tokens = await Promise.all(Array.from({ length: 8 }, () => sign(realClaims()))); + const results = await Promise.all(tokens.map((t) => verifyEdisonJwt(t, CONFIG))); + expect(results.every((r) => r.ok)).toBe(true); + expect(fetches).toBe(1); // 8 concurrent requests, one upstream GET + } finally { + globalThis.fetch = realFetch; + } + }); + + test("a JWKS whose keys contain a null/non-object entry is rejected (503, not a 500)", async () => { + __resetJwksCacheForTest(); + const realFetch = globalThis.fetch; + globalThis.fetch = (async (input: unknown) => { + expectJwksUrl(input); + return new Response(JSON.stringify({ keys: [null] }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as unknown as typeof fetch; + try { + // A malformed JWKS must be treated as "no JWKS" (caller 503s), never cached + // and dereferenced downstream into an uncaught 500. + const result = await verifyEdisonJwt(await sign(realClaims()), CONFIG); + expect(result).toMatchObject({ ok: false, status: 503 }); + } finally { + globalThis.fetch = realFetch; + } + }); + + test("a failed fetch does not suppress the next request's retry", async () => { + __resetJwksCacheForTest(); + let fetches = 0; + const realFetch = globalThis.fetch; + globalThis.fetch = (async (input: unknown) => { + expectJwksUrl(input); + fetches++; + if (fetches === 1) throw new Error("transient network blip"); + return new Response(JSON.stringify(jwks), { + status: 200, + headers: { "content-type": "application/json" }, + }); + }) as unknown as typeof fetch; + try { + const first = await verifyEdisonJwt(await sign(realClaims()), CONFIG); + expect(first).toMatchObject({ ok: false, status: 503 }); // fetch failed -> 503 + const second = await verifyEdisonJwt(await sign(realClaims()), CONFIG); + expect(second).toMatchObject({ ok: true }); // retried immediately, not suppressed + expect(fetches).toBe(2); + } finally { + globalThis.fetch = realFetch; + } + }); +}); diff --git a/servers/telegram-bot/test/unit/telegram.test.ts b/servers/telegram-bot/test/unit/telegram.test.ts new file mode 100644 index 0000000..1d5c051 --- /dev/null +++ b/servers/telegram-bot/test/unit/telegram.test.ts @@ -0,0 +1,240 @@ +import { describe, expect, test } from "bun:test"; + +import { + CHAT_ID_RE, + explainError, + fullName, + isValidBotToken, + isoDate, + nextOffset, + normalizeChatInfo, + normalizeMessage, + normalizeUpdate, + telegramClient, + validateFileRef, +} from "../../src/telegram"; + +const TOKEN = "123456789:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsawE"; + +describe("isValidBotToken", () => { + test("accepts a @BotFather token", () => { + expect(isValidBotToken(TOKEN)).toBe(true); + }); + + test("rejects anything that could reshape the request URL", () => { + for (const bad of [ + "", + "123", + "abc:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsawE", + `${TOKEN}/sendMessage`, + `${TOKEN}?x=1`, + "123456789:../../AAHdqTcvCH1vGWJxfSeofSAs0K5P", + `${TOKEN}#frag`, + ]) { + expect(isValidBotToken(bad)).toBe(false); + } + expect(isValidBotToken(null)).toBe(false); + }); +}); + +describe("CHAT_ID_RE", () => { + test("matches integer ids and public usernames only", () => { + for (const good of ["42", "-1001234567890", "@my_channel", "@abcde"]) expect(CHAT_ID_RE.test(good)).toBe(true); + for (const bad of ["general", "@ab", "@abcd", "1.5", "@1abcd", ""]) expect(CHAT_ID_RE.test(bad)).toBe(false); + }); +}); + +describe("telegramClient", () => { + const replying = (status: number, body: unknown) => + (async () => new Response(JSON.stringify(body), { status })) as unknown as typeof fetch; + + test("posts to /bot/ and unwraps the result", async () => { + let url = ""; + let sent: unknown; + const fetchImpl = (async (u: string, init: RequestInit) => { + url = u; + sent = JSON.parse(String(init.body)); + return new Response(JSON.stringify({ ok: true, result: { id: 1 } })); + }) as unknown as typeof fetch; + const call = telegramClient(TOKEN, { base: "http://local/", fetchImpl }); + expect(await call("getChat", { chat_id: "1" })).toEqual({ ok: true, result: { id: 1 } }); + expect(url).toBe(`http://local/bot${TOKEN}/getChat`); + expect(sent).toEqual({ chat_id: "1" }); + }); + + test("maps a Telegram error to an actionable message", async () => { + const call = telegramClient(TOKEN, { + fetchImpl: replying(401, { ok: false, error_code: 401, description: "Unauthorized" }), + }); + const res = await call("getMe", {}); + expect(res).toMatchObject({ ok: false, status: 401 }); + if (!res.ok) expect(res.error).toContain("@BotFather"); + }); + + test("never echoes the token when the network fails", async () => { + const fetchImpl = (async () => { + throw new TypeError(`fetch failed for https://api.telegram.org/bot${TOKEN}/getMe`); + }) as unknown as typeof fetch; + const res = await telegramClient(TOKEN, { fetchImpl })("getMe", {}); + expect(res.ok).toBe(false); + if (!res.ok) { + expect(res.error).not.toContain(TOKEN); + expect(res.error).toContain("TypeError"); + } + }); + + test("handles a non-JSON body", async () => { + const fetchImpl = (async () => new Response("bad gateway", { status: 502 })) as unknown as typeof fetch; + expect((await telegramClient(TOKEN, { fetchImpl })("getMe", {})).ok).toBe(false); + }); +}); + +describe("explainError", () => { + test("names the webhook conflict", () => { + expect(explainError(409, "Conflict: can't use getUpdates method while webhook is active")).toContain( + "deleteWebhook", + ); + }); + + test("names a second poller", () => { + expect(explainError(409, "Conflict: terminated by other getUpdates request")).toContain("one consumer"); + }); +}); + +describe("small helpers", () => { + test("isoDate converts unix seconds and rejects junk", () => { + expect(isoDate(1_700_000_000)).toBe("2023-11-14T22:13:20.000Z"); + expect(isoDate("1700000000")).toBeNull(); + }); + + test("fullName joins what is present", () => { + expect(fullName({ first_name: "Ada", last_name: "L" })).toBe("Ada L"); + expect(fullName({ first_name: "Ada" })).toBe("Ada"); + expect(fullName({})).toBeNull(); + }); +}); + +const RAW_MESSAGE = { + message_id: 7, + date: 1_700_000_000, + chat: { id: -100123, type: "supergroup", title: "Team" }, + from: { id: 55, is_bot: false, first_name: "Ada", last_name: "L", username: "ada" }, + text: "hello", + reply_to_message: { message_id: 6 }, + message_thread_id: 3, +}; + +describe("normalizeMessage", () => { + test("keeps the useful fields", () => { + expect(normalizeMessage(RAW_MESSAGE)).toEqual({ + message_id: 7, + date: "2023-11-14T22:13:20.000Z", + chat: { id: -100123, type: "supergroup", title: "Team", username: null }, + from: { id: 55, is_bot: false, username: "ada", name: "Ada L" }, + text: "hello", + reply_to_message_id: 6, + message_thread_id: 3, + media: [], + }); + }); + + test("uses the caption as text and lists media, largest photo first", () => { + const msg = normalizeMessage({ + message_id: 8, + chat: { id: 1, type: "private", first_name: "Bo" }, + caption: "look", + photo: [{ file_id: "small" }, { file_id: "large" }], + document: { file_id: "doc1", file_name: "a.pdf", mime_type: "application/pdf" }, + }); + expect(msg?.text).toBe("look"); + expect(msg?.chat?.title).toBe("Bo"); + expect(msg?.media).toEqual([ + { kind: "photo", file_id: "large", file_name: null, mime_type: null }, + { kind: "document", file_id: "doc1", file_name: "a.pdf", mime_type: "application/pdf" }, + ]); + }); + + test("returns null for non-objects", () => { + expect(normalizeMessage(undefined)).toBeNull(); + }); +}); + +describe("normalizeChatInfo", () => { + test("extends the chat with description, member count and pinned message", () => { + expect( + normalizeChatInfo({ id: 1, type: "private", first_name: "Ada", bio: "hi", pinned_message: RAW_MESSAGE }, null), + ).toMatchObject({ + id: 1, + title: "Ada", + description: "hi", + member_count: null, + pinned_message: { message_id: 7 }, + }); + expect(normalizeChatInfo({ id: 2, type: "group", title: "G" }, 12)?.member_count).toBe(12); + expect(normalizeChatInfo(null, 1)).toBeNull(); + }); +}); + +describe("normalizeUpdate", () => { + test("message kinds carry the message", () => { + const u = normalizeUpdate({ update_id: 10, channel_post: RAW_MESSAGE }); + expect(u.type).toBe("channel_post"); + if (u.type === "channel_post") expect(u.message?.message_id).toBe(7); + }); + + test("reactions carry their own fields, not a fake message", () => { + expect( + normalizeUpdate({ + update_id: 11, + message_reaction: { + chat: { id: 1, type: "private" }, + message_id: 7, + user: { id: 55, first_name: "Ada" }, + date: 1_700_000_000, + new_reaction: [{ type: "emoji", emoji: "👍" }, { type: "custom_emoji", custom_emoji_id: "x" }], + }, + }), + ).toEqual({ + update_id: 11, + type: "message_reaction", + chat: { id: 1, type: "private", title: null, username: null }, + from: { id: 55, is_bot: null, username: null, name: "Ada" }, + message_id: 7, + date: "2023-11-14T22:13:20.000Z", + emoji: ["👍"], + }); + }); + + test("callback queries carry the presser and the data", () => { + const u = normalizeUpdate({ + update_id: 12, + callback_query: { id: "q", from: { id: 99, first_name: "Cy" }, data: "approve", message: RAW_MESSAGE }, + }); + expect(u).toMatchObject({ type: "callback_query", data: "approve", from: { id: 99 }, message: { message_id: 7 } }); + }); + + test("unknown kinds are named, not dropped", () => { + expect(normalizeUpdate({ update_id: 13, poll: {} })).toEqual({ update_id: 13, type: "other", kind: "poll" }); + }); +}); + +describe("nextOffset", () => { + test("is the highest update id plus one", () => { + expect(nextOffset([normalizeUpdate({ update_id: 5 }), normalizeUpdate({ update_id: 9 })])).toBe(10); + expect(nextOffset([])).toBeNull(); + }); +}); + +describe("validateFileRef", () => { + test("accepts https URLs and file ids", () => { + expect(validateFileRef("https://example.com/a.png")).toBe("https://example.com/a.png"); + expect(validateFileRef("AgACAgQAAxkBAAIB")).toBe("AgACAgQAAxkBAAIB"); + }); + + test("rejects plain http, credentials, and junk", () => { + expect(validateFileRef("http://example.com/a.png")).toBeNull(); + expect(validateFileRef("https://user:pw@example.com/a.png")).toBeNull(); + expect(validateFileRef("file:///etc/passwd")).toBeNull(); + expect(validateFileRef(" ")).toBeNull(); + }); +}); diff --git a/servers/telegram-bot/tsconfig.json b/servers/telegram-bot/tsconfig.json new file mode 100644 index 0000000..0573d0f --- /dev/null +++ b/servers/telegram-bot/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "es2022", + "moduleResolution": "bundler", + "lib": ["es2022"], + "types": ["@cloudflare/workers-types", "bun"], + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/**/*.ts", "test/unit/**/*.ts"], + "exclude": ["test/integration"] +} diff --git a/servers/telegram-bot/vitest.config.ts b/servers/telegram-bot/vitest.config.ts new file mode 100644 index 0000000..0f987ad --- /dev/null +++ b/servers/telegram-bot/vitest.config.ts @@ -0,0 +1,38 @@ +import { defineWorkersConfig } from "@cloudflare/vitest-pool-workers/config"; + +// Integration tier: runs the real Worker inside workerd (via Miniflare) and +// drives /mcp over the stateless streamable-HTTP transport. Telegram itself is +// mocked with `fetchMock` from cloudflare:test, so no live Bot API call is made; +// the pure helpers are covered by `bun test test/unit`. +export default defineWorkersConfig({ + test: { + include: ["test/integration/**/*.spec.ts"], + testTimeout: 30_000, + hookTimeout: 30_000, + // The MCP SDK pulls in ajv (schema validation), whose CJS + // `require('./refs/data.json')` the workerd module loader can't resolve on + // its own. Pre-bundle the SDK graph with esbuild so the JSON is inlined. + deps: { + optimizer: { + ssr: { + enabled: true, + include: ["ajv", "ajv-formats"], + }, + }, + }, + poolOptions: { + workers: { + wrangler: { configPath: "./wrangler.jsonc" }, + miniflare: { + // The pool's bundled workerd supports compat dates up to 2025-04-17; + // pin the test runtime there (production uses wrangler.jsonc's date). + compatibilityDate: "2025-04-17", + // Pin bearer so the tier stays deterministic: production's edison-jwt + // would 401 a static test token (the JWT verify path has real-crypto + // coverage in test/unit/jwt.test.ts). + bindings: { AUTH_MODE: "bearer", AUTH_TOKEN: "test-token" }, + }, + }, + }, + }, +}); diff --git a/servers/telegram-bot/wrangler.jsonc b/servers/telegram-bot/wrangler.jsonc new file mode 100644 index 0000000..1f16028 --- /dev/null +++ b/servers/telegram-bot/wrangler.jsonc @@ -0,0 +1,27 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "telegram-bot", + "main": "src/index.ts", + "compatibility_date": "2026-01-01", + "compatibility_flags": ["nodejs_compat"], + // No Durable Object: the server is stateless on purpose, so a user's bot token + // lives only for the request that carries it (see src/index.ts). + // Company-owned hostname (zone must live in the same Cloudflare account), for + // the same reason as reddit: *.workers.dev bot protection intermittently 403s + // the Railway gateway. + "routes": [{ "pattern": "telegram.sealgate.ai", "custom_domain": true }], + "workers_dev": false, + "vars": { + // Two credentials on every call. The fleet gate is edison-jwt, like the other + // hosted connectors: SealGate mints a per-user JWT, so the endpoint is not an + // open relay and every call is attributable. The caller's own bot token rides + // beside it in X-Telegram-Bot-Token (the user's encrypted template value). + // None of these are secrets. ISSUER must byte-match SEALGATE_MCP_JWT_ISSUER on + // the issuing deploy; AUDIENCE must equal this server's catalog id. + "AUTH_MODE": "edison-jwt", + "EDISON_JWKS_URL": "https://demo-dashboard.sealgate.ai/.well-known/jwks.json", + "EDISON_JWT_ISSUER": "https://demo-dashboard.sealgate.ai", + "EDISON_JWT_AUDIENCE": "telegram-bot" + }, + "observability": { "enabled": true } +} diff --git a/shared/catalog/README.md b/shared/catalog/README.md index 132972d..88e20bc 100644 --- a/shared/catalog/README.md +++ b/shared/catalog/README.md @@ -69,6 +69,9 @@ the build on any invalid entry. `aggregate.py` enforce that `auth: edison-jwt` implies `edison_hosted: true`. An entry flips to it once its server is deployed behind Edison's issuer URL; see `first_party_mcp_integration.md` for the cutover runbook. +- An `edison-jwt` entry may also declare `headers` + `template_fields` for a + credential the user brings (e.g. `telegram-bot`'s `X-Telegram-Bot-Token`). + The JWT still owns `Authorization`, so a header by that name is rejected. ## Per-tool ACL defaults (`tools_configurations`) diff --git a/shared/catalog/aggregate.py b/shared/catalog/aggregate.py index 2322528..e9c92bc 100644 --- a/shared/catalog/aggregate.py +++ b/shared/catalog/aggregate.py @@ -118,6 +118,42 @@ def _headers_bad(entry: dict[str, Any]) -> bool: return any(not isinstance(v, str) for v in headers.values()) +_PLACEHOLDER_RE = re.compile(r"\{([A-Za-z_][A-Za-z0-9_]*)\}") + + +def _undeclared_placeholders(entry: dict[str, Any]) -> bool: + """A header ``{PLACEHOLDER}`` with no matching ``template_fields.env`` key can + never be filled at install, so the mount would forward the literal + placeholder as the credential.""" + headers = entry.get("headers") + if not isinstance(headers, dict): + return False + tf = entry.get("template_fields") + env = tf.get("env") if isinstance(tf, dict) else None + declared = set(env) if isinstance(env, dict) else set() + return any( + name not in declared + for value in headers.values() + if isinstance(value, str) + for name in _PLACEHOLDER_RE.findall(value) + ) + + +def _jwt_user_headers_bad(entry: dict[str, Any]) -> bool: + """An edison-jwt entry may also carry a user credential header (e.g. a + bring-your-own bot token), but never ``Authorization``: the gateway-minted + JWT owns it and would be overwritten. Headers and their template_fields + travel together, since a placeholder with no field can never resolve.""" + if entry.get("auth") != "edison-jwt": + return False + headers = entry.get("headers") + if headers is None: + return "template_fields" in entry + if not isinstance(headers, dict) or "template_fields" not in entry: + return True + return any(str(name).lower() == "authorization" for name in headers) + + def _template_fields_bad(entry: dict[str, Any]) -> bool: """schema: when present, an object whose optional `env` maps names to objects carrying a required string `description` (+ optional `example`).""" @@ -328,6 +364,15 @@ def _field_problems(entry: dict[str, Any], server_dir: Path) -> list[tuple[bool, entry["auth"] == "edison-jwt" and not entry.get("edison_hosted"), "auth 'edison-jwt' requires 'edison_hosted': true", ), + ( + _undeclared_placeholders(entry), + "every '{PLACEHOLDER}' in 'headers' must be declared in 'template_fields.env'", + ), + ( + _jwt_user_headers_bad(entry), + "auth 'edison-jwt' user headers need matching 'template_fields' and must " + "not set 'Authorization' (the gateway-minted JWT owns it)", + ), ( hosted is not None and not isinstance(hosted, bool), "'edison_hosted' must be a boolean", diff --git a/shared/catalog/schema.json b/shared/catalog/schema.json index 6d053d9..245164a 100644 --- a/shared/catalog/schema.json +++ b/shared/catalog/schema.json @@ -87,18 +87,18 @@ "oauth", "edison-jwt" ], - "description": "edison-watch mount auth mode. 'token' resolves {PLACEHOLDER}s from template_fields.env; 'oauth' is 401-discovered; 'none' is open; 'edison-jwt' means Edison mints a per-user JWT and injects it (first-party servers only; requires edison_hosted, no user token). The fleet server's bearer mode maps onto 'token'." + "description": "edison-watch mount auth mode. 'token' resolves {PLACEHOLDER}s from template_fields.env; 'oauth' is 401-discovered; 'none' is open; 'edison-jwt' means Edison mints a per-user JWT and injects it as Authorization (first-party servers only; requires edison_hosted). An edison-jwt entry may add its own user credential header via headers + template_fields (e.g. a bring-your-own bot token), never Authorization. The fleet server's bearer mode maps onto 'token'." }, "headers": { "type": "object", - "description": "Required when auth == 'token'. Values may contain {PLACEHOLDER} tokens resolved from template_fields.env.", + "description": "Required when auth == 'token'; optional for 'edison-jwt' (a user credential beside the JWT, never Authorization). Values may contain {PLACEHOLDER} tokens resolved from template_fields.env.", "additionalProperties": { "type": "string" } }, "template_fields": { "type": "object", - "description": "Required when auth == 'token'. Install-time fields the user supplies.", + "description": "Required when auth == 'token', and when an 'edison-jwt' entry declares headers. Install-time fields the user supplies.", "properties": { "env": { "type": "object", @@ -242,9 +242,24 @@ "required": [ "edison_hosted" ], + "dependentRequired": { + "headers": [ + "template_fields" + ], + "template_fields": [ + "headers" + ] + }, "properties": { "edison_hosted": { "const": true + }, + "headers": { + "propertyNames": { + "not": { + "pattern": "^[Aa][Uu][Tt][Hh][Oo][Rr][Ii][Zz][Aa][Tt][Ii][Oo][Nn]$" + } + } } } } diff --git a/tests/test_fleet_catalog.py b/tests/test_fleet_catalog.py index 63a27a0..51eb2ee 100644 --- a/tests/test_fleet_catalog.py +++ b/tests/test_fleet_catalog.py @@ -243,3 +243,86 @@ def test_schema_and_validator_allowed_keys_in_lockstep(): schema = json.loads(_SCHEMA_PATH.read_text()) schema_keys = set(schema["properties"]) assert schema_keys == set(_AGG.ALLOWED_KEYS) + + +_JWT_ENTRY: dict[str, Any] = dict(_BASE_ENTRY, auth="edison-jwt", edison_hosted=True) +_BOT_FIELDS = {"env": {"BOT_TOKEN": {"description": "bot token"}}} + + +def test_edison_jwt_without_user_headers_ok(): + assert _AGG._jwt_user_headers_bad(dict(_JWT_ENTRY)) is False + + +def test_edison_jwt_with_user_header_and_fields_ok(): + entry = dict( + _JWT_ENTRY, headers={"X-Bot-Token": "{BOT_TOKEN}"}, template_fields=_BOT_FIELDS + ) + assert _AGG._jwt_user_headers_bad(entry) is False + + +def test_edison_jwt_header_without_fields_rejected(): + entry = dict(_JWT_ENTRY, headers={"X-Bot-Token": "{BOT_TOKEN}"}) + assert _AGG._jwt_user_headers_bad(entry) is True + + +def test_edison_jwt_cannot_set_authorization_in_any_case(): + for name in ("authorization", "Authorization", "AUTHORIZATION"): + entry = dict( + _JWT_ENTRY, + headers={name: "Bearer {BOT_TOKEN}"}, + template_fields=_BOT_FIELDS, + ) + assert _AGG._jwt_user_headers_bad(entry) is True, name + + +def test_edison_jwt_fields_without_headers_rejected(): + assert ( + _AGG._jwt_user_headers_bad(dict(_JWT_ENTRY, template_fields=_BOT_FIELDS)) + is True + ) + + +def test_edison_jwt_non_dict_headers_rejected(): + entry = dict(_JWT_ENTRY, headers=["X-Bot-Token"], template_fields=_BOT_FIELDS) + assert _AGG._jwt_user_headers_bad(entry) is True + + +def test_undeclared_header_placeholder_rejected(): + entry = dict( + _JWT_ENTRY, headers={"X-Bot-Token": "{OTHER}"}, template_fields=_BOT_FIELDS + ) + assert _AGG._undeclared_placeholders(entry) is True + ok = dict( + _JWT_ENTRY, headers={"X-Bot-Token": "{BOT_TOKEN}"}, template_fields=_BOT_FIELDS + ) + assert _AGG._undeclared_placeholders(ok) is False + + +def test_schema_mirrors_edison_jwt_user_header_rule(): + schema = json.loads(_SCHEMA_PATH.read_text()) + validator = jsonschema.validators.validator_for(schema)(schema) + base = dict(_JWT_ENTRY, tools_configurations={"t": dict(_VALID_TOOL_CFG)}) + good = dict( + base, headers={"X-Bot-Token": "{BOT_TOKEN}"}, template_fields=_BOT_FIELDS + ) + assert list(validator.iter_errors(good)) == [] + for bad in ( + dict(base, headers={"X-Bot-Token": "{BOT_TOKEN}"}), + dict(base, template_fields=_BOT_FIELDS), + dict( + base, + headers={"Authorization": "Bearer {BOT_TOKEN}"}, + template_fields=_BOT_FIELDS, + ), + ): + assert list(validator.iter_errors(bad)) != [], bad + + +def test_user_header_rule_ignores_token_mode(): + entry = dict( + _BASE_ENTRY, + auth="token", + headers={"Authorization": "Bearer {X}"}, + template_fields=_BOT_FIELDS, + ) + assert _AGG._jwt_user_headers_bad(entry) is False