mirror of
https://github.com/vatesfr/xen-orchestra.git
synced 2026-11-09 17:06:13 -06:00
feat(mcp): add @xen-orchestra/mcp package (#9519)
This PR adds the new @xen-orchestra/mcp package — an MCP (Model Context Protocol) server that enables AI assistants (Claude Desktop, Claude Code, or any MCP-compatible client) to query a Xen Orchestra infrastructure in read-only mode.
This commit is contained in:
1
@xen-orchestra/mcp/.npmignore
Symbolic link
1
@xen-orchestra/mcp/.npmignore
Symbolic link
@@ -0,0 +1 @@
|
||||
../../scripts/npmignore
|
||||
128
@xen-orchestra/mcp/README.md
Normal file
128
@xen-orchestra/mcp/README.md
Normal file
@@ -0,0 +1,128 @@
|
||||
# @xen-orchestra/mcp
|
||||
|
||||
[MCP](https://modelcontextprotocol.io/) (Model Context Protocol) server for [Xen Orchestra](https://xen-orchestra.com/). Enables AI assistants to query your XO infrastructure.
|
||||
|
||||
## Features
|
||||
|
||||
- **Infrastructure queries** — List and inspect pools, hosts, and VMs
|
||||
- **Dashboard** — Aggregated pool statistics (host status, top consumers, alarms)
|
||||
- **Documentation** — Search XO docs directly from the assistant
|
||||
- **Read-only** — Safe by design, no destructive operations
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Claude Desktop
|
||||
|
||||
Add to your Claude Desktop config (`~/.config/claude-desktop/config.json`):
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"xo": {
|
||||
"command": "npx",
|
||||
"args": ["@xen-orchestra/mcp"],
|
||||
"env": {
|
||||
"XO_URL": "https://your-xo-server",
|
||||
"XO_USERNAME": "admin@example.com",
|
||||
"XO_PASSWORD": "your-password"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Claude Code
|
||||
|
||||
```bash
|
||||
claude mcp add xo -- env XO_URL=https://your-xo-server XO_USERNAME=admin@example.com XO_PASSWORD=secret npx @xen-orchestra/mcp
|
||||
```
|
||||
|
||||
### Any MCP Client
|
||||
|
||||
```bash
|
||||
XO_URL=https://your-xo-server XO_USERNAME=admin XO_PASSWORD=secret npx @xen-orchestra/mcp
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
| Variable | Required | Description |
|
||||
|----------|----------|-------------|
|
||||
| `XO_URL` | Yes | Xen Orchestra server URL |
|
||||
| `XO_USERNAME` | Yes | XO username |
|
||||
| `XO_PASSWORD` | Yes | XO password |
|
||||
|
||||
## Available Tools
|
||||
|
||||
### `check_connection`
|
||||
|
||||
Test the connection to the XO server.
|
||||
|
||||
### `list_pools`
|
||||
|
||||
List all pools with optional field selection.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `fields` | string? | Comma-separated fields to return |
|
||||
|
||||
### `get_pool_dashboard`
|
||||
|
||||
Get aggregated dashboard for a pool (host status, top consumers, alarms).
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `pool_id` | string | The pool ID |
|
||||
|
||||
### `list_hosts`
|
||||
|
||||
List all hosts (hypervisors) with optional filtering.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `filter` | string? | Filter expression (e.g., `productBrand:XCP-ng`) |
|
||||
| `fields` | string? | Comma-separated fields to return |
|
||||
|
||||
### `list_vms`
|
||||
|
||||
List virtual machines with optional filtering and pagination.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `filter` | string? | Filter expression (e.g., `power_state:Running`) |
|
||||
| `fields` | string? | Comma-separated fields to return |
|
||||
| `limit` | number? | Maximum number of results |
|
||||
|
||||
### `get_vm_details`
|
||||
|
||||
Get full details about a specific VM.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `vm_id` | string | The VM ID or UUID |
|
||||
|
||||
### `get_infrastructure_summary`
|
||||
|
||||
Get a high-level summary of the entire infrastructure (pool count, host count, VM counts by state).
|
||||
|
||||
### `search_documentation`
|
||||
|
||||
Retrieve XO documentation by topic.
|
||||
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `topic` | enum | One of: `installation`, `configuration`, `backups`, `restapi`, `manage`, `users`, `architecture`, `troubleshooting`, `releases` |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
npm install
|
||||
npm run build
|
||||
npm test
|
||||
|
||||
# Run locally
|
||||
XO_URL=https://your-xo XO_USERNAME=admin XO_PASSWORD=secret npm start
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
AGPL-3.0-or-later
|
||||
25
@xen-orchestra/mcp/eslint.config.mjs
Normal file
25
@xen-orchestra/mcp/eslint.config.mjs
Normal file
@@ -0,0 +1,25 @@
|
||||
import eslint from '@eslint/js'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname } from 'node:path'
|
||||
|
||||
const __dirname = dirname(fileURLToPath(import.meta.url))
|
||||
|
||||
export default tseslint.config(
|
||||
eslint.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
{
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
},
|
||||
rules: {
|
||||
'no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||
},
|
||||
},
|
||||
{
|
||||
ignores: ['dist/'],
|
||||
}
|
||||
)
|
||||
56
@xen-orchestra/mcp/package.json
Normal file
56
@xen-orchestra/mcp/package.json
Normal file
@@ -0,0 +1,56 @@
|
||||
{
|
||||
"private": false,
|
||||
"name": "@xen-orchestra/mcp",
|
||||
"version": "0.0.0",
|
||||
"description": "MCP server for Xen Orchestra — allows AI assistants to query and manage XO infrastructure",
|
||||
"type": "module",
|
||||
"main": "./dist/index.mjs",
|
||||
"bin": {
|
||||
"xo-mcp": "./dist/index.mjs"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"clean": "rimraf dist/",
|
||||
"lint": "eslint src/",
|
||||
"postversion": "npm publish --access public",
|
||||
"prebuild": "npm run clean",
|
||||
"prepublishOnly": "npm run build",
|
||||
"start": "node dist/index.mjs",
|
||||
"test": "tsc && node --test dist/*.test.mjs"
|
||||
},
|
||||
"keywords": [
|
||||
"xen-orchestra",
|
||||
"xcp-ng",
|
||||
"mcp",
|
||||
"model-context-protocol",
|
||||
"ai",
|
||||
"virtualization"
|
||||
],
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.26.0",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.19.0",
|
||||
"@types/node": "^22.3.0",
|
||||
"@vates/types": "^1.19.0",
|
||||
"rimraf": "^6.0.0",
|
||||
"typescript": "~5.6.3",
|
||||
"typescript-eslint": "^8.23.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
},
|
||||
"homepage": "https://github.com/vatesfr/xen-orchestra/tree/master/@xen-orchestra/mcp",
|
||||
"bugs": "https://github.com/vatesfr/xen-orchestra/issues",
|
||||
"repository": {
|
||||
"directory": "@xen-orchestra/mcp",
|
||||
"type": "git",
|
||||
"url": "https://github.com/vatesfr/xen-orchestra.git"
|
||||
},
|
||||
"author": {
|
||||
"name": "Vates SAS",
|
||||
"url": "https://vates.fr"
|
||||
},
|
||||
"license": "AGPL-3.0-or-later"
|
||||
}
|
||||
457
@xen-orchestra/mcp/src/index.mts
Normal file
457
@xen-orchestra/mcp/src/index.mts
Normal file
@@ -0,0 +1,457 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* XO MCP Server
|
||||
*
|
||||
* Model Context Protocol server for Xen Orchestra.
|
||||
* Allows AI assistants to query XO infrastructure and documentation.
|
||||
*
|
||||
* Usage:
|
||||
* XO_URL=http://xo.local XO_USERNAME=admin XO_PASSWORD=*** node dist/index.mjs
|
||||
*/
|
||||
|
||||
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
||||
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
||||
import { z } from 'zod'
|
||||
import { realpathSync } from 'node:fs'
|
||||
import { XoClient } from './xo-client.mjs'
|
||||
|
||||
const DOC_FETCH_TIMEOUT_MS = 30_000
|
||||
|
||||
const XO_DOCS_BASE_URL = 'https://docs.xen-orchestra.com'
|
||||
|
||||
const DOC_TOPICS = [
|
||||
'installation',
|
||||
'configuration',
|
||||
'backups',
|
||||
'restapi',
|
||||
'manage',
|
||||
'users',
|
||||
'architecture',
|
||||
'troubleshooting',
|
||||
'releases',
|
||||
] as const
|
||||
|
||||
export interface EnvConfig {
|
||||
url: string
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export function validateEnv(): EnvConfig {
|
||||
const url = process.env.XO_URL
|
||||
const username = process.env.XO_USERNAME
|
||||
const password = process.env.XO_PASSWORD
|
||||
|
||||
const missing: string[] = []
|
||||
if (!url) missing.push('XO_URL')
|
||||
if (!username) missing.push('XO_USERNAME')
|
||||
if (!password) missing.push('XO_PASSWORD')
|
||||
|
||||
if (missing.length > 0) {
|
||||
throw new Error(
|
||||
`Missing required environment variables: ${missing.join(', ')}\n\n` +
|
||||
'Please set:\n' +
|
||||
' XO_URL - Xen Orchestra server URL (e.g., http://xo.local:9000)\n' +
|
||||
' XO_USERNAME - Admin username\n' +
|
||||
' XO_PASSWORD - Password\n\n' +
|
||||
'Note: XO currently requires username/password authentication.\n' +
|
||||
'Token authentication is also supported via xo-cli create-token.'
|
||||
)
|
||||
}
|
||||
|
||||
return { url: url!, username: username!, password: password! }
|
||||
}
|
||||
|
||||
export async function fetchDocumentation(path: string): Promise<string> {
|
||||
const url = `${XO_DOCS_BASE_URL}${path}`
|
||||
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
headers: { Accept: 'text/html' },
|
||||
signal: AbortSignal.timeout(DOC_FETCH_TIMEOUT_MS),
|
||||
})
|
||||
} catch (cause) {
|
||||
throw new Error(`Cannot reach documentation server: ${cause instanceof Error ? cause.message : 'Unknown error'}`, {
|
||||
cause,
|
||||
})
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch documentation: HTTP ${response.status} ${response.statusText}`)
|
||||
}
|
||||
|
||||
const html = await response.text()
|
||||
|
||||
let text = html
|
||||
.replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, '')
|
||||
.replace(/<header[^>]*>[\s\S]*?<\/header>/gi, '')
|
||||
.replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, '')
|
||||
|
||||
text = text
|
||||
.replace(/<h[1-6][^>]*>([\s\S]*?)<\/h[1-6]>/gi, '\n## $1\n')
|
||||
.replace(/<p[^>]*>([\s\S]*?)<\/p>/gi, '$1\n')
|
||||
.replace(/<li[^>]*>([\s\S]*?)<\/li>/gi, '- $1\n')
|
||||
.replace(/<br\s*\/?>/gi, '\n')
|
||||
.replace(/<[^>]+>/g, '')
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, "'")
|
||||
.replace(/—/g, '\u2014')
|
||||
.replace(/–/g, '\u2013')
|
||||
.replace(/…/g, '\u2026')
|
||||
.replace(/\n\s*\n\s*\n/g, '\n\n')
|
||||
.trim()
|
||||
|
||||
return text
|
||||
}
|
||||
|
||||
function formatToolError(error: unknown): string {
|
||||
return error instanceof Error ? error.message : 'Unknown error occurred'
|
||||
}
|
||||
|
||||
export function createServer(getClient: () => XoClient): McpServer {
|
||||
const server = new McpServer({
|
||||
name: 'xo-mcp-server',
|
||||
version: '1.0.0',
|
||||
})
|
||||
|
||||
// =============================================================================
|
||||
// TOOL: check_connection
|
||||
// =============================================================================
|
||||
server.registerTool(
|
||||
'check_connection',
|
||||
{
|
||||
title: 'Check Connection',
|
||||
description: 'Test the connection to the Xen Orchestra server. Use this to validate your setup.',
|
||||
inputSchema: {},
|
||||
},
|
||||
async () => {
|
||||
const client = getClient()
|
||||
const result = await client.testConnection()
|
||||
if (result.ok) {
|
||||
return {
|
||||
content: [{ type: 'text', text: 'Connection to XO server successful.' }],
|
||||
}
|
||||
}
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Connection test failed: ${result.error ?? 'Unknown error'}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// TOOL: list_pools
|
||||
// =============================================================================
|
||||
server.registerTool(
|
||||
'list_pools',
|
||||
{
|
||||
title: 'List Pools',
|
||||
description: 'List all pools in Xen Orchestra with their basic information',
|
||||
inputSchema: {
|
||||
fields: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Comma-separated fields to return (default: id,name_label,name_description,auto_poweron,HA_enabled)'
|
||||
),
|
||||
},
|
||||
},
|
||||
async ({ fields }) => {
|
||||
try {
|
||||
const client = getClient()
|
||||
const pools = await client.listPools(fields)
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(pools, null, 2) }],
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Failed to list pools: ${formatToolError(error)}` }],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// TOOL: get_pool_dashboard
|
||||
// =============================================================================
|
||||
server.registerTool(
|
||||
'get_pool_dashboard',
|
||||
{
|
||||
title: 'Get Pool Dashboard',
|
||||
description: 'Get aggregated dashboard for a pool including hosts status, top consumers, and alarms',
|
||||
inputSchema: {
|
||||
pool_id: z.string().describe('The pool ID'),
|
||||
},
|
||||
},
|
||||
async ({ pool_id }) => {
|
||||
try {
|
||||
const client = getClient()
|
||||
const dashboard = await client.getPoolDashboard(pool_id)
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(dashboard, null, 2) }],
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Failed to get pool dashboard: ${formatToolError(error)}` }],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// TOOL: list_hosts
|
||||
// =============================================================================
|
||||
server.registerTool(
|
||||
'list_hosts',
|
||||
{
|
||||
title: 'List Hosts',
|
||||
description: 'List all hosts (hypervisors) in Xen Orchestra',
|
||||
inputSchema: {
|
||||
filter: z.string().optional().describe('Filter expression (e.g., productBrand:XCP-ng)'),
|
||||
fields: z.string().optional().describe('Comma-separated fields to return'),
|
||||
},
|
||||
},
|
||||
async ({ filter, fields }) => {
|
||||
try {
|
||||
const client = getClient()
|
||||
const hosts = await client.listHosts({ filter, fields })
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(hosts, null, 2) }],
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Failed to list hosts: ${formatToolError(error)}` }],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// TOOL: list_vms
|
||||
// =============================================================================
|
||||
server.registerTool(
|
||||
'list_vms',
|
||||
{
|
||||
title: 'List Virtual Machines',
|
||||
description: 'List virtual machines in Xen Orchestra with optional filtering',
|
||||
inputSchema: {
|
||||
filter: z.string().optional().describe('Filter expression (e.g., power_state:Running, name_label:web*)'),
|
||||
fields: z.string().optional().describe('Comma-separated fields to return'),
|
||||
limit: z.number().optional().describe('Maximum number of VMs to return'),
|
||||
},
|
||||
},
|
||||
async ({ filter, fields, limit }) => {
|
||||
try {
|
||||
const client = getClient()
|
||||
const vms = await client.listVms({ filter, fields, limit })
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(vms, null, 2) }],
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Failed to list VMs: ${formatToolError(error)}` }],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// TOOL: get_vm_details
|
||||
// =============================================================================
|
||||
server.registerTool(
|
||||
'get_vm_details',
|
||||
{
|
||||
title: 'Get VM Details',
|
||||
description: 'Get detailed information about a specific virtual machine',
|
||||
inputSchema: {
|
||||
vm_id: z.string().describe('The VM ID or UUID'),
|
||||
},
|
||||
},
|
||||
async ({ vm_id }) => {
|
||||
try {
|
||||
const client = getClient()
|
||||
const vm = await client.getVm(vm_id)
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(vm, null, 2) }],
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [{ type: 'text', text: `Failed to get VM details: ${formatToolError(error)}` }],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// TOOL: search_documentation
|
||||
// =============================================================================
|
||||
server.registerTool(
|
||||
'search_documentation',
|
||||
{
|
||||
title: 'Search XO Documentation',
|
||||
description:
|
||||
'Search and retrieve Xen Orchestra documentation. Use this to help users understand XO features, configuration, and best practices.',
|
||||
inputSchema: {
|
||||
topic: z.enum(DOC_TOPICS).describe('Documentation topic to retrieve'),
|
||||
},
|
||||
},
|
||||
async ({ topic }) => {
|
||||
const path = `/${topic}`
|
||||
|
||||
try {
|
||||
const content = await fetchDocumentation(path)
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `# XO Documentation: ${topic}\n\nSource: ${XO_DOCS_BASE_URL}${path}\n\n${content}`,
|
||||
},
|
||||
],
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Failed to fetch documentation for "${topic}": ${formatToolError(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// TOOL: get_infrastructure_summary
|
||||
// =============================================================================
|
||||
server.registerTool(
|
||||
'get_infrastructure_summary',
|
||||
{
|
||||
title: 'Get Infrastructure Summary',
|
||||
description: 'Get a high-level summary of the entire XO infrastructure (pools, hosts, VMs counts)',
|
||||
inputSchema: {},
|
||||
},
|
||||
async () => {
|
||||
try {
|
||||
const client = getClient()
|
||||
|
||||
const [pools, hosts, vms] = await Promise.all([
|
||||
client.listPools('id,name_label'),
|
||||
client.listHosts({ fields: 'id,name_label,power_state' }),
|
||||
client.listVms({ fields: 'id,power_state' }),
|
||||
])
|
||||
|
||||
const runningVms = vms.filter(vm => vm.power_state === 'Running').length
|
||||
const haltedVms = vms.filter(vm => vm.power_state === 'Halted').length
|
||||
|
||||
const summary = {
|
||||
pools: {
|
||||
total: pools.length,
|
||||
names: pools.map(p => p.name_label),
|
||||
},
|
||||
hosts: {
|
||||
total: hosts.length,
|
||||
},
|
||||
vms: {
|
||||
total: vms.length,
|
||||
running: runningVms,
|
||||
halted: haltedVms,
|
||||
other: vms.length - runningVms - haltedVms,
|
||||
},
|
||||
}
|
||||
|
||||
return {
|
||||
content: [{ type: 'text', text: JSON.stringify(summary, null, 2) }],
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `Failed to get infrastructure summary: ${formatToolError(error)}`,
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// PROMPT: infrastructure-overview
|
||||
// =============================================================================
|
||||
server.prompt(
|
||||
'infrastructure-overview',
|
||||
'Generate a natural language overview of the XO infrastructure',
|
||||
async () => {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: 'user',
|
||||
content: {
|
||||
type: 'text',
|
||||
text: `Please use the get_infrastructure_summary tool to fetch the current state of the Xen Orchestra infrastructure, then provide a clear, human-readable summary of:
|
||||
1. How many pools are configured and their names
|
||||
2. Total number of hosts
|
||||
3. VM statistics (total, running, halted)
|
||||
|
||||
Present this information in a friendly, conversational way.`,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return server
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Start server
|
||||
// =============================================================================
|
||||
async function main() {
|
||||
const env = validateEnv()
|
||||
|
||||
let xoClient: XoClient | null = null
|
||||
function getClient(): XoClient {
|
||||
if (!xoClient) {
|
||||
xoClient = new XoClient({ url: env.url, username: env.username, password: env.password })
|
||||
}
|
||||
return xoClient
|
||||
}
|
||||
|
||||
const server = createServer(getClient)
|
||||
|
||||
const transport = new StdioServerTransport()
|
||||
await server.connect(transport)
|
||||
}
|
||||
|
||||
// Only run main when executed directly (not when imported for testing)
|
||||
// realpathSync handles the case where the binary is invoked via a symlink (e.g., npx bin)
|
||||
import { fileURLToPath } from 'node:url'
|
||||
if (realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url))) {
|
||||
// console.error is used intentionally instead of @xen-orchestra/log because
|
||||
// MCP servers communicate via stdout (JSON-RPC) — only stderr is safe for logging.
|
||||
main().catch(error => {
|
||||
console.error('Fatal error:', error instanceof Error ? error.message : error)
|
||||
process.exit(1)
|
||||
})
|
||||
}
|
||||
419
@xen-orchestra/mcp/src/server.test.mts
Normal file
419
@xen-orchestra/mcp/src/server.test.mts
Normal file
@@ -0,0 +1,419 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it, beforeEach, afterEach } from 'node:test'
|
||||
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
||||
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
||||
import { createServer, validateEnv, fetchDocumentation } from './index.mjs'
|
||||
import type { XoClient } from './xo-client.mjs'
|
||||
|
||||
// Helper to create a mock XoClient
|
||||
// Uses `as unknown as XoClient` because @vates/types uses branded string IDs
|
||||
// that plain string literals don't satisfy at the type level.
|
||||
function createMockClient(overrides: Record<string, unknown> = {}): XoClient {
|
||||
return {
|
||||
testConnection: async () => ({ ok: true }),
|
||||
listPools: async () => [{ id: 'pool1', name_label: 'Pool 1' }],
|
||||
listHosts: async () => [{ id: 'host1', name_label: 'Host 1' }],
|
||||
listVms: async () => [
|
||||
{ id: 'vm1', name_label: 'VM 1', power_state: 'Running' },
|
||||
{ id: 'vm2', name_label: 'VM 2', power_state: 'Halted' },
|
||||
],
|
||||
getVm: async () => ({ id: 'vm1', name_label: 'VM 1', power_state: 'Running' }),
|
||||
getPool: async () => ({ id: 'pool1', name_label: 'Pool 1' }),
|
||||
getPoolDashboard: async () => ({ hostsByStatus: { running: 1 } }),
|
||||
getHost: async () => ({ id: 'host1', name_label: 'Host 1' }),
|
||||
getVmStats: async () => ({ endTimestamp: 0, interval: 0, stats: {} }),
|
||||
...overrides,
|
||||
} as unknown as XoClient
|
||||
}
|
||||
|
||||
// Helper to set up client + server connected via InMemoryTransport
|
||||
async function setupTestServer(mockClient?: XoClient) {
|
||||
const client = mockClient ?? createMockClient()
|
||||
const server = createServer(() => client)
|
||||
const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair()
|
||||
const mcpClient = new Client({ name: 'test-client', version: '1.0.0' })
|
||||
|
||||
await Promise.all([server.connect(serverTransport), mcpClient.connect(clientTransport)])
|
||||
|
||||
return { mcpClient, server }
|
||||
}
|
||||
|
||||
describe('createServer', () => {
|
||||
describe('tool listing', () => {
|
||||
it('registers all 8 tools', async () => {
|
||||
const { mcpClient } = await setupTestServer()
|
||||
const { tools } = await mcpClient.listTools()
|
||||
const toolNames = tools.map((t) => t.name).sort()
|
||||
|
||||
assert.deepStrictEqual(toolNames, [
|
||||
'check_connection',
|
||||
'get_infrastructure_summary',
|
||||
'get_pool_dashboard',
|
||||
'get_vm_details',
|
||||
'list_hosts',
|
||||
'list_pools',
|
||||
'list_vms',
|
||||
'search_documentation',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('check_connection tool', () => {
|
||||
it('returns success message on good connection', async () => {
|
||||
const { mcpClient } = await setupTestServer()
|
||||
const result = await mcpClient.callTool({ name: 'check_connection', arguments: {} })
|
||||
const text = (result.content as Array<{ type: string; text: string }>)[0].text
|
||||
assert.ok(text.includes('successful'))
|
||||
})
|
||||
|
||||
it('returns error message on failed connection', async () => {
|
||||
const mockClient = createMockClient({
|
||||
testConnection: async () => ({ ok: false, error: 'Cannot connect to XO server' }),
|
||||
})
|
||||
const { mcpClient } = await setupTestServer(mockClient)
|
||||
const result = await mcpClient.callTool({ name: 'check_connection', arguments: {} })
|
||||
const text = (result.content as Array<{ type: string; text: string }>)[0].text
|
||||
assert.ok(text.includes('failed'))
|
||||
assert.ok(text.includes('Cannot connect'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('list_pools tool', () => {
|
||||
it('returns pools as JSON', async () => {
|
||||
const { mcpClient } = await setupTestServer()
|
||||
const result = await mcpClient.callTool({ name: 'list_pools', arguments: {} })
|
||||
const text = (result.content as Array<{ type: string; text: string }>)[0].text
|
||||
const parsed = JSON.parse(text)
|
||||
assert.strictEqual(parsed[0].id, 'pool1')
|
||||
})
|
||||
|
||||
it('passes fields parameter', async () => {
|
||||
let receivedFields: string | undefined
|
||||
const mockClient = createMockClient({
|
||||
listPools: async (fields?: string) => {
|
||||
receivedFields = fields
|
||||
return [{ id: 'pool1', name_label: 'Pool 1' }]
|
||||
},
|
||||
})
|
||||
const { mcpClient } = await setupTestServer(mockClient)
|
||||
await mcpClient.callTool({ name: 'list_pools', arguments: { fields: 'id,name_label' } })
|
||||
assert.strictEqual(receivedFields, 'id,name_label')
|
||||
})
|
||||
|
||||
it('returns error on failure', async () => {
|
||||
const mockClient = createMockClient({
|
||||
listPools: async () => {
|
||||
throw new Error('Connection refused')
|
||||
},
|
||||
})
|
||||
const { mcpClient } = await setupTestServer(mockClient)
|
||||
const result = await mcpClient.callTool({ name: 'list_pools', arguments: {} })
|
||||
const text = (result.content as Array<{ type: string; text: string }>)[0].text
|
||||
assert.ok(text.includes('Failed to list pools'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('list_vms tool', () => {
|
||||
it('returns VMs as JSON', async () => {
|
||||
const { mcpClient } = await setupTestServer()
|
||||
const result = await mcpClient.callTool({ name: 'list_vms', arguments: {} })
|
||||
const text = (result.content as Array<{ type: string; text: string }>)[0].text
|
||||
const parsed = JSON.parse(text)
|
||||
assert.strictEqual(parsed.length, 2)
|
||||
})
|
||||
|
||||
it('passes filter, fields, and limit', async () => {
|
||||
let receivedArgs: { filter?: string; fields?: string; limit?: number } = {}
|
||||
const mockClient = createMockClient({
|
||||
listVms: async (options?: { filter?: string; fields?: string; limit?: number }) => {
|
||||
receivedArgs = { filter: options?.filter, fields: options?.fields, limit: options?.limit }
|
||||
return []
|
||||
},
|
||||
})
|
||||
const { mcpClient } = await setupTestServer(mockClient)
|
||||
await mcpClient.callTool({
|
||||
name: 'list_vms',
|
||||
arguments: { filter: 'power_state:Running', fields: 'id', limit: 5 },
|
||||
})
|
||||
assert.strictEqual(receivedArgs.filter, 'power_state:Running')
|
||||
assert.strictEqual(receivedArgs.fields, 'id')
|
||||
assert.strictEqual(receivedArgs.limit, 5)
|
||||
})
|
||||
})
|
||||
|
||||
describe('get_vm_details tool', () => {
|
||||
it('returns VM details', async () => {
|
||||
const { mcpClient } = await setupTestServer()
|
||||
const result = await mcpClient.callTool({
|
||||
name: 'get_vm_details',
|
||||
arguments: { vm_id: 'vm1' },
|
||||
})
|
||||
const text = (result.content as Array<{ type: string; text: string }>)[0].text
|
||||
const parsed = JSON.parse(text)
|
||||
assert.strictEqual(parsed.id, 'vm1')
|
||||
})
|
||||
|
||||
it('returns error when VM not found', async () => {
|
||||
const mockClient = createMockClient({
|
||||
getVm: async () => {
|
||||
throw new Error('XO API error (404 Not Found): Not found')
|
||||
},
|
||||
})
|
||||
const { mcpClient } = await setupTestServer(mockClient)
|
||||
const result = await mcpClient.callTool({
|
||||
name: 'get_vm_details',
|
||||
arguments: { vm_id: 'nonexistent' },
|
||||
})
|
||||
const text = (result.content as Array<{ type: string; text: string }>)[0].text
|
||||
assert.ok(text.includes('Failed to get VM details'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('get_infrastructure_summary tool', () => {
|
||||
it('returns aggregated summary', async () => {
|
||||
const { mcpClient } = await setupTestServer()
|
||||
const result = await mcpClient.callTool({
|
||||
name: 'get_infrastructure_summary',
|
||||
arguments: {},
|
||||
})
|
||||
const text = (result.content as Array<{ type: string; text: string }>)[0].text
|
||||
const parsed = JSON.parse(text)
|
||||
assert.strictEqual(parsed.pools.total, 1)
|
||||
assert.strictEqual(parsed.hosts.total, 1)
|
||||
assert.strictEqual(parsed.vms.total, 2)
|
||||
assert.strictEqual(parsed.vms.running, 1)
|
||||
assert.strictEqual(parsed.vms.halted, 1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('get_pool_dashboard tool', () => {
|
||||
it('returns dashboard data', async () => {
|
||||
const { mcpClient } = await setupTestServer()
|
||||
const result = await mcpClient.callTool({
|
||||
name: 'get_pool_dashboard',
|
||||
arguments: { pool_id: 'pool1' },
|
||||
})
|
||||
const text = (result.content as Array<{ type: string; text: string }>)[0].text
|
||||
const parsed = JSON.parse(text)
|
||||
assert.deepStrictEqual(parsed.hostsByStatus, { running: 1 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('list_hosts tool', () => {
|
||||
it('returns hosts as JSON', async () => {
|
||||
const { mcpClient } = await setupTestServer()
|
||||
const result = await mcpClient.callTool({ name: 'list_hosts', arguments: {} })
|
||||
const text = (result.content as Array<{ type: string; text: string }>)[0].text
|
||||
const parsed = JSON.parse(text)
|
||||
assert.strictEqual(parsed[0].id, 'host1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('search_documentation tool', () => {
|
||||
let originalFetch: typeof globalThis.fetch
|
||||
|
||||
beforeEach(() => {
|
||||
originalFetch = globalThis.fetch
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
|
||||
it('fetches and returns documentation', async () => {
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === 'string' ? input : input.toString()
|
||||
if (url.includes('docs.xen-orchestra.com')) {
|
||||
return new Response('<h1>Installation Guide</h1><p>Install XO here.</p>', {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'text/html' },
|
||||
})
|
||||
}
|
||||
return originalFetch(input)
|
||||
}
|
||||
|
||||
const { mcpClient } = await setupTestServer()
|
||||
const result = await mcpClient.callTool({
|
||||
name: 'search_documentation',
|
||||
arguments: { topic: 'installation' },
|
||||
})
|
||||
const text = (result.content as Array<{ type: string; text: string }>)[0].text
|
||||
assert.ok(text.includes('Installation Guide'))
|
||||
assert.ok(text.includes('Install XO here'))
|
||||
})
|
||||
|
||||
it('returns error when documentation fetch fails', async () => {
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === 'string' ? input : input.toString()
|
||||
if (url.includes('docs.xen-orchestra.com')) {
|
||||
return new Response('Not Found', { status: 404, statusText: 'Not Found' })
|
||||
}
|
||||
return originalFetch(input)
|
||||
}
|
||||
|
||||
const { mcpClient } = await setupTestServer()
|
||||
const result = await mcpClient.callTool({
|
||||
name: 'search_documentation',
|
||||
arguments: { topic: 'installation' },
|
||||
})
|
||||
const text = (result.content as Array<{ type: string; text: string }>)[0].text
|
||||
assert.ok(text.includes('Failed to fetch documentation'))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateEnv', () => {
|
||||
const originalEnv = { ...process.env }
|
||||
|
||||
afterEach(() => {
|
||||
process.env = { ...originalEnv }
|
||||
})
|
||||
|
||||
it('returns config when all env vars are set', () => {
|
||||
process.env.XO_URL = 'http://xo.local:9000'
|
||||
process.env.XO_USERNAME = 'admin'
|
||||
process.env.XO_PASSWORD = 'pass'
|
||||
|
||||
const config = validateEnv()
|
||||
assert.strictEqual(config.url, 'http://xo.local:9000')
|
||||
assert.strictEqual(config.username, 'admin')
|
||||
assert.strictEqual(config.password, 'pass')
|
||||
})
|
||||
|
||||
it('throws when XO_URL is missing', () => {
|
||||
delete process.env.XO_URL
|
||||
process.env.XO_USERNAME = 'admin'
|
||||
process.env.XO_PASSWORD = 'pass'
|
||||
|
||||
assert.throws(() => validateEnv(), { message: /XO_URL/ })
|
||||
})
|
||||
|
||||
it('throws when XO_USERNAME is missing', () => {
|
||||
process.env.XO_URL = 'http://xo.local:9000'
|
||||
delete process.env.XO_USERNAME
|
||||
process.env.XO_PASSWORD = 'pass'
|
||||
|
||||
assert.throws(() => validateEnv(), { message: /XO_USERNAME/ })
|
||||
})
|
||||
|
||||
it('throws when XO_PASSWORD is missing', () => {
|
||||
process.env.XO_URL = 'http://xo.local:9000'
|
||||
process.env.XO_USERNAME = 'admin'
|
||||
delete process.env.XO_PASSWORD
|
||||
|
||||
assert.throws(() => validateEnv(), { message: /XO_PASSWORD/ })
|
||||
})
|
||||
|
||||
it('lists all missing vars in error message', () => {
|
||||
delete process.env.XO_URL
|
||||
delete process.env.XO_USERNAME
|
||||
delete process.env.XO_PASSWORD
|
||||
|
||||
assert.throws(() => validateEnv(), (error: Error) => {
|
||||
assert.ok(error.message.includes('XO_URL'))
|
||||
assert.ok(error.message.includes('XO_USERNAME'))
|
||||
assert.ok(error.message.includes('XO_PASSWORD'))
|
||||
return true
|
||||
})
|
||||
})
|
||||
|
||||
it('includes help text in error message', () => {
|
||||
delete process.env.XO_URL
|
||||
process.env.XO_USERNAME = 'admin'
|
||||
process.env.XO_PASSWORD = 'pass'
|
||||
|
||||
assert.throws(() => validateEnv(), (error: Error) => {
|
||||
assert.ok(error.message.includes('Please set'))
|
||||
assert.ok(error.message.includes('XO_URL'))
|
||||
return true
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('fetchDocumentation', () => {
|
||||
let originalFetch: typeof globalThis.fetch
|
||||
|
||||
beforeEach(() => {
|
||||
originalFetch = globalThis.fetch
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
|
||||
it('strips HTML and returns clean text', async () => {
|
||||
globalThis.fetch = async () => {
|
||||
return new Response(
|
||||
'<html><body><h1>Title</h1><p>Content here.</p><script>evil()</script></body></html>',
|
||||
{ status: 200, headers: { 'content-type': 'text/html' } }
|
||||
)
|
||||
}
|
||||
|
||||
const text = await fetchDocumentation('/test')
|
||||
assert.ok(text.includes('## Title'))
|
||||
assert.ok(text.includes('Content here.'))
|
||||
assert.ok(!text.includes('evil()'))
|
||||
assert.ok(!text.includes('<script>'))
|
||||
})
|
||||
|
||||
it('decodes HTML entities', async () => {
|
||||
globalThis.fetch = async () => {
|
||||
return new Response('<p>A & B < C > D</p>', {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'text/html' },
|
||||
})
|
||||
}
|
||||
|
||||
const text = await fetchDocumentation('/test')
|
||||
assert.ok(text.includes('A & B < C > D'))
|
||||
})
|
||||
|
||||
it('decodes additional HTML entities', async () => {
|
||||
globalThis.fetch = async () => {
|
||||
return new Response('<p>"quoted" & it's — a – test…</p>', {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'text/html' },
|
||||
})
|
||||
}
|
||||
|
||||
const text = await fetchDocumentation('/test')
|
||||
assert.ok(text.includes('"quoted"'))
|
||||
assert.ok(text.includes("it's"))
|
||||
assert.ok(text.includes('\u2014'))
|
||||
assert.ok(text.includes('\u2013'))
|
||||
assert.ok(text.includes('\u2026'))
|
||||
})
|
||||
|
||||
it('handles multiline HTML tags', async () => {
|
||||
globalThis.fetch = async () => {
|
||||
return new Response('<h1>Title\nwith newline</h1><p>Para\nwith newline</p>', {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'text/html' },
|
||||
})
|
||||
}
|
||||
|
||||
const text = await fetchDocumentation('/test')
|
||||
assert.ok(text.includes('Title'))
|
||||
assert.ok(text.includes('with newline'))
|
||||
})
|
||||
|
||||
it('throws on HTTP error', async () => {
|
||||
globalThis.fetch = async () => {
|
||||
return new Response('Not Found', { status: 404, statusText: 'Not Found' })
|
||||
}
|
||||
|
||||
await assert.rejects(() => fetchDocumentation('/test'), {
|
||||
message: /404/,
|
||||
})
|
||||
})
|
||||
|
||||
it('throws on network error', async () => {
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error('Network failure')
|
||||
}
|
||||
|
||||
await assert.rejects(() => fetchDocumentation('/test'), {
|
||||
message: /Cannot reach documentation server/,
|
||||
})
|
||||
})
|
||||
})
|
||||
157
@xen-orchestra/mcp/src/xo-client.mts
Normal file
157
@xen-orchestra/mcp/src/xo-client.mts
Normal file
@@ -0,0 +1,157 @@
|
||||
/**
|
||||
* XO REST API Client
|
||||
*
|
||||
* Minimal client using native fetch to interact with Xen Orchestra REST API.
|
||||
* Authentication is done via Basic Auth or token cookie.
|
||||
*/
|
||||
|
||||
import type { XoPool, XoHost, XoVm } from '@vates/types/xo'
|
||||
import type { XapiVmStats, XapiStatsGranularity } from '@vates/types/common'
|
||||
|
||||
export type { XoPool, XoHost, XoVm, XapiVmStats, XapiStatsGranularity }
|
||||
|
||||
const REQUEST_TIMEOUT_MS = 30_000
|
||||
|
||||
export interface XoClientConfig {
|
||||
url: string
|
||||
username: string
|
||||
password: string
|
||||
}
|
||||
|
||||
export interface XoPoolDashboard {
|
||||
hostsByStatus?: Record<string, number>
|
||||
vmsByStatus?: Record<string, number>
|
||||
topHostsByRam?: Array<{ id: string; name: string; value: number }>
|
||||
topHostsByCpu?: Array<{ id: string; name: string; value: number }>
|
||||
alarms?: Array<{ id: string; name: string; time: number }>
|
||||
}
|
||||
|
||||
export class XoClient {
|
||||
private readonly baseUrl: string
|
||||
private readonly authHeader: string
|
||||
|
||||
constructor(config: XoClientConfig) {
|
||||
this.baseUrl = config.url.replace(/\/$/, '')
|
||||
|
||||
const credentials = Buffer.from(`${config.username}:${config.password}`).toString('base64')
|
||||
this.authHeader = `Basic ${credentials}`
|
||||
}
|
||||
|
||||
private async request<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
const url = `${this.baseUrl}/rest/v0${endpoint}`
|
||||
|
||||
let response: Response
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: this.authHeader,
|
||||
Accept: 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
signal: options.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
||||
})
|
||||
} catch (cause) {
|
||||
const message = cause instanceof Error ? cause.message : String(cause)
|
||||
if (message.includes('ECONNREFUSED') || message.includes('fetch failed')) {
|
||||
throw new Error(
|
||||
`Cannot connect to XO server at ${this.baseUrl}. Verify the server is running and XO_URL is correct.`,
|
||||
{ cause }
|
||||
)
|
||||
}
|
||||
if (message.includes('TimeoutError') || message.includes('abort')) {
|
||||
throw new Error(`Request to XO server timed out after ${REQUEST_TIMEOUT_MS}ms.`, { cause })
|
||||
}
|
||||
throw new Error(`Network error connecting to XO server: ${message}`, { cause })
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
throw new Error('Authentication failed: check XO_USERNAME and XO_PASSWORD.')
|
||||
}
|
||||
const errorText = await response.text().catch(() => response.statusText)
|
||||
throw new Error(`XO API error (${response.status} ${response.statusText}): ${errorText}`)
|
||||
}
|
||||
|
||||
try {
|
||||
return (await response.json()) as T
|
||||
} catch (cause) {
|
||||
throw new Error(`XO API returned invalid JSON for ${endpoint}`, { cause })
|
||||
}
|
||||
}
|
||||
|
||||
async testConnection(): Promise<{ ok: boolean; error?: string }> {
|
||||
try {
|
||||
await this.request('/pools?limit=1')
|
||||
return { ok: true }
|
||||
} catch (error) {
|
||||
return { ok: false, error: error instanceof Error ? error.message : 'Unknown error' }
|
||||
}
|
||||
}
|
||||
|
||||
async listPools(fields?: string): Promise<Partial<XoPool>[]> {
|
||||
const params = new URLSearchParams()
|
||||
params.set('fields', fields ?? 'id,name_label,name_description,auto_poweron,HA_enabled')
|
||||
|
||||
return this.request<Partial<XoPool>[]>(`/pools?${params}`)
|
||||
}
|
||||
|
||||
async getPool(poolId: string): Promise<XoPool> {
|
||||
return this.request<XoPool>(`/pools/${encodeURIComponent(poolId)}`)
|
||||
}
|
||||
|
||||
async getPoolDashboard(poolId: string): Promise<XoPoolDashboard> {
|
||||
return this.request<XoPoolDashboard>(
|
||||
`/pools/${encodeURIComponent(poolId)}/dashboard?ndjson=false`
|
||||
)
|
||||
}
|
||||
|
||||
async listHosts(options?: { filter?: string; fields?: string }): Promise<Partial<XoHost>[]> {
|
||||
const params = new URLSearchParams()
|
||||
params.set(
|
||||
'fields',
|
||||
options?.fields ?? 'id,name_label,productBrand,version,power_state'
|
||||
)
|
||||
if (options?.filter) {
|
||||
params.set('filter', options.filter)
|
||||
}
|
||||
|
||||
return this.request<Partial<XoHost>[]>(`/hosts?${params}`)
|
||||
}
|
||||
|
||||
async getHost(hostId: string): Promise<XoHost> {
|
||||
return this.request<XoHost>(`/hosts/${encodeURIComponent(hostId)}`)
|
||||
}
|
||||
|
||||
async listVms(options?: {
|
||||
filter?: string
|
||||
fields?: string
|
||||
limit?: number
|
||||
}): Promise<Partial<XoVm>[]> {
|
||||
const params = new URLSearchParams()
|
||||
params.set('fields', options?.fields ?? 'id,name_label,power_state,CPUs,memory')
|
||||
if (options?.filter) {
|
||||
params.set('filter', options.filter)
|
||||
}
|
||||
if (options?.limit !== undefined) {
|
||||
params.set('limit', String(options.limit))
|
||||
}
|
||||
|
||||
return this.request<Partial<XoVm>[]>(`/vms?${params}`)
|
||||
}
|
||||
|
||||
async getVm(vmId: string): Promise<XoVm> {
|
||||
return this.request<XoVm>(`/vms/${encodeURIComponent(vmId)}`)
|
||||
}
|
||||
|
||||
async getVmStats(
|
||||
vmId: string,
|
||||
granularity: XapiStatsGranularity = 'hours'
|
||||
): Promise<XapiVmStats> {
|
||||
const params = new URLSearchParams()
|
||||
params.set('granularity', granularity)
|
||||
return this.request<XapiVmStats>(
|
||||
`/vms/${encodeURIComponent(vmId)}/stats?${params}`
|
||||
)
|
||||
}
|
||||
}
|
||||
305
@xen-orchestra/mcp/src/xo-client.test.mts
Normal file
305
@xen-orchestra/mcp/src/xo-client.test.mts
Normal file
@@ -0,0 +1,305 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { describe, it, beforeEach, afterEach } from 'node:test'
|
||||
import { XoClient } from './xo-client.mjs'
|
||||
|
||||
// Helper to create a mock Response
|
||||
function mockResponse(body: unknown, init: ResponseInit = {}): Response {
|
||||
return new Response(JSON.stringify(body), {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'application/json' },
|
||||
...init,
|
||||
})
|
||||
}
|
||||
|
||||
describe('XoClient', () => {
|
||||
let originalFetch: typeof globalThis.fetch
|
||||
|
||||
beforeEach(() => {
|
||||
originalFetch = globalThis.fetch
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch
|
||||
})
|
||||
|
||||
describe('constructor', () => {
|
||||
it('strips trailing slash from URL', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000/', username: 'admin', password: 'pass' })
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === 'string' ? input : input.toString()
|
||||
assert.ok(!url.includes('9000//'), 'URL should not have double slashes')
|
||||
assert.ok(url.startsWith('http://xo.local:9000/rest/v0/'), 'URL should start with base URL + /rest/v0/')
|
||||
return mockResponse([])
|
||||
}
|
||||
await client.listPools()
|
||||
})
|
||||
|
||||
it('creates correct Basic Auth header', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'secret' })
|
||||
const expected = `Basic ${Buffer.from('admin:secret').toString('base64')}`
|
||||
|
||||
globalThis.fetch = async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const headers = init?.headers as Record<string, string>
|
||||
assert.strictEqual(headers?.Authorization, expected)
|
||||
return mockResponse([])
|
||||
}
|
||||
await client.listPools()
|
||||
})
|
||||
})
|
||||
|
||||
describe('request', () => {
|
||||
it('prepends /rest/v0 to endpoints', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === 'string' ? input : input.toString()
|
||||
assert.ok(url.includes('/rest/v0/pools'), `URL should contain /rest/v0/pools, got: ${url}`)
|
||||
return mockResponse([])
|
||||
}
|
||||
await client.listPools()
|
||||
})
|
||||
|
||||
it('sends Accept: application/json header', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const headers = init?.headers as Record<string, string>
|
||||
assert.strictEqual(headers?.Accept, 'application/json')
|
||||
return mockResponse([])
|
||||
}
|
||||
await client.listPools()
|
||||
})
|
||||
|
||||
it('throws descriptive error on connection refused', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
throw new TypeError('fetch failed: ECONNREFUSED')
|
||||
}
|
||||
await assert.rejects(() => client.listPools(), {
|
||||
message: /Cannot connect to XO server/,
|
||||
})
|
||||
})
|
||||
|
||||
it('preserves original error as cause', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
const originalError = new TypeError('fetch failed: ECONNREFUSED')
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
throw originalError
|
||||
}
|
||||
try {
|
||||
await client.listPools()
|
||||
assert.fail('Should have thrown')
|
||||
} catch (error) {
|
||||
assert.ok(error instanceof Error)
|
||||
assert.strictEqual(error.cause, originalError)
|
||||
}
|
||||
})
|
||||
|
||||
it('throws descriptive error on 401', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'wrong' })
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
return new Response('Unauthorized', { status: 401, statusText: 'Unauthorized' })
|
||||
}
|
||||
await assert.rejects(() => client.listPools(), {
|
||||
message: /Authentication failed/,
|
||||
})
|
||||
})
|
||||
|
||||
it('throws error with status code on other HTTP errors', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
return new Response('Not Found', { status: 404, statusText: 'Not Found' })
|
||||
}
|
||||
await assert.rejects(() => client.listPools(), {
|
||||
message: /404/,
|
||||
})
|
||||
})
|
||||
|
||||
it('throws network error with message', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error('DNS resolution failed')
|
||||
}
|
||||
await assert.rejects(() => client.listPools(), {
|
||||
message: /Network error.*DNS resolution failed/,
|
||||
})
|
||||
})
|
||||
|
||||
it('throws on invalid JSON response with cause', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
return new Response('not-json', { status: 200, headers: { 'content-type': 'text/plain' } })
|
||||
}
|
||||
try {
|
||||
await client.listPools()
|
||||
assert.fail('Should have thrown')
|
||||
} catch (error) {
|
||||
assert.ok(error instanceof Error)
|
||||
assert.ok(error.message.includes('invalid JSON'))
|
||||
assert.ok(error.cause !== undefined, 'Should preserve JSON parse error as cause')
|
||||
}
|
||||
})
|
||||
|
||||
it('sets AbortSignal timeout on requests', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async (_input: RequestInfo | URL, init?: RequestInit) => {
|
||||
assert.ok(init?.signal, 'Request should have an abort signal')
|
||||
return mockResponse([])
|
||||
}
|
||||
await client.listPools()
|
||||
})
|
||||
})
|
||||
|
||||
describe('testConnection', () => {
|
||||
it('returns { ok: true } on successful connection', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async () => mockResponse([{ id: 'pool1' }])
|
||||
const result = await client.testConnection()
|
||||
assert.deepStrictEqual(result, { ok: true })
|
||||
})
|
||||
|
||||
it('returns { ok: false, error } on connection failure', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async () => {
|
||||
throw new Error('ECONNREFUSED')
|
||||
}
|
||||
const result = await client.testConnection()
|
||||
assert.strictEqual(result.ok, false)
|
||||
assert.ok(result.error)
|
||||
assert.ok(result.error!.includes('Cannot connect'))
|
||||
})
|
||||
})
|
||||
|
||||
describe('listPools', () => {
|
||||
it('returns pools with default fields', async () => {
|
||||
const pools = [{ id: 'pool1', name_label: 'Pool 1' }]
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === 'string' ? input : input.toString()
|
||||
assert.ok(url.includes('fields=id%2Cname_label%2Cname_description%2Cauto_poweron%2CHA_enabled'))
|
||||
return mockResponse(pools)
|
||||
}
|
||||
const result = await client.listPools()
|
||||
assert.deepStrictEqual(result, pools)
|
||||
})
|
||||
|
||||
it('passes custom fields', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === 'string' ? input : input.toString()
|
||||
assert.ok(url.includes('fields=id%2Cname_label'))
|
||||
return mockResponse([])
|
||||
}
|
||||
await client.listPools('id,name_label')
|
||||
})
|
||||
})
|
||||
|
||||
describe('listHosts', () => {
|
||||
it('returns hosts with default fields', async () => {
|
||||
const hosts = [{ id: 'host1', name_label: 'Host 1' }]
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async () => mockResponse(hosts)
|
||||
const result = await client.listHosts()
|
||||
assert.deepStrictEqual(result, hosts)
|
||||
})
|
||||
|
||||
it('passes filter parameter via object', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === 'string' ? input : input.toString()
|
||||
assert.ok(url.includes('filter=productBrand'))
|
||||
return mockResponse([])
|
||||
}
|
||||
await client.listHosts({ filter: 'productBrand:XCP-ng' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('listVms', () => {
|
||||
it('returns VMs with default fields', async () => {
|
||||
const vms = [{ id: 'vm1', name_label: 'VM 1', power_state: 'Running' }]
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async () => mockResponse(vms)
|
||||
const result = await client.listVms()
|
||||
assert.deepStrictEqual(result, vms)
|
||||
})
|
||||
|
||||
it('passes filter, fields, and limit via object', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === 'string' ? input : input.toString()
|
||||
assert.ok(url.includes('filter=power_state'))
|
||||
assert.ok(url.includes('limit=10'))
|
||||
return mockResponse([])
|
||||
}
|
||||
await client.listVms({ filter: 'power_state:Running', fields: 'id,name_label', limit: 10 })
|
||||
})
|
||||
|
||||
it('passes limit=0 correctly', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === 'string' ? input : input.toString()
|
||||
assert.ok(url.includes('limit=0'), 'limit=0 should be included in URL')
|
||||
return mockResponse([])
|
||||
}
|
||||
await client.listVms({ limit: 0 })
|
||||
})
|
||||
})
|
||||
|
||||
describe('getVm', () => {
|
||||
it('returns VM details', async () => {
|
||||
const vm = { id: 'vm1', name_label: 'VM 1', power_state: 'Running' }
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === 'string' ? input : input.toString()
|
||||
assert.ok(url.includes('/vms/vm1'))
|
||||
return mockResponse(vm)
|
||||
}
|
||||
const result = await client.getVm('vm1')
|
||||
assert.deepStrictEqual(result, vm)
|
||||
})
|
||||
|
||||
it('encodes special characters in VM ID', async () => {
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === 'string' ? input : input.toString()
|
||||
assert.ok(url.includes('/vms/vm%2Fspecial'), `URL should encode slash: ${url}`)
|
||||
return mockResponse({ id: 'vm/special', name_label: 'Special', power_state: 'Running' })
|
||||
}
|
||||
await client.getVm('vm/special')
|
||||
})
|
||||
})
|
||||
|
||||
describe('getPoolDashboard', () => {
|
||||
it('returns dashboard data', async () => {
|
||||
const dashboard = { hostsByStatus: { running: 2 }, vmsByStatus: { running: 10 } }
|
||||
const client = new XoClient({ url: 'http://xo.local:9000', username: 'admin', password: 'pass' })
|
||||
|
||||
globalThis.fetch = async (input: RequestInfo | URL) => {
|
||||
const url = typeof input === 'string' ? input : input.toString()
|
||||
assert.ok(url.includes('/pools/pool1/dashboard'))
|
||||
assert.ok(url.includes('ndjson=false'))
|
||||
return mockResponse(dashboard)
|
||||
}
|
||||
const result = await client.getPoolDashboard('pool1')
|
||||
assert.deepStrictEqual(result, dashboard)
|
||||
})
|
||||
})
|
||||
})
|
||||
16
@xen-orchestra/mcp/tsconfig.json
Normal file
16
@xen-orchestra/mcp/tsconfig.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"rootDir": "src",
|
||||
"outDir": "dist",
|
||||
"strict": true,
|
||||
"declaration": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"types": ["node"]
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -13,6 +13,8 @@
|
||||
|
||||
- [Search Engine] Implement first version of the Query Builder on Pools, Hosts, VMs, networks and Storage tables (PR [#9488](https://github.com/vatesfr/xen-orchestra/pull/9488))
|
||||
- [REST API] Added POST `/srs/{id}/actions/scan` rest routes (PR [#9514](https://github.com/vatesfr/xen-orchestra/pull/9514))
|
||||
- [MCP] Initial release of `@xen-orchestra/mcp`: MCP server enabling AI assistants to query XO infrastructure (pools, hosts, VMs, dashboard, documentation) (PR [#9519](https://github.com/vatesfr/xen-orchestra/pull/9519))
|
||||
|
||||
- **XO 5:**
|
||||
- [Network] Ability to switch management PIF (PRs [#9369](https://github.com/vatesfr/xen-orchestra/pull/9369) [#9510](https://github.com/vatesfr/xen-orchestra/pull/9510))
|
||||
- [Licenses] Display bundle name next to license name (PR [#9512](https://github.com/vatesfr/xen-orchestra/pull/9512))
|
||||
@@ -41,6 +43,7 @@
|
||||
<!--packages-start-->
|
||||
|
||||
- @xen-orchestra/backups patch
|
||||
- @xen-orchestra/mcp major
|
||||
- @xen-orchestra/rest-api minor
|
||||
- @xen-orchestra/web minor
|
||||
- @xen-orchestra/web-core minor
|
||||
|
||||
359
yarn.lock
359
yarn.lock
@@ -2444,6 +2444,11 @@
|
||||
"@hapi/bourne" "^3.0.0"
|
||||
"@hapi/hoek" "^11.0.2"
|
||||
|
||||
"@hono/node-server@^1.19.9":
|
||||
version "1.19.9"
|
||||
resolved "https://registry.yarnpkg.com/@hono/node-server/-/node-server-1.19.9.tgz#8f37119b1acf283fd3f6035f3d1356fdb97a09ac"
|
||||
integrity sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==
|
||||
|
||||
"@humanwhocodes/config-array@^0.13.0":
|
||||
version "0.13.0"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz#fb907624df3256d04b9aa2df50d7aa97ec648748"
|
||||
@@ -2772,6 +2777,29 @@
|
||||
slash "^3.0.0"
|
||||
tsconfig-paths "^4"
|
||||
|
||||
"@modelcontextprotocol/sdk@^1.26.0":
|
||||
version "1.26.0"
|
||||
resolved "https://registry.yarnpkg.com/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz#5b35d73062125f126cc70b0be83cbab53bcdde74"
|
||||
integrity sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==
|
||||
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"
|
||||
|
||||
"@napi-rs/wasm-runtime@^0.2.11":
|
||||
version "0.2.12"
|
||||
resolved "https://registry.yarnpkg.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz#3e78a8b96e6c33a6c517e1894efbd5385a7cb6f2"
|
||||
@@ -5642,6 +5670,14 @@ accepts@^1.3.5, accepts@~1.3.8:
|
||||
mime-types "~2.1.34"
|
||||
negotiator "0.6.3"
|
||||
|
||||
accepts@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-2.0.0.tgz#bbcf4ba5075467f3f2131eab3cffc73c2f5d7895"
|
||||
integrity sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==
|
||||
dependencies:
|
||||
mime-types "^3.0.0"
|
||||
negotiator "^1.0.0"
|
||||
|
||||
acme-client@^5.0.0:
|
||||
version "5.4.0"
|
||||
resolved "https://registry.yarnpkg.com/acme-client/-/acme-client-5.4.0.tgz#989e0625c3ff916d16fb7d7377035e80565aa635"
|
||||
@@ -5724,6 +5760,13 @@ aggregate-error@^3.0.0:
|
||||
clean-stack "^2.0.0"
|
||||
indent-string "^4.0.0"
|
||||
|
||||
ajv-formats@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv-formats/-/ajv-formats-3.0.1.tgz#3d5dc762bca17679c3c2ea7e90ad6b7532309578"
|
||||
integrity sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==
|
||||
dependencies:
|
||||
ajv "^8.0.0"
|
||||
|
||||
ajv@^6.12.4:
|
||||
version "6.12.6"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4"
|
||||
@@ -5734,6 +5777,16 @@ ajv@^6.12.4:
|
||||
json-schema-traverse "^0.4.1"
|
||||
uri-js "^4.2.2"
|
||||
|
||||
ajv@^8.0.0, ajv@^8.17.1:
|
||||
version "8.18.0"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.18.0.tgz#8864186b6738d003eb3a933172bb3833e10cefbc"
|
||||
integrity sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==
|
||||
dependencies:
|
||||
fast-deep-equal "^3.1.3"
|
||||
fast-uri "^3.0.1"
|
||||
json-schema-traverse "^1.0.0"
|
||||
require-from-string "^2.0.2"
|
||||
|
||||
ajv@^8.0.3, ajv@^8.11.0:
|
||||
version "8.17.1"
|
||||
resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6"
|
||||
@@ -6487,6 +6540,11 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
|
||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||
|
||||
balanced-match@^4.0.2:
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-4.0.3.tgz#6337a2f23e0604a30481423432f99eac603599f9"
|
||||
integrity sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==
|
||||
|
||||
bare-events@^2.7.0:
|
||||
version "2.8.2"
|
||||
resolved "https://registry.yarnpkg.com/bare-events/-/bare-events-2.8.2.tgz#7b3e10bd8e1fc80daf38bb516921678f566ab89f"
|
||||
@@ -6655,6 +6713,21 @@ body-parser@^1.20.0, body-parser@~1.20.3:
|
||||
type-is "~1.6.18"
|
||||
unpipe "~1.0.0"
|
||||
|
||||
body-parser@^2.2.1:
|
||||
version "2.2.2"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-2.2.2.tgz#1a32cdb966beaf68de50a9dfbe5b58f83cb8890c"
|
||||
integrity sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==
|
||||
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"
|
||||
|
||||
body-parser@~1.14.0:
|
||||
version "1.14.2"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.14.2.tgz#1015cb1fe2c443858259581db53332f8d0cf50f9"
|
||||
@@ -6709,6 +6782,13 @@ brace-expansion@^2.0.1:
|
||||
dependencies:
|
||||
balanced-match "^1.0.0"
|
||||
|
||||
brace-expansion@^5.0.2:
|
||||
version "5.0.2"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-5.0.2.tgz#b6c16d0791087af6c2bc463f52a8142046c06b6f"
|
||||
integrity sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==
|
||||
dependencies:
|
||||
balanced-match "^4.0.2"
|
||||
|
||||
braces@^1.8.2:
|
||||
version "1.8.5"
|
||||
resolved "https://registry.yarnpkg.com/braces/-/braces-1.8.5.tgz#ba77962e12dff969d6b76711e914b737857bf6a7"
|
||||
@@ -7749,6 +7829,11 @@ constants-browserify@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75"
|
||||
integrity sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==
|
||||
|
||||
content-disposition@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-1.0.1.tgz#a8b7bbeb2904befdfb6787e5c0c086959f605f9b"
|
||||
integrity sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==
|
||||
|
||||
content-disposition@~0.5.2, content-disposition@~0.5.4:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
|
||||
@@ -7823,12 +7908,17 @@ cookie-signature@1.0.6:
|
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
|
||||
|
||||
cookie-signature@^1.2.1:
|
||||
version "1.2.2"
|
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.2.2.tgz#57c7fc3cc293acab9fec54d73e15690ebe4a1793"
|
||||
integrity sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==
|
||||
|
||||
cookie-signature@~1.0.6, cookie-signature@~1.0.7:
|
||||
version "1.0.7"
|
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.7.tgz#ab5dd7ab757c54e60f37ef6550f481c426d10454"
|
||||
integrity sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==
|
||||
|
||||
cookie@0.7.2, cookie@^0.7.0, cookie@^0.7.2, cookie@~0.7.1, cookie@~0.7.2:
|
||||
cookie@0.7.2, cookie@^0.7.0, cookie@^0.7.1, cookie@^0.7.2, cookie@~0.7.1, cookie@~0.7.2:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.7.2.tgz#556369c472a2ba910f2979891b526b3436237ed7"
|
||||
integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
|
||||
@@ -7895,6 +7985,14 @@ core-util-is@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
|
||||
integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
|
||||
|
||||
cors@^2.8.5:
|
||||
version "2.8.6"
|
||||
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.6.tgz#ff5dd69bd95e547503820d29aba4f8faf8dfec96"
|
||||
integrity sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==
|
||||
dependencies:
|
||||
object-assign "^4"
|
||||
vary "^1"
|
||||
|
||||
cosmiconfig-typescript-loader@^6.1.0:
|
||||
version "6.2.0"
|
||||
resolved "https://registry.yarnpkg.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.2.0.tgz#26399fa92e9569052062846afd038c94628f0f69"
|
||||
@@ -7977,7 +8075,7 @@ cross-spawn@^6.0.5:
|
||||
shebang-command "^1.2.0"
|
||||
which "^1.2.9"
|
||||
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.6:
|
||||
cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3, cross-spawn@^7.0.5, cross-spawn@^7.0.6:
|
||||
version "7.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f"
|
||||
integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
|
||||
@@ -9026,7 +9124,7 @@ encodeurl@^1.0.2:
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
|
||||
|
||||
encodeurl@~2.0.0:
|
||||
encodeurl@^2.0.0, encodeurl@~2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-2.0.0.tgz#7b8ea898077d7e409d3ac45474ea38eaf0857a58"
|
||||
integrity sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==
|
||||
@@ -9670,7 +9768,7 @@ esutils@^2.0.2:
|
||||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||
|
||||
etag@~1.8.1:
|
||||
etag@^1.8.1, etag@~1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
||||
@@ -9771,6 +9869,18 @@ events@^2.0.0:
|
||||
resolved "https://registry.yarnpkg.com/events/-/events-2.1.0.tgz#2a9a1e18e6106e0e812aa9ebd4a819b3c29c0ba5"
|
||||
integrity sha512-3Zmiobend8P9DjmKAty0Era4jV8oJ0yGYe2nJJAxgymF9+N8F2m0hhZiMoWtcfepExzNKZumFU3ksdQbInGWCg==
|
||||
|
||||
eventsource-parser@^3.0.0, eventsource-parser@^3.0.1:
|
||||
version "3.0.6"
|
||||
resolved "https://registry.yarnpkg.com/eventsource-parser/-/eventsource-parser-3.0.6.tgz#292e165e34cacbc936c3c92719ef326d4aeb4e90"
|
||||
integrity sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==
|
||||
|
||||
eventsource@^3.0.2:
|
||||
version "3.0.7"
|
||||
resolved "https://registry.yarnpkg.com/eventsource/-/eventsource-3.0.7.tgz#1157622e2f5377bb6aef2114372728ba0c156989"
|
||||
integrity sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==
|
||||
dependencies:
|
||||
eventsource-parser "^3.0.1"
|
||||
|
||||
evp_bytestokey@^1.0.0, evp_bytestokey@^1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz#7fcbdb198dc71959432efe13842684e0525acb02"
|
||||
@@ -9890,6 +10000,13 @@ exponential-backoff@^3.1.1:
|
||||
resolved "https://registry.yarnpkg.com/exponential-backoff/-/exponential-backoff-3.1.3.tgz#51cf92c1c0493c766053f9d3abee4434c244d2f6"
|
||||
integrity sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==
|
||||
|
||||
express-rate-limit@^8.2.1:
|
||||
version "8.2.1"
|
||||
resolved "https://registry.yarnpkg.com/express-rate-limit/-/express-rate-limit-8.2.1.tgz#ec75fdfe280ecddd762b8da8784c61bae47d7f7f"
|
||||
integrity sha512-PCZEIEIxqwhzw4KF0n7QF4QqruVTcF73O5kFKUnGOyjbCCgizBBiFaYpd/fnBLUMPw/BWw9OsiN7GgrNYr7j6g==
|
||||
dependencies:
|
||||
ip-address "10.0.1"
|
||||
|
||||
express-session@^1.15.6:
|
||||
version "1.19.0"
|
||||
resolved "https://registry.yarnpkg.com/express-session/-/express-session-1.19.0.tgz#682139f6eec6c69db9febbe2362e8a85dd84af2f"
|
||||
@@ -9941,6 +10058,40 @@ express@^4.16.2, express@^4.21.2:
|
||||
utils-merge "1.0.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
express@^5.2.1:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-5.2.1.tgz#8f21d15b6d327f92b4794ecf8cb08a72f956ac04"
|
||||
integrity sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==
|
||||
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"
|
||||
|
||||
exsolve@^1.0.7:
|
||||
version "1.0.8"
|
||||
resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.8.tgz#7f5e34da61cd1116deda5136e62292c096f50613"
|
||||
@@ -10219,6 +10370,18 @@ fill-range@^7.1.1:
|
||||
dependencies:
|
||||
to-regex-range "^5.0.1"
|
||||
|
||||
finalhandler@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-2.1.1.tgz#a2c517a6559852bcdb06d1f8bd7f51b68fad8099"
|
||||
integrity sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==
|
||||
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"
|
||||
|
||||
finalhandler@~1.3.1:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.3.2.tgz#1ebc2228fc7673aac4a472c310cc05b77d852b88"
|
||||
@@ -10442,6 +10605,11 @@ fragment-cache@^0.2.1:
|
||||
dependencies:
|
||||
map-cache "^0.2.2"
|
||||
|
||||
fresh@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-2.0.0.tgz#8dd7df6a1b3a1b3a5cf186c05a5dd267622635a4"
|
||||
integrity sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==
|
||||
|
||||
fresh@~0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||
@@ -10815,6 +10983,15 @@ glob@^13.0.0:
|
||||
minipass "^7.1.2"
|
||||
path-scurry "^2.0.0"
|
||||
|
||||
glob@^13.0.3:
|
||||
version "13.0.6"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-13.0.6.tgz#078666566a425147ccacfbd2e332deb66a2be71d"
|
||||
integrity sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==
|
||||
dependencies:
|
||||
minimatch "^10.2.2"
|
||||
minipass "^7.1.3"
|
||||
path-scurry "^2.0.2"
|
||||
|
||||
glob@^7.1.0, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.2.0:
|
||||
version "7.2.3"
|
||||
resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b"
|
||||
@@ -11385,6 +11562,11 @@ homedir-polyfill@^1.0.1:
|
||||
dependencies:
|
||||
parse-passwd "^1.0.0"
|
||||
|
||||
hono@^4.11.4:
|
||||
version "4.12.0"
|
||||
resolved "https://registry.yarnpkg.com/hono/-/hono-4.12.0.tgz#18f96a2962274398efc57a1a79e22b3daad2ca1c"
|
||||
integrity sha512-NekXntS5M94pUfiVZ8oXXK/kkri+5WpX2/Ik+LVsl+uvw+soj4roXIsPqO+XsWrAw20mOzaXOZf3Q7PfB9A/IA==
|
||||
|
||||
hookable@^5.5.3:
|
||||
version "5.5.3"
|
||||
resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d"
|
||||
@@ -11491,7 +11673,7 @@ http-errors@^1.6.3, http-errors@^1.8.1, http-errors@~1.8.0, http-errors@~1.8.1:
|
||||
statuses ">= 1.5.0 < 2"
|
||||
toidentifier "1.0.1"
|
||||
|
||||
http-errors@^2.0.0, http-errors@~2.0.0, http-errors@~2.0.1:
|
||||
http-errors@^2.0.0, http-errors@^2.0.1, http-errors@~2.0.0, http-errors@~2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.1.tgz#36d2f65bc909c8790018dd36fb4d93da6caae06b"
|
||||
integrity sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==
|
||||
@@ -11613,7 +11795,7 @@ iconv-lite@^0.6.2:
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
iconv-lite@^0.7.0:
|
||||
iconv-lite@^0.7.0, iconv-lite@~0.7.0:
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.7.2.tgz#d0bdeac3f12b4835b7359c2ad89c422a4d1cc72e"
|
||||
integrity sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==
|
||||
@@ -11880,6 +12062,11 @@ invert-kv@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6"
|
||||
integrity sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==
|
||||
|
||||
ip-address@10.0.1:
|
||||
version "10.0.1"
|
||||
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.0.1.tgz#a8180b783ce7788777d796286d61bce4276818ed"
|
||||
integrity sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==
|
||||
|
||||
ip-address@^10.0.1:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.1.0.tgz#d8dcffb34d0e02eb241427444a6e23f5b0595aa4"
|
||||
@@ -12285,6 +12472,11 @@ is-promise@^2.0.0, is-promise@^2.2.2:
|
||||
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.2.2.tgz#39ab959ccbf9a774cf079f7b40c7a26f763135f1"
|
||||
integrity sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==
|
||||
|
||||
is-promise@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-4.0.0.tgz#42ff9f84206c1991d26debf520dd5c01042dd2f3"
|
||||
integrity sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==
|
||||
|
||||
is-regex@^1.0.3, is-regex@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22"
|
||||
@@ -12556,6 +12748,11 @@ jiti@^2.6.1:
|
||||
resolved "https://registry.yarnpkg.com/jiti/-/jiti-2.6.1.tgz#178ef2fc9a1a594248c20627cd820187a4d78d92"
|
||||
integrity sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==
|
||||
|
||||
jose@^6.1.3:
|
||||
version "6.1.3"
|
||||
resolved "https://registry.yarnpkg.com/jose/-/jose-6.1.3.tgz#8453d7be88af7bb7d64a0481d6a35a0145ba3ea5"
|
||||
integrity sha512-0TpaTfihd4QMNwrz/ob2Bp7X04yuxJkjRGi4aKmOqwhov54i6u79oCv7T+C7lo70MKH6BesI3vscD1yb/yzKXQ==
|
||||
|
||||
"jquery@1.9.1 - 3":
|
||||
version "3.7.1"
|
||||
resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.7.1.tgz#083ef98927c9a6a74d05a6af02806566d16274de"
|
||||
@@ -12665,6 +12862,11 @@ json-schema-traverse@^1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2"
|
||||
integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==
|
||||
|
||||
json-schema-typed@^8.0.2:
|
||||
version "8.0.2"
|
||||
resolved "https://registry.yarnpkg.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz#e98ee7b1899ff4a184534d1f167c288c66bbeff4"
|
||||
integrity sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA==
|
||||
|
||||
json-stable-stringify-without-jsonify@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651"
|
||||
@@ -13627,6 +13829,11 @@ media-typer@0.3.0:
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
|
||||
|
||||
media-typer@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-1.1.0.tgz#6ab74b8f2d3320f2064b2a87a38e7931ff3a5561"
|
||||
integrity sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==
|
||||
|
||||
memoizee@0.4.X:
|
||||
version "0.4.17"
|
||||
resolved "https://registry.yarnpkg.com/memoizee/-/memoizee-0.4.17.tgz#942a5f8acee281fa6fb9c620bddc57e3b7382949"
|
||||
@@ -13676,6 +13883,11 @@ merge-descriptors@1.0.3:
|
||||
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.3.tgz#d80319a65f3c7935351e5cfdac8f9318504dbed5"
|
||||
integrity sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==
|
||||
|
||||
merge-descriptors@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-2.0.0.tgz#ea922f660635a2249ee565e0449f951e6b603808"
|
||||
integrity sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==
|
||||
|
||||
merge-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
||||
@@ -13750,7 +13962,7 @@ mime-db@1.52.0:
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
"mime-db@>= 1.43.0 < 2", mime-db@^1.52.0:
|
||||
"mime-db@>= 1.43.0 < 2", mime-db@^1.52.0, mime-db@^1.54.0:
|
||||
version "1.54.0"
|
||||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.54.0.tgz#cddb3ee4f9c64530dff640236661d42cb6a314f5"
|
||||
integrity sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==
|
||||
@@ -13762,6 +13974,13 @@ mime-types@^2.1.12, mime-types@^2.1.18, mime-types@~2.1.24, mime-types@~2.1.34:
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
mime-types@^3.0.0, mime-types@^3.0.2:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-3.0.2.tgz#39002d4182575d5af036ffa118100f2524b2e2ab"
|
||||
integrity sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==
|
||||
dependencies:
|
||||
mime-db "^1.54.0"
|
||||
|
||||
mime@1.6.0, mime@^1.4.1:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
@@ -13830,6 +14049,13 @@ minimatch@^10.1.2:
|
||||
dependencies:
|
||||
"@isaacs/brace-expansion" "^5.0.1"
|
||||
|
||||
minimatch@^10.2.2:
|
||||
version "10.2.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.2.tgz#361603ee323cfb83496fea2ae17cc44ea4e1f99f"
|
||||
integrity sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==
|
||||
dependencies:
|
||||
brace-expansion "^5.0.2"
|
||||
|
||||
minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
|
||||
@@ -13920,6 +14146,11 @@ minipass@^5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707"
|
||||
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
|
||||
|
||||
minipass@^7.1.3:
|
||||
version "7.1.3"
|
||||
resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.3.tgz#79389b4eb1bb2d003a9bba87d492f2bd37bdc65b"
|
||||
integrity sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==
|
||||
|
||||
minizlib@^2.1.1, minizlib@^2.1.2:
|
||||
version "2.1.2"
|
||||
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
|
||||
@@ -14533,6 +14764,11 @@ negotiator@^0.6.3, negotiator@~0.6.4:
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.4.tgz#777948e2452651c570b712dd01c23e262713fff7"
|
||||
integrity sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==
|
||||
|
||||
negotiator@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-1.0.0.tgz#b6c91bb47172d69f93cfd7c357bbb529019b5f6a"
|
||||
integrity sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==
|
||||
|
||||
neo-async@^2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||
@@ -14873,7 +15109,7 @@ oauth@0.10.x:
|
||||
resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.10.2.tgz#fd7139b0ce1a1037bd11fa4e236afc588132418c"
|
||||
integrity sha512-JtFnB+8nxDEXgNyniwz573xxbKSOu3R8D40xQKqcjwJ2CDkYqUDI53o6IuzDJBx60Z8VKCm271+t8iFjakrl8Q==
|
||||
|
||||
object-assign@4.X, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
|
||||
object-assign@4.X, object-assign@^4, object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
|
||||
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
||||
@@ -15024,7 +15260,7 @@ object.values@^1.1.6, object.values@^1.2.1:
|
||||
define-properties "^1.2.1"
|
||||
es-object-atoms "^1.0.0"
|
||||
|
||||
on-finished@^2.3.0, on-finished@~2.4.1:
|
||||
on-finished@^2.3.0, on-finished@^2.4.1, on-finished@~2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
||||
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
|
||||
@@ -15412,7 +15648,7 @@ parse5@^7.0.0, parse5@^7.1.2:
|
||||
dependencies:
|
||||
entities "^6.0.0"
|
||||
|
||||
parseurl@^1.3.2, parseurl@~1.3.0, parseurl@~1.3.3:
|
||||
parseurl@^1.3.2, parseurl@^1.3.3, parseurl@~1.3.0, parseurl@~1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
|
||||
@@ -15593,12 +15829,20 @@ path-scurry@^2.0.0:
|
||||
lru-cache "^11.0.0"
|
||||
minipass "^7.1.2"
|
||||
|
||||
path-scurry@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.2.tgz#6be0d0ee02a10d9e0de7a98bae65e182c9061f85"
|
||||
integrity sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==
|
||||
dependencies:
|
||||
lru-cache "^11.0.0"
|
||||
minipass "^7.1.2"
|
||||
|
||||
path-to-regexp@^6.3.0:
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz#2b6a26a337737a8e1416f9272ed0766b1c0389f4"
|
||||
integrity sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==
|
||||
|
||||
path-to-regexp@^8.1.0:
|
||||
path-to-regexp@^8.0.0, path-to-regexp@^8.1.0:
|
||||
version "8.3.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-8.3.0.tgz#aa818a6981f99321003a08987d3cec9c3474cd1f"
|
||||
integrity sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==
|
||||
@@ -15737,6 +15981,11 @@ pirates@^4.0.5, pirates@^4.0.6:
|
||||
resolved "https://registry.yarnpkg.com/pirates/-/pirates-4.0.7.tgz#643b4a18c4257c8a65104b73f3049ce9a0a15e22"
|
||||
integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==
|
||||
|
||||
pkce-challenge@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pkce-challenge/-/pkce-challenge-5.0.1.tgz#3b4446865b17b1745e9ace2016a31f48ddf6230d"
|
||||
integrity sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==
|
||||
|
||||
pkg-dir@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-3.0.0.tgz#2749020f239ed990881b1f71210d51eb6523bea3"
|
||||
@@ -16458,6 +16707,13 @@ qs@^6.12.3, qs@~6.14.0:
|
||||
dependencies:
|
||||
side-channel "^1.1.0"
|
||||
|
||||
qs@^6.14.0, qs@^6.14.1:
|
||||
version "6.15.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.15.0.tgz#db8fd5d1b1d2d6b5b33adaf87429805f1909e7b3"
|
||||
integrity sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==
|
||||
dependencies:
|
||||
side-channel "^1.1.0"
|
||||
|
||||
qs@~2.2.3:
|
||||
version "2.2.5"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-2.2.5.tgz#1088abaf9dcc0ae5ae45b709e6c6b5888b23923c"
|
||||
@@ -16525,11 +16781,21 @@ randomfill@^1.0.4:
|
||||
randombytes "^2.0.5"
|
||||
safe-buffer "^5.1.0"
|
||||
|
||||
range-parser@~1.2.1:
|
||||
range-parser@^1.2.1, range-parser@~1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||
|
||||
raw-body@^3.0.0, raw-body@^3.0.1:
|
||||
version "3.0.2"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.2.tgz#3e3ada5ae5568f9095d84376fd3a49b8fb000a51"
|
||||
integrity sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==
|
||||
dependencies:
|
||||
bytes "~3.1.2"
|
||||
http-errors "~2.0.1"
|
||||
iconv-lite "~0.7.0"
|
||||
unpipe "~1.0.0"
|
||||
|
||||
raw-body@~2.1.5:
|
||||
version "2.1.7"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.1.7.tgz#adfeace2e4fb3098058014d08c072dcc59758774"
|
||||
@@ -17380,6 +17646,14 @@ rimraf@^5.0.1, rimraf@^5.0.5:
|
||||
dependencies:
|
||||
glob "^10.3.7"
|
||||
|
||||
rimraf@^6.0.0:
|
||||
version "6.1.3"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.1.3.tgz#afbee236b3bd2be331d4e7ce4493bac1718981af"
|
||||
integrity sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==
|
||||
dependencies:
|
||||
glob "^13.0.3"
|
||||
package-json-from-dist "^1.0.1"
|
||||
|
||||
rimraf@^6.0.1:
|
||||
version "6.1.2"
|
||||
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-6.1.2.tgz#9a0f3cea2ab853e81291127422116ecf2a86ae89"
|
||||
@@ -17430,6 +17704,17 @@ rollup@^4.34.9:
|
||||
"@rollup/rollup-win32-x64-msvc" "4.57.1"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
router@^2.2.0:
|
||||
version "2.2.0"
|
||||
resolved "https://registry.yarnpkg.com/router/-/router-2.2.0.tgz#019be620b711c87641167cc79b99090f00b146ef"
|
||||
integrity sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==
|
||||
dependencies:
|
||||
debug "^4.4.0"
|
||||
depd "^2.0.0"
|
||||
is-promise "^4.0.0"
|
||||
parseurl "^1.3.3"
|
||||
path-to-regexp "^8.0.0"
|
||||
|
||||
run-async@^2.4.0:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455"
|
||||
@@ -17597,6 +17882,23 @@ semver@^7.0.0, semver@^7.1.1, semver@^7.3.2, semver@^7.3.5, semver@^7.3.6, semve
|
||||
resolved "https://registry.yarnpkg.com/semver/-/semver-7.7.4.tgz#28464e36060e991fa7a11d0279d2d3f3b57a7e8a"
|
||||
integrity sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==
|
||||
|
||||
send@^1.1.0, send@^1.2.0:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-1.2.1.tgz#9eab743b874f3550f40a26867bf286ad60d3f3ed"
|
||||
integrity sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==
|
||||
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"
|
||||
|
||||
send@~0.19.0, send@~0.19.1:
|
||||
version "0.19.2"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.19.2.tgz#59bc0da1b4ea7ad42736fd642b1c4294e114ff29"
|
||||
@@ -17626,6 +17928,16 @@ serve-static@^1.13.1, serve-static@~1.16.2:
|
||||
parseurl "~1.3.3"
|
||||
send "~0.19.1"
|
||||
|
||||
serve-static@^2.2.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-2.2.1.tgz#7f186a4a4e5f5b663ad7a4294ff1bf37cf0e98a9"
|
||||
integrity sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==
|
||||
dependencies:
|
||||
encodeurl "^2.0.0"
|
||||
escape-html "^1.0.3"
|
||||
parseurl "^1.3.3"
|
||||
send "^1.2.0"
|
||||
|
||||
set-blocking@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
|
||||
@@ -18145,7 +18457,7 @@ statuses@1, "statuses@>= 1.5.0 < 2", statuses@^1.5.0:
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.5.0.tgz#161c7dac177659fd9811f43771fa99381478628c"
|
||||
integrity sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==
|
||||
|
||||
statuses@~2.0.1, statuses@~2.0.2:
|
||||
statuses@^2.0.1, statuses@^2.0.2, statuses@~2.0.1, statuses@~2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.2.tgz#8f75eecef765b5e1cfcdc080da59409ed424e382"
|
||||
integrity sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==
|
||||
@@ -19161,6 +19473,15 @@ type-is@^1.6.16, type-is@~1.6.10, type-is@~1.6.18:
|
||||
media-typer "0.3.0"
|
||||
mime-types "~2.1.24"
|
||||
|
||||
type-is@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-2.0.1.tgz#64f6cf03f92fce4015c2b224793f6bdd4b068c97"
|
||||
integrity sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==
|
||||
dependencies:
|
||||
content-type "^1.0.5"
|
||||
media-typer "^1.1.0"
|
||||
mime-types "^3.0.0"
|
||||
|
||||
type@^2.7.2:
|
||||
version "2.7.3"
|
||||
resolved "https://registry.yarnpkg.com/type/-/type-2.7.3.tgz#436981652129285cc3ba94f392886c2637ea0486"
|
||||
@@ -19712,7 +20033,7 @@ varint@^5.0.0, varint@~5.0.0:
|
||||
resolved "https://registry.yarnpkg.com/varint/-/varint-5.0.2.tgz#5b47f8a947eb668b848e034dcfa87d0ff8a7f7a4"
|
||||
integrity sha512-lKxKYG6H03yCZUpAGOPOsMcGxd1RHCu1iKvEHYDPmTyq2HueGhD73ssNBqqQWfvYs04G9iUFRvmAVLW20Jw6ow==
|
||||
|
||||
vary@^1.1.2, vary@~1.1.2:
|
||||
vary@^1, vary@^1.1.2, vary@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
||||
@@ -20519,6 +20840,16 @@ yoga-wasm-web@~0.3.3:
|
||||
resolved "https://registry.yarnpkg.com/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz#eb8e9fcb18e5e651994732f19a220cb885d932ba"
|
||||
integrity sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==
|
||||
|
||||
zod-to-json-schema@^3.25.1:
|
||||
version "3.25.1"
|
||||
resolved "https://registry.yarnpkg.com/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz#7f24962101a439ddade2bf1aeab3c3bfec7d84ba"
|
||||
integrity sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==
|
||||
|
||||
"zod@^3.25 || ^4.0", zod@^4.3.6:
|
||||
version "4.3.6"
|
||||
resolved "https://registry.yarnpkg.com/zod/-/zod-4.3.6.tgz#89c56e0aa7d2b05107d894412227087885ab112a"
|
||||
integrity sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==
|
||||
|
||||
zrender@5.6.1:
|
||||
version "5.6.1"
|
||||
resolved "https://registry.yarnpkg.com/zrender/-/zrender-5.6.1.tgz#e08d57ecf4acac708c4fcb7481eb201df7f10a6b"
|
||||
|
||||
Reference in New Issue
Block a user