@silkweave/edge 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,6 +92,8 @@ const { adapter, handler } = edge({
92
92
  | `enableJsonResponse` | `boolean` | `false` | Return JSON responses instead of SSE streams |
93
93
  | `auth` | `AuthConfig` | - | Bearer-token validation + OAuth routes (see [Auth](#auth)) |
94
94
  | `path` | `string` | `/mcp` | The MCP transport path |
95
+ | `filterActions` | `FilterActions` | - | Per-request tool filter, applied before tools are registered on every POST (the stateless transport recomputes the tool list per request, so permission changes apply on the next `tools/list`). Receives the actions (with their `tags`) and a request stand-in `{ headers, url, method, toolName }` - `method` is the JSON-RPC method (skip DB lookups on `initialize`/`ping`), `toolName` is set for `tools/call`. May be async. A throw surfaces as its `SilkweaveError.statusCode` (e.g. 401) with a JSON-RPC error body, or 500 for other errors - never an empty tool list |
96
+ | `onToolCall` | `OnToolCall` | - | Telemetry hook invoked once per tool call, fire-and-forget (never awaited on the result path; errors logged and swallowed). Events carry `{ action, tool, transport: 'mcp', durationMs, ok, errorCode?, errorMessage?, resultBytes?, sideloaded?, context }` |
95
97
 
96
98
  ## Compound Return Pattern
97
99
 
package/build/index.d.mts CHANGED
@@ -1,11 +1,22 @@
1
1
  import { AuthConfig } from "@silkweave/auth";
2
- import { AdapterGenerator } from "@silkweave/core";
2
+ import { AdapterGenerator, OnToolCall } from "@silkweave/core";
3
+ import { FilterActions } from "@silkweave/mcp/tools";
3
4
 
4
5
  //#region src/adapter/edge.d.ts
5
6
  interface EdgeAdapterOptions {
6
7
  enableJsonResponse?: boolean;
7
8
  auth?: AuthConfig;
8
9
  path?: string;
10
+ /**
11
+ * Per-request tool filter, applied before `registerTools()` on every POST
12
+ * (the stateless transport recomputes the tool list per request). See
13
+ * `FilterActions` for the request stand-in (`headers`/`url`/`method`/
14
+ * `toolName`) and error semantics (a throw surfaces as its
15
+ * `SilkweaveError.statusCode` or 500 - never an empty tool list).
16
+ */
17
+ filterActions?: FilterActions;
18
+ /** Telemetry hook invoked once per tool call (fire-and-forget). */
19
+ onToolCall?: OnToolCall;
9
20
  }
10
21
  interface EdgeAdapter {
11
22
  adapter: AdapterGenerator;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.mts","names":[],"sources":["../src/adapter/edge.ts"],"mappings":";;;;UAMiB,kBAAA;EACf,kBAAA;EACA,IAAA,GAAO,UAAA;EACP,IAAA;AAAA;AAAA,UAGe,WAAA;EACf,OAAA,EAAS,gBAAA;EACT,OAAA,GAAU,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;EACvC,GAAA,GAAM,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;EACnC,IAAA,GAAO,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;EACpC,MAAA,GAAS,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;AAAA;AAAA,iBA0DxB,IAAA,CAAK,OAAA,GAAS,kBAAA,GAA0B,WAAA"}
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/adapter/edge.ts"],"mappings":";;;;;UAMiB,kBAAA;EACf,kBAAA;EACA,IAAA,GAAO,UAAA;EACP,IAAA;;;;;;;;EAQA,aAAA,GAAgB,aAAA;EATT;EAWP,UAAA,GAAa,UAAA;AAAA;AAAA,UAGE,WAAA;EACf,OAAA,EAAS,gBAAA;EACT,OAAA,GAAU,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;EACvC,GAAA,GAAM,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;EACnC,IAAA,GAAO,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;EACpC,MAAA,GAAS,OAAA,EAAS,OAAA,KAAY,OAAA,CAAQ,QAAA;AAAA;AAAA,iBA0DxB,IAAA,CAAK,OAAA,GAAS,kBAAA,GAA0B,WAAA"}
package/build/index.mjs CHANGED
@@ -1,7 +1,8 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
3
3
  import { generateProtectedResourceMetadata, validateToken } from "@silkweave/auth";
4
- import { registerTools } from "@silkweave/mcp/tools";
4
+ import { validateActionDisposition } from "@silkweave/core";
5
+ import { filterErrorResponse, registerTools, rpcInfo } from "@silkweave/mcp/tools";
5
6
  //#region src/adapter/edge.ts
6
7
  const CORS_HEADERS = {
7
8
  "Access-Control-Allow-Origin": "*",
@@ -106,6 +107,26 @@ function edge(options = {}) {
106
107
  });
107
108
  if (result.auth) requestContext = _context.fork({ auth: result.auth });
108
109
  }
110
+ let activeActions = _actions;
111
+ if (options.filterActions) {
112
+ const body = await request.clone().json().catch(() => void 0);
113
+ try {
114
+ activeActions = await options.filterActions(_actions, {
115
+ headers: Object.fromEntries(request.headers.entries()),
116
+ url: request.url,
117
+ ...rpcInfo(body)
118
+ });
119
+ } catch (error) {
120
+ const mapped = filterErrorResponse(error, body);
121
+ return new Response(JSON.stringify(mapped.body), {
122
+ status: mapped.status,
123
+ headers: {
124
+ ...CORS_HEADERS,
125
+ "Content-Type": "application/json"
126
+ }
127
+ });
128
+ }
129
+ }
109
130
  const transport = new WebStandardStreamableHTTPServerTransport({
110
131
  sessionIdGenerator: void 0,
111
132
  enableJsonResponse: options.enableJsonResponse
@@ -118,7 +139,7 @@ function edge(options = {}) {
118
139
  tools: {},
119
140
  logging: {}
120
141
  } });
121
- registerTools(server, _actions, requestContext);
142
+ registerTools(server, activeActions, requestContext, { onToolCall: options.onToolCall });
122
143
  await server.connect(transport);
123
144
  return transport.handleRequest(request);
124
145
  };
@@ -128,6 +149,7 @@ function edge(options = {}) {
128
149
  return {
129
150
  context: _context,
130
151
  start: async (actions) => {
152
+ actions.forEach(validateActionDisposition);
131
153
  _actions = actions;
132
154
  _readyResolve();
133
155
  },
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":[],"sources":["../src/adapter/edge.ts"],"sourcesContent":["import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'\nimport { AuthConfig, generateProtectedResourceMetadata, OAuthRequest, OAuthResponse, validateToken } from '@silkweave/auth'\nimport { Action, AdapterGenerator, SilkweaveContext, SilkweaveOptions } from '@silkweave/core'\nimport { registerTools } from '@silkweave/mcp/tools'\n\nexport interface EdgeAdapterOptions {\n enableJsonResponse?: boolean\n auth?: AuthConfig\n path?: string\n}\n\nexport interface EdgeAdapter {\n adapter: AdapterGenerator\n handler: (request: Request) => Promise<Response>\n GET: (request: Request) => Promise<Response>\n POST: (request: Request) => Promise<Response>\n DELETE: (request: Request) => Promise<Response>\n}\n\nconst CORS_HEADERS: Record<string, string> = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',\n 'Access-Control-Allow-Headers': '*',\n 'Access-Control-Max-Age': '86400'\n}\n\nasync function parseOAuthRequest(url: URL, request: Request): Promise<OAuthRequest> {\n let body: Record<string, string> | undefined\n if (request.method === 'POST') {\n const contentType = request.headers.get('content-type') ?? ''\n const text = await request.text()\n if (contentType.includes('json')) {\n body = JSON.parse(text)\n } else {\n body = Object.fromEntries(new URLSearchParams(text))\n }\n }\n return {\n method: request.method,\n url,\n headers: Object.fromEntries(request.headers.entries()),\n body\n }\n}\n\nfunction oauthResponseToResponse(oauthRes: OAuthResponse): Response {\n const responseBody = oauthRes.body\n ? (typeof oauthRes.body === 'string' ? oauthRes.body : JSON.stringify(oauthRes.body))\n : null\n return new Response(responseBody, { status: oauthRes.status, headers: oauthRes.headers })\n}\n\nasync function routeOAuth(\n url: URL,\n request: Request,\n provider: NonNullable<AuthConfig['provider']>,\n callbackPath: string\n): Promise<Response> {\n const oauthReq = await parseOAuthRequest(url, request)\n let oauthRes\n if (url.pathname === '/.well-known/oauth-authorization-server') {\n oauthRes = provider.metadata()\n } else if (url.pathname === '/authorize') {\n oauthRes = await provider.authorize(oauthReq)\n } else if (url.pathname === callbackPath) {\n oauthRes = await provider.callback(oauthReq)\n } else if (url.pathname === '/token') {\n oauthRes = await provider.token(oauthReq)\n } else {\n oauthRes = await provider.register(oauthReq)\n }\n return oauthResponseToResponse(oauthRes)\n}\n\nexport function edge(options: EdgeAdapterOptions = {}): EdgeAdapter {\n const mcpPath = options.path ?? '/mcp'\n const callbackPath = options.auth?.callbackPath ?? '/auth/callback'\n\n let _actions: Action[] = []\n let _options: SilkweaveOptions | null = null\n let _context: SilkweaveContext | null = null\n let _readyResolve: () => void\n const _ready = new Promise<void>((resolve) => {\n _readyResolve = resolve\n })\n\n // Pre-compute valid paths for fast rejection of bogus requests\n const validPaths = new Set<string>([mcpPath])\n if (options.auth?.authorizationServers?.length && options.auth.resourceUrl) {\n validPaths.add('/.well-known/oauth-protected-resource')\n }\n if (options.auth?.provider) {\n validPaths.add('/.well-known/oauth-authorization-server')\n validPaths.add('/authorize')\n validPaths.add(callbackPath)\n validPaths.add('/token')\n validPaths.add('/register')\n }\n\n // OAuth path → allowed methods (built once, not per-request)\n const oauthPaths: Record<string, string[]> | null = options.auth?.provider\n ? {\n '/.well-known/oauth-authorization-server': ['GET'],\n '/authorize': ['GET'],\n [callbackPath]: ['GET'],\n '/token': ['POST'],\n '/register': ['POST']\n }\n : null\n\n const handleRequest = async (request: Request): Promise<Response> => {\n const url = new URL(request.url)\n\n // Fast rejection - no async work, no allocations for unknown paths\n if (!validPaths.has(url.pathname)) {\n return new Response('Not Found', { status: 404 })\n }\n\n // CORS preflight\n if (request.method === 'OPTIONS') {\n return new Response(null, { status: 200, headers: CORS_HEADERS })\n }\n\n // Protected resource metadata (RFC 9728)\n if (url.pathname === '/.well-known/oauth-protected-resource') {\n const metadata = generateProtectedResourceMetadata(options.auth!.resourceUrl!, options.auth!.authorizationServers!, options.auth!.requiredScopes)\n return new Response(JSON.stringify(metadata), {\n headers: { ...CORS_HEADERS, 'Content-Type': 'application/json', 'Cache-Control': 'max-age=3600' }\n })\n }\n\n // OAuth provider routes\n if (oauthPaths) {\n const methods = oauthPaths[url.pathname]\n if (methods) {\n if (!methods.includes(request.method)) {\n return new Response('Method not allowed', { status: 405 })\n }\n return routeOAuth(url, request, options.auth!.provider!, callbackPath)\n }\n }\n\n // MCP transport (stateless): only POST carries JSON-RPC. A standing-stream\n // GET or a session-teardown DELETE has no session to act on - and the SDK's\n // GET handler would open an SSE stream that never closes, hanging the request\n // on serverless runtimes (e.g. Cloudflare Workers). Answer them with 405, which\n // the Streamable HTTP spec explicitly permits for servers without a GET stream.\n if (request.method !== 'POST') {\n return new Response('Method Not Allowed', {\n status: 405,\n headers: { ...CORS_HEADERS, Allow: 'POST' }\n })\n }\n\n // wait for silkweave().start() to complete\n await _ready\n\n let requestContext = _context!\n if (options.auth) {\n const result = await validateToken(request.headers.get('authorization'), options.auth, _context!.fork({ request }))\n if (result.error) {\n return new Response(JSON.stringify(result.error.body), {\n status: result.error.statusCode,\n headers: result.error.headers\n })\n }\n if (result.auth) {\n requestContext = _context!.fork({ auth: result.auth })\n }\n }\n\n const transport = new WebStandardStreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: options.enableJsonResponse\n })\n\n const server = new McpServer({\n name: _options!.name,\n description: _options!.description,\n version: _options!.version\n }, {\n capabilities: { tools: {}, logging: {} }\n })\n\n registerTools(server, _actions, requestContext)\n\n await server.connect(transport)\n return transport.handleRequest(request)\n }\n\n const adapter: AdapterGenerator = (silkweaveOptions: SilkweaveOptions, baseContext: SilkweaveContext) => {\n _options = silkweaveOptions\n _context = baseContext.fork({ adapter: 'edge' })\n return {\n context: _context,\n start: async (actions) => {\n _actions = actions\n _readyResolve()\n },\n stop: async () => {\n _actions = []\n }\n }\n }\n\n return {\n adapter,\n handler: handleRequest,\n GET: handleRequest,\n POST: handleRequest,\n DELETE: handleRequest\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/adapter/edge.ts"],"sourcesContent":["import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'\nimport { WebStandardStreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js'\nimport { AuthConfig, generateProtectedResourceMetadata, OAuthRequest, OAuthResponse, validateToken } from '@silkweave/auth'\nimport { Action, AdapterGenerator, OnToolCall, SilkweaveContext, SilkweaveOptions, validateActionDisposition } from '@silkweave/core'\nimport { filterErrorResponse, registerTools, rpcInfo, type FilterActions } from '@silkweave/mcp/tools'\n\nexport interface EdgeAdapterOptions {\n enableJsonResponse?: boolean\n auth?: AuthConfig\n path?: string\n /**\n * Per-request tool filter, applied before `registerTools()` on every POST\n * (the stateless transport recomputes the tool list per request). See\n * `FilterActions` for the request stand-in (`headers`/`url`/`method`/\n * `toolName`) and error semantics (a throw surfaces as its\n * `SilkweaveError.statusCode` or 500 - never an empty tool list).\n */\n filterActions?: FilterActions\n /** Telemetry hook invoked once per tool call (fire-and-forget). */\n onToolCall?: OnToolCall\n}\n\nexport interface EdgeAdapter {\n adapter: AdapterGenerator\n handler: (request: Request) => Promise<Response>\n GET: (request: Request) => Promise<Response>\n POST: (request: Request) => Promise<Response>\n DELETE: (request: Request) => Promise<Response>\n}\n\nconst CORS_HEADERS: Record<string, string> = {\n 'Access-Control-Allow-Origin': '*',\n 'Access-Control-Allow-Methods': 'GET, POST, DELETE, OPTIONS',\n 'Access-Control-Allow-Headers': '*',\n 'Access-Control-Max-Age': '86400'\n}\n\nasync function parseOAuthRequest(url: URL, request: Request): Promise<OAuthRequest> {\n let body: Record<string, string> | undefined\n if (request.method === 'POST') {\n const contentType = request.headers.get('content-type') ?? ''\n const text = await request.text()\n if (contentType.includes('json')) {\n body = JSON.parse(text)\n } else {\n body = Object.fromEntries(new URLSearchParams(text))\n }\n }\n return {\n method: request.method,\n url,\n headers: Object.fromEntries(request.headers.entries()),\n body\n }\n}\n\nfunction oauthResponseToResponse(oauthRes: OAuthResponse): Response {\n const responseBody = oauthRes.body\n ? (typeof oauthRes.body === 'string' ? oauthRes.body : JSON.stringify(oauthRes.body))\n : null\n return new Response(responseBody, { status: oauthRes.status, headers: oauthRes.headers })\n}\n\nasync function routeOAuth(\n url: URL,\n request: Request,\n provider: NonNullable<AuthConfig['provider']>,\n callbackPath: string\n): Promise<Response> {\n const oauthReq = await parseOAuthRequest(url, request)\n let oauthRes\n if (url.pathname === '/.well-known/oauth-authorization-server') {\n oauthRes = provider.metadata()\n } else if (url.pathname === '/authorize') {\n oauthRes = await provider.authorize(oauthReq)\n } else if (url.pathname === callbackPath) {\n oauthRes = await provider.callback(oauthReq)\n } else if (url.pathname === '/token') {\n oauthRes = await provider.token(oauthReq)\n } else {\n oauthRes = await provider.register(oauthReq)\n }\n return oauthResponseToResponse(oauthRes)\n}\n\nexport function edge(options: EdgeAdapterOptions = {}): EdgeAdapter {\n const mcpPath = options.path ?? '/mcp'\n const callbackPath = options.auth?.callbackPath ?? '/auth/callback'\n\n let _actions: Action[] = []\n let _options: SilkweaveOptions | null = null\n let _context: SilkweaveContext | null = null\n let _readyResolve: () => void\n const _ready = new Promise<void>((resolve) => {\n _readyResolve = resolve\n })\n\n // Pre-compute valid paths for fast rejection of bogus requests\n const validPaths = new Set<string>([mcpPath])\n if (options.auth?.authorizationServers?.length && options.auth.resourceUrl) {\n validPaths.add('/.well-known/oauth-protected-resource')\n }\n if (options.auth?.provider) {\n validPaths.add('/.well-known/oauth-authorization-server')\n validPaths.add('/authorize')\n validPaths.add(callbackPath)\n validPaths.add('/token')\n validPaths.add('/register')\n }\n\n // OAuth path → allowed methods (built once, not per-request)\n const oauthPaths: Record<string, string[]> | null = options.auth?.provider\n ? {\n '/.well-known/oauth-authorization-server': ['GET'],\n '/authorize': ['GET'],\n [callbackPath]: ['GET'],\n '/token': ['POST'],\n '/register': ['POST']\n }\n : null\n\n const handleRequest = async (request: Request): Promise<Response> => {\n const url = new URL(request.url)\n\n // Fast rejection - no async work, no allocations for unknown paths\n if (!validPaths.has(url.pathname)) {\n return new Response('Not Found', { status: 404 })\n }\n\n // CORS preflight\n if (request.method === 'OPTIONS') {\n return new Response(null, { status: 200, headers: CORS_HEADERS })\n }\n\n // Protected resource metadata (RFC 9728)\n if (url.pathname === '/.well-known/oauth-protected-resource') {\n const metadata = generateProtectedResourceMetadata(options.auth!.resourceUrl!, options.auth!.authorizationServers!, options.auth!.requiredScopes)\n return new Response(JSON.stringify(metadata), {\n headers: { ...CORS_HEADERS, 'Content-Type': 'application/json', 'Cache-Control': 'max-age=3600' }\n })\n }\n\n // OAuth provider routes\n if (oauthPaths) {\n const methods = oauthPaths[url.pathname]\n if (methods) {\n if (!methods.includes(request.method)) {\n return new Response('Method not allowed', { status: 405 })\n }\n return routeOAuth(url, request, options.auth!.provider!, callbackPath)\n }\n }\n\n // MCP transport (stateless): only POST carries JSON-RPC. A standing-stream\n // GET or a session-teardown DELETE has no session to act on - and the SDK's\n // GET handler would open an SSE stream that never closes, hanging the request\n // on serverless runtimes (e.g. Cloudflare Workers). Answer them with 405, which\n // the Streamable HTTP spec explicitly permits for servers without a GET stream.\n if (request.method !== 'POST') {\n return new Response('Method Not Allowed', {\n status: 405,\n headers: { ...CORS_HEADERS, Allow: 'POST' }\n })\n }\n\n // wait for silkweave().start() to complete\n await _ready\n\n let requestContext = _context!\n if (options.auth) {\n const result = await validateToken(request.headers.get('authorization'), options.auth, _context!.fork({ request }))\n if (result.error) {\n return new Response(JSON.stringify(result.error.body), {\n status: result.error.statusCode,\n headers: result.error.headers\n })\n }\n if (result.auth) {\n requestContext = _context!.fork({ auth: result.auth })\n }\n }\n\n let activeActions = _actions\n if (options.filterActions) {\n // Clone so the transport can still consume the original body stream.\n const body: unknown = await request.clone().json().catch(() => undefined)\n try {\n activeActions = await options.filterActions(_actions, {\n headers: Object.fromEntries(request.headers.entries()),\n url: request.url,\n ...rpcInfo(body)\n })\n } catch (error) {\n const mapped = filterErrorResponse(error, body)\n return new Response(JSON.stringify(mapped.body), {\n status: mapped.status,\n headers: { ...CORS_HEADERS, 'Content-Type': 'application/json' }\n })\n }\n }\n\n const transport = new WebStandardStreamableHTTPServerTransport({\n sessionIdGenerator: undefined,\n enableJsonResponse: options.enableJsonResponse\n })\n\n const server = new McpServer({\n name: _options!.name,\n description: _options!.description,\n version: _options!.version\n }, {\n capabilities: { tools: {}, logging: {} }\n })\n\n registerTools(server, activeActions, requestContext, { onToolCall: options.onToolCall })\n\n await server.connect(transport)\n return transport.handleRequest(request)\n }\n\n const adapter: AdapterGenerator = (silkweaveOptions: SilkweaveOptions, baseContext: SilkweaveContext) => {\n _options = silkweaveOptions\n _context = baseContext.fork({ adapter: 'edge' })\n return {\n context: _context,\n start: async (actions) => {\n actions.forEach(validateActionDisposition)\n _actions = actions\n _readyResolve()\n },\n stop: async () => {\n _actions = []\n }\n }\n }\n\n return {\n adapter,\n handler: handleRequest,\n GET: handleRequest,\n POST: handleRequest,\n DELETE: handleRequest\n }\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@silkweave/edge",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Silkweave Web-Standard edge/serverless adapter (Cloudflare Workers, Vercel, Bun, Deno, Hono)",
5
5
  "license": "MIT",
6
6
  "homepage": "https://www.silkweave.dev",
@@ -28,9 +28,9 @@
28
28
  "dependencies": {
29
29
  "@modelcontextprotocol/sdk": "^1.29.0",
30
30
  "zod": "^3.25.0",
31
- "@silkweave/auth": "3.1.0",
32
- "@silkweave/core": "3.1.0",
33
- "@silkweave/mcp": "3.1.0"
31
+ "@silkweave/auth": "3.2.0",
32
+ "@silkweave/core": "3.2.0",
33
+ "@silkweave/mcp": "3.2.0"
34
34
  },
35
35
  "devDependencies": {
36
36
  "@eslint/js": "^10.0.1",
@@ -41,7 +41,8 @@
41
41
  "tsdown": "^0.21.8",
42
42
  "tsx": "^4.21.0",
43
43
  "typescript": "^5.9.3",
44
- "typescript-eslint": "^8.56.1"
44
+ "typescript-eslint": "^8.56.1",
45
+ "vitest": "^4.1.9"
45
46
  },
46
47
  "scripts": {
47
48
  "clean": "rimraf build",
@@ -49,6 +50,7 @@
49
50
  "watch": "tsdown --watch",
50
51
  "typecheck": "tsc --noEmit",
51
52
  "lint": "eslint",
52
- "check": "pnpm lint && pnpm typecheck"
53
+ "check": "pnpm lint && pnpm typecheck",
54
+ "test": "vitest run"
53
55
  }
54
56
  }