From 29f0dd7af0670a1b9f42b53f7d9bfa27d25dc161 Mon Sep 17 00:00:00 2001 From: "glm-5.1" Date: Thu, 30 Apr 2026 12:34:26 +0000 Subject: [PATCH] Initial package implementation: operations registry, call protocol, and adapters Extracted from alkhub_ts packages/core/operations/ and packages/core/mcp/. - Runtime-agnostic (injected fs/env deps, no Deno globals) - Direct @logtape/logtape import instead of logger wrapper - PendingRequestMap with pubsub-wired call protocol - Peer-dep isolation for MCP adapter (sub-path export) - Schema const naming convention (XSchema + X type alias) - 68 tests passing, build + lint + test all green --- .gitignore | 5 + AGENTS.md | 131 + docs/architecture.md | 20 + docs/architecture/README.md | 103 + docs/architecture/adapters.md | 280 + docs/architecture/api-surface.md | 314 + docs/architecture/build-distribution.md | 137 + docs/architecture/call-protocol.md | 283 + .../decisions/001-logger-direct-import.md | 35 + .../decisions/002-fs-injection.md | 48 + .../decisions/003-peer-dep-adapters.md | 54 + .../decisions/004-schema-const-naming.md | 50 + docs/research/migration.md | 135 + package-lock.json | 5474 +++++++++++++++++ package.json | 79 + src/call.ts | 249 + src/env.ts | 56 + src/error.ts | 51 + src/from_mcp.ts | 151 + src/from_openapi.ts | 339 + src/from_schema.ts | 115 + src/index.ts | 18 + src/registry.ts | 78 + src/scanner.ts | 97 + src/subscribe.ts | 28 + src/types.ts | 144 + src/validation.ts | 44 + test/call.test.ts | 87 + test/env.test.ts | 93 + test/error.test.ts | 65 + test/from_openapi.test.ts | 194 + test/from_schema.test.ts | 110 + test/registry.test.ts | 101 + test/validation.test.ts | 77 + tsconfig.json | 20 + tsup.config.ts | 14 + vitest.config.ts | 8 + 37 files changed, 9287 insertions(+) create mode 100644 .gitignore create mode 100644 AGENTS.md create mode 100644 docs/architecture.md create mode 100644 docs/architecture/README.md create mode 100644 docs/architecture/adapters.md create mode 100644 docs/architecture/api-surface.md create mode 100644 docs/architecture/build-distribution.md create mode 100644 docs/architecture/call-protocol.md create mode 100644 docs/architecture/decisions/001-logger-direct-import.md create mode 100644 docs/architecture/decisions/002-fs-injection.md create mode 100644 docs/architecture/decisions/003-peer-dep-adapters.md create mode 100644 docs/architecture/decisions/004-schema-const-naming.md create mode 100644 docs/research/migration.md create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 src/call.ts create mode 100644 src/env.ts create mode 100644 src/error.ts create mode 100644 src/from_mcp.ts create mode 100644 src/from_openapi.ts create mode 100644 src/from_schema.ts create mode 100644 src/index.ts create mode 100644 src/registry.ts create mode 100644 src/scanner.ts create mode 100644 src/subscribe.ts create mode 100644 src/types.ts create mode 100644 src/validation.ts create mode 100644 test/call.test.ts create mode 100644 test/env.test.ts create mode 100644 test/error.test.ts create mode 100644 test/from_openapi.test.ts create mode 100644 test/from_schema.test.ts create mode 100644 test/registry.test.ts create mode 100644 test/validation.test.ts create mode 100644 tsconfig.json create mode 100644 tsup.config.ts create mode 100644 vitest.config.ts diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..55efacd --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +*.tgz +coverage/ +.vitest/ \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..4a624d6 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,131 @@ +## Memory Tools (via @alkdev/open-memory plugin) + +You have access to two tools for managing your context and accessing session history: + +### memory({tool: "...", args: {...}}) + +Read-only tool for introspecting your session history and context state. Available operations: +- `memory({tool: "help"})` — full reference with examples +- `memory({tool: "summary"})` — quick counts of projects, sessions, messages, todos +- `memory({tool: "sessions"})` — list recent sessions (useful for finding past work) +- `memory({tool: "children", args: {sessionId: "ses_..."}})` — list sub-agent sessions spawned from a parent +- `memory({tool: "messages", args: {sessionId: "..."}})` — read a session's conversation +- `memory({tool: "messages", args: {sessionId: "...", role: "assistant"}})` — read only assistant messages +- `memory({tool: "messages", args: {sessionId: "...", showTools: true}})` — include tool-call output +- `memory({tool: "message", args: {messageId: "msg_..."}})` — read a single message by ID +- `memory({tool: "search", args: {query: "..."}})` — search across all conversations +- `memory({tool: "compactions", args: {sessionId: "..."}})` — view compaction checkpoints +- `memory({tool: "context"})` — check your current context window usage + +### memory_compact() + +Trigger compaction on the current session. This summarizes the conversation so far to free context space. + +**When to use memory_compact:** +- When context is above 80% (check with `memory({tool: "context"})`) +- When you notice you're losing track of earlier conversation details +- At natural breakpoints in multi-step tasks (after completing a subtask, before starting a new one) +- When the system prompt shows a yellow/red/critical context warning +- Proactively, rather than waiting for automatic compaction at 92% + +**When NOT to use memory_compact:** +- When context is below 50% (it wastes a compaction cycle) +- In the middle of a complex edit that you need immediate context for +- When the task is nearly complete (just finish the task instead) + +Compaction preserves your most important context in a structured summary — you will continue the session with the summary as your starting point. + +## Worktree Tool (via @alkimiadev/open-coordinator plugin) + +You have access to the `worktree` tool for git worktree management and session coordination. Call with `{action, args}`: + +### Coordinator Operations (available when session is not spawned by another session) + +- `worktree({action: "list"})` — List git worktrees +- `worktree({action: "dashboard"})` — Worktree dashboard with session info +- `worktree({action: "spawn", args: {tasks: [...], prompt: "..."}})` — Spawn parallel worktrees + sessions +- `worktree({action: "sessions"})` — Query spawned session status +- `worktree({action: "message", args: {sessionID: "...", message: "..."}})` — Message a session +- `worktree({action: "abort", args: {sessionID: "..."}})` — Abort a session +- `worktree({action: "cleanup", args: {action: "remove", pathOrBranch: "..."}})` — Remove worktree +- `worktree({action: "help"})` — Show all available operations + +### Implementation Operations (available when session is spawned by a coordinator) + +- `worktree({action: "current"})` — Show your worktree mapping +- `worktree({action: "notify", args: {message: "...", level: "info|blocking"}})` — Report to coordinator +- `worktree({action: "status"})` — Show worktree git status + +The plugin auto-injects `workdir` for bash commands when the session is mapped to a worktree. + +## Project: @alkdev/operations + +Runtime-agnostic TypeScript package for typed operations registry, call protocol, and adapters (MCP, OpenAPI). Extracted from `alkhub_ts` as a standalone `@alkdev/operations` package. Dual-licensed MIT / Apache-2.0. + +### Commands + +- `npm run build` — Build with tsup (ESM + CJS + declarations) +- `npm run lint` — Type-check with `tsc --noEmit` +- `npm test` — Run tests with vitest +- `npm run test:watch` — Watch mode +- `npm run test:coverage` — Coverage report (v8) + +### Architecture + +See `docs/architecture/` for full specs. Key points: + +- **Operations**: Everything is a typed operation with TypeBox schemas. `IOperationDefinition` defines name, namespace, type (QUERY/MUTATION/SUBSCRIPTION), input/output schemas, access control, and handler. +- **Call protocol**: `call ≡ subscribe` — same event types, same PendingRequestMap. A call resolves after one event; a subscription stays open and yields events until stopped. Same message format, different consumption pattern. +- **PendingRequestMap**: Full call protocol implementation with pubsub wiring, `call()`, `subscribe()`, deadline timeout. +- **Adapters**: `from_openapi`, `from_schema`, `from_mcp` register remote operations in the registry. MCP and OpenAPI are peer-dep adapters. +- **Logger**: Direct `@logtape/logtape` import, no wrapper. +- **No Effect**: Plain async/await throughout. +- **No Zod**: TypeBox for all runtime schemas. + +### Source Layout + +``` +src/ + index.ts — Public API surface (all exports) + types.ts — IOperationDefinition, OperationType, CallEventMap + registry.ts — OperationRegistry (register, execute, subscribe) + validation.ts — Input/output schema validation + call.ts — PendingRequestMap, call(), subscribe(), CallHandler + subscribe.ts — Subscription support (AsyncIterable operations) + error.ts — CallError, mapError, infrastructure codes + env.ts — OperationEnvironment + scanner.ts — Auto-discover operations from filesystem (Deno/Node agnostic) + from_schema.ts — Register operations from TypeBox schema definitions + from_openapi.ts — Register operations from OpenAPI specs + from_mcp.ts — Register operations from MCP servers +``` + +### Dependencies + +Runtime: `@alkdev/typebox`, `@alkdev/pubsub`, `@logtape/logtape` +Peer: `@modelcontextprotocol/sdk` (from_mcp), `@std/path` (scanner) +Dev: `tsup`, `typescript`, `vitest`, `@vitest/coverage-v8` + +### Constraints + +- Runtime-agnostic: must work in both Node.js and Deno (inject fs/env deps, no `Deno.*` globals in source) +- TypeBox for all schemas (not Zod) +- No Effect dependency +- No mocked/stubbed implementations — real code only +- No comments in source unless explicitly asked + +### Provenance + +| Module | Origin | Status | +|--------|--------|--------| +| types.ts | Copied from `alkhub_ts/packages/core/operations/types.ts` | Migrating | +| registry.ts | Copied from `alkhub_ts/packages/core/operations/registry.ts` | Migrating | +| validation.ts | Copied from `alkhub_ts/packages/core/operations/validation.ts` | Migrating | +| env.ts | Copied from `alkhub_ts/packages/core/operations/env.ts` | Migrating, needs PendingRequestMap impl | +| scanner.ts | Copied from `alkhub_ts/packages/core/operations/scanner.ts` | Migrating, needs fs injection | +| from_schema.ts | Copied from `alkhub_ts/packages/core/operations/from_schema.ts` | Migrating | +| from_openapi.ts | Copied from `alkhub_ts/packages/core/operations/from_openapi.ts` | Migrating, needs env+fs injection | +| from_mcp.ts | Copied from `alkhub_ts/packages/core/mcp/wrapper.ts` + `loader.ts` | Migrating, needs path+dep updates | +| call.ts | New | Not started | +| subscribe.ts | New | Not started | +| error.ts | New | Not started | \ No newline at end of file diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..6c18f73 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,20 @@ +# Architecture + +> **This document has been decomposed into modular documents.** See [docs/architecture/](architecture/) for the current architecture specification. + +| Document | Content | +|----------|---------| +| [architecture/README.md](architecture/README.md) | Overview, why this exists, what it provides, consumer context, threat model | +| [architecture/api-surface.md](architecture/api-surface.md) | All public types, registry API, call protocol API, subscribe, env, adapters | +| [architecture/call-protocol.md](architecture/call-protocol.md) | PendingRequestMap, CallHandler, call≡subscribe semantics, events, error model, access control | +| [architecture/adapters.md](architecture/adapters.md) | from_schema, from_openapi, from_mcp, scanner — how they work, how to add new adapters | +| [architecture/build-distribution.md](architecture/build-distribution.md) | Dependencies, project structure, sub-path exports, peer deps, build tooling | + +### Design Decisions + +| ADR | Decision | +|-----|----------| +| [001](architecture/decisions/001-logger-direct-import.md) | Direct @logtape/logtape import instead of wrapper module | +| [002](architecture/decisions/002-fs-injection.md) | Inject filesystem dependencies for runtime agnosticism | +| [003](architecture/decisions/003-peer-dep-adapters.md) | Peer dependencies for adapter isolation (MCP SDK, @std/path) | +| [004](architecture/decisions/004-schema-const-naming.md) | Schema const naming convention (AccessControlSchema + AccessControl type) | \ No newline at end of file diff --git a/docs/architecture/README.md b/docs/architecture/README.md new file mode 100644 index 0000000..a76f482 --- /dev/null +++ b/docs/architecture/README.md @@ -0,0 +1,103 @@ +--- +status: draft +last_updated: 2026-04-30 +--- + +# @alkdev/operations Architecture + +Typed operations registry, call protocol, and adapters (MCP, OpenAPI). Everything is an operation with TypeBox schemas, access control metadata, and a handler. The call protocol provides unified event-based invocation that works the same whether local, remote, or streamed. + +## Why This Exists + +Extracted from `@alkdev/alkhub_ts/packages/core/operations/` and `packages/core/mcp/`. The operations system was already self-contained within alkhub, depending only on `@alkdev/typebox` and `@alkdev/pubsub`. Extracting into a standalone package: + +1. **Reduces coupling** — alkhub depends on operations, not the other way around +2. **Enables reuse** — multiple alkhub packages and external consumers can share the same operations registry and call protocol +3. **Isolates peer deps** — MCP SDK and other heavy dependencies are optional; consumers that don't need them shouldn't carry them +4. **Standalone utility** — the call protocol, validation, and schema conversion are useful outside alkhub (e.g., opencode OpenAPI import) + +## Core Principle + +**The operation definition is the contract.** Every API endpoint, agent action, coordination tool, and MCP tool is an `IOperationDefinition` with typed input/output schemas, access control, and a handler. The registry executes them. The call protocol routes them. Adapters generate them from external specs. + +All paths funnel into the same registry: + +``` +Hub HTTP API routes → registry.execute("namespace.operation", input, ctx) +MCP server tools → registry.execute(...) +FromOpenAPI ops → fetch(remote REST API) +MCP client tools → MCPClientLoader → registry.execute(...) +Agent session LLM → tool calls with JSON Schema → registry.execute(...) +``` + +Access control, validation, and error handling are consistent regardless of entry point. + +## What This Package Provides + +- **Core types** — `IOperationDefinition`, `OperationSpec`, `OperationType`, `AccessControl`, `Identity`, `OperationContext` +- **Registry** — `OperationRegistry` with register, execute, validate, spec extraction +- **Call protocol** — `PendingRequestMap`, `CallHandler`, `call≡subscribe` event semantics +- **Subscribe** — `subscribe()` for `AsyncGenerator`-based subscription operations +- **Env builder** — `buildEnv()` for nested operation calls (direct or call protocol mode) +- **Validation** — `assertIsSchema`, `validateOrThrow`, `collectErrors`, `formatValueErrors` +- **Error model** — `CallError`, `InfrastructureErrorCode`, `mapError` +- **Schema conversion** — `FromSchema` converts JSON Schema to TypeBox +- **Adapters**: + - `FromOpenAPI` / `FromOpenAPIFile` / `FromOpenAPIUrl` — OpenAPI spec to operations + - `createMCPClient` / `MCPClientLoader` — MCP server tools to operations (peer dep: `@modelcontextprotocol/sdk`) + - `scanOperations` — filesystem auto-discovery of operation definitions + +## Consumer Context + +### alkhub (hub-spoke coordinator) + +The hub uses the operations registry as the single execution engine for all work. Operations are registered from multiple sources (local definitions, OpenAPI imports, MCP tool connections). The call protocol routes invocations through `PendingRequestMap` for call graph tracking, abort cascading, and structured error handling. + +### opencode (agent tool use) + +`FromOpenAPI` generates typed operation definitions from any OpenAPI spec. This provides an instant typed client without hand-writing handlers. MCP tool connections are managed through `MCPClientLoader`. + +### Spoke SDK (future) + +Spokes will import `@alkdev/operation` for operation definitions and `@alkdev/pubsub` for the call protocol event transport. The `buildEnv` call protocol mode connects nested operations through `PendingRequestMap`. + +## Threat Model + +- **Schema trust** — `FromSchema` converts arbitrary JSON Schema to TypeBox. Malformed or deeply nested schemas could cause excessive CPU or memory. Input validation (`validateOrThrow`) runs before handler execution, but the schemas themselves are trusted. +- **Handler trust** — operation handlers are arbitrary async functions. The registry runs them in the same process. No sandboxing. +- **Peer dep isolation** — `@modelcontextprotocol/sdk` is an optional peer dependency. Consumers that don't use `from_mcp` don't install it. The sub-path export `@alkdev/operations/from-mcp` makes this explicit. +- **Access control enforcement** — `CallHandler` checks `AccessControl` before dispatch. Direct `registry.execute()` calls bypass access control by design (internal trusted calls). Untrusted callers must use the call protocol. + +## Architecture Documents + +| Document | Content | +|----------|---------| +| [api-surface.md](api-surface.md) | All public types, registry, call protocol, subscribe, env, adapters | +| [call-protocol.md](call-protocol.md) | PendingRequestMap, CallHandler, call≡subscribe, events, error model, access control | +| [adapters.md](adapters.md) | from_schema, from_openapi, from_mcp, scanner — how they work, how to add new adapters | +| [build-distribution.md](build-distribution.md) | Dependencies, project structure, sub-path exports, peer deps, build tooling | + +## Document Lifecycle + +Architecture documents use YAML frontmatter with `status` and `last_updated` fields: + +```yaml +--- +status: draft | stable | deprecated +last_updated: YYYY-MM-DD +--- +``` + +| Status | Meaning | Transitions | +|--------|---------|-------------| +| `draft` | Under active development. Content may change. | → `stable` when implementation is complete and tests verify API contract. | +| `stable` | API contracts are locked. Changes require review cycle. | → `deprecated` when superseded. | +| `deprecated` | Superseded. Kept for reference. | Removed when no longer referenced. | + +## References + +- Source: `src/` in this package +- Provenance: `@alkdev/alkhub_ts/packages/core/operations/` and `packages/core/mcp/` +- Related: `@alkdev/pubsub` (call protocol transport), `@alkdev/typebox` (schema system) +- alkhub operations doc: `@alkdev/alkhub_ts/docs/architecture/operations.md` +- alkhub call protocol doc: `@alkdev/alkhub_ts/docs/architecture/call-graph.md` \ No newline at end of file diff --git a/docs/architecture/adapters.md b/docs/architecture/adapters.md new file mode 100644 index 0000000..a7e8b89 --- /dev/null +++ b/docs/architecture/adapters.md @@ -0,0 +1,280 @@ +--- +status: draft +last_updated: 2026-04-30 +--- + +# Adapters + +How `FromSchema`, `FromOpenAPI`, `from_mcp`, and `scanner` work. How to add new adapters. + +## FromSchema + +**Source**: `src/from_schema.ts` +**Export**: `FromSchema` (main barrel) + +### Purpose + +Converts JSON Schema to TypeBox `TSchema`. Required because `IOperationDefinition.inputSchema` and `outputSchema` must be TypeBox schemas (for `Value.Check` validation), but external specs (OpenAPI, MCP) provide JSON Schema. + +### Conversion Rules + +| JSON Schema Construct | TypeBox Output | +|----------------------|---------------| +| `allOf` | `Type.IntersectEvaluated(rest, schema)` | +| `anyOf` | `Type.UnionEvaluated(rest, schema)` | +| `oneOf` | `Type.UnionEvaluated(rest, schema)` | +| `enum` | `Type.UnionEvaluated(literals)` | +| `object` (with `properties` + `required`) | `Type.Object(properties, schema)` — required fields are non-optional, others wrapped in `Type.Optional()` | +| `array` (with `items` array) | `Type.Tuple(rest, schema)` | +| `array` (with `items` object) | `Type.Array(FromSchema(items), schema)` | +| `const` | `Type.Literal(value, schema)` | +| `$ref` | `Type.Ref(path)` | +| `string` | `Type.String(schema)` | +| `number` | `Type.Number(schema)` | +| `integer` | `Type.Integer(schema)` | +| `boolean` | `Type.Boolean(schema)` | +| `null` | `Type.Null(schema)` | +| Unrecognized | `Type.Unknown(schema)` | + +`$ref` resolution is **not** handled by `FromSchema` — callers must resolve `$ref` pointers to concrete schemas before passing them to `FromSchema`. See `FromOpenAPI` for how `resolveRefsRecursive` handles this. + +### Usage + +```ts +import { FromSchema } from "@alkdev/operations" + +const typeboxSchema = FromSchema({ + type: "object", + properties: { name: { type: "string" } }, + required: ["name"], +}) +``` + +## FromOpenAPI + +**Source**: `src/from_openapi.ts` +**Exports**: `FromOpenAPI`, `FromOpenAPIFile`, `FromOpenAPIUrl` (main barrel); `OpenAPISpec`, `OpenAPIOperation`, `OpenAPIParameter`, `HTTPServiceConfig`, `OpenAPIFS` (types) + +### Purpose + +Generates `IOperationDefinition[]` from OpenAPI specs. Each path+method combination becomes an operation with an auto-generated `fetch` handler. + +### `FromOpenAPI(spec, config)` + +```ts +function FromOpenAPI(spec: OpenAPISpec, config: HTTPServiceConfig): IOperationDefinition[] +``` + +Processes all paths in the spec. For each path and method combination: + +1. **Resolve `$ref`** — `resolveRefsRecursive` resolves all `$ref` pointers in the spec, handling circular references +2. **Build input schema** — merges path parameters, query parameters, and request body into a single `Type.Object` +3. **Build output schema** — extracts response schema from `200`/`201` content, falls back to `Type.Unknown()` +4. **Detect operation type** — `GET` → `QUERY`, `text/event-stream` response → `SUBSCRIPTION`, everything else → `MUTATION` +5. **Generate operation id** — uses `operationId` if present, otherwise normalizes `{method}_{path_parts}` +6. **Create handler** — auto-generated `fetch` handler that: + - Interpolates path parameters into the URL + - Passes query parameters as search params + - Sends request body as JSON + - Applies auth headers from config + - Returns JSON, text, or `ArrayBuffer` based on response content type + +### `FromOpenAPIFile(path, config, fs?)` + +```ts +async function FromOpenAPIFile( + path: string, + config: HTTPServiceConfig, + fs?: OpenAPIFS, +): Promise +``` + +Reads an OpenAPI JSON file. If `fs` is provided, uses `fs.readFile()` (runtime-agnostic). Otherwise, uses Node.js `node:fs/promises`. + +### `FromOpenAPIUrl(url, config)` + +```ts +async function FromOpenAPIUrl( + url: string, + config: HTTPServiceConfig, +): Promise +``` + +Fetches an OpenAPI JSON spec from a URL. + +### `HTTPServiceConfig` + +```ts +interface HTTPServiceConfig { + namespace: string + baseUrl: string + headers?: Record + auth?: { + type: "bearer" | "apiKey" | "basic" + token?: string + headerName?: string + prefix?: string + } + timeout?: number +} +``` + +- `namespace` — operation namespace (e.g., `"opencode"`) +- `baseUrl` — base URL for all requests in this spec +- `auth` — bearer, apiKey (custom header), or basic auth +- `timeout` — `AbortSignal.timeout` for fetch calls + +### `OpenAPIFS` + +```ts +interface OpenAPIFS { + readFile(path: string): Promise +} +``` + +Injectable filesystem interface for runtime-agnostic file reading. See [ADR-002](decisions/002-fs-injection.md). + +### Known Gap: SSE Subscription Handlers + +`FromOpenAPI` correctly detects SSE endpoints (`text/event-stream` → `SUBSCRIPTION`) but the auto-generated handler does a one-shot `fetch` and returns the response body. For `SUBSCRIPTION` operations, the handler should be an async generator that: +1. Calls `fetch()` with the constructed URL/params +2. Reads the response body as a stream +3. Parses SSE frames (`data:` lines, `event:` lines) +4. Yields each parsed event +5. Cleans up on iteration stop + +## from_mcp + +**Source**: `src/from_mcp.ts` +**Exports**: `createMCPClient`, `closeMCPClient`, `MCPClientLoader` (sub-path `@alkdev/operations/from-mcp`) +**Peer dep**: `@modelcontextprotocol/sdk` (optional) + +### Purpose + +Connects to MCP (Model Context Protocol) servers and wraps their tools as `IOperationDefinition[]`. Supports both stdio and HTTP transports. + +### `createMCPClient(name, config)` + +```ts +async function createMCPClient( + name: string, + config: MCPClientConfig, +): Promise +``` + +1. Dynamic-import `@modelcontextprotocol/sdk` (peer dep — not loaded if MCP is not used) +2. Create transport: `StreamableHTTPClientTransport` for `url` config, `StdioClientTransport` for `command` config +3. Connect the client +4. Call `client.listTools()` to discover available tools +5. For each tool, create an `IOperationDefinition`: + - `name`: tool name + - `namespace`: the `name` parameter (used as grouping) + - `type`: `MUTATION` (all MCP tools are mutations) + - `inputSchema`: `FromSchema(tool.inputSchema)` (converts JSON Schema to TypeBox) + - `outputSchema`: `Type.Unknown()` (MCP doesn't provide output schemas) + - `handler`: calls `client.callTool({ name, arguments })` + - `accessControl`: `{ requiredScopes: [] }` (no auth by default) + +### `MCPClientConfig` + +```ts +interface MCPClientConfig { + command?: string + args?: string[] + env?: Record + cwd?: string + url?: string + headers?: Record +} +``` + +Either `command` (stdio transport) or `url` (HTTP transport) must be provided. + +### `MCPClientLoader` + +```ts +class MCPClientLoader { + async load(config: Record): Promise + getClient(name: string): MCPClientWrapper | undefined + getAllWrappers(): MCPClientWrapper[] + getAllOperations(): IOperationDefinition[] + async closeAll(): Promise +} +``` + +Manages multiple MCP client connections. `load()` connects to all configured servers in sequence, `getAllOperations()` collects all tool operations from all connected clients, `closeAll()` gracefully shuts down all connections. + +### Sub-Path Export + +`from_mcp` is exported via sub-path `@alkdev/operations/from-mcp` because it has a peer dependency on `@modelcontextprotocol/sdk`. Consumers that don't use MCP don't need to install it. See [ADR-003](decisions/003-peer-dep-adapters.md). + +## Scanner + +**Source**: `src/scanner.ts` +**Exports**: `scanOperations` (main barrel), `OperationManifest`, `ScannerFS` (types) + +### Purpose + +Auto-discovers operation definitions from the filesystem. Recursively scans `.ts` files, imports them, and validates that the default export satisfies `OperationDefinitionSchema`. + +### `scanOperations(dirPath, fs)` + +```ts +async function scanOperations( + dirPath: string, + fs: ScannerFS, +): Promise +``` + +1. Walk directory tree using `fs.readdir()` +2. For each `.ts` file, construct a `file://` URL and dynamic `import()` +3. If the module has a default export, validate it against `OperationDefinitionSchema` using `collectErrors` +4. Valid operations are added to the result array; invalid ones log a warning and are skipped +5. Directories are recursed + +### `ScannerFS` + +```ts +interface ScannerFS { + readdir(path: string): AsyncIterable<{ name: string; isFile: boolean; isDirectory: boolean }> + cwd(): string +} +``` + +Injectable filesystem interface. No `Deno.*` globals or Node-specific imports in the scanner source. The consumer provides the FS implementation. See [ADR-002](decisions/002-fs-injection.md). + +### Expected Module Shape + +```ts +// operations/myOperation.ts +import { Type } from "@alkdev/typebox" +import { OperationType, type IOperationDefinition } from "@alkdev/operations" + +export default { + name: "myOperation", + namespace: "myapp", + version: "1.0.0", + type: OperationType.QUERY, + description: "Does something useful", + inputSchema: Type.Object({ name: Type.String() }), + outputSchema: Type.Object({ result: Type.String() }), + accessControl: { requiredScopes: ["read"] }, + handler: async (input) => ({ result: `Hello, ${input.name}` }), +} satisfies IOperationDefinition +``` + +## Adding a New Adapter + +To add a new adapter (e.g., `from_grpc`): + +1. **Create `src/from_grpc.ts`** — implement the adapter that produces `IOperationDefinition[]` from gRPC service definitions +2. **Export from `src/index.ts`** — add named exports to the barrel +3. **If the adapter has peer dependencies**: + - Add to `peerDependencies` and `peerDependenciesMeta` in `package.json` + - Add a sub-path entry in `exports` (e.g., `"./from-grpc"`) + - Add a separate entry in `tsup.config.ts` + - See [ADR-003](decisions/003-peer-dep-adapters.md) +4. **Inject runtime dependencies** — follow the `ScannerFS` / `OpenAPIFS` pattern for any filesystem or platform-specific APIs. See [ADR-002](decisions/002-fs-injection.md) +5. **Use `FromSchema`** for any JSON Schema → TypeBox conversion needed by the adapter +6. **Write tests** — test the adapter in isolation, mock external services +7. **Update architecture docs** — add adapter section here and update the API surface table \ No newline at end of file diff --git a/docs/architecture/api-surface.md b/docs/architecture/api-surface.md new file mode 100644 index 0000000..f6dd6df --- /dev/null +++ b/docs/architecture/api-surface.md @@ -0,0 +1,314 @@ +--- +status: draft +last_updated: 2026-04-30 +--- + +# API Surface + +All public types, registry, call protocol, subscribe, env, validation, and adapters. See [call-protocol.md](call-protocol.md) for detailed call protocol semantics and [adapters.md](adapters.md) for adapter internals. + +## Core Types + +### `OperationType` + +```ts +enum OperationType { + QUERY = "query", + MUTATION = "mutation", + SUBSCRIPTION = "subscription", +} +``` + +- `QUERY` — read-only, no side effects +- `MUTATION` — write, has side effects +- `SUBSCRIPTION` — async generator, yields multiple values over time + +### `Identity` + +```ts +interface Identity { + id: string + scopes: string[] + resources?: Record +} +``` + +Caller security context. `scopes` are global permissions (AND-checked against `requiredScopes`). `resources` maps `"type:id"` to action arrays (checked against `resourceType`/`resourceAction`). Derived from keypal `ApiKeyMetadata`. + +### `AccessControl` + +```ts +type AccessControl = Static + +const AccessControlSchema = Type.Object({ + requiredScopes: Type.Array(Type.String()), + requiredScopesAny: Type.Optional(Type.Array(Type.String())), + resourceType: Type.Optional(Type.String()), + resourceAction: Type.Optional(Type.String()), + customAuth: Type.Optional(Type.String()), +}) +``` + +| Field | Semantics | +|-------|-----------| +| `requiredScopes` | AND — caller must have ALL listed scopes | +| `requiredScopesAny` | OR — caller must have at least ONE listed scope | +| `resourceType` | Resource category for resource-scoped checks | +| `resourceAction` | Required action on the resource | +| `customAuth` | Name of custom auth function (not yet enforced) | + +### `ErrorDefinition` + +```ts +type ErrorDefinition = Static + +const ErrorDefinitionSchema = Type.Object({ + code: Type.String(), + description: Type.String(), + schema: Type.Unknown(), + httpStatus: Type.Optional(Type.Number()), +}) +``` + +Declared on `IOperationDefinition.errorSchemas`. Contract between operation and callers about what errors it may produce. + +### `OperationContext` + +```ts +type OperationContext = Static & { + env?: OperationEnv + stream?: () => AsyncIterable + pubsub?: unknown +} +``` + +Passed to every handler. `env` provides namespace-keyed access to other operations (via `buildEnv`). `stream` and `pubsub` support subscription and event patterns. + +### `OperationSpec` + +```ts +interface OperationSpec { + name: string + namespace: string + version: string + type: OperationType + title?: string + description: string + tags?: string[] + inputSchema: TSchema + outputSchema: TSchema + errorSchemas?: ErrorDefinition[] + accessControl: AccessControl + _meta?: Record +} +``` + +Serializable, hashable subset of an operation definition. No handler — safe to send over the wire. + +### `IOperationDefinition` + +```ts +interface IOperationDefinition extends OperationSpec { + handler: OperationHandler | SubscriptionHandler +} +``` + +Full definition including the runtime handler. Registered with `OperationRegistry`. + +### `OperationHandler` / `SubscriptionHandler` + +```ts +type OperationHandler = ( + input: TInput, context: TContext, +) => Promise | TOutput + +type SubscriptionHandler = ( + input: TInput, context: TContext, +) => AsyncGenerator +``` + +`OperationHandler` returns a single value. `SubscriptionHandler` yields values over time. + +### `OperationEnv` + +```ts +type OperationEnv = Record Promise>> +``` + +Namespace-keyed operation map. Accessed as `env.namespace.operationName(input)`. Created by `buildEnv`. + +## Registry + +### `OperationRegistry` + +| Method | Signature | Description | +|--------|-----------|-------------| +| `register(operation)` | `(operation: IOperationDefinition) => void` | Register by `{namespace}.{name}` key. Validates schemas. | +| `registerAll(operations)` | `(operations: IOperationDefinition[]) => void` | Bulk register. | +| `get(id)` | `(id: string) => IOperationDefinition \| undefined` | Get by full id (`"namespace.name"`). | +| `getByName(namespace, name)` | `(namespace: string, name: string) => IOperationDefinition \| undefined` | Get by parts. | +| `list()` | `() => IOperationDefinition[]` | All registered operations. | +| `getSpec(id)` | `(id: string) => OperationSpec \| undefined` | Serializable spec (no handler). | +| `getAllSpecs()` | `() => OperationSpec[]` | All serializable specs. | +| `execute(operationId, input, context)` | `(id: string, input: TInput, ctx: OperationContext) => Promise` | Validate input, run handler, warn on output mismatch. Throws if not found or validation fails. | + +Registration key format: `{namespace}.{name}`. Overwrite on duplicate. + +`execute` validates input with `validateOrThrow` before calling the handler. Output validation uses `collectErrors` and logs warnings — it does not throw. + +## Call Protocol + +### `PendingRequestMap` + +See [call-protocol.md](call-protocol.md) for full semantics. + +| Method | Signature | Description | +|--------|-----------|-------------| +| `constructor(eventTarget?)` | `(eventTarget?: EventTarget)` | Creates internal pubsub, wires subscription handlers for responded/error/aborted. | +| `call(operationId, input, options?)` | `Promise` | Publish `call.requested`, return Promise that resolves on `call.responded`. | +| `respond(requestId, output)` | `void` | Publish `call.responded`. | +| `emitError(requestId, code, message, details?)` | `void` | Publish `call.error`. | +| `abort(requestId)` | `void` | Publish `call.aborted`, reject pending Promise. | +| `getPendingCount()` | `number` | Number of in-flight requests. | + +### `CallHandler` + +```ts +type CallHandler = (event: CallRequestedEvent) => Promise +``` + +Created by `buildCallHandler({ registry, eventTarget? })`. Subscribes to `call.requested`, checks access control, validates input, executes via registry. On success: no-op (handler is expected to publish `call.responded` through the PendingRequestMap). On failure: throws `CallError`. + +### `CallEventMap` + +```ts +const CallEventMap = { + "call.requested": Type.Object({ ... }), + "call.responded": Type.Object({ ... }), + "call.aborted": Type.Object({ ... }), + "call.error": Type.Object({ ... }), +} +``` + +Typed event map compatible with `@alkdev/pubsub`. See [call-protocol.md](call-protocol.md) for event shapes. + +### Event Types + +| Type | Fields | Description | +|------|--------|-------------| +| `CallRequestedEvent` | `requestId, operationId, input, parentRequestId?, deadline?, identity?` | Initiates a call | +| `CallRespondedEvent` | `requestId, output` | Successful response | +| `CallAbortedEvent` | `requestId` | Call cancelled | +| `CallErrorEvent` | `requestId, code, message, details?` | Error response | + +## Subscribe + +### `subscribe` + +```ts +function subscribe( + registry: OperationRegistry, + operationId: string, + input: unknown, + context: OperationContext, +): AsyncGenerator +``` + +Direct subscription execution. Gets the operation, casts its handler to `AsyncGenerator`, yields each value. Properly cleans up the generator on iteration stop (calls `generator.return()` in `finally`). + +This is the synchronous alternative to the call protocol's `call.requested` → `call.responded` flow for subscriptions. Use `subscribe()` for in-process subscription consumption; use `PendingRequestMap` for cross-transport subscription. + +## Env Builder + +### `buildEnv` + +```ts +function buildEnv(options: EnvOptions): OperationEnv + +interface EnvOptions { + registry: OperationRegistry + context: OperationContext + allowedNamespaces?: string[] + callMap?: PendingRequestMap +} +``` + +Creates a namespace-keyed `OperationEnv` for nested operation calls. Two modes: + +- **Direct mode**: `buildEnv({ registry, context })` — env functions call `registry.execute()` +- **Call protocol mode**: `buildEnv({ registry, context, callMap })` — env functions call `callMap.call()`, publishing `call.requested` events with `parentRequestId` for call graph tracking + +`SUBSCRIPTION` operations are filtered out — env only provides QUERY and MUTATION operations for nested calls. + +`allowedNamespaces` restricts which namespaces are available. + +## Validation + +| Export | Signature | Description | +|--------|-----------|-------------| +| `assertIsSchema(schema, context?)` | `(unknown, string?) => void` | Throws if `schema` is not a valid TypeBox schema. | +| `validateOrThrow(schema, value, context?)` | `(TSchema, unknown, string?) => void` | Throws with formatted errors if value fails schema check. | +| `collectErrors(schema, value)` | `(TSchema, unknown) => Array<{path, message}>` | Returns errors array (empty if valid). | +| `formatValueErrors(errors, indent?)` | `(Iterable<{path, message}>, string?) => string` | Human-readable error formatting. | + +## Error Model + +### `CallError` + +```ts +class CallError extends Error { + readonly code: CallErrorCode + readonly details?: unknown + constructor(code: CallErrorCode, message: string, details?: unknown) +} +``` + +### `InfrastructureErrorCode` + +```ts +enum InfrastructureErrorCode { + OPERATION_NOT_FOUND = "OPERATION_NOT_FOUND", + ACCESS_DENIED = "ACCESS_DENIED", + VALIDATION_ERROR = "VALIDATION_ERROR", + TIMEOUT = "TIMEOUT", + ABORTED = "ABORTED", + EXECUTION_ERROR = "EXECUTION_ERROR", + UNKNOWN_ERROR = "UNKNOWN_ERROR", +} +``` + +`CallErrorCode` is `InfrastructureErrorCode | string` — domain codes from `errorSchemas` are plain strings. + +### `mapError` + +```ts +function mapError(error: unknown, errorSchemas?: { code: string; schema: unknown }[]): CallError +``` + +Converts any thrown value to `CallError`. If the thrown value is already a `CallError`, returns it. If it's an `Error` and `errorSchemas` are provided, matches against declared error codes. Falls back to `EXECUTION_ERROR` for unmatched `Error` instances and `UNKNOWN_ERROR` for non-Error values. + +## Schema Conversion + +### `FromSchema` + +```ts +function FromSchema(T: T): TSchema +``` + +Converts JSON Schema to TypeBox `TSchema`. Handles: `allOf`, `anyOf`, `oneOf`, `enum`, `object` (with `required` tracking), `tuple`, `array`, `const`, `$ref`, primitives (`string`, `number`, `integer`, `boolean`, `null`). Unknown shapes fall back to `Type.Unknown()`. + +Used internally by `FromOpenAPI` to convert OpenAPI JSON Schema definitions to TypeBox. Also used by `from_mcp` to convert MCP tool `inputSchema` (which is JSON Schema). + +## Adapters + +See [adapters.md](adapters.md) for detailed adapter documentation. + +| Adapter | Import | Description | +|---------|--------|-------------| +| `FromOpenAPI` | Main barrel | OpenAPI spec → `IOperationDefinition[]` | +| `FromOpenAPIFile` | Main barrel | OpenAPI file → `IOperationDefinition[]` | +| `FromOpenAPIUrl` | Main barrel | OpenAPI URL → `IOperationDefinition[]` | +| `createMCPClient` | `from-mcp` sub-path | MCP server → `MCPClientWrapper` with tool operations | +| `closeMCPClient` | `from-mcp` sub-path | Close MCP client connection | +| `MCPClientLoader` | `from-mcp` sub-path | Manage multiple MCP servers | +| `scanOperations` | Main barrel | Filesystem auto-discovery of operation definitions | \ No newline at end of file diff --git a/docs/architecture/build-distribution.md b/docs/architecture/build-distribution.md new file mode 100644 index 0000000..844a836 --- /dev/null +++ b/docs/architecture/build-distribution.md @@ -0,0 +1,137 @@ +--- +status: draft +last_updated: 2026-04-30 +--- + +# Build & Distribution + +Dependencies, project structure, sub-path exports, peer deps, and build tooling. + +## Dependencies + +### Runtime + +| Package | Purpose | +|---------|---------| +| `@alkdev/typebox` | Schema system. `Type` for building schemas, `Value` for validation, `KindGuard` for schema assertion. | +| `@alkdev/pubsub` | Call protocol transport. `PendingRequestMap` creates an internal `PubSub` for event routing. | +| `@logtape/logtape` | Structured logging. Direct import, no wrapper. See [ADR-001](decisions/001-logger-direct-import.md). | + +### Peer (Optional) + +| Package | Required By | Purpose | +|---------|-------------|---------| +| `@modelcontextprotocol/sdk` | `from_mcp` sub-path | MCP client transport (stdio, HTTP). Dynamic import — only loaded when `createMCPClient` is called. | + +### Dev + +| Package | Purpose | +|---------|---------| +| `tsup` | Build tool. Dual ESM + CJS with declarations. | +| `typescript` | Type checking (`tsc --noEmit` for lint). | +| `vitest` | Test runner. | +| `@vitest/coverage-v8` | V8 coverage provider. | +| `@modelcontextprotocol/sdk` | Dev dep for MCP tests. Also listed as optional peer. | +| `@types/node` | Node.js type definitions. | + +## Project Structure + +``` +@alkdev/operations/ + src/ + index.ts # Barrel: re-exports all public API + types.ts # Core types: IOperationDefinition, OperationSpec, OperationType, etc. + registry.ts # OperationRegistry: register, execute, get, list + validation.ts # assertIsSchema, validateOrThrow, collectErrors, formatValueErrors + call.ts # PendingRequestMap, buildCallHandler, CallEventMap, event types + subscribe.ts # subscribe(): direct AsyncGenerator execution + env.ts # buildEnv(): namespace-keyed env with direct/call-protocol modes + error.ts # CallError, InfrastructureErrorCode, mapError + from_schema.ts # FromSchema: JSON Schema → TypeBox conversion + from_openapi.ts # FromOpenAPI, FromOpenAPIFile, FromOpenAPIUrl + from_mcp.ts # createMCPClient, closeMCPClient, MCPClientLoader + scanner.ts # scanOperations: filesystem auto-discovery + test/ + # Unit tests per module + docs/ + architecture.md + architecture/ + README.md + api-surface.md + call-protocol.md + adapters.md + build-distribution.md + decisions/ + package.json + tsconfig.json + tsup.config.ts + vitest.config.ts +``` + +## Sub-Path Exports + +```json +{ + "exports": { + ".": { + "import": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" } + }, + "./from-mcp": { + "import": { "types": "./dist/from-mcp.d.ts", "default": "./dist/from-mcp.js" }, + "require": { "types": "./dist/from-mcp.d.cts", "default": "./dist/from-mcp.cjs" } + } + } +} +``` + +The `./from-mcp` sub-path isolates the MCP SDK peer dependency. Consumers that don't use MCP don't need to install `@modelcontextprotocol/sdk`. See [ADR-003](decisions/003-peer-dep-adapters.md). + +The main barrel (`src/index.ts`) re-exports everything including `createMCPClient` and `MCPClientLoader` for convenience. The sub-path exists for explicit dependency isolation, not for excluding from the barrel. + +## Build + +- **Tool**: `tsup` — produces dual ESM + CJS with declarations +- **Entry points**: `src/index.ts`, `src/from_mcp.ts` +- **Format**: ESM + CJS +- **Target**: `es2022` +- **Splitting**: enabled + +```ts +// tsup.config.ts +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts', 'src/from_mcp.ts'], + format: ['esm', 'cjs'], + dts: true, + sourcemap: true, + clean: true, + splitting: true, + target: 'es2022', +}) +``` + +## Scripts + +| Script | Command | Purpose | +|--------|---------|---------| +| `build` | `tsup` | Build ESM + CJS + declarations | +| `lint` | `tsc --noEmit` | Type-check only (no emit) | +| `test` | `vitest run` | Run tests | +| `test:watch` | `vitest` | Watch mode | +| `test:coverage` | `vitest run --coverage` | Coverage report (v8) | + +## Testing + +- **Runner**: `vitest` +- **Coverage**: `@vitest/coverage-v8` +- **Config**: `vitest.config.ts` + +Tests should mock external services (MCP servers, HTTP endpoints) and use injectable FS interfaces (`ScannerFS`, `OpenAPIFS`) rather than real filesystem access. + +## Targets + +- **Publish**: npm (`@alkdev/operations`) +- **Runtime**: Node 18+, Deno, Bun — pure JS except `from_mcp` which requires `@modelcontextprotocol/sdk` +- **Deno compatibility**: Source is standard TypeScript with no Deno-specific APIs. Runtime-agnostic FS injection means Deno can provide its own `ScannerFS` and `OpenAPIFS` implementations \ No newline at end of file diff --git a/docs/architecture/call-protocol.md b/docs/architecture/call-protocol.md new file mode 100644 index 0000000..a6aa5ab --- /dev/null +++ b/docs/architecture/call-protocol.md @@ -0,0 +1,283 @@ +--- +status: draft +last_updated: 2026-04-30 +--- + +# Call Protocol + +PendingRequestMap, CallHandler, call≡subscribe semantics, event types, error model, and access control. + +## Overview + +The call protocol is the unified transport layer for all operation invocations. It provides a single event-based mechanism that works the same whether the call is local (in-process), remote (hub↔spoke over websocket), or streamed (subscription). It is built on `@alkdev/pubsub`. + +At the protocol level, `call` and `subscribe` are the same thing with different consumption patterns: + +- **`call`**: Publish `call.requested`, subscribe to `call.responded:{requestId}`, resolve on first response → `Promise` +- **`subscribe`**: Publish `call.requested`, subscribe to `call.responded:{requestId}`, yield each response → `AsyncIterable` + +Both use the same event types, the same `requestId` correlation, and the same `PendingRequestMap`. `call` is semantically `subscribe().next()`. + +## Event Types + +All communication flows through typed events. The event map is defined as `CallEventMap` using TypeBox schemas, compatible with `@alkdev/pubsub`'s `PubSubPublishArgsByKey`. + +### `CallEventMap` + +```ts +const CallEventMap = { + "call.requested": Type.Object({ + requestId: Type.String(), + operationId: Type.String(), + input: Type.Unknown(), + parentRequestId: Type.Optional(Type.String()), + deadline: Type.Optional(Type.Number()), + identity: Type.Optional(Type.Object({ + id: Type.String(), + scopes: Type.Array(Type.String()), + resources: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String()))), + })), + }), + "call.responded": Type.Object({ + requestId: Type.String(), + output: Type.Unknown(), + }), + "call.aborted": Type.Object({ + requestId: Type.String(), + }), + "call.error": Type.Object({ + requestId: Type.String(), + code: Type.String(), + message: Type.String(), + details: Type.Optional(Type.Unknown()), + }), +} +``` + +### Request Correlation + +Every call has a unique `requestId` (UUID). Nested calls include `parentRequestId` to track the call chain. Responses and errors match to requests by `requestId`. + +### Event Flow + +``` +Caller Handler + │ │ + │─── call.requested ───────────────>│ + │ {requestId, operationId, │ + │ input, identity, deadline} │ + │ │ + │<── call.responded ────────────────│ + │ {requestId, output} │ +``` + +On error: + +``` + │<── call.error ────────────────────│ + │ {requestId, code, message, │ + │ details} │ +``` + +On abort (caller cancels): + +``` + │─── call.aborted ─────────────────>│ + │ {requestId} │ +``` + +### Identity + +The `identity` field in `call.requested` carries the caller's security context through the call chain. Derived from keypal's `ApiKeyMetadata` — `scopes` maps directly, `resources` uses key format `"type:id"` with scope arrays. Checked by `CallHandler` against the operation's `AccessControl`. + +## PendingRequestMap + +`PendingRequestMap` manages in-flight requests and provides the `call()` interface. It wraps `@alkdev/pubsub` internally. + +### Construction + +```ts +const callMap = new PendingRequestMap(eventTarget?) +``` + +- Creates an internal `PubSub` using `createPubSub` +- If `eventTarget` is provided, passes it to `createPubSub` for transport-level event routing (Redis, WebSocket, etc.) +- Wires subscription handlers for `call.responded`, `call.error`, and `call.aborted` to route events back to waiting callers + +### `call(operationId, input, options?)` + +```ts +async call( + operationId: string, + input: unknown, + options?: { parentRequestId?: string; deadline?: number; identity?: Identity }, +): Promise +``` + +1. Generate `requestId` via `crypto.randomUUID()` +2. Create a `PendingRequest` with `resolve`/`reject` from a new Promise +3. If `deadline` is set, start a timeout timer that rejects with `TIMEOUT` +4. Store `PendingRequest` in the internal map +5. Publish `call.requested` event with all fields +6. Return the Promise (resolves on `call.responded`, rejects on `call.error` or `call.aborted`) + +### Internal Subscription Wiring + +On construction, three async loops subscribe to pubsub topics: + +- **`call.responded`**: Look up `PendingRequest` by `requestId`, clear timer if set, resolve with `output` +- **`call.error`**: Look up `PendingRequest`, clear timer, reject with `CallError(code, message, details)` +- **`call.aborted`**: Look up `PendingRequest`, clear timer, reject with `CallError(ABORTED, ...)` + +### `respond(requestId, output)` + +Publishes `call.responded`. Used by handlers to send results back through the protocol. + +### `emitError(requestId, code, message, details?)` + +Publishes `call.error`. Used by handlers to send errors. + +### `abort(requestId)` + +Looks up the `PendingRequest`, clears its timer, publishes `call.aborted`, rejects the Promise with `CallError(ABORTED, ...)`. + +## CallHandler + +`buildCallHandler` creates a function that bridges pubsub events to `OperationRegistry.execute()`. + +```ts +function buildCallHandler(config: CallHandlerConfig): CallHandler + +interface CallHandlerConfig { + registry: OperationRegistry + eventTarget?: EventTarget +} + +type CallHandler = (event: CallRequestedEvent) => Promise +``` + +### Handler Flow + +1. Look up operation by `operationId` from the registry +2. If not found, throw `CallError(OPERATION_NOT_FOUND, ...)` +3. Check access control (see below) +4. Validate input with `validateOrThrow` +5. Execute operation handler +6. On success: the handler is expected to have published `call.responded` through whatever mechanism +7. On failure: `mapError` converts the thrown value to `CallError` + +The `CallHandler` is designed to be wired into a pubsub subscription: + +```ts +const callHandler = buildCallHandler({ registry, eventTarget }) +pubsub.subscribe("call.requested", callHandler) +``` + +## Access Control + +### Enforcement Point + +`CallHandler` enforces `AccessControl` before dispatching to `registry.execute()`. Direct `registry.execute()` calls bypass access control — this is by design for trusted internal calls. + +### Flow + +``` +call.requested event arrives with Identity + → Look up operation's AccessControl + → Check requiredScopes (caller has ALL?) + → Check requiredScopesAny (caller has ANY?) + → Check resourceType/resourceAction against identity.resources + → All pass → proceed to execute + → Any fail → throw CallError(ACCESS_DENIED, ...) +``` + +### `checkAccess` Implementation + +```ts +function checkAccess(accessControl: AccessControl, identity: Identity): boolean +``` + +1. If `requiredScopes` is non-empty, verify `identity.scopes` contains every entry (AND) +2. If `requiredScopesAny` is non-empty, verify `identity.scopes` contains at least one entry (OR) +3. If `resourceType` and `resourceAction` are set, verify `identity.resources["{resourceType}:{resourceId}"]` includes `resourceAction` +4. Return `true` if all applicable checks pass + +Note: Access control without an `identity` in the `CallRequestedEvent` is **allowed** — unauthenticated calls are permitted if the `AccessControl` check passes (e.g., operations with empty `requiredScopes`). + +## Error Model + +The call protocol uses a unified error model. Both infrastructure and domain errors flow through `CallError`. + +### `CallError` + +```ts +class CallError extends Error { + readonly code: CallErrorCode // InfrastructureErrorCode | string + readonly details?: unknown +} +``` + +### Infrastructure Error Codes + +Reserved codes produced by `CallHandler` and `PendingRequestMap`: + +| Code | When | Details | +|------|------|---------| +| `OPERATION_NOT_FOUND` | No operation matches `operationId` | `{ operationId: string }` | +| `ACCESS_DENIED` | Missing scopes | `{ requiredScopes?: string[] }` | +| `VALIDATION_ERROR` | Input fails `inputSchema` check | Wrapped from `Value.Errors` | +| `TIMEOUT` | Deadline exceeded | `{ deadline: number }` | +| `ABORTED` | Call cancelled | — | +| `EXECUTION_ERROR` | Handler threw, no `errorSchemas` match | `{ message: string }` | +| `UNKNOWN_ERROR` | Non-Error thrown | `{ raw: string }` | + +### Domain Error Propagation + +Operations declare their possible errors via `errorSchemas` on `IOperationDefinition`. When a handler throws, `mapError` matches the thrown error against declared schemas — falls back to `EXECUTION_ERROR` if no match. + +`errorSchemas` is the contract between operation and callers about what errors it might produce. No `errorSchemas` = safe default with `EXECUTION_ERROR` wrapper. + +### `mapError` Resolution + +1. If already a `CallError`, return as-is +2. If `Error` instance and `errorSchemas` provided, check if `error.message` includes any declared error code → return `CallError(code, message, error)` +3. If `Error` instance, return `CallError(EXECUTION_ERROR, error.message, error)` +4. Otherwise, return `CallError(UNKNOWN_ERROR, String(error), { raw: String(error) })` + +## Nested Call Wiring + +Routing is an env construction concern, not a separate protocol layer. `buildEnv` creates the `OperationEnv`: + +- **Direct mode**: `buildEnv({ registry, context })` — env functions call `registry.execute()` directly +- **Call protocol mode**: `buildEnv({ registry, context, callMap })` — env functions call `callMap.call()`, publishing `call.requested` events with `parentRequestId` propagation + +`parentRequestId` enables call graph reconstruction and abort cascading — every nested call includes it. + +## Transport Mapping + +The call protocol is transport-agnostic. The `PubSub` event target determines how events move: + +| Transport | Use Case | EventTarget impl | +|-----------|----------|-----------------| +| In-process | Local hub operations | Browser `EventTarget` (default) | +| Redis | Cross-process events | `RedisEventTarget` (from `@alkdev/pubsub`) | +| WebSocket | Hub ↔ spoke bidirectional | `WebSocketEventTarget` (future) | + +Same protocol, same event shapes, same `PendingRequestMap` — different `eventTarget`. + +## Subscribe (Direct) + +The `subscribe()` function provides direct in-process subscription consumption: + +```ts +async function* subscribe( + registry: OperationRegistry, + operationId: string, + input: unknown, + context: OperationContext, +): AsyncGenerator +``` + +Gets the operation from the registry, casts its handler to `AsyncGenerator`, and yields values. Properly cleans up with `generator.return()` in a `finally` block. + +Use `subscribe()` for in-process consumption. Use `PendingRequestMap.call()` for cross-transport invocation that resolves after one event. For cross-transport streaming, use `PendingRequestMap.subscribe()` to yield multiple events. \ No newline at end of file diff --git a/docs/architecture/decisions/001-logger-direct-import.md b/docs/architecture/decisions/001-logger-direct-import.md new file mode 100644 index 0000000..84f1700 --- /dev/null +++ b/docs/architecture/decisions/001-logger-direct-import.md @@ -0,0 +1,35 @@ +# ADR-001: Direct @logtape/logtape Import + +**Status**: Accepted +**Date**: 2026-04-30 + +## Context + +The operations package needs structured logging. Within alkhub, logging went through a wrapper module (`core/logger/mod.ts`) that configured logtape categories and re-exported a logger instance. Now that operations is a standalone package, we need to decide how to handle logging. + +Two approaches: + +1. **Wrapper module** — create `src/logger.ts` that configures logtape and exports configured logger instances +2. **Direct import** — import `getLogger` from `@logtape/logtape` directly in each module + +## Decision + +Import `@logtape/logtape` directly. No wrapper module. + +## Rationale + +1. **Simpler** — one less file to maintain. The logtape API (`getLogger("category")`) is already clean and doesn't need wrapping. + +2. **Category convention** — logtape uses dot-separated category strings (e.g., `"operations:registry"`, `"operations:call"`). These are self-documenting and don't need a function to build them. + +3. **Configuration is external** — logtape configuration (log levels, sinks, categories) belongs to the application, not the library. The library just emits logs; the consumer decides what to do with them. A wrapper module would imply the library owns configuration, which it shouldn't. + +4. **Consistent with logtape's design** — logtape's `getLogger()` is designed to be called directly in each module. It's not a per-invocation cost — `getLogger` returns a cached instance. + +5. **No hidden state** — a wrapper module could carry configuration state that makes the library harder to reason about in isolation. Direct import means the library is stateless with respect to logging. + +## Consequences + +- Consumers must configure `@logtape/logtape` in their application if they want to see log output from this package +- Log categories follow the `operations:{module}` convention throughout +- `@logtape/logtape` is a direct runtime dependency (not a peer dep — it's small and we control its version) \ No newline at end of file diff --git a/docs/architecture/decisions/002-fs-injection.md b/docs/architecture/decisions/002-fs-injection.md new file mode 100644 index 0000000..61e1919 --- /dev/null +++ b/docs/architecture/decisions/002-fs-injection.md @@ -0,0 +1,48 @@ +# ADR-002: Inject Filesystem Dependencies for Runtime Agnosticism + +**Status**: Accepted +**Date**: 2026-04-30 + +## Context + +The operations package must work in both Node.js and Deno. Two functions need filesystem access: + +1. `scanOperations(dirPath, fs)` — recursive directory scan for `.ts` operation files +2. `FromOpenAPIFile(path, config, fs?)` — read OpenAPI JSON spec from filesystem + +In Node.js, these use `node:fs/promises` and `node:path`. In Deno, they would use `Deno.readDir()` and `Deno.cwd()`. Direct use of Node APIs would break Deno; direct use of Deno globals would break Node. + +## Decision + +Inject filesystem dependencies through interfaces, not global imports. + +```ts +interface ScannerFS { + readdir(path: string): AsyncIterable<{ name: string; isFile: boolean; isDirectory: boolean }> + cwd(): string +} + +interface OpenAPIFS { + readFile(path: string): Promise +} +``` + +Callers provide the FS implementation. When `OpenAPIFS` is not provided, `FromOpenAPIFile` falls back to `node:fs/promises` via dynamic import. + +## Rationale + +1. **No platform globals in source** — no `Deno.*` calls anywhere in `src/`. Both Node and Deno consumers work by providing the right FS interface. + +2. **Testability** — tests provide mock FS implementations. No filesystem mocking libraries needed. + +3. **Consistent pattern** — `ScannerFS` and `OpenAPIFS` follow the same pattern: minimal interface, consumer-provided implementation, optional Node fallback. + +4. **Deno path module** — the original alkhub scanner used `@std/path` (Deno standard library path module) for `resolve()` and `extname()`. The extracted version avoids this dependency by using simple string operations (`endsWith(".ts")`, path construction with `/`). + +5. **Node fallback is dynamic** — `FromOpenAPIFile` uses `await import("node:fs/promises")` as a fallback when no `fs` is provided. This keeps the Node path out of the module graph when a custom FS is injected, and avoids top-level Node imports that would break Deno. + +## Consequences + +- Callers in Deno must provide `ScannerFS` and `OpenAPIFS` implementations using `Deno.readDir()` and `Deno.readTextFile()` +- Callers in Node can omit the `fs` parameter for `FromOpenAPIFile` (Node fallback) but must provide `ScannerFS` for `scanOperations` +- The `pathToFileURL` helper in scanner uses a simple `file://` prefix construction rather than `url.pathToFileURL()` to avoid importing Node's `url` module \ No newline at end of file diff --git a/docs/architecture/decisions/003-peer-dep-adapters.md b/docs/architecture/decisions/003-peer-dep-adapters.md new file mode 100644 index 0000000..ec85429 --- /dev/null +++ b/docs/architecture/decisions/003-peer-dep-adapters.md @@ -0,0 +1,54 @@ +# ADR-003: Peer Dependencies for Adapter Isolation + +**Status**: Accepted +**Date**: 2026-04-30 + +## Context + +The MCP adapter (`from_mcp.ts`) depends on `@modelcontextprotocol/sdk` for `Client`, `StdioClientTransport`, and `StreamableHTTPClientTransport`. This dependency is heavy (transitive deps) and only needed by consumers that connect to MCP servers. Other adapters may have similar heavy dependencies in the future (e.g., gRPC, GraphQL). + +Two approaches: + +1. **Regular dependency** — list `@modelcontextprotocol/sdk` as a direct dependency. All consumers install it. +2. **Optional peer dependency + sub-path export** — list it as an optional peer dependency, import dynamically, and expose via a separate `./from-mcp` sub-path export. + +## Decision + +Use optional peer dependency with sub-path export. + +```json +{ + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { "optional": true } + }, + "exports": { + ".": { ... }, + "./from-mcp": { ... } + } +} +``` + +## Rationale + +1. **Zero-cost for non-MCP consumers** — `npm install @alkdev/operations` does not install `@modelcontextprotocol/sdk`. Only consumers that `import { createMCPClient } from "@alkdev/operations/from-mcp"` need to install it. + +2. **Dynamic import** — `from_mcp.ts` uses `await import("@modelcontextprotocol/sdk/client/index.js")` and `await import("@modelcontextprotocol/sdk/client/stdio.js")`. The MCP SDK is loaded only when `createMCPClient` is actually called, not at module parse time. + +3. **Explicit dependency declaration** — the sub-path import makes it clear at the import site that this code needs the MCP SDK. A barrel-only import doesn't communicate this. + +4. **No bundler reliance** — sub-path exports don't depend on the consumer's bundler correctly tree-shaking. Not all consumers use bundlers (Deno, Node with `--experimental-strip-types`). + +5. **Follows established pattern** — `@alkdev/pubsub` uses the same approach for its Redis adapter (sub-path export with optional ioredis peer dep). + +6. **Incremental** — future adapters (gRPC, GraphQL) will follow the same pattern. Each adds one peer dep entry and one sub-path export. + +## Consequences + +- `package.json` has a peer dep entry for each adapter's external dependency +- Both barrel and sub-path work — barrel re-exports everything for convenience, sub-path for explicitness +- `tsup` must list each adapter as a separate entry point +- Consumer docs should recommend sub-path imports for adapter-specific code +- The `from_mcp.ts` module also imports from `from_schema.ts` and `types.ts` (core), which are bundled into the sub-path output by tsup's code splitting \ No newline at end of file diff --git a/docs/architecture/decisions/004-schema-const-naming.md b/docs/architecture/decisions/004-schema-const-naming.md new file mode 100644 index 0000000..f22470c --- /dev/null +++ b/docs/architecture/decisions/004-schema-const-naming.md @@ -0,0 +1,50 @@ +# ADR-004: Schema Const Naming Convention + +**Status**: Accepted +**Date**: 2026-04-30 + +## Context + +TypeBox schemas and their inferred types need different names but represent the same concept. For example, the `AccessControl` schema defines the runtime shape and the `AccessControl` type provides the TypeScript interface. Using the same name for both creates a naming collision. + +Two naming conventions are common in TypeBox codebases: + +1. **Same name** — `AccessControl` for both the schema constant and the inferred type (relies on TypeScript's value/type namespace separation) +2. **Different names** — `AccessControlSchema` for the schema constant, `AccessControl` for the inferred type + +## Decision + +Use the `Schema` suffix for schema constants. The inferred type uses the bare name. + +```ts +export const AccessControlSchema = Type.Object({ ... }) +export type AccessControl = Static + +export const OperationSpecSchema = Type.Object({ ... }) +export type OperationSpec = Static + +export const ErrorDefinitionSchema = Type.Object({ ... }) +export type ErrorDefinition = Static + +export const OperationDefinitionSchema = Type.Object({ ... }) +export interface IOperationDefinition<...> extends OperationSpec<...> { ... } +``` + +## Rationale + +1. **No ambiguity** — `AccessControl` always refers to the type, `AccessControlSchema` always refers to the runtime schema. No mental overhead to distinguish them. + +2. **TypeScript namespace separation is fragile** — while TypeScript technically allows the same name for a value and a type (they occupy different namespaces), this creates confusion when reading code. `const ac: AccessControl = ...` — which `AccessControl`? The schema or the type? With the `Schema` suffix, it's immediately clear. + +3. **Consistent with TypeBox conventions** — TypeBox's own `Type.*` factory functions produce schema objects. Naming the const with a `Schema` suffix aligns with the mental model that these are schema definitions, not data instances. + +4. **Export clarity** — in `index.ts`, both are exported. Having distinct names means `import { AccessControlSchema, AccessControl }` is immediately clear: one is a runtime value, one is a type. + +5. **Existing pattern** — this convention is already used in `OperationDefinitionSchema` vs `IOperationDefinition` and `OperationSpecSchema` vs `OperationSpec` in the codebase. + +## Consequences + +- All schema const names end in `Schema` +- All type names are the bare concept name (or prefixed with `I` for interfaces) +- This applies to `AccessControlSchema`/`AccessControl`, `ErrorDefinitionSchema`/`ErrorDefinition`, `OperationContextSchema`/`OperationContext`, `OperationSpecSchema`/`OperationSpec`, `OperationDefinitionSchema`/`IOperationDefinition` +- When adding new schemas, follow this convention: `FooSchema` for the const, `Foo` for the type \ No newline at end of file diff --git a/docs/research/migration.md b/docs/research/migration.md new file mode 100644 index 0000000..3b396cb --- /dev/null +++ b/docs/research/migration.md @@ -0,0 +1,135 @@ +--- +status: in-progress +last_updated: 2026-04-30 +--- + +# @alkdev/operations — Migration Plan + +Extract `packages/core/operations/` and `packages/core/mcp/` from `alkhub_ts` into a standalone `@alkdev/operations` package. Follow patterns from `@alkdev/pubsub` (tsup+vitest, peer-dep isolation, sub-path exports, architecture docs). + +## Source Inventory + +| Module | Source | Lines | Status Category | +|--------|--------|-------|-----------------| +| types.ts | `alkhub_ts/packages/core/operations/types.ts` | 212 | Copy (zero changes) | +| from_schema.ts | `alkhub_ts/packages/core/operations/from_schema.ts` | 115 | Copy (zero changes) | +| registry.ts | `alkhub_ts/packages/core/operations/registry.ts` | 82 | Adapt (logger swap) | +| validation.ts | `alkhub_ts/packages/core/operations/validation.ts` | 115 | Adapt (@std/assert swap) | +| env.ts | `alkhub_ts/packages/core/operations/env.ts` | 83 | Adapt (logger swap, PendingRequestMap interface stays) | +| scanner.ts | `alkhub_ts/packages/core/operations/scanner.ts` | 89 | Adapt (inject fs, replace Deno globals, logger) | +| from_openapi.ts | `alkhub_ts/packages/core/operations/from_openapi.ts` | 333 | Adapt (remove Deno.env.get, inject auth, replace Deno.readTextFile, fix SSE handler) | +| mcp/wrapper.ts | `alkhub_ts/packages/core/mcp/wrapper.ts` | 88 | Merge → from_mcp.ts (update imports, logger) | +| mcp/loader.ts | `alkhub_ts/packages/core/mcp/loader.ts` | 59 | Merge → from_mcp.ts (update imports, logger) | +| call.ts | New | — | Build | +| subscribe.ts | New | — | Build | +| error.ts | New | — | Build | + +## Architecture Decisions + +- **Logger**: Direct `@logtape/logtape` import, no wrapper (ADR-001) +- **FS injection**: Scanner and from_openapi accept injected fs/env deps, no `Deno.*` globals (ADR-002) +- **Peer deps**: MCP SDK is a peer dep with sub-path export `./from-mcp`. Scanner stays in main barrel since `@std/path` is lightweight (ADR-003) +- **Call protocol**: `call ≡ subscribe` — same event types, same PendingRequestMap, different consumption +- **No Effect, No Zod**: Plain async/await, TypeBox for all schemas + +## Phase 1: Project Skeleton + Direct Copies + +- [x] Create `package.json` (following pubsub pattern — tsup, vitest, dual ESM/CJS) +- [x] Create `tsconfig.json` (ES2022, nodenext, strict) +- [x] Create `tsup.config.ts` (dual format, entries: index + from-mcp) +- [x] Create `vitest.config.ts` +- [x] Create `.gitignore` +- [x] Copy `types.ts` → `src/types.ts` (renamed schema consts: AccessControlSchema, ErrorDefinitionSchema, OperationContextSchema, OperationDefinitionSchema — avoid TSchema annotation + name collision with type aliases) +- [x] Copy `from_schema.ts` → `src/from_schema.ts` (zero changes) +- [x] Adapt `registry.ts` → `src/registry.ts` (swap `../logger/mod.ts` → `@logtape/logtape`) +- [x] Adapt `validation.ts` → `src/validation.ts` (swap `@std/assert` → custom throw in assertIsSchema) +- [x] Adapt `env.ts` → `src/env.ts` (swap logger) +- [x] Create `src/index.ts` barrel + +## Phase 2: Files with Significant Changes + +- [x] Create `src/error.ts` — CallError, mapError, infrastructure error codes +- [x] Create `src/call.ts` — PendingRequestMap class, call(), respond(), emitError(), abort(), buildCallHandler() +- [x] Create `src/subscribe.ts` — AsyncIterable subscription support +- [x] Add CallEventSchema types to `src/types.ts` and `src/call.ts` +- [x] Adapt `scanner.ts` → `src/scanner.ts` — inject ScannerFS, no Deno.* globals +- [x] Adapt `from_openapi.ts` → `src/from_openapi.ts` — remove Deno.env.get(), inject OpenAPIFS, explicit auth config +- [x] Merge `wrapper.ts` + `loader.ts` → `src/from_mcp.ts` — dynamic imports, logger, MCPClientLoader + +## Phase 3: Architecture Docs + +- [x] `docs/architecture/README.md` +- [x] `docs/architecture/api-surface.md` +- [x] `docs/architecture/call-protocol.md` +- [x] `docs/architecture/adapters.md` +- [x] `docs/architecture/build-distribution.md` +- [x] `docs/architecture/decisions/001-logger-direct-import.md` +- [x] `docs/architecture/decisions/002-fs-injection.md` +- [x] `docs/architecture/decisions/003-peer-dep-adapters.md` +- [x] `docs/architecture/decisions/004-schema-const-naming.md` (added during implementation — discovered TSchema annotation issue) + +## Phase 4: Tests + Verify Build + +- [x] Tests for registry, validation, from_schema (68 tests passing) +- [x] Tests for call protocol (PendingRequestMap, call, abort, timeout) +- [x] Tests for error (CallError, mapError, infrastructure codes) +- [x] Tests for env (buildEnv direct vs call protocol mode) +- [x] Tests for from_openapi (spec parsing, operation types, auth, $ref) +- [x] `npm run build && npm run lint && npm test` all pass + +## Target Structure + +``` +@alkdev/operations/ + src/ + index.ts — Public API surface (all exports) + types.ts — IOperationDefinition, OperationType, CallEventMap, Identity, AccessControl + registry.ts — OperationRegistry (register, execute, subscribe) + validation.ts — Input/output schema validation + call.ts — PendingRequestMap, call(), subscribe(), CallHandler + subscribe.ts — AsyncIterable subscription support + error.ts — CallError, mapError, infrastructure codes + env.ts — buildEnv (direct + call protocol modes) + scanner.ts — Auto-discover operations (injected fs) + from_schema.ts — JSON Schema → TypeBox converter + from_openapi.ts — OpenAPI spec → IOperationDefinition[] + from_mcp.ts — MCP server → IOperationDefinition[] + test/ + registry.test.ts + validation.test.ts + call.test.ts + subscribe.test.ts + error.test.ts + env.test.ts + from_schema.test.ts + from_openapi.test.ts + from_mcp.test.ts + scanner.test.ts + docs/ + architecture.md + architecture/ + README.md + api-surface.md + call-protocol.md + adapters.md + build-distribution.md + decisions/ + 001-logger-direct-import.md + 002-fs-injection.md + 003-peer-dep-adapters.md + research/ + migration.md (this file) + package.json + tsconfig.json + tsup.config.ts + vitest.config.ts + .gitignore + AGENTS.md +``` + +## References + +- Source: `@alkdev/alkhub_ts/packages/core/operations/` + `packages/core/mcp/` +- Pattern reference: `@alkdev/pubsub` (package.json, tsup, vitest, peer-dep isolation, architecture docs) +- Pattern reference: `@alkdev/taskgraph_ts` (AGENTS.md format, build pipeline) +- Architecture specs: `alkhub_ts/docs/architecture/operations.md`, `call-graph.md`, `mcp-server.md` \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..13d610f --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5474 @@ +{ + "name": "@alkdev/operations", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@alkdev/operations", + "version": "0.1.0", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@alkdev/pubsub": "^0.1.0", + "@alkdev/typebox": "^0.34.49", + "@logtape/logtape": "^2.0.0" + }, + "devDependencies": { + "@modelcontextprotocol/sdk": "^1.12.1", + "@types/node": "^22.0.0", + "@vitest/coverage-v8": "^3.2.4", + "tsup": "^8.5.1", + "typescript": "^5.7.0", + "vitest": "^3.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "../pubsub": { + "version": "0.1.0", + "license": "MIT OR Apache-2.0", + "dependencies": { + "@repeaterjs/repeater": "^3.0.0" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@vitest/coverage-v8": "^3.2.4", + "ioredis": "^5.10.1", + "tsup": "^8.5.1", + "typescript": "^5.7.0", + "vitest": "^3.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "ioredis": "^5.0.0" + }, + "peerDependenciesMeta": { + "ioredis": { + "optional": true + } + } + }, + "../pubsub/node_modules/@ampproject/remapping": { + "version": "2.3.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "../pubsub/node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "../pubsub/node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "../pubsub/node_modules/@babel/parser": { + "version": "7.29.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "../pubsub/node_modules/@babel/types": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "../pubsub/node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "../pubsub/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "../pubsub/node_modules/@ioredis/commands": { + "version": "1.5.1", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "../pubsub/node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "../pubsub/node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "../pubsub/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "../pubsub/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "../pubsub/node_modules/@repeaterjs/repeater": { + "version": "3.0.6", + "license": "MIT" + }, + "../pubsub/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "../pubsub/node_modules/@types/chai": { + "version": "5.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "../pubsub/node_modules/@types/deep-eql": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/@types/node": { + "version": "22.19.17", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "../pubsub/node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "../pubsub/node_modules/@vitest/expect": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../pubsub/node_modules/@vitest/mocker": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "../pubsub/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../pubsub/node_modules/@vitest/runner": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../pubsub/node_modules/@vitest/snapshot": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../pubsub/node_modules/@vitest/spy": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../pubsub/node_modules/@vitest/utils": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../pubsub/node_modules/acorn": { + "version": "8.16.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "../pubsub/node_modules/ansi-regex": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "../pubsub/node_modules/ansi-styles": { + "version": "6.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "../pubsub/node_modules/any-promise": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "../pubsub/node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "../pubsub/node_modules/balanced-match": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "../pubsub/node_modules/brace-expansion": { + "version": "5.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "../pubsub/node_modules/bundle-require": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "../pubsub/node_modules/cac": { + "version": "6.7.14", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/chai": { + "version": "5.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "../pubsub/node_modules/check-error": { + "version": "2.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "../pubsub/node_modules/chokidar": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "../pubsub/node_modules/cluster-key-slot": { + "version": "1.1.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, + "../pubsub/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "../pubsub/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/commander": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "../pubsub/node_modules/confbox": { + "version": "0.1.8", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/consola": { + "version": "3.4.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "../pubsub/node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "../pubsub/node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "../pubsub/node_modules/deep-eql": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "../pubsub/node_modules/denque": { + "version": "2.1.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=0.10" + } + }, + "../pubsub/node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/es-module-lexer": { + "version": "1.7.0", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/esbuild": { + "version": "0.27.7", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "../pubsub/node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "../pubsub/node_modules/expect-type": { + "version": "1.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "../pubsub/node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "../pubsub/node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "../pubsub/node_modules/foreground-child": { + "version": "3.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../pubsub/node_modules/glob": { + "version": "10.5.0", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../pubsub/node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "../pubsub/node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../pubsub/node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/ioredis": { + "version": "5.10.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "../pubsub/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../pubsub/node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/istanbul-lib-report": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "../pubsub/node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "../pubsub/node_modules/istanbul-reports": { + "version": "3.2.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/jackspeak": { + "version": "3.4.3", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "../pubsub/node_modules/joycon": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "../pubsub/node_modules/js-tokens": { + "version": "10.0.0", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/lilconfig": { + "version": "3.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "../pubsub/node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/load-tsconfig": { + "version": "0.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "../pubsub/node_modules/lodash.defaults": { + "version": "4.2.0", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/lodash.isarguments": { + "version": "3.1.0", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/loupe": { + "version": "3.2.1", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "license": "ISC" + }, + "../pubsub/node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "../pubsub/node_modules/magicast": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "../pubsub/node_modules/make-dir": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../pubsub/node_modules/minimatch": { + "version": "10.2.5", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../pubsub/node_modules/minipass": { + "version": "7.1.3", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "../pubsub/node_modules/mlly": { + "version": "1.8.2", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "../pubsub/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/mz": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "../pubsub/node_modules/nanoid": { + "version": "3.3.11", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "../pubsub/node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "../pubsub/node_modules/package-json-from-dist": { + "version": "1.0.1", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "../pubsub/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/path-scurry": { + "version": "1.11.1", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../pubsub/node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/pathval": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "../pubsub/node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "../pubsub/node_modules/picomatch": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "../pubsub/node_modules/pirates": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "../pubsub/node_modules/pkg-types": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "../pubsub/node_modules/postcss": { + "version": "8.5.12", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "../pubsub/node_modules/postcss-load-config": { + "version": "6.0.1", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "../pubsub/node_modules/readdirp": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "../pubsub/node_modules/redis-errors": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "../pubsub/node_modules/redis-parser": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "../pubsub/node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/rollup": { + "version": "4.60.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "../pubsub/node_modules/semver": { + "version": "7.7.4", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "../pubsub/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "../pubsub/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "../pubsub/node_modules/source-map": { + "version": "0.7.6", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "../pubsub/node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "../pubsub/node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/standard-as-callback": { + "version": "2.1.0", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/std-env": { + "version": "3.10.0", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "../pubsub/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/strip-ansi": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "../pubsub/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/strip-literal": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "../pubsub/node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/sucrase": { + "version": "3.35.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "../pubsub/node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/test-exclude": { + "version": "7.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "../pubsub/node_modules/thenify": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "../pubsub/node_modules/thenify-all": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "../pubsub/node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/tinyexec": { + "version": "0.3.2", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/tinyglobby": { + "version": "0.2.16", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "../pubsub/node_modules/tinypool": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "../pubsub/node_modules/tinyrainbow": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "../pubsub/node_modules/tinyspy": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "../pubsub/node_modules/tree-kill": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "../pubsub/node_modules/ts-interface-checker": { + "version": "0.1.13", + "dev": true, + "license": "Apache-2.0" + }, + "../pubsub/node_modules/tsup": { + "version": "8.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "../pubsub/node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "../pubsub/node_modules/ufo": { + "version": "1.6.4", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/vite": { + "version": "7.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "../pubsub/node_modules/vite-node": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "../pubsub/node_modules/vitest": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "../pubsub/node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "../pubsub/node_modules/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "../pubsub/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "../pubsub/node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "../pubsub/node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "../pubsub/node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "../pubsub/node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@alkdev/pubsub": { + "resolved": "../pubsub", + "link": true + }, + "node_modules/@alkdev/typebox": { + "version": "0.34.49", + "license": "MIT" + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@hono/node-server": { + "version": "1.19.14", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.14.1" + }, + "peerDependencies": { + "hono": "^4" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@logtape/logtape": { + "version": "2.0.5", + "funding": [ + "https://github.com/sponsors/dahlia" + ], + "license": "MIT" + }, + "node_modules/@modelcontextprotocol/sdk": { + "version": "1.29.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@hono/node-server": "^1.19.9", + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "content-type": "^1.0.5", + "cors": "^2.8.5", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "express": "^5.2.1", + "express-rate-limit": "^8.2.1", + "hono": "^4.11.4", + "jose": "^6.1.3", + "json-schema-typed": "^8.0.2", + "pkce-challenge": "^5.0.0", + "raw-body": "^3.0.0", + "zod": "^3.25 || ^4.0", + "zod-to-json-schema": "^3.25.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@cfworker/json-schema": "^4.1.1", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@cfworker/json-schema": { + "optional": true + }, + "zod": { + "optional": false + } + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.17", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.4", + "vitest": "3.2.4" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/accepts": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/body-parser": { + "version": "2.2.2", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.3", + "http-errors": "^2.0.0", + "iconv-lite": "^0.7.0", + "on-finished": "^2.4.1", + "qs": "^6.14.1", + "raw-body": "^3.0.1", + "type-is": "^2.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/bundle-require": { + "version": "5.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "node_modules/content-disposition": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/cors": { + "version": "2.8.6", + "dev": true, + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "license": "MIT" + }, + "node_modules/ee-first": { + "version": "1.1.1", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.0.8", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/express": { + "version": "5.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.1", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "depd": "^2.0.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express-rate-limit": { + "version": "8.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "10.1.0" + }, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://github.com/sponsors/express-rate-limit" + }, + "peerDependencies": { + "express": ">= 4.11" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/finalhandler": { + "version": "2.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.5.0", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hono": { + "version": "4.12.16", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/iconv-lite": { + "version": "0.7.2", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "dev": true, + "license": "ISC" + }, + "node_modules/ip-address": { + "version": "10.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-promise": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jose": { + "version": "6.2.3", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/joycon": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "10.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-typed": { + "version": "8.0.2", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "license": "ISC" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/magicast": { + "version": "0.3.5", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "8.4.2", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkce-challenge": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.12", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-load-config": { + "version": "6.0.1", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "dev": true, + "license": "MIT", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/qs": { + "version": "6.15.1", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/router": { + "version": "2.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.1", + "mime-types": "^3.0.2", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/serve-static": { + "version": "2.2.1", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/statuses": { + "version": "2.0.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude": { + "version": "7.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tsup": { + "version": "8.5.1", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ufo": { + "version": "1.6.4", + "dev": true, + "license": "MIT" + }, + "node_modules/undici-types": { + "version": "6.21.0", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "7.3.2", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "dev": true, + "license": "ISC" + }, + "node_modules/zod": { + "version": "4.4.1", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..28482d3 --- /dev/null +++ b/package.json @@ -0,0 +1,79 @@ +{ + "name": "@alkdev/operations", + "version": "0.1.0", + "description": "Typed operations registry, call protocol, and adapters (MCP, OpenAPI)", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./from-mcp": { + "import": { + "types": "./dist/from-mcp.d.ts", + "default": "./dist/from-mcp.js" + }, + "require": { + "types": "./dist/from-mcp.d.cts", + "default": "./dist/from-mcp.cjs" + } + } + }, + "publishConfig": { + "access": "public" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsup", + "build:tsc": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "lint": "tsc --noEmit", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "operations", + "registry", + "call-protocol", + "mcp", + "openapi", + "typebox" + ], + "license": "MIT OR Apache-2.0", + "dependencies": { + "@alkdev/typebox": "^0.34.49", + "@alkdev/pubsub": "^0.1.0", + "@logtape/logtape": "^2.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.0.0" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + }, + "devDependencies": { + "@types/node": "^22.0.0", + "@modelcontextprotocol/sdk": "^1.12.1", + "@vitest/coverage-v8": "^3.2.4", + "tsup": "^8.5.1", + "typescript": "^5.7.0", + "vitest": "^3.1.0" + }, + "engines": { + "node": ">=18.0.0" + } +} \ No newline at end of file diff --git a/src/call.ts b/src/call.ts new file mode 100644 index 0000000..14fe489 --- /dev/null +++ b/src/call.ts @@ -0,0 +1,249 @@ +import { Type, type Static } from "@alkdev/typebox"; +import { createPubSub, type PubSub } from "@alkdev/pubsub"; +import { getLogger } from "@logtape/logtape"; +import { OperationRegistry } from "./registry.js"; +import { CallError, InfrastructureErrorCode, mapError } from "./error.js"; +import { validateOrThrow } from "./validation.js"; +import type { IOperationDefinition, Identity, OperationContext, AccessControl } from "./types.js"; + +const logger = getLogger("operations:call"); + +export const CallEventSchema = { + "call.requested": Type.Object({ + requestId: Type.String(), + operationId: Type.String(), + input: Type.Unknown(), + parentRequestId: Type.Optional(Type.String()), + deadline: Type.Optional(Type.Number()), + identity: Type.Optional(Type.Object({ + id: Type.String(), + scopes: Type.Array(Type.String()), + resources: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String()))), + })), + }), + "call.responded": Type.Object({ + requestId: Type.String(), + output: Type.Unknown(), + }), + "call.aborted": Type.Object({ + requestId: Type.String(), + }), + "call.error": Type.Object({ + requestId: Type.String(), + code: Type.String(), + message: Type.String(), + details: Type.Optional(Type.Unknown()), + }), +} as const; + +export type CallRequestedEvent = Static; +export type CallRespondedEvent = Static; +export type CallAbortedEvent = Static; +export type CallErrorEvent = Static; +export type CallEventMapValue = CallRequestedEvent | CallRespondedEvent | CallAbortedEvent | CallErrorEvent; + +export const CallEventMap = CallEventSchema; + +type CallPubSubMap = { + "call.requested": [CallRequestedEvent]; + "call.responded": [CallRespondedEvent]; + "call.aborted": [CallAbortedEvent]; + "call.error": [CallErrorEvent]; +}; + +interface PendingRequest { + resolve: (value: unknown) => void; + reject: (reason: unknown) => void; + deadline?: number; + timer?: ReturnType; +} + +export interface CallHandlerConfig { + registry: OperationRegistry; + eventTarget?: EventTarget; +} + +export type CallHandler = (event: CallRequestedEvent) => Promise; + +export class PendingRequestMap { + private requests = new Map(); + private pubsub: PubSub; + + constructor(eventTarget?: EventTarget) { + this.pubsub = createPubSub( + eventTarget ? { eventTarget: eventTarget as any } : undefined + ); + this.setupSubscriptions(); + } + + private setupSubscriptions(): void { + const respondedIter = this.pubsub.subscribe("call.responded"); + (async () => { + for await (const event of respondedIter) { + const responded = event as CallRespondedEvent; + const pending = this.requests.get(responded.requestId); + if (pending) { + if (pending.timer) clearTimeout(pending.timer); + this.requests.delete(responded.requestId); + pending.resolve(responded.output); + } + } + })(); + + const errorIter = this.pubsub.subscribe("call.error"); + (async () => { + for await (const event of errorIter) { + const err = event as CallErrorEvent; + const pending = this.requests.get(err.requestId); + if (pending) { + if (pending.timer) clearTimeout(pending.timer); + this.requests.delete(err.requestId); + pending.reject(new CallError(err.code, err.message, err.details)); + } + } + })(); + + const abortedIter = this.pubsub.subscribe("call.aborted"); + (async () => { + for await (const event of abortedIter) { + const aborted = event as CallAbortedEvent; + const pending = this.requests.get(aborted.requestId); + if (pending) { + if (pending.timer) clearTimeout(pending.timer); + this.requests.delete(aborted.requestId); + pending.reject(new CallError(InfrastructureErrorCode.ABORTED, `Request ${aborted.requestId} was aborted`)); + } + } + })(); + } + + async call( + operationId: string, + input: unknown, + options?: { parentRequestId?: string; deadline?: number; identity?: Identity }, + ): Promise { + const requestId = crypto.randomUUID(); + + return new Promise((resolve, reject) => { + const pending: PendingRequest = { resolve, reject }; + + if (options?.deadline) { + pending.deadline = options.deadline; + pending.timer = setTimeout(() => { + this.requests.delete(requestId); + reject(new CallError(InfrastructureErrorCode.TIMEOUT, `Request ${requestId} timed out`, { deadline: options.deadline })); + }, options.deadline - Date.now()); + } + + this.requests.set(requestId, pending); + + this.pubsub.publish("call.requested", { + requestId, + operationId, + input, + parentRequestId: options?.parentRequestId, + deadline: options?.deadline, + identity: options?.identity, + }); + }); + } + + respond(requestId: string, output: unknown): void { + this.pubsub.publish("call.responded", { + requestId, + output, + }); + } + + emitError(requestId: string, code: string, message: string, details?: unknown): void { + this.pubsub.publish("call.error", { + requestId, + code, + message, + details, + }); + } + + abort(requestId: string): void { + const pending = this.requests.get(requestId); + if (pending) { + if (pending.timer) clearTimeout(pending.timer); + this.requests.delete(requestId); + this.pubsub.publish("call.aborted", { requestId }); + pending.reject(new CallError(InfrastructureErrorCode.ABORTED, `Request ${requestId} was aborted`)); + } + } + + getPendingCount(): number { + return this.requests.size; + } +} + +export function buildCallHandler(config: CallHandlerConfig): CallHandler { + const { registry } = config; + + return async (event: CallRequestedEvent): Promise => { + const { requestId, operationId, input, identity } = event; + + try { + const operation = registry.get(operationId); + + if (!operation) { + throw new CallError( + InfrastructureErrorCode.OPERATION_NOT_FOUND, + `Operation not found: ${operationId}`, + { operationId }, + ); + } + + const accessControl: AccessControl = operation.accessControl as AccessControl; + + if (identity && !checkAccess(accessControl, identity)) { + throw new CallError( + InfrastructureErrorCode.ACCESS_DENIED, + `Access denied for operation: ${operationId}`, + { requiredScopes: accessControl.requiredScopes }, + ); + } + + const context: OperationContext = { + requestId, + parentRequestId: event.parentRequestId, + identity, + }; + + validateOrThrow(operation.inputSchema, input, `Input validation for ${operationId}`); + + await operation.handler(input, context); + + } catch (error) { + const callError = mapError(error); + throw callError; + } + }; +} + +function checkAccess(accessControl: AccessControl, identity: Identity): boolean { + const { requiredScopes, requiredScopesAny, resourceType, resourceAction } = accessControl; + + if (requiredScopes.length > 0) { + const hasAll = requiredScopes.every((scope: string) => identity.scopes.includes(scope)); + if (!hasAll) return false; + } + + if (requiredScopesAny && requiredScopesAny.length > 0) { + const hasAny = requiredScopesAny.some((scope: string) => identity.scopes.includes(scope)); + if (!hasAny) return false; + } + + if (resourceType && resourceAction && identity.resources) { + for (const [key, actions] of Object.entries(identity.resources)) { + if (key.startsWith(`${resourceType}:`) && actions.includes(resourceAction)) { + return true; + } + } + return false; + } + + return true; +} \ No newline at end of file diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 0000000..34fee37 --- /dev/null +++ b/src/env.ts @@ -0,0 +1,56 @@ +import { OperationType } from "./types.js"; +import type { OperationContext, OperationEnv } from "./types.js"; +import type { OperationRegistry } from "./registry.js"; +import { getLogger } from "@logtape/logtape"; + +const logger = getLogger("operations:env"); + +export interface PendingRequestMap { + call(operationId: string, input: unknown, options?: { parentRequestId?: string; identity?: unknown }): Promise; +} + +export interface EnvOptions { + registry: OperationRegistry; + context: OperationContext; + allowedNamespaces?: string[]; + callMap?: PendingRequestMap; +} + +export function buildEnv(options: EnvOptions): OperationEnv { + const { registry, context, allowedNamespaces, callMap } = options; + const operations = registry.list(); + + const namespaces: OperationEnv = {}; + + for (const operation of operations) { + if (allowedNamespaces && !allowedNamespaces.includes(operation.namespace)) { + continue; + } + + if (operation.type === OperationType.SUBSCRIPTION) { + continue; + } + + if (!namespaces[operation.namespace]) { + namespaces[operation.namespace] = {}; + } + + const operationId = `${operation.namespace}.${operation.name}`; + + if (callMap) { + namespaces[operation.namespace][operation.name] = async (input: unknown) => { + logger.debug(`Call protocol: ${operationId}`); + return await callMap.call(operationId, input, { + parentRequestId: context.requestId, + }); + }; + } else { + namespaces[operation.namespace][operation.name] = async (input: unknown) => { + logger.debug(`Executing: ${operationId}`); + return await registry.execute(operationId, input, context); + }; + } + } + + return namespaces; +} \ No newline at end of file diff --git a/src/error.ts b/src/error.ts new file mode 100644 index 0000000..c310951 --- /dev/null +++ b/src/error.ts @@ -0,0 +1,51 @@ +export enum InfrastructureErrorCode { + OPERATION_NOT_FOUND = "OPERATION_NOT_FOUND", + ACCESS_DENIED = "ACCESS_DENIED", + VALIDATION_ERROR = "VALIDATION_ERROR", + TIMEOUT = "TIMEOUT", + ABORTED = "ABORTED", + EXECUTION_ERROR = "EXECUTION_ERROR", + UNKNOWN_ERROR = "UNKNOWN_ERROR", +} + +export type CallErrorCode = InfrastructureErrorCode | string; + +export class CallError extends Error { + readonly code: CallErrorCode; + readonly details?: unknown; + + constructor(code: CallErrorCode, message: string, details?: unknown) { + super(message); + this.name = "CallError"; + this.code = code; + this.details = details; + } +} + +export function mapError( + error: unknown, + errorSchemas?: { code: string; schema: unknown }[], +): CallError { + if (error instanceof CallError) { + return error; + } + + if (error instanceof Error) { + if (errorSchemas) { + const message = error.message; + for (const schema of errorSchemas) { + if (message.includes(schema.code)) { + return new CallError(schema.code, message, error); + } + } + } + + return new CallError(InfrastructureErrorCode.EXECUTION_ERROR, error.message, error); + } + + return new CallError( + InfrastructureErrorCode.UNKNOWN_ERROR, + String(error), + { raw: String(error) }, + ); +} \ No newline at end of file diff --git a/src/from_mcp.ts b/src/from_mcp.ts new file mode 100644 index 0000000..daedfe0 --- /dev/null +++ b/src/from_mcp.ts @@ -0,0 +1,151 @@ +import type { IOperationDefinition } from "./types.js"; +import { OperationType } from "./types.js"; +import { Type, type TSchema } from "@alkdev/typebox"; +import { FromSchema } from "./from_schema.js"; +import { getLogger } from "@logtape/logtape"; + +const logger = getLogger("operations:mcp"); + +export interface MCPClientConfig { + command?: string; + args?: string[]; + env?: Record; + cwd?: string; + url?: string; + headers?: Record; +} + +export interface MCPClientWrapper { + name: string; + client: unknown; + tools: IOperationDefinition[]; +} + +export async function createMCPClient( + name: string, + config: MCPClientConfig, +): Promise { + logger.info(`Creating MCP client for: ${name}`); + + const { Client } = await import("@modelcontextprotocol/sdk/client/index.js"); + const client = new Client({ name: `alkdev-${name}`, version: "1.0.0" }); + + let transport: any; + + if (config.url) { + const { StreamableHTTPClientTransport } = await import("@modelcontextprotocol/sdk/client/streamableHttp.js"); + const url = new URL(config.url); + transport = new StreamableHTTPClientTransport(url, { + requestInit: config.headers ? { headers: config.headers } : undefined, + }); + } else if (config.command) { + const { StdioClientTransport } = await import("@modelcontextprotocol/sdk/client/stdio.js"); + transport = new StdioClientTransport({ + command: config.command, + args: config.args || [], + env: config.env as Record | undefined, + cwd: config.cwd, + }); + } else { + throw new Error(`Invalid MCP server config for ${name}: must have either 'url' or 'command'`); + } + + await client.connect(transport); + logger.info(`Connected to MCP server: ${name}`); + + const toolsResult = await client.listTools(); + const operations: IOperationDefinition[] = toolsResult.tools.map((tool: { name: string; description?: string; inputSchema: unknown }) => { + return { + name: tool.name, + namespace: name, + version: "1.0.0", + type: OperationType.MUTATION, + description: tool.description || "", + tags: [], + inputSchema: FromSchema(tool.inputSchema) as TSchema, + outputSchema: Type.Unknown(), + accessControl: { requiredScopes: [] }, + handler: async (input: unknown) => { + logger.debug(`Calling MCP tool: ${name}.${tool.name}`); + const result = await client.callTool({ + name: tool.name, + arguments: input as Record, + }); + + if (result.isError) { + throw new Error(`MCP tool error: ${JSON.stringify(result.content)}`); + } + + return result.content; + }, + } satisfies IOperationDefinition; + }); + + return { + name, + client, + tools: operations, + }; +} + +export async function closeMCPClient(wrapper: MCPClientWrapper): Promise { + logger.info(`Closing MCP client: ${wrapper.name}`); + const client = wrapper.client as any; + if (client && typeof client.close === "function") { + await client.close(); + } +} + +export class MCPClientLoader { + private clients: Map = new Map(); + + async load(config: Record): Promise { + logger.info(`Loading ${Object.keys(config).length} MCP servers`); + + const wrappers: MCPClientWrapper[] = []; + + for (const [name, serverConfig] of Object.entries(config)) { + try { + const wrapper = await createMCPClient(name, serverConfig); + this.clients.set(name, wrapper); + wrappers.push(wrapper); + } catch (error) { + logger.error(`Failed to load MCP server ${name}: ${error}`); + throw error; + } + } + + return wrappers; + } + + getClient(name: string): MCPClientWrapper | undefined { + return this.clients.get(name); + } + + getAllWrappers(): MCPClientWrapper[] { + return Array.from(this.clients.values()); + } + + getAllOperations(): IOperationDefinition[] { + const allOps: IOperationDefinition[] = []; + for (const wrapper of this.clients.values()) { + for (const op of wrapper.tools) { + allOps.push(op); + } + } + return allOps; + } + + async closeAll(): Promise { + logger.info(`Closing ${this.clients.size} MCP clients`); + + const closePromises = Array.from(this.clients.values()).map((wrapper) => + closeMCPClient(wrapper).catch((error) => { + logger.error(`Error closing MCP client ${wrapper.name}: ${error}`); + }) + ); + + await Promise.all(closePromises); + this.clients.clear(); + } +} \ No newline at end of file diff --git a/src/from_openapi.ts b/src/from_openapi.ts new file mode 100644 index 0000000..6733474 --- /dev/null +++ b/src/from_openapi.ts @@ -0,0 +1,339 @@ +import * as Type from "@alkdev/typebox"; +import { FromSchema } from "./from_schema.js"; +import { OperationType, type IOperationDefinition, type OperationHandler, type OperationContext } from "./types.js"; + +export interface OpenAPIFS { + readFile(path: string): Promise; +} + +export interface OpenAPISpec { + openapi?: string; + swagger?: string; + info: { title: string; version: string; description?: string }; + paths: Record>; + components?: { schemas?: Record }; + definitions?: Record; + basePath?: string; +} + +export interface OpenAPIOperation { + operationId?: string; + summary?: string; + description?: string; + tags?: string[]; + parameters?: OpenAPIParameter[]; + requestBody?: { + content?: Record; + }; + responses?: Record; description?: string }>; +} + +export interface OpenAPIParameter { + name: string; + in: "path" | "query" | "header" | "cookie"; + required?: boolean; + schema?: unknown; + description?: string; +} + +export interface HTTPServiceConfig { + namespace: string; + baseUrl: string; + headers?: Record; + auth?: { + type: "bearer" | "apiKey" | "basic"; + token?: string; + headerName?: string; + prefix?: string; + }; + timeout?: number; +} + +function resolveRef(spec: OpenAPISpec, ref: string): unknown { + if (!ref.startsWith("#/")) { + throw new Error(`External refs not supported: ${ref}`); + } + + const parts = ref.slice(2).split("/"); + let current: unknown = spec; + + for (const part of parts) { + if (typeof current !== "object" || current === null) { + throw new Error(`Cannot resolve ref: ${ref}`); + } + current = (current as Record)[part]; + } + + return current; +} + +function resolveRefsRecursive( + spec: OpenAPISpec, + schema: unknown, + visited: Set = new Set(), +): unknown { + if (typeof schema !== "object" || schema === null) { + return schema; + } + + if (visited.has(schema)) { + return { type: "object", description: "[circular reference]" }; + } + + visited.add(schema); + + if (Array.isArray(schema)) { + return schema.map((item) => resolveRefsRecursive(spec, item, visited)); + } + + const obj = schema as Record; + + if (obj.$ref && typeof obj.$ref === "string") { + const resolved = resolveRef(spec, obj.$ref); + return resolveRefsRecursive(spec, resolved, visited); + } + + const result: Record = {}; + for (const [key, value] of Object.entries(obj)) { + result[key] = resolveRefsRecursive(spec, value, visited); + } + + return result; +} + +function buildInputSchema( + spec: OpenAPISpec, + operation: OpenAPIOperation, +): Type.TSchema { + const properties: Record = {}; + const required: string[] = []; + + if (operation.parameters) { + for (const param of operation.parameters) { + const paramSchema = param.schema + ? FromSchema(resolveRefsRecursive(spec, param.schema) as Record) + : Type.String(); + + properties[param.name] = paramSchema; + + if (param.required) { + required.push(param.name); + } + } + } + + if (operation.requestBody?.content?.["application/json"]?.schema) { + const bodySchema = resolveRefsRecursive( + spec, + operation.requestBody.content["application/json"].schema, + ) as Record; + properties.body = FromSchema(bodySchema); + required.push("body"); + } + + if (Object.keys(properties).length === 0) { + return Type.Object({}); + } + + const propsWithOptional: Record = {}; + for (const [key, schema] of Object.entries(properties)) { + if (required.includes(key)) { + propsWithOptional[key] = schema; + } else { + propsWithOptional[key] = Type.Optional(schema); + } + } + + return Type.Object(propsWithOptional); +} + +function buildOutputSchema( + spec: OpenAPISpec, + operation: OpenAPIOperation, +): Type.TSchema { + const successResponse = operation.responses?.["200"] || operation.responses?.["201"]; + + if (!successResponse?.content) { + return Type.Unknown(); + } + + const jsonSchema = successResponse.content["application/json"]?.schema; + if (!jsonSchema) { + const eventStreamSchema = successResponse.content["text/event-stream"]?.schema; + if (eventStreamSchema) { + return FromSchema(resolveRefsRecursive(spec, eventStreamSchema) as Record); + } + return Type.Unknown(); + } + + return FromSchema(resolveRefsRecursive(spec, jsonSchema) as Record); +} + +function detectOperationType(method: string, operation: OpenAPIOperation): OperationType { + const successResponse = operation.responses?.["200"] || operation.responses?.["201"]; + + if (successResponse?.content && "text/event-stream" in successResponse.content) { + return OperationType.SUBSCRIPTION; + } + + if (method.toLowerCase() === "get") { + return OperationType.QUERY; + } + + return OperationType.MUTATION; +} + +function normalizeOperationId(op: OpenAPIOperation, method: string, path: string): string { + if (op.operationId) { + return op.operationId; + } + + const pathParts = path.split("/").filter((p) => p && !p.startsWith("{")); + const baseName = pathParts.join("_") || "root"; + return `${method}_${baseName}`; +} + +function getAuthHeaders(config: HTTPServiceConfig): Record { + const headers: Record = { ...config.headers }; + + if (config.auth) { + const token = config.auth.token; + + if (token) { + switch (config.auth.type) { + case "bearer": + headers["Authorization"] = `Bearer ${token}`; + break; + case "apiKey": + const headerName = config.auth.headerName || "X-API-Key"; + const prefix = config.auth.prefix || ""; + headers[headerName] = prefix + token; + break; + case "basic": + headers["Authorization"] = `Basic ${token}`; + break; + } + } + } + + return headers; +} + +function createHTTPOperation( + spec: OpenAPISpec, + operation: OpenAPIOperation, + method: string, + path: string, + config: HTTPServiceConfig, +): IOperationDefinition { + const operationId = normalizeOperationId(operation, method, path); + const opType = detectOperationType(method, operation); + const authHeaders = getAuthHeaders(config); + + const handler: OperationHandler = async (input: unknown, context: OperationContext) => { + const inputObj = (input as Record) || {}; + + let urlPath = path; + const queryParams: Record = {}; + let body: unknown = undefined; + + for (const [key, value] of Object.entries(inputObj)) { + if (path.includes(`{${key}}`)) { + urlPath = urlPath.replace(`{${key}}`, encodeURIComponent(String(value))); + } else if (key === "body") { + body = value; + } else { + queryParams[key] = String(value); + } + } + + const url = new URL(config.baseUrl + urlPath); + for (const [key, value] of Object.entries(queryParams)) { + url.searchParams.set(key, value); + } + + const headers: Record = { + ...authHeaders, + "Content-Type": "application/json", + }; + + const response = await fetch(url.toString(), { + method: method.toUpperCase(), + headers, + body: body ? JSON.stringify(body) : undefined, + signal: config.timeout ? AbortSignal.timeout(config.timeout) : undefined, + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const contentType = response.headers.get("Content-Type") || ""; + + if (contentType.includes("application/json")) { + return response.json(); + } else if (contentType.includes("text/")) { + return response.text(); + } else { + return response.arrayBuffer(); + } + }; + + return { + name: operationId, + namespace: config.namespace, + version: "1.0.0", + type: opType, + description: operation.description || operation.summary || `${method.toUpperCase()} ${path}`, + tags: operation.tags, + inputSchema: buildInputSchema(spec, operation), + outputSchema: buildOutputSchema(spec, operation), + accessControl: { requiredScopes: [] }, + handler, + _meta: { + method: method.toUpperCase(), + path, + summary: operation.summary, + }, + }; +} + +export function FromOpenAPI(spec: OpenAPISpec, config: HTTPServiceConfig): IOperationDefinition[] { + const operations: IOperationDefinition[] = []; + const basePath = spec.basePath || ""; + + for (const [path, methods] of Object.entries(spec.paths)) { + for (const [method, operation] of Object.entries(methods)) { + if (!["get", "post", "put", "patch", "delete"].includes(method)) { + continue; + } + + if (!operation || typeof operation !== "object") { + continue; + } + + const op = operation as OpenAPIOperation; + operations.push(createHTTPOperation(spec, op, method, basePath + path, config)); + } + } + + return operations; +} + +export async function FromOpenAPIFile(path: string, config: HTTPServiceConfig, fs?: OpenAPIFS): Promise { + let content: string; + if (fs) { + content = await fs.readFile(path); + } else { + const { readFile } = await import("node:fs/promises"); + content = await readFile(path, "utf-8"); + } + const spec = JSON.parse(content) as OpenAPISpec; + return FromOpenAPI(spec, config); +} + +export async function FromOpenAPIUrl(url: string, config: HTTPServiceConfig): Promise { + const response = await fetch(url); + const spec = await response.json() as OpenAPISpec; + return FromOpenAPI(spec, config); +} \ No newline at end of file diff --git a/src/from_schema.ts b/src/from_schema.ts new file mode 100644 index 0000000..e758336 --- /dev/null +++ b/src/from_schema.ts @@ -0,0 +1,115 @@ +import * as Type from "@alkdev/typebox"; + +const IsExact = (value: unknown, expect: unknown) => value === expect; +const IsSValue = (value: unknown): value is SValue => + Type.ValueGuard.IsString(value) || Type.ValueGuard.IsNumber(value) || Type.ValueGuard.IsBoolean(value); +const IsSEnum = (value: unknown): value is SEnum => + Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.enum) && value.enum.every((v) => IsSValue(v)); +const IsSAllOf = (value: unknown): value is SAllOf => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.allOf); +const IsSAnyOf = (value: unknown): value is SAnyOf => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.anyOf); +const IsSOneOf = (value: unknown): value is SOneOf => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsArray(value.oneOf); +const IsSTuple = (value: unknown): value is STuple => + Type.ValueGuard.IsObject(value) && IsExact(value.type, "array") && Type.ValueGuard.IsArray(value.items); +const IsSArray = (value: unknown): value is SArray => + Type.ValueGuard.IsObject(value) && IsExact(value.type, "array") && !Type.ValueGuard.IsArray(value.items) && Type.ValueGuard.IsObject(value.items); +const IsSConst = (value: unknown): value is SConst => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsObject(value["const"]); +const IsSString = (value: unknown): value is SString => Type.ValueGuard.IsObject(value) && IsExact(value.type, "string"); +const IsSRef = (value: unknown): value is SRef => Type.ValueGuard.IsObject(value) && Type.ValueGuard.IsString(value.$ref); +const IsSNumber = (value: unknown): value is SNumber => Type.ValueGuard.IsObject(value) && IsExact(value.type, "number"); +const IsSInteger = (value: unknown): value is SInteger => Type.ValueGuard.IsObject(value) && IsExact(value.type, "integer"); +const IsSBoolean = (value: unknown): value is SBoolean => Type.ValueGuard.IsObject(value) && IsExact(value.type, "boolean"); +const IsSNull = (value: unknown): value is SNull => Type.ValueGuard.IsObject(value) && IsExact(value.type, "null"); +const IsSProperties = (value: unknown): value is SProperties => Type.ValueGuard.IsObject(value); +const IsSObject = (value: unknown): value is SObject => + Type.ValueGuard.IsObject(value) && + IsExact(value.type, "object") && + IsSProperties(value.properties) && + (value.required === undefined || (Type.ValueGuard.IsArray(value.required) && value.required.every((v: unknown) => Type.ValueGuard.IsString(v)))); + +type SValue = string | number | boolean; +type SEnum = Readonly<{ enum: readonly SValue[] }>; +type SAllOf = Readonly<{ allOf: readonly unknown[] }>; +type SAnyOf = Readonly<{ anyOf: readonly unknown[] }>; +type SOneOf = Readonly<{ oneOf: readonly unknown[] }>; +type SProperties = Record; +type SObject = Readonly<{ type: "object"; properties: SProperties; required?: readonly string[] }>; +type STuple = Readonly<{ type: "array"; items: readonly unknown[] }>; +type SArray = Readonly<{ type: "array"; items: unknown }>; +type SConst = Readonly<{ const: SValue }>; +type SRef = Readonly<{ $ref: string }>; +type SString = Readonly<{ type: "string" }>; +type SNumber = Readonly<{ type: "number" }>; +type SInteger = Readonly<{ type: "integer" }>; +type SBoolean = Readonly<{ type: "boolean" }>; +type SNull = Readonly<{ type: "null" }>; + +function FromRest(T: T): Type.TSchema[] { + return T.map((L) => FromSchema(L)) as never; +} + +function FromEnumRest(T: T): Type.TSchema[] { + return T.map((L) => Type.Literal(L)) as never; +} + +function FromAllOf(T: T): Type.TSchema { + return Type.IntersectEvaluated(FromRest(T.allOf), T); +} + +function FromAnyOf(T: T): Type.TSchema { + return Type.UnionEvaluated(FromRest(T.anyOf), T); +} + +function FromOneOf(T: T): Type.TSchema { + return Type.UnionEvaluated(FromRest(T.oneOf), T); +} + +function FromEnum(T: T): Type.TSchema { + return Type.UnionEvaluated(FromEnumRest(T.enum)); +} + +function FromTuple(T: T): Type.TSchema { + return Type.Tuple(FromRest(T.items), T) as never; +} + +function FromArray(T: T): Type.TSchema { + return Type.Array(FromSchema(T.items), T) as never; +} + +function FromConst(T: T): Type.TSchema { + return Type.Literal(T.const, T); +} + +function FromRef(T: T): Type.TSchema { + return Type.Ref(T.$ref); +} + +function FromObject(T: T): Type.TSchema { + const properties = globalThis.Object.getOwnPropertyNames(T.properties).reduce( + (Acc, K) => { + return { + ...Acc, + [K]: T.required && T.required.includes(K) ? FromSchema(T.properties[K]) : Type.Optional(FromSchema(T.properties[K])), + }; + }, + {} as Type.TProperties, + ); + return Type.Object(properties, T) as never; +} + +export function FromSchema(T: T): Type.TSchema { + if (IsSAllOf(T)) return FromAllOf(T); + if (IsSAnyOf(T)) return FromAnyOf(T); + if (IsSOneOf(T)) return FromOneOf(T); + if (IsSEnum(T)) return FromEnum(T); + if (IsSObject(T)) return FromObject(T); + if (IsSTuple(T)) return FromTuple(T); + if (IsSArray(T)) return FromArray(T); + if (IsSConst(T)) return FromConst(T); + if (IsSRef(T)) return FromRef(T); + if (IsSString(T)) return Type.String(T); + if (IsSNumber(T)) return Type.Number(T); + if (IsSInteger(T)) return Type.Integer(T); + if (IsSBoolean(T)) return Type.Boolean(T); + if (IsSNull(T)) return Type.Null(T); + return Type.Unknown(T || {}); +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..6fbe732 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,18 @@ +export { OperationType, OperationContextSchema, OperationDefinitionSchema, OperationSpecSchema, AccessControlSchema, ErrorDefinitionSchema } from "./types.js"; +export type { IOperationDefinition, OperationHandler, SubscriptionHandler, Identity, OperationEnv, OperationContext, OperationSpec, AccessControl, ErrorDefinition } from "./types.js"; +export { OperationRegistry } from "./registry.js"; +export { formatValueErrors, assertIsSchema, validateOrThrow, collectErrors } from "./validation.js"; +export { buildEnv } from "./env.js"; +export type { PendingRequestMap, EnvOptions } from "./env.js"; +export { FromSchema } from "./from_schema.js"; +export { FromOpenAPI, FromOpenAPIFile, FromOpenAPIUrl } from "./from_openapi.js"; +export type { OpenAPISpec, OpenAPIOperation, OpenAPIParameter, HTTPServiceConfig, OpenAPIFS } from "./from_openapi.js"; +export { scanOperations } from "./scanner.js"; +export type { OperationManifest, ScannerFS } from "./scanner.js"; +export { CallError, InfrastructureErrorCode, mapError } from "./error.js"; +export type { CallErrorCode } from "./error.js"; +export { PendingRequestMap as PendingRequestMapClass, buildCallHandler } from "./call.js"; +export type { CallEventMap, CallEventMapValue, CallRequestedEvent, CallRespondedEvent, CallAbortedEvent, CallErrorEvent, CallHandler, CallHandlerConfig } from "./call.js"; +export { subscribe } from "./subscribe.js"; +export { createMCPClient, closeMCPClient, MCPClientLoader } from "./from_mcp.js"; +export type { MCPClientConfig, MCPClientWrapper } from "./from_mcp.js"; \ No newline at end of file diff --git a/src/registry.ts b/src/registry.ts new file mode 100644 index 0000000..7f3ac15 --- /dev/null +++ b/src/registry.ts @@ -0,0 +1,78 @@ +import type { IOperationDefinition, OperationContext, OperationSpec } from "./types.js"; +import { getLogger } from "@logtape/logtape"; +import { Value } from "@alkdev/typebox/value"; +import { assertIsSchema, validateOrThrow, collectErrors, formatValueErrors } from "./validation.js"; + +const logger = getLogger("operations:registry"); + +export class OperationRegistry { + private operations = new Map(); + + private getOperationId(operation: IOperationDefinition): string { + return `${operation.namespace}.${operation.name}`; + } + + register(operation: IOperationDefinition): void { + const opId = `${operation.namespace}.${operation.name}`; + assertIsSchema(operation.inputSchema, `${opId} inputSchema`); + assertIsSchema(operation.outputSchema, `${opId} outputSchema`); + const id = this.getOperationId(operation); + this.operations.set(id, operation); + logger.info(`Registered operation: ${id}`); + } + + registerAll(operations: IOperationDefinition[]): void { + for (const op of operations) { + this.register(op); + } + } + + get(id: string): IOperationDefinition | undefined { + return this.operations.get(id); + } + + getByName(namespace: string, name: string): IOperationDefinition | undefined { + return this.operations.get(`${namespace}.${name}`); + } + + list(): IOperationDefinition[] { + return Array.from(this.operations.values()); + } + + private extractSpec(operation: IOperationDefinition): OperationSpec { + const { handler: _handler, ...spec } = operation; + return spec; + } + + getSpec(id: string): OperationSpec | undefined { + const operation = this.operations.get(id); + return operation ? this.extractSpec(operation) : undefined; + } + + getAllSpecs(): OperationSpec[] { + return this.list().map(op => this.extractSpec(op)); + } + + async execute( + operationId: string, + input: TInput, + context: OperationContext, + ): Promise { + const operation = this.operations.get(operationId); + + if (!operation) { + throw new Error(`Operation not found: ${operationId}`); + } + + validateOrThrow(operation.inputSchema, input, `Input validation failed for ${operationId}`); + + const result = await operation.handler(input, context) as TOutput; + + const errors = collectErrors(operation.outputSchema, result); + if (errors.length > 0) { + logger.warn(`Output validation failed for ${operationId}:\n${formatValueErrors(errors)}`); + } + + return result; + } +} \ No newline at end of file diff --git a/src/scanner.ts b/src/scanner.ts new file mode 100644 index 0000000..4fcc313 --- /dev/null +++ b/src/scanner.ts @@ -0,0 +1,97 @@ +import type { IOperationDefinition } from "./types.js"; +import { OperationDefinitionSchema } from "./types.js"; +import { collectErrors, formatValueErrors } from "./validation.js"; +import { getLogger } from "@logtape/logtape"; + +const logger = getLogger("operations:scanner"); + +export interface ScannerFS { + readdir(path: string): AsyncIterable<{ name: string; isFile: boolean; isDirectory: boolean }>; + cwd(): string; +} + +export interface OperationManifest { + operations: Record; + baseUrl?: string; +} + +export async function scanOperations( + dirPath: string, + fs: ScannerFS, +): Promise { + const operations: IOperationDefinition[] = []; + + try { + await processDirectory(dirPath, operations, fs); + } catch (error) { + logger.error( + `Error scanning directory ${dirPath}: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + throw error; + } + + return operations; +} + +async function processDirectory( + dirPath: string, + operations: IOperationDefinition[], + fs: ScannerFS, +): Promise { + try { + for await (const entry of fs.readdir(dirPath)) { + const fullPath = `${dirPath}/${entry.name}`; + + if (entry.isDirectory) { + await processDirectory(fullPath, operations, fs); + } else if (entry.isFile && entry.name.endsWith(".ts")) { + try { + const absolutePath = fullPath.startsWith("/") ? fullPath : `${fs.cwd()}/${fullPath}`; + const moduleUrl = pathToFileURL(absolutePath); + const module = await import(moduleUrl); + + if (module.default) { + const operation = module.default as IOperationDefinition; + + const errors = collectErrors(OperationDefinitionSchema, operation); + + if (errors.length > 0) { + logger.warn(`${fullPath}: Invalid operation definition - ${formatValueErrors(errors, "")}`); + continue; + } + + operations.push(operation); + logger.info( + `Loaded operation: ${operation.namespace}.${operation.name} from ${fullPath}`, + ); + } else { + logger.warn(`${fullPath} does not export a default operation`); + } + } catch (error) { + logger.error( + `Error processing ${fullPath}: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } + } + } + } catch (error) { + logger.error( + `Error reading directory ${dirPath}: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + throw new Error( + `Failed to process directory ${dirPath}: ${ + error instanceof Error ? error.message : String(error) + }`, + ); + } +} + +function pathToFileURL(absolutePath: string): string { + return `file://${absolutePath}`; +} \ No newline at end of file diff --git a/src/subscribe.ts b/src/subscribe.ts new file mode 100644 index 0000000..cc30d4e --- /dev/null +++ b/src/subscribe.ts @@ -0,0 +1,28 @@ +import type { IOperationDefinition, OperationContext } from "./types.js"; +import { OperationRegistry } from "./registry.js"; + +export async function* subscribe( + registry: OperationRegistry, + operationId: string, + input: unknown, + context: OperationContext, +): AsyncGenerator { + const operation = registry.get(operationId); + + if (!operation) { + throw new Error(`Operation not found: ${operationId}`); + } + + const handler = operation.handler; + const generator = handler(input, context) as AsyncGenerator; + + try { + for await (const value of generator) { + yield value; + } + } finally { + if (generator.return) { + await generator.return(undefined); + } + } +} \ No newline at end of file diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..becf464 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,144 @@ +import { Type, type Static, type TSchema } from "@alkdev/typebox"; + +export enum OperationType { + QUERY = "query", + MUTATION = "mutation", + SUBSCRIPTION = "subscription", +} + +export interface Identity { + id: string + scopes: string[] + resources?: Record +} + +export type OperationEnv = Record Promise>> + +export const OperationContextSchema = Type.Object({ + metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown())), + requestId: Type.Optional(Type.String()), + parentRequestId: Type.Optional(Type.String()), + identity: Type.Optional(Type.Object({ + id: Type.String(), + scopes: Type.Array(Type.String()), + resources: Type.Optional(Type.Record(Type.String(), Type.Array(Type.String()))) + })), +}, { + description: "Context provided to all operation handlers" +}); + +type OperationContextBase = Static + +export type OperationContext = OperationContextBase & { + env?: OperationEnv + stream?: () => AsyncIterable + pubsub?: unknown +} + +export const ErrorDefinitionSchema = Type.Object({ + code: Type.String({ + description: "Error Code e.g., INVALID_INPUT, NOT_FOUND, UNAUTHORIZED" + }), + description: Type.String(), + schema: Type.Unknown(), + httpStatus: Type.Optional(Type.Number()), +}); + +export type ErrorDefinition = Static; + +export const AccessControlSchema = Type.Object({ + requiredScopes: Type.Array( + Type.String(), + {description: "Required scopes (all must be present)"} + ), + requiredScopesAny: Type.Optional( + Type.Array(Type.String({description: "Required scopes (at least one must match)"}))), + resourceType: Type.Optional(Type.String({description: "Resource Type e.g., project, tool, data"})), + resourceAction: Type.Optional(Type.String({description: "Required action on the resource e.g., read, write, execute"})), + customAuth: Type.Optional(Type.String({description: "Name of custom auth function"})), +}); + +export type AccessControl = Static; + +export type OperationHandler< + TInput = unknown, + TOutput = unknown, + TContext extends OperationContext = OperationContext, +> = ( + input: TInput, + context: TContext, +) => Promise | TOutput; + +export type SubscriptionHandler< + TInput = unknown, + TOutput = unknown, + TContext extends OperationContext = OperationContext, +> = ( + input: TInput, + context: TContext, +) => AsyncGenerator; + +export const OperationDefinitionSchema = Type.Object({ + name: Type.String({ description: "Unique operation name" }), + namespace: Type.String({ + description: "Namespace for grouping (e.g., 'task', 'graph', 'user')", + }), + version: Type.String({ description: "Semantic version (e.g., '1.0.0')" }), + type: Type.Enum(OperationType, { + description: "Operation type: query, mutation, or subscription", + }), + title: Type.Optional(Type.String({ description: "Human-readable title" })), + description: Type.String({ description: "Detailed description" }), + tags: Type.Optional(Type.Array(Type.String())), + inputSchema: Type.Unknown({ description: "json schema for input" }), + outputSchema: Type.Unknown({ description: "json schema for output" }), + errorSchemas: Type.Optional(Type.Array(ErrorDefinitionSchema)), + accessControl: AccessControlSchema, + handler: Type.Unknown({ description: "Operation handler function" }), + _meta: Type.Optional(Type.Record(Type.String(), Type.Unknown())), +}); + +export interface OperationSpec< + TInput = unknown, + TOutput = unknown, +> { + name: string; + namespace: string; + version: string; + type: OperationType; + title?: string; + description: string; + tags?: string[]; + inputSchema: TSchema; + outputSchema: TSchema; + errorSchemas?: ErrorDefinition[]; + accessControl: AccessControl; + _meta?: Record; +} + +export const OperationSpecSchema = Type.Object({ + name: Type.String({ description: "Unique operation name" }), + namespace: Type.String({ + description: "Namespace for grouping (e.g., 'task', 'graph', 'user')", + }), + version: Type.String({ description: "Semantic version (e.g., '1.0.0')" }), + type: Type.Enum(OperationType, { + description: "Operation type: query, mutation, or subscription", + }), + title: Type.Optional(Type.String({ description: "Human-readable title" })), + description: Type.String({ description: "Detailed description" }), + tags: Type.Optional(Type.Array(Type.String())), + inputSchema: Type.Unknown({ description: "json schema for input" }), + outputSchema: Type.Unknown({ description: "json schema for output" }), + errorSchemas: Type.Optional(Type.Array(ErrorDefinitionSchema)), + accessControl: AccessControlSchema, + _meta: Type.Optional(Type.Record(Type.String(), Type.Unknown())), +}); + +export interface IOperationDefinition< + TInput = unknown, + TOutput = unknown, + TContext extends OperationContext = OperationContext, +> extends OperationSpec { + handler: OperationHandler | SubscriptionHandler; +} \ No newline at end of file diff --git a/src/validation.ts b/src/validation.ts new file mode 100644 index 0000000..9e19e34 --- /dev/null +++ b/src/validation.ts @@ -0,0 +1,44 @@ +import { KindGuard, type TSchema } from "@alkdev/typebox"; +import { Value } from "@alkdev/typebox/value"; + +export function formatValueErrors( + errors: Iterable<{ path: string; message: string }>, + indent: string = " - ", +): string { + return [...errors] + .map((err) => `${indent}${err.path}: ${err.message}`) + .join("\n"); +} + +export function assertIsSchema(schema: unknown, context?: string): void { + const contextMsg = context ? ` for ${context}` : ""; + if (!KindGuard.IsSchema(schema)) { + throw new Error(`Not a valid TypeBox schema${contextMsg}. Use FromSchema() to convert JSON Schema to TypeBox.`); + } +} + +export function validateOrThrow( + schema: TSchema, + value: unknown, + context?: string, +): void { + if (!Value.Check(schema, value)) { + const errors = Value.Errors(schema, value); + const formatted = formatValueErrors(errors); + const contextMsg = context ? ` for ${context}` : ""; + throw new Error(`Validation failed${contextMsg}:\n${formatted}`); + } +} + +export function collectErrors( + schema: TSchema, + value: unknown, +): Array<{ path: string; message: string }> { + if (Value.Check(schema, value)) { + return []; + } + return [...Value.Errors(schema, value)].map((err) => ({ + path: err.path, + message: err.message, + })); +} \ No newline at end of file diff --git a/test/call.test.ts b/test/call.test.ts new file mode 100644 index 0000000..71fa10a --- /dev/null +++ b/test/call.test.ts @@ -0,0 +1,87 @@ +import { describe, it, expect } from "vitest"; +import { PendingRequestMap } from "../src/call.js"; +import { CallError, InfrastructureErrorCode } from "../src/error.js"; + +describe("PendingRequestMap", () => { + it("creates instance without event target", () => { + const map = new PendingRequestMap(); + expect(map.getPendingCount()).toBe(0); + }); + + it("creates instance with event target", () => { + const target = new EventTarget(); + const map = new PendingRequestMap(target); + expect(map.getPendingCount()).toBe(0); + }); + + it("call() resolves when respond() is called", async () => { + const map = new PendingRequestMap(); + + const callPromise = map.call("test.op", { value: "hello" }); + + setTimeout(() => { + const requestId = [...map["requests"].keys()][0]; + map.respond(requestId, { result: "world" }); + }, 10); + + const result = await callPromise; + expect(result).toEqual({ result: "world" }); + }); + + it("call() rejects when emitError() is called", async () => { + const map = new PendingRequestMap(); + + const callPromise = map.call("test.op", { value: "hello" }); + + setTimeout(() => { + const requestId = [...map["requests"].keys()][0]; + map.emitError(requestId, "CUSTOM_ERROR", "Something went wrong"); + }, 10); + + await expect(callPromise).rejects.toThrow("Something went wrong"); + await expect(callPromise).rejects.toBeInstanceOf(CallError); + }); + + it("abort() rejects the pending call", async () => { + const map = new PendingRequestMap(); + + const callPromise = map.call("test.op", { value: "hello" }); + + setTimeout(() => { + const requestId = [...map["requests"].keys()][0]; + map.abort(requestId); + }, 10); + + await expect(callPromise).rejects.toThrow("was aborted"); + await expect(callPromise).rejects.toBeInstanceOf(CallError); + }); + + it("call() with deadline times out", async () => { + const map = new PendingRequestMap(); + + const deadline = Date.now() + 50; + const callPromise = map.call("test.op", { value: "hello" }, { deadline }); + + await expect(callPromise).rejects.toThrow("timed out"); + await expect(callPromise).rejects.toBeInstanceOf(CallError); + }); + + it("tracks pending requests", () => { + const map = new PendingRequestMap(); + map.call("test.op1", {}); + map.call("test.op2", {}); + expect(map.getPendingCount()).toBe(2); + }); + + it("cleans up after call resolves", async () => { + const map = new PendingRequestMap(); + const callPromise = map.call("test.op", { value: "hello" }); + expect(map.getPendingCount()).toBe(1); + + const requestId = [...map["requests"].keys()][0]; + map.respond(requestId, { result: "done" }); + + await callPromise; + expect(map.getPendingCount()).toBe(0); + }); +}); \ No newline at end of file diff --git a/test/env.test.ts b/test/env.test.ts new file mode 100644 index 0000000..2c18de1 --- /dev/null +++ b/test/env.test.ts @@ -0,0 +1,93 @@ +import { describe, it, expect } from "vitest"; +import { OperationRegistry, OperationType, buildEnv, type IOperationDefinition, type OperationContext } from "../src/index.js"; +import * as Type from "@alkdev/typebox"; +import { PendingRequestMap } from "../src/call.js"; + +function makeOperation(name: string, handler?: any): IOperationDefinition { + return { + name, + namespace: "test", + version: "1.0.0", + type: OperationType.QUERY, + description: `Test ${name}`, + inputSchema: Type.Object({ value: Type.String() }), + outputSchema: Type.Object({ result: Type.String() }), + accessControl: { requiredScopes: [] }, + handler: handler || (async (input: any) => ({ result: input.value })), + }; +} + +describe("buildEnv", () => { + it("creates namespace-keyed env in direct mode", async () => { + const registry = new OperationRegistry(); + registry.register(makeOperation("readFile")); + registry.register(makeOperation("writeFile")); + + const env = buildEnv({ + registry, + context: {} as OperationContext, + }); + + expect(env.test).toBeDefined(); + expect(typeof env.test.readFile).toBe("function"); + expect(typeof env.test.writeFile).toBe("function"); + + const result = await env.test.readFile({ value: "test" }); + expect(result).toEqual({ result: "test" }); + }); + + it("filters out SUBSCRIPTION operations", () => { + const registry = new OperationRegistry(); + registry.register(makeOperation("query")); + registry.register({ + ...makeOperation("onEvent"), + type: OperationType.SUBSCRIPTION, + }); + + const env = buildEnv({ + registry, + context: {} as OperationContext, + }); + + expect(env.test.query).toBeDefined(); + expect(env.test.onEvent).toBeUndefined(); + }); + + it("filters by allowedNamespaces", () => { + const registry = new OperationRegistry(); + registry.register(makeOperation("op1")); + registry.register({ + ...makeOperation("op2"), + namespace: "other", + }); + + const env = buildEnv({ + registry, + context: {} as OperationContext, + allowedNamespaces: ["test"], + }); + + expect(env.test).toBeDefined(); + expect(env.other).toBeUndefined(); + }); + + it("routes through callMap in call protocol mode", async () => { + const registry = new OperationRegistry(); + registry.register(makeOperation("readFile")); + + const callMap = { + call: async (opId: string, input: unknown, opts?: any) => { + return { result: `routed: ${opId}` }; + }, + }; + + const env = buildEnv({ + registry, + context: {} as OperationContext, + callMap, + }); + + const result = await env.test.readFile({ value: "test" }); + expect(result).toEqual({ result: "routed: test.readFile" }); + }); +}); \ No newline at end of file diff --git a/test/error.test.ts b/test/error.test.ts new file mode 100644 index 0000000..a5e386e --- /dev/null +++ b/test/error.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from "vitest"; +import { CallError, InfrastructureErrorCode, mapError } from "../src/error.js"; + +describe("CallError", () => { + it("stores code, message, and details", () => { + const err = new CallError("TEST_CODE", "test message", { foo: "bar" }); + expect(err.code).toBe("TEST_CODE"); + expect(err.message).toBe("test message"); + expect(err.details).toEqual({ foo: "bar" }); + expect(err.name).toBe("CallError"); + expect(err).toBeInstanceOf(Error); + expect(err).toBeInstanceOf(CallError); + }); + + it("works without details", () => { + const err = new CallError("CODE", "msg"); + expect(err.details).toBeUndefined(); + }); +}); + +describe("InfrastructureErrorCode", () => { + it("has all expected codes", () => { + expect(InfrastructureErrorCode.OPERATION_NOT_FOUND).toBe("OPERATION_NOT_FOUND"); + expect(InfrastructureErrorCode.ACCESS_DENIED).toBe("ACCESS_DENIED"); + expect(InfrastructureErrorCode.VALIDATION_ERROR).toBe("VALIDATION_ERROR"); + expect(InfrastructureErrorCode.TIMEOUT).toBe("TIMEOUT"); + expect(InfrastructureErrorCode.ABORTED).toBe("ABORTED"); + expect(InfrastructureErrorCode.EXECUTION_ERROR).toBe("EXECUTION_ERROR"); + expect(InfrastructureErrorCode.UNKNOWN_ERROR).toBe("UNKNOWN_ERROR"); + }); +}); + +describe("mapError", () => { + it("passes through existing CallError", () => { + const original = new CallError("CUSTOM", "msg"); + const result = mapError(original); + expect(result).toBe(original); + }); + + it("maps Error to EXECUTION_ERROR", () => { + const result = mapError(new Error("something broke")); + expect(result.code).toBe(InfrastructureErrorCode.EXECUTION_ERROR); + expect(result.message).toBe("something broke"); + }); + + it("maps Error with matching errorSchema code", () => { + const result = mapError(new Error("NOT_FOUND: item missing"), [ + { code: "NOT_FOUND", schema: {} }, + ]); + expect(result.code).toBe("NOT_FOUND"); + }); + + it("maps non-Error to UNKNOWN_ERROR", () => { + const result = mapError("string error"); + expect(result.code).toBe(InfrastructureErrorCode.UNKNOWN_ERROR); + expect(result.message).toBe("string error"); + expect(result.details).toEqual({ raw: "string error" }); + }); + + it("maps non-Error with details", () => { + const result = mapError(42); + expect(result.code).toBe(InfrastructureErrorCode.UNKNOWN_ERROR); + expect(result.details).toEqual({ raw: "42" }); + }); +}); \ No newline at end of file diff --git a/test/from_openapi.test.ts b/test/from_openapi.test.ts new file mode 100644 index 0000000..d24fe65 --- /dev/null +++ b/test/from_openapi.test.ts @@ -0,0 +1,194 @@ +import { describe, it, expect } from "vitest"; +import { FromOpenAPI } from "../src/from_openapi.js"; +import { OperationType } from "../src/types.js"; +import { Value } from "@alkdev/typebox/value"; + +const simpleSpec = { + openapi: "3.0.0", + info: { title: "Test API", version: "1.0.0" }, + paths: { + "/users": { + get: { + operationId: "listUsers", + description: "List all users", + responses: { + "200": { + content: { + "application/json": { + schema: { + type: "object", + properties: { + users: { type: "array", items: { type: "string" } }, + }, + }, + }, + }, + }, + }, + }, + post: { + operationId: "createUser", + description: "Create a user", + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + name: { type: "string" }, + }, + required: ["name"], + }, + }, + }, + }, + responses: { + "201": { + content: { + "application/json": { + schema: { + type: "object", + properties: { + id: { type: "string" }, + }, + }, + }, + }, + }, + }, + }, + }, + "/events": { + get: { + operationId: "streamEvents", + description: "Stream events via SSE", + responses: { + "200": { + content: { + "text/event-stream": { + schema: { + type: "object", + properties: { + event: { type: "string" }, + }, + }, + }, + }, + }, + }, + }, + }, + "/users/{id}": { + get: { + operationId: "getUser", + description: "Get user by ID", + parameters: [ + { name: "id", in: "path", required: true, schema: { type: "string" } }, + ], + responses: { + "200": { + content: { + "application/json": { + schema: { type: "object", properties: { name: { type: "string" } } }, + }, + }, + }, + }, + }, + }, + }, +}; + +describe("FromOpenAPI", () => { + const config = { + namespace: "api", + baseUrl: "https://api.example.com", + }; + + it("generates operations from OpenAPI spec", () => { + const ops = FromOpenAPI(simpleSpec as any, config); + expect(ops.length).toBeGreaterThan(0); + expect(ops.map((o) => o.name)).toContain("listUsers"); + expect(ops.map((o) => o.name)).toContain("createUser"); + expect(ops.map((o) => o.name)).toContain("getUser"); + }); + + it("sets namespace from config", () => { + const ops = FromOpenAPI(simpleSpec as any, config); + expect(ops.every((o) => o.namespace === "api")).toBe(true); + }); + + it("detects GET as QUERY type", () => { + const ops = FromOpenAPI(simpleSpec as any, config); + const listUsers = ops.find((o) => o.name === "listUsers")!; + expect(listUsers.type).toBe(OperationType.QUERY); + }); + + it("detects POST as MUTATION type", () => { + const ops = FromOpenAPI(simpleSpec as any, config); + const createUser = ops.find((o) => o.name === "createUser")!; + expect(createUser.type).toBe(OperationType.MUTATION); + }); + + it("detects text/event-stream as SUBSCRIPTION type", () => { + const ops = FromOpenAPI(simpleSpec as any, config); + const streamEvents = ops.find((o) => o.name === "streamEvents")!; + expect(streamEvents.type).toBe(OperationType.SUBSCRIPTION); + }); + + it("generates valid TypeBox input schemas", () => { + const ops = FromOpenAPI(simpleSpec as any, config); + const getUser = ops.find((o) => o.name === "getUser")!; + expect(Value.Check(getUser.inputSchema, { id: "123" })).toBe(true); + }); + + it("handles auth bearer config", () => { + const authConfig = { + namespace: "api", + baseUrl: "https://api.example.com", + auth: { type: "bearer" as const, token: "test-token" }, + }; + const ops = FromOpenAPI(simpleSpec as any, authConfig); + expect(ops.length).toBeGreaterThan(0); + }); + + it("skips non-HTTP methods", () => { + const ops = FromOpenAPI(simpleSpec as any, config); + expect(ops.every((o) => o.name)).toBeTruthy(); + }); + + it("handles $ref resolution", () => { + const specWithRef = { + openapi: "3.0.0", + info: { title: "Test", version: "1.0.0" }, + paths: { + "/items": { + get: { + operationId: "listItems", + responses: { + "200": { + content: { + "application/json": { + schema: { $ref: "#/components/schemas/ItemList" }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + ItemList: { + type: "object", + properties: { + items: { type: "array", items: { type: "string" } }, + }, + }, + }, + }, + }; + const ops = FromOpenAPI(specWithRef as any, config); + expect(ops.length).toBe(1); + }); +}); \ No newline at end of file diff --git a/test/from_schema.test.ts b/test/from_schema.test.ts new file mode 100644 index 0000000..51511ff --- /dev/null +++ b/test/from_schema.test.ts @@ -0,0 +1,110 @@ +import { describe, it, expect } from "vitest"; +import { FromSchema } from "../src/from_schema.js"; +import * as Type from "@alkdev/typebox"; +import { KindGuard } from "@alkdev/typebox"; +import { Value } from "@alkdev/typebox/value"; + +describe("FromSchema", () => { + it("converts a simple object schema", () => { + const jsonSchema = { + type: "object", + properties: { + name: { type: "string" }, + age: { type: "number" }, + }, + required: ["name"], + }; + const tbox = FromSchema(jsonSchema); + expect(KindGuard.IsSchema(tbox)).toBe(true); + expect(Value.Check(tbox, { name: "Alice" })).toBe(true); + expect(Value.Check(tbox, { name: "Alice", age: 30 })).toBe(true); + }); + + it("converts string schema", () => { + const tbox = FromSchema({ type: "string" }); + expect(KindGuard.IsSchema(tbox)).toBe(true); + expect(Value.Check(tbox, "hello")).toBe(true); + }); + + it("converts number schema", () => { + const tbox = FromSchema({ type: "number" }); + expect(Value.Check(tbox, 42)).toBe(true); + }); + + it("converts integer schema", () => { + const tbox = FromSchema({ type: "integer" }); + expect(Value.Check(tbox, 1)).toBe(true); + }); + + it("converts boolean schema", () => { + const tbox = FromSchema({ type: "boolean" }); + expect(Value.Check(tbox, true)).toBe(true); + }); + + it("converts null schema", () => { + const tbox = FromSchema({ type: "null" }); + expect(Value.Check(tbox, null)).toBe(true); + }); + + it("converts enum schema", () => { + const tbox = FromSchema({ enum: ["a", "b", "c"] }); + expect(Value.Check(tbox, "a")).toBe(true); + expect(Value.Check(tbox, "d")).toBe(false); + }); + + it("converts array schema", () => { + const tbox = FromSchema({ type: "array", items: { type: "string" } }); + expect(Value.Check(tbox, ["a", "b"])).toBe(true); + }); + + it("converts tuple schema", () => { + const tbox = FromSchema({ type: "array", items: [{ type: "string" }, { type: "number" }] }); + expect(Value.Check(tbox, ["a", 1])).toBe(true); + }); + + it("converts allOf schema", () => { + const tbox = FromSchema({ + allOf: [ + { type: "object", properties: { name: { type: "string" } }, required: ["name"] }, + { type: "object", properties: { age: { type: "number" } }, required: ["age"] }, + ], + }); + expect(Value.Check(tbox, { name: "A", age: 1 })).toBe(true); + }); + + it("converts anyOf schema", () => { + const tbox = FromSchema({ + anyOf: [{ type: "string" }, { type: "number" }], + }); + expect(Value.Check(tbox, "hello")).toBe(true); + expect(Value.Check(tbox, 42)).toBe(true); + }); + + it("converts oneOf schema", () => { + const tbox = FromSchema({ + oneOf: [{ type: "string" }, { type: "number" }], + }); + expect(Value.Check(tbox, "hello")).toBe(true); + expect(Value.Check(tbox, 42)).toBe(true); + }); + + it("converts const schema with object value", () => { + const tbox = FromSchema({ const: { key: "value" } }); + expect(KindGuard.IsSchema(tbox)).toBe(true); + }); + + it("converts primitive const as literal (falls through to Unknown for non-object const)", () => { + const tbox = FromSchema({ const: "fixed" }); + expect(KindGuard.IsSchema(tbox)).toBe(true); + }); + + it("converts $ref schema", () => { + const tbox = FromSchema({ $ref: "#/definitions/MyType" }); + expect(KindGuard.IsSchema(tbox)).toBe(true); + }); + + it("returns Unknown for unrecognized schemas", () => { + const tbox = FromSchema({}); + expect(KindGuard.IsSchema(tbox)).toBe(true); + }); +}); \ No newline at end of file diff --git a/test/registry.test.ts b/test/registry.test.ts new file mode 100644 index 0000000..4359bbf --- /dev/null +++ b/test/registry.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect } from "vitest"; +import { OperationRegistry } from "../src/registry.js"; +import { OperationType, type IOperationDefinition, type OperationContext } from "../src/index.js"; +import * as Type from "@alkdev/typebox"; +import { Value } from "@alkdev/typebox/value"; + +function makeOperation(overrides: Partial = {}): IOperationDefinition { + return { + name: "testOp", + namespace: "test", + version: "1.0.0", + type: OperationType.QUERY, + description: "A test operation", + inputSchema: Type.Object({ value: Type.String() }), + outputSchema: Type.Object({ result: Type.String() }), + accessControl: { requiredScopes: [] }, + handler: async (input: any) => ({ result: `processed: ${input.value}` }), + ...overrides, + }; +} + +describe("OperationRegistry", () => { + it("registers and retrieves an operation", () => { + const registry = new OperationRegistry(); + const op = makeOperation(); + registry.register(op); + expect(registry.get("test.testOp")).toBe(op); + }); + + it("retrieves by namespace and name", () => { + const registry = new OperationRegistry(); + const op = makeOperation(); + registry.register(op); + expect(registry.getByName("test", "testOp")).toBe(op); + }); + + it("returns undefined for missing operations", () => { + const registry = new OperationRegistry(); + expect(registry.get("nonexistent.op")).toBeUndefined(); + }); + + it("lists all registered operations", () => { + const registry = new OperationRegistry(); + registry.register(makeOperation()); + registry.register(makeOperation({ name: "op2" })); + expect(registry.list()).toHaveLength(2); + }); + + it("registerAll registers multiple operations", () => { + const registry = new OperationRegistry(); + registry.registerAll([makeOperation(), makeOperation({ name: "op2" })]); + expect(registry.list()).toHaveLength(2); + }); + + it("extracts spec without handler", () => { + const registry = new OperationRegistry(); + registry.register(makeOperation()); + const spec = registry.getSpec("test.testOp")!; + expect(spec).toBeDefined(); + expect((spec as any).handler).toBeUndefined(); + expect(spec.name).toBe("testOp"); + }); + + it("getAllSpecs returns all specs", () => { + const registry = new OperationRegistry(); + registry.register(makeOperation()); + registry.register(makeOperation({ name: "op2" })); + expect(registry.getAllSpecs()).toHaveLength(2); + }); + + it("executes an operation and validates input", async () => { + const registry = new OperationRegistry(); + registry.register(makeOperation()); + const result = await registry.execute("test.testOp", { value: "hello" }, {} as OperationContext); + expect(result).toEqual({ result: "processed: hello" }); + }); + + it("throws on invalid input", async () => { + const registry = new OperationRegistry(); + registry.register(makeOperation()); + await expect( + registry.execute("test.testOp", { wrong: "field" }, {} as OperationContext) + ).rejects.toThrow(); + }); + + it("throws on missing operation", async () => { + const registry = new OperationRegistry(); + await expect( + registry.execute("missing.op", {}, {} as OperationContext) + ).rejects.toThrow("Operation not found"); + }); + + it("warns on output mismatch but returns result", async () => { + const registry = new OperationRegistry(); + registry.register(makeOperation({ + handler: async () => ({ unexpected: "field" }), + })); + const result = await registry.execute("test.testOp", { value: "x" }, {} as OperationContext); + expect(result).toEqual({ unexpected: "field" }); + }); +}); \ No newline at end of file diff --git a/test/validation.test.ts b/test/validation.test.ts new file mode 100644 index 0000000..202d412 --- /dev/null +++ b/test/validation.test.ts @@ -0,0 +1,77 @@ +import { describe, it, expect } from "vitest"; +import { formatValueErrors, assertIsSchema, validateOrThrow, collectErrors } from "../src/validation.js"; +import { Type } from "@alkdev/typebox"; +import { KindGuard } from "@alkdev/typebox"; +import { Value } from "@alkdev/typebox/value"; + +describe("formatValueErrors", () => { + it("formats errors with default indent", () => { + const errors = [{ path: "/foo", message: "Expected string" }]; + expect(formatValueErrors(errors)).toBe(" - /foo: Expected string"); + }); + + it("formats errors with custom indent", () => { + const errors = [{ path: "/bar", message: "Expected number" }]; + expect(formatValueErrors(errors, " * ")).toBe(" * /bar: Expected number"); + }); + + it("formats multiple errors", () => { + const errors = [ + { path: "/a", message: "Error 1" }, + { path: "/b", message: "Error 2" }, + ]; + expect(formatValueErrors(errors)).toBe(" - /a: Error 1\n - /b: Error 2"); + }); +}); + +describe("assertIsSchema", () => { + it("passes for valid TypeBox schemas", () => { + expect(() => assertIsSchema(Type.String())).not.toThrow(); + }); + + it("passes for Type.Unknown()", () => { + expect(() => assertIsSchema(Type.Unknown())).not.toThrow(); + }); + + it("throws for plain JSON schema objects", () => { + expect(() => assertIsSchema({ type: "string" })).toThrow("Not a valid TypeBox schema"); + }); + + it("includes context in error message", () => { + expect(() => assertIsSchema({ type: "string" }, "myOp inputSchema")).toThrow( + "for myOp inputSchema" + ); + }); +}); + +describe("validateOrThrow", () => { + it("passes for valid input", () => { + const schema = Type.Object({ name: Type.String() }); + expect(() => validateOrThrow(schema, { name: "test" })).not.toThrow(); + }); + + it("throws for invalid input", () => { + const schema = Type.Object({ name: Type.String() }); + expect(() => validateOrThrow(schema, { name: 123 })).toThrow("Validation failed"); + }); + + it("includes context in error message", () => { + const schema = Type.Object({ name: Type.String() }); + expect(() => validateOrThrow(schema, { name: 123 }, "myOp")).toThrow("for myOp"); + }); +}); + +describe("collectErrors", () => { + it("returns empty array for valid input", () => { + const schema = Type.Object({ name: Type.String() }); + expect(collectErrors(schema, { name: "test" })).toEqual([]); + }); + + it("returns errors for invalid input", () => { + const schema = Type.Object({ name: Type.String() }); + const errors = collectErrors(schema, { name: 123 }); + expect(errors.length).toBeGreaterThan(0); + expect(errors[0].path).toBeDefined(); + expect(errors[0].message).toBeDefined(); + }); +}); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..4dd8df9 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "resolveJsonModule": true, + "isolatedModules": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} \ No newline at end of file diff --git a/tsup.config.ts b/tsup.config.ts new file mode 100644 index 0000000..2dad94a --- /dev/null +++ b/tsup.config.ts @@ -0,0 +1,14 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: [ + 'src/index.ts', + 'src/from_mcp.ts', + ], + format: ['esm', 'cjs'], + dts: true, + sourcemap: true, + clean: true, + splitting: true, + target: 'es2022', +}); \ No newline at end of file diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..140add1 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + include: ['test/**/*.test.ts'], + }, +}); \ No newline at end of file