diff --git a/README.md b/README.md new file mode 100644 index 0000000..fddd46e --- /dev/null +++ b/README.md @@ -0,0 +1,85 @@ +# Churnkey SDK + +A production-ready cancel flow for React. Open source. Optionally connects to [Churnkey](https://churnkey.co) for analytics and AI-powered retention. + +## Packages + +| Package | Description | +| --- | --- | +| [`@churnkey/react`](./packages/react) | The cancel flow. Drop-in component, headless hook, or core state machine. React 18 and 19. | +| [`@churnkey/node`](./packages/node) | Server-side token generation. Only needed if Churnkey is handling billing actions for you, or if you're using the hosted embed. | +| [`@churnkey/mcp`](./packages/mcp) | Model Context Protocol server. Lets Claude, Cursor, and other AI agents query your Churnkey data (sessions, analytics, DSR) using a Data API key. | + +## Quick start + +```bash +npm install @churnkey/react +``` + +```tsx +import { CancelFlow } from '@churnkey/react' +import '@churnkey/react/styles.css' + + myBilling.applyCoupon(offer.couponId)} + handlePause={async (offer) => myBilling.pause({ months: offer.months })} + handleCancel={async () => myBilling.cancel()} + onClose={() => setOpen(false)} +/> +``` + +## Documentation and resources + +- [`@churnkey/react`](./packages/react/README.md) — full API reference, customization, headless usage, custom step types +- [`@churnkey/node`](./packages/node/README.md) — `createToken` and `authHash` +- [`@churnkey/mcp`](./packages/mcp/README.md) — MCP server for Claude / Cursor / Claude Desktop +- [churnkey.co](https://churnkey.co) — dashboard, hosted embed, AI retention features + +## Repo layout + +``` +sdk/ +├── packages/ +│ ├── react/ @churnkey/react +│ ├── node/ @churnkey/node +│ └── mcp/ @churnkey/mcp +├── apps/ +│ └── playground/ internal dev playground +└── scripts/ release + tooling helpers +``` + +## Local development + +```bash +pnpm install +pnpm --filter @churnkey/react build +pnpm --filter @churnkey/react test +``` + +To work against the playground: + +```bash +pnpm --filter @churnkey/react dev # rebuilds on change +pnpm --filter @churnkey/playground dev # runs the playground app +``` + +## License + +MIT diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md new file mode 100644 index 0000000..a33bece --- /dev/null +++ b/packages/mcp/CHANGELOG.md @@ -0,0 +1,19 @@ +# Changelog + +Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Expect breaking changes in minor versions while we're pre-1.0. + +## 0.1.0 — Unreleased + +First public release. + +### Added + +- MCP server (`npx -y @churnkey/mcp`) authenticating with a Churnkey Data API key (`x-ck-app` + `x-ck-api-key`). +- Read-only tools backed by `/v1/data/*`: + - `list_sessions` — session-level detail with structured filters (enums for `saveType`/`offerType`/`billingInterval`, typed booleans/integers, ID lookups) and a `not` exclusion object for negation. + - `aggregate_sessions` — counts grouped by one or more breakdown dimensions (time series via `day`/`week`/`month`/`invoiceMonth`). + - `get_api_usage` — API call volume by date range. +- Compliance tools: + - `dsr_access` — GDPR/CCPA right-to-know. + - `dsr_delete` — GDPR/CCPA right-to-delete (destructive). +- Programmatic exports (`createServer`, `loadConfig`) for embedding the server in another Node process. diff --git a/packages/mcp/README.md b/packages/mcp/README.md new file mode 100644 index 0000000..5be40c4 --- /dev/null +++ b/packages/mcp/README.md @@ -0,0 +1,86 @@ +# @churnkey/mcp + +Model Context Protocol server for [Churnkey](https://churnkey.co). Lets AI agents (Claude Code, Cursor, Claude Desktop, etc.) read your sessions, run analytics queries, and handle GDPR/unsubscribe requests. + +## Tools + +| Tool | Description | +|------|-------------| +| `list_sessions` | Cancel/dunning sessions, with filters for date range, customer, outcome (saveType/canceled/aborted), plan, segment, A/B test, etc. Negation via `not: { ... }`. Default 50 / max 500 per call. | +| `aggregate_sessions` | Session counts, optionally grouped by `breakdownBy` dimensions (saveType, offerType, planId, day/week/month, …). Same filter set as `list_sessions`. | +| `get_api_usage` | API call volume — useful for "is the embed firing?" debugging. | +| `dsr_access` | GDPR/CCPA data access by email. | +| `dsr_delete` | GDPR/CCPA data delete by email. *Destructive.* | + +Each tool's input schema is fully described to the MCP client — enums for `saveType` / `offerType` / `billingInterval` / breakdown dimensions, `not` object for exclusions, structured types for booleans and numbers. Mode (live vs test) is set by the API key prefix; pass a `test_`-prefixed key to query test data. + +## Setup + +1. Get your **App ID** and **Data API Key** from `app.churnkey.co/settings/data-api`. +2. Add the server to your MCP client config. + +### Claude Desktop / Claude Code + +`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) or `~/.claude/claude_desktop_config.json`: + +```json +{ + "mcpServers": { + "churnkey": { + "command": "npx", + "args": ["-y", "@churnkey/mcp"], + "env": { + "CHURNKEY_APP_ID": "your_app_id", + "CHURNKEY_API_KEY": "your_api_key" + } + } + } +} +``` + +### Cursor + +`~/.cursor/mcp.json`: + +```json +{ + "mcpServers": { + "churnkey": { + "command": "npx", + "args": ["-y", "@churnkey/mcp"], + "env": { + "CHURNKEY_APP_ID": "your_app_id", + "CHURNKEY_API_KEY": "your_api_key" + } + } + } +} +``` + +Restart the client after editing config. + +## Environment variables + +| Var | Required | Default | +|-----|----------|---------| +| `CHURNKEY_APP_ID` | yes | — | +| `CHURNKEY_API_KEY` | yes | — | +| `CHURNKEY_API_URL` | no | `https://api.churnkey.co/v1` | + +Use a `test_`-prefixed API key for staging data. + +## Programmatic use + +You can also embed the server in another Node process: + +```ts +import { createServer, loadConfig } from '@churnkey/mcp' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' + +const server = createServer(loadConfig()) +await server.connect(new StdioServerTransport()) +``` + +## License + +MIT diff --git a/packages/mcp/package.json b/packages/mcp/package.json new file mode 100644 index 0000000..434df9e --- /dev/null +++ b/packages/mcp/package.json @@ -0,0 +1,43 @@ +{ + "name": "@churnkey/mcp", + "version": "0.1.0", + "description": "Model Context Protocol server for Churnkey. Read-only access to sessions, analytics, and DSR/unsubscribe endpoints.", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/churnkey/sdk.git", + "directory": "packages/mcp" + }, + "keywords": ["churnkey", "mcp", "model-context-protocol", "claude", "cursor", "subscription", "retention"], + "type": "module", + "bin": { + "churnkey-mcp": "./dist/index.js" + }, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + } + }, + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "files": ["dist"], + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "typecheck": "tsc --noEmit", + "start": "node dist/index.js" + }, + "dependencies": { + "@modelcontextprotocol/sdk": "^1.0.4", + "zod": "^3.23.8" + }, + "devDependencies": { + "@types/node": "^22.0.0", + "tsup": "^8.0.0", + "typescript": "^5.5.0" + } +} diff --git a/packages/mcp/src/client.ts b/packages/mcp/src/client.ts new file mode 100644 index 0000000..774d81e --- /dev/null +++ b/packages/mcp/src/client.ts @@ -0,0 +1,102 @@ +import type { ChurnkeyMcpConfig } from './config' + +export class ChurnkeyApiError extends Error { + readonly status: number + readonly body: unknown + + constructor(status: number, message: string, body: unknown) { + super(message) + this.name = 'ChurnkeyApiError' + this.status = status + this.body = body + } +} + +export interface RequestOptions { + query?: Record + body?: unknown +} + +export class ChurnkeyClient { + constructor(private readonly config: ChurnkeyMcpConfig) {} + + async get(path: string, options: RequestOptions = {}): Promise { + return this.request('GET', path, options) + } + + async post(path: string, options: RequestOptions = {}): Promise { + return this.request('POST', path, options) + } + + private async request(method: 'GET' | 'POST', path: string, options: RequestOptions): Promise { + const url = new URL(`${this.config.baseUrl}${path}`) + if (options.query) { + for (const [key, value] of Object.entries(options.query)) { + if (value === undefined || value === null) continue + if (Array.isArray(value)) { + for (const v of value) url.searchParams.append(key, String(v)) + } else { + url.searchParams.set(key, String(value)) + } + } + } + + const headers: Record = { + 'x-ck-app': this.config.appId, + 'x-ck-api-key': this.config.apiKey, + accept: 'application/json', + } + if (options.body !== undefined) { + headers['content-type'] = 'application/json' + } + + const res = await fetch(url, { + method, + headers, + body: options.body !== undefined ? JSON.stringify(options.body) : undefined, + }) + + const text = await res.text() + let parsed: unknown = text + if (text) { + try { + parsed = JSON.parse(text) + } catch { + // leave as text + } + } + + if (!res.ok) { + throw new ChurnkeyApiError(res.status, mapErrorMessage(res.status, parsed), parsed) + } + + if (parsed && typeof parsed === 'object' && 'data' in (parsed as Record)) { + return (parsed as { data: T }).data + } + return parsed as T + } +} + +function mapErrorMessage(status: number, body: unknown): string { + const apiMessage = + body && typeof body === 'object' && 'message' in (body as Record) + ? String((body as Record).message) + : null + + if (status === 401) { + return 'Churnkey API rejected the credentials. Check CHURNKEY_APP_ID and CHURNKEY_API_KEY in your MCP server config.' + } + if (status === 403) { + return apiMessage ?? 'Churnkey API forbids this action for the supplied API key.' + } + if (status === 404) { + return apiMessage ?? 'Resource not found.' + } + if (status === 422) { + return apiMessage ?? 'Invalid request parameters.' + } + if (status >= 500) { + return apiMessage ?? `Churnkey API returned ${status}. Try again or check status.churnkey.co.` + } + return apiMessage ?? `Churnkey API error ${status}` +} diff --git a/packages/mcp/src/config.ts b/packages/mcp/src/config.ts new file mode 100644 index 0000000..08e6c3d --- /dev/null +++ b/packages/mcp/src/config.ts @@ -0,0 +1,15 @@ +export interface ChurnkeyMcpConfig { + appId: string + apiKey: string + baseUrl: string +} + +export function loadConfig(env: NodeJS.ProcessEnv = process.env): ChurnkeyMcpConfig { + const appId = env.CHURNKEY_APP_ID + const apiKey = env.CHURNKEY_API_KEY + if (!appId) throw new Error('CHURNKEY_APP_ID environment variable is required') + if (!apiKey) throw new Error('CHURNKEY_API_KEY environment variable is required') + + const baseUrl = env.CHURNKEY_API_URL ?? 'https://api.churnkey.co/v1' + return { appId, apiKey, baseUrl: baseUrl.replace(/\/$/, '') } +} diff --git a/packages/mcp/src/index.ts b/packages/mcp/src/index.ts new file mode 100644 index 0000000..fcd48b5 --- /dev/null +++ b/packages/mcp/src/index.ts @@ -0,0 +1,24 @@ +import { fileURLToPath } from 'node:url' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { loadConfig } from './config' +import { createServer } from './server' + +export type { ChurnkeyMcpConfig } from './config' +export { loadConfig } from './config' +export { createServer, SERVER_NAME, SERVER_VERSION } from './server' + +async function main() { + const config = loadConfig() + const server = createServer(config) + const transport = new StdioServerTransport() + await server.connect(transport) +} + +const isEntrypoint = process.argv[1] ? fileURLToPath(import.meta.url) === process.argv[1] : false + +if (isEntrypoint) { + main().catch((err) => { + console.error(err instanceof Error ? err.message : err) + process.exit(1) + }) +} diff --git a/packages/mcp/src/rate-limit.ts b/packages/mcp/src/rate-limit.ts new file mode 100644 index 0000000..489c658 --- /dev/null +++ b/packages/mcp/src/rate-limit.ts @@ -0,0 +1,23 @@ +/** + * Soft client-side limiter so a runaway agent loop doesn't hammer the upstream API. + * Hard limits live in churnkey-api; this is just a courtesy throttle. + */ +export class RateLimiter { + private timestamps: number[] = [] + + constructor( + private readonly maxRequests: number, + private readonly windowMs: number, + ) {} + + async acquire(): Promise { + const now = Date.now() + this.timestamps = this.timestamps.filter((t) => now - t < this.windowMs) + if (this.timestamps.length >= this.maxRequests) { + const wait = this.windowMs - (now - this.timestamps[0]!) + await new Promise((r) => setTimeout(r, wait)) + return this.acquire() + } + this.timestamps.push(now) + } +} diff --git a/packages/mcp/src/server.ts b/packages/mcp/src/server.ts new file mode 100644 index 0000000..088b937 --- /dev/null +++ b/packages/mcp/src/server.ts @@ -0,0 +1,45 @@ +import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js' +import { ChurnkeyApiError, ChurnkeyClient } from './client' +import type { ChurnkeyMcpConfig } from './config' +import { RateLimiter } from './rate-limit' +import { allTools } from './tools' + +export const SERVER_NAME = 'churnkey-mcp' +export const SERVER_VERSION = '0.1.0' + +export function createServer(config: ChurnkeyMcpConfig): McpServer { + const server = new McpServer({ name: SERVER_NAME, version: SERVER_VERSION }) + const client = new ChurnkeyClient(config) + const limiter = new RateLimiter(10, 1000) + + for (const tool of allTools(client)) { + server.registerTool( + tool.name, + { + title: tool.title, + description: tool.description, + inputSchema: tool.inputSchema.shape, + annotations: tool.annotations, + }, + async (args: unknown) => { + await limiter.acquire() + try { + const parsed = tool.inputSchema.parse(args ?? {}) + const result = await tool.handler(parsed) + return { + content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], + } + } catch (err) { + const message = + err instanceof ChurnkeyApiError ? err.message : err instanceof Error ? err.message : String(err) + return { + isError: true, + content: [{ type: 'text', text: message }], + } + } + }, + ) + } + + return server +} diff --git a/packages/mcp/src/tools/dsr.ts b/packages/mcp/src/tools/dsr.ts new file mode 100644 index 0000000..c9fc796 --- /dev/null +++ b/packages/mcp/src/tools/dsr.ts @@ -0,0 +1,42 @@ +import { z } from 'zod' +import type { ChurnkeyClient } from '../client' +import type { ToolDefinition } from './types' + +const accessInput = z.object({ + email: z + .string() + .email() + .describe('Customer email (exact match) to fetch all stored Churnkey data for. Case-insensitive.'), +}) + +const deleteInput = z.object({ + email: z + .string() + .email() + .describe( + 'Customer email (exact match) to permanently delete from Churnkey. All sessions, feedback, and PII associated with this email are removed.', + ), +}) + +export function dsrTools(client: ChurnkeyClient): ToolDefinition[] { + return [ + { + name: 'dsr_access', + title: 'GDPR/CCPA data access request', + description: + 'Fetch every record Churnkey holds for a customer email — sessions, surveys, feedback, accepted offers. Read-only. Use to fulfill GDPR Article 15 / CCPA right-to-know requests.', + inputSchema: accessInput, + annotations: { readOnlyHint: true, openWorldHint: true }, + handler: async (args) => client.post('/data/dsr/access', { body: args }), + }, + { + name: 'dsr_delete', + title: 'GDPR/CCPA data delete request', + description: + 'Permanently delete all Churnkey data for a customer email (GDPR Article 17 / CCPA right-to-delete). DESTRUCTIVE and irreversible. Always confirm the exact email with the user before invoking.', + inputSchema: deleteInput, + annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: true }, + handler: async (args) => client.post('/data/dsr/delete', { body: args }), + }, + ] +} diff --git a/packages/mcp/src/tools/filters.ts b/packages/mcp/src/tools/filters.ts new file mode 100644 index 0000000..c4c3dc4 --- /dev/null +++ b/packages/mcp/src/tools/filters.ts @@ -0,0 +1,174 @@ +import { z } from 'zod' + +/** + * Mirrors src/helpers/shared.js OFFER_TYPE_LIST in churnkey-api. + * Keep in sync if new offer types are added there. + */ +export const OFFER_TYPE_VALUES = [ + 'PAUSE', + 'DISCOUNT', + 'CONTACT', + 'PLAN_CHANGE', + 'REDIRECT', + 'TRIAL_EXTENSION', + 'CUSTOM', +] as const + +/** + * `saveType` is derived: null when canceled=true, otherwise the offerType the + * customer accepted, or 'ABANDON' if the session ended without a decision. + */ +export const SAVE_TYPE_VALUES = [...OFFER_TYPE_VALUES, 'ABANDON'] as const + +/** Common Stripe billing intervals. Other values pass through as strings. */ +export const BILLING_INTERVAL_VALUES = ['day', 'week', 'month', 'year'] as const + +/** + * Breakdown dimensions accepted by /v1/data/session-aggregation. + * Time dimensions (day/week/month/invoiceMonth) produce time series; + * combine with attribute dimensions to break a series down further. + */ +export const BREAKDOWN_VALUES = [ + 'day', + 'week', + 'month', + 'invoiceMonth', + 'saveType', + 'offerType', + 'response', + 'aborted', + 'canceled', + 'trial', + 'segmentId', + 'abtest', + 'planId', + 'billingInterval', + 'couponId', + 'pauseDuration', + 'currency', + 'sessionCurrency', + 'bounced', + 'ageMonths', + 'ageQuarters', + 'ageYears', + 'cooldownProjection', +] as const + +const dateRange = { + startDate: z + .string() + .optional() + .describe( + 'Inclusive lower bound on session createdAt. ISO 8601 date or datetime, e.g. "2026-01-01" or "2026-01-01T00:00:00Z".', + ), + endDate: z.string().optional().describe('Inclusive upper bound on session createdAt. ISO 8601 date or datetime.'), +} + +/** Filters supported by both list_sessions and aggregate_sessions. */ +const filterShape = { + sessionId: z.string().optional().describe('Single Churnkey session ID. Returns the matching session only.'), + customerEmail: z + .string() + .optional() + .describe('Customer email (exact match). Use this to look up all sessions for one customer.'), + customerId: z + .string() + .optional() + .describe('Customer ID as it was passed from the merchant (matches customer.id). Exact match.'), + segmentId: z.string().optional().describe('Filter to sessions that matched a specific segment.'), + abtest: z.string().optional().describe('Filter to sessions that ran a specific A/B test (test ID).'), + saveType: z + .enum(SAVE_TYPE_VALUES) + .optional() + .describe( + 'Outcome bucket. ABANDON = customer left without deciding. Null in the response means the cancel went through. One of the OFFER_TYPE values means the customer accepted that offer.', + ), + offerType: z + .enum(OFFER_TYPE_VALUES) + .optional() + .describe('The type of offer the customer accepted (only set on saved sessions).'), + response: z + .string() + .optional() + .describe('Survey choice value the customer selected (e.g. "TOO_EXPENSIVE", "MISSING_FEATURE"). Free text.'), + aborted: z.boolean().optional().describe('Customer closed the flow without completing it.'), + canceled: z.boolean().optional().describe('Customer fully canceled their subscription.'), + trial: z.boolean().optional().describe('Customer was on trial when the session started.'), + bounced: z + .boolean() + .optional() + .describe( + 'Whether the session bounced (loaded but did not interact). Default API behavior excludes bounced sessions; set explicitly to include or exclude.', + ), + planId: z.string().optional().describe('Customer plan/price ID at session start.'), + billingInterval: z.enum(BILLING_INTERVAL_VALUES).optional().describe('Billing interval at session start.'), + couponId: z.string().optional().describe('Coupon ID applied as part of the accepted offer.'), + pauseDuration: z.number().int().optional().describe('Pause duration (months) on the accepted pause offer.'), + sessionCurrency: z.string().optional().describe('Customer currency at session start (ISO 4217, e.g. "usd", "eur").'), + ageYears: z.number().int().optional().describe('Customer account age in years at session start.'), +} + +/** Negation versions of the same filters — pass "not: { saveType: 'DISCOUNT' }" etc. */ +const notShape = { + sessionId: filterShape.sessionId, + customerEmail: filterShape.customerEmail, + customerId: filterShape.customerId, + segmentId: filterShape.segmentId, + abtest: filterShape.abtest, + saveType: filterShape.saveType, + offerType: filterShape.offerType, + response: filterShape.response, + aborted: filterShape.aborted, + canceled: filterShape.canceled, + trial: filterShape.trial, + bounced: filterShape.bounced, + planId: filterShape.planId, + billingInterval: filterShape.billingInterval, + couponId: filterShape.couponId, + pauseDuration: filterShape.pauseDuration, + sessionCurrency: filterShape.sessionCurrency, + ageYears: filterShape.ageYears, +} + +export const sharedFilterFields = { + ...dateRange, + ...filterShape, + not: z + .object(notShape) + .partial() + .optional() + .describe( + 'Exclusion filters. Each key is the same as the top-level filter but matches "not equal" instead. Example: { saveType: "ABANDON" } returns only saved sessions.', + ), +} + +/** + * Convert structured input ({ saveType, not: { canceled }, breakdownBy }) into the + * flat query-string shape the API expects ({ saveType, '-canceled', breakdown }). + */ +export function buildQuery(args: Record): Record { + const { not, breakdownBy, ...rest } = args as { + not?: Record + breakdownBy?: readonly string[] + [k: string]: unknown + } + + const query: Record = {} + for (const [k, v] of Object.entries(rest)) { + if (v === undefined || v === null) continue + query[k] = v + } + + if (not) { + for (const [k, v] of Object.entries(not)) { + if (v === undefined || v === null) continue + query[`-${k}`] = v + } + } + + if (breakdownBy && breakdownBy.length > 0) { + query.breakdown = breakdownBy.join('-') + } + + return query +} diff --git a/packages/mcp/src/tools/index.ts b/packages/mcp/src/tools/index.ts new file mode 100644 index 0000000..5bb7429 --- /dev/null +++ b/packages/mcp/src/tools/index.ts @@ -0,0 +1,10 @@ +import type { ChurnkeyClient } from '../client' +import { dsrTools } from './dsr' +import { sessionTools } from './sessions' +import type { ToolDefinition } from './types' + +export function allTools(client: ChurnkeyClient): ToolDefinition[] { + return [...sessionTools(client), ...dsrTools(client)] +} + +export type { ToolDefinition } from './types' diff --git a/packages/mcp/src/tools/sessions.ts b/packages/mcp/src/tools/sessions.ts new file mode 100644 index 0000000..875b1d1 --- /dev/null +++ b/packages/mcp/src/tools/sessions.ts @@ -0,0 +1,81 @@ +import { z } from 'zod' +import type { ChurnkeyClient } from '../client' +import { BREAKDOWN_VALUES, buildQuery, sharedFilterFields } from './filters' +import type { ToolDefinition } from './types' + +const listSessionsInput = z.object({ + ...sharedFilterFields, + limit: z + .number() + .int() + .min(1) + .max(500) + .default(50) + .describe( + 'Max sessions to return per call. Defaults to 50; capped at 500 to keep responses small for agent context. For totals, use aggregate_sessions instead — it does not load session documents.', + ), + skip: z.number().int().min(0).optional().describe('Pagination offset. Combine with limit to page through results.'), +}) + +const aggregateSessionsInput = z.object({ + ...sharedFilterFields, + breakdownBy: z + .array(z.enum(BREAKDOWN_VALUES)) + .optional() + .describe( + 'Group counts by these dimensions. Multiple dimensions produce a cross-tab; e.g. ["month","saveType"] returns one row per (month, saveType) pair. Omit for a single grand total. Time dimensions: day, week, month, invoiceMonth.', + ), +}) + +const apiUsageInput = z.object({ + startDate: z.string().optional().describe('Inclusive lower bound. ISO 8601 date or datetime.'), + endDate: z.string().optional().describe('Inclusive upper bound. ISO 8601 date or datetime.'), +}) + +export function sessionTools(client: ChurnkeyClient): ToolDefinition[] { + return [ + { + name: 'list_sessions', + title: 'List Churnkey sessions', + description: [ + 'List individual cancel/dunning sessions for the org. Each session is one customer interaction with a retention flow.', + '', + 'Filters: scope by date (startDate/endDate), customer (customerEmail/customerId), outcome (saveType/offerType/canceled/aborted/trial), or attributes (planId/billingInterval/segmentId/abtest). Use the `not` object to exclude values.', + '', + 'Use this when you need session-level detail (e.g. "show me the 10 most recent sessions where a discount was offered"). For counts and breakdowns, prefer aggregate_sessions — it ships less data.', + '', + 'Mode (live vs test) is determined by the API key prefix; pass a `test_`-prefixed key in your MCP server env to query test data.', + ].join('\n'), + inputSchema: listSessionsInput, + annotations: { readOnlyHint: true, openWorldHint: true }, + handler: async (args) => client.get('/data/sessions', { query: buildQuery(args) }), + }, + { + name: 'aggregate_sessions', + title: 'Aggregate session counts', + description: [ + 'Count sessions, optionally grouped by one or more dimensions. This is the primary stats query — use it before list_sessions when answering volume, breakdown, or trend questions.', + '', + 'Examples:', + '- breakdownBy: [] → single total count', + '- breakdownBy: ["saveType"] → counts by outcome bucket', + '- breakdownBy: ["month","saveType"] → monthly time series broken down by outcome', + '- breakdownBy: ["planId"], filter not: { canceled: true } → saved-session counts per plan', + '', + 'Filters work the same as list_sessions.', + ].join('\n'), + inputSchema: aggregateSessionsInput, + annotations: { readOnlyHint: true, openWorldHint: true }, + handler: async (args) => client.get('/data/session-aggregation', { query: buildQuery(args) }), + }, + { + name: 'get_api_usage', + title: 'Get Churnkey API usage', + description: + 'Return Churnkey API call volume for the org over a date range. Useful for confirming the embed/SDK is firing in production, or diagnosing a sudden drop in tracked sessions.', + inputSchema: apiUsageInput, + annotations: { readOnlyHint: true, openWorldHint: true }, + handler: async (args) => client.get('/data/api-usage', { query: args }), + }, + ] +} diff --git a/packages/mcp/src/tools/types.ts b/packages/mcp/src/tools/types.ts new file mode 100644 index 0000000..0a95c12 --- /dev/null +++ b/packages/mcp/src/tools/types.ts @@ -0,0 +1,17 @@ +import type { z } from 'zod' + +export interface ToolAnnotations { + readOnlyHint?: boolean + destructiveHint?: boolean + idempotentHint?: boolean + openWorldHint?: boolean +} + +export interface ToolDefinition = z.ZodObject> { + name: string + title: string + description: string + inputSchema: Schema + annotations?: ToolAnnotations + handler: (args: z.infer) => Promise +} diff --git a/packages/mcp/tsconfig.json b/packages/mcp/tsconfig.json new file mode 100644 index 0000000..3fbff51 --- /dev/null +++ b/packages/mcp/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src"] +} diff --git a/packages/mcp/tsup.config.ts b/packages/mcp/tsup.config.ts new file mode 100644 index 0000000..aa8e67d --- /dev/null +++ b/packages/mcp/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + sourcemap: true, + clean: true, + banner: { + js: '#!/usr/bin/env node', + }, +}) diff --git a/packages/node/LICENSE b/packages/node/LICENSE new file mode 100644 index 0000000..438c6a5 --- /dev/null +++ b/packages/node/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2026 Churnkey, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE +OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/packages/react/recipes/nps-with-faces.tsx b/packages/react/recipes/nps-with-faces.tsx index 0316f82..2e3c006 100644 --- a/packages/react/recipes/nps-with-faces.tsx +++ b/packages/react/recipes/nps-with-faces.tsx @@ -48,6 +48,7 @@ export function NpsWithFaces({ step, onNext }: CustomStepProps) { aria-pressed={isSelected} style={{ appearance: 'none', + textAlign: 'center', flex: 1, padding: '12px 0', border: `1.5px solid ${isSelected ? 'var(--ck-color-primary)' : 'var(--ck-color-border)'}`, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index de5dddc..b5171c3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -49,6 +49,25 @@ importers: specifier: ^6.0.0 version: 6.4.1(@types/node@22.19.15) + packages/mcp: + dependencies: + '@modelcontextprotocol/sdk': + specifier: ^1.0.4 + version: 1.29.0(zod@3.25.76) + zod: + specifier: ^3.23.8 + version: 3.25.76 + devDependencies: + '@types/node': + specifier: ^22.0.0 + version: 22.19.15 + tsup: + specifier: ^8.0.0 + version: 8.5.1(postcss@8.5.8)(typescript@5.9.3) + typescript: + specifier: ^5.5.0 + version: 5.9.3 + packages/node: devDependencies: '@types/node': @@ -726,6 +745,12 @@ packages: cpu: [x64] os: [win32] + '@hono/node-server@1.19.14': + resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -742,6 +767,16 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} + engines: {node: '>=18'} + peerDependencies: + '@cfworker/json-schema': ^4.1.1 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@cfworker/json-schema': + optional: true + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -963,6 +998,10 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + accepts@2.0.0: + resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} + engines: {node: '>= 0.6'} + acorn@8.16.0: resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} @@ -972,6 +1011,17 @@ packages: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1002,6 +1052,10 @@ packages: engines: {node: '>=6.0.0'} hasBin: true + body-parser@2.2.2: + resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} + engines: {node: '>=18'} + browserslist@4.28.1: resolution: {integrity: sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -1013,6 +1067,10 @@ packages: peerDependencies: esbuild: '>=0.18' + bytes@3.1.2: + resolution: {integrity: sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==} + engines: {node: '>= 0.8'} + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -1021,6 +1079,10 @@ packages: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + caniuse-lite@1.0.30001777: resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} @@ -1051,9 +1113,33 @@ packages: resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} engines: {node: ^14.18.0 || >=16.10.0} + content-disposition@1.1.0: + resolution: {integrity: sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g==} + engines: {node: '>=18'} + + content-type@1.0.5: + resolution: {integrity: sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==} + engines: {node: '>= 0.6'} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie-signature@1.2.2: + resolution: {integrity: sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==} + engines: {node: '>=6.6.0'} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + cors@2.8.6: + resolution: {integrity: sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -1088,6 +1174,10 @@ packages: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} + depd@2.0.0: + resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} + engines: {node: '>= 0.8'} + dequal@2.0.3: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} @@ -1102,9 +1192,16 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + ee-first@1.1.1: + resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} + electron-to-chromium@1.5.307: resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + encodeurl@2.0.0: + resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} + engines: {node: '>= 0.8'} + entities@6.0.1: resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} @@ -1147,13 +1244,44 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + etag@1.8.1: + resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} + engines: {node: '>= 0.6'} + + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} + + eventsource@3.0.7: + resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} + engines: {node: '>=18.0.0'} + expect-type@1.3.0: resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} engines: {node: '>=12.0.0'} + express-rate-limit@8.5.1: + resolution: {integrity: sha512-5O6KYmyJEpuPJV5hNTXKbAHWRqrzyu+OI3vUnSd2kXFubIVpG7ezpgxQy76Zo5GQZtrQBg86hF+CM/NX+cioiQ==} + engines: {node: '>= 16'} + peerDependencies: + express: '>= 4.11' + + express@5.2.1: + resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} + engines: {node: '>= 18'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -1163,6 +1291,10 @@ packages: picomatch: optional: true + finalhandler@2.1.1: + resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} + engines: {node: '>= 18.0.0'} + fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} @@ -1170,6 +1302,14 @@ packages: resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} engines: {node: '>= 6'} + forwarded@0.2.0: + resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} + engines: {node: '>= 0.6'} + + fresh@2.0.0: + resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} + engines: {node: '>= 0.8'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1206,10 +1346,18 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hono@4.12.18: + resolution: {integrity: sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==} + engines: {node: '>=16.9.0'} + html-encoding-sniffer@4.0.0: resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} engines: {node: '>=18'} + http-errors@2.0.1: + resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} + engines: {node: '>= 0.8'} + http-proxy-agent@7.0.2: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} @@ -1222,13 +1370,37 @@ packages: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: + resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} + engines: {node: '>=0.10.0'} + indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + + ipaddr.js@1.9.1: + resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} + engines: {node: '>= 0.10'} + is-potential-custom-element-name@1.0.1: resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-promise@4.0.0: + resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jose@6.2.3: + resolution: {integrity: sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -1250,6 +1422,12 @@ packages: engines: {node: '>=6'} hasBin: true + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-schema-typed@8.0.2: + resolution: {integrity: sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==} + json5@2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -1340,14 +1518,30 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + media-typer@1.1.0: + resolution: {integrity: sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==} + engines: {node: '>= 0.8'} + + merge-descriptors@2.0.0: + resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} + engines: {node: '>=18'} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} + mime-db@1.54.0: + resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} + engines: {node: '>= 0.6'} + mime-types@2.1.35: resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} engines: {node: '>= 0.6'} + mime-types@3.0.2: + resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} + engines: {node: '>=18'} + min-indent@1.0.1: resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} engines: {node: '>=4'} @@ -1366,6 +1560,10 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + negotiator@1.0.0: + resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} + engines: {node: '>= 0.6'} + node-releases@2.0.36: resolution: {integrity: sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==} @@ -1376,9 +1574,31 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + on-finished@2.4.1: + resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} + engines: {node: '>= 0.8'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + parse5@7.3.0: resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + parseurl@1.3.3: + resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} + engines: {node: '>= 0.8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-to-regexp@8.4.2: + resolution: {integrity: sha512-qRcuIdP69NPm4qbACK+aDogI5CBDMi1jKe0ry5rSQJz8JVLsC7jV8XpiJjGRLLol3N+R5ihGYcrPLTno6pAdBA==} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -1400,6 +1620,10 @@ packages: resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==} engines: {node: '>= 6'} + pkce-challenge@5.0.1: + resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} + engines: {node: '>=16.20.0'} + pkg-types@1.3.1: resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} @@ -1429,10 +1653,26 @@ packages: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + proxy-addr@2.0.7: + resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} + engines: {node: '>= 0.10'} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + + range-parser@1.2.1: + resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} + engines: {node: '>= 0.6'} + + raw-body@3.0.2: + resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==} + engines: {node: '>= 0.10'} + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -1457,6 +1697,10 @@ packages: resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} engines: {node: '>=8'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@5.0.0: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} @@ -1466,6 +1710,10 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + router@2.2.0: + resolution: {integrity: sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==} + engines: {node: '>= 18'} + rrweb-cssom@0.7.1: resolution: {integrity: sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==} @@ -1486,6 +1734,41 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true + send@1.2.1: + resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} + engines: {node: '>= 18'} + + serve-static@2.2.1: + resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} + engines: {node: '>= 18'} + + setprototypeof@1.2.0: + resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} @@ -1500,6 +1783,10 @@ packages: stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: + resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} + engines: {node: '>= 0.8'} + std-env@3.10.0: resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} @@ -1551,6 +1838,10 @@ packages: resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true + toidentifier@1.0.1: + resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} + engines: {node: '>=0.6'} + tough-cookie@5.1.2: resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} engines: {node: '>=16'} @@ -1619,6 +1910,10 @@ packages: resolution: {integrity: sha512-ERZf7pKOR155NKs/PZt1+83NrSEJfUL7+p9/TGZg/8xzDVMntXEFQlX4CsNJQTyu4h3j+dZYiQWOOlv5pssuHQ==} hasBin: true + type-is@2.0.1: + resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} + engines: {node: '>= 0.6'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1630,12 +1925,20 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + unpipe@1.0.0: + resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} + engines: {node: '>= 0.8'} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + vite-node@2.1.9: resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1758,11 +2061,19 @@ packages: resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} engines: {node: '>=18'} + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + why-is-node-running@2.3.0: resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} engines: {node: '>=8'} hasBin: true + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.19.0: resolution: {integrity: sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==} engines: {node: '>=10.0.0'} @@ -1785,6 +2096,14 @@ packages: yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + zod-to-json-schema@3.25.2: + resolution: {integrity: sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==} + peerDependencies: + zod: ^3.25.28 || ^4 + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + snapshots: '@adobe/css-tools@4.4.4': {} @@ -2191,6 +2510,10 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true + '@hono/node-server@1.19.14(hono@4.12.18)': + dependencies: + hono: 4.12.18 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2210,6 +2533,28 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@modelcontextprotocol/sdk@1.29.0(zod@3.25.76)': + dependencies: + '@hono/node-server': 1.19.14(hono@4.12.18) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + content-type: 1.0.5 + cors: 2.8.6 + cross-spawn: 7.0.6 + eventsource: 3.0.7 + eventsource-parser: 3.0.8 + express: 5.2.1 + express-rate-limit: 8.5.1(express@5.2.1) + hono: 4.12.18 + jose: 6.2.3 + json-schema-typed: 8.0.2 + pkce-challenge: 5.0.1 + raw-body: 3.0.2 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.59.0': @@ -2422,10 +2767,26 @@ snapshots: loupe: 3.2.1 tinyrainbow: 1.2.0 + accepts@2.0.0: + dependencies: + mime-types: 3.0.2 + negotiator: 1.0.0 + acorn@8.16.0: {} agent-base@7.1.4: {} + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-regex@5.0.1: {} ansi-styles@5.2.0: {} @@ -2444,6 +2805,20 @@ snapshots: baseline-browser-mapping@2.10.0: {} + body-parser@2.2.2: + dependencies: + bytes: 3.1.2 + content-type: 1.0.5 + debug: 4.4.3 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + on-finished: 2.4.1 + qs: 6.15.1 + raw-body: 3.0.2 + type-is: 2.0.1 + transitivePeerDependencies: + - supports-color + browserslist@4.28.1: dependencies: baseline-browser-mapping: 2.10.0 @@ -2457,6 +2832,8 @@ snapshots: esbuild: 0.27.3 load-tsconfig: 0.2.5 + bytes@3.1.2: {} + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -2464,6 +2841,11 @@ snapshots: es-errors: 1.3.0 function-bind: 1.1.2 + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + caniuse-lite@1.0.30001777: {} chai@5.3.3: @@ -2490,8 +2872,27 @@ snapshots: consola@3.4.2: {} + content-disposition@1.1.0: {} + + content-type@1.0.5: {} + convert-source-map@2.0.0: {} + cookie-signature@1.2.2: {} + + cookie@0.7.2: {} + + cors@2.8.6: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + css.escape@1.5.1: {} cssstyle@4.6.0: @@ -2516,6 +2917,8 @@ snapshots: delayed-stream@1.0.0: {} + depd@2.0.0: {} + dequal@2.0.3: {} dom-accessibility-api@0.5.16: {} @@ -2528,8 +2931,12 @@ snapshots: es-errors: 1.3.0 gopd: 1.2.0 + ee-first@1.1.1: {} + electron-to-chromium@1.5.307: {} + encodeurl@2.0.0: {} + entities@6.0.1: {} es-define-property@1.0.1: {} @@ -2635,16 +3042,79 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 + etag@1.8.1: {} + + eventsource-parser@3.0.8: {} + + eventsource@3.0.7: + dependencies: + eventsource-parser: 3.0.8 + expect-type@1.3.0: {} + express-rate-limit@8.5.1(express@5.2.1): + dependencies: + express: 5.2.1 + ip-address: 10.2.0 + + express@5.2.1: + dependencies: + accepts: 2.0.0 + body-parser: 2.2.2 + content-disposition: 1.1.0 + content-type: 1.0.5 + cookie: 0.7.2 + cookie-signature: 1.2.2 + debug: 4.4.3 + depd: 2.0.0 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + finalhandler: 2.1.1 + fresh: 2.0.0 + http-errors: 2.0.1 + merge-descriptors: 2.0.0 + mime-types: 3.0.2 + on-finished: 2.4.1 + once: 1.4.0 + parseurl: 1.3.3 + proxy-addr: 2.0.7 + qs: 6.15.1 + range-parser: 1.2.1 + router: 2.2.0 + send: 1.2.1 + serve-static: 2.2.1 + statuses: 2.0.2 + type-is: 2.0.1 + vary: 1.1.2 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.2: {} + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 + finalhandler@2.1.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + on-finished: 2.4.1 + parseurl: 1.3.3 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + fix-dts-default-cjs-exports@1.0.1: dependencies: magic-string: 0.30.21 @@ -2659,6 +3129,10 @@ snapshots: hasown: 2.0.2 mime-types: 2.1.35 + forwarded@0.2.0: {} + + fresh@2.0.0: {} + fsevents@2.3.3: optional: true @@ -2696,10 +3170,20 @@ snapshots: dependencies: function-bind: 1.1.2 + hono@4.12.18: {} + html-encoding-sniffer@4.0.0: dependencies: whatwg-encoding: 3.1.1 + http-errors@2.0.1: + dependencies: + depd: 2.0.0 + inherits: 2.0.4 + setprototypeof: 1.2.0 + statuses: 2.0.2 + toidentifier: 1.0.1 + http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.4 @@ -2718,10 +3202,26 @@ snapshots: dependencies: safer-buffer: 2.1.2 + iconv-lite@0.7.2: + dependencies: + safer-buffer: 2.1.2 + indent-string@4.0.0: {} + inherits@2.0.4: {} + + ip-address@10.2.0: {} + + ipaddr.js@1.9.1: {} + is-potential-custom-element-name@1.0.1: {} + is-promise@4.0.0: {} + + isexe@2.0.0: {} + + jose@6.2.3: {} + joycon@3.1.1: {} js-tokens@4.0.0: {} @@ -2756,6 +3256,10 @@ snapshots: jsesc@3.1.0: {} + json-schema-traverse@1.0.0: {} + + json-schema-typed@8.0.2: {} + json5@2.2.3: {} lefthook-darwin-arm64@2.1.6: @@ -2823,12 +3327,22 @@ snapshots: math-intrinsics@1.1.0: {} + media-typer@1.1.0: {} + + merge-descriptors@2.0.0: {} + mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 + mime-types@3.0.2: + dependencies: + mime-db: 1.54.0 + min-indent@1.0.1: {} mlly@1.8.1: @@ -2848,16 +3362,34 @@ snapshots: nanoid@3.3.11: {} + negotiator@1.0.0: {} + node-releases@2.0.36: {} nwsapi@2.2.23: {} object-assign@4.1.1: {} + object-inspect@1.13.4: {} + + on-finished@2.4.1: + dependencies: + ee-first: 1.1.1 + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + parse5@7.3.0: dependencies: entities: 6.0.1 + parseurl@1.3.3: {} + + path-key@3.1.1: {} + + path-to-regexp@8.4.2: {} + pathe@1.1.2: {} pathe@2.0.3: {} @@ -2870,6 +3402,8 @@ snapshots: pirates@4.0.7: {} + pkce-challenge@5.0.1: {} + pkg-types@1.3.1: dependencies: confbox: 0.1.8 @@ -2894,8 +3428,26 @@ snapshots: ansi-styles: 5.2.0 react-is: 17.0.2 + proxy-addr@2.0.7: + dependencies: + forwarded: 0.2.0 + ipaddr.js: 1.9.1 + punycode@2.3.1: {} + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + + range-parser@1.2.1: {} + + raw-body@3.0.2: + dependencies: + bytes: 3.1.2 + http-errors: 2.0.1 + iconv-lite: 0.7.2 + unpipe: 1.0.0 + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -2914,6 +3466,8 @@ snapshots: indent-string: 4.0.0 strip-indent: 3.0.0 + require-from-string@2.0.2: {} + resolve-from@5.0.0: {} rollup@4.59.0: @@ -2947,6 +3501,16 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.59.0 fsevents: 2.3.3 + router@2.2.0: + dependencies: + debug: 4.4.3 + depd: 2.0.0 + is-promise: 4.0.0 + parseurl: 1.3.3 + path-to-regexp: 8.4.2 + transitivePeerDependencies: + - supports-color + rrweb-cssom@0.7.1: {} rrweb-cssom@0.8.0: {} @@ -2961,6 +3525,67 @@ snapshots: semver@6.3.1: {} + send@1.2.1: + dependencies: + debug: 4.4.3 + encodeurl: 2.0.0 + escape-html: 1.0.3 + etag: 1.8.1 + fresh: 2.0.0 + http-errors: 2.0.1 + mime-types: 3.0.2 + ms: 2.1.3 + on-finished: 2.4.1 + range-parser: 1.2.1 + statuses: 2.0.2 + transitivePeerDependencies: + - supports-color + + serve-static@2.2.1: + dependencies: + encodeurl: 2.0.0 + escape-html: 1.0.3 + parseurl: 1.3.3 + send: 1.2.1 + transitivePeerDependencies: + - supports-color + + setprototypeof@1.2.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} source-map-js@1.2.1: {} @@ -2969,6 +3594,8 @@ snapshots: stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} strip-indent@3.0.0: @@ -3016,6 +3643,8 @@ snapshots: dependencies: tldts-core: 6.1.86 + toidentifier@1.0.1: {} + tough-cookie@5.1.2: dependencies: tldts: 6.1.86 @@ -3083,18 +3712,28 @@ snapshots: turbo-windows-64: 2.8.15 turbo-windows-arm64: 2.8.15 + type-is@2.0.1: + dependencies: + content-type: 1.0.5 + media-typer: 1.1.0 + mime-types: 3.0.2 + typescript@5.9.3: {} ufo@1.6.3: {} undici-types@6.21.0: {} + unpipe@1.0.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.1): dependencies: browserslist: 4.28.1 escalade: 3.2.0 picocolors: 1.1.1 + vary@1.1.2: {} + vite-node@2.1.9(@types/node@22.19.15): dependencies: cac: 6.7.14 @@ -3187,11 +3826,17 @@ snapshots: tr46: 5.1.1 webidl-conversions: 7.0.0 + which@2.0.2: + dependencies: + isexe: 2.0.0 + why-is-node-running@2.3.0: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + wrappy@1.0.2: {} + ws@8.19.0: {} xml-name-validator@5.0.0: {} @@ -3199,3 +3844,9 @@ snapshots: xmlchars@2.2.0: {} yallist@3.1.1: {} + + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + + zod@3.25.76: {}