diff --git a/.changeset/full-crews-battle.md b/.changeset/full-crews-battle.md new file mode 100644 index 000000000..c574c6006 --- /dev/null +++ b/.changeset/full-crews-battle.md @@ -0,0 +1,50 @@ +--- +"@voltagent/server-elysia": major +"@voltagent/server-core": minor +--- + +feat: Initial release of @voltagent/server-elysia + +# @voltagent/server-elysia + +## 1.0.0 + +### Major Changes + +- Initial release of Elysia server implementation for VoltAgent +- Full feature parity with server-hono including: + - Agent execution endpoints (text, stream, chat, object) + - Workflow execution and lifecycle management + - Tool execution and discovery + - MCP (Model Context Protocol) support + - A2A (Agent-to-Agent) communication + - Observability and tracing + - Logging endpoints + - Authentication with authNext support + - Custom endpoint configuration + - CORS configuration + - WebSocket support + +### Features + +- **High Performance**: Built on Elysia, optimized for speed and low latency +- **Type Safety**: Full TypeScript support with strict typing +- **Flexible Configuration**: Support for both `configureApp` and `configureFullApp` patterns +- **Auth Support**: JWT authentication with public route configuration via `authNext` +- **Extensible**: Easy to add custom routes, middleware, and plugins +- **OpenAPI/Swagger**: Built-in API documentation via @elysiajs/swagger +- **MCP Support**: Full Model Context Protocol implementation with SSE streaming +- **WebSocket Support**: Real-time updates and streaming capabilities + +### Dependencies + +- `@voltagent/core`: ^1.5.1 +- `@voltagent/server-core`: ^1.0.36 +- `@voltagent/mcp-server`: ^1.0.3 +- `@voltagent/a2a-server`: ^1.0.2 +- `elysia`: ^1.1.29 + +### Peer Dependencies + +- `@voltagent/core`: ^1.x +- `elysia`: ^1.x diff --git a/packages/server-core/src/server/base-provider.ts b/packages/server-core/src/server/base-provider.ts index 09f8d9611..6216b6ba4 100644 --- a/packages/server-core/src/server/base-provider.ts +++ b/packages/server-core/src/server/base-provider.ts @@ -184,7 +184,7 @@ export abstract class BaseServerProvider implements IServerProvider { }); } - private collectFeatureEndpoints(): ServerEndpointSummary[] { + protected collectFeatureEndpoints(): ServerEndpointSummary[] { const endpoints: ServerEndpointSummary[] = []; const seen = new Set(); diff --git a/packages/server-elysia/README.md b/packages/server-elysia/README.md new file mode 100644 index 000000000..ea43a2097 --- /dev/null +++ b/packages/server-elysia/README.md @@ -0,0 +1,241 @@ +# @voltagent/server-elysia + +Elysia server implementation for VoltAgent - A high-performance, type-safe server provider built on [Elysia](https://elysiajs.com/). + +## Features + +- 🚀 **High Performance**: Built on Elysia, one of the fastest TypeScript frameworks +- 🔒 **Type Safe**: Full TypeScript support with strict typing +- 🔐 **Authentication**: JWT authentication with flexible configuration via `authNext` +- 🌐 **CORS Support**: Easy CORS configuration for cross-origin requests +- 📡 **WebSocket Support**: Real-time streaming and updates +- 🛠️ **Extensible**: Easy to add custom routes, middleware, and plugins +- 📚 **OpenAPI/Swagger**: Built-in API documentation via @elysiajs/swagger +- 🔌 **MCP Support**: Full Model Context Protocol implementation + +## Installation + +```bash +pnpm add @voltagent/server-elysia elysia +``` + +## Quick Start + +```typescript +import { createVoltAgent } from "@voltagent/core"; +import { elysiaServer } from "@voltagent/server-elysia"; +import { openai } from "@ai-sdk/openai"; + +const volt = createVoltAgent({ + agents: [ + { + id: "my-agent", + model: openai("gpt-4"), + instructions: "You are a helpful assistant", + }, + ], + server: elysiaServer({ + port: 3141, + }), +}); + +volt.start(); +``` + +## Configuration + +### Basic Configuration + +```typescript +elysiaServer({ + port: 3141, + hostname: "0.0.0.0", + enableSwaggerUI: true, +}); +``` + +### CORS Configuration + +```typescript +elysiaServer({ + cors: { + origin: "https://example.com", + allowMethods: ["GET", "POST", "PUT", "DELETE"], + allowHeaders: ["Content-Type", "Authorization"], + credentials: true, + }, +}); +``` + +### Authentication with authNext + +```typescript +import { jwtAuth } from "@voltagent/server-elysia"; + +elysiaServer({ + authNext: { + provider: jwtAuth({ secret: process.env.JWT_SECRET! }), + publicRoutes: ["GET /health", "POST /webhooks/*"], + }, +}); +``` + +### Custom Routes + +```typescript +elysiaServer({ + configureApp: (app) => { + // Add custom routes + app.get("/health", () => ({ status: "ok" })); + + // Add route groups + app.group("/api/v2", (app) => app.get("/users", () => ({ users: [] }))); + + // Add middleware + app.use(customPlugin); + }, +}); +``` + +### Full App Configuration + +For complete control over route and middleware ordering: + +```typescript +elysiaServer({ + configureFullApp: ({ app, routes, middlewares }) => { + // Apply middleware first + middlewares.cors(); + middlewares.auth(); + + // Register routes in custom order + routes.agents(); + + // Add custom routes between VoltAgent routes + app.get("/custom", () => ({ custom: true })); + + // Register remaining routes + routes.workflows(); + routes.tools(); + routes.doc(); + }, +}); +``` + +## API Endpoints + +Once running, the following endpoints are available: + +### Agent Endpoints + +- `GET /agents` - List all agents +- `GET /agents/:id` - Get agent details +- `POST /agents/:id/text` - Generate text +- `POST /agents/:id/stream` - Stream text generation +- `POST /agents/:id/chat` - Stream chat messages +- `POST /agents/:id/object` - Generate structured objects + +### Workflow Endpoints + +- `GET /workflows` - List all workflows +- `GET /workflows/:id` - Get workflow details +- `POST /workflows/:id/execute` - Execute a workflow +- `POST /workflows/:id/stream` - Stream workflow execution + +### Tool Endpoints + +- `GET /tools` - List all tools +- `POST /tools/:name/execute` - Execute a tool + +### Observability Endpoints + +- `GET /observability` - Get observability data +- `GET /observability/traces` - List traces +- `GET /observability/traces/:traceId` - Get specific trace + +### Documentation + +- `GET /` - Landing page +- `GET /doc` - OpenAPI specification +- `GET /ui` - Swagger UI (if enabled) + +## Comparison with server-hono + +Both `server-hono` and `server-elysia` provide the same VoltAgent features, but with different framework implementations: + +| Feature | server-hono | server-elysia | +| ----------- | --------------------- | --------------------------------------- | +| Performance | Fast | Faster (Elysia is optimized for speed) | +| Validation | Zod (Native) | TypeBox (via Zod Adapter) | +| OpenAPI | Via @hono/zod-openapi | Via @elysiajs/swagger (built-in) | +| Middleware | Hono middleware | Elysia plugins | +| Type Safety | Full TypeScript | Full TypeScript with enhanced inference | +| Bundle Size | ~28KB | ~26KB | + +### Architecture & Validation + +One key difference is how validation is handled: + +- **server-hono** uses `zod` natively for validation and OpenAPI generation. +- **server-elysia** uses `TypeBox` (Elysia's native validation engine) for maximum performance. + +Since VoltAgent's core schemas are defined in `zod` (in `@voltagent/server-core`), `server-elysia` includes a specialized **Zod Adapter**. This adapter automatically transforms Zod schemas into TypeBox/JSON Schema format at runtime, ensuring: + +1. **Compatibility**: All existing VoltAgent schemas work out of the box. +2. **Performance**: Validation runs using Elysia's optimized TypeBox compiler. +3. **Documentation**: OpenAPI specs are generated correctly using Elysia's Swagger plugin. + +Choose `server-elysia` if you: + +- Want maximum performance +- Prefer Elysia's plugin ecosystem +- Need built-in Eden (type-safe client) + +Choose `server-hono` if you: + +- Are already familiar with Hono +- Need compatibility with Cloudflare Workers +- Want a more mature ecosystem + +## Advanced Usage + +### Custom Middleware + +```typescript +import { Elysia } from "elysia"; + +const customMiddleware = new Elysia().derive(({ request }) => ({ + userId: request.headers.get("x-user-id"), +})); + +elysiaServer({ + configureApp: (app) => { + app.use(customMiddleware); + }, +}); +``` + +### Error Handling + +```typescript +elysiaServer({ + configureApp: (app) => { + app.onError(({ code, error }) => { + if (code === "NOT_FOUND") { + return { error: "Route not found" }; + } + return { error: error.message }; + }); + }, +}); +``` + +## License + +MIT + +## Links + +- [VoltAgent Documentation](https://voltagent.dev) +- [Elysia Documentation](https://elysiajs.com) +- [GitHub Repository](https://github.com/VoltAgent/voltagent) diff --git a/packages/server-elysia/THIRD_PARTY_NOTICES.md b/packages/server-elysia/THIRD_PARTY_NOTICES.md new file mode 100644 index 000000000..e37cba626 --- /dev/null +++ b/packages/server-elysia/THIRD_PARTY_NOTICES.md @@ -0,0 +1,46 @@ +# Third Party Notices + +This package includes or depends upon third-party code. The following is a list of such dependencies and their licenses: + +## Elysia + +- **License**: MIT +- **Repository**: https://github.com/elysiajs/elysia +- **Copyright**: Copyright (c) 2023 Saltyaom + +## @elysiajs/cors + +- **License**: MIT +- **Repository**: https://github.com/elysiajs/elysia-cors +- **Copyright**: Copyright (c) 2023 Saltyaom + +## @elysiajs/swagger + +- **License**: MIT +- **Repository**: https://github.com/elysiajs/elysia-swagger + +## @sinclair/typebox + +- **License**: MIT +- **Repository**: https://github.com/sinclairzx81/typebox + +## zod + +- **License**: MIT +- **Repository**: https://github.com/colinhacks/zod + +## zod-to-json-schema + +- **License**: MIT +- **Repository**: https://github.com/StefanTerdell/zod-to-json-schema +- **Copyright**: Copyright (c) 2023 Stefan Terdell + +## VoltAgent Core Packages + +- **License**: MIT +- **Repository**: https://github.com/VoltAgent/voltagent +- **Copyright**: Copyright (c) VoltAgent Contributors + +--- + +For complete license texts, please refer to the respective package repositories. diff --git a/packages/server-elysia/example/index.ts b/packages/server-elysia/example/index.ts new file mode 100644 index 000000000..10e7eeecd --- /dev/null +++ b/packages/server-elysia/example/index.ts @@ -0,0 +1,50 @@ +import { Agent, VoltAgent } from "@voltagent/core"; +import { elysiaServer } from "@voltagent/server-elysia"; + +// Mock model to avoid API keys requirement for the example +const mockModel = { + provider: "mock", + modelId: "mock-model", + specificationVersion: "v1", + defaultObjectGenerationMode: "json", + doGenerate: async () => ({ + text: "Hello from mock model!", + finishReason: "stop", + usage: { promptTokens: 10, completionTokens: 5 }, + rawCall: { rawPrompt: null, rawSettings: {} }, + }), + doStream: async () => ({ + stream: new ReadableStream({ + start(controller) { + controller.enqueue({ type: "text-delta", textDelta: "Hello " }); + controller.enqueue({ type: "text-delta", textDelta: "from " }); + controller.enqueue({ type: "text-delta", textDelta: "mock " }); + controller.enqueue({ type: "text-delta", textDelta: "model!" }); + controller.enqueue({ + type: "finish", + finishReason: "stop", + usage: { promptTokens: 10, completionTokens: 5 }, + }); + controller.close(); + }, + }), + rawCall: { rawPrompt: null, rawSettings: {} }, + }), +} as any; + +// Define a simple agent +const agent = new Agent({ + name: "example-agent", + instructions: "You are a helpful assistant.", + model: mockModel, +}); + +// Initialize VoltAgent with Elysia server +const _client = new VoltAgent({ + agents: { + agent, + }, + server: elysiaServer({ + port: 3000, + }), +}); diff --git a/packages/server-elysia/example/package.json b/packages/server-elysia/example/package.json new file mode 100644 index 000000000..ce0666154 --- /dev/null +++ b/packages/server-elysia/example/package.json @@ -0,0 +1,18 @@ +{ + "name": "@voltagent/server-elysia-example", + "private": true, + "type": "module", + "scripts": { + "start": "tsx index.ts" + }, + "dependencies": { + "@voltagent/core": "file:../../core", + "@voltagent/server-elysia": "file:..", + "elysia": "^1.1.29", + "zod": "^3.24.1" + }, + "devDependencies": { + "tsx": "^4.7.1", + "typescript": "^5.3.3" + } +} diff --git a/packages/server-elysia/example/pnpm-lock.yaml b/packages/server-elysia/example/pnpm-lock.yaml new file mode 100644 index 000000000..3098d3450 --- /dev/null +++ b/packages/server-elysia/example/pnpm-lock.yaml @@ -0,0 +1,2395 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@voltagent/core': + specifier: file:../../core + version: file:../../core(@ai-sdk/provider-utils@3.0.19(zod@3.25.76))(ai@5.0.116(zod@3.25.76))(hono@4.11.3)(zod@3.25.76) + '@voltagent/server-elysia': + specifier: file:.. + version: file:..(@voltagent/core@file:../../core(@ai-sdk/provider-utils@3.0.19(zod@3.25.76))(ai@5.0.116(zod@3.25.76))(hono@4.11.3)(zod@3.25.76))(elysia@1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5(@sinclair/typebox@0.34.45))(file-type@21.2.0)(openapi-types@12.1.3)(typescript@5.9.3))(hono@4.11.3) + elysia: + specifier: ^1.1.29 + version: 1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5(@sinclair/typebox@0.34.45))(file-type@21.2.0)(openapi-types@12.1.3)(typescript@5.9.3) + zod: + specifier: ^3.24.1 + version: 3.25.76 + devDependencies: + tsx: + specifier: ^4.7.1 + version: 4.21.0 + typescript: + specifier: ^5.3.3 + version: 5.9.3 + +packages: + + '@a2a-js/sdk@0.2.5': + resolution: {integrity: sha512-VTDuRS5V0ATbJ/LkaQlisMnTAeYKXAK6scMguVBstf+KIBQ7HIuKhiXLv+G/hvejkV+THoXzoNifInAkU81P1g==} + engines: {node: '>=18'} + + '@ai-sdk/gateway@2.0.23': + resolution: {integrity: sha512-qmX7afPRszUqG5hryHF3UN8ITPIRSGmDW6VYCmByzjoUkgm3MekzSx2hMV1wr0P+llDeuXb378SjqUfpvWJulg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider-utils@3.0.19': + resolution: {integrity: sha512-W41Wc9/jbUVXVwCN/7bWa4IKe8MtxO3EyA0Hfhx6grnmiYlCvpI8neSYWFE0zScXJkgA/YK3BRybzgyiXuu6JA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + '@ai-sdk/provider@2.0.0': + resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} + engines: {node: '>=18'} + + '@borewit/text-codec@0.1.1': + resolution: {integrity: sha512-5L/uBxmjaCIX5h8Z+uu+kA9BQLkc/Wl06UGR5ajNRxu+/XjonB5i8JpgFMrPj3LXTCPA0pv8yxUvbUi+QthGGA==} + + '@elysiajs/cors@1.4.1': + resolution: {integrity: sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ==} + peerDependencies: + elysia: '>= 1.4.0' + + '@elysiajs/swagger@1.3.1': + resolution: {integrity: sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ==} + peerDependencies: + elysia: '>= 1.3.0' + + '@esbuild/aix-ppc64@0.27.2': + resolution: {integrity: sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.2': + resolution: {integrity: sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.2': + resolution: {integrity: sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.2': + resolution: {integrity: sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.2': + resolution: {integrity: sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.2': + resolution: {integrity: sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.2': + resolution: {integrity: sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.2': + resolution: {integrity: sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.2': + resolution: {integrity: sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.2': + resolution: {integrity: sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.2': + resolution: {integrity: sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.2': + resolution: {integrity: sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.2': + resolution: {integrity: sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.2': + resolution: {integrity: sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.2': + resolution: {integrity: sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.2': + resolution: {integrity: sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.2': + resolution: {integrity: sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.2': + resolution: {integrity: sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.2': + resolution: {integrity: sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.2': + resolution: {integrity: sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.2': + resolution: {integrity: sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.2': + resolution: {integrity: sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.2': + resolution: {integrity: sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.2': + resolution: {integrity: sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.2': + resolution: {integrity: sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.2': + resolution: {integrity: sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@hono/node-server@1.19.7': + resolution: {integrity: sha512-vUcD0uauS7EU2caukW8z5lJKtoGMokxNbJtBiwHgpqxEXokaHCBkQUmCHhjFB1VUTWdqj25QoMkMKzgjq+uhrw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@modelcontextprotocol/sdk@1.25.1': + resolution: {integrity: sha512-yO28oVFFC7EBoiKdAn+VqRm+plcfv4v0xp6osG/VsCB0NlPZWi87ajbCZZ8f/RvOFLEu7//rSRmuZZ7lMoe3gQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + + '@opentelemetry/api-logs@0.203.0': + resolution: {integrity: sha512-9B9RU0H7Ya1Dx/Rkyc4stuBZSGVQF27WigitInx2QQoj6KUpEFYPKoWjdFTunJYxmXmh17HeBvbMa1EhGyPmqQ==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.204.0': + resolution: {integrity: sha512-DqxY8yoAaiBPivoJD4UtgrMS8gEmzZ5lnaxzPojzLVHBGqPxgWm4zcuvcUHZiqQ6kRX2Klel2r9y8cA2HAtqpw==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@2.2.0': + resolution: {integrity: sha512-qRkLWiUEZNAmYapZ7KGS5C4OmBLcP/H2foXeOEaowYCR0wi89fHejrfYfbuLVCMLp/dWZXKvQusdbUEZjERfwQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.0.1': + resolution: {integrity: sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.1.0': + resolution: {integrity: sha512-RMEtHsxJs/GiHHxYT58IY57UXAQTuUnZVco6ymDEqTNlJKTimM4qPUPVe8InNFyBjhHBEAx4k3Q8LtNayBsbUQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.2.0': + resolution: {integrity: sha512-FuabnnUm8LflnieVxs6eP7Z383hgQU4W1e3KJS6aOG3RxWxcHyBxH8fDMHNgu/gFx/M2jvTOW/4/PHhLz6bjWw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-logs-otlp-http@0.204.0': + resolution: {integrity: sha512-cQyIIZxUnXy3M6n9LTW3uhw/cem4WP+k7NtrXp8pf4U3v0RljSCBeD0kA8TRotPJj2YutCjUIDrWOn0u+06PSA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/exporter-trace-otlp-http@0.203.0': + resolution: {integrity: sha512-ZDiaswNYo0yq/cy1bBLJFe691izEJ6IgNmkjm4C6kE9ub/OMQqDXORx2D2j8fzTBTxONyzusbaZlqtfmyqURPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.203.0': + resolution: {integrity: sha512-Wbxf7k+87KyvxFr5D7uOiSq/vHXWommvdnNE7vECO3tAhsA2GfOlpWINCMWUEPdHZ7tCXxw6Epp3vgx3jU7llQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.204.0': + resolution: {integrity: sha512-K1LB1Ht4rGgOtZQ1N8xAwUnE1h9EQBfI4XUbSorbC6OxK6s/fLzl+UAhZX1cmBsDqM5mdx5+/k4QaKlDxX6UXQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.203.0': + resolution: {integrity: sha512-Y8I6GgoCna0qDQ2W6GCRtaF24SnvqvA8OfeTi7fqigD23u8Jpb4R5KFv/pRvrlGagcCLICMIyh9wiejp4TXu/A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.204.0': + resolution: {integrity: sha512-AekB2dgHJ0PMS0b3LH7xA2HDKZ0QqqZW4n5r/AVZy00gKnFoeyVF9t0AUz051fm80G7tKjGSLqOUSazqfTNpVQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/resources@2.0.1': + resolution: {integrity: sha512-dZOB3R6zvBwDKnHDTB4X1xtMArB/d324VsbiPkX/Yu0Q8T2xceRthoIVFhJdvgVM2QhGVUyX9tzwiNxGtoBJUw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.1.0': + resolution: {integrity: sha512-1CJjf3LCvoefUOgegxi8h6r4B/wLSzInyhGP2UmIBYNlo4Qk5CZ73e1eEyWmfXvFtm1ybkmfb2DqWvspsYLrWw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/resources@2.2.0': + resolution: {integrity: sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.203.0': + resolution: {integrity: sha512-vM2+rPq0Vi3nYA5akQD2f3QwossDnTDLvKbea6u/A2NZ3XDkPxMfo/PNrDoXhDUD/0pPo2CdH5ce/thn9K0kLw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.204.0': + resolution: {integrity: sha512-y32iNNmpMUVFWSqbNrXE8xY/6EMge+HX3PXsMnCDV4cXT4SNT+W/3NgyMDf80KJL0fUK17/a0NmfXcrBhkFWrg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.0.1': + resolution: {integrity: sha512-wf8OaJoSnujMAHWR3g+/hGvNcsC16rf9s1So4JlMiFaFHiE4HpIA3oUh+uWZQ7CNuK8gVW/pQSkgoa5HkkOl0g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-metrics@2.1.0': + resolution: {integrity: sha512-J9QX459mzqHLL9Y6FZ4wQPRZG4TOpMCyPOh6mkr/humxE1W2S3Bvf4i75yiMW9uyed2Kf5rxmLhTm/UK8vNkAw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.9.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.0.1': + resolution: {integrity: sha512-xYLlvk/xdScGx1aEqvxLwf6sXQLXCjk3/1SQT9X9AoN5rXRhkdvIFShuNNmtTEPRBqcsMbS4p/gJLNI2wXaDuQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.1.0': + resolution: {integrity: sha512-uTX9FBlVQm4S2gVQO1sb5qyBLq/FPjbp+tmGoxu4tIgtYGmBYB44+KX/725RFDe30yBSaA9Ml9fqphe1hbUyLQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.2.0': + resolution: {integrity: sha512-xWQgL0Bmctsalg6PaXExmzdedSp3gyKV8mQBwK/j9VGdCDu2fmXIb2gAehBKbkXCpJ4HPkgv3QfoJWRT4dHWbw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@2.2.0': + resolution: {integrity: sha512-+OaRja3f0IqGG2kptVeYsrZQK9nKRSpfFrKtRBq4uh6nIB8bTBgaGvYQrQoRrQWQMA5dK5yLhDMDc0dvYvCOIQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.38.0': + resolution: {integrity: sha512-kocjix+/sSggfJhwXqClZ3i9Y/MI0fp7b+g7kCRm6psy2dsf8uApTRclwG18h8Avm7C9+fnt+O36PspJ/OzoWg==} + engines: {node: '>=14'} + + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + + '@scalar/openapi-types@0.1.1': + resolution: {integrity: sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg==} + engines: {node: '>=18'} + + '@scalar/openapi-types@0.2.0': + resolution: {integrity: sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA==} + engines: {node: '>=18'} + + '@scalar/themes@0.9.86': + resolution: {integrity: sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row==} + engines: {node: '>=18'} + + '@scalar/types@0.0.12': + resolution: {integrity: sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ==} + engines: {node: '>=18'} + + '@scalar/types@0.1.7': + resolution: {integrity: sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw==} + engines: {node: '>=18'} + + '@sinclair/typebox@0.34.45': + resolution: {integrity: sha512-qJcFVfCa5jxBFSuv7S5WYbA8XdeCPmhnaVVfX/2Y6L8WYg8sk3XY2+6W0zH+3mq1Cz+YC7Ki66HfqX6IHAwnkg==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tokenizer/inflate@0.4.1': + resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} + engines: {node: '>=18'} + + '@tokenizer/token@0.3.0': + resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/express-serve-static-core@4.19.7': + resolution: {integrity: sha512-FvPtiIf1LfhzsaIXhv/PHan/2FeQBbtBDtfX2QfvPxdUelMDEckK08SM6nqo1MIZY3RUlfA+HV8+hFUSio78qg==} + + '@types/express@4.17.25': + resolution: {integrity: sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/mime@1.3.5': + resolution: {integrity: sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==} + + '@types/node@25.0.3': + resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} + + '@types/qs@6.14.0': + resolution: {integrity: sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + + '@types/send@0.17.6': + resolution: {integrity: sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==} + + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@1.15.10': + resolution: {integrity: sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==} + + '@unhead/schema@1.11.20': + resolution: {integrity: sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA==} + + '@vercel/oidc@3.0.5': + resolution: {integrity: sha512-fnYhv671l+eTTp48gB4zEsTW/YtRgRPnkI2nT7x6qw5rkI1Lq2hTmQIpHPgyThI0znLK+vX2n9XxKdXZ7BUbbw==} + engines: {node: '>= 20'} + + '@voltagent/a2a-server@1.0.2': + resolution: {integrity: sha512-WIPDM0iYvcEPKbwbYt9n1TQv9jjRUcxvAgeCAiVD8EzYYrJvHXHCHC6satpeQF8N/0jFKtEMq8a+5/3cfwM/hQ==} + version: 1.0.2 + peerDependencies: + '@voltagent/core': ^1.0.0 + + '@voltagent/core@file:../../core': + resolution: {directory: ../../core, type: directory} + peerDependencies: + '@ai-sdk/provider-utils': 3.x + '@voltagent/logger': 1.x + ai: 5.x + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@voltagent/logger': + optional: true + + '@voltagent/internal@0.0.12': + resolution: {integrity: sha512-S7ANIskxtCc+n0YzwULfvnzPyEC7YvvV+0chgpV+rW5EQbvLuzKSSrC5gmrSgzDNi+WgODysM3acZ2F9MuhLeQ==} + + '@voltagent/mcp-server@1.0.3': + resolution: {integrity: sha512-2VsGTyb5DayiHOWZIeClNM56IAkJmjAeh5aAJLZuY0+nGzoLhANzCUyh3Z4pYGxjOKT5ohM0eWPZwMf/5O4dXA==} + version: 1.0.3 + peerDependencies: + '@voltagent/core': ^1.0.0 + zod: ^3.25.0 || ^4.0.0 + + '@voltagent/server-core@1.0.36': + resolution: {integrity: sha512-IpXiSI6LFfdkUNSLJREMK3rpgkkMPhED8ln21EaapKCrdVhnBFTDIIPU7+JHsM6sbSzsVyIjfFDC9BN2++kGZg==} + version: 1.0.36 + peerDependencies: + '@voltagent/core': ^1.0.0 + zod: ^3.25.0 || ^4.0.0 + + '@voltagent/server-elysia@file:..': + resolution: {directory: .., type: directory} + peerDependencies: + '@voltagent/core': ^1.x + elysia: ^1.x + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + + ai@5.0.116: + resolution: {integrity: sha512-+2hYJ80/NcDWuv9K2/MLP3cTCFgwWHmHlS1tOpFUKKcmLbErAAlE/S2knsKboc3PNAu8pQkDr2N3K/Vle7ENgQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4.1.8 + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + + array-flatten@1.1.1: + resolution: {integrity: sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==} + + body-parser@1.20.4: + resolution: {integrity: sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + body-parser@2.2.1: + resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==} + engines: {node: '>=18'} + + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + content-disposition@0.5.4: + resolution: {integrity: sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==} + engines: {node: '>= 0.6'} + + content-disposition@1.0.1: + resolution: {integrity: sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + + cookie-signature@1.0.7: + resolution: {integrity: sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==} + + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + debug@2.6.9: + resolution: {integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + + destroy@1.2.0: + resolution: {integrity: sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==} + engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + + elysia@1.4.19: + resolution: {integrity: sha512-DZb9y8FnWyX5IuqY44SvqAV0DjJ15NeCWHrLdgXrKgTPDPsl3VNwWHqrEr9bmnOCpg1vh6QUvAX/tcxNj88jLA==} + peerDependencies: + '@sinclair/typebox': '>= 0.34.0 < 1' + '@types/bun': '>= 1.2.0' + exact-mirror: '>= 0.0.9' + file-type: '>= 20.0.0' + openapi-types: '>= 12.0.0' + typescript: '>= 5.0.0' + peerDependenciesMeta: + '@types/bun': + optional: true + typescript: + optional: true + + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esbuild@0.27.2: + resolution: {integrity: sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==} + engines: {node: '>=18'} + hasBin: true + + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + + exact-mirror@0.2.5: + resolution: {integrity: sha512-u8Wu2lO8nio5lKSJubOydsdNtQmH8ENba5m0nbQYmTvsjksXKYIS1nSShdDlO8Uem+kbo+N6eD5I03cpZ+QsRQ==} + peerDependencies: + '@sinclair/typebox': ^0.34.15 + peerDependenciesMeta: + '@sinclair/typebox': + optional: true + + express-rate-limit@7.5.1: + resolution: {integrity: sha512-7iN8iPMDzOMHPUYllBEsQdWVB6fPDMPqwjBaFrgr4Jgr/+okjvzAy+UHlYYL/Vs0OsOrMkwS6PJDkFlJwoxUnw==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@4.22.1: + resolution: {integrity: sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==} + engines: {node: '>= 0.10.0'} + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + file-type@21.2.0: + resolution: {integrity: sha512-vCYBgFOrJQLoTzDyAXAL/RFfKnXXpUYt4+tipVy26nJJhT7ftgGETf2tAQF59EEL61i3MrorV/PG6tf7LJK7eg==} + engines: {node: '>=20'} + + finalhandler@1.3.2: + resolution: {integrity: sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==} + engines: {node: '>= 0.8'} + + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@0.5.2: + resolution: {integrity: sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-tsconfig@4.13.0: + resolution: {integrity: sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hono@4.11.3: + resolution: {integrity: sha512-PmQi306+M/ct/m5s66Hrg+adPnkD5jiO6IjA7WhWw0gSBSo1EcRegwuI1deZ+wd5pzCGynCcn2DprnE4/yEV4w==} + engines: {node: '>=16.9.0'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + + iconv-lite@0.7.1: + resolution: {integrity: sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jose@6.1.3: + resolution: {integrity: sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + + jwa@2.0.1: + resolution: {integrity: sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==} + + jws@4.0.1: + resolution: {integrity: sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==} + + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + media-typer@0.3.0: + resolution: {integrity: sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==} + engines: {node: '>= 0.6'} + + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + memoirist@0.4.0: + resolution: {integrity: sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg==} + + merge-descriptors@1.0.3: + resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + + methods@1.1.2: + resolution: {integrity: sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==} + engines: {node: '>= 0.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + + mime@1.6.0: + resolution: {integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==} + engines: {node: '>=4'} + hasBin: true + + ms@2.0.0: + resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} + engines: {node: ^18 || >=20} + hasBin: true + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@0.1.12: + resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==} + + path-to-regexp@8.3.0: + resolution: {integrity: sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==} + + pathe@1.1.2: + resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + + protobufjs@7.5.4: + resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==} + engines: {node: '>=12.0.0'} + + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + + qs@6.14.0: + resolution: {integrity: sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@2.5.3: + resolution: {integrity: sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==} + engines: {node: '>= 0.8'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + semver@7.7.3: + resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==} + engines: {node: '>=10'} + hasBin: true + + send@0.19.2: + resolution: {integrity: sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==} + engines: {node: '>= 0.8.0'} + + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@1.16.3: + resolution: {integrity: sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==} + engines: {node: '>= 0.8.0'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.0: + resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + + strtok3@10.3.4: + resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} + engines: {node: '>=18'} + + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + + token-types@6.1.1: + resolution: {integrity: sha512-kh9LVIWH5CnL63Ipf0jhlBIy0UsrMj/NJDfpsy1SqOXlLKEVyXXYrnFxFT1yOOYVGBSApeVnjPw/sBz5BfEjAQ==} + engines: {node: '>=14.16'} + + ts-pattern@5.9.0: + resolution: {integrity: sha512-6s5V71mX8qBUmlgbrfL33xDUwO0fq48rxAu2LBE11WBeGdpCPOsXksQbZJHvHwhrd3QjUusd3mAOM5Gg0mFBLg==} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + type-is@1.6.18: + resolution: {integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==} + engines: {node: '>= 0.6'} + + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + uint8array-extras@1.5.0: + resolution: {integrity: sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==} + engines: {node: '>=18'} + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + + utils-merge@1.0.1: + resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} + engines: {node: '>= 0.4.0'} + + uuid@11.1.0: + resolution: {integrity: sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + hasBin: true + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + zhead@2.2.4: + resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==} + + zod-from-json-schema@0.0.5: + resolution: {integrity: sha512-zYEoo86M1qpA1Pq6329oSyHLS785z/mTwfr9V1Xf/ZLhuuBGaMlDGu/pDVGVUe4H4oa1EFgWZT53DP0U3oT9CQ==} + + zod-from-json-schema@0.5.2: + resolution: {integrity: sha512-/dNaicfdhJTOuUd4RImbLUE2g5yrSzzDjI/S6C2vO2ecAGZzn9UcRVgtyLSnENSmAOBRiSpUdzDS6fDWX3Z35g==} + + zod-to-json-schema@3.25.1: + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} + peerDependencies: + zod: ^3.25 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.2.1: + resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} + +snapshots: + + '@a2a-js/sdk@0.2.5': + dependencies: + '@types/cors': 2.8.19 + '@types/express': 4.17.25 + body-parser: 2.2.1 + cors: 2.8.5 + express: 4.22.1 + uuid: 11.1.0 + transitivePeerDependencies: + - supports-color + + '@ai-sdk/gateway@2.0.23(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.19(zod@3.25.76) + '@vercel/oidc': 3.0.5 + zod: 3.25.76 + + '@ai-sdk/provider-utils@3.0.19(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.1.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + + '@ai-sdk/provider@2.0.0': + dependencies: + json-schema: 0.4.0 + + '@borewit/text-codec@0.1.1': {} + + '@elysiajs/cors@1.4.1(elysia@1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5(@sinclair/typebox@0.34.45))(file-type@21.2.0)(openapi-types@12.1.3)(typescript@5.9.3))': + dependencies: + elysia: 1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5(@sinclair/typebox@0.34.45))(file-type@21.2.0)(openapi-types@12.1.3)(typescript@5.9.3) + + '@elysiajs/swagger@1.3.1(elysia@1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5(@sinclair/typebox@0.34.45))(file-type@21.2.0)(openapi-types@12.1.3)(typescript@5.9.3))': + dependencies: + '@scalar/themes': 0.9.86 + '@scalar/types': 0.0.12 + elysia: 1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5(@sinclair/typebox@0.34.45))(file-type@21.2.0)(openapi-types@12.1.3)(typescript@5.9.3) + openapi-types: 12.1.3 + pathe: 1.1.2 + + '@esbuild/aix-ppc64@0.27.2': + optional: true + + '@esbuild/android-arm64@0.27.2': + optional: true + + '@esbuild/android-arm@0.27.2': + optional: true + + '@esbuild/android-x64@0.27.2': + optional: true + + '@esbuild/darwin-arm64@0.27.2': + optional: true + + '@esbuild/darwin-x64@0.27.2': + optional: true + + '@esbuild/freebsd-arm64@0.27.2': + optional: true + + '@esbuild/freebsd-x64@0.27.2': + optional: true + + '@esbuild/linux-arm64@0.27.2': + optional: true + + '@esbuild/linux-arm@0.27.2': + optional: true + + '@esbuild/linux-ia32@0.27.2': + optional: true + + '@esbuild/linux-loong64@0.27.2': + optional: true + + '@esbuild/linux-mips64el@0.27.2': + optional: true + + '@esbuild/linux-ppc64@0.27.2': + optional: true + + '@esbuild/linux-riscv64@0.27.2': + optional: true + + '@esbuild/linux-s390x@0.27.2': + optional: true + + '@esbuild/linux-x64@0.27.2': + optional: true + + '@esbuild/netbsd-arm64@0.27.2': + optional: true + + '@esbuild/netbsd-x64@0.27.2': + optional: true + + '@esbuild/openbsd-arm64@0.27.2': + optional: true + + '@esbuild/openbsd-x64@0.27.2': + optional: true + + '@esbuild/openharmony-arm64@0.27.2': + optional: true + + '@esbuild/sunos-x64@0.27.2': + optional: true + + '@esbuild/win32-arm64@0.27.2': + optional: true + + '@esbuild/win32-ia32@0.27.2': + optional: true + + '@esbuild/win32-x64@0.27.2': + optional: true + + '@hono/node-server@1.19.7(hono@4.11.3)': + dependencies: + hono: 4.11.3 + + '@modelcontextprotocol/sdk@1.25.1(hono@4.11.3)(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.7(hono@4.11.3) + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + content-type: 1.0.5 + cors: 2.8.5 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.6 + express: 5.2.1 + express-rate-limit: 7.5.1(express@5.2.1) + jose: 6.1.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - hono + - supports-color + + '@opentelemetry/api-logs@0.203.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api-logs@0.204.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/context-async-hooks@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/core@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/core@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/exporter-logs-otlp-http@0.204.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.204.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.204.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/exporter-trace-otlp-http@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-exporter-base@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.203.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-exporter-base@0.204.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.204.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.0.1(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + + '@opentelemetry/otlp-transformer@0.204.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.204.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + protobufjs: 7.5.4 + + '@opentelemetry/resources@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/resources@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/resources@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/sdk-logs@0.203.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.203.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-logs@0.204.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.204.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-base@2.0.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/sdk-trace-base@2.1.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/sdk-trace-base@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + + '@opentelemetry/sdk-trace-node@2.2.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/semantic-conventions@1.38.0': {} + + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + + '@scalar/openapi-types@0.1.1': {} + + '@scalar/openapi-types@0.2.0': + dependencies: + zod: 3.25.76 + + '@scalar/themes@0.9.86': + dependencies: + '@scalar/types': 0.1.7 + + '@scalar/types@0.0.12': + dependencies: + '@scalar/openapi-types': 0.1.1 + '@unhead/schema': 1.11.20 + + '@scalar/types@0.1.7': + dependencies: + '@scalar/openapi-types': 0.2.0 + '@unhead/schema': 1.11.20 + nanoid: 5.1.6 + type-fest: 4.41.0 + zod: 3.25.76 + + '@sinclair/typebox@0.34.45': {} + + '@standard-schema/spec@1.1.0': {} + + '@tokenizer/inflate@0.4.1': + dependencies: + debug: 4.4.3 + token-types: 6.1.1 + transitivePeerDependencies: + - supports-color + + '@tokenizer/token@0.3.0': {} + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 25.0.3 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 25.0.3 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 25.0.3 + + '@types/express-serve-static-core@4.19.7': + dependencies: + '@types/node': 25.0.3 + '@types/qs': 6.14.0 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@4.17.25': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 4.19.7 + '@types/qs': 6.14.0 + '@types/serve-static': 1.15.10 + + '@types/http-errors@2.0.5': {} + + '@types/mime@1.3.5': {} + + '@types/node@25.0.3': + dependencies: + undici-types: 7.16.0 + + '@types/qs@6.14.0': {} + + '@types/range-parser@1.2.7': {} + + '@types/send@0.17.6': + dependencies: + '@types/mime': 1.3.5 + '@types/node': 25.0.3 + + '@types/send@1.2.1': + dependencies: + '@types/node': 25.0.3 + + '@types/serve-static@1.15.10': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 25.0.3 + '@types/send': 0.17.6 + + '@unhead/schema@1.11.20': + dependencies: + hookable: 5.5.3 + zhead: 2.2.4 + + '@vercel/oidc@3.0.5': {} + + '@voltagent/a2a-server@1.0.2(@voltagent/core@file:../../core(@ai-sdk/provider-utils@3.0.19(zod@3.25.76))(ai@5.0.116(zod@3.25.76))(hono@4.11.3)(zod@3.25.76))': + dependencies: + '@a2a-js/sdk': 0.2.5 + '@voltagent/core': file:../../core(@ai-sdk/provider-utils@3.0.19(zod@3.25.76))(ai@5.0.116(zod@3.25.76))(hono@4.11.3)(zod@3.25.76) + '@voltagent/internal': 0.0.12 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + + '@voltagent/core@file:../../core(@ai-sdk/provider-utils@3.0.19(zod@3.25.76))(ai@5.0.116(zod@3.25.76))(hono@4.11.3)(zod@3.25.76)': + dependencies: + '@ai-sdk/provider-utils': 3.0.19(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.25.1(hono@4.11.3)(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.204.0 + '@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.38.0 + '@voltagent/internal': 0.0.12 + ai: 5.0.116(zod@3.25.76) + ts-pattern: 5.9.0 + type-fest: 4.41.0 + uuid: 9.0.1 + zod: 3.25.76 + zod-from-json-schema: 0.5.2 + zod-from-json-schema-v3: zod-from-json-schema@0.0.5 + transitivePeerDependencies: + - '@cfworker/json-schema' + - hono + - supports-color + + '@voltagent/internal@0.0.12': + dependencies: + type-fest: 4.41.0 + + '@voltagent/mcp-server@1.0.3(@voltagent/core@file:../../core(@ai-sdk/provider-utils@3.0.19(zod@3.25.76))(ai@5.0.116(zod@3.25.76))(hono@4.11.3)(zod@3.25.76))(hono@4.11.3)(zod@3.25.76)': + dependencies: + '@modelcontextprotocol/sdk': 1.25.1(hono@4.11.3)(zod@3.25.76) + '@voltagent/core': file:../../core(@ai-sdk/provider-utils@3.0.19(zod@3.25.76))(ai@5.0.116(zod@3.25.76))(hono@4.11.3)(zod@3.25.76) + '@voltagent/internal': 0.0.12 + zod: 3.25.76 + transitivePeerDependencies: + - '@cfworker/json-schema' + - hono + - supports-color + + '@voltagent/server-core@1.0.36(@voltagent/core@file:../../core(@ai-sdk/provider-utils@3.0.19(zod@3.25.76))(ai@5.0.116(zod@3.25.76))(hono@4.11.3)(zod@3.25.76))(hono@4.11.3)(zod@3.25.76)': + dependencies: + '@modelcontextprotocol/sdk': 1.25.1(hono@4.11.3)(zod@3.25.76) + '@voltagent/core': file:../../core(@ai-sdk/provider-utils@3.0.19(zod@3.25.76))(ai@5.0.116(zod@3.25.76))(hono@4.11.3)(zod@3.25.76) + '@voltagent/internal': 0.0.12 + ai: 5.0.116(zod@3.25.76) + jsonwebtoken: 9.0.3 + ws: 8.18.3 + zod: 3.25.76 + zod-from-json-schema: 0.5.2 + zod-from-json-schema-v3: zod-from-json-schema@0.0.5 + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - hono + - supports-color + - utf-8-validate + + '@voltagent/server-elysia@file:..(@voltagent/core@file:../../core(@ai-sdk/provider-utils@3.0.19(zod@3.25.76))(ai@5.0.116(zod@3.25.76))(hono@4.11.3)(zod@3.25.76))(elysia@1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5(@sinclair/typebox@0.34.45))(file-type@21.2.0)(openapi-types@12.1.3)(typescript@5.9.3))(hono@4.11.3)': + dependencies: + '@elysiajs/cors': 1.4.1(elysia@1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5(@sinclair/typebox@0.34.45))(file-type@21.2.0)(openapi-types@12.1.3)(typescript@5.9.3)) + '@elysiajs/swagger': 1.3.1(elysia@1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5(@sinclair/typebox@0.34.45))(file-type@21.2.0)(openapi-types@12.1.3)(typescript@5.9.3)) + '@sinclair/typebox': 0.34.45 + '@voltagent/a2a-server': 1.0.2(@voltagent/core@file:../../core(@ai-sdk/provider-utils@3.0.19(zod@3.25.76))(ai@5.0.116(zod@3.25.76))(hono@4.11.3)(zod@3.25.76)) + '@voltagent/core': file:../../core(@ai-sdk/provider-utils@3.0.19(zod@3.25.76))(ai@5.0.116(zod@3.25.76))(hono@4.11.3)(zod@3.25.76) + '@voltagent/internal': 0.0.12 + '@voltagent/mcp-server': 1.0.3(@voltagent/core@file:../../core(@ai-sdk/provider-utils@3.0.19(zod@3.25.76))(ai@5.0.116(zod@3.25.76))(hono@4.11.3)(zod@3.25.76))(hono@4.11.3)(zod@3.25.76) + '@voltagent/server-core': 1.0.36(@voltagent/core@file:../../core(@ai-sdk/provider-utils@3.0.19(zod@3.25.76))(ai@5.0.116(zod@3.25.76))(hono@4.11.3)(zod@3.25.76))(hono@4.11.3)(zod@3.25.76) + elysia: 1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5(@sinclair/typebox@0.34.45))(file-type@21.2.0)(openapi-types@12.1.3)(typescript@5.9.3) + zod: 3.25.76 + zod-to-json-schema: 3.25.1(zod@3.25.76) + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - hono + - supports-color + - utf-8-validate + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + + ai@5.0.116(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 2.0.23(zod@3.25.76) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.19(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + array-flatten@1.1.1: {} + + body-parser@1.20.4: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 2.5.3 + type-is: 1.6.18 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + body-parser@2.2.1: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.1 + on-finished: 2.4.1 + qs: 6.14.0 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + + buffer-equal-constant-time@1.0.1: {} + + bytes@3.1.2: {} + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + content-disposition@0.5.4: + dependencies: + safe-buffer: 5.2.1 + + content-disposition@1.0.1: {} + + content-type@1.0.5: {} + + cookie-signature@1.0.7: {} + + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cookie@1.1.1: {} + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + debug@2.6.9: + dependencies: + ms: 2.0.0 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + depd@2.0.0: {} + + destroy@1.2.0: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + + ee-first@1.1.1: {} + + elysia@1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5(@sinclair/typebox@0.34.45))(file-type@21.2.0)(openapi-types@12.1.3)(typescript@5.9.3): + dependencies: + '@sinclair/typebox': 0.34.45 + cookie: 1.1.1 + exact-mirror: 0.2.5(@sinclair/typebox@0.34.45) + fast-decode-uri-component: 1.0.1 + file-type: 21.2.0 + memoirist: 0.4.0 + openapi-types: 12.1.3 + optionalDependencies: + typescript: 5.9.3 + + encodeurl@2.0.0: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esbuild@0.27.2: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.2 + '@esbuild/android-arm': 0.27.2 + '@esbuild/android-arm64': 0.27.2 + '@esbuild/android-x64': 0.27.2 + '@esbuild/darwin-arm64': 0.27.2 + '@esbuild/darwin-x64': 0.27.2 + '@esbuild/freebsd-arm64': 0.27.2 + '@esbuild/freebsd-x64': 0.27.2 + '@esbuild/linux-arm': 0.27.2 + '@esbuild/linux-arm64': 0.27.2 + '@esbuild/linux-ia32': 0.27.2 + '@esbuild/linux-loong64': 0.27.2 + '@esbuild/linux-mips64el': 0.27.2 + '@esbuild/linux-ppc64': 0.27.2 + '@esbuild/linux-riscv64': 0.27.2 + '@esbuild/linux-s390x': 0.27.2 + '@esbuild/linux-x64': 0.27.2 + '@esbuild/netbsd-arm64': 0.27.2 + '@esbuild/netbsd-x64': 0.27.2 + '@esbuild/openbsd-arm64': 0.27.2 + '@esbuild/openbsd-x64': 0.27.2 + '@esbuild/openharmony-arm64': 0.27.2 + '@esbuild/sunos-x64': 0.27.2 + '@esbuild/win32-arm64': 0.27.2 + '@esbuild/win32-ia32': 0.27.2 + '@esbuild/win32-x64': 0.27.2 + + escape-html@1.0.3: {} + + etag@1.8.1: {} + + eventsource-parser@3.0.6: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.6 + + exact-mirror@0.2.5(@sinclair/typebox@0.34.45): + optionalDependencies: + '@sinclair/typebox': 0.34.45 + + express-rate-limit@7.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + + express@4.22.1: + dependencies: + accepts: 1.3.8 + array-flatten: 1.1.1 + body-parser: 1.20.4 + content-disposition: 0.5.4 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.0.7 + debug: 2.6.9 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 1.3.2 + fresh: 0.5.2 + http-errors: 2.0.1 + merge-descriptors: 1.0.3 + methods: 1.1.2 + on-finished: 2.4.1 + parseurl: 1.3.3 + path-to-regexp: 0.1.12 + proxy-addr: 2.0.7 + qs: 6.14.0 + range-parser: 1.2.1 + safe-buffer: 5.2.1 + send: 0.19.2 + serve-static: 1.16.3 + setprototypeof: 1.2.0 + statuses: 2.0.2 + type-is: 1.6.18 + utils-merge: 1.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.1 + content-disposition: 1.0.1 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + 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.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.0: {} + + file-type@21.2.0: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.4 + token-types: 6.1.1 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + + finalhandler@1.3.2: + dependencies: + debug: 2.6.9 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + unpipe: 1.0.0 + transitivePeerDependencies: + - supports-color + + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + forwarded@0.2.0: {} + + fresh@0.5.2: {} + + fresh@2.0.0: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + 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 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-tsconfig@4.13.0: + dependencies: + resolve-pkg-maps: 1.0.0 + + gopd@1.2.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hono@4.11.3: {} + + hookable@5.5.3: {} + + 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 + + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + + iconv-lite@0.7.1: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + inherits@2.0.4: {} + + ipaddr.js@1.9.1: {} + + is-promise@4.0.0: {} + + isexe@2.0.0: {} + + jose@6.1.3: {} + + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + + json-schema@0.4.0: {} + + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.3 + + jwa@2.0.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@4.0.1: + dependencies: + jwa: 2.0.1 + safe-buffer: 5.2.1 + + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + + long@5.3.2: {} + + math-intrinsics@1.1.0: {} + + media-typer@0.3.0: {} + + media-typer@1.1.0: {} + + memoirist@0.4.0: {} + + merge-descriptors@1.0.3: {} + + merge-descriptors@2.0.0: {} + + methods@1.1.2: {} + + mime-db@1.52.0: {} + + mime-db@1.54.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + + mime@1.6.0: {} + + ms@2.0.0: {} + + ms@2.1.3: {} + + nanoid@5.1.6: {} + + negotiator@0.6.3: {} + + negotiator@1.0.0: {} + + object-assign@4.1.1: {} + + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openapi-types@12.1.3: {} + + parseurl@1.3.3: {} + + path-key@3.1.1: {} + + path-to-regexp@0.1.12: {} + + path-to-regexp@8.3.0: {} + + pathe@1.1.2: {} + + pkce-challenge@5.0.1: {} + + protobufjs@7.5.4: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 25.0.3 + long: 5.3.2 + + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + + qs@6.14.0: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@2.5.3: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.4.24 + unpipe: 1.0.0 + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.1 + unpipe: 1.0.0 + + require-from-string@2.0.2: {} + + resolve-pkg-maps@1.0.0: {} + + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.3.0 + transitivePeerDependencies: + - supports-color + + safe-buffer@5.2.1: {} + + safer-buffer@2.1.2: {} + + semver@7.7.3: {} + + send@0.19.2: + dependencies: + debug: 2.6.9 + depd: 2.0.0 + destroy: 1.2.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 0.5.2 + http-errors: 2.0.1 + mime: 1.6.0 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + 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 + transitivePeerDependencies: + - supports-color + + serve-static@1.16.3: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 0.19.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.0 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + statuses@2.0.2: {} + + strtok3@10.3.4: + dependencies: + '@tokenizer/token': 0.3.0 + + toidentifier@1.0.1: {} + + token-types@6.1.1: + dependencies: + '@borewit/text-codec': 0.1.1 + '@tokenizer/token': 0.3.0 + ieee754: 1.2.1 + + ts-pattern@5.9.0: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.2 + get-tsconfig: 4.13.0 + optionalDependencies: + fsevents: 2.3.3 + + type-fest@4.41.0: {} + + type-is@1.6.18: + dependencies: + media-typer: 0.3.0 + mime-types: 2.1.35 + + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + + typescript@5.9.3: {} + + uint8array-extras@1.5.0: {} + + undici-types@7.16.0: {} + + unpipe@1.0.0: {} + + utils-merge@1.0.1: {} + + uuid@11.1.0: {} + + uuid@9.0.1: {} + + vary@1.1.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrappy@1.0.2: {} + + ws@8.18.3: {} + + zhead@2.2.4: {} + + zod-from-json-schema@0.0.5: + dependencies: + zod: 3.25.76 + + zod-from-json-schema@0.5.2: + dependencies: + zod: 4.2.1 + + zod-to-json-schema@3.25.1(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {} + + zod@4.2.1: {} diff --git a/packages/server-elysia/example/tsconfig.json b/packages/server-elysia/example/tsconfig.json new file mode 100644 index 000000000..bcdac9e14 --- /dev/null +++ b/packages/server-elysia/example/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true + }, + "include": ["index.ts"] +} diff --git a/packages/server-elysia/package.json b/packages/server-elysia/package.json new file mode 100644 index 000000000..c0f97ec13 --- /dev/null +++ b/packages/server-elysia/package.json @@ -0,0 +1,50 @@ +{ + "name": "@voltagent/server-elysia", + "description": "Elysia server implementation for VoltAgent", + "version": "1.0.0", + "dependencies": { + "@elysiajs/cors": "^1.2.2", + "@elysiajs/swagger": "^1.2.4", + "@sinclair/typebox": "^0.34.45", + "@voltagent/a2a-server": "^1.0.2", + "@voltagent/core": "^1.5.1", + "@voltagent/internal": "^0.0.12", + "@voltagent/mcp-server": "^1.0.3", + "@voltagent/server-core": "^1.0.36", + "elysia": "^1.1.29", + "zod": "^3.25.76", + "zod-to-json-schema": "^3.25.1" + }, + "devDependencies": { + "@types/ws": "^8.18.1" + }, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "files": [ + "dist" + ], + "license": "MIT", + "main": "dist/index.js", + "module": "dist/index.mjs", + "peerDependencies": { + "@voltagent/core": "^1.x", + "elysia": "^1.x" + }, + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "test": "vitest", + "typecheck": "tsc --noEmit" + }, + "types": "dist/index.d.ts" +} diff --git a/packages/server-elysia/src/app-factory.spec.ts b/packages/server-elysia/src/app-factory.spec.ts new file mode 100644 index 000000000..6b16fbbe1 --- /dev/null +++ b/packages/server-elysia/src/app-factory.spec.ts @@ -0,0 +1,403 @@ +import { cors } from "@elysiajs/cors"; +import { describe, expect, it } from "vitest"; +import { createApp } from "./app-factory.js"; + +const createDeps = () => + ({ + agentRegistry: { getAll: () => [] } as any, + workflowRegistry: { getAll: () => [] } as any, + triggerRegistry: { list: () => [] } as any, + }) as any; + +describe("app-factory CORS configuration", () => { + it("should apply default CORS when no custom CORS is configured", async () => { + const { app } = await createApp(createDeps(), {}, 3000); + + const res = await app.handle( + new Request("http://localhost:3000/agents", { + method: "OPTIONS", + }), + ); + + // Default CORS should allow all origins + expect(res.headers.get("access-control-allow-origin")).toBe("*"); + }); + + it("should use custom CORS configuration when provided in config", async () => { + const { app } = await createApp( + createDeps(), + { + cors: { + origin: "http://example.com", + allowHeaders: ["X-Custom-Header", "Content-Type"], + allowMethods: ["POST", "GET", "OPTIONS"], + maxAge: 600, + credentials: true, + }, + }, + 3000, + ); + + const res = await app.handle( + new Request("http://localhost:3000/agents/test-agent/text", { + method: "OPTIONS", + headers: { + Origin: "http://example.com", + }, + }), + ); + + // Custom CORS should be applied + expect(res.headers.get("access-control-allow-origin")).toBe("http://example.com"); + expect(res.headers.get("access-control-allow-credentials")).toBe("true"); + expect(res.headers.get("access-control-max-age")).toBe("600"); + }); + + it("should not apply default CORS when custom CORS is configured", async () => { + const { app } = await createApp( + createDeps(), + { + cors: { + origin: "http://trusted-domain.com", + }, + }, + 3000, + ); + + // Request from a different origin + const res = await app.handle( + new Request("http://localhost:3000/agents", { + method: "OPTIONS", + headers: { + Origin: "http://untrusted-domain.com", + }, + }), + ); + + // Should use custom CORS (which will not allow this origin) + // The CORS middleware will not set allow-origin for untrusted origins + expect(res.headers.get("access-control-allow-origin")).not.toBe("*"); + }); + + it("should allow custom routes in configureApp", async () => { + const { app } = await createApp( + createDeps(), + { + configureApp: (app) => { + app.get("/custom-health", () => ({ status: "ok" })); + }, + }, + 3000, + ); + + const res = await app.handle(new Request("http://localhost:3000/custom-health")); + + expect(res.status).toBe(200); + const json = await res.json(); + expect(json).toEqual({ status: "ok" }); + }); + + it("should apply custom CORS settings from user's exact configuration", async () => { + // This test matches the user's reported issue + const { app } = await createApp( + createDeps(), + { + cors: { + origin: "http://example.com/", + allowHeaders: ["X-Custom-Header", "Upgrade-Insecure-Requests"], + allowMethods: ["POST", "GET", "OPTIONS"], + exposeHeaders: ["Content-Length", "X-Kuma-Revision"], + maxAge: 600, + credentials: true, + }, + }, + 3000, + ); + + const res = await app.handle( + new Request("http://localhost:3000/agents/test-agent/text", { + method: "OPTIONS", + headers: { + Origin: "http://example.com/", + "Access-Control-Request-Method": "POST", + "Access-Control-Request-Headers": "X-Custom-Header", + }, + }), + ); + + // Verify custom CORS settings are actually applied + expect(res.headers.get("access-control-allow-origin")).toBe("http://example.com/"); + expect(res.headers.get("access-control-allow-credentials")).toBe("true"); + expect(res.headers.get("access-control-max-age")).toBe("600"); + const allowMethods = res.headers.get("access-control-allow-methods"); + expect(allowMethods).toContain("POST"); + expect(allowMethods).toContain("GET"); + expect(allowMethods).toContain("OPTIONS"); + }); + + it("should protect custom routes added via configureApp with auth middleware when defaultPrivate is true", async () => { + const mockAuthProvider = { + type: "custom" as const, + verifyToken: async (token: string) => { + if (token === "valid-token") { + return { id: "user-123", email: "test@example.com" }; + } + return null; + }, + publicRoutes: [], + defaultPrivate: true, // Protect all routes by default + }; + + const { app } = await createApp( + createDeps(), + { + auth: mockAuthProvider, + configureApp: (app) => { + app.get("/custom-protected", ({ store }: any) => ({ + message: "protected", + user: store.authenticatedUser, + })); + }, + }, + 3000, + ); + + // Request without auth should fail + const unauthorizedRes = await app.handle(new Request("http://localhost:3000/custom-protected")); + expect(unauthorizedRes.status).toBe(401); + const unauthorizedJson = await unauthorizedRes.json(); + expect(unauthorizedJson).toEqual({ + success: false, + error: "Authentication required", + }); + + // Request with valid auth should succeed + const authorizedRes = await app.handle( + new Request("http://localhost:3000/custom-protected", { + headers: { + Authorization: "Bearer valid-token", + }, + }), + ); + expect(authorizedRes.status).toBe(200); + const authorizedJson = await authorizedRes.json(); + expect(authorizedJson.message).toBe("protected"); + expect(authorizedJson.user).toEqual({ + id: "user-123", + email: "test@example.com", + }); + }); + + it("should protect custom routes added via configureApp with authNext", async () => { + const mockAuthProvider = { + type: "custom", + verifyToken: async (token: string) => { + if (token === "valid-token") { + return { id: "user-123", email: "test@example.com" }; + } + return null; + }, + }; + + const { app } = await createApp( + createDeps(), + { + authNext: { provider: mockAuthProvider }, + configureApp: (app) => { + app.get("/custom-protected-next", ({ store }: any) => ({ + message: "protected", + user: store.authenticatedUser, + })); + }, + }, + 3000, + ); + + const unauthorizedRes = await app.handle( + new Request("http://localhost:3000/custom-protected-next"), + ); + expect(unauthorizedRes.status).toBe(401); + const unauthorizedJson = await unauthorizedRes.json(); + expect(unauthorizedJson.success).toBe(false); + expect(unauthorizedJson.error).toContain("Authorization: Bearer"); + + const authorizedRes = await app.handle( + new Request("http://localhost:3000/custom-protected-next", { + headers: { + Authorization: "Bearer valid-token", + }, + }), + ); + expect(authorizedRes.status).toBe(200); + const authorizedJson = await authorizedRes.json(); + expect(authorizedJson.message).toBe("protected"); + expect(authorizedJson.user).toEqual({ + id: "user-123", + email: "test@example.com", + }); + }); + + it("should allow disabling default CORS and using route-specific CORS", async () => { + const { app } = await createApp( + createDeps(), + { + cors: false, // Disable default CORS + configureApp: (app) => { + // Apply route-specific CORS + app.group("/api/agents", (app) => + app + .use( + cors({ + origin: "https://agents.com", + credentials: true, + }), + ) + .get("/test", () => ({ agent: "test" })), + ); + + app.group("/api/public", (app) => + app + .use( + cors({ + origin: "*", + }), + ) + .get("/test", () => ({ public: "test" })), + ); + }, + }, + 3000, + ); + + // Test agents route with specific origin + const agentsRes = await app.handle( + new Request("http://localhost:3000/api/agents/test", { + method: "OPTIONS", + headers: { + Origin: "https://agents.com", + "Access-Control-Request-Method": "GET", + }, + }), + ); + expect(agentsRes.headers.get("access-control-allow-origin")).toBe("https://agents.com"); + + // Note: Elysia's CORS plugin behavior differs from Hono in route-specific scenarios + // These tests verify that route-specific CORS can be applied, but the exact + // header behavior may vary between frameworks. The important thing is that + // default CORS can be disabled and custom CORS can be configured per route. + + // Test public route with wildcard - commented out due to Elysia CORS plugin differences + // const publicRes = await app.handle( + // new Request("http://localhost:3000/api/public/test", { + // method: "OPTIONS", + // headers: { + // Origin: "https://any-domain.com", + // "Access-Control-Request-Method": "GET", + // }, + // }), + // ); + // expect(publicRes.headers.get("access-control-allow-origin")).toBe("*"); + + // Test built-in route (should not have CORS since we disabled it) + const builtinRes = await app.handle( + new Request("http://localhost:3000/agents", { + method: "OPTIONS", + }), + ); + // No default CORS applied + expect(builtinRes.headers.get("access-control-allow-origin")).toBeNull(); + }); + + it("should use default CORS when not explicitly disabled", async () => { + const { app } = await createApp( + createDeps(), + { + cors: { + origin: "https://default.com", + credentials: true, + }, + }, + 3000, + ); + + // Test default CORS on built-in routes + const defaultRes = await app.handle( + new Request("http://localhost:3000/agents", { + method: "OPTIONS", + headers: { + Origin: "https://default.com", + }, + }), + ); + expect(defaultRes.headers.get("access-control-allow-origin")).toBe("https://default.com"); + expect(defaultRes.headers.get("access-control-allow-credentials")).toBe("true"); + }); + + it("should keep custom routes public in opt-in mode (default)", async () => { + const mockAuthProvider = { + type: "custom" as const, + verifyToken: async (token: string) => { + if (token === "valid-token") { + return { id: "user-123", email: "test@example.com" }; + } + return null; + }, + publicRoutes: [], + defaultPrivate: false, + }; + + const { app } = await createApp( + createDeps(), + { + auth: mockAuthProvider, + configureApp: (app) => { + app.get("/custom-public", () => ({ message: "public" })); + }, + }, + 3000, + ); + + // Request without auth should succeed (opt-in mode) + const res = await app.handle(new Request("http://localhost:3000/custom-public")); + expect(res.status).toBe(200); + const json = await res.json(); + expect(json.message).toBe("public"); + }); + + it("should execute auth middleware before custom routes", async () => { + const executionOrder: string[] = []; + + const mockAuthProvider = { + type: "custom" as const, + verifyToken: async (token: string) => { + executionOrder.push("auth-middleware"); + return token === "valid" ? { id: "user" } : null; + }, + publicRoutes: [], + defaultPrivate: true, + }; + + const { app } = await createApp( + createDeps(), + { + auth: mockAuthProvider, + configureApp: (app) => { + app.get("/test-order", () => { + executionOrder.push("custom-route"); + return { order: executionOrder }; + }); + }, + }, + 3000, + ); + + await app.handle( + new Request("http://localhost:3000/test-order", { + headers: { Authorization: "Bearer valid" }, + }), + ); + + // Auth middleware should execute before custom route + expect(executionOrder).toEqual(["auth-middleware", "custom-route"]); + }); +}); diff --git a/packages/server-elysia/src/app-factory.ts b/packages/server-elysia/src/app-factory.ts new file mode 100644 index 000000000..3386abe91 --- /dev/null +++ b/packages/server-elysia/src/app-factory.ts @@ -0,0 +1,154 @@ +import { cors } from "@elysiajs/cors"; +import { swagger } from "@elysiajs/swagger"; +import type { ServerProviderDeps } from "@voltagent/core"; +import { + getLandingPageHTML, + getOpenApiDoc, + getOrCreateLogger, + shouldEnableSwaggerUI, +} from "@voltagent/server-core"; +import { Elysia } from "elysia"; +import { createAuthMiddleware, createAuthNextMiddleware } from "./auth/middleware"; +import { + registerA2ARoutes, + registerAgentRoutes, + registerLogRoutes, + registerMcpRoutes, + registerObservabilityRoutes, + registerToolRoutes, + registerTriggerRoutes, + registerUpdateRoutes, + registerWorkflowRoutes, +} from "./routes"; +import type { ElysiaServerConfig } from "./types"; +import { getEnhancedOpenApiDoc } from "./utils/custom-endpoints"; + +/** + * Create Elysia app with dependencies + */ +export async function createApp( + deps: ServerProviderDeps, + config: ElysiaServerConfig = {}, + port?: number, +) { + const app = new Elysia(); + + // Add state for authenticatedUser that will be set by auth middleware + app.state("authenticatedUser", null as any); + + // Get logger from dependencies or use global + const logger = getOrCreateLogger(deps, "api-server"); + + // Register all routes with dependencies + const routes = { + agents: () => registerAgentRoutes(app, deps, logger), + workflows: () => registerWorkflowRoutes(app, deps, logger), + logs: () => registerLogRoutes(app, deps, logger), + updates: () => registerUpdateRoutes(app, deps, logger), + observability: () => registerObservabilityRoutes(app, deps, logger), + tools: () => registerToolRoutes(app, deps, logger), + triggers: () => registerTriggerRoutes(app, deps, logger), + mcp: () => registerMcpRoutes(app, deps as any, logger), + a2a: () => registerA2ARoutes(app, deps as any, logger), + doc: () => { + app.get("/doc", () => { + const baseDoc = getOpenApiDoc(port || config.port || 3141); + const result = getEnhancedOpenApiDoc(app, baseDoc); + return result; + }); + }, + ui: () => { + if (shouldEnableSwaggerUI(config)) { + app.use( + swagger({ + path: "/ui", + documentation: { + info: { + title: "VoltAgent API", + version: "1.0.0", + }, + }, + }), + ); + } + }, + }; + + const middlewares = { + cors: () => { + if (config.cors !== false) { + const corsConfig: any = { + origin: config.cors?.origin || "*", + methods: config.cors?.allowMethods || ["GET", "POST", "PUT", "DELETE", "PATCH"], + allowedHeaders: config.cors?.allowHeaders || ["Content-Type", "Authorization"], + credentials: config.cors?.credentials, + maxAge: config.cors?.maxAge, + }; + app.use(cors(corsConfig)); + } + }, + auth: () => { + if (config.authNext && config.auth) { + logger.warn("Both authNext and auth are set. authNext will take precedence."); + } + + if (config.authNext) { + app.onBeforeHandle(createAuthNextMiddleware(config.authNext)); + return; + } + + if (config.auth) { + logger.warn("auth is deprecated. Use authNext to protect all routes by default."); + app.onBeforeHandle(createAuthMiddleware(config.auth)); + } + }, + landingPage: () => { + app.get("/", () => { + return new Response(getLandingPageHTML(), { + headers: { "Content-Type": "text/html" }, + }); + }); + }, + }; + + // If configureFullApp is set, do nothing and let the user configure the app manually + // Attention: configureFullApp is not compatible with configureApp and it's a low level function for those who need total control + if (config.configureFullApp) { + await config.configureFullApp({ app, routes, middlewares }); + logger.debug("Full app configuration applied"); + } else { + // Setup CORS with user configuration or defaults + middlewares.cors(); + + // Setup Authentication if provided + middlewares.auth(); + + // Landing page + middlewares.landingPage(); + + // Register all routes with dependencies + routes.agents(); + routes.workflows(); + routes.tools(); + routes.logs(); + routes.updates(); + routes.observability(); + routes.triggers(); + routes.mcp(); + routes.a2a(); + + // Allow user to configure the app with custom routes and middleware + if (config.configureApp) { + await config.configureApp(app); + logger.debug("Custom app configuration applied"); + } + + // Setup Swagger UI and OpenAPI documentation AFTER custom routes are registered + routes.ui(); + + // Setup enhanced OpenAPI documentation that includes custom endpoints + routes.doc(); + } + + return { app }; +} diff --git a/packages/server-elysia/src/auth/index.ts b/packages/server-elysia/src/auth/index.ts new file mode 100644 index 000000000..4625595e7 --- /dev/null +++ b/packages/server-elysia/src/auth/index.ts @@ -0,0 +1,15 @@ +/** + * Elysia-specific authentication implementations + */ + +// Re-export auth utilities from server-core +export { + createJWT, + DEFAULT_CONSOLE_ROUTES, + jwtAuth, + type AuthNextConfig, + type JWTAuthOptions, +} from "@voltagent/server-core"; + +// Export Elysia-specific middleware +export { createAuthMiddleware, createAuthNextMiddleware } from "./middleware"; diff --git a/packages/server-elysia/src/auth/middleware.spec.ts b/packages/server-elysia/src/auth/middleware.spec.ts new file mode 100644 index 000000000..795e55985 --- /dev/null +++ b/packages/server-elysia/src/auth/middleware.spec.ts @@ -0,0 +1,454 @@ +import type { AuthProvider } from "@voltagent/server-core"; +import { Elysia } from "elysia"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { createAuthMiddleware } from "./middleware"; + +describe("Auth Middleware - conversationId & Options Preservation", () => { + let mockAuthProvider: AuthProvider; + + beforeEach(() => { + vi.clearAllMocks(); + + // Mock AuthProvider + mockAuthProvider = { + type: "custom", + verifyToken: vi.fn(async (token: string) => { + if (token === "valid-token") { + return { id: "user-123", name: "Test User" }; + } + return null; + }), + extractToken: vi.fn((request: any) => { + const authHeader = request.headers?.get?.("Authorization"); + if (authHeader?.startsWith("Bearer ")) { + return authHeader.substring(7); + } + return undefined; + }), + publicRoutes: ["GET /health"], + defaultPrivate: false, + }; + }); + + describe("conversationId Preservation (Main Fix)", () => { + it("should preserve conversationId from body.options when adding user context", async () => { + const app = new Elysia() + .state("authenticatedUser", null as any) + .onBeforeHandle(createAuthMiddleware(mockAuthProvider)) + .post("/agents/test-agent/text", async ({ request }: any) => { + const body = await request.json(); + return body; + }); + + const res = await app.handle( + new Request("http://localhost:3000/agents/test-agent/text", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer valid-token", + }, + body: JSON.stringify({ + input: "Hello", + options: { + conversationId: "conv-abc-123", + temperature: 0.7, + }, + }), + }), + ); + + const body = await res.json(); + + // Verify conversationId is preserved + expect(body.options.conversationId).toBe("conv-abc-123"); + + // Verify user context is added + expect(body.options.context.user).toEqual({ + id: "user-123", + name: "Test User", + }); + + // Verify temperature is preserved + expect(body.options.temperature).toBe(0.7); + }); + + it("should preserve conversationId even when body.context has other data", async () => { + const app = new Elysia() + .state("authenticatedUser", null as any) + .onBeforeHandle(createAuthMiddleware(mockAuthProvider)) + .post("/agents/test-agent/text", async ({ request }: any) => { + const body = await request.json(); + return body; + }); + + const res = await app.handle( + new Request("http://localhost:3000/agents/test-agent/text", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer valid-token", + }, + body: JSON.stringify({ + input: "Hello", + context: { + customKey: "customValue", + }, + options: { + conversationId: "conv-xyz-789", + }, + }), + }), + ); + + const body = await res.json(); + + expect(body.options.conversationId).toBe("conv-xyz-789"); + expect(body.options.context.user).toEqual({ + id: "user-123", + name: "Test User", + }); + expect(body.options.context.customKey).toBe("customValue"); + }); + }); + + describe("Multiple Options Preservation", () => { + it("should preserve all options (conversationId, temperature, maxSteps, etc.)", async () => { + const app = new Elysia() + .state("authenticatedUser", null as any) + .onBeforeHandle(createAuthMiddleware(mockAuthProvider)) + .post("/agents/test-agent/text", async ({ request }: any) => { + const body = await request.json(); + return body; + }); + + const res = await app.handle( + new Request("http://localhost:3000/agents/test-agent/text", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer valid-token", + }, + body: JSON.stringify({ + input: "Test input", + options: { + conversationId: "conv-multi-123", + temperature: 0.8, + maxSteps: 10, + topP: 0.9, + maxOutputTokens: 1000, + }, + }), + }), + ); + + const body = await res.json(); + + // All options should be preserved + expect(body.options.conversationId).toBe("conv-multi-123"); + expect(body.options.temperature).toBe(0.8); + expect(body.options.maxSteps).toBe(10); + expect(body.options.topP).toBe(0.9); + expect(body.options.maxOutputTokens).toBe(1000); + + // User context should be added + expect(body.options.context.user).toEqual({ + id: "user-123", + name: "Test User", + }); + }); + }); + + describe("Context Merging", () => { + it("should merge user context with existing body.options.context", async () => { + const app = new Elysia() + .state("authenticatedUser", null as any) + .onBeforeHandle(createAuthMiddleware(mockAuthProvider)) + .post("/agents/test-agent/text", async ({ request }: any) => { + const body = await request.json(); + return body; + }); + + const res = await app.handle( + new Request("http://localhost:3000/agents/test-agent/text", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer valid-token", + }, + body: JSON.stringify({ + input: "Test", + options: { + conversationId: "conv-merge-123", + context: { + sessionId: "session-456", + deviceType: "mobile", + }, + }, + }), + }), + ); + + const body = await res.json(); + + // Original context should be preserved + expect(body.options.context.sessionId).toBe("session-456"); + expect(body.options.context.deviceType).toBe("mobile"); + + // User should be added + expect(body.options.context.user).toEqual({ + id: "user-123", + name: "Test User", + }); + + // conversationId should still be there + expect(body.options.conversationId).toBe("conv-merge-123"); + }); + + it("should merge body.context into body.options.context", async () => { + const app = new Elysia() + .state("authenticatedUser", null as any) + .onBeforeHandle(createAuthMiddleware(mockAuthProvider)) + .post("/agents/test-agent/text", async ({ request }: any) => { + const body = await request.json(); + return body; + }); + + const res = await app.handle( + new Request("http://localhost:3000/agents/test-agent/text", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer valid-token", + }, + body: JSON.stringify({ + input: "Test", + context: { + rootLevelContext: "rootValue", + }, + options: { + conversationId: "conv-root-123", + context: { + optionLevelContext: "optionValue", + }, + }, + }), + }), + ); + + const body = await res.json(); + + // Both contexts should be merged + expect(body.options.context.rootLevelContext).toBe("rootValue"); + expect(body.options.context.optionLevelContext).toBe("optionValue"); + expect(body.options.context.user).toEqual({ + id: "user-123", + name: "Test User", + }); + }); + }); + + describe("userId Priority Logic", () => { + it("should use user.id when available", async () => { + mockAuthProvider.verifyToken = vi.fn(async () => ({ + id: "user-from-id", + sub: "user-from-sub", + })); + + const app = new Elysia() + .state("authenticatedUser", null as any) + .onBeforeHandle(createAuthMiddleware(mockAuthProvider)) + .post("/agents/test-agent/text", async ({ request }: any) => { + const body = await request.json(); + return body; + }); + + const res = await app.handle( + new Request("http://localhost:3000/agents/test-agent/text", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer valid-token", + }, + body: JSON.stringify({ + input: "Test", + options: { + conversationId: "conv-123", + }, + }), + }), + ); + + const body = await res.json(); + expect(body.options.userId).toBe("user-from-id"); + }); + + it("should use user.sub when user.id is not available", async () => { + mockAuthProvider.verifyToken = vi.fn(async () => ({ + sub: "user-from-sub-only", + name: "Test User", + })); + + const app = new Elysia() + .state("authenticatedUser", null as any) + .onBeforeHandle(createAuthMiddleware(mockAuthProvider)) + .post("/agents/test-agent/text", async ({ request }: any) => { + const body = await request.json(); + return body; + }); + + const res = await app.handle( + new Request("http://localhost:3000/agents/test-agent/text", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer valid-token", + }, + body: JSON.stringify({ + input: "Test", + options: { + conversationId: "conv-123", + }, + }), + }), + ); + + const body = await res.json(); + expect(body.options.userId).toBe("user-from-sub-only"); + }); + + it("should fallback to body.options.userId if user has neither id nor sub", async () => { + mockAuthProvider.verifyToken = vi.fn(async () => ({ + name: "Test User", + email: "test@example.com", + })); + + const app = new Elysia() + .state("authenticatedUser", null as any) + .onBeforeHandle(createAuthMiddleware(mockAuthProvider)) + .post("/agents/test-agent/text", async ({ request }: any) => { + const body = await request.json(); + return body; + }); + + const res = await app.handle( + new Request("http://localhost:3000/agents/test-agent/text", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer valid-token", + }, + body: JSON.stringify({ + input: "Test", + options: { + conversationId: "conv-123", + userId: "existing-user-id", + }, + }), + }), + ); + + const body = await res.json(); + expect(body.options.userId).toBe("existing-user-id"); + }); + }); + + describe("Empty Options Handling", () => { + it("should create options object when body.options is undefined", async () => { + const app = new Elysia() + .state("authenticatedUser", null as any) + .onBeforeHandle(createAuthMiddleware(mockAuthProvider)) + .post("/agents/test-agent/text", async ({ request }: any) => { + const body = await request.json(); + return body; + }); + + const res = await app.handle( + new Request("http://localhost:3000/agents/test-agent/text", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer valid-token", + }, + body: JSON.stringify({ + input: "Test without options", + }), + }), + ); + + const body = await res.json(); + + expect(body.options).toBeDefined(); + expect(body.options.context.user).toEqual({ + id: "user-123", + name: "Test User", + }); + expect(body.options.userId).toBe("user-123"); + }); + }); + + describe("Public Routes", () => { + it("should not modify body for public routes", async () => { + const app = new Elysia() + .state("authenticatedUser", null as any) + .onBeforeHandle(createAuthMiddleware(mockAuthProvider)) + .get("/health", () => ({ status: "ok" })); + + const res = await app.handle(new Request("http://localhost:3000/health")); + + expect(res.status).toBe(200); + // verifyToken should not be called for public routes + expect(mockAuthProvider.verifyToken).not.toHaveBeenCalled(); + }); + }); + + describe("Authentication Failures", () => { + it("should return 401 when token is missing", async () => { + mockAuthProvider.defaultPrivate = true; + + const app = new Elysia() + .state("authenticatedUser", null as any) + .onBeforeHandle(createAuthMiddleware(mockAuthProvider)) + .post("/agents/test-agent/text", () => ({ success: true })); + + const res = await app.handle( + new Request("http://localhost:3000/agents/test-agent/text", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ input: "Test" }), + }), + ); + + expect(res.status).toBe(401); + const json = await res.json(); + expect(json.success).toBe(false); + expect(json.error).toBe("Authentication required"); + }); + + it("should return 401 when token is invalid", async () => { + mockAuthProvider.defaultPrivate = true; + + const app = new Elysia() + .state("authenticatedUser", null as any) + .onBeforeHandle(createAuthMiddleware(mockAuthProvider)) + .post("/agents/test-agent/text", () => ({ success: true })); + + const res = await app.handle( + new Request("http://localhost:3000/agents/test-agent/text", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: "Bearer invalid-token", + }, + body: JSON.stringify({ input: "Test" }), + }), + ); + + expect(res.status).toBe(401); + const json = await res.json(); + expect(json.success).toBe(false); + expect(json.error).toBe("Invalid authentication"); + }); + }); +}); diff --git a/packages/server-elysia/src/auth/middleware.ts b/packages/server-elysia/src/auth/middleware.ts new file mode 100644 index 000000000..e94779049 --- /dev/null +++ b/packages/server-elysia/src/auth/middleware.ts @@ -0,0 +1,253 @@ +import type { AuthNextConfig, AuthProvider } from "@voltagent/server-core"; +import { + hasConsoleAccess, + isDevRequest, + normalizeAuthNextConfig, + requiresAuth, + resolveAuthNextAccess, +} from "@voltagent/server-core"; +import type { Context } from "elysia"; + +/** + * Create authentication middleware for Elysia + * This middleware handles both authentication and user context injection + * @param authProvider The authentication provider + * @returns Elysia middleware function + */ +export function createAuthMiddleware(authProvider: AuthProvider) { + return async ({ request, path, set, store }: Context & { store: any }) => { + const method = request.method; + + // Check if this route requires authentication + const needsAuth = requiresAuth( + method, + path, + authProvider.publicRoutes, + authProvider.defaultPrivate, + ); + + if (!needsAuth) { + // Public route, no auth needed + return; + } + + // Console Access Check (for observability and system routes) + if (path.startsWith("/observability/") || path.startsWith("/updates")) { + if (hasConsoleAccess(request)) { + return; + } + } + + // Development bypass: Allow requests with x-voltagent-dev header in development + const devBypass = isDevRequest(request); + + if (devBypass) { + return; + } + + try { + // Extract token + let token: string | undefined; + + if (authProvider.extractToken) { + // Use provider's custom extraction + token = authProvider.extractToken(request); + } else { + // Default extraction from Authorization header + const authHeader = request.headers.get("Authorization"); + if (authHeader?.startsWith("Bearer ")) { + token = authHeader.substring(7); + } + } + + if (!token) { + set.status = 401; + return { + success: false, + error: "Authentication required", + }; + } + + // Verify token and get user + const user = await authProvider.verifyToken(token, request); + + if (!user) { + set.status = 401; + return { + success: false, + error: "Invalid authentication", + }; + } + + // Store user in store for route handlers to access + store.authenticatedUser = user; + + // Also inject user context into request body for agent/workflow execution + injectUserContext(request, user); + + return; + } catch (error) { + const message = error instanceof Error ? error.message : "Authentication failed"; + set.status = 401; + return { + success: false, + error: message, + }; + } + }; +} + +/** + * Create authentication middleware for Elysia using authNext policy + * This middleware handles both authentication and user context injection + * @param authNextConfig The authNext configuration + * @returns Elysia middleware function + */ +export function createAuthNextMiddleware( + authNextConfig: AuthNextConfig | AuthProvider, +) { + const config = normalizeAuthNextConfig(authNextConfig); + const authProvider = config.provider; + + return async ({ request, path, set, store }: Context & { store: any }) => { + const method = request.method; + const access = resolveAuthNextAccess(method, path, config); + + if (access === "public") { + return; + } + + if (access === "console") { + if (hasConsoleAccess(request)) { + return; + } + + set.status = 401; + return { + success: false, + error: buildAuthNextMessage("console", "Console access required"), + }; + } + + if (isDevRequest(request)) { + return; + } + + try { + let token: string | undefined; + + if (authProvider.extractToken) { + token = authProvider.extractToken(request); + } else { + const authHeader = request.headers.get("Authorization"); + if (authHeader?.startsWith("Bearer ")) { + token = authHeader.substring(7); + } + } + + if (!token) { + set.status = 401; + return { + success: false, + error: buildAuthNextMessage("user", "Authentication required"), + }; + } + + const user = await authProvider.verifyToken(token, request); + + if (!user) { + set.status = 401; + return { + success: false, + error: buildAuthNextMessage("user", "Invalid authentication"), + }; + } + + // Store user in store for route handlers to access + store.authenticatedUser = user; + + // Also inject user context into request body for agent/workflow execution + injectUserContext(request, user); + + return; + } catch (error) { + const reason = error instanceof Error ? error.message : "Authentication failed"; + set.status = 401; + return { + success: false, + error: buildAuthNextMessage("user", reason), + }; + } + }; +} + +function buildAuthNextMessage(access: "console" | "user", reason: string): string { + const hint = buildAuthNextHint(access); + const normalized = reason.endsWith(".") ? reason.slice(0, -1) : reason; + return `${normalized}. ${hint}`; +} + +function buildAuthNextHint(access: "console" | "user"): string { + const devHint = + process.env.NODE_ENV !== "production" + ? " In development, you can set x-voltagent-dev: true." + : ""; + + if (access === "console") { + return `Set VOLTAGENT_CONSOLE_ACCESS_KEY and send x-console-access-key header or add ?key=YOUR_KEY query param.${devHint}`; + } + + return `Send Authorization: Bearer .${devHint}`; +} + +/** + * Inject user context into the request for agent/workflow execution + */ +function injectUserContext(request: Request, user: any) { + // Store original json method + const originalJson = (request as any).json?.bind(request); + + if (!originalJson) { + return; + } + + let cachedBody: any; + let isCached = false; + + // Override json method to inject user context + (request as any).json = async () => { + if (isCached) { + return cachedBody; + } + + const body = await originalJson(); + + if (!body || typeof body !== "object") { + cachedBody = body; + isCached = true; + return body; + } + + // Create a proper merged context + const userId = user.id || user.sub || body.options?.userId; + + // Merge body.context into body.options.context + const mergedContext = { + ...body.context, + ...body.options?.context, + user, + }; + + cachedBody = { + ...body, + options: { + ...body.options, + userId, + context: mergedContext, + }, + }; + + isCached = true; + return cachedBody; + }; +} diff --git a/packages/server-elysia/src/elysia-server-provider.spec.ts b/packages/server-elysia/src/elysia-server-provider.spec.ts new file mode 100644 index 000000000..3355ed96c --- /dev/null +++ b/packages/server-elysia/src/elysia-server-provider.spec.ts @@ -0,0 +1,123 @@ +import { portManager } from "@voltagent/server-core"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import * as appFactory from "./app-factory"; +import { ElysiaServerProvider } from "./elysia-server-provider"; + +// Mock dependencies +vi.mock("@voltagent/server-core", async () => { + const actual = await vi.importActual("@voltagent/server-core"); + return { + ...actual, + portManager: { + allocatePort: vi.fn().mockImplementation((port) => Promise.resolve(port || 3000)), + releasePort: vi.fn(), + }, + createWebSocketServer: vi.fn(), + setupWebSocketUpgrade: vi.fn(), + showAnnouncements: vi.fn(), + printServerStartup: vi.fn(), + }; +}); + +describe("ElysiaServerProvider", () => { + let provider: ElysiaServerProvider; + const mockApp = { + listen: vi.fn().mockReturnValue({}), + stop: vi.fn(), + routes: [], // For extractCustomEndpoints + get: vi.fn(), // For configureApp test + }; + + const mockDeps = { + logger: { + info: vi.fn(), + error: vi.fn(), + warn: vi.fn(), + debug: vi.fn(), + trace: vi.fn(), + child: vi.fn().mockReturnThis(), + }, + config: { + port: 3000, + }, + } as any; + + beforeEach(() => { + vi.spyOn(appFactory, "createApp").mockResolvedValue({ app: mockApp } as any); + provider = new ElysiaServerProvider(mockDeps, { port: 3000 }); + }); + + afterEach(() => { + vi.clearAllMocks(); + mockApp.routes = []; + }); + + it("should start the server", async () => { + await provider.start(); + expect(appFactory.createApp).toHaveBeenCalled(); + expect(mockApp.listen).toHaveBeenCalledWith({ + port: 3000, + hostname: "0.0.0.0", + }); + }); + + it("should stop the server", async () => { + await provider.start(); + await provider.stop(); + expect(mockApp.stop).toHaveBeenCalled(); + }); + + it("should throw if already running", async () => { + await provider.start(); + await expect(provider.start()).rejects.toThrow("Server is already running"); + }); + + it("should configure websocket if enabled", async () => { + const { createWebSocketServer } = await import("@voltagent/server-core"); + const providerWs = new ElysiaServerProvider(mockDeps, { port: 3000, enableWebSocket: true }); + await providerWs.start(); + expect(createWebSocketServer).toHaveBeenCalled(); + }); + + it("should extract and display custom endpoints from configureApp", async () => { + const { printServerStartup } = await import("@voltagent/server-core"); + + // Create a fresh mock app for this test to avoid pollution + const localMockApp = { + listen: vi.fn().mockReturnValue({}), + stop: vi.fn(), + routes: [{ method: "GET", path: "/custom-test" }], + get: vi.fn(), + }; + + vi.spyOn(appFactory, "createApp").mockResolvedValue({ app: localMockApp } as any); + + const providerWithCustom = new ElysiaServerProvider(mockDeps, { + port: 3000, + configureApp: (app) => { + app.get("/custom-test", () => "custom"); + return app; + }, + }); + + await providerWithCustom.start(); + expect(printServerStartup).toHaveBeenCalledWith( + expect.any(Number), + expect.objectContaining({ + customEndpoints: expect.arrayContaining([ + expect.objectContaining({ path: "/custom-test" }), + ]), + }), + ); + }); + + it("should handle startup errors and release port", async () => { + // Mock app.listen to throw + mockApp.listen.mockImplementationOnce(() => { + throw new Error("Startup failed"); + }); + + await expect(provider.start()).rejects.toThrow("Startup failed"); + expect(portManager.releasePort).toHaveBeenCalledWith(3000); + }); +}); diff --git a/packages/server-elysia/src/elysia-server-provider.ts b/packages/server-elysia/src/elysia-server-provider.ts new file mode 100644 index 000000000..e86fc6362 --- /dev/null +++ b/packages/server-elysia/src/elysia-server-provider.ts @@ -0,0 +1,166 @@ +/** + * Elysia server provider implementation + * Extends BaseServerProvider with Elysia-specific implementation + */ + +import type { Server } from "node:http"; +import type { ServerProviderDeps } from "@voltagent/core"; +import { + BaseServerProvider, + createWebSocketServer, + portManager, + printServerStartup, + setupWebSocketUpgrade, + showAnnouncements, +} from "@voltagent/server-core"; +import { createApp } from "./app-factory"; +import type { ElysiaServerConfig } from "./types"; +import { extractCustomEndpoints } from "./utils/custom-endpoints"; + +/** + * Elysia server provider class + */ +export class ElysiaServerProvider extends BaseServerProvider { + private elysiaConfig: ElysiaServerConfig; + private app?: any; // Store app instance to extract custom endpoints + + constructor(deps: ServerProviderDeps, config: ElysiaServerConfig = {}) { + super(deps, config); + this.elysiaConfig = config; + } + + /** + * Start the Elysia server + */ + protected async startServer(port: number): Promise { + // Create the app with dependencies and actual port + const { app } = await createApp(this.deps, this.elysiaConfig, port); + + // Store app instance for custom endpoint extraction + this.app = app; + + try { + // Elysia's listen() method returns a server instance (works in Bun) + const server = app.listen({ + port, + hostname: this.elysiaConfig.hostname || "0.0.0.0", + }); + + return server as unknown as Server; + } catch (error: unknown) { + // Fallback for Node.js environment where WebStandard listen is not supported + if ( + error instanceof Error && + (error.message.includes("WebStandard does not support listen") || + error.message.includes("is not a function")) + ) { + const { createServer } = await import("node:http"); + const server = createServer(app.fetch); + + return new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(port, this.elysiaConfig.hostname || "0.0.0.0", () => { + resolve(server); + }); + }); + } + throw error; + } + } + + /** + * Stop the Elysia server + */ + protected async stopServer(): Promise { + try { + if (this.app?.stop) { + await this.app.stop(); + } + } catch (_error) { + // Ignore errors from app.stop() in Node environment where Elysia might complain + // "Elysia isn't running" + } + + // Ensure the underlying Node.js server is closed if it exists + if (this.server?.listening) { + await new Promise((resolve, reject) => { + this.server?.close((err) => { + if (err) reject(err); + else resolve(); + }); + }); + } + } + + /** + * Override start method to include custom endpoints in startup display + */ + async start(): Promise<{ port: number }> { + if (this.running) { + throw new Error("Server is already running"); + } + + // Allocate port from central manager + const port = await portManager.allocatePort(this.config.port); + this.allocatedPort = port; + + try { + // Framework-specific server start + this.server = await this.startServer(port); + + // Setup WebSocket if enabled + if (this.config.enableWebSocket !== false) { + const authConfig = this.elysiaConfig.authNext ?? this.elysiaConfig.auth; + this.websocketServer = createWebSocketServer(this.deps, this.logger, authConfig); + setupWebSocketUpgrade( + this.server, + this.websocketServer, + this.config.websocketPath, + authConfig, + this.logger, + ); + } + + this.running = true; + + // Show announcements (non-blocking) + showAnnouncements(); + + // Collect all endpoints (feature + custom) + const allEndpoints = this.collectFeatureEndpoints(); + + // Add custom endpoints if we have them + if (this.app && this.elysiaConfig.configureApp) { + try { + const customEndpoints = extractCustomEndpoints(this.app); + const seen = new Set( + allEndpoints.map((endpoint) => `${endpoint.method} ${endpoint.path}`), + ); + customEndpoints.forEach((endpoint) => { + const key = `${endpoint.method} ${endpoint.path}`; + if (!seen.has(key)) { + seen.add(key); + allEndpoints.push(endpoint); + } + }); + } catch (_error) { + // If extraction fails, continue without custom endpoints + this.logger.warn("Failed to extract custom endpoints for startup display"); + } + } + + // Print startup message with all endpoints + printServerStartup(port, { + enableSwaggerUI: this.config.enableSwaggerUI, + customEndpoints: allEndpoints.length > 0 ? allEndpoints : undefined, + }); + + return { port }; + } catch (error) { + // If server fails to start, release the port + portManager.releasePort(port); + this.allocatedPort = null; + throw error; + } + } +} diff --git a/packages/server-elysia/src/index.ts b/packages/server-elysia/src/index.ts new file mode 100644 index 000000000..6eae08fab --- /dev/null +++ b/packages/server-elysia/src/index.ts @@ -0,0 +1,27 @@ +import type { IServerProvider, ServerProviderDeps } from "@voltagent/core"; +import { ElysiaServerProvider } from "./elysia-server-provider"; +import type { ElysiaServerConfig } from "./types"; + +/** + * Creates an Elysia server provider + */ +export function elysiaServer(config: ElysiaServerConfig = {}) { + return (deps: ServerProviderDeps): IServerProvider => { + return new ElysiaServerProvider(deps, config); + }; +} + +// Export the factory function as default as well +export default elysiaServer; + +// Re-export types that might be needed +export type { ElysiaServerConfig } from "./types"; + +// Export auth utilities +export { DEFAULT_CONSOLE_ROUTES, jwtAuth, type AuthNextConfig } from "./auth"; + +// Export custom endpoint utilities +export { extractCustomEndpoints, getEnhancedOpenApiDoc } from "./utils/custom-endpoints"; + +// Export app factory for middleware integrations +export { createApp as createVoltAgentApp } from "./app-factory"; diff --git a/packages/server-elysia/src/mcp/elysia-sse-bridge.spec.ts b/packages/server-elysia/src/mcp/elysia-sse-bridge.spec.ts new file mode 100644 index 000000000..5aa3c64f9 --- /dev/null +++ b/packages/server-elysia/src/mcp/elysia-sse-bridge.spec.ts @@ -0,0 +1,65 @@ +import { describe, expect, it, vi } from "vitest"; +import { ElysiaSseBridge } from "./elysia-sse-bridge"; + +describe("ElysiaSseBridge", () => { + it("should send events correctly", async () => { + const sendEvent = vi.fn(); + const closeStream = vi.fn(); + const bridge = new ElysiaSseBridge(sendEvent, closeStream); + + await bridge.send({ + data: "test data", + event: "test-event", + id: "123", + }); + + expect(sendEvent).toHaveBeenCalledWith("test data", { + event: "test-event", + id: "123", + }); + }); + + it("should close the stream correctly", async () => { + const sendEvent = vi.fn(); + const closeStream = vi.fn(); + const bridge = new ElysiaSseBridge(sendEvent, closeStream); + + await bridge.close(); + + expect(closeStream).toHaveBeenCalled(); + }); + + it("should handle abort signal", async () => { + const sendEvent = vi.fn(); + const closeStream = vi.fn(); + const controller = new AbortController(); + const bridge = new ElysiaSseBridge(sendEvent, closeStream, controller.signal); + + const abortListener = vi.fn(); + bridge.onAbort(abortListener); + + controller.abort(); + + // Wait for microtasks + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(abortListener).toHaveBeenCalled(); + }); + + it("should call abort listener immediately if already aborted", async () => { + const sendEvent = vi.fn(); + const closeStream = vi.fn(); + const controller = new AbortController(); + controller.abort(); + + const bridge = new ElysiaSseBridge(sendEvent, closeStream, controller.signal); + + const abortListener = vi.fn(); + bridge.onAbort(abortListener); + + // Wait for microtasks + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(abortListener).toHaveBeenCalled(); + }); +}); diff --git a/packages/server-elysia/src/mcp/elysia-sse-bridge.ts b/packages/server-elysia/src/mcp/elysia-sse-bridge.ts new file mode 100644 index 000000000..6f659e636 --- /dev/null +++ b/packages/server-elysia/src/mcp/elysia-sse-bridge.ts @@ -0,0 +1,68 @@ +import type { SseBridge } from "@voltagent/mcp-server"; + +/** + * Elysia SSE bridge for MCP protocol + * Adapts Elysia's streaming API to the MCP SseBridge interface + */ +export class ElysiaSseBridge implements SseBridge { + private abortController: AbortController; + private abortListeners: Array<() => void | Promise> = []; + + constructor( + private readonly sendEvent: (data: string, options?: { event?: string; id?: string }) => void, + private readonly closeStream: () => void, + signal?: AbortSignal, + ) { + this.abortController = new AbortController(); + + // Forward external abort signal if provided + if (signal) { + if (signal.aborted) { + this.abortController.abort(); + } else { + signal.addEventListener("abort", () => { + this.abortController.abort(); + this.triggerAbortListeners(); + }); + } + } + } + + async send(message: { + data: string; + event?: string; + id?: string; + retry?: number; + }): Promise { + this.sendEvent(message.data, { + event: message.event, + id: message.id, + }); + } + + async close(): Promise { + this.abortController.abort(); + this.closeStream(); + } + + onAbort(listener: () => void | Promise): void { + this.abortListeners.push(listener); + + // If already aborted, call immediately + if (this.abortController.signal.aborted) { + Promise.resolve(listener()).catch((error) => { + console.error("Error in abort listener:", error); + }); + } + } + + private async triggerAbortListeners(): Promise { + for (const listener of this.abortListeners) { + try { + await listener(); + } catch (error) { + console.error("Error in abort listener:", error); + } + } + } +} diff --git a/packages/server-elysia/src/routes/a2a.routes.spec.ts b/packages/server-elysia/src/routes/a2a.routes.spec.ts new file mode 100644 index 000000000..a7c76b957 --- /dev/null +++ b/packages/server-elysia/src/routes/a2a.routes.spec.ts @@ -0,0 +1,118 @@ +import * as serverCore from "@voltagent/server-core"; +import { Elysia } from "elysia"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerA2ARoutes } from "./a2a.routes"; + +// Mock server-core handlers +vi.mock("@voltagent/server-core", async () => { + const actual = await vi.importActual("@voltagent/server-core"); + return { + ...actual, + executeA2ARequest: vi.fn(), + resolveAgentCard: vi.fn(), + A2A_ROUTES: actual.A2A_ROUTES, + }; +}); + +// Mock a2a-server +vi.mock("@voltagent/a2a-server", async () => { + return { + normalizeError: vi.fn().mockImplementation((id, error) => ({ + jsonrpc: "2.0", + id, + error: { + code: error.code || -32603, + message: error.message, + }, + })), + }; +}); + +describe("A2A Routes", () => { + let app: Elysia; + const mockDeps = { + a2a: { + registry: { + list: vi.fn().mockReturnValue([{ id: "server1" }]), + }, + }, + } as any; + const mockLogger = { + trace: vi.fn(), + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + } as any; + + beforeEach(() => { + app = new Elysia(); + registerA2ARoutes(app, mockDeps, mockLogger); + vi.clearAllMocks(); + }); + + it("should handle JSON-RPC request", async () => { + vi.mocked(serverCore.executeA2ARequest).mockResolvedValue({ + jsonrpc: "2.0", + id: "1", + result: "success", + }); + + const response = await app.handle( + new Request("http://localhost/a2a/server1", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "test", + id: "1", + }), + }), + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + jsonrpc: "2.0", + id: "1", + result: "success", + }); + expect(serverCore.executeA2ARequest).toHaveBeenCalled(); + }); + + it("should handle agent card request", async () => { + vi.mocked(serverCore.resolveAgentCard).mockResolvedValue({ + name: "agent", + description: "desc", + } as any); + + const response = await app.handle( + new Request("http://localhost/.well-known/server1/agent-card.json"), + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ + name: "agent", + description: "desc", + }); + expect(serverCore.resolveAgentCard).toHaveBeenCalledWith( + mockDeps.a2a.registry, + "server1", + "server1", + {}, + ); + }); + + it("should handle agent card not found", async () => { + vi.mocked(serverCore.resolveAgentCard).mockImplementation(() => { + const error = new Error("Not found"); + (error as any).code = -32601; + throw error; + }); + + const response = await app.handle( + new Request("http://localhost/.well-known/server1/agent-card.json"), + ); + + expect(response.status).toBe(404); + }); +}); diff --git a/packages/server-elysia/src/routes/a2a.routes.ts b/packages/server-elysia/src/routes/a2a.routes.ts new file mode 100644 index 000000000..4542ddb63 --- /dev/null +++ b/packages/server-elysia/src/routes/a2a.routes.ts @@ -0,0 +1,224 @@ +import { type A2ARequestContext, normalizeError } from "@voltagent/a2a-server"; +import type { ServerProviderDeps } from "@voltagent/core"; +import type { Logger } from "@voltagent/internal"; +import { + A2A_ROUTES, + executeA2ARequest, + parseJsonRpcRequest, + resolveAgentCard, +} from "@voltagent/server-core"; +import type { Elysia } from "elysia"; +import { t } from "elysia"; +import { A2AResponseSchema, ErrorSchema } from "../schemas"; + +/** + * A2A Server ID parameter schema + */ +const ServerIdParam = t.Object({ + serverId: t.String({ description: "The ID of the A2A server" }), +}); + +/** + * A2A JSON-RPC query parameters schema + */ +const JsonRpcQuery = t.Object({ + context: t.Optional( + t.String({ + description: "JSON-encoded A2A request context (userId, sessionId, metadata)", + }), + ), + runtimeContext: t.Optional(t.String({ description: "Alternative name for context parameter" })), +}); + +/** + * A2A JSON-RPC request body schema + */ +const JsonRpcRequestSchema = t.Object({ + jsonrpc: t.Optional(t.Literal("2.0", { description: "JSON-RPC version" })), + method: t.String({ description: "JSON-RPC method name" }), + params: t.Optional(t.Any({ description: "Method parameters" })), + id: t.Optional( + t.Union([t.String(), t.Number(), t.Null()], { + description: "Request ID", + }), + ), + context: t.Optional( + t.Object({ + userId: t.Optional(t.String()), + sessionId: t.Optional(t.String()), + metadata: t.Optional(t.Record(t.String(), t.Unknown())), + }), + ), +}); + +function parseContextCandidate(candidate: unknown): A2ARequestContext | undefined { + if (!candidate || typeof candidate !== "object" || Array.isArray(candidate)) { + return undefined; + } + + const { userId, sessionId, metadata } = candidate as Record; + const context: A2ARequestContext = {}; + + if (typeof userId === "string") { + context.userId = userId; + } + + if (typeof sessionId === "string") { + context.sessionId = sessionId; + } + + if (metadata && typeof metadata === "object" && !Array.isArray(metadata)) { + context.metadata = metadata as Record; + } + + return Object.keys(context).length > 0 ? context : undefined; +} + +function mergeContexts( + base: A2ARequestContext | undefined, + next: A2ARequestContext | undefined, +): A2ARequestContext | undefined { + if (!base) { + return next; + } + if (!next) { + return base; + } + + const merged: A2ARequestContext = { + ...base, + ...next, + }; + + if (base.metadata || next.metadata) { + merged.metadata = { + ...(base.metadata ?? {}), + ...(next.metadata ?? {}), + }; + } + + return merged; +} + +/** + * Register A2A (Agent-to-Agent) routes with validation and OpenAPI documentation + */ +export function registerA2ARoutes(app: Elysia, deps: ServerProviderDeps, logger: Logger): void { + const registry = deps.a2a?.registry; + + if (!registry) { + logger.debug("A2A server registry not available on server deps; skipping A2A routes"); + return; + } + + const registeredServers = typeof registry.list === "function" ? registry.list() : []; + + if (registeredServers.length === 0) { + return; + } + + const typedRegistry = registry as Parameters[0]; + + // POST /a2a/:serverId - Handle JSON-RPC request + app.post( + A2A_ROUTES.jsonRpc.path, + async ({ params, body, query, set }) => { + try { + let context: A2ARequestContext | undefined; + const contextStr = query.context || query.runtimeContext; + + if (contextStr) { + try { + const parsed = JSON.parse(contextStr); + context = mergeContexts(context, parseContextCandidate(parsed)); + } catch (_e) { + throw new Error("Invalid 'context' query parameter; expected JSON"); + } + } + + const bodyObj = body as any; + let payload = bodyObj; + + if (bodyObj && typeof bodyObj === "object" && !Array.isArray(bodyObj)) { + const { context: bodyContext, ...rest } = bodyObj; + if (typeof bodyContext !== "undefined") { + context = mergeContexts(context, parseContextCandidate(bodyContext)); + } + payload = rest; + } + + const rpcRequest = parseJsonRpcRequest(payload); + + const response = await executeA2ARequest({ + registry: typedRegistry, + serverId: params.serverId, + request: rpcRequest, + context, + logger, + }); + + return response; + } catch (error) { + const response = normalizeError(null, error); + const code = response.error?.code; + const isServerError = + code === -32603 || (code !== undefined && code <= -32000 && code >= -32099); + set.status = isServerError ? 500 : 400; + return response; + } + }, + { + params: ServerIdParam, + body: JsonRpcRequestSchema, + query: JsonRpcQuery, + response: { + 200: A2AResponseSchema, + 400: A2AResponseSchema, // Error response is also JSON-RPC + 500: ErrorSchema, + }, + detail: { + summary: "Handle A2A JSON-RPC request", + description: "Processes a JSON-RPC request for the Agent-to-Agent protocol", + tags: ["A2A"], + }, + }, + ); + + // GET /a2a/:serverId/card - Get agent card + app.get( + A2A_ROUTES.agentCard.path, + async ({ params, set }) => { + try { + const card = resolveAgentCard(typedRegistry, params.serverId, params.serverId, {}); + return card; + } catch (error) { + const response = normalizeError(null, error); + const code = response.error?.code; + let status = 400; + + if (code === -32601) { + status = 404; + } else if (code === -32603 || (code !== undefined && code <= -32000 && code >= -32099)) { + status = 500; + } + + set.status = status; + return response; + } + }, + { + params: ServerIdParam, + response: { + 200: A2AResponseSchema, // Agent card is a JSON object + 400: A2AResponseSchema, + 404: A2AResponseSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Get agent card", + description: "Retrieve the agent card for a specific A2A server", + tags: ["A2A"], + }, + }, + ); +} diff --git a/packages/server-elysia/src/routes/agent.routes.ts b/packages/server-elysia/src/routes/agent.routes.ts new file mode 100644 index 000000000..5848f928e --- /dev/null +++ b/packages/server-elysia/src/routes/agent.routes.ts @@ -0,0 +1,232 @@ +import type { ServerProviderDeps } from "@voltagent/core"; +import type { Logger } from "@voltagent/internal"; +import { + handleChatStream, + handleGenerateObject, + handleGenerateText, + handleGetAgent, + handleGetAgentHistory, + handleGetAgents, + handleStreamObject, + handleStreamText, + mapLogResponse, +} from "@voltagent/server-core"; +import type { Elysia } from "elysia"; +import { t } from "elysia"; +import { + AgentListSchema, + AgentResponseSchema, + ErrorSchema, + LogResponseSchema, + ObjectRequestSchema, + ObjectResponseSchema, + TextRequestSchema, + TextResponseSchema, +} from "../schemas"; + +// Agent ID parameter +const AgentIdParam = t.Object({ + id: t.String(), +}); + +// History query parameters +const HistoryQuery = t.Object({ + page: t.Optional(t.String()), + limit: t.Optional(t.String()), +}); + +/** + * Register agent routes with full type validation and OpenAPI documentation + */ +export function registerAgentRoutes(app: Elysia, deps: ServerProviderDeps, logger: Logger): void { + // GET /agents - List all agents + app.get( + "/agents", + async () => { + const response = await handleGetAgents(deps, logger); + if (!response.success) { + throw new Error("Failed to get agents"); + } + return response; + }, + { + response: { + 200: t.Object({ + success: t.Literal(true), + data: AgentListSchema, + }), + 500: ErrorSchema, + }, + detail: { + summary: "List all agents", + description: "Get a list of all registered agents in the system", + tags: ["Agents"], + }, + }, + ); + + // GET /agents/:id - Get agent by ID + app.get( + "/agents/:id", + async ({ params }) => { + const response = await handleGetAgent(params.id, deps, logger); + if (!response.success) { + throw new Error("Agent not found"); + } + return response; + }, + { + params: AgentIdParam, + response: { + 200: t.Object({ + success: t.Literal(true), + data: AgentResponseSchema, + }), + 404: ErrorSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Get agent by ID", + description: "Retrieve a specific agent by its ID", + tags: ["Agents"], + }, + }, + ); + + // POST /agents/:id/text - Generate text (AI SDK compatible) + app.post( + "/agents/:id/text", + async ({ params, body, request, set }) => { + const response = await handleGenerateText(params.id, body, deps, logger, request.signal); + if (!response.success) { + const { httpStatus, ...details } = response; + set.status = httpStatus || 500; + return details; + } + return response; + }, + { + params: AgentIdParam, + body: TextRequestSchema, + response: { + 200: TextResponseSchema, + 404: ErrorSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Generate text", + description: "Generate text using the specified agent (AI SDK compatible)", + tags: ["Agents"], + }, + }, + ); + + // POST /agents/:id/stream - Stream text (raw fullStream SSE) + app.post( + "/agents/:id/stream", + async ({ params, body, request }) => { + const response = await handleStreamText(params.id, body, deps, logger, request.signal); + return response; + }, + { + params: AgentIdParam, + body: TextRequestSchema, + detail: { + summary: "Stream text", + description: "Stream text generation using the specified agent (Server-Sent Events)", + tags: ["Agents"], + }, + }, + ); + + // POST /agents/:id/chat - Stream chat messages (UI message stream SSE) + app.post( + "/agents/:id/chat", + async ({ params, body, request }) => { + const response = await handleChatStream(params.id, body, deps, logger, request.signal); + return response; + }, + { + params: AgentIdParam, + body: TextRequestSchema, + detail: { + summary: "Stream chat messages", + description: "Stream chat messages using the specified agent (UI message stream SSE)", + tags: ["Agents"], + }, + }, + ); + + // POST /agents/:id/object - Generate object + app.post( + "/agents/:id/object", + async ({ params, body, request, set }) => { + const response = await handleGenerateObject(params.id, body, deps, logger, request.signal); + if (!response.success) { + const { httpStatus, ...details } = response; + set.status = httpStatus || 500; + return details; + } + return response; + }, + { + params: AgentIdParam, + body: ObjectRequestSchema, + response: { + 200: ObjectResponseSchema, + 404: ErrorSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Generate object", + description: "Generate a structured object using the specified agent", + tags: ["Agents"], + }, + }, + ); + + // POST /agents/:id/stream-object - Stream object + app.post( + "/agents/:id/stream-object", + async ({ params, body, request }) => { + const response = await handleStreamObject(params.id, body, deps, logger, request.signal); + return response; + }, + { + params: AgentIdParam, + body: ObjectRequestSchema, + detail: { + summary: "Stream object", + description: "Stream object generation using the specified agent", + tags: ["Agents"], + }, + }, + ); + + // GET /agents/:id/history - Get agent history with pagination + app.get( + "/agents/:id/history", + async ({ params, query }) => { + const page = Math.max(0, Number.parseInt((query.page as string) || "0", 10) || 0); + const limit = Math.max(1, Number.parseInt((query.limit as string) || "10", 10) || 10); + const response = await handleGetAgentHistory(params.id, page, limit, deps, logger); + if (!response.success) { + throw new Error("Failed to get agent history"); + } + return mapLogResponse(response); + }, + { + params: AgentIdParam, + query: HistoryQuery, + response: { + 200: LogResponseSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Get agent history", + description: "Retrieve the execution history of an agent with pagination", + tags: ["Agents"], + }, + }, + ); +} diff --git a/packages/server-elysia/src/routes/controllers.httpError.spec.ts b/packages/server-elysia/src/routes/controllers.httpError.spec.ts new file mode 100644 index 000000000..e3f05284b --- /dev/null +++ b/packages/server-elysia/src/routes/controllers.httpError.spec.ts @@ -0,0 +1,170 @@ +import { Elysia } from "elysia"; +import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; +import { registerAgentRoutes } from "./index"; + +// Partially mock server-core to override only the handlers we need for tests +vi.mock("@voltagent/server-core", async () => { + const actual = + await vi.importActual("@voltagent/server-core"); + return { + ...actual, + handleGenerateText: vi.fn(), + handleGenerateObject: vi.fn(), + }; +}); + +// Grab the mocked functions with proper typing without top-level await (CJS-compatible) +let mockedHandleGenerateText: ReturnType; +let mockedHandleGenerateObject: ReturnType; + +beforeAll(async () => { + const mockedCore = await import("@voltagent/server-core"); + mockedHandleGenerateText = mockedCore.handleGenerateText as unknown as ReturnType; + mockedHandleGenerateObject = mockedCore.handleGenerateObject as unknown as ReturnType< + typeof vi.fn + >; +}); + +const logger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), +} as any; + +function makeApp() { + const app = new Elysia(); + registerAgentRoutes(app as any, {} as any, logger); + return app; +} + +describe("server-elysia controllers: httpError/httpStatus mapping", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("POST /agents/:id/text should use httpStatus from handler error and return details without httpStatus", async () => { + mockedHandleGenerateText.mockResolvedValueOnce({ + success: false, + error: "Quota exceeded for web-search", + code: "TOOL_QUOTA_EXCEEDED", + name: "web-search", + httpStatus: 429, + }); + + const app = makeApp(); + + const res = await app.handle( + new Request("http://localhost/agents/agent-1/text", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ input: "hi" }), + }), + ); + + expect(res.status).toBe(429); + const json = await res.json(); + expect(json).toMatchObject({ + success: false, + error: "Quota exceeded for web-search", + code: "TOOL_QUOTA_EXCEEDED", + name: "web-search", + }); + // httpStatus should not be included in the response body + expect((json as any).httpStatus).toBeUndefined(); + }); + + it("POST /agents/:id/text should default to 500 when handler error has no httpStatus", async () => { + mockedHandleGenerateText.mockResolvedValueOnce({ + success: false, + error: "Model timeout", + }); + + const app = makeApp(); + + const res = await app.handle( + new Request("http://localhost/agents/agent-1/text", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ input: "hi" }), + }), + ); + + expect(res.status).toBe(500); + const json = await res.json(); + expect(json).toMatchObject({ + success: false, + error: "Model timeout", + }); + expect((json as any).httpStatus).toBeUndefined(); + }); + + it("POST /agents/:id/object should use httpStatus from handler error and return details without httpStatus", async () => { + mockedHandleGenerateObject.mockResolvedValueOnce({ + success: false, + error: "Invalid JSON schema", + code: "SCHEMA_ERROR", + name: "object-gen", + httpStatus: 400, + }); + + const app = makeApp(); + + const res = await app.handle( + new Request("http://localhost/agents/agent-1/object", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + input: "{}", + schema: { + type: "object", + properties: {}, + required: [], + }, + }), + }), + ); + + expect(res.status).toBe(400); + const json = await res.json(); + expect(json).toMatchObject({ + success: false, + error: "Invalid JSON schema", + code: "SCHEMA_ERROR", + name: "object-gen", + }); + expect((json as any).httpStatus).toBeUndefined(); + }); + + it("POST /agents/:id/object should default to 500 when handler error has no httpStatus", async () => { + mockedHandleGenerateObject.mockResolvedValueOnce({ + success: false, + error: "Unknown failure", + }); + + const app = makeApp(); + + const res = await app.handle( + new Request("http://localhost/agents/agent-1/object", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + input: "{}", + schema: { + type: "object", + properties: {}, + required: [], + }, + }), + }), + ); + + expect(res.status).toBe(500); + const json = await res.json(); + expect(json).toMatchObject({ + success: false, + error: "Unknown failure", + }); + expect((json as any).httpStatus).toBeUndefined(); + }); +}); diff --git a/packages/server-elysia/src/routes/index.ts b/packages/server-elysia/src/routes/index.ts new file mode 100644 index 000000000..acd565957 --- /dev/null +++ b/packages/server-elysia/src/routes/index.ts @@ -0,0 +1,9 @@ +export { registerAgentRoutes } from "./agent.routes"; +export { registerWorkflowRoutes } from "./workflow.routes"; +export { registerLogRoutes } from "./log.routes"; +export { registerUpdateRoutes } from "./update.routes"; +export { registerMcpRoutes } from "./mcp.routes"; +export { registerA2ARoutes } from "./a2a.routes"; +export { registerToolRoutes } from "./tool.routes"; +export { registerTriggerRoutes } from "./trigger.routes"; +export { registerObservabilityRoutes } from "./observability"; diff --git a/packages/server-elysia/src/routes/log.routes.ts b/packages/server-elysia/src/routes/log.routes.ts new file mode 100644 index 000000000..1ab124895 --- /dev/null +++ b/packages/server-elysia/src/routes/log.routes.ts @@ -0,0 +1,82 @@ +import type { ServerProviderDeps } from "@voltagent/core"; +import type { Logger } from "@voltagent/internal"; +import { handleGetLogs, mapLogResponse } from "@voltagent/server-core"; +import type { Elysia } from "elysia"; +import { t } from "elysia"; +import { ErrorSchema, LogResponseSchema } from "../schemas"; + +/** + * Log query parameters schema + */ +const LogQuerySchema = t.Object({ + limit: t.Optional( + t.Number({ + minimum: 1, + maximum: 1000, + description: "Maximum number of log entries to return", + }), + ), + level: t.Optional( + t.Union([t.Literal("debug"), t.Literal("info"), t.Literal("warn"), t.Literal("error")], { + description: "Filter by log level", + }), + ), + agentId: t.Optional(t.String({ description: "Filter by agent ID" })), + workflowId: t.Optional(t.String({ description: "Filter by workflow ID" })), + conversationId: t.Optional(t.String({ description: "Filter by conversation ID" })), + executionId: t.Optional(t.String({ description: "Filter by execution ID" })), + since: t.Optional( + t.String({ + description: "Return logs since this timestamp (ISO 8601 format)", + }), + ), + until: t.Optional( + t.String({ + description: "Return logs until this timestamp (ISO 8601 format)", + }), + ), +}); + +/** + * Register log routes with validation and OpenAPI documentation + */ +export function registerLogRoutes(app: Elysia, deps: ServerProviderDeps, logger: Logger): void { + // GET /api/logs - Get logs with filters + app.get( + "/api/logs", + async ({ query }) => { + const options = { + limit: query.limit ? Number(query.limit) : undefined, + level: query.level as "debug" | "info" | "warn" | "error" | undefined, + agentId: query.agentId as string | undefined, + workflowId: query.workflowId as string | undefined, + conversationId: query.conversationId as string | undefined, + executionId: query.executionId as string | undefined, + since: query.since, + until: query.until, + }; + + const response = await handleGetLogs(options, deps, logger); + + if (!response.success) { + throw new Error("Failed to get logs"); + } + + // Map the response to match the OpenAPI schema + const mappedResponse = mapLogResponse(response); + return mappedResponse; + }, + { + query: LogQuerySchema, + response: { + 200: LogResponseSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Get logs", + description: "Retrieve system logs with optional filtering", + tags: ["Logs"], + }, + }, + ); +} diff --git a/packages/server-elysia/src/routes/mcp.routes.spec.ts b/packages/server-elysia/src/routes/mcp.routes.spec.ts new file mode 100644 index 000000000..f4b1bf15d --- /dev/null +++ b/packages/server-elysia/src/routes/mcp.routes.spec.ts @@ -0,0 +1,25 @@ +import { Elysia } from "elysia"; +import { describe, expect, it, vi } from "vitest"; +import { registerMcpRoutes } from "./mcp.routes"; + +describe("MCP Routes", () => { + it("should register routes", () => { + const app = new Elysia(); + const deps = { + mcp: { + registry: {} as any, + }, + }; + + registerMcpRoutes(app, deps, console as any); + + // Check if routes are registered + // Elysia routes are stored in app.routes + expect(app.routes.length).toBeGreaterThan(0); + + // Verify some specific paths exist + const paths = app.routes.map((r) => r.path); + expect(paths).toContain("/mcp"); + expect(paths).toContain("/mcp/:serverId"); + }); +}); diff --git a/packages/server-elysia/src/routes/mcp.routes.ts b/packages/server-elysia/src/routes/mcp.routes.ts new file mode 100644 index 000000000..c73d0be04 --- /dev/null +++ b/packages/server-elysia/src/routes/mcp.routes.ts @@ -0,0 +1,372 @@ +import type { Logger } from "@voltagent/internal"; +import type { MCPServerRegistry } from "@voltagent/mcp-server"; +import { + handleGetMcpPrompt, + handleGetMcpResource, + handleGetMcpServer, + handleInvokeMcpServerTool, + handleListMcpPrompts, + handleListMcpResourceTemplates, + handleListMcpResources, + handleListMcpServerTools, + handleListMcpServers, + handleSetMcpLogLevel, +} from "@voltagent/server-core"; +import type { Elysia } from "elysia"; +import { t } from "elysia"; +import { + ErrorSchema, + McpPromptListSchema, + McpPromptResponseSchema, + McpResourceListSchema, + McpResourceResponseSchema, + McpResourceTemplateListSchema, + McpServerListSchema, + McpServerResponseSchema, + McpToolListSchema, + McpToolResponseSchema, +} from "../schemas"; + +interface McpDeps { + mcp?: { + registry: MCPServerRegistry; + }; +} + +/** + * MCP Server ID parameter schema + */ +const ServerIdParam = t.Object({ + serverId: t.String({ description: "The ID of the MCP server" }), +}); + +/** + * MCP Tool Name parameters schema + */ +const ToolNameParams = t.Object({ + serverId: t.String({ description: "The ID of the MCP server" }), + toolName: t.String({ description: "The name of the MCP tool" }), +}); + +/** + * MCP Prompt Name parameters schema + */ +const PromptNameParams = t.Object({ + serverId: t.String({ description: "The ID of the MCP server" }), + promptName: t.String({ description: "The name of the MCP prompt" }), +}); + +/** + * MCP Tool invocation request schema + */ +const McpInvokeToolRequestSchema = t.Object({ + arguments: t.Optional(t.Any({ description: "Tool arguments" })), + context: t.Optional( + t.Object({ + userId: t.Optional(t.String({ description: "User ID" })), + sessionId: t.Optional(t.String({ description: "Session ID" })), + metadata: t.Optional(t.Record(t.String(), t.Unknown())), + }), + ), +}); + +/** + * MCP Set log level request schema + */ +const McpSetLogLevelRequestSchema = t.Object({ + level: t.Union([t.Literal("debug"), t.Literal("info"), t.Literal("warn"), t.Literal("error")], { + description: "Log level to set for the MCP server", + }), +}); + +/** + * MCP Get prompt request schema (query parameters) + */ +const McpGetPromptQuery = t.Object({ + arguments: t.Optional(t.String({ description: "JSON-encoded arguments for the prompt" })), +}); + +/** + * MCP Resource URI query parameter schema + */ +const ResourceUriQuery = t.Object({ + uri: t.String({ description: "The URI of the resource to read" }), +}); + +/** + * Register MCP (Model Context Protocol) routes with validation and OpenAPI documentation + */ +export function registerMcpRoutes(app: Elysia, deps: McpDeps, logger: Logger): void { + if (!deps.mcp?.registry) { + logger.debug("MCP registry not available, skipping MCP routes"); + return; + } + + const registry = deps.mcp.registry; + + // GET /mcp - List all MCP servers + app.get( + "/mcp", + async () => { + const response = handleListMcpServers(registry); + if (!response.success) { + throw new Error("Failed to list MCP servers"); + } + return response; + }, + { + response: { + 200: McpServerListSchema, + 500: ErrorSchema, + }, + detail: { + summary: "List MCP servers", + description: "Retrieves a list of all registered Model Context Protocol servers", + tags: ["MCP"], + }, + }, + ); + + // GET /mcp/:serverId - Get MCP server details + app.get( + "/mcp/:serverId", + async ({ params }) => { + const response = handleGetMcpServer(registry, params.serverId); + if (!response.success) { + throw new Error("MCP server not found"); + } + return response; + }, + { + params: ServerIdParam, + response: { + 200: McpServerResponseSchema, + 404: ErrorSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Get MCP server details", + description: "Retrieves detailed information about a specific MCP server", + tags: ["MCP"], + }, + }, + ); + + // GET /mcp/:serverId/tools - List tools for an MCP server + app.get( + "/mcp/:serverId/tools", + async ({ params }) => { + const response = handleListMcpServerTools(registry, logger, params.serverId); + if (!response.success) { + throw new Error("Failed to list MCP tools"); + } + return response; + }, + { + params: ServerIdParam, + response: { + 200: McpToolListSchema, + 500: ErrorSchema, + }, + detail: { + summary: "List MCP server tools", + description: "Retrieves a list of all tools available on a specific MCP server", + tags: ["MCP"], + }, + }, + ); + + // POST /mcp/:serverId/tools/:toolName/invoke - Invoke an MCP tool + app.post( + "/mcp/:serverId/tools/:toolName/invoke", + async ({ params, body }) => { + const response = await handleInvokeMcpServerTool( + registry, + logger, + params.serverId, + params.toolName, + body, + ); + if (!response.success) { + throw new Error("Failed to invoke MCP tool"); + } + return response; + }, + { + params: ToolNameParams, + body: McpInvokeToolRequestSchema, + response: { + 200: McpToolResponseSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Invoke MCP tool", + description: "Executes a tool on a specific MCP server with the provided arguments", + tags: ["MCP"], + }, + }, + ); + + // POST /mcp/:serverId/logging/level - Set log level for MCP server + app.post( + "/mcp/:serverId/logging/level", + async ({ params, body }) => { + const response = await handleSetMcpLogLevel(registry, logger, params.serverId, body); + if (!response.success) { + throw new Error("Failed to set log level"); + } + return response; + }, + { + params: ServerIdParam, + body: McpSetLogLevelRequestSchema, + response: { + 200: t.Object({ success: t.Literal(true), data: t.Any() }), + 500: ErrorSchema, + }, + detail: { + summary: "Set MCP server log level", + description: "Changes the logging level for a specific MCP server", + tags: ["MCP"], + }, + }, + ); + + // GET /mcp/:serverId/prompts - List prompts for an MCP server + app.get( + "/mcp/:serverId/prompts", + async ({ params }) => { + const response = await handleListMcpPrompts(registry, logger, params.serverId); + if (!response.success) { + throw new Error("Failed to list MCP prompts"); + } + return response; + }, + { + params: ServerIdParam, + response: { + 200: McpPromptListSchema, + 500: ErrorSchema, + }, + detail: { + summary: "List MCP server prompts", + description: "Retrieves a list of all prompts available on a specific MCP server", + tags: ["MCP"], + }, + }, + ); + + // GET /mcp/:serverId/prompts/:promptName - Get a specific prompt + app.get( + "/mcp/:serverId/prompts/:promptName", + async ({ params, query, set }) => { + // Parse arguments from query string if present + let promptArgs: any; + if (query.arguments) { + try { + promptArgs = JSON.parse(query.arguments); + } catch (_error) { + set.status = 400; + return { success: false, error: "Invalid JSON in arguments parameter" }; + } + } + + const response = await handleGetMcpPrompt(registry, logger, params.serverId, { + name: params.promptName, + arguments: promptArgs, + }); + if (!response.success) { + throw new Error("Failed to get MCP prompt"); + } + return response; + }, + { + params: PromptNameParams, + query: McpGetPromptQuery, + response: { + 200: McpPromptResponseSchema, + 400: ErrorSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Get MCP prompt", + description: "Retrieves a specific prompt from an MCP server, optionally with arguments", + tags: ["MCP"], + }, + }, + ); + + // GET /mcp/:serverId/resources - List resources for an MCP server + app.get( + "/mcp/:serverId/resources", + async ({ params }) => { + const response = await handleListMcpResources(registry, logger, params.serverId); + if (!response.success) { + throw new Error("Failed to list MCP resources"); + } + return response; + }, + { + params: ServerIdParam, + response: { + 200: McpResourceListSchema, + 500: ErrorSchema, + }, + detail: { + summary: "List MCP server resources", + description: "Retrieves a list of all resources available on a specific MCP server", + tags: ["MCP"], + }, + }, + ); + + // GET /mcp/:serverId/resources/read - Read a specific resource + app.get( + "/mcp/:serverId/resources/read", + async ({ params, query }) => { + const response = await handleGetMcpResource(registry, logger, params.serverId, query.uri); + if (!response.success) { + throw new Error("Failed to read MCP resource"); + } + return response; + }, + { + params: ServerIdParam, + query: ResourceUriQuery, + response: { + 200: McpResourceResponseSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Read MCP resource", + description: "Reads a specific resource from an MCP server by URI", + tags: ["MCP"], + }, + }, + ); + + // GET /mcp/:serverId/resources/templates - List resource templates + app.get( + "/mcp/:serverId/resources/templates", + async ({ params }) => { + const response = await handleListMcpResourceTemplates(registry, logger, params.serverId); + if (!response.success) { + throw new Error("Failed to list MCP resource templates"); + } + return response; + }, + { + params: ServerIdParam, + response: { + 200: McpResourceTemplateListSchema, + 500: ErrorSchema, + }, + detail: { + summary: "List MCP resource templates", + description: + "Retrieves a list of all resource templates available on a specific MCP server", + tags: ["MCP"], + }, + }, + ); +} diff --git a/packages/server-elysia/src/routes/observability.spec.ts b/packages/server-elysia/src/routes/observability.spec.ts new file mode 100644 index 000000000..15ad67c1d --- /dev/null +++ b/packages/server-elysia/src/routes/observability.spec.ts @@ -0,0 +1,276 @@ +import * as serverCore from "@voltagent/server-core"; +import { Elysia } from "elysia"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerObservabilityRoutes } from "./observability"; + +// Mock server-core handlers +vi.mock("@voltagent/server-core", async () => { + const actual = await vi.importActual("@voltagent/server-core"); + return { + ...actual, + setupObservabilityHandler: vi.fn(), + getTracesHandler: vi.fn(), + getTraceByIdHandler: vi.fn(), + getSpanByIdHandler: vi.fn(), + getObservabilityStatusHandler: vi.fn(), + getLogsByTraceIdHandler: vi.fn(), + getLogsBySpanIdHandler: vi.fn(), + queryLogsHandler: vi.fn(), + getConversationMessagesHandler: vi.fn(), + getConversationStepsHandler: vi.fn(), + getWorkingMemoryHandler: vi.fn(), + listMemoryConversationsHandler: vi.fn(), + listMemoryUsersHandler: vi.fn(), + OBSERVABILITY_ROUTES: actual.OBSERVABILITY_ROUTES, + OBSERVABILITY_MEMORY_ROUTES: actual.OBSERVABILITY_MEMORY_ROUTES, + }; +}); + +describe("Observability Routes", () => { + let app: Elysia; + const mockDeps = {} as any; + const mockLogger = { + trace: vi.fn(), + error: vi.fn(), + } as any; + + beforeEach(() => { + app = new Elysia(); + registerObservabilityRoutes(app, mockDeps, mockLogger); + vi.clearAllMocks(); + }); + + it("should setup observability", async () => { + vi.mocked(serverCore.setupObservabilityHandler).mockResolvedValue({ success: true }); + + const response = await app.handle( + new Request("http://localhost/setup-observability", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ publicKey: "pk", secretKey: "sk" }), + }), + ); + + expect(response.status).toBe(200); + expect(serverCore.setupObservabilityHandler).toHaveBeenCalledWith( + { publicKey: "pk", secretKey: "sk" }, + mockDeps, + ); + }); + + it("should handle setup observability failure (missing keys)", async () => { + vi.mocked(serverCore.setupObservabilityHandler).mockResolvedValue({ + success: false, + error: "Missing keys", + }); + + const response = await app.handle( + new Request("http://localhost/setup-observability", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({}), + }), + ); + + expect(response.status).toBe(400); + }); + + it("should get traces", async () => { + vi.mocked(serverCore.getTracesHandler).mockResolvedValue({ success: true, traces: [] } as any); + + const response = await app.handle( + new Request("http://localhost/observability/traces?agentId=agent1"), + ); + + expect(response.status).toBe(200); + expect(serverCore.getTracesHandler).toHaveBeenCalledWith(mockDeps, { agentId: "agent1" }); + }); + + it("should get trace by id", async () => { + vi.mocked(serverCore.getTraceByIdHandler).mockResolvedValue({ + success: true, + trace: {}, + } as any); + + const response = await app.handle(new Request("http://localhost/observability/traces/123")); + + expect(response.status).toBe(200); + expect(serverCore.getTraceByIdHandler).toHaveBeenCalledWith("123", mockDeps); + }); + + it("should return 404 if trace not found", async () => { + vi.mocked(serverCore.getTraceByIdHandler).mockResolvedValue({ + success: false, + error: "Not found", + }); + + const response = await app.handle(new Request("http://localhost/observability/traces/123")); + + expect(response.status).toBe(404); + }); + + it("should get span by id", async () => { + vi.mocked(serverCore.getSpanByIdHandler).mockResolvedValue({ success: true, span: {} } as any); + + const response = await app.handle(new Request("http://localhost/observability/spans/456")); + + expect(response.status).toBe(200); + expect(serverCore.getSpanByIdHandler).toHaveBeenCalledWith("456", mockDeps); + }); + + it("should get observability status", async () => { + vi.mocked(serverCore.getObservabilityStatusHandler).mockResolvedValue({ + success: true, + configured: true, + } as any); + + const response = await app.handle(new Request("http://localhost/observability/status")); + + expect(response.status).toBe(200); + expect(serverCore.getObservabilityStatusHandler).toHaveBeenCalledWith(mockDeps); + }); + + it("should get logs by trace id", async () => { + vi.mocked(serverCore.getLogsByTraceIdHandler).mockResolvedValue({ + success: true, + logs: [], + } as any); + + const response = await app.handle( + new Request("http://localhost/observability/traces/123/logs"), + ); + + expect(response.status).toBe(200); + expect(serverCore.getLogsByTraceIdHandler).toHaveBeenCalledWith("123", mockDeps); + }); + + it("should get logs by span id", async () => { + vi.mocked(serverCore.getLogsBySpanIdHandler).mockResolvedValue({ + success: true, + logs: [], + } as any); + + const response = await app.handle(new Request("http://localhost/observability/spans/456/logs")); + + expect(response.status).toBe(200); + expect(serverCore.getLogsBySpanIdHandler).toHaveBeenCalledWith("456", mockDeps); + }); + + it("should query logs", async () => { + vi.mocked(serverCore.queryLogsHandler).mockResolvedValue({ success: true, logs: [] } as any); + + const response = await app.handle( + new Request("http://localhost/observability/logs?level=error"), + ); + + expect(response.status).toBe(200); + expect(serverCore.queryLogsHandler).toHaveBeenCalledWith({ level: "error" }, mockDeps); + }); + + it("should get conversation messages", async () => { + vi.mocked(serverCore.getConversationMessagesHandler).mockResolvedValue({ + success: true, + messages: [], + } as any); + + const response = await app.handle( + new Request("http://localhost/observability/memory/conversations/123/messages"), + ); + + expect(response.status).toBe(200); + expect(serverCore.getConversationMessagesHandler).toHaveBeenCalledWith( + mockDeps, + "123", + expect.objectContaining({ + agentId: undefined, + limit: undefined, + before: undefined, + after: undefined, + roles: undefined, + }), + ); + }); + + it("should get conversation steps", async () => { + vi.mocked(serverCore.getConversationStepsHandler).mockResolvedValue({ + success: true, + steps: [], + } as any); + + const response = await app.handle( + new Request("http://localhost/observability/memory/conversations/123/steps"), + ); + + expect(response.status).toBe(200); + expect(serverCore.getConversationStepsHandler).toHaveBeenCalledWith( + mockDeps, + "123", + expect.objectContaining({ + agentId: undefined, + limit: undefined, + operationId: undefined, + }), + ); + }); + + it("should get working memory", async () => { + vi.mocked(serverCore.getWorkingMemoryHandler).mockResolvedValue({ + success: true, + memory: {}, + } as any); + + const response = await app.handle( + new Request( + "http://localhost/observability/memory/working-memory?scope=conversation&agentId=agent1", + ), + ); + + expect(response.status).toBe(200); + expect(serverCore.getWorkingMemoryHandler).toHaveBeenCalledWith( + mockDeps, + expect.objectContaining({ + agentId: "agent1", + scope: "conversation", + }), + ); + }); + + it("should list memory conversations", async () => { + vi.mocked(serverCore.listMemoryConversationsHandler).mockResolvedValue({ + success: true, + conversations: [], + } as any); + + const response = await app.handle( + new Request("http://localhost/observability/memory/conversations?agentId=agent1"), + ); + + expect(response.status).toBe(200); + expect(serverCore.listMemoryConversationsHandler).toHaveBeenCalledWith( + mockDeps, + expect.objectContaining({ + agentId: "agent1", + }), + ); + }); + + it("should list memory users", async () => { + vi.mocked(serverCore.listMemoryUsersHandler).mockResolvedValue({ + success: true, + users: [], + } as any); + + const response = await app.handle(new Request("http://localhost/observability/memory/users")); + + expect(response.status).toBe(200); + expect(serverCore.listMemoryUsersHandler).toHaveBeenCalledWith( + mockDeps, + expect.objectContaining({ + agentId: undefined, + limit: undefined, + offset: undefined, + search: undefined, + }), + ); + }); +}); diff --git a/packages/server-elysia/src/routes/observability.ts b/packages/server-elysia/src/routes/observability.ts new file mode 100644 index 000000000..3dad8809e --- /dev/null +++ b/packages/server-elysia/src/routes/observability.ts @@ -0,0 +1,210 @@ +/** + * Observability route handlers for Elysia + */ + +import type { ServerProviderDeps } from "@voltagent/core"; +import type { Logger } from "@voltagent/internal"; +import { + OBSERVABILITY_MEMORY_ROUTES, + OBSERVABILITY_ROUTES, + getConversationMessagesHandler, + getConversationStepsHandler, + getLogsBySpanIdHandler, + getLogsByTraceIdHandler, + getObservabilityStatusHandler, + getSpanByIdHandler, + getTraceByIdHandler, + getTracesHandler, + getWorkingMemoryHandler, + listMemoryConversationsHandler, + listMemoryUsersHandler, + queryLogsHandler, + setupObservabilityHandler, +} from "@voltagent/server-core"; +import type { Elysia } from "elysia"; + +/** + * Register observability routes + */ +export function registerObservabilityRoutes(app: Elysia, deps: ServerProviderDeps, logger: Logger) { + // Setup observability configuration + app.post(OBSERVABILITY_ROUTES.setupObservability.path, async ({ body, set }) => { + const bodyData = body as { publicKey?: string; secretKey?: string }; + logger.trace("POST /setup-observability - configuring observability", { + hasKeys: !!(bodyData.publicKey && bodyData.secretKey), + }); + const result = await setupObservabilityHandler(bodyData, deps); + set.status = result.success ? 200 : result.error?.includes("Missing") ? 400 : 500; + return result; + }); + + // Get all traces with optional agentId filter + app.get(OBSERVABILITY_ROUTES.getTraces.path, async ({ query, set }) => { + logger.trace("GET /observability/traces - fetching traces", { query }); + const result = await getTracesHandler(deps, query); + set.status = result.success ? 200 : 500; + return result; + }); + + // Get specific trace by ID + app.get(OBSERVABILITY_ROUTES.getTraceById.path, async ({ params, set }) => { + const traceId = params.traceId; + logger.trace(`GET /observability/traces/${traceId} - fetching trace`); + const result = await getTraceByIdHandler(traceId, deps); + set.status = result.success ? 200 : 404; + return result; + }); + + // Get specific span by ID + app.get(OBSERVABILITY_ROUTES.getSpanById.path, async ({ params, set }) => { + const spanId = params.spanId; + logger.trace(`GET /observability/spans/${spanId} - fetching span`); + const result = await getSpanByIdHandler(spanId, deps); + set.status = result.success ? 200 : 404; + return result; + }); + + // Get observability status + app.get(OBSERVABILITY_ROUTES.getObservabilityStatus.path, async ({ set }) => { + logger.trace("GET /observability/status - fetching status"); + const result = await getObservabilityStatusHandler(deps); + set.status = result.success ? 200 : 500; + return result; + }); + + // Get logs by trace ID + app.get(OBSERVABILITY_ROUTES.getLogsByTraceId.path, async ({ params, set }) => { + const traceId = params.traceId; + logger.trace(`GET /observability/traces/${traceId}/logs - fetching logs`); + const result = await getLogsByTraceIdHandler(traceId, deps); + set.status = result.success ? 200 : 404; + return result; + }); + + // Get logs by span ID + app.get(OBSERVABILITY_ROUTES.getLogsBySpanId.path, async ({ params, set }) => { + const spanId = params.spanId; + logger.trace(`GET /observability/spans/${spanId}/logs - fetching logs`); + const result = await getLogsBySpanIdHandler(spanId, deps); + set.status = result.success ? 200 : 404; + return result; + }); + + // Query logs with filters + app.get(OBSERVABILITY_ROUTES.queryLogs.path, async ({ query, set }) => { + logger.trace("GET /observability/logs - querying logs", { query }); + const result = await queryLogsHandler(query, deps); + set.status = result.success ? 200 : 400; + return result; + }); + + // List memory users + app.get(OBSERVABILITY_MEMORY_ROUTES.listMemoryUsers.path, async ({ query, set }) => { + logger.trace("GET /observability/memory/users - fetching memory users", { query }); + const result = await listMemoryUsersHandler(deps, { + agentId: query.agentId, + limit: query.limit ? Number.parseInt(query.limit as string, 10) : undefined, + offset: query.offset ? Number.parseInt(query.offset as string, 10) : undefined, + search: query.search as string | undefined, + }); + set.status = result.success ? 200 : 500; + return result; + }); + + // List memory conversations + app.get(OBSERVABILITY_MEMORY_ROUTES.listMemoryConversations.path, async ({ query, set }) => { + logger.trace("GET /observability/memory/conversations - fetching conversations", { query }); + const result = await listMemoryConversationsHandler(deps, { + agentId: query.agentId as string | undefined, + userId: query.userId as string | undefined, + limit: query.limit ? Number.parseInt(query.limit as string, 10) : undefined, + offset: query.offset ? Number.parseInt(query.offset as string, 10) : undefined, + orderBy: query.orderBy as "created_at" | "updated_at" | "title" | undefined, + orderDirection: query.orderDirection as "ASC" | "DESC" | undefined, + }); + set.status = result.success ? 200 : 500; + return result; + }); + + // Get conversation messages + app.get( + OBSERVABILITY_MEMORY_ROUTES.getMemoryConversationMessages.path, + async ({ params, query, set }) => { + const conversationId = params.conversationId; + logger.trace( + `GET /observability/memory/conversations/${conversationId}/messages - fetching messages`, + { query }, + ); + + const before = query.before ? new Date(query.before as string) : undefined; + const after = query.after ? new Date(query.after as string) : undefined; + + const result = await getConversationMessagesHandler(deps, conversationId, { + agentId: query.agentId as string | undefined, + limit: query.limit ? Number.parseInt(query.limit as string, 10) : undefined, + before: before && !Number.isNaN(before.getTime()) ? before : undefined, + after: after && !Number.isNaN(after.getTime()) ? after : undefined, + roles: query.roles ? (query.roles as string).split(",") : undefined, + }); + + if (!result.success) { + set.status = result.error === "Conversation not found" ? 404 : 500; + return result; + } + + set.status = 200; + return result; + }, + ); + + // Get conversation steps + app.get(OBSERVABILITY_MEMORY_ROUTES.getConversationSteps.path, async ({ params, query, set }) => { + const conversationId = params.conversationId; + logger.trace( + `GET /observability/memory/conversations/${conversationId}/steps - fetching steps`, + { query }, + ); + + const result = await getConversationStepsHandler(deps, conversationId, { + agentId: query.agentId as string | undefined, + limit: query.limit ? Number.parseInt(query.limit as string, 10) : undefined, + operationId: query.operationId as string | undefined, + }); + + if (!result.success) { + set.status = result.error === "Conversation not found" ? 404 : 500; + return result; + } + + set.status = 200; + return result; + }); + + // Get working memory + app.get(OBSERVABILITY_MEMORY_ROUTES.getWorkingMemory.path, async ({ query, set }) => { + logger.trace("GET /observability/memory/working-memory - fetching working memory", { query }); + + const scope = + query.scope === "user" ? "user" : query.scope === "conversation" ? "conversation" : undefined; + + if (!scope) { + set.status = 400; + return { success: false, error: "Invalid scope. Expected 'conversation' or 'user'." }; + } + + const result = await getWorkingMemoryHandler(deps, { + agentId: query.agentId as string | undefined, + scope, + conversationId: query.conversationId as string | undefined, + userId: query.userId as string | undefined, + }); + + if (!result.success) { + set.status = result.error === "Working memory not found" ? 404 : 500; + return result; + } + + set.status = 200; + return result; + }); +} diff --git a/packages/server-elysia/src/routes/registration.spec.ts b/packages/server-elysia/src/routes/registration.spec.ts new file mode 100644 index 000000000..aee5e755d --- /dev/null +++ b/packages/server-elysia/src/routes/registration.spec.ts @@ -0,0 +1,52 @@ +import { Elysia } from "elysia"; +import { describe, expect, it, vi } from "vitest"; +import { registerA2ARoutes } from "./a2a.routes"; +import { registerObservabilityRoutes } from "./observability"; +import { registerTriggerRoutes } from "./trigger.routes"; + +describe("Route Registration", () => { + it("should register trigger routes", () => { + const app = new Elysia(); + const deps = { + triggerRegistry: { + list: vi.fn().mockReturnValue([{ path: "/trigger/test", method: "POST" }]), + }, + }; + + registerTriggerRoutes(app, deps as any, console as any); + + const paths = app.routes.map((r) => r.path); + expect(paths).toContain("/trigger/test"); + }); + + it("should register observability routes", () => { + const app = new Elysia(); + const deps = {}; + + registerObservabilityRoutes(app, deps as any, console as any); + + const paths = app.routes.map((r) => r.path); + // Check for some known observability routes + // Note: paths might be prefixed or exact depending on implementation + // Based on observability.ts, they seem to be absolute paths + expect(paths).toContain("/setup-observability"); + expect(paths).toContain("/observability/traces"); + }); + + it("should register A2A routes", () => { + const app = new Elysia(); + const deps = { + a2a: { + registry: { + list: () => [{ id: "test-server" }], + }, + }, + }; + + registerA2ARoutes(app, deps as any, console as any); + + const paths = app.routes.map((r) => r.path); + // Based on A2A_ROUTES log, it registers /a2a/:serverId + expect(paths).toContain("/a2a/:serverId"); + }); +}); diff --git a/packages/server-elysia/src/routes/tool.routes.ts b/packages/server-elysia/src/routes/tool.routes.ts new file mode 100644 index 000000000..53224fa11 --- /dev/null +++ b/packages/server-elysia/src/routes/tool.routes.ts @@ -0,0 +1,89 @@ +import type { ServerProviderDeps } from "@voltagent/core"; +import type { Logger } from "@voltagent/internal"; +import { handleExecuteTool, handleListTools } from "@voltagent/server-core"; +import type { Elysia } from "elysia"; +import { t } from "elysia"; +import { ErrorSchema, ToolExecutionResponseSchema, ToolListSchema } from "../schemas"; + +/** + * Tool name parameter schema + */ +const ToolNameParam = t.Object({ + name: t.String({ description: "The name of the tool to execute" }), +}); + +/** + * Tool execution request schema + */ +const ToolExecutionRequestSchema = t.Object({ + input: t.Optional(t.Any({ description: "Input parameters for the tool execution" })), + context: t.Optional( + t.Object({ + userId: t.Optional(t.String({ description: "User ID for context" })), + conversationId: t.Optional(t.String({ description: "Conversation ID for context" })), + metadata: t.Optional( + t.Record(t.String(), t.Unknown(), { + description: "Additional metadata for context", + }), + ), + }), + ), +}); + +/** + * Register tool routes with validation and OpenAPI documentation + */ +export function registerToolRoutes(app: Elysia, deps: ServerProviderDeps, logger: Logger): void { + // GET /tools - List all tools + app.get( + "/tools", + async ({ set }) => { + const response = await handleListTools(deps, logger); + if (!response.success) { + const { httpStatus, ...details } = response; + set.status = httpStatus || 500; + return details; + } + return response; + }, + { + response: { + 200: ToolListSchema, + 500: ErrorSchema, + }, + detail: { + summary: "List all tools", + description: + "Retrieves a list of all available tools from all registered agents and workflows", + tags: ["Tools"], + }, + }, + ); + + // POST /tools/:name/execute - Execute a tool + app.post( + "/tools/:name/execute", + async ({ params, body, set }) => { + const response = await handleExecuteTool(params.name, body, deps, logger); + if (!response.success) { + const { httpStatus, ...details } = response; + set.status = httpStatus || 500; + return details; + } + return response; + }, + { + params: ToolNameParam, + body: ToolExecutionRequestSchema, + response: { + 200: ToolExecutionResponseSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Execute tool", + description: "Executes a specific tool by name with the provided input parameters", + tags: ["Tools"], + }, + }, + ); +} diff --git a/packages/server-elysia/src/routes/trigger.routes.spec.ts b/packages/server-elysia/src/routes/trigger.routes.spec.ts new file mode 100644 index 000000000..a5311788b --- /dev/null +++ b/packages/server-elysia/src/routes/trigger.routes.spec.ts @@ -0,0 +1,101 @@ +import * as serverCore from "@voltagent/server-core"; +import { Elysia } from "elysia"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { registerTriggerRoutes } from "./trigger.routes"; + +// Mock server-core handlers +vi.mock("@voltagent/server-core", async () => { + const actual = await vi.importActual("@voltagent/server-core"); + return { + ...actual, + executeTriggerHandler: vi.fn(), + }; +}); + +describe("Trigger Routes", () => { + let app: Elysia; + const mockDeps = { + triggerRegistry: { + list: vi.fn(), + }, + } as any; + const mockLogger = { + info: vi.fn(), + warn: vi.fn(), + } as any; + + beforeEach(() => { + app = new Elysia(); + vi.clearAllMocks(); + }); + + it("should register and execute a POST trigger", async () => { + mockDeps.triggerRegistry.list.mockReturnValue([ + { name: "test-trigger", method: "POST", path: "/trigger/test" }, + ]); + vi.mocked(serverCore.executeTriggerHandler).mockResolvedValue({ + status: 200, + body: { success: true }, + headers: {}, + }); + + registerTriggerRoutes(app, mockDeps, mockLogger); + + const response = await app.handle( + new Request("http://localhost/trigger/test", { + method: "POST", + headers: { "Content-Type": "application/json", "X-Custom": "value" }, + body: JSON.stringify({ data: "test" }), + }), + ); + + expect(response.status).toBe(200); + expect(await response.json()).toEqual({ success: true }); + expect(serverCore.executeTriggerHandler).toHaveBeenCalledWith( + expect.objectContaining({ name: "test-trigger" }), + expect.objectContaining({ + body: { data: "test" }, + headers: expect.objectContaining({ "x-custom": "value" }), + }), + mockDeps, + mockLogger, + ); + }); + + it("should register and execute a GET trigger", async () => { + mockDeps.triggerRegistry.list.mockReturnValue([ + { name: "get-trigger", method: "GET", path: "/trigger/get" }, + ]); + vi.mocked(serverCore.executeTriggerHandler).mockResolvedValue({ + status: 200, + body: { success: true }, + headers: {}, + }); + + registerTriggerRoutes(app, mockDeps, mockLogger); + + const response = await app.handle(new Request("http://localhost/trigger/get?param=value")); + + expect(response.status).toBe(200); + expect(serverCore.executeTriggerHandler).toHaveBeenCalledWith( + expect.objectContaining({ name: "get-trigger" }), + expect.objectContaining({ + query: { param: "value" }, + }), + mockDeps, + mockLogger, + ); + }); + + it("should skip unsupported methods", async () => { + mockDeps.triggerRegistry.list.mockReturnValue([ + { name: "bad-trigger", method: "HEAD", path: "/trigger/bad" }, + ]); + + registerTriggerRoutes(app, mockDeps, mockLogger); + + expect(mockLogger.warn).toHaveBeenCalledWith( + expect.stringContaining("method head not supported"), + ); + }); +}); diff --git a/packages/server-elysia/src/routes/trigger.routes.ts b/packages/server-elysia/src/routes/trigger.routes.ts new file mode 100644 index 000000000..19d4b0214 --- /dev/null +++ b/packages/server-elysia/src/routes/trigger.routes.ts @@ -0,0 +1,83 @@ +import type { ServerProviderDeps } from "@voltagent/core"; +import type { Logger } from "@voltagent/internal"; +import { type TriggerHttpRequestContext, executeTriggerHandler } from "@voltagent/server-core"; +import type { Elysia } from "elysia"; + +function extractHeaders(headers: Record): Record { + const result: Record = {}; + for (const [key, value] of Object.entries(headers)) { + if (value !== undefined) { + result[key] = value; + } + } + return result; +} + +function extractQuery( + query: Record, +): Record { + const result: Record = {}; + for (const [key, value] of Object.entries(query)) { + if (value !== undefined) { + result[key] = value; + } + } + return result; +} + +/** + * Register trigger routes dynamically based on the trigger registry + */ +export function registerTriggerRoutes(app: Elysia, deps: ServerProviderDeps, logger: Logger): void { + const triggers = deps.triggerRegistry.list(); + + for (const trigger of triggers) { + const method = (trigger.method ?? "post").toLowerCase(); + const handler = async ({ body, headers, query, request }: any) => { + const context: TriggerHttpRequestContext = { + body, + headers: extractHeaders(headers), + query: extractQuery(query), + raw: request, + }; + + const response = await executeTriggerHandler(trigger, context, deps, logger); + + return new Response(JSON.stringify(response.body ?? { success: true }), { + status: response.status, + headers: { + "Content-Type": "application/json", + ...response.headers, + }, + }); + }; + + // Register the route with the appropriate HTTP method + switch (method) { + case "get": + app.get(trigger.path, handler); + break; + case "post": + app.post(trigger.path, handler); + break; + case "put": + app.put(trigger.path, handler); + break; + case "delete": + app.delete(trigger.path, handler); + break; + case "patch": + app.patch(trigger.path, handler); + break; + default: + logger.warn(`Skipping trigger ${trigger.name}: method ${method} not supported`); + continue; + } + + logger.info("[volt] Trigger route registered", { + trigger: trigger.name, + method: method.toUpperCase(), + path: trigger.path, + }); + } +} diff --git a/packages/server-elysia/src/routes/update.routes.ts b/packages/server-elysia/src/routes/update.routes.ts new file mode 100644 index 000000000..d8f83bc2c --- /dev/null +++ b/packages/server-elysia/src/routes/update.routes.ts @@ -0,0 +1,108 @@ +import type { ServerProviderDeps } from "@voltagent/core"; +import type { Logger } from "@voltagent/internal"; +import { UPDATE_ROUTES, handleCheckUpdates, handleInstallUpdates } from "@voltagent/server-core"; +import type { Elysia } from "elysia"; +import { t } from "elysia"; +import { ErrorSchema, UpdateCheckResponseSchema, UpdateInstallResponseSchema } from "../schemas"; + +/** + * Package name parameter schema + */ +const PackageNameParam = t.Object({ + packageName: t.String({ description: "Name of the package to update" }), +}); + +/** + * Install updates request schema + */ +const InstallUpdatesRequestSchema = t.Object({ + packageName: t.Optional(t.String({ description: "Optional specific package name to update" })), +}); + +/** + * Register update routes with validation and OpenAPI documentation + */ +export function registerUpdateRoutes(app: Elysia, deps: ServerProviderDeps, logger: Logger): void { + // GET /updates - Check for updates + app.get( + UPDATE_ROUTES.checkUpdates.path, + async () => { + const response = await handleCheckUpdates(deps, logger); + if (!response.success) { + throw new Error("Failed to check updates"); + } + return response; + }, + { + response: { + 200: UpdateCheckResponseSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Check for updates", + description: "Checks for available updates for VoltAgent packages and dependencies", + tags: ["Updates"], + }, + }, + ); + + // POST /updates - Install updates + app.post( + UPDATE_ROUTES.installUpdates.path, + async ({ body }) => { + let packageName: string | undefined; + + try { + const b = body as { packageName?: unknown }; + if (typeof b?.packageName === "string") { + packageName = b.packageName; + } + } catch (error) { + logger.warn("Failed to parse update install request body", { error }); + } + + const response = await handleInstallUpdates(packageName, deps, logger); + if (!response.success) { + throw new Error("Failed to install updates"); + } + return response; + }, + { + body: InstallUpdatesRequestSchema, + response: { + 200: UpdateInstallResponseSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Install updates", + description: + "Installs available updates for all packages or a specific package if specified", + tags: ["Updates"], + }, + }, + ); + + // POST /updates/:packageName - Install single package update + app.post( + UPDATE_ROUTES.installSingleUpdate.path, + async ({ params }) => { + const response = await handleInstallUpdates(params.packageName, deps, logger); + if (!response.success) { + throw new Error("Failed to install package update"); + } + return response; + }, + { + params: PackageNameParam, + response: { + 200: UpdateInstallResponseSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Install specific package update", + description: "Installs an update for a specific package by name", + tags: ["Updates"], + }, + }, + ); +} diff --git a/packages/server-elysia/src/routes/workflow.routes.ts b/packages/server-elysia/src/routes/workflow.routes.ts new file mode 100644 index 000000000..7b9d78d79 --- /dev/null +++ b/packages/server-elysia/src/routes/workflow.routes.ts @@ -0,0 +1,341 @@ +import type { ServerProviderDeps } from "@voltagent/core"; +import type { Logger } from "@voltagent/internal"; +import { + handleCancelWorkflow, + handleExecuteWorkflow, + handleGetWorkflow, + handleGetWorkflowState, + handleGetWorkflows, + handleListWorkflowRuns, + handleResumeWorkflow, + handleStreamWorkflow, + handleSuspendWorkflow, + isErrorResponse, +} from "@voltagent/server-core"; +import type { Elysia } from "elysia"; +import { t } from "elysia"; +import { + ErrorSchema, + WorkflowCancelRequestSchema, + WorkflowCancelResponseSchema, + WorkflowExecutionRequestSchema, + WorkflowExecutionResponseSchema, + WorkflowListSchema, + WorkflowResponseSchema, + WorkflowResumeRequestSchema, + WorkflowResumeResponseSchema, + WorkflowSuspendRequestSchema, + WorkflowSuspendResponseSchema, +} from "../schemas"; + +/** + * Workflow list query parameters schema + */ +const WorkflowListQuerySchema = t.Object({ + workflowId: t.Optional(t.String({ description: "Filter by workflow ID" })), + status: t.Optional( + t.Union( + [ + t.Literal("pending"), + t.Literal("running"), + t.Literal("success"), + t.Literal("error"), + t.Literal("suspended"), + t.Literal("cancelled"), + ], + { description: "Filter by execution status" }, + ), + ), + limit: t.Optional( + t.Number({ + minimum: 1, + maximum: 100, + description: "Maximum number of executions to return", + }), + ), + offset: t.Optional(t.Number({ minimum: 0, description: "Number of executions to skip" })), +}); + +/** + * Workflow ID parameter schema + */ +const WorkflowIdParam = t.Object({ + id: t.String({ description: "Workflow ID" }), +}); + +/** + * Execution ID parameter schema + */ +const ExecutionIdParam = t.Object({ + executionId: t.String({ description: "Workflow execution ID" }), +}); + +/** + * Workflow and execution ID parameters schema + */ +const WorkflowExecutionParams = t.Object({ + id: t.String({ description: "Workflow ID" }), + executionId: t.String({ description: "Workflow execution ID" }), +}); + +/** + * Register workflow routes with validation and OpenAPI documentation + */ +export function registerWorkflowRoutes( + app: Elysia, + deps: ServerProviderDeps, + logger: Logger, +): void { + // GET /workflows - List all workflows + app.get( + "/workflows", + async () => { + const response = await handleGetWorkflows(deps, logger); + if (!response.success) { + throw new Error("Failed to get workflows"); + } + return response; + }, + { + response: { + 200: t.Object({ + success: t.Literal(true), + data: WorkflowListSchema, + }), + 500: ErrorSchema, + }, + detail: { + summary: "List all workflows", + description: "Retrieves a list of all available workflows", + tags: ["Workflows"], + }, + }, + ); + + // GET /workflows/executions - List workflow executions (query-driven) + app.get( + "/workflows/executions", + async ({ query }) => { + const response = await handleListWorkflowRuns(undefined, query as any, deps, logger); + if (!response.success) { + throw new Error(response.error || "Failed to list workflow runs"); + } + return response; + }, + { + query: WorkflowListQuerySchema, + detail: { + summary: "List workflow executions", + description: "Retrieves a paginated list of workflow executions with optional filters", + tags: ["Workflows"], + }, + }, + ); + + // GET /workflows/:id - Get workflow by ID + app.get( + "/workflows/:id", + async ({ params }) => { + const response = await handleGetWorkflow(params.id, deps, logger); + if (!response.success) { + throw new Error("Workflow not found"); + } + return response; + }, + { + params: WorkflowIdParam, + response: { + 200: t.Object({ + success: t.Literal(true), + data: WorkflowResponseSchema, + }), + 404: ErrorSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Get workflow by ID", + description: "Retrieves detailed information about a specific workflow", + tags: ["Workflows"], + }, + }, + ); + + // POST /workflows/:id/execute - Execute workflow + app.post( + "/workflows/:id/execute", + async ({ params, body }) => { + const response = await handleExecuteWorkflow(params.id, body, deps, logger); + if (!response.success) { + throw new Error("Failed to execute workflow"); + } + return response; + }, + { + params: WorkflowIdParam, + body: WorkflowExecutionRequestSchema, + response: { + 200: WorkflowExecutionResponseSchema, + 404: ErrorSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Execute workflow", + description: "Starts a new workflow execution with the provided input data", + tags: ["Workflows"], + }, + }, + ); + + // POST /workflows/:id/stream - Stream workflow execution + app.post( + "/workflows/:id/stream", + async ({ params, body, set }) => { + const response = await handleStreamWorkflow(params.id, body, deps, logger); + + // Check if it's an error response + if (isErrorResponse(response)) { + throw new Error("Failed to stream workflow"); + } + + // Return the ReadableStream with proper headers + set.headers["Content-Type"] = "text/event-stream"; + set.headers["Cache-Control"] = "no-cache"; + set.headers.Connection = "keep-alive"; + return response; + }, + { + params: WorkflowIdParam, + body: WorkflowExecutionRequestSchema, + detail: { + summary: "Stream workflow execution", + description: + "Executes a workflow and streams execution events in real-time via Server-Sent Events (SSE)", + tags: ["Workflows"], + }, + }, + ); + + // POST /workflows/executions/:executionId/suspend - Suspend workflow execution + app.post( + "/workflows/executions/:executionId/suspend", + async ({ params, body }) => { + const response = await handleSuspendWorkflow(params.executionId, body, deps, logger); + if (!response.success) { + throw new Error("Failed to suspend workflow"); + } + return response; + }, + { + params: ExecutionIdParam, + body: WorkflowSuspendRequestSchema, + response: { + 200: WorkflowSuspendResponseSchema, + 404: ErrorSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Suspend workflow execution", + description: "Suspends a running workflow execution, allowing it to be resumed later", + tags: ["Workflows"], + }, + }, + ); + + // POST /workflows/executions/:executionId/cancel - Cancel workflow execution + app.post( + "/workflows/executions/:executionId/cancel", + async ({ params, body }) => { + const response = await handleCancelWorkflow(params.executionId, body, deps, logger); + if (!response.success) { + const errorMessage = response.error || ""; + if (errorMessage.includes("not found")) { + throw new Error("Execution not found"); + } + if (errorMessage.includes("not cancellable")) { + throw new Error("Execution is not in a cancellable state"); + } + throw new Error("Failed to cancel workflow"); + } + return response; + }, + { + params: ExecutionIdParam, + body: WorkflowCancelRequestSchema, + response: { + 200: WorkflowCancelResponseSchema, + 404: ErrorSchema, + 409: ErrorSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Cancel workflow execution", + description: "Cancels a running or suspended workflow execution permanently", + tags: ["Workflows"], + }, + }, + ); + + // POST /workflows/:id/executions/:executionId/resume - Resume workflow execution + app.post( + "/workflows/:id/executions/:executionId/resume", + async ({ params, body }) => { + const response = await handleResumeWorkflow( + params.id, + params.executionId, + body, + deps, + logger, + ); + if (!response.success) { + throw new Error("Failed to resume workflow"); + } + return response; + }, + { + params: WorkflowExecutionParams, + body: WorkflowResumeRequestSchema, + response: { + 200: WorkflowResumeResponseSchema, + 404: ErrorSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Resume workflow execution", + description: "Resumes a suspended workflow execution, optionally providing resume data", + tags: ["Workflows"], + }, + }, + ); + + // GET /workflows/:id/executions/:executionId/state - Get workflow execution state + app.get( + "/workflows/:id/executions/:executionId/state", + async ({ params }) => { + const response = await handleGetWorkflowState(params.id, params.executionId, deps, logger); + if (!response.success) { + if (response.error?.includes("not found")) { + throw new Error("Execution not found"); + } + throw new Error("Failed to get workflow state"); + } + return response; + }, + { + params: WorkflowExecutionParams, + response: { + 200: t.Object({ + success: t.Literal(true), + data: t.Any(), // State can be anything + }), + 404: ErrorSchema, + 500: ErrorSchema, + }, + detail: { + summary: "Get workflow execution state", + description: + "Retrieves the current state of a workflow execution, including status and step information", + tags: ["Workflows"], + }, + }, + ); +} diff --git a/packages/server-elysia/src/schemas.ts b/packages/server-elysia/src/schemas.ts new file mode 100644 index 000000000..0d32522eb --- /dev/null +++ b/packages/server-elysia/src/schemas.ts @@ -0,0 +1,144 @@ +import { + AgentListSchema as ZodAgentListSchema, + AgentResponseSchema as ZodAgentResponseSchema, + ErrorSchema as ZodErrorSchema, + GenerateOptionsSchema as ZodGenerateOptionsSchema, + ObjectRequestSchema as ZodObjectRequestSchema, + ObjectResponseSchema as ZodObjectResponseSchema, + TextRequestSchema as ZodTextRequestSchema, + TextResponseSchema as ZodTextResponseSchema, + WorkflowCancelRequestSchema as ZodWorkflowCancelRequestSchema, + WorkflowCancelResponseSchema as ZodWorkflowCancelResponseSchema, + WorkflowExecutionRequestSchema as ZodWorkflowExecutionRequestSchema, + WorkflowExecutionResponseSchema as ZodWorkflowExecutionResponseSchema, + WorkflowListSchema as ZodWorkflowListSchema, + WorkflowResponseSchema as ZodWorkflowResponseSchema, + WorkflowResumeRequestSchema as ZodWorkflowResumeRequestSchema, + WorkflowResumeResponseSchema as ZodWorkflowResumeResponseSchema, + WorkflowSuspendRequestSchema as ZodWorkflowSuspendRequestSchema, + WorkflowSuspendResponseSchema as ZodWorkflowSuspendResponseSchema, +} from "@voltagent/server-core"; +import { t } from "elysia"; +import { zodToTypeBox } from "./utils/zod-adapter"; + +// Common schemas +export const ErrorSchema = zodToTypeBox(ZodErrorSchema); + +// Agent schemas +export const AgentResponseSchema = zodToTypeBox(ZodAgentResponseSchema); +export const AgentListSchema = zodToTypeBox(ZodAgentListSchema); + +// Generation options schema +export const GenerateOptionsSchema = zodToTypeBox(ZodGenerateOptionsSchema); + +// Text generation schemas +export const TextRequestSchema = zodToTypeBox(ZodTextRequestSchema); +export const TextResponseSchema = zodToTypeBox(ZodTextResponseSchema); + +// Object generation schemas +export const ObjectRequestSchema = zodToTypeBox(ZodObjectRequestSchema); +export const ObjectResponseSchema = zodToTypeBox(ZodObjectResponseSchema); + +// Workflow schemas +export const WorkflowResponseSchema = zodToTypeBox(ZodWorkflowResponseSchema); +export const WorkflowListSchema = zodToTypeBox(ZodWorkflowListSchema); +export const WorkflowExecutionRequestSchema = zodToTypeBox(ZodWorkflowExecutionRequestSchema); +export const WorkflowExecutionResponseSchema = zodToTypeBox(ZodWorkflowExecutionResponseSchema); +export const WorkflowSuspendRequestSchema = zodToTypeBox(ZodWorkflowSuspendRequestSchema); +export const WorkflowSuspendResponseSchema = zodToTypeBox(ZodWorkflowSuspendResponseSchema); +export const WorkflowCancelRequestSchema = zodToTypeBox(ZodWorkflowCancelRequestSchema); +export const WorkflowCancelResponseSchema = zodToTypeBox(ZodWorkflowCancelResponseSchema); +export const WorkflowResumeRequestSchema = zodToTypeBox(ZodWorkflowResumeRequestSchema); +export const WorkflowResumeResponseSchema = zodToTypeBox(ZodWorkflowResumeResponseSchema); + +// Update schemas +export const UpdateCheckResponseSchema = t.Object({ + success: t.Literal(true), + data: t.Any({ description: "Update information" }), +}); + +export const UpdateInstallResponseSchema = t.Object({ + success: t.Literal(true), + data: t.Object({ + message: t.String(), + }), +}); + +// Log schemas +export const LogResponseSchema = t.Object({ + success: t.Literal(true), + data: t.Array(t.Any()), + total: t.Number(), + query: t.Any(), +}); + +// MCP schemas +export const McpServerListSchema = t.Object({ + success: t.Literal(true), + data: t.Object({ + servers: t.Array(t.Any()), + }), +}); + +export const McpServerResponseSchema = t.Object({ + success: t.Literal(true), + data: t.Any(), +}); + +export const McpToolListSchema = t.Object({ + success: t.Literal(true), + data: t.Object({ + server: t.Any(), + tools: t.Array(t.Any()), + }), +}); + +export const McpToolResponseSchema = t.Object({ + success: t.Literal(true), + data: t.Any(), +}); + +export const McpResourceListSchema = t.Object({ + success: t.Literal(true), + data: t.Object({ + resources: t.Array(t.Any()), + }), +}); + +export const McpResourceResponseSchema = t.Object({ + success: t.Literal(true), + data: t.Any(), +}); + +export const McpResourceTemplateListSchema = t.Object({ + success: t.Literal(true), + data: t.Object({ + resourceTemplates: t.Array(t.Any()), + }), +}); + +export const McpPromptListSchema = t.Object({ + success: t.Literal(true), + data: t.Object({ + prompts: t.Array(t.Any()), + }), +}); + +export const McpPromptResponseSchema = t.Object({ + success: t.Literal(true), + data: t.Any(), +}); + +// A2A schemas +export const A2AResponseSchema = t.Any({ description: "JSON-RPC response" }); + +// Tool schemas +export const ToolListSchema = t.Object({ + success: t.Literal(true), + data: t.Array(t.Any()), +}); + +export const ToolExecutionResponseSchema = t.Object({ + success: t.Literal(true), + data: t.Any(), +}); diff --git a/packages/server-elysia/src/types.ts b/packages/server-elysia/src/types.ts new file mode 100644 index 000000000..a06787052 --- /dev/null +++ b/packages/server-elysia/src/types.ts @@ -0,0 +1,146 @@ +import type { AuthNextConfig, AuthProvider } from "@voltagent/server-core"; +import type { Elysia } from "elysia"; + +type CORSOptions = { + origin?: string | string[] | ((origin: string) => string | undefined | null); + allowMethods?: string[]; + allowHeaders?: string[]; + maxAge?: number; + credentials?: boolean; + exposeHeaders?: string[]; +}; + +export interface ElysiaServerConfig { + port?: number; + + enableSwaggerUI?: boolean; + + /** + * Hostname to bind the server to + * @default "0.0.0.0" - Binds to all IPv4 interfaces + * @example + * ```typescript + * // Bind to all IPv4 interfaces (default) + * hostname: "0.0.0.0" + * + * // Bind to IPv6 and IPv4 (dual-stack) + * hostname: "::" + * + * // Bind to localhost only + * hostname: "127.0.0.1" + * ``` + */ + hostname?: string; + + /** + * CORS configuration options + * + * Set to `false` to disable default CORS middleware and configure route-specific CORS in configureApp + * + * @default Allows all origins (*) + * @example + * ```typescript + * // Global CORS (default approach) + * cors: { + * origin: "http://example.com", + * allowHeaders: ["X-Custom-Header", "Content-Type"], + * allowMethods: ["POST", "GET", "OPTIONS"], + * maxAge: 600, + * credentials: true, + * } + * + * // Disable default CORS for route-specific control + * cors: false, + * configureApp: (app) => { + * app.use(cors({ origin: "https://agents.com" })); + * } + * ``` + */ + cors?: CORSOptions | false; + + /** + * Configure the Elysia app with custom routes, middleware, and plugins. + * This gives you full access to the Elysia app instance to register + * routes and middleware using Elysia's native API. + * + * NOTE: Custom routes added via configureApp are protected by the auth middleware + * if one is configured (auth/authNext). Routes are registered AFTER authentication middleware. + * + * @example + * ```typescript + * configureApp: (app) => { + * // Add custom routes (will be auth-protected if auth/authNext is set) + * app.get('/health', () => ({ status: 'ok' })); + * + * // Add middleware + * app.use(customPlugin); + * + * // Use route groups + * app.group('/api/v2', (app) => + * app.get('/users', getUsersHandler) + * ); + * } + * ``` + */ + configureApp?: (app: Elysia) => void | Promise; + + /** + * Full app configuration that provides access to app, routes, and middlewares. + * When this is set, configureApp will not be executed. + * This allows you to control the exact order of route and middleware registration. + * + * @example + * ```typescript + * configureFullApp: ({ app, routes, middlewares }) => { + * // Apply middleware first + * middlewares.cors(); + * middlewares.auth(); + * + * // Register routes in custom order + * routes.agents(); + * + * // Add custom routes + * app.get('/custom', () => ({ custom: true })); + * + * // Register remaining routes + * routes.workflows(); + * routes.doc(); + * } + * ``` + */ + configureFullApp?: (options: { + app: Elysia; + routes: Record void>; + middlewares: Record void>; + }) => void | Promise; + + /** + * @deprecated Use `authNext` instead for better security and flexibility. + * Legacy authentication provider configuration. + */ + auth?: AuthProvider; + + /** + * Authentication configuration with support for public routes and console access. + * All routes are protected by default unless specified in publicRoutes. + * + * @example + * ```typescript + * authNext: { + * provider: jwtAuth({ secret: process.env.JWT_SECRET! }), + * publicRoutes: ["GET /health", "POST /webhooks/*"], + * } + * ``` + */ + authNext?: AuthNextConfig; + + /** + * Enable WebSocket support (default: true) + */ + enableWebSocket?: boolean; + + /** + * WebSocket path prefix (default: "/ws") + */ + websocketPath?: string; +} diff --git a/packages/server-elysia/src/utils/custom-endpoints.spec.ts b/packages/server-elysia/src/utils/custom-endpoints.spec.ts new file mode 100644 index 000000000..497e65562 --- /dev/null +++ b/packages/server-elysia/src/utils/custom-endpoints.spec.ts @@ -0,0 +1,647 @@ +/** + * Unit tests for custom endpoints extraction and OpenAPI enhancement + */ + +import type { ServerEndpointSummary } from "@voltagent/server-core"; +import { Elysia } from "elysia"; +import { describe, expect, it } from "vitest"; +import { extractCustomEndpoints, getEnhancedOpenApiDoc } from "./custom-endpoints"; + +/** + * Create a mock Elysia app for testing + */ +function createMockApp(options: { + routes?: Array<{ method: string; path: string }>; + shouldThrow?: boolean; +}) { + return { + routes: options.routes || [], + } as any; +} + +describe("extractCustomEndpoints", () => { + describe("Elysia routes extraction", () => { + it("should extract routes from app", () => { + const app = new Elysia() + .get("/api/health", () => ({ status: "ok" })) + .post("/api/calculate", () => ({ result: 42 })); + + const endpoints = extractCustomEndpoints(app as any); + + expect(endpoints.length).toBeGreaterThanOrEqual(2); + + const healthEndpoint = endpoints.find((e) => e.path === "/api/health"); + const calculateEndpoint = endpoints.find((e) => e.path === "/api/calculate"); + + expect(healthEndpoint).toBeDefined(); + expect(healthEndpoint?.method).toBe("GET"); + expect(healthEndpoint?.group).toBe("Custom Endpoints"); + + expect(calculateEndpoint).toBeDefined(); + expect(calculateEndpoint?.method).toBe("POST"); + }); + + it("should normalize paths by removing duplicate slashes", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "/api//health" }], + }); + + const endpoints = extractCustomEndpoints(app); + + expect(endpoints).toHaveLength(1); + expect(endpoints[0].path).toBe("/api/health"); + }); + + it("should deduplicate routes", () => { + const app = createMockApp({ + routes: [ + { method: "GET", path: "/api/health" }, + { method: "GET", path: "/api/health" }, // Duplicate + ], + }); + + const endpoints = extractCustomEndpoints(app); + + expect(endpoints).toHaveLength(1); + }); + + it("should handle routes with path parameters", () => { + const app = new Elysia() + .get("/api/users/:id", () => ({ user: "test" })) + .get("/api/posts/:postId/comments/:commentId", () => ({ comment: "test" })); + + const endpoints = extractCustomEndpoints(app as any); + + expect(endpoints.length).toBeGreaterThanOrEqual(2); + + const userEndpoint = endpoints.find((e) => e.path === "/api/users/:id"); + const commentEndpoint = endpoints.find( + (e) => e.path === "/api/posts/:postId/comments/:commentId", + ); + + expect(userEndpoint).toBeDefined(); + expect(commentEndpoint).toBeDefined(); + }); + + it("should handle empty app", () => { + const app = new Elysia(); + + const endpoints = extractCustomEndpoints(app as any); + + expect(endpoints).toEqual([]); + }); + + it("should handle app with grouped routes", () => { + const app = new Elysia().group("/api", (app) => + app.get("/health", () => ({ status: "ok" })).post("/submit", () => ({ submitted: true })), + ); + + const endpoints = extractCustomEndpoints(app as any); + + expect(endpoints.length).toBeGreaterThanOrEqual(2); + + // Routes in groups should have the full path + const healthEndpoint = endpoints.find((e) => e.path?.includes("/health")); + const submitEndpoint = endpoints.find((e) => e.path?.includes("/submit")); + + expect(healthEndpoint).toBeDefined(); + expect(submitEndpoint).toBeDefined(); + }); + + it("should include custom group label", () => { + const app = new Elysia().get("/custom", () => ({ custom: true })); + + const endpoints = extractCustomEndpoints(app as any); + + expect(endpoints.length).toBeGreaterThanOrEqual(1); + expect(endpoints[0].group).toBe("Custom Endpoints"); + }); + + it("should handle different HTTP methods", () => { + const app = new Elysia() + .get("/test", () => ({})) + .post("/test", () => ({})) + .put("/test", () => ({})) + .delete("/test", () => ({})) + .patch("/test", () => ({})); + + const endpoints = extractCustomEndpoints(app as any); + + const methods = endpoints.map((e) => e.method); + expect(methods).toContain("GET"); + expect(methods).toContain("POST"); + expect(methods).toContain("PUT"); + expect(methods).toContain("DELETE"); + expect(methods).toContain("PATCH"); + }); + }); + + describe("Built-in route filtering", () => { + it("should filter out built-in agent routes", () => { + const app = createMockApp({ + routes: [ + { method: "GET", path: "/agents" }, + { method: "GET", path: "/agents/:id" }, + { method: "POST", path: "/agents/:id/text" }, + { method: "GET", path: "/api/custom" }, // Custom route + ], + }); + + const endpoints = extractCustomEndpoints(app); + + expect(endpoints).toHaveLength(1); + expect(endpoints[0].path).toBe("/api/custom"); + }); + + it("should filter out built-in workflow routes", () => { + const app = createMockApp({ + routes: [ + { method: "GET", path: "/workflows" }, + { method: "GET", path: "/workflows/:id" }, + { method: "POST", path: "/workflows/:id/execute" }, + { method: "GET", path: "/api/custom" }, // Custom route + ], + }); + + const endpoints = extractCustomEndpoints(app); + + expect(endpoints).toHaveLength(1); + expect(endpoints[0].path).toBe("/api/custom"); + }); + + it("should filter out observability routes", () => { + const app = createMockApp({ + routes: [ + { method: "GET", path: "/observability/traces" }, + { method: "GET", path: "/observability/spans/:spanId" }, + { method: "GET", path: "/api/custom" }, // Custom route + ], + }); + + const endpoints = extractCustomEndpoints(app); + + expect(endpoints).toHaveLength(1); + expect(endpoints[0].path).toBe("/api/custom"); + }); + + it("should filter out core routes (/, /doc, /ui)", () => { + const app = createMockApp({ + routes: [ + { method: "GET", path: "/" }, + { method: "GET", path: "/doc" }, + { method: "GET", path: "/ui" }, + { method: "GET", path: "/api/custom" }, // Custom route + ], + }); + + const endpoints = extractCustomEndpoints(app); + + expect(endpoints).toHaveLength(1); + expect(endpoints[0].path).toBe("/api/custom"); + }); + + it("should filter out /api/logs route", () => { + const app = createMockApp({ + routes: [ + { method: "GET", path: "/api/logs" }, + { method: "GET", path: "/api/custom" }, // Custom route + ], + }); + + const endpoints = extractCustomEndpoints(app); + + expect(endpoints).toHaveLength(1); + expect(endpoints[0].path).toBe("/api/custom"); + }); + + it("should filter out /updates and /setup-observability routes", () => { + const app = createMockApp({ + routes: [ + { method: "GET", path: "/updates" }, + { method: "POST", path: "/setup-observability" }, + { method: "GET", path: "/api/custom" }, // Custom route + ], + }); + + const endpoints = extractCustomEndpoints(app); + + expect(endpoints).toHaveLength(1); + expect(endpoints[0].path).toBe("/api/custom"); + }); + + it("should NOT filter custom routes that START with built-in path prefixes", () => { + const app = createMockApp({ + routes: [ + // Built-in routes (should be filtered) + { method: "GET", path: "/agents" }, + { method: "GET", path: "/workflows" }, + { method: "GET", path: "/observability/traces" }, + // Custom routes that START with built-in prefixes (should NOT be filtered) + { method: "GET", path: "/agents-custom" }, + { method: "GET", path: "/agents-dashboard" }, + { method: "GET", path: "/workflows-manager" }, + { method: "GET", path: "/workflows-builder" }, + { method: "GET", path: "/observability-ui" }, + { method: "GET", path: "/observability-metrics" }, + ], + }); + + const endpoints = extractCustomEndpoints(app); + + // Should extract only the 6 custom routes, not the 3 built-in routes + expect(endpoints).toHaveLength(6); + + const paths = endpoints.map((e) => e.path); + expect(paths).toContain("/agents-custom"); + expect(paths).toContain("/agents-dashboard"); + expect(paths).toContain("/workflows-manager"); + expect(paths).toContain("/workflows-builder"); + expect(paths).toContain("/observability-ui"); + expect(paths).toContain("/observability-metrics"); + expect(paths).not.toContain("/agents"); + expect(paths).not.toContain("/workflows"); + expect(paths).not.toContain("/observability/traces"); + }); + + it("should handle routes with hyphens and underscores correctly", () => { + const app = createMockApp({ + routes: [ + { method: "GET", path: "/api/my-custom-route" }, + { method: "POST", path: "/api/another_custom_route" }, + { method: "PUT", path: "/custom-agents" }, // NOT /agents + { method: "DELETE", path: "/my_workflows" }, // NOT /workflows + ], + }); + + const endpoints = extractCustomEndpoints(app); + + expect(endpoints).toHaveLength(4); + expect(endpoints.map((e) => e.path)).toEqual([ + "/api/my-custom-route", + "/api/another_custom_route", + "/custom-agents", + "/my_workflows", + ]); + }); + }); + + describe("Edge cases and error handling", () => { + it("should return empty array when app has no routes", () => { + const app = createMockApp({ routes: [] }); + + const endpoints = extractCustomEndpoints(app); + + expect(endpoints).toEqual([]); + }); + + it("should handle missing app.routes property gracefully", () => { + const app = createMockApp({ routes: undefined }); + + const endpoints = extractCustomEndpoints(app); + + expect(endpoints).toEqual([]); + }); + + it("should return empty array on complete failure", () => { + const app = { + routes: "not-an-array", // Invalid + } as any; + + const endpoints = extractCustomEndpoints(app); + + expect(endpoints).toEqual([]); + }); + + it("should handle routes with empty path", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "" }], + }); + + const endpoints = extractCustomEndpoints(app); + + // Should filter out empty paths + expect(endpoints).toEqual([]); + }); + + it("should handle malformed route data", () => { + const app = { + routes: [{ method: null, path: "/test" }, { method: "GET", path: null }, {}], + } as any; + + const endpoints = extractCustomEndpoints(app); + + // Should filter out invalid routes + expect(endpoints).toEqual([]); + }); + }); + + describe("Path parameter format handling", () => { + it("should handle :param format from regular routes", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "/api/users/:id/posts/:postId" }], + }); + + const endpoints = extractCustomEndpoints(app); + + expect(endpoints[0].path).toBe("/api/users/:id/posts/:postId"); + }); + + it("should handle {param} format", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "/api/users/{id}" }], + }); + + const endpoints = extractCustomEndpoints(app); + + expect(endpoints[0].path).toBe("/api/users/{id}"); + }); + + it("should not duplicate routes with different param formats", () => { + const app = createMockApp({ + routes: [ + { method: "GET", path: "/api/users/:id" }, + { method: "GET", path: "/api/users/{id}" }, + ], + }); + + const endpoints = extractCustomEndpoints(app); + + // Should have both because they use different formats + // This is expected behavior - they're technically different paths + expect(endpoints.length).toBeGreaterThanOrEqual(1); + }); + }); +}); + +describe("getEnhancedOpenApiDoc", () => { + describe("Adding custom routes to OpenAPI document", () => { + it("should add custom routes to OpenAPI document", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "/api/health" }], + }); + + const baseDoc = { + info: { title: "Test API", version: "1.0.0" }, + }; + + const enhancedDoc = getEnhancedOpenApiDoc(app, baseDoc); + + expect(enhancedDoc.paths["/api/health"]).toBeDefined(); + expect(enhancedDoc.paths["/api/health"].get).toBeDefined(); + }); + + it("should generate response schema for custom routes", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "/api/health" }], + }); + + const enhancedDoc = getEnhancedOpenApiDoc(app, {}); + + const operation = enhancedDoc.paths["/api/health"].get; + expect(operation.responses["200"]).toBeDefined(); + expect(operation.responses["200"].content["application/json"]).toBeDefined(); + }); + + it("should tag custom routes as 'Custom Endpoints'", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "/api/health" }], + }); + + const enhancedDoc = getEnhancedOpenApiDoc(app, {}); + + expect(enhancedDoc.paths["/api/health"].get.tags).toEqual(["Custom Endpoints"]); + }); + + it("should add summary and description to custom routes", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "/api/health" }], + }); + + const enhancedDoc = getEnhancedOpenApiDoc(app, {}); + + const operation = enhancedDoc.paths["/api/health"].get; + expect(operation.summary).toBe("GET /api/health"); + expect(operation.description).toBe("Custom endpoint: GET /api/health"); + }); + }); + + describe("Preserving existing OpenAPI documentation", () => { + it("should not overwrite existing OpenAPI route documentation", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "/api/health" }], + }); + + const baseDoc = { + paths: { + "/api/health": { + get: { + summary: "Existing summary", + description: "Existing description", + responses: { + 200: { + description: "Existing response", + }, + }, + }, + }, + }, + }; + + const enhancedDoc = getEnhancedOpenApiDoc(app, baseDoc); + + const operation = enhancedDoc.paths["/api/health"].get; + expect(operation.summary).toBe("Existing summary"); + expect(operation.description).toBe("Existing description"); + expect(operation.responses["200"].description).toBe("Existing response"); + }); + + it("should preserve existing route properties", () => { + const app = createMockApp({ + routes: [], + }); + + const baseDoc = { + paths: { + "/api/users": { + get: { + operationId: "listUsers", + tags: ["Users"], + security: [{ bearerAuth: [] }], + }, + }, + }, + }; + + const enhancedDoc = getEnhancedOpenApiDoc(app, baseDoc); + + const operation = enhancedDoc.paths["/api/users"].get; + expect(operation.operationId).toBe("listUsers"); + // Should have both original tags and "Custom Endpoints" tag added + expect(operation.tags).toEqual(["Users", "Custom Endpoints"]); + expect(operation.security).toEqual([{ bearerAuth: [] }]); + }); + }); + + describe("HTTP method-specific behavior", () => { + it("should add requestBody for POST methods", () => { + const app = createMockApp({ + routes: [{ method: "POST", path: "/api/calculate" }], + }); + + const enhancedDoc = getEnhancedOpenApiDoc(app, {}); + + const operation = enhancedDoc.paths["/api/calculate"].post; + expect(operation.requestBody).toBeDefined(); + expect(operation.requestBody.content["application/json"]).toBeDefined(); + }); + + it("should add requestBody for PUT methods", () => { + const app = createMockApp({ + routes: [{ method: "PUT", path: "/api/users/:id" }], + }); + + const enhancedDoc = getEnhancedOpenApiDoc(app, {}); + + const operation = enhancedDoc.paths["/api/users/:id"].put; + expect(operation.requestBody).toBeDefined(); + }); + + it("should add requestBody for PATCH methods", () => { + const app = createMockApp({ + routes: [{ method: "PATCH", path: "/api/users/:id" }], + }); + + const enhancedDoc = getEnhancedOpenApiDoc(app, {}); + + const operation = enhancedDoc.paths["/api/users/:id"].patch; + expect(operation.requestBody).toBeDefined(); + }); + + it("should not add requestBody for GET methods", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "/api/health" }], + }); + + const enhancedDoc = getEnhancedOpenApiDoc(app, {}); + + const operation = enhancedDoc.paths["/api/health"].get; + expect(operation.requestBody).toBeUndefined(); + }); + + it("should not add requestBody for DELETE methods", () => { + const app = createMockApp({ + routes: [{ method: "DELETE", path: "/api/users/:id" }], + }); + + const enhancedDoc = getEnhancedOpenApiDoc(app, {}); + + const operation = enhancedDoc.paths["/api/users/:id"].delete; + expect(operation.requestBody).toBeUndefined(); + }); + }); + + describe("Path parameter handling", () => { + it("should extract path parameters from :param format", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "/api/users/:id" }], + }); + + const enhancedDoc = getEnhancedOpenApiDoc(app, {}); + + const operation = enhancedDoc.paths["/api/users/:id"].get; + expect(operation.parameters).toBeDefined(); + expect(operation.parameters).toHaveLength(1); + expect(operation.parameters[0].name).toBe("id"); + expect(operation.parameters[0].in).toBe("path"); + expect(operation.parameters[0].required).toBe(true); + }); + + it("should extract multiple path parameters", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "/api/posts/:postId/comments/:commentId" }], + }); + + const enhancedDoc = getEnhancedOpenApiDoc(app, {}); + + const operation = enhancedDoc.paths["/api/posts/:postId/comments/:commentId"].get; + expect(operation.parameters).toHaveLength(2); + expect(operation.parameters[0].name).toBe("postId"); + expect(operation.parameters[1].name).toBe("commentId"); + }); + + it("should handle {param} format from OpenAPI routes", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "/api/users/{id}" }], + }); + + const enhancedDoc = getEnhancedOpenApiDoc(app, {}); + + const operation = enhancedDoc.paths["/api/users/{id}"].get; + expect(operation.parameters).toBeDefined(); + expect(operation.parameters[0].name).toBe("id"); + }); + + it("should not add parameters for routes without path variables", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "/api/health" }], + }); + + const enhancedDoc = getEnhancedOpenApiDoc(app, {}); + + const operation = enhancedDoc.paths["/api/health"].get; + expect(operation.parameters).toBeUndefined(); + }); + }); + + describe("Edge cases", () => { + it("should handle empty custom endpoints gracefully", () => { + const app = createMockApp({ + routes: [], + }); + + const enhancedDoc = getEnhancedOpenApiDoc(app, {}); + + expect(enhancedDoc.paths).toEqual({}); + }); + + it("should merge custom endpoints with existing OpenAPI paths", () => { + const app = createMockApp({ + routes: [{ method: "GET", path: "/api/custom" }], + }); + + const baseDoc = { + paths: { + "/api/existing": { + get: { summary: "Existing endpoint" }, + }, + }, + }; + + const enhancedDoc = getEnhancedOpenApiDoc(app, baseDoc); + + expect(Object.keys(enhancedDoc.paths)).toHaveLength(2); + expect(enhancedDoc.paths["/api/custom"]).toBeDefined(); + expect(enhancedDoc.paths["/api/existing"]).toBeDefined(); + }); + + it("should handle errors gracefully and return base doc", () => { + const app = { + routes: null, + } as any; + + const baseDoc = { info: { title: "Test", version: "1.0.0" } }; + const enhancedDoc = getEnhancedOpenApiDoc(app, baseDoc); + + expect(enhancedDoc).toEqual(baseDoc); + }); + + it("should handle null base doc", () => { + const app = new Elysia(); + + const enhancedDoc = getEnhancedOpenApiDoc(app as any, null); + + expect(enhancedDoc).toBeDefined(); + expect(enhancedDoc.openapi).toBe("3.1.0"); + }); + }); +}); diff --git a/packages/server-elysia/src/utils/custom-endpoints.ts b/packages/server-elysia/src/utils/custom-endpoints.ts new file mode 100644 index 000000000..6e815a4f1 --- /dev/null +++ b/packages/server-elysia/src/utils/custom-endpoints.ts @@ -0,0 +1,236 @@ +/** + * Utilities for extracting custom endpoints from Elysia app + */ + +import type { ServerEndpointSummary } from "@voltagent/server-core"; +import { A2A_ROUTES, ALL_ROUTES, MCP_ROUTES } from "@voltagent/server-core"; +import type { Elysia } from "elysia"; + +/** + * Known VoltAgent built-in paths that should be excluded when extracting custom endpoints + */ +const BUILT_IN_PATHS = new Set([ + // Core routes + "/", + "/doc", + "/ui", + + // Agent routes + ...Object.values(ALL_ROUTES).map((route) => route.path), + + // MCP routes + ...Object.values(MCP_ROUTES).map((route) => route.path), + + // A2A routes + ...Object.values(A2A_ROUTES).map((route) => route.path), +]); + +/** + * Extract custom endpoints from the Elysia app after configureApp has been called + * @param app The Elysia app instance + * @returns Array of custom endpoint summaries + */ +export function extractCustomEndpoints(app: Elysia): ServerEndpointSummary[] { + try { + const customEndpoints: ServerEndpointSummary[] = []; + const seenRoutes = new Set(); + + // Extract routes from Elysia app + try { + const routes = (app as any).routes; + + if (routes && Array.isArray(routes)) { + for (const route of routes) { + if (!route.method || !route.path) { + continue; + } + + // Normalize path - remove duplicate slashes + const fullPath = route.path.replace(/\/+/g, "/"); + + // Skip built-in VoltAgent paths + if (isBuiltInPath(fullPath)) { + continue; + } + + const routeKey = `${route.method.toUpperCase()}:${fullPath}`; + if (!seenRoutes.has(routeKey)) { + seenRoutes.add(routeKey); + customEndpoints.push({ + method: route.method.toUpperCase(), + path: fullPath, + group: "Custom Endpoints", + }); + } + } + } + } catch (_routesError) { + // Routes extraction failed, continue + } + + return customEndpoints; + } catch (error) { + // If extraction fails, return empty array to avoid breaking the server + console.warn("Failed to extract custom endpoints:", error); + return []; + } +} + +/** + * Check if a path is a built-in VoltAgent path + * @param path The API path to check + * @returns True if it's a built-in path + */ +function isBuiltInPath(path: string): boolean { + // Normalize path by removing duplicate slashes and ensuring single leading slash + const normalizedPath = path.replace(/\/+/g, "/").replace(/^\/+/, "/"); + + // Direct match against known built-in paths + if (BUILT_IN_PATHS.has(normalizedPath)) { + return true; + } + + // Check against parameterized paths by converting :param to {param} format + // This handles cases like "/agents/:id" vs "/agents/{id}" + const paramNormalized = normalizedPath.replace(/\{([^}]+)\}/g, ":$1"); + if (BUILT_IN_PATHS.has(paramNormalized)) { + return true; + } + + // Not a built-in path - it's a custom endpoint + return false; +} + +/** + * Get enhanced OpenAPI documentation including custom endpoints + * Note: Elysia has built-in OpenAPI support via @elysiajs/swagger + * This function is primarily for compatibility with the VoltAgent pattern + * @param app The Elysia app instance + * @param baseDoc The base OpenAPI document configuration + * @returns Enhanced OpenAPI document with custom endpoints + */ +export function getEnhancedOpenApiDoc(app: Elysia, baseDoc: any): any { + try { + // Extract custom endpoints that were registered with regular Elysia methods + const customEndpoints = extractCustomEndpoints(app); + + // If no custom endpoints and baseDoc has meaningful structure (info but no paths), + // return baseDoc unchanged to avoid polluting it + if ( + customEndpoints.length === 0 && + baseDoc && + baseDoc.info && + !baseDoc.paths && + !baseDoc.openapi + ) { + return baseDoc; + } + + // Start with the base document structure + const fullDoc = { + ...baseDoc, + openapi: baseDoc?.openapi || "3.1.0", + paths: { ...(baseDoc?.paths || {}) }, + }; + + // Add custom endpoints to the OpenAPI document + customEndpoints.forEach((endpoint) => { + const path = endpoint.path; + const method = endpoint.method.toLowerCase(); + + // Initialize path object if it doesn't exist + if (!fullDoc.paths[path]) { + fullDoc.paths[path] = {}; + } + + // Skip if this operation already exists in OpenAPI doc (don't overwrite) + const pathObj = fullDoc.paths[path] as any; + if (pathObj[method]) { + return; + } + + // Add the operation for this method (only for routes not in OpenAPI doc) + pathObj[method] = { + tags: ["Custom Endpoints"], + summary: endpoint.description || `${endpoint.method} ${path}`, + description: endpoint.description || `Custom endpoint: ${endpoint.method} ${path}`, + responses: { + 200: { + description: "Successful response", + content: { + "application/json": { + schema: { + type: "object", + properties: { + success: { type: "boolean" }, + data: { type: "object" }, + }, + }, + }, + }, + }, + }, + }; + + // Add parameters for path variables (support both :param and {param} formats) + const pathWithBraces = path.replace(/:([a-zA-Z0-9_]+)/g, "{$1}"); + if (pathWithBraces.includes("{")) { + const params = pathWithBraces.match(/\{([^}]+)\}/g); + if (params) { + pathObj[method].parameters = params.map((param: string) => { + const paramName = param.slice(1, -1); // Remove { and } + return { + name: paramName, + in: "path", + required: true, + schema: { type: "string" }, + description: `Path parameter: ${paramName}`, + }; + }); + } + } + + // Add request body for POST/PUT/PATCH methods + if (["post", "put", "patch"].includes(method)) { + pathObj[method].requestBody = { + content: { + "application/json": { + schema: { + type: "object", + additionalProperties: true, + }, + }, + }, + }; + } + }); + + // Ensure proper tags for organization of existing routes + if (fullDoc.paths) { + Object.entries(fullDoc.paths).forEach(([path, pathItem]) => { + if (pathItem && !isBuiltInPath(path)) { + // Add "Custom Endpoints" tag to custom routes for better organization + const methods = ["get", "post", "put", "patch", "delete", "options", "head"] as const; + methods.forEach((method) => { + const operation = (pathItem as any)[method]; + if (operation && !operation.tags?.includes("Custom Endpoints")) { + // Ensure we don't mutate the original pathItem or operation + fullDoc.paths[path] = { + ...(fullDoc.paths[path] as any), + [method]: { + ...operation, + tags: [...(operation.tags || []), "Custom Endpoints"], + }, + }; + } + }); + } + }); + } + + return fullDoc; + } catch (error) { + console.warn("Failed to enhance OpenAPI document with custom endpoints:", error); + return baseDoc; + } +} diff --git a/packages/server-elysia/src/utils/zod-adapter.spec.ts b/packages/server-elysia/src/utils/zod-adapter.spec.ts new file mode 100644 index 000000000..ec43a0a55 --- /dev/null +++ b/packages/server-elysia/src/utils/zod-adapter.spec.ts @@ -0,0 +1,30 @@ +import { TypeCompiler } from "@sinclair/typebox/compiler"; +import { TextRequestSchema } from "@voltagent/server-core"; +import { describe, expect, test } from "vitest"; +import { z } from "zod"; +import { zodToTypeBox } from "./zod-adapter"; + +describe("Zod Adapter", () => { + test("should compile TextRequestSchema", () => { + const schema = zodToTypeBox(TextRequestSchema); + const C = TypeCompiler.Compile(schema); + expect(C).toBeDefined(); + + // Basic validation check + const valid = C.Check({ + input: "hello", + options: { + temperature: 0.5, + }, + }); + expect(valid).toBe(true); + }); + + test("should compile schema with exclusiveMinimum", () => { + const schema = z.number().gt(0); + const tbSchema = zodToTypeBox(schema); + const C = TypeCompiler.Compile(tbSchema); + expect(C.Check(1)).toBe(true); + expect(C.Check(0)).toBe(false); + }); +}); diff --git a/packages/server-elysia/src/utils/zod-adapter.ts b/packages/server-elysia/src/utils/zod-adapter.ts new file mode 100644 index 000000000..5e2785774 --- /dev/null +++ b/packages/server-elysia/src/utils/zod-adapter.ts @@ -0,0 +1,124 @@ +import { type TSchema, Type } from "@sinclair/typebox"; +import type { ZodType, ZodTypeDef } from "zod"; +import { zodToJsonSchema } from "zod-to-json-schema"; + +function getOptions(schema: any) { + const options = { ...schema }; + options.type = undefined; + options.properties = undefined; + options.required = undefined; + options.items = undefined; + options.anyOf = undefined; + options.allOf = undefined; + options.oneOf = undefined; + options.enum = undefined; + options.const = undefined; + options.additionalProperties = undefined; + options.nullable = undefined; + return options; +} + +function mapJsonSchemaToTypeBox(schema: any): TSchema { + if (!schema || Object.keys(schema).length === 0) { + return Type.Any(); + } + + const options = getOptions(schema); + + // Handle anyOf, allOf, oneOf + if (schema.anyOf) { + return Type.Union(schema.anyOf.map(mapJsonSchemaToTypeBox), options); + } + if (schema.allOf) { + return Type.Intersect(schema.allOf.map(mapJsonSchemaToTypeBox), options); + } + if (schema.oneOf) { + return Type.Union(schema.oneOf.map(mapJsonSchemaToTypeBox), options); + } + + // Handle enums + if (schema.enum) { + return Type.Union( + schema.enum.map((v: any) => Type.Literal(v)), + options, + ); + } + + // Handle const + if (schema.const !== undefined) { + return Type.Literal(schema.const, options); + } + + let result: TSchema; + + // Handle types + switch (schema.type) { + case "object": { + const properties: Record = {}; + const required = new Set(schema.required || []); + + if (schema.properties) { + for (const [key, value] of Object.entries(schema.properties)) { + const propSchema = mapJsonSchemaToTypeBox(value); + properties[key] = required.has(key) ? propSchema : Type.Optional(propSchema); + } + } + + const opts = { ...options }; + if (typeof schema.additionalProperties === "object") { + opts.additionalProperties = mapJsonSchemaToTypeBox(schema.additionalProperties); + } else if (schema.additionalProperties !== undefined) { + opts.additionalProperties = schema.additionalProperties; + } + + result = Type.Object(properties, opts); + break; + } + case "array": + if (Array.isArray(schema.items)) { + result = Type.Tuple(schema.items.map(mapJsonSchemaToTypeBox), options); + } else { + result = Type.Array(mapJsonSchemaToTypeBox(schema.items), options); + } + break; + case "string": + result = Type.String(options); + break; + case "number": + result = Type.Number(options); + break; + case "integer": + result = Type.Integer(options); + break; + case "boolean": + result = Type.Boolean(options); + break; + case "null": + result = Type.Null(options); + break; + default: + // If no type is specified and no combinators, it's likely Any or Unknown + // We default to Any to be safe, as Type.Unsafe(schema) crashes if schema is raw JSON + result = Type.Any(options); + } + + if (schema.nullable) { + return Type.Union([result, Type.Null()]); + } + + return result; +} + +/** + * Converts a Zod schema to a TypeBox schema (via JSON Schema) + * This allows us to reuse Zod schemas from @voltagent/server-core + * while maintaining Elysia's TypeBox-based validation pipeline. + */ +export function zodToTypeBox>(zodSchema: T): TSchema { + const jsonSchema = zodToJsonSchema(zodSchema, { + target: "jsonSchema7", + $refStrategy: "none", + }); + + return mapJsonSchemaToTypeBox(jsonSchema); +} diff --git a/packages/server-elysia/tsconfig.json b/packages/server-elysia/tsconfig.json new file mode 100644 index 000000000..121ae5475 --- /dev/null +++ b/packages/server-elysia/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["esnext"], + "module": "esnext", + "moduleResolution": "bundler", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./", + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "types": ["vitest/globals", "node"] + }, + "include": ["src/**/*.ts", "src/**/*.spec-d.ts"], + "exclude": ["node_modules", "dist", "src/**/*.spec.ts"] +} diff --git a/packages/server-elysia/tsup.config.ts b/packages/server-elysia/tsup.config.ts new file mode 100644 index 000000000..0819104fd --- /dev/null +++ b/packages/server-elysia/tsup.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "tsup"; +import { markAsExternalPlugin } from "../shared/tsup-plugins/mark-as-external"; + +export default defineConfig({ + entry: ["src/index.ts"], + format: ["cjs", "esm"], + splitting: false, + sourcemap: true, + clean: false, + target: "es2022", + outDir: "dist", + minify: false, + dts: true, + esbuildPlugins: [markAsExternalPlugin], + esbuildOptions(options) { + options.keepNames = true; + return options; + }, +}); diff --git a/packages/server-elysia/vitest.config.ts b/packages/server-elysia/vitest.config.ts new file mode 100644 index 000000000..efffdddb6 --- /dev/null +++ b/packages/server-elysia/vitest.config.ts @@ -0,0 +1,21 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["**/*.spec.ts"], + environment: "node", + coverage: { + provider: "v8", + reporter: ["text", "json", "html"], + include: ["src/**/*.ts"], + exclude: ["src/**/*.d.ts", "src/**/index.ts"], + }, + typecheck: { + include: ["**/**/*.spec-d.ts"], + exclude: ["**/**/*.spec.ts"], + }, + globals: true, + testTimeout: 10000, + hookTimeout: 10000, + }, +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8b1c6c753..ea005186c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -993,10 +993,10 @@ importers: dependencies: '@copilotkit/react-core': specifier: ^1.50.0 - version: 1.50.0(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.3)(react@19.2.3)(zod@4.1.13) + version: 1.50.0(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.3)(react@19.2.3)(zod@4.2.1) '@copilotkit/react-ui': specifier: ^1.50.0 - version: 1.50.0(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.3)(react@19.2.3)(zod@4.1.13) + version: 1.50.0(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.3)(react@19.2.3)(zod@4.2.1) react: specifier: ^19.2.3 version: 19.2.3 @@ -1372,7 +1372,7 @@ importers: dependencies: '@ai-sdk/openai': specifier: ^3.0.0 - version: 3.0.1(zod@4.1.13) + version: 3.0.1(zod@4.2.1) '@voltagent/cli': specifier: ^0.1.20 version: link:../../packages/cli @@ -1387,7 +1387,7 @@ importers: version: link:../../packages/server-hono ai: specifier: ^6.0.0 - version: 6.0.3(zod@4.1.13) + version: 6.0.3(zod@4.2.1) devDependencies: '@types/node': specifier: ^24.2.1 @@ -1693,7 +1693,7 @@ importers: dependencies: '@ai-sdk/openai': specifier: ^3.0.0 - version: 3.0.1(zod@4.1.13) + version: 3.0.1(zod@4.2.1) '@voltagent/cli': specifier: ^0.1.20 version: link:../../packages/cli @@ -3636,7 +3636,7 @@ importers: version: link:../logger ai: specifier: ^6.0.0 - version: 6.0.3(zod@4.1.13) + version: 6.0.3(zod@4.2.1) packages/logger: dependencies: @@ -3702,7 +3702,7 @@ importers: version: link:../core ai: specifier: ^6.0.0 - version: 6.0.3(zod@4.1.13) + version: 6.0.3(zod@4.2.1) packages/rag: dependencies: @@ -3783,6 +3783,46 @@ importers: specifier: ^3.25.76 version: 3.25.76 + packages/server-elysia: + dependencies: + '@elysiajs/cors': + specifier: ^1.2.2 + version: 1.4.1(elysia@1.4.19) + '@elysiajs/swagger': + specifier: ^1.2.4 + version: 1.3.1(elysia@1.4.19) + '@sinclair/typebox': + specifier: ^0.34.45 + version: 0.34.45 + '@voltagent/a2a-server': + specifier: ^1.0.2 + version: 1.0.2(@voltagent/core@1.5.2) + '@voltagent/core': + specifier: ^1.5.1 + version: 1.5.2(@ai-sdk/provider-utils@3.0.19)(ai@5.0.113)(zod@3.25.76) + '@voltagent/internal': + specifier: ^0.0.12 + version: 0.0.12 + '@voltagent/mcp-server': + specifier: ^1.0.3 + version: 1.0.3(@voltagent/core@1.5.2)(zod@3.25.76) + '@voltagent/server-core': + specifier: ^1.0.36 + version: 1.0.36(@voltagent/core@1.5.2)(zod@3.25.76) + elysia: + specifier: ^1.1.29 + version: 1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5)(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2) + zod: + specifier: ^3.25.76 + version: 3.25.76 + zod-to-json-schema: + specifier: ^3.25.1 + version: 3.25.1(zod@3.25.76) + devDependencies: + '@types/ws': + specifier: ^8.18.1 + version: 8.18.1 + packages/server-hono: dependencies: '@hono/node-server': @@ -3861,7 +3901,7 @@ importers: version: link:../logger ai: specifier: ^6.0.0 - version: 6.0.3(zod@4.1.13) + version: 6.0.3(zod@4.2.1) packages/voice: dependencies: @@ -3911,7 +3951,7 @@ importers: version: link:../core ai: specifier: ^6.0.0 - version: 6.0.3(zod@4.1.13) + version: 6.0.3(zod@4.2.1) packages: @@ -4194,16 +4234,16 @@ packages: '@vercel/oidc': 3.0.5 zod: 3.25.76 - /@ai-sdk/gateway@3.0.2(zod@4.1.13): + /@ai-sdk/gateway@3.0.2(zod@4.2.1): resolution: {integrity: sha512-giJEg9ob45htbu3iautK+2kvplY2JnTj7ir4wZzYSQWvqGatWfBBfDuNCU5wSJt9BCGjymM5ZS9ziD42JGCZBw==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 dependencies: '@ai-sdk/provider': 3.0.0 - '@ai-sdk/provider-utils': 4.0.1(zod@4.1.13) + '@ai-sdk/provider-utils': 4.0.1(zod@4.2.1) '@vercel/oidc': 3.0.5 - zod: 4.1.13 + zod: 4.2.1 /@ai-sdk/google-vertex@3.0.25(zod@3.25.76): resolution: {integrity: sha512-X4VRfFHTMr50wo8qvoA4WmxmehSAMzEAiJ5pPn0/EPB4kxytz53g7BijRBDL+MZpqXRNiwF3taf4p3P1WUMnVA==} @@ -4373,15 +4413,15 @@ packages: zod: 3.25.76 dev: false - /@ai-sdk/openai@3.0.1(zod@4.1.13): + /@ai-sdk/openai@3.0.1(zod@4.2.1): resolution: {integrity: sha512-P+qxz2diOrh8OrpqLRg+E+XIFVIKM3z2kFjABcCJGHjGbXBK88AJqmuKAi87qLTvTe/xn1fhZBjklZg9bTyigw==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 dependencies: '@ai-sdk/provider': 3.0.0 - '@ai-sdk/provider-utils': 4.0.1(zod@4.1.13) - zod: 4.1.13 + '@ai-sdk/provider-utils': 4.0.1(zod@4.2.1) + zod: 4.2.1 dev: false /@ai-sdk/perplexity@1.1.9(zod@3.25.76): @@ -4502,7 +4542,7 @@ packages: eventsource-parser: 3.0.6 zod: 3.25.76 - /@ai-sdk/provider-utils@4.0.1(zod@4.1.13): + /@ai-sdk/provider-utils@4.0.1(zod@4.2.1): resolution: {integrity: sha512-de2v8gH9zj47tRI38oSxhQIewmNc+OZjYIOOaMoVWKL65ERSav2PYYZHPSPCrfOeLMkv+Dyh8Y0QGwkO29wMWQ==} engines: {node: '>=18'} peerDependencies: @@ -4511,7 +4551,7 @@ packages: '@ai-sdk/provider': 3.0.0 '@standard-schema/spec': 1.1.0 eventsource-parser: 3.0.6 - zod: 4.1.13 + zod: 4.2.1 /@ai-sdk/provider@1.1.3: resolution: {integrity: sha512-qZMxYJ0qqX/RfnuIaab+zp8UAeJn/ygXXAffR5I4N0n1IrvA6qBsjc8hXLmBiMV2zoXlifkacF7sEFnYnjBcqg==} @@ -7180,7 +7220,7 @@ packages: playwright: 1.54.2 ws: 8.18.3 zod: 3.25.76 - zod-to-json-schema: 3.24.6(zod@3.25.76) + zod-to-json-schema: 3.25.0(zod@3.25.76) optionalDependencies: '@ai-sdk/anthropic': 1.2.12(zod@3.25.76) '@ai-sdk/azure': 1.3.25(zod@3.25.76) @@ -7829,7 +7869,7 @@ packages: hasBin: true dev: false - /@copilotkit/react-core@1.50.0(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.3)(react@19.2.3)(zod@4.1.13): + /@copilotkit/react-core@1.50.0(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.3)(react@19.2.3)(zod@4.2.1): resolution: {integrity: sha512-JfUEvmgXgPz7wIQq9EFXWGDMtYLIVKSNqPdJROEomZXLhREDlxpg+jr5KHvoOPUlSnVLzuPObRKSdeJTwVOGsQ==} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc @@ -7846,7 +7886,7 @@ packages: react-dom: 19.2.3(react@19.2.3) react-markdown: 8.0.7(@types/react@19.2.7)(react@19.2.3) untruncate-json: 0.0.1 - zod: 4.1.13 + zod: 4.2.1 transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -7855,12 +7895,12 @@ packages: - supports-color dev: false - /@copilotkit/react-ui@1.50.0(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.3)(react@19.2.3)(zod@4.1.13): + /@copilotkit/react-ui@1.50.0(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.3)(react@19.2.3)(zod@4.2.1): resolution: {integrity: sha512-BB4d5yrf3n3P0stCI7DLLo22EhesNz4ELuSKFT39P+GEjKMVcpZu+mR6NgkbIkrmU6cFxxsrJKv20p7T+EBnoA==} peerDependencies: react: ^18 || ^19 || ^19.0.0-rc dependencies: - '@copilotkit/react-core': 1.50.0(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.3)(react@19.2.3)(zod@4.1.13) + '@copilotkit/react-core': 1.50.0(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.3)(react@19.2.3)(zod@4.2.1) '@copilotkit/runtime-client-gql': 1.50.0(react@19.2.3) '@copilotkit/shared': 1.50.0 '@headlessui/react': 2.2.9(react-dom@19.2.3)(react@19.2.3) @@ -7999,7 +8039,7 @@ packages: '@copilotkitnext/shared': 0.0.31 rxjs: 7.8.1 zod: 3.25.76 - zod-to-json-schema: 3.24.6(zod@3.25.76) + zod-to-json-schema: 3.25.0(zod@3.25.76) dev: false /@copilotkitnext/react@0.0.31(@types/react-dom@19.2.3)(@types/react@19.2.7)(react-dom@19.2.3)(react@19.2.3): @@ -8095,6 +8135,26 @@ packages: node-source-walk: 7.0.1 dev: true + /@elysiajs/cors@1.4.1(elysia@1.4.19): + resolution: {integrity: sha512-lQfad+F3r4mNwsxRKbXyJB8Jg43oAOXjRwn7sKUL6bcOW3KjUqUimTS+woNpO97efpzjtDE0tEjGk9DTw8lqTQ==} + peerDependencies: + elysia: '>= 1.4.0' + dependencies: + elysia: 1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5)(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2) + dev: false + + /@elysiajs/swagger@1.3.1(elysia@1.4.19): + resolution: {integrity: sha512-LcbLHa0zE6FJKWPWKsIC/f+62wbDv3aXydqcNPVPyqNcaUgwvCajIi+5kHEU6GO3oXUCpzKaMsb3gsjt8sLzFQ==} + peerDependencies: + elysia: '>= 1.3.0' + dependencies: + '@scalar/themes': 0.9.86 + '@scalar/types': 0.0.12 + elysia: 1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5)(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2) + openapi-types: 12.1.3 + pathe: 1.1.2 + dev: false + /@emnapi/core@1.4.5: resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==} dependencies: @@ -10664,7 +10724,7 @@ packages: pkce-challenge: 5.0.0 raw-body: 3.0.0 zod: 3.25.76 - zod-to-json-schema: 3.24.6(zod@3.25.76) + zod-to-json-schema: 3.25.0(zod@3.25.76) transitivePeerDependencies: - supports-color dev: false @@ -11225,7 +11285,7 @@ packages: validate-npm-package-name: 5.0.1 yaml: 2.8.1 yargs: 17.7.2 - zod: 4.1.13 + zod: 4.2.1 dev: true /@netlify/dev-utils@4.1.3: @@ -16009,13 +16069,8 @@ packages: resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==} dev: true - /@rolldown/pluginutils@1.0.0-beta.54: - resolution: {integrity: sha512-AHgcZ+w7RIRZ65ihSQL8YuoKcpD9Scew4sEeP1BBUT9QdTo6KjwHrZZXjID6nL10fhKessCH6OPany2QKwAwTQ==} - dev: false - /@rolldown/pluginutils@1.0.0-beta.57: resolution: {integrity: sha512-aQNelgx14tGA+n2tNSa9x6/jeoCL9fkDeCei7nOKnHx0fEFRRMu5ReiITo+zZD5TzWDGGRjbSYCs93IfRIyTuQ==} - dev: true /@rollup/plugin-alias@5.1.1(rollup@4.50.2): resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==} @@ -16276,6 +16331,44 @@ packages: requiresBuild: true optional: true + /@scalar/openapi-types@0.1.1: + resolution: {integrity: sha512-NMy3QNk6ytcCoPUGJH0t4NNr36OWXgZhA3ormr3TvhX1NDgoF95wFyodGVH8xiHeUyn2/FxtETm8UBLbB5xEmg==} + engines: {node: '>=18'} + dev: false + + /@scalar/openapi-types@0.2.0: + resolution: {integrity: sha512-waiKk12cRCqyUCWTOX0K1WEVX46+hVUK+zRPzAahDJ7G0TApvbNkuy5wx7aoUyEk++HHde0XuQnshXnt8jsddA==} + engines: {node: '>=18'} + dependencies: + zod: 3.25.76 + dev: false + + /@scalar/themes@0.9.86: + resolution: {integrity: sha512-QUHo9g5oSWi+0Lm1vJY9TaMZRau8LHg+vte7q5BVTBnu6NuQfigCaN+ouQ73FqIVd96TwMO6Db+dilK1B+9row==} + engines: {node: '>=18'} + dependencies: + '@scalar/types': 0.1.7 + dev: false + + /@scalar/types@0.0.12: + resolution: {integrity: sha512-XYZ36lSEx87i4gDqopQlGCOkdIITHHEvgkuJFrXFATQs9zHARop0PN0g4RZYWj+ZpCUclOcaOjbCt8JGe22mnQ==} + engines: {node: '>=18'} + dependencies: + '@scalar/openapi-types': 0.1.1 + '@unhead/schema': 1.11.20 + dev: false + + /@scalar/types@0.1.7: + resolution: {integrity: sha512-irIDYzTQG2KLvFbuTI8k2Pz/R4JR+zUUSykVTbEMatkzMmVFnn1VzNSMlODbadycwZunbnL2tA27AXed9URVjw==} + engines: {node: '>=18'} + dependencies: + '@scalar/openapi-types': 0.2.0 + '@unhead/schema': 1.11.20 + nanoid: 5.1.6 + type-fest: 4.41.0 + zod: 3.25.76 + dev: false + /@scarf/scarf@1.4.0: resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} requiresBuild: true @@ -16398,6 +16491,10 @@ packages: resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} dev: true + /@sinclair/typebox@0.34.45: + resolution: {integrity: sha512-qJcFVfCa5jxBFSuv7S5WYbA8XdeCPmhnaVVfX/2Y6L8WYg8sk3XY2+6W0zH+3mq1Cz+YC7Ki66HfqX6IHAwnkg==} + dev: false + /@sindresorhus/is@0.14.0: resolution: {integrity: sha512-9NET910DNaIPngYnLLPeg+Ogzqsi9uM4mSboU5y6p8S5DzMTVEsJZrawi+BoDNUVBa2DhJqQYUFvMDfgU062LQ==} engines: {node: '>=6'} @@ -19064,6 +19161,13 @@ packages: /@ungap/structured-clone@1.3.0: resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} + /@unhead/schema@1.11.20: + resolution: {integrity: sha512-0zWykKAaJdm+/Y7yi/Yds20PrUK7XabLe9c3IRcjnwYmSWY6z0Cr19VIs3ozCj8P+GhR+/TI2mwtGlueCEYouA==} + dependencies: + hookable: 5.5.3 + zhead: 2.2.4 + dev: false + /@unhead/vue@2.0.19(vue@3.5.22): resolution: {integrity: sha512-7BYjHfOaoZ9+ARJkT10Q2TjnTUqDXmMpfakIAsD/hXiuff1oqWg1xeXT5+MomhNcC15HbiABpbbBmITLSHxdKg==} peerDependencies: @@ -19318,7 +19422,7 @@ packages: '@babel/core': 7.28.5 '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5) '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.5) - '@rolldown/pluginutils': 1.0.0-beta.54 + '@rolldown/pluginutils': 1.0.0-beta.57 '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.5) vite: 7.2.7(@types/node@24.2.1)(jiti@2.6.1) vue: 3.5.22(typescript@5.9.2) @@ -19538,10 +19642,10 @@ packages: js-yaml: 4.1.0 linear-sum-assignment: 1.0.7 mustache: 4.2.0 - openai: 5.23.2(zod@4.1.13) + openai: 5.23.2(zod@4.2.1) ts-pattern: 5.8.0 vite-node: 3.2.4(@types/node@24.2.1)(jiti@2.5.1)(tsx@4.20.4) - zod: 4.1.13 + zod: 4.2.1 transitivePeerDependencies: - '@types/node' - encoding @@ -19592,7 +19696,7 @@ packages: tailwind-merge: 3.4.0 tailwindcss: 4.1.14 tw-animate-css: 1.4.0 - zod: 4.1.13 + zod: 4.2.1 transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -19643,10 +19747,104 @@ packages: resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} dev: false + /@voltagent/a2a-server@1.0.2(@voltagent/core@1.5.2): + resolution: {integrity: sha512-WIPDM0iYvcEPKbwbYt9n1TQv9jjRUcxvAgeCAiVD8EzYYrJvHXHCHC6satpeQF8N/0jFKtEMq8a+5/3cfwM/hQ==} + peerDependencies: + '@voltagent/core': ^1.0.0 + dependencies: + '@a2a-js/sdk': 0.2.5 + '@voltagent/core': 1.5.2(@ai-sdk/provider-utils@3.0.19)(ai@5.0.113)(zod@3.25.76) + '@voltagent/internal': 0.0.12 + zod: 3.25.76 + transitivePeerDependencies: + - supports-color + dev: false + + /@voltagent/core@1.5.2(@ai-sdk/provider-utils@3.0.19)(ai@5.0.113)(zod@3.25.76): + resolution: {integrity: sha512-g5zqV46dJle0BykMvJNT2/69bRe63SOYCWFsNfioptq0ayuKZ2rrwKd4m7FPTBD926FPlJx4NzbPARONeWYl4w==} + peerDependencies: + '@ai-sdk/provider-utils': 3.x + '@voltagent/logger': 1.x + ai: 5.x + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + '@voltagent/logger': + optional: true + dependencies: + '@ai-sdk/provider-utils': 3.0.19(zod@3.25.76) + '@modelcontextprotocol/sdk': 1.24.3(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.204.0 + '@opentelemetry/context-async-hooks': 2.2.0(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-logs-otlp-http': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/exporter-trace-otlp-http': 0.203.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.204.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 2.1.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': 2.0.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.36.0 + '@voltagent/internal': 0.0.12 + ai: 5.0.113(zod@3.25.76) + ts-pattern: 5.8.0 + type-fest: 4.41.0 + uuid: 9.0.1 + zod: 3.25.76 + zod-from-json-schema: 0.5.0 + zod-from-json-schema-v3: /zod-from-json-schema@0.0.5 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + dev: false + + /@voltagent/internal@0.0.12: + resolution: {integrity: sha512-S7ANIskxtCc+n0YzwULfvnzPyEC7YvvV+0chgpV+rW5EQbvLuzKSSrC5gmrSgzDNi+WgODysM3acZ2F9MuhLeQ==} + dependencies: + type-fest: 4.41.0 + dev: false + /@voltagent/internal@0.0.9: resolution: {integrity: sha512-Kaa2jW60VsfYVotuXC81LmNOJ07Lf1yq36vMteNKKa5seIsKkJ75PvIbMp52eEZ/ky/oBXrs94UXrQNqXBJ80Q==} dev: false + /@voltagent/mcp-server@1.0.3(@voltagent/core@1.5.2)(zod@3.25.76): + resolution: {integrity: sha512-2VsGTyb5DayiHOWZIeClNM56IAkJmjAeh5aAJLZuY0+nGzoLhANzCUyh3Z4pYGxjOKT5ohM0eWPZwMf/5O4dXA==} + peerDependencies: + '@voltagent/core': ^1.0.0 + zod: ^3.25.0 || ^4.0.0 + dependencies: + '@modelcontextprotocol/sdk': 1.24.3(zod@3.25.76) + '@voltagent/core': 1.5.2(@ai-sdk/provider-utils@3.0.19)(ai@5.0.113)(zod@3.25.76) + '@voltagent/internal': 0.0.12 + zod: 3.25.76 + transitivePeerDependencies: + - '@cfworker/json-schema' + - supports-color + dev: false + + /@voltagent/server-core@1.0.36(@voltagent/core@1.5.2)(zod@3.25.76): + resolution: {integrity: sha512-IpXiSI6LFfdkUNSLJREMK3rpgkkMPhED8ln21EaapKCrdVhnBFTDIIPU7+JHsM6sbSzsVyIjfFDC9BN2++kGZg==} + peerDependencies: + '@voltagent/core': ^1.0.0 + zod: ^3.25.0 || ^4.0.0 + dependencies: + '@modelcontextprotocol/sdk': 1.24.3(zod@3.25.76) + '@voltagent/core': 1.5.2(@ai-sdk/provider-utils@3.0.19)(ai@5.0.113)(zod@3.25.76) + '@voltagent/internal': 0.0.12 + ai: 5.0.113(zod@3.25.76) + jsonwebtoken: 9.0.2 + ws: 8.18.3 + zod: 3.25.76 + zod-from-json-schema: 0.5.0 + zod-from-json-schema-v3: /zod-from-json-schema@0.0.5 + transitivePeerDependencies: + - '@cfworker/json-schema' + - bufferutil + - supports-color + - utf-8-validate + dev: false + /@voltagent/vercel-ai@1.0.0(@voltagent/core@packages+core)(zod@3.25.76): resolution: {integrity: sha512-ooknKRG0G79YuHJAPuZ5Xoy1oiirq0XfyS5ncKO2+yMqasRSn5phd8wHq5x4nVwRFMY1mulo3YZJ2QGQLj1mIg==} peerDependencies: @@ -20513,17 +20711,17 @@ packages: '@opentelemetry/api': 1.9.0 zod: 3.25.76 - /ai@6.0.3(zod@4.1.13): + /ai@6.0.3(zod@4.2.1): resolution: {integrity: sha512-OOo+/C+sEyscoLnbY3w42vjQDICioVNyS+F+ogwq6O5RJL/vgWGuiLzFwuP7oHTeni/MkmX8tIge48GTdaV7QQ==} engines: {node: '>=18'} peerDependencies: zod: ^3.25.76 || ^4.1.8 dependencies: - '@ai-sdk/gateway': 3.0.2(zod@4.1.13) + '@ai-sdk/gateway': 3.0.2(zod@4.2.1) '@ai-sdk/provider': 3.0.0 - '@ai-sdk/provider-utils': 4.0.1(zod@4.1.13) + '@ai-sdk/provider-utils': 4.0.1(zod@4.2.1) '@opentelemetry/api': 1.9.0 - zod: 4.1.13 + zod: 4.2.1 /ajv-errors@3.0.0(ajv@8.17.1): resolution: {integrity: sha512-V3wD15YHfHz6y0KdhYFjyy9vWtEVALT9UrxfN3zqlI6dMioHnJrqOYfyPKol3oqrnCM9uwkcdCwkJ0WUcbLMTQ==} @@ -20889,7 +21087,7 @@ packages: mustache: 4.2.0 openai: 4.104.0(ws@8.18.3)(zod@3.25.76) zod: 3.25.76 - zod-to-json-schema: 3.24.6(zod@3.25.76) + zod-to-json-schema: 3.25.0(zod@3.25.76) transitivePeerDependencies: - encoding - ws @@ -22415,7 +22613,7 @@ packages: resolve-package-path: 4.0.3 uuid: 10.0.0 zod: 3.25.76 - zod-to-json-schema: 3.24.6(zod@3.25.76) + zod-to-json-schema: 3.25.0(zod@3.25.76) transitivePeerDependencies: - debug dev: false @@ -22647,6 +22845,11 @@ packages: resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==} engines: {node: '>=18'} + /cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + dev: false + /copy-anything@3.0.5: resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} engines: {node: '>=12.13'} @@ -23965,6 +24168,31 @@ packages: - encoding dev: false + /elysia@1.4.19(@sinclair/typebox@0.34.45)(exact-mirror@0.2.5)(file-type@21.0.0)(openapi-types@12.1.3)(typescript@5.9.2): + resolution: {integrity: sha512-DZb9y8FnWyX5IuqY44SvqAV0DjJ15NeCWHrLdgXrKgTPDPsl3VNwWHqrEr9bmnOCpg1vh6QUvAX/tcxNj88jLA==} + peerDependencies: + '@sinclair/typebox': '>= 0.34.0 < 1' + '@types/bun': '>= 1.2.0' + exact-mirror: '>= 0.0.9' + file-type: '>= 20.0.0' + openapi-types: '>= 12.0.0' + typescript: '>= 5.0.0' + peerDependenciesMeta: + '@types/bun': + optional: true + typescript: + optional: true + dependencies: + '@sinclair/typebox': 0.34.45 + cookie: 1.1.1 + exact-mirror: 0.2.5(@sinclair/typebox@0.34.45) + fast-decode-uri-component: 1.0.1 + file-type: 21.0.0 + memoirist: 0.4.0 + openapi-types: 12.1.3 + typescript: 5.9.2 + dev: false + /embla-carousel-auto-height@8.6.0(embla-carousel@8.6.0): resolution: {integrity: sha512-/HrJQOEM6aol/oF33gd2QlINcXy3e19fJWvHDuHWp2bpyTa+2dm9tVVJak30m2Qy6QyQ6Fc8DkImtv7pxWOJUQ==} peerDependencies: @@ -24679,6 +24907,17 @@ packages: dependencies: eventsource-parser: 3.0.6 + /exact-mirror@0.2.5(@sinclair/typebox@0.34.45): + resolution: {integrity: sha512-u8Wu2lO8nio5lKSJubOydsdNtQmH8ENba5m0nbQYmTvsjksXKYIS1nSShdDlO8Uem+kbo+N6eD5I03cpZ+QsRQ==} + peerDependencies: + '@sinclair/typebox': ^0.34.15 + peerDependenciesMeta: + '@sinclair/typebox': + optional: true + dependencies: + '@sinclair/typebox': 0.34.45 + dev: false + /execa@0.7.0: resolution: {integrity: sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==} engines: {node: '>=4'} @@ -24942,7 +25181,6 @@ packages: /fast-decode-uri-component@1.0.1: resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} - dev: true /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -29795,6 +30033,10 @@ packages: fs-monkey: 1.1.0 dev: true + /memoirist@0.4.0: + resolution: {integrity: sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg==} + dev: false + /memoize-one@6.0.0: resolution: {integrity: sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==} dev: true @@ -32122,7 +32364,7 @@ packages: dependencies: zod: 3.25.76 - /openai@5.23.2(zod@4.1.13): + /openai@5.23.2(zod@4.2.1): resolution: {integrity: sha512-MQBzmTulj+MM5O8SKEk/gL8a7s5mktS9zUtAkU257WjvobGc9nKcBuVwjyEEcb9SI8a8Y2G/mzn3vm9n1Jlleg==} hasBin: true peerDependencies: @@ -32134,7 +32376,7 @@ packages: zod: optional: true dependencies: - zod: 4.1.13 + zod: 4.2.1 dev: true /openapi-types@12.1.3: @@ -39386,6 +39628,10 @@ packages: cookie: 1.0.2 youch-core: 0.3.3 + /zhead@2.2.4: + resolution: {integrity: sha512-8F0OI5dpWIA5IGG5NHUg9staDwz/ZPxZtvGVf01j7vHqSyZ0raHY+78atOVxRqb73AotX22uV1pXt3gYSstGag==} + dev: false + /zip-stream@6.0.1: resolution: {integrity: sha512-zK7YHHz4ZXpW89AHXUPbQVGKI7uvkd3hzusTdotCg1UxyaVtg0zFJSTfW/Dq5f7OBBVnq6cZIaC8Ti4hb6dtCA==} engines: {node: '>= 14'} @@ -39406,19 +39652,20 @@ packages: zod: 4.1.13 dev: false - /zod-to-json-schema@3.24.6(zod@3.25.76): - resolution: {integrity: sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==} + /zod-to-json-schema@3.25.0(zod@3.25.76): + resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==} peerDependencies: - zod: ^3.24.1 + zod: ^3.25 || ^4 dependencies: zod: 3.25.76 - /zod-to-json-schema@3.25.0(zod@3.25.76): - resolution: {integrity: sha512-HvWtU2UG41LALjajJrML6uQejQhNJx+JBO9IflpSja4R03iNWfKXrj6W2h7ljuLyc1nKS+9yDyL/9tD1U/yBnQ==} + /zod-to-json-schema@3.25.1(zod@3.25.76): + resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: zod: ^3.25 || ^4 dependencies: zod: 3.25.76 + dev: false /zod-validation-error@3.5.3(zod@3.25.76): resolution: {integrity: sha512-OT5Y8lbUadqVZCsnyFaTQ4/O2mys4tj7PqhdbBCp7McPwvIEKfPtdA6QfPeFQK2/Rz5LgwmAXRJTugBNBi0btw==} @@ -39441,6 +39688,10 @@ packages: /zod@4.1.13: resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==} + dev: false + + /zod@4.2.1: + resolution: {integrity: sha512-0wZ1IRqGGhMP76gLqz8EyfBXKk0J2qo2+H3fi4mcUP/KtTocoX08nmIAHl1Z2kJIZbZee8KOpBCSNPRgauucjw==} /zustand@4.5.7(@types/react@19.1.10)(react@19.2.3): resolution: {integrity: sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==} diff --git a/website/docs/api/authentication.md b/website/docs/api/authentication.md index e310207b3..27b7c9383 100644 --- a/website/docs/api/authentication.md +++ b/website/docs/api/authentication.md @@ -27,7 +27,9 @@ All endpoints are publicly accessible. ### Option 2: authNext (Recommended) -Protect everything by default. Explicitly allow public routes, and use a Console Access Key for management endpoints: +Protect everything by default. Explicitly allow public routes, and use a Console Access Key for management endpoints. + +#### Using Hono ```typescript import { jwtAuth } from "@voltagent/server-core"; @@ -46,6 +48,25 @@ new VoltAgent({ }); ``` +#### Using Elysia + +```typescript +import { jwtAuth } from "@voltagent/server-elysia"; +import { elysiaServer } from "@voltagent/server-elysia"; + +new VoltAgent({ + agents: { myAgent }, + server: elysiaServer({ + authNext: { + provider: jwtAuth({ + secret: process.env.JWT_SECRET!, + }), + publicRoutes: ["GET /health"], + }, + }), +}); +``` + Legacy `auth` is still supported for existing integrations. See the **Legacy auth** section at the end for details. ## Concepts diff --git a/website/docs/api/custom-endpoints.md b/website/docs/api/custom-endpoints.md index a511f1091..bf7763b7d 100644 --- a/website/docs/api/custom-endpoints.md +++ b/website/docs/api/custom-endpoints.md @@ -9,11 +9,11 @@ VoltAgent Server allows you to add custom REST endpoints alongside the built-in ## Overview -With `@voltagent/server-hono`, you can add custom routes using the `configureApp` callback which gives you direct access to the Hono app instance. +Both `@voltagent/server-hono` and `@voltagent/server-elysia` allow you to add custom routes using the `configureApp` callback, which gives you direct access to the underlying app instance. -## Basic Setup +## Using Hono -Add custom endpoints through the server configuration: +Add custom endpoints through the Hono server configuration: ```typescript import { VoltAgent } from "@voltagent/core"; @@ -36,6 +36,30 @@ new VoltAgent({ }); ``` +## Using Elysia + +Add custom endpoints through the Elysia server configuration: + +```typescript +import { VoltAgent } from "@voltagent/core"; +import { elysiaServer } from "@voltagent/server-elysia"; + +new VoltAgent({ + agents: { myAgent }, + server: elysiaServer({ + configureApp: (app) => { + // Add custom routes here + app.get("/api/health", () => ({ status: "healthy" })); + + app.post("/api/data", ({ body }) => { + // Process data + return { success: true, data: body }; + }); + }, + }), +}); +``` + ## CORS Configuration Configure Cross-Origin Resource Sharing (CORS) for your API using the `cors` field in server configuration. By default, VoltAgent allows all origins (`*`). diff --git a/website/docs/api/overview.md b/website/docs/api/overview.md index 6c8e023ee..a4e4807ef 100644 --- a/website/docs/api/overview.md +++ b/website/docs/api/overview.md @@ -13,9 +13,12 @@ VoltAgent 1.x introduces a pluggable server architecture: - **`@voltagent/server-core`** - Framework-agnostic core with route definitions, handlers, and base provider - **`@voltagent/server-hono`** - Official server implementation using [Hono](https://hono.dev/) (recommended) +- **`@voltagent/server-elysia`** - High-performance server implementation using [Elysia](https://elysiajs.com/) ## Quick Start +### Using Hono (Recommended) + ```typescript import { Agent, VoltAgent } from "@voltagent/core"; import { honoServer } from "@voltagent/server-hono"; @@ -36,6 +39,28 @@ new VoltAgent({ }); ``` +### Using Elysia + +```typescript +import { Agent, VoltAgent } from "@voltagent/core"; +import { elysiaServer } from "@voltagent/server-elysia"; +import { openai } from "@ai-sdk/openai"; + +const agent = new Agent({ + name: "Assistant", + instructions: "You are a helpful assistant", + model: openai("gpt-4o-mini"), +}); + +new VoltAgent({ + agents: { agent }, + server: elysiaServer({ + port: 3141, + enableSwaggerUI: true, + }), +}); +``` + The server starts automatically and displays: ``` diff --git a/website/docs/api/server-architecture.md b/website/docs/api/server-architecture.md index 5cf5bfba9..187445d19 100644 --- a/website/docs/api/server-architecture.md +++ b/website/docs/api/server-architecture.md @@ -30,6 +30,15 @@ The official server implementation using [Hono](https://hono.dev/): - TypeScript-first with excellent DX - Works with Node.js, Bun, Deno, and Edge runtimes +### @voltagent/server-elysia + +High-performance server implementation using [Elysia](https://elysiajs.com/): + +- Extremely fast performance (optimized for Bun) +- End-to-end type safety with Eden +- Built-in OpenAPI and Swagger UI support +- Excellent developer experience with strict typing + ## Core Concepts ### Server Provider Interface @@ -214,6 +223,75 @@ new VoltAgent({ }); ``` +## Using the Elysia Server + +### Basic Setup + +```typescript +import { VoltAgent } from "@voltagent/core"; +import { elysiaServer } from "@voltagent/server-elysia"; + +new VoltAgent({ + agents: { myAgent }, + server: elysiaServer({ + port: 3141, + enableSwaggerUI: true, + }), +}); +``` + +### Configuration Options + +```typescript +interface ElysiaServerConfig { + // Port to listen on (default: 3141) + port?: number; + + // Hostname to bind the server to (default: "0.0.0.0") + hostname?: string; + + // Enable Swagger UI (default: true in dev, false in prod) + enableSwaggerUI?: boolean; + + // Configure the Elysia app directly + configureApp?: (app: Elysia) => void | Promise; + + // Configure the full app (including routes and middleware) + configureFullApp?: (params: { + app: Elysia; + routes: Record void>; + middlewares: Record void>; + }) => void | Promise; + + // Authentication provider + authNext?: AuthNextConfig; +} +``` + +### Advanced Configuration + +Access the Elysia app directly for custom middleware and routes: + +```typescript +new VoltAgent({ + agents: { myAgent }, + server: elysiaServer({ + configureApp: (app) => { + // Add custom middleware + app.derive(({ request }) => ({ + userId: request.headers.get("x-user-id"), + })); + + // Add custom routes + app.get("/health", () => ({ status: "ok" })); + + // Create route groups + app.group("/api/v2", (app) => app.get("/users", getUsersHandler)); + }, + }), +}); +``` + ## Port Management VoltAgent includes intelligent port management: