From 00fa0f70af29b65718b1ce1666caa0cfc5f48943 Mon Sep 17 00:00:00 2001 From: Mingholy Date: Thu, 12 Jun 2025 15:50:12 +0800 Subject: [PATCH 1/2] fix: add stream log support --- infra/sandbox/src/services/container-service.ts | 6 ++++++ infra/sandbox/src/services/dependency-service.ts | 8 ++++++++ infra/sandbox/src/services/execution-service.ts | 7 +++++++ infra/sandbox/src/services/file-service.ts | 10 +++++++--- infra/sandbox/src/services/git-service.ts | 4 ++++ 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/infra/sandbox/src/services/container-service.ts b/infra/sandbox/src/services/container-service.ts index 9cd29d65..21eb9402 100644 --- a/infra/sandbox/src/services/container-service.ts +++ b/infra/sandbox/src/services/container-service.ts @@ -586,6 +586,12 @@ class ContainerService { } ); + stream.on("data", (chunk: Buffer) => { + logger.info(`${chunk.toString()}`, { + containerId: container.id, + }); + }); + stream.on("end", () => { resolve({ stdout, stderr }); }); diff --git a/infra/sandbox/src/services/dependency-service.ts b/infra/sandbox/src/services/dependency-service.ts index c20a1038..3a8c2fee 100644 --- a/infra/sandbox/src/services/dependency-service.ts +++ b/infra/sandbox/src/services/dependency-service.ts @@ -72,6 +72,10 @@ class NodePackageHandler implements DependencyHandler { } ); + stream.on("data", (chunk: Buffer) => { + logger.info(`${chunk.toString()}`, { position: 'NodePackageHandler', containerId: container.id }); + }); + stream.on("end", async () => { try { const inspectData = await exec.inspect(); @@ -186,6 +190,10 @@ class PythonPackageHandler implements DependencyHandler { } ); + stream.on("data", (chunk: Buffer) => { + logger.info(`${chunk.toString()}`, { position: 'PythonPackageHandler', containerId: container.id }); + }); + stream.on("end", async () => { try { const inspectData = await exec.inspect(); diff --git a/infra/sandbox/src/services/execution-service.ts b/infra/sandbox/src/services/execution-service.ts index 2c6b8912..99989dd9 100644 --- a/infra/sandbox/src/services/execution-service.ts +++ b/infra/sandbox/src/services/execution-service.ts @@ -384,6 +384,12 @@ class ExecutionService { } ); + stream.on("data", (chunk: Buffer) => { + logger.info(`${chunk.toString()}`, { + containerId: container.id, + }); + }); + stream.on("end", async () => { try { const inspectData = await exec.inspect(); @@ -418,6 +424,7 @@ class ExecutionService { }); }); } + } export default new ExecutionService(); diff --git a/infra/sandbox/src/services/file-service.ts b/infra/sandbox/src/services/file-service.ts index f443138b..192a1657 100644 --- a/infra/sandbox/src/services/file-service.ts +++ b/infra/sandbox/src/services/file-service.ts @@ -8,6 +8,7 @@ import { ApiError } from "../middleware/error-handler"; import { SandboxFiles } from "../types"; import gitService from "./git-service"; import dockerConfig from "./docker-config"; +import tar from 'tar-stream' // 固定工作目录,所有代码都存放在这里,由Git管理版本 const WORK_DIR = "/home/sandbox"; @@ -363,7 +364,6 @@ class FileService { }); // Extract the zip file from the tar archive - const tar = require("tar-stream"); const extract = tar.extract(); // Use promise to properly handle async extraction @@ -376,6 +376,7 @@ class FileService { stream.on("data", (chunk: Buffer) => { chunks.push(chunk); + logger.info(`${chunk.toString()}`, { position: 'FileService', containerId: container.id }); }); stream.on("end", () => { @@ -501,6 +502,10 @@ class FileService { } ); + stream.on("data", (chunk: Buffer) => { + logger.info(`${chunk.toString()}`, { position: 'FileService', containerId: container.id }); + }); + stream.on("end", () => { resolve({ stdout, stderr }); }); @@ -530,8 +535,7 @@ class FileService { // Read the file content const content = fs.readFileSync(localPath); - // Create a tar stream with the file - const pack = require("tar-stream").pack(); + const pack = tar.pack(); // Add file to the tar stream const fileName = path.basename(containerPath); diff --git a/infra/sandbox/src/services/git-service.ts b/infra/sandbox/src/services/git-service.ts index 57370159..ce664ab1 100644 --- a/infra/sandbox/src/services/git-service.ts +++ b/infra/sandbox/src/services/git-service.ts @@ -597,6 +597,10 @@ Thumbs.db } ); + stream.on("data", (chunk: Buffer) => { + logger.info(`${chunk.toString()}`, { position: 'GitService', containerId: container.id }); + }); + stream.on("end", () => { resolve({ stdout, stderr }); }); From 2113c89f5cf7c0d504189f514688afb79abd85b4 Mon Sep 17 00:00:00 2001 From: Mingholy Date: Thu, 12 Jun 2025 15:50:16 +0800 Subject: [PATCH 2/2] refactor(logger): add http log service with session control --- infra/logger/README.md | 10 + infra/logger/logger-design.md | 304 +++--- infra/logger/package.json | 15 +- infra/logger/src/http/DynamicHttpTransport.ts | 37 + infra/logger/src/http/__test__/logger.test.ts | 419 +++++++++ .../src/http/__test__/storage-memory.test.ts | 448 +++++++++ infra/logger/src/http/index.ts | 3 + infra/logger/src/http/middlewares.ts | 54 ++ infra/logger/src/http/routers.ts | 288 ++++++ infra/logger/src/http/server.ts | 45 + infra/logger/src/http/storage-memory.ts | 162 ++++ infra/logger/src/http/types.ts | 68 ++ infra/logger/src/index.ts | 10 +- infra/logger/src/log-server.ts | 85 -- infra/logger/src/log-service.ts | 55 ++ infra/logger/src/logger.ts | 207 ++-- infra/logger/src/types/index.ts | 119 ++- infra/logger/src/ws/server.ts | 58 ++ .../socket-io.ts => ws/transport.ts} | 0 infra/logger/vitest.config.ts | 20 + .../design/infra-unified-server-design.md | 253 +++++ infra/sandbox/package.json | 10 +- infra/sandbox/src/middleware/context.ts | 42 + infra/sandbox/src/server.ts | 4 +- infra/sandbox/src/utils/logger.ts | 32 +- infra/sandbox/src/utils/request-context.ts | 18 + package.json | 5 +- pnpm-lock.yaml | 882 ++++++++++++++++-- 28 files changed, 3143 insertions(+), 510 deletions(-) create mode 100644 infra/logger/src/http/DynamicHttpTransport.ts create mode 100644 infra/logger/src/http/__test__/logger.test.ts create mode 100644 infra/logger/src/http/__test__/storage-memory.test.ts create mode 100644 infra/logger/src/http/index.ts create mode 100644 infra/logger/src/http/middlewares.ts create mode 100644 infra/logger/src/http/routers.ts create mode 100644 infra/logger/src/http/server.ts create mode 100644 infra/logger/src/http/storage-memory.ts create mode 100644 infra/logger/src/http/types.ts delete mode 100644 infra/logger/src/log-server.ts create mode 100644 infra/logger/src/log-service.ts create mode 100644 infra/logger/src/ws/server.ts rename infra/logger/src/{transports/socket-io.ts => ws/transport.ts} (100%) create mode 100644 infra/logger/vitest.config.ts create mode 100644 infra/sandbox/design/infra-unified-server-design.md create mode 100644 infra/sandbox/src/middleware/context.ts create mode 100644 infra/sandbox/src/utils/request-context.ts diff --git a/infra/logger/README.md b/infra/logger/README.md index 9897c1ef..b3941f4a 100644 --- a/infra/logger/README.md +++ b/infra/logger/README.md @@ -214,6 +214,16 @@ The Socket.IO transport automatically handles reconnection when connection is lo - Message queuing - logs are cached when connection is lost and sent once reconnected - Graceful degradation - console and file outputs continue to work when WebSocket is unavailable +## Environment Variables + +This package requires a `TOKEN_SECRET` environment variable for JWT authentication on the `/logs/health` endpoint. If you do not have a `.env` file, create one in the `infra/logger` directory with the following content: + +``` +TOKEN_SECRET=your-secret-key +``` + +Replace `your-secret-key` with a secure value in production. For development, you can use any string. + ## License MIT diff --git a/infra/logger/logger-design.md b/infra/logger/logger-design.md index 2f9e8bc8..e73a9c4f 100644 --- a/infra/logger/logger-design.md +++ b/infra/logger/logger-design.md @@ -2,235 +2,159 @@ ## 项目概述 -本项目实现了一个基于 Winston 的日志工具,保持与 Winston API 完全一致的同时,支持将日志同步输出到三个不同渠道: +本项目实现了一个基于 Winston 的多通道日志工具,保持与 Winston API 完全一致的同时,支持将日志同步输出到多个渠道: -1. **标准输出 (stdout)**:直接在控制台显示日志,支持彩色输出 -2. **本地文件**:将日志保存到指定的本地文件,支持文件轮转 -3. **WebSocket**:通过 Socket.IO 实时推送日志信息到客户端,支持断线重连 +1. **标准输出 (stdout)**:控制台彩色输出 +2. **本地文件**:日志文件持久化,支持轮转 +3. **WebSocket**:通过 Socket.IO 实时推送日志到前端/订阅端 +4. **HTTP**:通过 HTTP 动态推送日志,支持自定义 header(如鉴权、容器隔离) ## 技术栈 - - Node.js - TypeScript - Winston (日志库) -- Socket.io (WebSocket 通信) -- Jest (单元测试) - -## 设计方案 +- Socket.IO (WebSocket 通信) +- Express (HTTP 日志服务) -### 核心组件 +## 主要组件与职责 -1. **Logger 类**:主要的日志处理类,封装 Winston 功能并扩展多通道能力 - - `createMultiChannelLogger()`: 创建具有自定义配置的 logger 实例 - - `createDefaultLogger()`: 创建具有默认配置的 logger 实例,简化常见场景 -2. **Transport 实现**: - - 使用原生 Winston Console Transport 处理标准输出 - - 使用原生 Winston File Transport 处理文件日志 - - 自定义 Socket.IO Transport 处理 WebSocket 实时日志 -3. **配置管理**:提供灵活的配置选项和默认配置,包括: - - 日志格式化配置 - - 日志级别管理 - - 文件轮转策略 - - WebSocket 连接和重连策略 +### 1. Logger(多通道日志采集器) +- 通过 `createLogger` 创建,支持 console、file、WebSocket、HTTP 多种 transport。 +- 保持 Winston API 兼容,支持 info/warn/error/debug 等所有标准方法。 +- 支持自定义格式、日志级别、文件轮转、动态 header。 +- WebSocket/HTTP transport 可选,按需启用。 -### API 设计 +### 2. SocketIOTransport(WebSocket 日志推送) +- 继承自 winston-transport,自定义实现。 +- 通过 Socket.IO 客户端连接指定 WS server,推送日志。 +- 支持断线重连、消息队列缓存、事件名自定义。 +- 连接失败时自动降级,日志不会丢失。 -Logger 类将保持与 Winston 完全一致的 API,包括但不限于: +### 3. DynamicHttpTransport(HTTP 日志推送) +- 继承自 winston.transports.Http。 +- 支持每条日志动态生成 header(如 Authorization、x-container-id 等)。 +- 通过 fetch 发送 POST 请求到 HTTP 日志服务。 +- 失败时自动忽略,不影响主流程。 -```typescript -logger.info(message, ...meta); -logger.error(message, ...meta); -logger.warn(message, ...meta); -logger.debug(message, ...meta); -logger.verbose(message, ...meta); -logger.silly(message, ...meta); -``` +### 4. 日志服务端(WS/HTTP Server) +- WS server:通过 `createWsLogServer` 创建,负责接收 logger 推送的日志并广播给所有客户端。 +- HTTP server:通过 `createHttpLogServer` 创建,负责日志的 HTTP API(如 ingest/query/stream/health),不做日志推送。 +- 支持 session 隔离、SSE 流式查询、健康检查等。 -### Socket.io Transport 实现 +### 5. 统一入口(createLogService) +- 通过一份配置同时创建 logger、WS server、HTTP server。 +- 便于业务方一键集成多通道日志能力。 -自定义一个 Winston Transport,用于将日志消息通过 Socket.io 发送到指定的 WebSocket 端点。 +## 主要类型定义(TypeScript) ```typescript -class SocketIOTransport extends winston.Transport { - private socket: SocketIOClient.Socket; - - constructor(options: SocketIOTransportOptions) { - super(options); - this.socket = io(options.url); - // 初始化连接管理与错误处理 - } - - log(info: any, callback: () => void) { - this.socket.emit("log", info); - callback(); - } +export interface LogServiceConfig { + logFilePath: string; // 日志文件路径 + level?: string; // 日志级别 + debug?: boolean; // 是否开启 debug 输出 + service?: string; // 服务名(可选) + ws?: { + port: number; + enabled: boolean; + }; + http?: { + port: number; + enabled: boolean; + host?: string; + path?: string; + getContext?: () => { authorization?: string; xContainerId?: string }; + }; } ``` -### 配置示例 +## 典型用法 +### 1. 创建多通道 logger ```typescript +import { createLogger, SocketIOTransport } from '@graphscope/logger'; + const logger = createLogger({ - level: "info", - format: combine(timestamp(), json()), - transports: [ - // 标准输出 - new transports.Console(), - - // 文件输出 - new transports.File({ - filename: "/tmp/logs/app.log", - maxsize: 5242880, // 5MB - maxFiles: 5 - }), - - // WebSocket 输出 - new SocketIOTransport({ - url: "ws://localhost:3000/ws", - level: "info" - }) - ] + level: 'info', + file: { filename: '/tmp/app.log' }, + console: {}, + ws: { + enabled: true, + url: 'http://localhost:3001', + eventName: 'log', + level: 'info', + reconnection: true, + reconnectionAttempts: 10, + reconnectionDelay: 2000, + }, + http: { + enabled: true, + host: '127.0.0.1', + port: 4002, + path: '/logs/log', + getContext: () => ({ authorization: 'Bearer ...', xContainerId: 'abc' }) + } }); -``` - -## 系统架构 - -### 组件架构 - -``` -客户端应用 <---- Socket.IO ----> 独立日志服务器 <---- Socket.IO ---- 日志库 - | | | - | | | - +------ HTTP API -------> API服务器 <---------------------------- 控制台输出 - | - | - 日志文件 -``` - -### 项目结构 +logger.info('This is an info log'); +logger.error('This is an error log', { error: new Error('fail') }); ``` -/ -├── src/ -│ ├── index.ts # 主入口文件 -│ ├── logger.ts # Logger 类实现 -│ ├── transports/ -│ │ └── socket-io.ts # Socket.IO Transport 实现 -│ └── types/ -│ └── index.ts # 类型定义 -├── examples/ -│ ├── server/ # 服务器示例 -│ │ ├── index.ts # API 服务器示例 -│ │ └── log-socket-server.ts # 日志 Socket.IO 服务器 -│ ├── client/ # 客户端示例 (React) -│ ├── logs/ # 日志文件存储目录 -│ ├── curl-test.sh # cURL 测试脚本 -│ └── run.sh # 运行脚本 -│ ├── logger.test.ts # Logger 单元测试 -│ └── socket-transport.test.ts # Socket.io Transport 测试 -├── examples/ -│ ├── basic-usage.ts # 基本使用示例 -│ └── websocket-client.html # WebSocket 客户端示例 -├── package.json -├── tsconfig.json -└── README.md -``` - -## 实现详情 - -### 日志格式化 - -- 支持 Winston 所有的内置格式化器,如 json、simple、colorize 等 -- 为控制台和文件输出提供不同的格式 -- 确保在不同输出渠道保持语义一致性 - -### 错误处理机制 - -- **WebSocket 连接失败处理**: - - - 自动重连机制 - - 消息队列缓存 - 当连接断开时,日志消息会被缓存,连接恢复后重新发送 - - 降级策略 - 在 WebSocket 不可用时,仍能保证控制台和文件输出正常工作 - -- **文件写入错误处理**: - - 目录自动创建 - - 文件轮转策略,避免单个文件过大 - - 日志备份配置 - -### 日志服务端架构 - -为了避免循环连接问题并提高可维护性,日志系统分为两个独立服务: - -1. **API 服务器**:提供 HTTP 接口,生成日志 -2. **日志 WebSocket 服务器**:专门用于接收和广播日志信息 - -这种分离架构解决了以下问题: - -- 避免了循环连接问题 -- 提高了系统可伸缩性 -- 降低了单个服务的负担 - -### 性能优化 - -- **批量处理**:WebSocket Transport 实现了消息队列 -- **缓冲策略**:在高日志量场景下,通过队列管理减少网络压力 -- **轻量级事件传输**:避免冗余信息传输 - -## 使用示例 - -### 基本使用 +### 2. 统一入口创建 logger + WS/HTTP server ```typescript -import { createLogger } from "./path-to-logger"; - -const logger = createLogger({ - // 配置选项 +import { createLogService } from '@graphscope/logger'; + +const { logger, wsServer, httpServer } = createLogService({ + logFilePath: '/var/log/app.log', + level: 'info', + ws: { port: 3002, enabled: true }, + http: { port: 4001, enabled: true }, + service: 'my-service', }); -logger.info("Hello, world!"); -logger.error("An error occurred", { error: "Details" }); +logger.info('hello world'); ``` -### WebSocket 客户端订阅 - +### 3. WebSocket 客户端订阅日志 ```javascript -// 浏览器中 -const socket = io("ws://localhost:3000/ws"); -socket.on("log", (logMessage) => { - console.log("Received log:", logMessage); +const socket = io('http://localhost:3002'); +socket.on('log', (logMessage) => { + console.log('Received log:', logMessage); }); ``` -### Curl 测试 (WebSocket) - -使用 websocat 等工具测试 WebSocket 连接: +### 4. HTTP 日志 API 查询/推送 +- POST `/logs/log` 发送日志(需带鉴权 header) +- GET `/logs/query` 查询历史日志 +- GET `/logs/stream` SSE 实时流式日志 +- GET `/logs/health` 健康检查 -```bash -# 安装 websocat -brew install websocat # macOS -apt-get install websocat # Ubuntu +## 架构图 -# 连接到 WebSocket 端点 -websocat ws://localhost:3000/ws - -# 此时终端将显示接收到的日志消息 +``` +客户端 <---Socket.IO---> WS 日志服务 <---Socket.IO---> Logger (WS Transport) + | | + | | + +-------------------HTTP----------------------+ + | + HTTP 日志服务 (API) + | + 日志文件/内存存储 ``` -## 部署与配置 - -提供详细的配置项文档,包括: - -- 日志级别设置 -- 文件轮转策略 -- WebSocket 连接配置 -- 格式化选项 +## 设计要点与实现细节 -## 后续扩展可能性 +- **多通道输出**:console/file 必须,ws/http 可选,均可单独启用/关闭。 +- **WebSocket 断线重连**:自动重连,消息队列缓存,恢复后补发。 +- **HTTP 动态 header**:每条日志可带不同 header,支持多租户/鉴权。 +- **文件写入安全**:自动创建目录,支持文件轮转与备份。 +- **API 兼容性**:logger 完全兼容 winston API,便于迁移。 +- **统一入口**:`createLogService` 一键集成所有能力。 +- **类型安全**:所有配置项、API、日志结构均有完整 TypeScript 类型定义。 -1. 添加更多传输渠道,如数据库存储、云日志服务等 -2. 支持日志分析和可视化工具集成 -3. 实现分布式日志收集与聚合 +## 后续扩展方向 +- 支持更多 transport(如数据库、云日志等) +- 日志分析与可视化集成 +- 分布式日志聚合 ## 结论 - -这个日志工具将提供与 Winston 完全兼容的 API,同时扩展了多通道输出能力,特别是实时 WebSocket 传输功能。通过合理的设计和实现,确保日志处理的可靠性、灵活性和高性能。 +本工具为 Node.js 应用提供了高性能、可扩展、易用的多通道日志采集与分发能力,适合现代微服务、云原生等多场景。 diff --git a/infra/logger/package.json b/infra/logger/package.json index 3dbd097d..aff0959f 100644 --- a/infra/logger/package.json +++ b/infra/logger/package.json @@ -19,7 +19,10 @@ "build:esm": "tsc -p tsconfig.build.esm.json && echo '{\"type\": \"module\"}' > dist/esm/package.json", "build:cjs": "tsc -p tsconfig.build.cjs.json && echo '{\"type\": \"commonjs\"}' > dist/lib/package.json", "prepare": "npm run build", - "lint": "eslint --ext .ts src" + "lint": "eslint --ext .ts src", + "test": "vitest", + "test:watch": "vitest --watch", + "test:coverage": "vitest run --coverage" }, "keywords": [ "winston", @@ -31,7 +34,9 @@ "author": "", "license": "MIT", "dependencies": { + "dotenv": "^16.5.0", "express": "^4.21.2", + "jsonwebtoken": "^9.0.2", "socket.io": "^4.7.2", "socket.io-client": "^4.7.2", "winston": "^3.11.0", @@ -40,14 +45,18 @@ "devDependencies": { "@types/express": "^4.17.21", "@types/jest": "^29.5.8", + "@types/jsonwebtoken": "^9.0.9", "@types/node": "^20.9.0", "@types/socket.io-client": "^3.0.0", + "@vitest/coverage-v8": "^3.2.3", "concurrently": "^7.6.0", + "eventsource": "^4.0.0", "http-server": "^14.1.1", "jest": "^29.7.0", "rimraf": "^5.0.10", "ts-jest": "^29.1.1", "ts-node": "^10.9.1", - "typescript": "^5.2.2" + "typescript": "^5.2.2", + "vitest": "^3.0.0" } -} +} \ No newline at end of file diff --git a/infra/logger/src/http/DynamicHttpTransport.ts b/infra/logger/src/http/DynamicHttpTransport.ts new file mode 100644 index 00000000..751dc681 --- /dev/null +++ b/infra/logger/src/http/DynamicHttpTransport.ts @@ -0,0 +1,37 @@ +import { transports } from "winston"; + +/** + * Winston transport that sends logs via HTTP with dynamic headers per log entry, using fetch. + */ +export default class DynamicHttpTransport extends transports.Http { + private getContext: () => { authorization?: string; xContainerId?: string }; + + constructor(option: transports.HttpTransportOptions & { getContext: () => { authorization?: string; xContainerId?: string } }) { + super(option); + this.getContext = option.getContext; + } + + async log(info: any, callback: () => void) { + setImmediate(() => this.emit("logged", info)); + + const { authorization, xContainerId } = this.getContext(); + + const headers: Record = { + "Content-Type": "application/json" + }; + if (authorization) headers["Authorization"] = authorization; + if (xContainerId) headers["x-container-id"] = xContainerId; + + try { + await fetch(`http://${this.host}:${this.port}${this.path}`, { + method: "POST", + headers, + body: JSON.stringify(info) + }); + } catch (e) { + // ignore error + } + + callback(); + } +} \ No newline at end of file diff --git a/infra/logger/src/http/__test__/logger.test.ts b/infra/logger/src/http/__test__/logger.test.ts new file mode 100644 index 00000000..2357d82d --- /dev/null +++ b/infra/logger/src/http/__test__/logger.test.ts @@ -0,0 +1,419 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { createLogger, type MultiChannelLogger } from '../../logger'; +import { createHttpLogServer as createLogServer } from '../server'; +import { LogEntry, LogLevel } from '../../types'; +import jwt from 'jsonwebtoken'; +import fetch from 'node-fetch'; +import { format } from 'winston'; +import { EventSource } from 'eventsource'; + + +const PORT = 31001; +const BASE_URL = `http://localhost:${PORT}/logs`; +const TRANSPORT_OPTION = { + level: 'info', + file: { filename: 'test-log.log' }, + http: { + enabled: true, + host: 'localhost', + port: PORT, + path: '/logs/log', + format: format.combine(format.timestamp()), + } +} + +const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + +/** + * Waits for the logger server to be ready by polling the /health endpoint. + * Retries up to maxRetries times, waiting 100ms between attempts. + * Throws an error if the server does not become ready in time. + * @param baseUrl - The base URL of the server (e.g., http://localhost:31001) + * @param maxRetries - Maximum number of retries (default: 5) + */ +async function waitForServerHealth(baseUrl: string, maxRetries = 5) { + let retries = 0; + while (retries < maxRetries) { + try { + const response = await fetch(`${baseUrl}/health`); + if (response.ok) { + return; + } + } catch (error) { + // ignore + } + retries++; + if (retries === maxRetries) { + throw new Error('Server failed to become ready after multiple attempts'); + } + await new Promise(resolve => setTimeout(resolve, 300)); + } +} + +// Helper to generate a fake JWT for test user +/** + * Generates a JWT token for testing purposes using RS256 algorithm + * @param userId - The user ID to include in the token payload + * @returns A signed JWT token string + */ +function generateTestJWT(userId: string): string { + const tokenSecret = process.env.PRIVATE_KEY?.replace(/\\n/g, "\n"); + + if (!tokenSecret) { + throw new Error('TOKEN_SECRET environment variable is required for JWT generation'); + } + + return jwt.sign( + { id: userId, name: 'test-user' }, + tokenSecret, + { algorithm: 'RS256' } + ); +} + +// Helper to generate a test sessionId, matching getSessionId logic +function getTestSessionId(userId: string, containerId?: string): string | undefined { + if (!userId) return undefined; + return containerId ? `${userId}:${containerId}` : userId; +} + +const TEST_USER_ID = 'test-user-id'; +const TEST_CONTAINER_ID = 'test-container-123'; +const AUTH_HEADER = `Bearer ${generateTestJWT(TEST_USER_ID)}`; +const CONTAINER_HEADER = { 'x-container-id': TEST_CONTAINER_ID }; +const AUTH_HEADERS = { ...CONTAINER_HEADER, 'Authorization': AUTH_HEADER }; + +// Helper for fetch with headers +async function fetchWithHeaders(url: string, options: Record = {}) { + options.headers = { ...AUTH_HEADERS, ...(options.headers || {}) }; + return fetch(url, options); +} + +// Helper for fetch with custom user/container +async function fetchWithCustomHeaders(url: string, userId: string, containerId?: string, options: Record = {}) { + const headers = { + ...(containerId ? { 'x-container-id': containerId } : {}), + 'Authorization': `Bearer ${generateTestJWT(userId)}`, + ...(options.headers || {}) + }; + return fetch(url, { ...options, headers }); +} + +/** + * Test suite for the Logger Module, including Logger and LogService. + * Covers logging, querying, streaming, and session isolation. + */ +describe('Logger Module', () => { + /** + * Test cases for Logger, covering log levels, context, metadata, and session ID handling. + */ + describe('Logger', () => { + let logger: ReturnType; + + beforeEach(() => { + logger = createLogger(TRANSPORT_OPTION); + }); + + afterEach(() => { + logger.close(); + }); + + /** + * Should create a logger with default options. + */ + it('should create a logger with default options', () => { + expect(logger).toBeDefined(); + }); + + /** + * Should log messages with different levels. + */ + it('should log messages with different levels', () => { + const levels: LogLevel[] = ['error', 'warn', 'info', 'debug', 'verbose', 'silly']; + levels.forEach((level) => { + const message = `Test ${level} message`; + expect(() => (logger as any)[level](message)).not.toThrow(); + }); + }); + + /** + * Should include context and metadata in logs. + */ + it('should include context and metadata in logs', () => { + const context = { userId: '123', action: 'test' }; + const meta = { duration: 100, success: true }; + expect(() => { + logger.info('Test message', context, meta); + }).not.toThrow(); + }); + + /** + * Should use default session ID when not provided. + */ + it('should use default session ID when not provided', () => { + const logSpy = vi.spyOn(logger as any, 'info'); + logger.info('Test message'); + expect(logSpy).toHaveBeenCalledWith( + 'Test message', + ); + logSpy.mockRestore(); + }); + + /** + * Should override default session ID when provided. + */ + it('should override default session ID when provided', () => { + const logSpy = vi.spyOn(logger as any, 'log'); + logger.info('Test message', { sessionId: 'custom-session' }); + expect(logSpy).toHaveBeenCalledWith( + 'info', + 'Test message', + { sessionId: 'custom-session' }, + ); + logSpy.mockRestore(); + }); + }); + + /** + * Test cases for LogService, covering log ingestion, querying, streaming, clearing, and error handling. + */ + describe('LogService', () => { + let service: ReturnType; + let logger: MultiChannelLogger; + + beforeEach(async () => { + service = createLogServer({ + port: PORT, + debug: true, + }); + await waitForServerHealth(BASE_URL); + + // Create logger instance with HTTP transport enabled + logger = createLogger({ + ...TRANSPORT_OPTION, + http: { + ...TRANSPORT_OPTION.http, + getContext: () => ({ + xContainerId: TEST_CONTAINER_ID, + authorization: AUTH_HEADER + }) + } + }); + }); + + afterEach(async () => { + await service.server?.close(); + }); + + it('should ingest logs via POST /log with sessionId', async () => { + logger.info('Test log entry', { source: 'test' }); + await wait(1000); + const response = await fetchWithHeaders(`${BASE_URL}/query`); + const logs = await response.json() as any[]; + expect(logs).toHaveLength(1); + expect(logs[0]).toMatchObject({ + level: 'info', + message: 'Test log entry', + sessionId: getTestSessionId(TEST_USER_ID, TEST_CONTAINER_ID), + source: 'test', + }); + }); + + it('should return 200 if x-container-id header is missing', async () => { + const response = await fetch(`${BASE_URL}/query`, { headers: { Authorization: AUTH_HEADER } }); + expect(response.status).toBe(200); + }); + + it('should return 401 if Authorization header is missing', async () => { + const response = await fetch(`${BASE_URL}/query`, { headers: { 'x-container-id': TEST_CONTAINER_ID } }); + expect(response.status).toBe(401); + }); + + it('should isolate logs by sessionId (userId+containerId)', async () => { + await(2000) + // Create two loggers with different contexts + const logger1 = createLogger({ + ...TRANSPORT_OPTION, + http: { + ...TRANSPORT_OPTION.http, + getContext: () => ({ + xContainerId: TEST_CONTAINER_ID, + authorization: AUTH_HEADER + }) + } + }); + + const otherContainerId = 'other-container'; + const logger2 = createLogger({ + ...TRANSPORT_OPTION, + http: { + ...TRANSPORT_OPTION.http, + getContext: () => ({ + xContainerId: otherContainerId, + authorization: AUTH_HEADER + }) + } + }); + + // Send logs using different loggers + logger1.info('Session 1 log', { source: 'test' }); + await wait(2000); + logger2.info('Other session log', { source: 'test' }); + + // Query logs for session 1 + const resp1 = await fetch(`${BASE_URL}/query`, { + headers: { + 'x-container-id': TEST_CONTAINER_ID, + 'Authorization': AUTH_HEADER + } + }); + const logs1 = await resp1.json() as any[]; + expect(logs1).toHaveLength(1); + expect(logs1[0].sessionId).toBe(getTestSessionId(TEST_USER_ID, TEST_CONTAINER_ID)); + + // Query logs for session 2 + const resp2 = await fetch(`${BASE_URL}/query`, { + headers: { + 'x-container-id': otherContainerId, + 'Authorization': AUTH_HEADER + } + }); + const logs2 = await resp2.json() as any[]; + expect(logs2).toHaveLength(1); + expect(logs2[0].sessionId).toBe(getTestSessionId(TEST_USER_ID, otherContainerId)); + }); + + it('should clear logs only for current session', async () => { + logger.info('Test log for clearing', { sessionId: getTestSessionId(TEST_USER_ID, TEST_CONTAINER_ID), source: 'test' }); + const clearResp = await fetchWithHeaders(`${BASE_URL}/clear`, { method: 'POST' }); + expect(clearResp.status).toBe(200); + const queryResp = await fetchWithHeaders(`${BASE_URL}/query`); + const logs = await queryResp.json() as any[]; + expect(logs).toHaveLength(0); + }); + + it('should not clear logs for other session', async () => { + logger.info('Session 1 log', { sessionId: getTestSessionId(TEST_USER_ID, TEST_CONTAINER_ID), source: 'test' }); + await wait(2000); + const otherUserId = 'other-user'; + const otherContainerId = 'other-container'; + const otherSessionId = getTestSessionId(otherUserId, otherContainerId); + await fetchWithCustomHeaders(`${BASE_URL}/log`, otherUserId, otherContainerId, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ level: 'info', message: 'Other session log', sessionId: otherSessionId, source: 'test' }) + }); + await fetchWithHeaders(`${BASE_URL}/clear`, { method: 'POST' }); + const resp2 = await fetchWithCustomHeaders(`${BASE_URL}/query`, otherUserId, otherContainerId); + const logs2 = await resp2.json() as any[]; + expect(logs2).toHaveLength(1); + expect(logs2[0].sessionId).toBe(getTestSessionId(otherUserId, otherContainerId)); + }); + + it('should stream logs for correct session only', async () => { + // First send the log + logger.info('Stream test log', { sessionId: getTestSessionId(TEST_USER_ID, TEST_CONTAINER_ID), source: 'test' }); + await wait(1000); // Wait for log to be processed + + // Setup fetch request with stream + const response = await fetch(`${BASE_URL}/stream`, { + headers: AUTH_HEADERS + }); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + if (!response.body) { + throw new Error('No response body available'); + } + + const logPromise = new Promise((resolve, reject) => { + if (!response.body) { + reject(new Error('No response body available')); + return; + } + + response.body.on('data', (chunk: Buffer) => { + // Convert Buffer to string and split by double newlines + const text = chunk.toString(); + const events = text.split('\n\n').filter(Boolean); + + for (const event of events) { + if (event.startsWith('data: ')) { + const log = JSON.parse(event.slice(6)); + resolve(log); + return; + } + } + }); + + response.body.on('error', (error) => { + reject(error); + }); + + response.body.on('end', () => { + // Stream ended without finding log + reject(new Error('Stream ended without finding log')); + }); + }); + + // Add timeout to prevent test hanging + const timeoutPromise = new Promise((_, reject) => { + setTimeout(() => reject(new Error('Test timeout')), 5000); + }); + + const receivedLog = await Promise.race([logPromise, timeoutPromise]); + + expect(receivedLog).toMatchObject({ + level: 'info', + message: 'Stream test log', + sessionId: getTestSessionId(TEST_USER_ID, TEST_CONTAINER_ID), + }); + }); + }); + + describe('Session Isolation', () => { + let service: ReturnType; + + beforeEach(async () => { + service = createLogServer({ + port: PORT, + debug: false, + }); + await waitForServerHealth(BASE_URL); + }); + + afterEach(async () => { + await service.server?.close(); + }); + + it('should maintain separate log streams for different sessions', async () => { + const session1User = 'user1'; + const session1Container = 'container1'; + const session2User = 'user2'; + const session2Container = 'container2'; + // session1 + await fetchWithCustomHeaders(`${BASE_URL}/log`, session1User, session1Container, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ level: 'info', message: 'Session 1 log', sessionId: getTestSessionId(session1User, session1Container), source: 'test' }) + }); + // session2 + await fetchWithCustomHeaders(`${BASE_URL}/log`, session2User, session2Container, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ level: 'info', message: 'Session 2 log', sessionId: getTestSessionId(session2User, session2Container), source: 'test' }) + }); + // 查询 session1 + const resp1 = await fetchWithCustomHeaders(`${BASE_URL}/query`, session1User, session1Container); + const logs1 = await resp1.json() as any[]; + expect(logs1).toHaveLength(1); + expect(logs1[0].message).toBe('Session 1 log'); + // 查询 session2 + const resp2 = await fetchWithCustomHeaders(`${BASE_URL}/query`, session2User, session2Container); + const logs2 = await resp2.json() as any[]; + expect(logs2).toHaveLength(1); + expect(logs2[0].message).toBe('Session 2 log'); + }); + }); +}); \ No newline at end of file diff --git a/infra/logger/src/http/__test__/storage-memory.test.ts b/infra/logger/src/http/__test__/storage-memory.test.ts new file mode 100644 index 00000000..150e4395 --- /dev/null +++ b/infra/logger/src/http/__test__/storage-memory.test.ts @@ -0,0 +1,448 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest'; +import { MemoryLoggerStorage } from '../storage-memory'; +import { LogEntry } from '../types'; + +/** + * Test suite for MemoryLoggerStorage, covering all public methods and edge cases. + */ +describe('MemoryLoggerStorage', () => { + let storage: MemoryLoggerStorage; + + beforeEach(() => { + storage = new MemoryLoggerStorage(); + }); + + afterEach(async () => { + await storage.close(); + }); + + /** + * Test the cursor parameter in both query and stream methods. + * Ensures that logs are correctly filtered and streamed starting from the given cursor index. + */ + describe('cursor functionality', () => { + const sessionId = 'cursor-test-session'; + const entries: LogEntry[] = [ + { + timestamp: new Date().toISOString(), + level: 'info', + message: 'Log 1', + sessionId, + source: 'test', + }, + { + timestamp: new Date().toISOString(), + level: 'info', + message: 'Log 2', + sessionId, + source: 'test', + }, + { + timestamp: new Date().toISOString(), + level: 'info', + message: 'Log 3', + sessionId, + source: 'test', + }, + ]; + + beforeEach(async () => { + // Write test logs + for (const entry of entries) { + await storage.write(entry); + } + }); + + /** + * Should query logs with cursor parameter, returning only logs after the cursor. + */ + it('should query logs with cursor parameter', async () => { + // Query all logs first + const allLogs = await storage.query({ sessionId }); + expect(allLogs).toHaveLength(3); + + // Query with cursor after first log + const cursor = allLogs[0].index!; + const remainingLogs = await storage.query({ sessionId, cursor }); + expect(remainingLogs).toHaveLength(2); + expect(remainingLogs[0].message).toBe('Log 2'); + expect(remainingLogs[1].message).toBe('Log 3'); + }); + + /** + * Should stream logs with cursor parameter, yielding only logs after the cursor. + */ + it('should stream logs with cursor parameter', async () => { + // Get all logs first to get the cursor + const allLogs = await storage.query({ sessionId }); + const cursor = allLogs[0].index!; + + // Start streaming from cursor + const streamedLogs: LogEntry[] = []; + const stream = storage.stream({ sessionId, cursor }); + + // Collect logs from stream + for await (const log of stream) { + streamedLogs.push(log); + if (streamedLogs.length === 2) break; // We expect 2 logs after cursor + } + + expect(streamedLogs).toHaveLength(2); + expect(streamedLogs[0].message).toBe('Log 2'); + expect(streamedLogs[1].message).toBe('Log 3'); + }); + + /** + * Should handle cursor with no matching logs, returning empty results. + */ + it('should handle cursor with no matching logs', async () => { + // Query with a cursor that's beyond all logs + const allLogs = await storage.query({ sessionId }); + const highCursor = allLogs[allLogs.length - 1].index! + 100; + + // Test query + const queryResult = await storage.query({ sessionId, cursor: highCursor }); + expect(queryResult).toHaveLength(0); + + // Test stream + const streamedLogs: LogEntry[] = []; + const stream = storage.stream({ sessionId, cursor: highCursor }); + + // Try to collect logs for a short time + const timeoutPromise = new Promise(resolve => setTimeout(resolve, 100)); + for await (const log of stream) { + streamedLogs.push(log); + if (streamedLogs.length > 0) break; + } + await timeoutPromise; + + expect(streamedLogs).toHaveLength(0); + }); + }); + + /** + * Test the limit parameter in both query and stream methods. + * Ensures that logs are correctly limited in number according to the limit parameter. + */ + describe('limit parameter', () => { + const sessionId = 'limit-test-session'; + const entries: LogEntry[] = [ + { + timestamp: new Date().toISOString(), + level: 'info', + message: 'Log 1', + sessionId, + source: 'test', + }, + { + timestamp: new Date().toISOString(), + level: 'info', + message: 'Log 2', + sessionId, + source: 'test', + }, + { + timestamp: new Date().toISOString(), + level: 'info', + message: 'Log 3', + sessionId, + source: 'test', + }, + { + timestamp: new Date().toISOString(), + level: 'info', + message: 'Log 4', + sessionId, + source: 'test', + }, + ]; + + beforeEach(async () => { + // Write test logs + for (const entry of entries) { + await storage.write(entry); + } + }); + + /** + * Should limit the number of logs returned by query to the specified limit. + */ + it('should limit logs returned by query', async () => { + const logs = await storage.query({ sessionId, limit: 2 }); + expect(logs).toHaveLength(2); + // Should be the last two logs (slice from end) + expect(logs[0].message).toBe('Log 3'); + expect(logs[1].message).toBe('Log 4'); + }); + + /** + * Should return all logs if limit exceeds available logs in query. + */ + it('should return all logs if limit exceeds available logs in query', async () => { + const logs = await storage.query({ sessionId, limit: 10 }); + expect(logs).toHaveLength(4); + }); + + /** + * Should return no logs if limit is 0 in query (even if there are logs). + */ + it('should return no logs if limit is 0 in query (with logs)', async () => { + const logs = await storage.query({ sessionId, limit: 0 }); + expect(logs).toHaveLength(0); + }); + + /** + * Should return no logs if limit is 0 in query (when there are no logs). + */ + it('should return no logs if limit is 0 in query (no logs)', async () => { + const emptyStorage = new MemoryLoggerStorage(); + const logs = await emptyStorage.query({ sessionId, limit: 0 }); + expect(logs).toHaveLength(0); + }); + + /** + * Should limit the number of logs yielded by stream to the specified limit. + */ + it('should limit logs yielded by stream', async () => { + const streamedLogs: LogEntry[] = []; + const stream = storage.stream({ sessionId, limit: 2 }); + for await (const log of stream) { + streamedLogs.push(log); + if (streamedLogs.length === 2) break; + } + expect(streamedLogs).toHaveLength(2); + // Should be the first two logs after cursor (default cursor is 0, so index > 0) + expect(streamedLogs[0].message).toBe('Log 1'); + expect(streamedLogs[1].message).toBe('Log 2'); + }); + + /** + * Should yield all logs if limit exceeds available logs in stream. + */ + it('should yield all logs if limit exceeds available logs in stream', async () => { + const streamedLogs: LogEntry[] = []; + const stream = storage.stream({ sessionId, limit: 10 }); + for await (const log of stream) { + streamedLogs.push(log); + if (streamedLogs.length === 4) break; + } + expect(streamedLogs).toHaveLength(4); + }); + + /** + * Should yield no logs if limit is 0 in stream (even if there are logs). + */ + it('should yield no logs if limit is 0 in stream (with logs)', async () => { + const streamedLogs: LogEntry[] = []; + const stream = storage.stream({ sessionId, limit: 0 }); + for await (const log of stream) { + streamedLogs.push(log); + } + expect(streamedLogs).toHaveLength(0); + }); + + /** + * Should yield no logs if limit is 0 in stream (when there are no logs). + */ + it('should yield no logs if limit is 0 in stream (no logs)', async () => { + const emptyStorage = new MemoryLoggerStorage(); + const streamedLogs: LogEntry[] = []; + const stream = emptyStorage.stream({ sessionId, limit: 0 }); + for await (const log of stream) { + streamedLogs.push(log); + } + expect(streamedLogs).toHaveLength(0); + }); + + /** + * Should combine cursor and limit in stream to yield correct logs. + */ + it('should combine cursor and limit in stream', async () => { + // Get all logs to find a cursor + const allLogs = await storage.query({ sessionId }); + const cursor = allLogs[1].index!; // After Log 2 + const streamedLogs: LogEntry[] = []; + const stream = storage.stream({ sessionId, cursor, limit: 1 }); + for await (const log of stream) { + streamedLogs.push(log); + if (streamedLogs.length === 1) break; + } + expect(streamedLogs).toHaveLength(1); + expect(streamedLogs[0].message).toBe('Log 3'); + }); + }); + + /** + * Should write and query logs for different sessions, ensuring session isolation. + */ + it('should isolate logs by session', async () => { + const entry1: LogEntry = { + timestamp: new Date().toISOString(), + level: 'info', + message: 'Session 1 log', + sessionId: 'session-1', + source: 'test', + }; + const entry2: LogEntry = { + timestamp: new Date().toISOString(), + level: 'info', + message: 'Session 2 log', + sessionId: 'session-2', + source: 'test', + }; + await storage.write(entry1); + await storage.write(entry2); + const logs1 = await storage.query({ sessionId: 'session-1' }); + const logs2 = await storage.query({ sessionId: 'session-2' }); + expect(logs1).toHaveLength(1); + expect(logs1[0].message).toBe('Session 1 log'); + expect(logs2).toHaveLength(1); + expect(logs2[0].message).toBe('Session 2 log'); + }); + + /** + * Should enforce per-session memory cap and evict oldest logs. + */ + it('should evict oldest logs when exceeding memory cap', async () => { + const sessionId = 'evict-session'; + // Each log is about 1KB, so 6000 logs will exceed 5MB + for (let i = 0; i < 6000; i++) { + await storage.write({ + timestamp: new Date().toISOString(), + level: 'info', + message: `Log ${i}`, + sessionId, + source: 'test', + }); + } + const logs = await storage.query({ sessionId }); + // Should not exceed 6000, and should be less than or equal to the cap + expect(logs.length).toBeLessThanOrEqual(6000); + // The first log should not be 'Log 0' (evicted) + expect(logs[0].message).not.toBe('Log 0'); + }); + + /** + * Should return an empty array when querying a non-existent session. + */ + it('should return empty array for non-existent session', async () => { + const logs = await storage.query({ sessionId: 'no-session' }); + expect(logs).toEqual([]); + }); + + /** + * Should clear logs for a specific session. + */ + it('should clear logs for a specific session', async () => { + const sessionId = 'clear-session'; + await storage.write({ + timestamp: new Date().toISOString(), + level: 'info', + message: 'To be cleared', + sessionId, + source: 'test', + }); + await storage.clear(sessionId); + const logs = await storage.query({ sessionId }); + expect(logs).toEqual([]); + }); + + /** + * Should clear all sessions when no sessionId is provided. + */ + it('should clear all sessions', async () => { + await storage.write({ + timestamp: new Date().toISOString(), + level: 'info', + message: 'Session 1', + sessionId: 's1', + source: 'test', + }); + await storage.write({ + timestamp: new Date().toISOString(), + level: 'info', + message: 'Session 2', + sessionId: 's2', + source: 'test', + }); + await storage.clear(); + const logs1 = await storage.query({ sessionId: 's1' }); + const logs2 = await storage.query({ sessionId: 's2' }); + expect(logs1).toEqual([]); + expect(logs2).toEqual([]); + }); + + /** + * Should assign default sessionId if not provided. + */ + it('should use default sessionId if not provided', async () => { + await storage.write({ + timestamp: new Date().toISOString(), + level: 'info', + message: 'Default session', + sessionId: undefined as any, // simulate missing sessionId + source: 'test', + }); + const logs = await storage.query({}); + expect(logs.length).toBe(1); + expect(logs[0].message).toBe('Default session'); + }); + + /** + * Should filter logs by level, source, since, until, and limit. + */ + it('should filter logs by level, source, since, until, and limit', async () => { + const now = new Date(); + const logsData: LogEntry[] = [ + { + timestamp: new Date(now.getTime() - 10000).toISOString(), + level: 'info', + message: 'Old info', + sessionId: 'filter-session', + source: 'src1', + }, + { + timestamp: new Date(now.getTime() - 5000).toISOString(), + level: 'error', + message: 'Recent error', + sessionId: 'filter-session', + source: 'src2', + }, + { + timestamp: now.toISOString(), + level: 'info', + message: 'Newest info', + sessionId: 'filter-session', + source: 'src1', + }, + ]; + for (const entry of logsData) { + await storage.write(entry); + } + // Filter by level + let logs = await storage.query({ sessionId: 'filter-session', level: 'error' }); + expect(logs).toHaveLength(1); + expect(logs[0].message).toBe('Recent error'); + // Filter by source + logs = await storage.query({ sessionId: 'filter-session', source: 'src1' }); + expect(logs).toHaveLength(2); + // Filter by since + logs = await storage.query({ sessionId: 'filter-session', since: now.toISOString() }); + expect(logs).toHaveLength(1); + expect(logs[0].message).toBe('Newest info'); + // Filter by until + logs = await storage.query({ sessionId: 'filter-session', until: new Date(now.getTime() - 6000).toISOString() }); + expect(logs).toHaveLength(1); + expect(logs[0].message).toBe('Old info'); + // Filter by limit + logs = await storage.query({ sessionId: 'filter-session', limit: 2 }); + expect(logs).toHaveLength(2); + }); + + /** + * Should not throw when closing an empty storage. + */ + it('should not throw when closing empty storage', async () => { + await expect(storage.close()).resolves.not.toThrow(); + }); +}); \ No newline at end of file diff --git a/infra/logger/src/http/index.ts b/infra/logger/src/http/index.ts new file mode 100644 index 00000000..c6804edc --- /dev/null +++ b/infra/logger/src/http/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export { createHttpLogServer } from './server'; +export { MemoryLoggerStorage } from './storage-memory'; diff --git a/infra/logger/src/http/middlewares.ts b/infra/logger/src/http/middlewares.ts new file mode 100644 index 00000000..6e9f510b --- /dev/null +++ b/infra/logger/src/http/middlewares.ts @@ -0,0 +1,54 @@ +import { Request, Response, NextFunction } from 'express'; +import jwt, { JwtPayload, VerifyErrors } from 'jsonwebtoken'; +import dotenv from 'dotenv'; + +dotenv.config(); + +/** + * 用户 JWT Payload 类型定义 + */ +export interface UserPayload extends JwtPayload { + name: string; + id: string; +} + +/** + * JWT 认证中间件,解析 Authorization header 并将用户信息挂载到 req.user + * @param req Express request + * @param res Express response + * @param next Express next + */ +export function jwtAuthMiddleware(req: Request, res: Response, next: NextFunction) { + const authHeader = req.headers['authorization']; + if (!authHeader || !authHeader.startsWith('Bearer ')) { + return res.status(401).json({ error: 'Missing or invalid Authorization header' }); + } + const token = authHeader.slice(7); // Remove 'Bearer ' + const secret = process.env.TOKEN_SECRET?.replace(/\\n/g, "\n"); + if (!secret) { + return res.status(500).json({ error: 'Server misconfiguration: TOKEN_SECRET not set' }); + } + jwt.verify(token, secret, (err: VerifyErrors | null, decoded: JwtPayload | string | undefined) => { + if (err) { + return res.status(401).json({ error: 'Invalid or expired token' }); + } + (req as any).user = decoded as UserPayload; + next(); + }); +} + +/** + * 从 req.user.id 和可选的 req.containerId 生成 sessionId + * 如果有 containerId 则返回 userId:containerId 格式 + * 否则仅返回 userId + * @param req Express request + * @returns sessionId 或 undefined + */ +export function getSessionId(req: Request): string | undefined { + const userId = (req as any).user?.id; + if (!userId) { + return undefined; + } + const containerId = req.headers['x-container-id']; + return containerId ? `${userId}:${containerId}` : userId; +} \ No newline at end of file diff --git a/infra/logger/src/http/routers.ts b/infra/logger/src/http/routers.ts new file mode 100644 index 00000000..05c0345a --- /dev/null +++ b/infra/logger/src/http/routers.ts @@ -0,0 +1,288 @@ +import { Router } from 'express'; +import { LogEntry, LoggerQueryParams } from '../types'; +import { getSessionId, jwtAuthMiddleware } from './middlewares'; +import { MemoryLoggerStorage } from './storage-memory'; + +const router: Router = Router(); +const storage = new MemoryLoggerStorage(); + + +/** + * @openapi + * /logs/health: + * get: + * summary: Health check endpoint + * tags: + * - Logs + * responses: + * 200: + * description: Server is healthy + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * timestamp: + * type: string + * version: + * type: string + */ +router.get('/health', (req, res) => { + res.json({ + status: 'ok', + timestamp: new Date().toISOString(), + version: '1.0.0', + }); +}); + +// Apply JWT auth middleware to all routes +router.use(jwtAuthMiddleware); + +/** + * @openapi + * /logs/log: + * post: + * summary: Ingest a log entry + * tags: + * - Logs + * parameters: + * - in: header + * name: x-container-id + * required: false + * schema: + * type: string + * description: Container ID for log session isolation + * requestBody: + * required: true + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LogEntry' + * responses: + * 201: + * description: Log entry created + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/LogEntry' + * 400: + * description: Invalid log entry + */ +router.post('/log', async (req, res) => { + const entry = req.body as LogEntry; + if (!entry || !entry.level || !entry.message) { + return res.status(400).json({ error: 'Invalid log entry' }); + } + const sessionId = getSessionId(req); + if (!sessionId) { + return res.status(400).json({ error: 'Invalid or missing sessionId (userId or containerId missing)' }); + } + entry.sessionId = sessionId; + console.log('[HTTP Log Server] Ingesting log entry:', entry); + await storage.write(entry); + res.status(201).json(entry); +}); + +/** + * @openapi + * /logs/query: + * get: + * summary: Query logs with filters + * tags: + * - Logs + * parameters: + * - in: header + * name: x-container-id + * required: false + * schema: + * type: string + * description: Container ID for log session isolation + * - in: query + * name: level + * schema: + * type: string + * description: Log level filter + * - in: query + * name: source + * schema: + * type: string + * description: Log source filter + * - in: query + * name: since + * schema: + * type: string + * description: Start time (ISO8601) + * - in: query + * name: until + * schema: + * type: string + * description: End time (ISO8601) + * - in: query + * name: limit + * schema: + * type: integer + * description: Max number of logs + * - in: query + * name: sessionId + * schema: + * type: string + * description: Session ID filter + * responses: + * 200: + * description: List of log entries + * content: + * application/json: + * schema: + * type: array + * items: + * $ref: '#/components/schemas/LogEntry' + */ +router.get('/query', async (req, res) => { + const sessionId = getSessionId(req); + if (!sessionId) { + return res.status(400).json({ error: 'Invalid or missing sessionId (userId or containerId missing)' }); + } + const params: LoggerQueryParams = { + level: req.query.level as any, + source: req.query.source as string, + since: req.query.since as string, + until: req.query.until as string, + limit: req.query.limit ? parseInt(req.query.limit as string, 10) : undefined, + sessionId, + }; + const logs = await storage.query(params); + res.json(logs); +}); + +/** + * @openapi + * /logs/stream: + * get: + * summary: Stream logs as Server-Sent Events (SSE) + * tags: + * - Logs + * parameters: + * - in: header + * name: x-container-id + * required: false + * schema: + * type: string + * description: Container ID for log session isolation + * - in: query + * name: level + * schema: + * type: string + * description: Log level filter + * - in: query + * name: source + * schema: + * type: string + * description: Log source filter + * - in: query + * name: since + * schema: + * type: string + * description: Start time (ISO8601) + * - in: query + * name: until + * schema: + * type: string + * description: End time (ISO8601) + * - in: query + * name: sessionId + * schema: + * type: string + * description: Session ID filter + * responses: + * 200: + * description: SSE stream of log entries + * content: + * text/event-stream: + * schema: + * type: string + */ +router.get('/stream', async (req, res) => { + const sessionId = getSessionId(req); + if (!sessionId) { + res.status(400).json({ error: 'Invalid or missing sessionId (userId or containerId missing)' }); + return; + } + res.setHeader('Content-Type', 'text/event-stream'); + res.setHeader('Cache-Control', 'no-cache'); + res.setHeader('Connection', 'keep-alive'); + res.flushHeaders(); + + const params: LoggerQueryParams = { + level: req.query.level as any, + source: req.query.source as string, + since: req.query.since as string, + until: req.query.until as string, + sessionId, + }; + + let heartbeatCount = 0; + const MAX_HEARTBEATS = 3; + const HEARTBEAT_INTERVAL = 5000; + + const heartbeat = setInterval(() => { + heartbeatCount++; + if (heartbeatCount > MAX_HEARTBEATS) { + clearInterval(heartbeat); + res.end(); + return; + } + res.write(':heartbeat\n\n'); + }, HEARTBEAT_INTERVAL); + + try { + if (storage.stream) { + for await (const entry of storage.stream(params)) { + heartbeatCount = 0; + res.write(`data: ${JSON.stringify(entry)}\n\n`); + } + } + } finally { + clearInterval(heartbeat); + } + + req.on('close', () => clearInterval(heartbeat)); +}); + +/** + * @openapi + * /logs/clear: + * post: + * summary: Clear logs for current session + * tags: + * - Logs + * parameters: + * - in: header + * name: x-container-id + * required: false + * schema: + * type: string + * description: Container ID for log session isolation + * responses: + * 200: + * description: Logs cleared for current session + * 400: + * description: Invalid or missing sessionId + * 501: + * description: Clear not implemented + */ +router.post('/clear', async (req, res) => { + const sessionId = getSessionId(req); + if (!sessionId) { + return res.status(400).json({ error: 'Invalid or missing sessionId (userId or containerId missing)' }); + } + if (typeof storage.clear === 'function') { + await storage.clear(sessionId); + res.status(200).end(); + } else { + res.status(501).json({ error: 'Clear not implemented' }); + } +}); + +export { router as loggerRouter }; diff --git a/infra/logger/src/http/server.ts b/infra/logger/src/http/server.ts new file mode 100644 index 00000000..22243877 --- /dev/null +++ b/infra/logger/src/http/server.ts @@ -0,0 +1,45 @@ +import { loggerRouter } from './routers'; +import http from 'http'; +import express from 'express'; + +export interface HttpLogServerOptions { + port?: number; + debug?: boolean; +} + +/** + * Creates and configures an Express server for logging functionality + * @param options - Configuration options for the logger server + * @returns Configured Express application instance + */ +export function createHttpLogServer(options: HttpLogServerOptions = {}): { + server: http.Server | null; + app: express.Express; +} { + const { port = 4002, debug = true } = options; + const app = express(); + + // Enable JSON body parsing + app.use(express.json()); + + // Mount the logger router + app.use('/logs', loggerRouter); + + // Start the server if port is specified + if (port) { + const server = app.listen(port, '127.0.0.1', () => { + if (debug) { + console.log(`[HTTP Log Server] HTTP Logger server running on http://127.0.0.1:${port}`); + } + }); + return { + server, + app, + }; + } + + return { + server: null, + app, + }; +} diff --git a/infra/logger/src/http/storage-memory.ts b/infra/logger/src/http/storage-memory.ts new file mode 100644 index 00000000..56246e15 --- /dev/null +++ b/infra/logger/src/http/storage-memory.ts @@ -0,0 +1,162 @@ +import { LoggerStorage, LogEntry, LoggerQueryParams } from '../types'; + +const MAX_SESSION_BYTES = 5 * 1024 * 1024; // 5MB per session +const MAX_SESSION_LOGS = 1000; // Maximum number of logs per session + +interface SessionLogState { + logs: LogEntry[]; + totalBytes: number; + nextIndex: number; +} + +/** + * MemoryLoggerStorage is an in-memory implementation of LoggerStorage. + * It stores logs in memory, grouped by session, and enforces both a per-session memory cap + * and a maximum number of logs per session. + * Useful for development, testing, or ephemeral log storage. + */ +export class MemoryLoggerStorage implements LoggerStorage { + private sessions: Map = new Map(); + + /** + * Get or create the log state for a session. + * @param sessionId - The session identifier (defaults to '__default__') + * @returns The session's log state object + */ + private getSessionState(sessionId: string = '__default__'): SessionLogState { + if (!this.sessions.has(sessionId)) { + this.sessions.set(sessionId, { logs: [], totalBytes: 0, nextIndex: 1 }); + } + return this.sessions.get(sessionId)!; + } + + /** + * Write a log entry to the appropriate session, assigning a log index and enforcing memory limits. + * Evicts oldest logs when either the total bytes or number of logs exceeds the limit. + * @param entry - The log entry to store + */ + async write(entry: LogEntry): Promise { + const sessionId = entry.sessionId || '__default__'; + const state = this.getSessionState(sessionId); + // Assign index + entry.index = state.nextIndex++; + // Estimate log size (rough, for memory cap) + const logStr = JSON.stringify(entry); + const logBytes = Buffer.byteLength(logStr, 'utf8'); + state.logs.push(entry); + state.totalBytes += logBytes; + + // Enforce limits (evict oldest logs) + while ((state.totalBytes > MAX_SESSION_BYTES || state.logs.length > MAX_SESSION_LOGS) && state.logs.length > 0) { + const removed = state.logs.shift(); + if (removed) { + state.totalBytes -= Buffer.byteLength(JSON.stringify(removed), 'utf8'); + } + } + } + + /** + * Query logs for a session, supporting filtering by level, source, time, and limit. + * If limit is 0, returns an empty array. + * @param params - Query parameters + * @returns Array of matching log entries + */ + async query(params: LoggerQueryParams): Promise { + const sessionId = params.sessionId || '__default__'; + const state = this.sessions.get(sessionId); + if (!state) return []; + + // Check if cursor is out of bounds + if (typeof params.cursor === 'number' && params.cursor >= state.nextIndex) { + return []; + } + + let filtered = state.logs; + if (typeof params.cursor === 'number') { + filtered = filtered.filter(entry => (entry.index ?? 0) > params.cursor!); + } + if (params.level) filtered = filtered.filter(entry => entry.level === params.level); + if (params.source) filtered = filtered.filter(entry => entry.source === params.source); + if (params.since) filtered = filtered.filter(entry => entry.timestamp >= (params.since ?? '')); + if (params.until) filtered = filtered.filter(entry => entry.timestamp <= (params.until ?? '')); + if (typeof params.limit === 'number') { + if (params.limit === 0) return []; + // Return the last N logs after all filters (most recent N) + filtered = filtered.slice(-params.limit); + } + return filtered; + } + + /** + * Stream logs for a session as an async iterable, supporting cursor-based pagination. + * This allows for resuming log streaming from a specific point, useful for implementing + * continuous log streaming with reconnection support. + * If limit is 0, yields no logs. + * + * @param params - Query parameters including optional cursor for resuming from a specific log index + * @yields Log entries matching the query parameters + */ + async *stream(params: LoggerQueryParams = {}): AsyncIterable { + const sessionId = params.sessionId || '__default__'; + const state = this.sessions.get(sessionId); + if (!state) return; + + // Check if cursor is out of bounds + if (typeof params.cursor === 'number' && params.cursor >= state.nextIndex) { + return; + } + + // If limit is 0, yield nothing + if (typeof params.limit === 'number' && params.limit === 0) { + return; + } + + let currentIndex = typeof params.cursor === 'number' ? params.cursor : 0; + const logs = state.logs; + + while (true) { + // Find the next batch of logs starting from currentIndex + const nextBatch = logs.filter(entry => { + if ((entry.index ?? 0) <= currentIndex) return false; + if (params.level && entry.level !== params.level) return false; + if (params.source && entry.source !== params.source) return false; + if (params.since && entry.timestamp < params.since) return false; + if (params.until && entry.timestamp > params.until) return false; + return true; + }); + + // Yield each log entry in the batch + for (const entry of nextBatch) { + yield entry; + currentIndex = (entry.index ?? 0) + 1; + } + + // If we have a limit, stop after reaching it + if (typeof params.limit === 'number' && nextBatch.length >= params.limit) { + break; + } + + // Wait a bit before checking for new logs + await new Promise(resolve => setTimeout(resolve, 100)); + } + } + + /** + * Close the storage, clearing all sessions. + */ + async close() { + await this.clear(); + } + + /** + * Clear logs for a specific session or all sessions if sessionId is not provided. + * @param sessionId - Optional session identifier + */ + async clear(sessionId?: string): Promise { + if (sessionId) { + this.sessions.delete(sessionId); + } else { + this.sessions.clear(); + } + } +} \ No newline at end of file diff --git a/infra/logger/src/http/types.ts b/infra/logger/src/http/types.ts new file mode 100644 index 00000000..db1f1fd9 --- /dev/null +++ b/infra/logger/src/http/types.ts @@ -0,0 +1,68 @@ +// Logger types for infra/sandbox-v2/logger +import Transport from 'winston-transport'; +// 已迁移类型定义,若有本地私有类型可在此补充 + +/** + * LogLevel defines the supported log severity levels. + */ +export type LogLevel = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly'; + +/** + * LogEntry represents a single log message with optional context and metadata. + */ +export interface LogEntry { + timestamp: string; // ISO8601 + level: LogLevel; + message: string; + sessionId: string; // Session isolation support + source?: string; // e.g., service/component name + context?: Record; // Arbitrary structured context + meta?: Record; // Additional metadata + index?: number; // Monotonic index for log ordering (per session) +} + +/** + * LoggerStorage is an interface for log storage backends (memory, file, database, etc). + * Implementations must provide write() and query() methods, and may support streaming, close, and clear. + */ +export interface LoggerStorage { + write(entry: LogEntry): Promise; + query(params: LoggerQueryParams): Promise; + stream?(params: LoggerQueryParams): AsyncIterable; + close?(): Promise; + clear?(sessionId?: string): Promise; // Clear logs for a session or all if not provided +} + +/** + * LoggerQueryParams defines the parameters for querying or streaming logs. + */ +export interface LoggerQueryParams { + level?: LogLevel; + source?: string; + since?: string; // ISO8601 + until?: string; // ISO8601 + limit?: number; + sessionId?: string; // Session isolation support + cursor?: number; // Log index cursor for SSE reconnection +} + +/** + * LoggerClientOptions configures a LoggerClient instance. + */ +export interface LoggerClientOptions { + transports: Transport[]; + defaultLevel?: LogLevel; + source?: string; + service?: string; + defaultSessionId?: string; // Optional default session context +} + +/** + * LoggerServerOptions configures a logger server instance. + */ +export interface LoggerServerOptions { + storage: LoggerStorage; + transports?: Transport[]; // For broadcasting + port?: number; + debug?: boolean; +} \ No newline at end of file diff --git a/infra/logger/src/index.ts b/infra/logger/src/index.ts index 70dbc4f2..8e32adc6 100644 --- a/infra/logger/src/index.ts +++ b/infra/logger/src/index.ts @@ -1,3 +1,7 @@ -export * from "./logger"; -export * from "./log-server"; -export { default } from "./logger"; +export { createLogger } from './logger'; +export { createWsLogServer } from './ws/server'; +export { createHttpLogServer } from './http/server'; +import SocketIOTransport from './ws/transport'; +export { SocketIOTransport }; +export * from './types'; +export { createLogService } from './log-service'; diff --git a/infra/logger/src/log-server.ts b/infra/logger/src/log-server.ts deleted file mode 100644 index 2d03f5a8..00000000 --- a/infra/logger/src/log-server.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Log Socket Server - * - * A dedicated WebSocket server for receiving and broadcasting logs. - * This is integrated directly into the logger package to simplify setup. - * - * Architecture: - * - Logger -> Log Socket Server -> Connected clients - */ - -import http from 'http'; -import { Server as SocketIOServer } from 'socket.io'; - -/** - * Options for the log server - */ -export interface LogServerOptions { - /** The port for the WebSocket server to listen on */ - port?: number; - /** Enable or disable debug console output from the server */ - debug?: boolean; -} - -/** - * Creates and starts a dedicated WebSocket server for log broadcasting - * - * @param options Configuration options for the server - * @returns Object containing the server instance and URL - */ -export function createLogServer(options: LogServerOptions = {}) { - const port = options.port || 3001; - const debug = options.debug !== false; - - // Create a standalone HTTP server for Socket.IO - const server = http.createServer(); - const io = new SocketIOServer(server, { - cors: { - origin: '*', // Allow connections from any origin - methods: ['GET', 'POST'], // Allow these HTTP methods for CORS preflight - }, - }); - - // Handle client connections - io.on('connection', socket => { - if (debug) { - // console.log(`[Log Server] Client connected: ${socket.id}`); - } - - // Listen for incoming log events from loggers - socket.on('log', logEntry => { - // Debug output to show received logs - if (debug) { - console.log(`[Log Server] Received log: ${JSON.stringify(logEntry)}`); - } - - // Broadcast the log to ALL connected clients (including sender) - // This ensures logs are delivered even in PM2 environment - io.emit('log', logEntry); - }); - - // Handle client disconnection - socket.on('disconnect', () => { - if (debug) { - // console.log(`[Log Server] Client disconnected: ${socket.id}`); - } - }); - }); - - // Start the WebSocket server - server.listen(port); - - if (debug) { - console.log(`[Log Server] WebSocket server running on http://127.0.0.1:${port}`); - } - - // Return server information - return { - server: io, - url: `http://127.0.0.1:${port}`, - port, - close: () => server.close(), - }; -} - -export default createLogServer; diff --git a/infra/logger/src/log-service.ts b/infra/logger/src/log-service.ts new file mode 100644 index 00000000..50328ece --- /dev/null +++ b/infra/logger/src/log-service.ts @@ -0,0 +1,55 @@ +import { createLogger } from './logger'; +import { createWsLogServer } from './ws/server'; +import { createHttpLogServer } from './http/server'; +import { LogServiceConfig } from './types'; + +/** + * Create a unified log service: logger + ws server + http server + * @param config LogServiceConfig + * @returns { logger, wsServer, httpServer } + */ +export function createLogService(config: LogServiceConfig) { + // 1. 创建 logger(console/file 必须,ws/http 可选) + const logger = createLogger({ + level: config.level, + file: { + filename: config.logFilePath, + maxsize: 5242880, // 5MB + maxFiles: 5 + }, + console: { + // 可自定义 format 或其他 ConsoleTransportOptions + }, + ws: config.ws?.enabled && config.ws.port ? { + enabled: true, + url: `http://localhost:${config.ws.port}`, + level: config.level + } : undefined, + http: config.http?.enabled && config.http.port ? { + enabled: true, + host: config.http.host || '127.0.0.1', + port: config.http.port, + path: config.http.path || '/logs/log', + level: config.level, + getContext: config.http.getContext + } : undefined + }); + + // 2. 创建 WS server + let wsServer; + if (config.ws?.enabled) { + wsServer = createWsLogServer({ port: config.ws.port }); + } + + // 3. 创建 HTTP server + let httpServer; + if (config.http?.enabled) { + httpServer = createHttpLogServer({ port: config.http.port }); + } + + return { + logger, + wsServer, + httpServer, + }; +} diff --git a/infra/logger/src/logger.ts b/infra/logger/src/logger.ts index 9bfe379c..7eaff381 100644 --- a/infra/logger/src/logger.ts +++ b/infra/logger/src/logger.ts @@ -1,169 +1,98 @@ -import winston, { createLogger, format, transports } from "winston"; +import { createLogger as winstonCreateLogger, format, transports } from "winston"; import { MultiChannelLogger, MultiChannelLoggerOptions } from "./types"; -import SocketIOTransport from "./transports/socket-io"; -import { Socket } from "socket.io-client"; +import SocketIOTransport from "./ws/transport"; import path from "path"; import fs from "fs"; -import { createLogServer } from "./log-server"; - -/** - * Creates a multi-channel logger instance that can output to console, file, and WebSocket - * - * The logger maintains Winston's original API while adding WebSocket capabilities. - * - * @param options Configuration options for the logger - * @returns MultiChannelLogger instance that extends Winston Logger - */ -export function createMultiChannelLogger( - options: MultiChannelLoggerOptions = {} -): MultiChannelLogger { - // Default format: timestamp + JSON format - const defaultFormat = format.combine(format.timestamp(), format.json()); - - // Default configuration options - const defaultOptions: MultiChannelLoggerOptions = { - level: "info", - format: options.format || defaultFormat, - transports: [] - }; - - // Merge provided options with defaults - const loggerOptions = { ...defaultOptions, ...options }; - - // Add Console Transport if no transports specified - if ( - !loggerOptions.transports || - (Array.isArray(loggerOptions.transports) && - loggerOptions.transports.length === 0) - ) { - loggerOptions.transports = [new transports.Console()]; - } - - // Create Winston Logger instance and cast to our extended interface - const logger = createLogger(loggerOptions) as MultiChannelLogger; - - // Initialize socket property as null - logger.socket = null; - - // Add Socket.IO Transport if URL is provided - if (options.socketIO?.url) { - const socketTransport = new SocketIOTransport(options.socketIO); - logger.add(socketTransport); - // Store socket instance for external access (useful for manual connections/disconnections) - logger.socket = socketTransport["socket"]; - } - - return logger; -} +import TransportStream from "winston-transport"; +import DynamicHttpTransport from "./http/DynamicHttpTransport"; +const DEFAULT_FILE_MAX_SIZE = 5242880; // 5MB +const DEFAULT_FILE_MAX_FILES = 5; /** * Ensures the directory for a log file exists, creating it if necessary * @param filePath Path to the log file */ function ensureDirectoryExists(filePath: string): void { const dirname = path.dirname(filePath); - if (fs.existsSync(dirname)) { - return; + if (!fs.existsSync(dirname)) { + fs.mkdirSync(dirname, { recursive: true }); } - fs.mkdirSync(dirname, { recursive: true }); } /** - * Creates a multi-channel logger with sensible defaults for console, file, and WebSocket output + * Creates a multi-channel logger instance that can output to console, file, WebSocket, and HTTP * - * This is a convenience function that configures all three output channels at once. + * The logger maintains Winston's original API while adding WebSocket capabilities. * - * @param logFilePath Path to the log file (optional, defaults to '/tmp/logs/app.log') - * @param socketUrl Socket.IO server URL (optional, if not provided WebSocket transport is disabled) - * @returns MultiChannelLogger instance + * @param options Configuration options for the logger + * @returns MultiChannelLogger instance that extends Winston Logger */ -export function createDefaultLogger( - logFilePath: string = "/tmp/logs/app.log", - socketUrl?: string +export function createLogger( + options: MultiChannelLoggerOptions ): MultiChannelLogger { - // Ensure log directory exists - ensureDirectoryExists(logFilePath); - - const options: MultiChannelLoggerOptions = { - level: "info", - format: format.combine( - format.timestamp(), - format.printf(({ timestamp, level, message, ...rest }) => { - const restString = Object.keys(rest).length ? JSON.stringify(rest) : ""; - return `${timestamp} ${level}: ${message} ${restString}`; - }) - ), - transports: [ - // Console output with colorized format - new winston.transports.Console({ - format: format.combine( - format.colorize(), - format.printf(({ timestamp, level, message, ...rest }) => { - const restString = Object.keys(rest).length - ? JSON.stringify(rest) - : ""; - return `${timestamp} ${level}: ${message} ${restString}`; - }) - ) - }), - // File output with rotation settings - new winston.transports.File({ - filename: logFilePath, - maxsize: 5242880, // 5MB - maxFiles: 5 - }) - ] - }; - - // Add Socket.IO configuration if URL is provided - if (socketUrl) { - options.socketIO = { - url: socketUrl, - level: "info", - reconnection: true - }; + // 创建公共的format配置 + const commonFormat = format.combine( + format.timestamp(), + format.simple() + ); + + // 1. Console transport (always enabled) + const consoleTransport = new transports.Console({ + format: format.combine(format.colorize(), commonFormat), + ...options.console, + }); + + // 2. File transport (always enabled, filename 必须) + if (!options.file?.filename) { + throw new Error("File transport requires a filename (log file path). Please provide file.filename in options."); + } + ensureDirectoryExists(options.file.filename); + const fileTransport = new transports.File({ + format: commonFormat, + maxsize: Number(process.env.LOG_FILE_MAX_SIZE) || DEFAULT_FILE_MAX_SIZE, + maxFiles: Number(process.env.LOG_FILE_MAX_FILES) || DEFAULT_FILE_MAX_FILES, + ...options.file, + }); + + // 3. 组装 transports,类型为 TransportStream[] + const loggerTransports: TransportStream[] = [consoleTransport, fileTransport]; + + // 4. WS transport(可选) + let socket: any = null; + if (options.ws?.enabled && options.ws.url) { + const wsTransport = new SocketIOTransport({ + ...options.ws, + format: commonFormat + }); + loggerTransports.push(wsTransport as unknown as TransportStream); + socket = (wsTransport as any).socket; } - return createMultiChannelLogger(options); -} + // 5. HTTP transport(可选) + if (options.http?.enabled) { + loggerTransports.push(new DynamicHttpTransport({ + ...options.http, + format: commonFormat, + getContext: options.http.getContext || (() => ({})) + })); + } -/** - * Creates both a log server and a logger that connects to it - * - * This is a convenience function that: - * 1. Creates and starts an integrated log server - * 2. Creates a default logger configured to connect to that server - * - * @param logFilePath Path to the log file (optional, defaults to '/tmp/logs/app.log') - * @param port Port for the log server (optional, defaults to 3001) - * @param debug Whether to output debug logs from the server (optional, defaults to false) - * @returns Object containing both the logger and log server information - */ -export function createLoggerWithServer( - logFilePath: string = "/tmp/logs/app.log", - port: number = 3001, - debug: boolean = false -) { - // Create and start the integrated log server - const logServer = createLogServer({ port, debug }); + // 6. 创建 logger + const logger = winstonCreateLogger({ + ...options, + transports: loggerTransports, + }) as MultiChannelLogger; - // Create a logger that connects to the server - const logger = createDefaultLogger(logFilePath, logServer.url); + // 7. 挂载 ws socket 实例 + (logger as any).socket = socket; - return { - logger, - logServer - }; + return logger; } // Export types and classes export * from "./types"; export { SocketIOTransport }; -export { createLogServer }; export default { - createMultiChannelLogger, - createDefaultLogger, - SocketIOTransport, - createLogServer, - createLoggerWithServer + createLogger, + SocketIOTransport }; diff --git a/infra/logger/src/types/index.ts b/infra/logger/src/types/index.ts index c637ad53..6fcffbd1 100644 --- a/infra/logger/src/types/index.ts +++ b/infra/logger/src/types/index.ts @@ -1,6 +1,22 @@ -import { Logger as WinstonLogger, LoggerOptions } from "winston"; +import { Logger as WinstonLogger, LoggerOptions, transports } from "winston"; import { Socket } from "socket.io-client"; +/** + * Request context type definition for logging + * + * Contains common request metadata that can be included in log entries: + * - authorization: Authorization header/token + * - xContainerId: Container identifier from headers/params/body + * - userId: User identifier + * - [key: string]: any: Additional custom context fields + */ +export type RequestContext = { + authorization?: string; + xContainerId?: string; + userId?: string; + [key: string]: any; +}; + /** * Configuration options for the Socket.IO Transport * @@ -8,7 +24,7 @@ import { Socket } from "socket.io-client"; */ export interface SocketIOTransportOptions extends LoggerOptions { /** WebSocket server URL (using http:// or https:// protocol, NOT ws:// or wss://) */ - url: string; + url?: string; /** Event name for log messages, defaults to 'log' */ eventName?: string; /** Log level for this transport */ @@ -22,14 +38,37 @@ export interface SocketIOTransportOptions extends LoggerOptions { } /** - * Extended Winston Logger configuration options with Socket.IO support + * Console transport options, extends winston's ConsoleTransportOptions + */ +export type ConsoleTransportOptions = transports.ConsoleTransportOptions; + +/** + * File transport options, extends winston's FileTransportOptions + */ +export type FileTransportOptions = transports.FileTransportOptions; + +/** + * Http transport options, extends winston's HttpTransportOptions + */ +export type HttpTransportOptions = transports.HttpTransportOptions; + +/** + * Extended Winston Logger configuration options with multi-channel support * - * This interface extends Winston's LoggerOptions to include a socketIO property - * for configuring the WebSocket transport. + * - console: Console transport options (required) + * - file: File transport options (required) + * - ws: Socket.IO transport options (optional) + * - http: Http transport options (optional) */ export interface MultiChannelLoggerOptions extends LoggerOptions { - /** Socket.IO transport configuration */ - socketIO?: SocketIOTransportOptions; + /** Console transport options (required) */ + console?: ConsoleTransportOptions; + /** File transport options (required, must provide filename) */ + file?: FileTransportOptions; + /** WebSocket transport options (optional) */ + ws?: SocketIOTransportOptions & { enabled?: boolean, port?: number }; + /** Http transport options (optional) */ + http?: HttpTransportOptions & { enabled?: boolean, getContext?: () => RequestContext }; } /** @@ -42,3 +81,69 @@ export interface MultiChannelLogger extends WinstonLogger { /** The Socket.IO client instance or null if WebSocket transport is not configured */ socket: Socket | null; } + +/** + * Unified configuration for log service (logger + ws server + http server) + */ +export interface LogServiceConfig extends MultiChannelLoggerOptions { + logFilePath: string; + level?: string; + debug?: boolean; + service?: string; +} + +/** + * LogLevel defines the supported log severity levels. + */ +export type LogLevel = 'error' | 'warn' | 'info' | 'http' | 'verbose' | 'debug' | 'silly'; + +/** + * LogEntry represents a single log message with optional context and metadata. + */ +export interface LogEntry { + timestamp: string; // ISO8601 + level: LogLevel; + message: string; + sessionId: string; // Session isolation support + source?: string; // e.g., service/component name + context?: Record; // Arbitrary structured context + meta?: Record; // Additional metadata + index?: number; // Monotonic index for log ordering (per session) +} + +/** + * LoggerStorage is an interface for log storage backends (memory, file, database, etc). + * Implementations must provide write() and query() methods, and may support streaming, close, and clear. + */ +export interface LoggerStorage { + write(entry: LogEntry): Promise; + query(params: LoggerQueryParams): Promise; + stream?(params: LoggerQueryParams): AsyncIterable; + close?(): Promise; + clear?(sessionId?: string): Promise; // Clear logs for a session or all if not provided +} + +/** + * LoggerQueryParams defines the parameters for querying or streaming logs. + */ +export interface LoggerQueryParams { + level?: LogLevel; + source?: string; + since?: string; // ISO8601 + until?: string; // ISO8601 + limit?: number; + sessionId?: string; // Session isolation support + cursor?: number; // Log index cursor for SSE reconnection +} + +/** + * LoggerClientOptions configures a LoggerClient instance. + * This is the base shape; implementations may extend it. + */ +export interface LoggerClientOptions { + transports: any[]; // Use 'any' to avoid direct winston dependency in types + defaultLevel?: LogLevel; + source?: string; + service?: string; + defaultSessionId?: string; // Optional default session context +} diff --git a/infra/logger/src/ws/server.ts b/infra/logger/src/ws/server.ts new file mode 100644 index 00000000..3c42caf7 --- /dev/null +++ b/infra/logger/src/ws/server.ts @@ -0,0 +1,58 @@ +import http from 'http'; +import { Server as SocketIOServer } from 'socket.io'; + +/** + * Options for the WebSocket log server + */ +export interface WsLogServerOptions { + port?: number; + debug?: boolean; +} + +/** + * Creates and starts a dedicated WebSocket server for log broadcasting + * @param options Configuration options for the server + * @returns Object containing the server instance and URL + */ +export function createWsLogServer(options: WsLogServerOptions = {}) { + const port = options.port || 3002; + const debug = options.debug !== false; + + // Create a standalone HTTP server for Socket.IO + const server = http.createServer(); + const io = new SocketIOServer(server, { + cors: { + origin: '*', + methods: ['GET', 'POST'], + }, + }); + + io.on('connection', socket => { + if (debug) { + // console.log(`[WS Log Server] Client connected: ${socket.id}`); + } + socket.on('log', logEntry => { + if (debug) { + console.log(`[WS Log Server] Received log: ${JSON.stringify(logEntry)}`); + } + io.emit('log', logEntry); + }); + socket.on('disconnect', () => { + if (debug) { + // console.log(`[WS Log Server] Client disconnected: ${socket.id}`); + } + }); + }); + + server.listen(port); + if (debug) { + console.log(`[WS Log Server] WebSocket server running on http://127.0.0.1:${port}`); + } + + return { + server: io, + url: `http://127.0.0.1:${port}`, + port, + close: () => server.close(), + }; +} \ No newline at end of file diff --git a/infra/logger/src/transports/socket-io.ts b/infra/logger/src/ws/transport.ts similarity index 100% rename from infra/logger/src/transports/socket-io.ts rename to infra/logger/src/ws/transport.ts diff --git a/infra/logger/vitest.config.ts b/infra/logger/vitest.config.ts new file mode 100644 index 00000000..81d27a17 --- /dev/null +++ b/infra/logger/vitest.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts'], + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'node_modules/', + 'dist/', + '**/*.d.ts', + '**/*.test.ts', + '**/__test__/**' + ] + } + } +}); diff --git a/infra/sandbox/design/infra-unified-server-design.md b/infra/sandbox/design/infra-unified-server-design.md new file mode 100644 index 00000000..6373e550 --- /dev/null +++ b/infra/sandbox/design/infra-unified-server-design.md @@ -0,0 +1,253 @@ +# Sandbox V2 Unified Server Design (Revised) + +## High-Level Architecture & Code Design + +### Modular, Decoupled Directory Structure + +To maximize modularity, extensibility, and maintainability, the unified infra server is organized into three main parts: + +- **container/**: Core implementations for browser, Docker, code execution, file system, git operations, and container lifecycle management. Pure logic, no dependency on HTTP/MCP/Logger servers. +- **servers/**: Contains all server entrypoints and logic. Each server is independent and only depends on `container` and `shared` logic. + - **http/**: Express server for RESTful API (OpenAPI 3.0, `/api/v1/`), SSE endpoints, and authentication. Uses a central controller class with route triples for declarative route registration. No separate `routes/` folder needed. + - **mcp/**: MCP server for streamable endpoints (`/mcp`, `/mcp/sse`), including transport, tools, and connection management. No dependency on Express or HTTP server. + - **logger/**: Dedicated log server (SSE/stream/REST), log client for integration, and multiple transports. Used by both HTTP and MCP servers, as well as container logic. +- **shared/**: Common types, utilities, Zod schemas, and config shared across all modules. + +#### Example Directory Structure +``` +infra/ + sandbox-v2/ + container/ + browser/ + docker/ + code/ + fs/ + git/ + manager.ts + types.ts + servers/ + http/ + controllers/ + sandbox-controller.ts # Central controller with route triples + middlewares/ + openapi/ + app.ts + index.ts + types.ts + mcp/ + server.ts + transport/ + tools/ + connections/ + types.ts + logger/ + server.ts + client.ts + transports/ + types.ts + storage-memory.ts + other-service/ + server.ts + middlewares/ + auth.ts + openapi/ + swagger.ts + types/ + index.ts + shared/ + types/ + utils/ + schemas/ + config/ + tests/ + Dockerfile + README.md + package.json + tsconfig.json +``` + +--- + +## Architectural Rationale + +- **Decoupling**: Each server (HTTP, MCP, Logger) is independent and only depends on `container` and `shared` logic. No cross-dependencies between servers. +- **Centralized Route Management**: HTTP server uses the route triple pattern in the central controller—no separate `routes/` folder. +- **Extensibility**: Easy to add new transports, tools, or container types. +- **Reusability**: Logger client can be used by any component to push logs. +- **Testability**: Clear separation for unit/integration testing. +- **Scalability**: Each server can be scaled or deployed independently if needed. + +--- + +## Endpoint Mapping + +- **RESTful HTTP (OpenAPI 3.0, `/api/v1/`)** + - CRUD and management for containers, dependencies, fs, code, git, etc. + - SSE endpoints for streaming (e.g., logs, file changes). +- **MCP HTTP Streaming (`/mcp`)** + - Streamable endpoint for MCP protocol. +- **MCP SSE (`/mcp/sse`)** + - SSE endpoint for backward compatibility. +- **Logger Service (`/logs`)** + - SSE/stream endpoint for real-time logs from all containers/services. + - Log ingestion endpoint for clients/components to push logs. + +--- + +## Key Design Principles + +- **Separation of Concerns**: + - `container/` implements all low-level logic (browser, Docker, code, fs, git, management). + - `servers/` contains only server-specific logic and entrypoints. + - `shared/` holds all cross-cutting types, schemas, and utilities. +- **Declarative Routing**: + - HTTP server uses static route triples in the central controller for easy registration and maintainability. +- **OpenAPI Integration**: + - JSDoc + Zod for validation and documentation. +- **Streaming/SSE**: + - Use native HTTP streaming and SSE for real-time features. +- **Authentication & Security**: + - Pluggable authentication middleware for sensitive endpoints. +- **Logger as a Service**: + - Logger server exposes both push (ingest) and pull (stream/SSE) endpoints, and a logger client is provided for all components. + +## Logger Module Design + +### Rationale and Role + +The logger module is foundational to the unified infra server, providing observability, debugging, and auditability for all components (HTTP, MCP, container logic, etc.). As a core dependency, it must be implemented first to enable seamless integration and consistent logging across the system. + +### Design Goals +- **Decoupled and Extensible**: The logger is a standalone module, not tied to any specific server or transport. It can be used by any component (HTTP server, MCP server, container logic, etc.). +- **Push/Pull Model**: Supports both log ingestion (push) from clients/components and log streaming (pull) for real-time or historical log access. +- **Multiple Transports**: Easily extensible to support various transports (SSE, HTTP, file, cloud, etc.) for both ingestion and streaming. +- **Client/Server Separation**: Provides a logger client for all components to push logs, and a logger server to aggregate, store, and serve logs. +- **Structured Logging**: All logs are structured (e.g., JSON), supporting rich metadata (timestamp, level, source, context, etc.). +- **Scalable and Reliable**: Designed for high-throughput, concurrent log ingestion and streaming, with proper buffering and backpressure handling. + +### High-Level Architecture + +``` ++-------------------+ push +-------------------+ +| Any Component |-------------------->| Logger Client | ++-------------------+ +-------------------+ + | + | (push) + v + +-------------------+ + | Logger Server | + +-------------------+ + ^ + | (pull/stream) + | + +-------------------+ + | Log Consumer | + +-------------------+ +``` + +- **Logger Client**: Lightweight library for all components to send logs to the logger server, supporting batching, retries, and structured log formatting. +- **Logger Server**: Central service that receives, stores, and streams logs. Exposes endpoints for log ingestion (push) and log streaming/query (pull), supporting multiple transports (SSE, HTTP, etc.). +- **Log Consumer**: Any client (UI, CLI, monitoring tool) that subscribes to or queries logs from the logger server. + +### Key Features +- **API Endpoints**: + - `POST /logs` — Ingest logs (push) + - `GET /logs/stream` — Real-time log streaming (SSE/HTTP) + - `GET /logs/query` — Query historical logs +- **Transports**: + - **Console**: For local development and debugging, logs are output to the console. + - **File**: Logs are persisted to local files for archiving and later analysis. + - **HTTP**: Logs are automatically pushed to the logger server via HTTP POST, suitable for distributed log aggregation. This uses winston's official `transports.Http` implementation directly. + - **Extensibility**: If additional transports are needed in the future (such as SSE broadcast, cloud log services, etc.), they can be added via winston's transport plugin mechanism. + +> The logger client currently supports Console, File, and HTTP transports out of the box, all based on winston's official implementations. No custom transport implementation is required at this stage. + +### Implementation Steps +1. **Define structured log format and types** (in `shared/types/`) +2. **Implement logger client** (in `servers/logger/client.ts`) + - API for log events, batching, retries + - Configurable transport +3. **Implement logger server** (in `servers/logger/server.ts`) + - Endpoints for ingestion and streaming + - Pluggable storage and transport + - SSE/HTTP support +4. **Add transports** (in `servers/logger/transports/`) + - SSE, HTTP, file, etc. +5. **Integrate logger client** into all other modules +6. **Test reliability, performance, and extensibility** + +### Extensibility +- Add new transports by implementing a transport interface +- Support for log filtering, search, and retention policies +- Optional: Integrate with external log aggregation/monitoring systems + +### Summary + +The logger module is the backbone of observability for the unified infra server. Its decoupled, extensible design ensures that all components can reliably push and consume logs, supporting robust debugging, monitoring, and auditing across the system. Implementing the logger first enables consistent, structured logging and paves the way for scalable, maintainable infrastructure. + +--- + +## Example Usage + +- **HTTP server** imports `container` logic and `logger` client, exposes REST/SSE endpoints. +- **MCP server** imports `container` logic and `logger` client, exposes streaming endpoints. +- **Logger server** exposes log ingestion and streaming endpoints, and provides a client for other modules. + +--- + +## Action List for Aligning /infra/sandbox-v2 with Unified Infra Server Design + +### 1. Project Structure & Modularity +- [ ] Restructure project to match the proposed directory layout. +- [ ] Ensure all low-level logic is in `container/`, and all server logic is in `servers/`. + +### 2. API Endpoints & Routing +- [ ] Adopt versioned RESTful endpoints as per the design. +- [ ] Implement or refactor streaming endpoints to use HTTP streaming or SSE (no WebSocket). +- [ ] Use route triple pattern in the central controller for HTTP server. + +### 3. Streaming & SSE +- [ ] Implement SSE for log streaming (`/logs`), including heartbeat support. +- [ ] Implement HTTP streaming for MCP (`/mcp`), with heartbeat messages. +- [ ] Implement SSE for MCP (`/mcp/sse`), with heartbeat. + +### 4. API Documentation & Validation +- [ ] Integrate OpenAPI (swagger-jsdoc + swagger-ui-express) for API docs at `/api-docs`. +- [ ] Add JSDoc comments to all routes/controllers for OpenAPI generation. +- [ ] Adopt Zod for runtime validation of request bodies, params, and queries. +- [ ] (Optional) Use zod-to-openapi to auto-generate OpenAPI schemas from Zod. + +### 5. Authentication & Security +- [ ] Implement pluggable authentication middleware (e.g., JWT) for all sensitive endpoints. +- [ ] Add RBAC/authorization checks where needed. + +### 6. Service Integration +- [ ] Unify all sandbox-related services (container, dependency, fs, code, git, logger, MCP) under the new modular structure. +- [ ] Refactor existing services to be modular and easily extensible. + +### 7. Logger Service +- [ ] Implement a logger server with push/pull endpoints and multiple transports. +- [ ] Provide a logger client for all components to push logs. + +### 8. Testing & Reliability +- [ ] Add unit and integration tests for all endpoints, especially streaming/SSE. +- [ ] Test SSE/streaming reliability and performance (e.g., heartbeat, disconnect handling). + +### 9. Deployment & Ops +- [ ] Dockerize the unified server for deployment. +- [ ] Add health checks and monitoring endpoints. + +### 10. Documentation +- [ ] Update README to reflect new architecture, endpoints, and usage. +- [ ] Document streaming protocols (SSE/HTTP streaming, heartbeat format, etc). + +### Optional/Advanced Improvements +- [ ] Consider using Fastify for better performance if Express is not a hard requirement. +- [ ] Add support for tool registry in MCP service for easy extensibility. +- [ ] Implement graceful shutdown and resource cleanup for streaming endpoints. + +--- + +## Conclusion + +This revised design enables all sandbox and MCP services to be unified under a modular, decoupled architecture, with real-time log streaming via a dedicated logger service, and all APIs accessible via HTTP. Each server (HTTP, MCP, Logger) is independent, extensible, and easy to test and maintain. The logger service provides a robust foundation for observability and debugging across all components. \ No newline at end of file diff --git a/infra/sandbox/package.json b/infra/sandbox/package.json index 65ad1c80..aac44de0 100644 --- a/infra/sandbox/package.json +++ b/infra/sandbox/package.json @@ -43,6 +43,7 @@ "express-validator": "^7.0.1", "helmet": "^7.1.0", "http-proxy": "^1.18.1", + "jsonwebtoken": "^9.0.2", "morgan": "^1.10.0", "uuid": "^9.0.1", "winston": "^3.11.0", @@ -55,8 +56,12 @@ "@types/dockerode": "^3.3.38", "@types/express": "^4.17.21", "@types/jest": "^29.5.11", + "@types/jsonwebtoken": "^9.0.9", "@types/morgan": "^1.9.9", "@types/node": "^20.10.5", + "@types/swagger-jsdoc": "^6.0.4", + "@types/swagger-ui-express": "^4.1.8", + "@types/tar-stream": "^3.1.4", "@types/uuid": "^9.0.7", "@typescript-eslint/eslint-plugin": "^6.15.0", "@typescript-eslint/parser": "^6.15.0", @@ -64,10 +69,13 @@ "eslint": "^8.56.0", "jest": "^29.7.0", "supertest": "^6.3.3", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", "tar-stream": "^3.1.7", "ts-jest": "^29.1.1", "ts-node": "^10.9.2", "tsx": "^4.16.5", - "typescript": "^5.3.3" + "typescript": "^5.3.3", + "typescript-json-schema": "^0.65.1" } } diff --git a/infra/sandbox/src/middleware/context.ts b/infra/sandbox/src/middleware/context.ts new file mode 100644 index 00000000..062d877e --- /dev/null +++ b/infra/sandbox/src/middleware/context.ts @@ -0,0 +1,42 @@ +import { Request, Response, NextFunction } from "express"; +import { runWithContext } from "../utils/request-context"; +import jwt, { JwtPayload } from "jsonwebtoken"; + +/** + * Express middleware to extract context from the request. + * - Extracts the Authorization header and verifies the JWT to get userId (from payload.id). + * - Extracts x-container-id from header, params, body, or query. + * - Passes { authorization, xContainerId, userId } to runWithContext. + * + * If the Authorization header is missing or invalid, userId will be undefined. + * + * @param req Express request + * @param res Express response + * @param next Express next + */ +export function contextMiddleware(req: Request, res: Response, next: NextFunction) { + const authorization = req.headers.authorization; + // Get containerId from header first, then try params/body/query if they exist + const xContainerId = req.headers["x-container-id"] as string || + (req.params?.containerId) || + (req.body?.containerId) || + (req.query?.containerId); + + let userId: string | undefined = undefined; + if (authorization && authorization.startsWith("Bearer ")) { + const token = authorization.slice(7); + const secret = process.env.TOKEN_SECRET?.replace(/\\n/g, "\n"); + if (secret) { + try { + const decoded = jwt.verify(token, secret) as JwtPayload | string; + if (typeof decoded === "object" && decoded && "id" in decoded) { + userId = (decoded as any).id; + } + } catch (err) { + // Invalid token, userId remains undefined + } + } + } + + runWithContext({ authorization, xContainerId, userId }, () => next()); +} \ No newline at end of file diff --git a/infra/sandbox/src/server.ts b/infra/sandbox/src/server.ts index e3d50a16..aa77bcac 100644 --- a/infra/sandbox/src/server.ts +++ b/infra/sandbox/src/server.ts @@ -13,6 +13,7 @@ import { import sandboxController from "./controllers/sandbox-controller"; import logger from "./utils/logger"; import config from "./config"; +import { contextMiddleware } from "./middleware/context"; /** * Create and configure Express app @@ -24,6 +25,7 @@ export function createApp(): { const app = express(); // Apply middleware + app.use(contextMiddleware); // 注册 contextMiddleware app.use(helmet()); // Security headers app.use(cors()); // Enable CORS app.use(express.json()); // Parse JSON bodies @@ -58,8 +60,6 @@ export function createApp(): { sandboxController.useBrowser.bind(sandboxController) ); - // Removed real-time logs endpoint - app.get( "/api/sandbox/:containerId", containerIdValidation, diff --git a/infra/sandbox/src/utils/logger.ts b/infra/sandbox/src/utils/logger.ts index 918ed6e1..c16211e6 100644 --- a/infra/sandbox/src/utils/logger.ts +++ b/infra/sandbox/src/utils/logger.ts @@ -1,24 +1,30 @@ import path from 'path'; -import { createLoggerWithServer } from '@graphscope/logger'; +import { createLogService } from '@graphscope/logger'; import config from '../config'; import { dirname } from './paths'; +import { getContext } from './request-context'; // 设置日志文件路径 const logFilePath = path.join(dirname, '../../logs/sandbox.log'); -// 设置WebSocket服务器端口,用于实时日志流 +// 设置WebSocket和HTTP服务器端口 const WS_PORT = Number(process.env.WS_LOG_PORT || '3002'); +const HTTP_PORT = Number(process.env.HTTP_LOG_PORT || '3003'); -// 使用createLoggerWithServer创建logger和集成的日志服务器 -// 参数1: 日志文件路径 -// 参数2: WebSocket服务器端口 -// 参数3: 是否启用调试模式(非生产环境下启用) -const { logger, logServer } = createLoggerWithServer(logFilePath, WS_PORT, config.nodeEnv !== 'production'); - -// 设置日志级别,从配置中获取 -logger.level = config.logLevel; - -// 添加默认元数据 -logger.defaultMeta = { service: '@graphscope' }; +// 创建统一的日志服务 +const { logger } = createLogService({ + level: config.logLevel || 'info', + logFilePath, + ws: { + enabled: true, + port: WS_PORT + }, + http: { + enabled: true, + port: HTTP_PORT, + host: '127.0.0.1', + getContext, + } +}); export default logger; diff --git a/infra/sandbox/src/utils/request-context.ts b/infra/sandbox/src/utils/request-context.ts new file mode 100644 index 00000000..6f915dcd --- /dev/null +++ b/infra/sandbox/src/utils/request-context.ts @@ -0,0 +1,18 @@ +import { AsyncLocalStorage } from "async_hooks"; + +export type RequestContext = { + authorization?: string; + xContainerId?: string; + userId?: string; + [key: string]: any; +}; + +const asyncLocalStorage = new AsyncLocalStorage(); + +export function runWithContext(context: RequestContext, fn: () => T) { + return asyncLocalStorage.run(context, fn); +} + +export function getContext(): RequestContext { + return asyncLocalStorage.getStore() || {}; +} \ No newline at end of file diff --git a/package.json b/package.json index b74a3278..5dbc7c92 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "docs": "dumi dev", "version": "changeset version", "publish": "npm run build && pnpm changeset publish -r", + "lint": "", "check:publish": "npm run build && pnpm changeset publish -r --dry-run" }, "husky": { @@ -84,7 +85,9 @@ "typescript": "^5.3.3", "webpack": "^5.96.1", "webpack-cli": "^5.1.4", - "terser-webpack-plugin": "^5.3.10" + "terser-webpack-plugin": "^5.3.10", + "node-fetch": "^2.6.7", + "@types/node-fetch": "^2.6.4" }, "publishConfig": { "access": "public" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3546058f..40f14755 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -75,6 +75,9 @@ importers: '@types/node': specifier: latest version: 22.15.21 + '@types/node-fetch': + specifier: ^2.6.4 + version: 2.6.12 '@types/react': specifier: 18.2.0 version: 18.2.0 @@ -306,9 +309,15 @@ importers: infra/logger: dependencies: + dotenv: + specifier: ^16.5.0 + version: 16.5.0 express: specifier: ^4.21.2 version: 4.21.2 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 socket.io: specifier: ^4.7.2 version: 4.8.1 @@ -328,15 +337,24 @@ importers: '@types/jest': specifier: ^29.5.8 version: 29.5.14 + '@types/jsonwebtoken': + specifier: ^9.0.9 + version: 9.0.9 '@types/node': specifier: ^20.9.0 version: 20.17.28 '@types/socket.io-client': specifier: ^3.0.0 version: 3.0.0 + '@vitest/coverage-v8': + specifier: ^3.2.3 + version: 3.2.3(vitest@3.2.3(@types/debug@4.1.12)(@types/node@20.17.28)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.5.1)) concurrently: specifier: ^7.6.0 version: 7.6.0 + eventsource: + specifier: ^4.0.0 + version: 4.0.0 http-server: specifier: ^14.1.1 version: 14.1.1 @@ -355,6 +373,9 @@ importers: typescript: specifier: ^5.2.2 version: 5.8.2 + vitest: + specifier: ^3.0.0 + version: 3.2.3(@types/debug@4.1.12)(@types/node@20.17.28)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.5.1) infra/sandbox: dependencies: @@ -388,6 +409,9 @@ importers: http-proxy: specifier: ^1.18.1 version: 1.18.1 + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 morgan: specifier: ^1.10.0 version: 1.10.0 @@ -419,12 +443,24 @@ importers: '@types/jest': specifier: ^29.5.11 version: 29.5.14 + '@types/jsonwebtoken': + specifier: ^9.0.9 + version: 9.0.9 '@types/morgan': specifier: ^1.9.9 version: 1.9.9 '@types/node': specifier: ^20.10.5 version: 20.17.28 + '@types/swagger-jsdoc': + specifier: ^6.0.4 + version: 6.0.4 + '@types/swagger-ui-express': + specifier: ^4.1.8 + version: 4.1.8 + '@types/tar-stream': + specifier: ^3.1.4 + version: 3.1.4 '@types/uuid': specifier: ^9.0.7 version: 9.0.8 @@ -446,6 +482,12 @@ importers: supertest: specifier: ^6.3.3 version: 6.3.4 + swagger-jsdoc: + specifier: ^6.2.8 + version: 6.2.8(openapi-types@12.1.3) + swagger-ui-express: + specifier: ^5.0.1 + version: 5.0.1(express@4.21.2) tar-stream: specifier: ^3.1.7 version: 3.1.7 @@ -461,6 +503,9 @@ importers: typescript: specifier: ^5.3.3 version: 5.8.2 + typescript-json-schema: + specifier: ^0.65.1 + version: 0.65.1(@swc/core@1.9.2(@swc/helpers@0.5.15)) infra/simple-logger: dependencies: @@ -1275,6 +1320,21 @@ packages: '@antv/util@3.3.10': resolution: {integrity: sha512-basGML3DFA3O87INnzvDStjzS+n0JLEhRnRsDzP9keiXz8gT1z/fTdmJAZFOzMMWxy+HKbi7NbSt0+8vz/OsBQ==} + '@apidevtools/json-schema-ref-parser@9.1.2': + resolution: {integrity: sha512-r1w81DpR+KyRWd3f+rk6TNqMgedmAxZP5v5KWlXQWlgMUUtyEJch0DKEci1SorPMiSeM8XPl7MZ3miJ60JIpQg==} + + '@apidevtools/openapi-schemas@2.1.0': + resolution: {integrity: sha512-Zc1AlqrJlX3SlpupFGpiLi2EbteyP7fXmUOGup6/DnkRgjP9bgMM/ag+n91rsv0U1Gpz0H3VILA/o3bW7Ua6BQ==} + engines: {node: '>=10'} + + '@apidevtools/swagger-methods@3.0.2': + resolution: {integrity: sha512-QAkD5kK2b1WfjDS/UQn/qQkbwF31uqRjPTrsCs5ZG9BQGAkjwvqGFjjPqAuzac/IYzpPtRzjCP1WrTuAIjMrXg==} + + '@apidevtools/swagger-parser@10.0.3': + resolution: {integrity: sha512-sNiLY51vZOmSPFZA5TF35KZ2HbgYklQnTSDnkghamzLb3EkNtcQnrBQEj5AOCxHpTtXpqMCRM1CrmV2rG6nw4g==} + peerDependencies: + openapi-types: '>=7' + '@arvinxu/layout-kit@1.4.0': resolution: {integrity: sha512-dEsmFwZa/NJ2XvDBL4sCPbgFPvCvpxP+G+90Ay9zqN92vc4YbgVo4NjpjsDihiNqwDQjWhasGCC3+v4w7bdYqg==} @@ -1577,6 +1637,10 @@ packages: '@bcoe/v8-coverage@0.2.3': resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@bcoe/v8-coverage@1.0.2': + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} + '@bloomberg/record-tuple-polyfill@0.0.4': resolution: {integrity: sha512-h0OYmPR3A5Dfbetra/GzxBAzQk8sH7LhRkRUTdagX6nrtlUgJGYCTv4bBK33jsTQw9HDd8PE2x1Ma+iRKEDUsw==} @@ -2917,6 +2981,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@jsdevtools/ono@7.1.3': + resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + '@kuzu/kuzu-wasm@0.7.0': resolution: {integrity: sha512-lul2p4EEaz3h6gfXZNSAVCxOlLTYFeBWCGs1hJsYtJQ+HprrUqJ7jPYDv9UNItXQGoeBicwTlFOaJTs87N9Q5Q==} @@ -3657,6 +3724,9 @@ packages: '@rushstack/ts-command-line@4.17.1': resolution: {integrity: sha512-2jweO1O57BYP5qdBGl6apJLB+aRIn5ccIRTPDyULh0KMwVzFqWtw6IZWt1qtUoZD/pD2RNkIOosH6Cq45rIYeg==} + '@scarf/scarf@1.4.0': + resolution: {integrity: sha512-xxeapPiUXdZAE3che6f3xogoJPeZgig6omHEy1rIY5WVsB3H2BHNnZH+gHG6x91SCWyQCzWGsuL2Hh3ClO5/qQ==} + '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} @@ -3985,6 +4055,9 @@ packages: '@types/body-parser@1.19.5': resolution: {integrity: sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==} + '@types/chai@5.2.2': + resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/command-line-args@5.2.3': resolution: {integrity: sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==} @@ -4096,6 +4169,9 @@ packages: '@types/debug@4.1.12': resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/docker-modem@3.0.6': resolution: {integrity: sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==} @@ -4183,6 +4259,9 @@ packages: '@types/jsonfile@6.1.4': resolution: {integrity: sha512-D5qGUYwjvnNNextdU59/+fI+spnwtTFmyQP0h+PfIOSkNfpU6AOICUOkm4i0OnSk+NyjdPJrxCDro0sJsWlRpQ==} + '@types/jsonwebtoken@9.0.9': + resolution: {integrity: sha512-uoe+GxEuHbvy12OUQct2X9JenKM3qAscquYymuQN4fMWG9DBQtykrQEFcAbVACF7qaLw9BePSodUL0kquqBJpQ==} + '@types/katex@0.16.7': resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} @@ -4213,6 +4292,9 @@ packages: '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} + '@types/node-fetch@2.6.12': + resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} + '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} @@ -4307,6 +4389,15 @@ packages: '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} + '@types/swagger-jsdoc@6.0.4': + resolution: {integrity: sha512-W+Xw5epcOZrF/AooUM/PccNMSAFOKWZA5dasNyMujTwsBkU74njSJBpvCCJhHAJ95XRMzQrrW844Btu0uoetwQ==} + + '@types/swagger-ui-express@4.1.8': + resolution: {integrity: sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==} + + '@types/tar-stream@3.1.4': + resolution: {integrity: sha512-921gW0+g29mCJX0fRvqeHzBlE/XclDaAG0Ousy1LCghsOhvaKacDeRGEVzQP9IPfKn8Vysy7FEXAIxycpc/CMg==} + '@types/triple-beam@1.3.5': resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} @@ -4736,9 +4827,21 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + '@vitest/coverage-v8@3.2.3': + resolution: {integrity: sha512-D1QKzngg8PcDoCE8FHSZhREDuEy+zcKmMiMafYse41RZpBE5EDJyKOTdqK3RQfsV2S2nyKor5KCs8PyPRFqKPg==} + peerDependencies: + '@vitest/browser': 3.2.3 + vitest: 3.2.3 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@2.1.5': resolution: {integrity: sha512-nZSBTW1XIdpZvEJyoP/Sy8fUg0b8od7ZpGDkTUcfJ7wz/VoZAFzFfLyxVxGFhUjJzhYqSbIpfMtl/+k/dpWa3Q==} + '@vitest/expect@3.2.3': + resolution: {integrity: sha512-W2RH2TPWVHA1o7UmaFKISPvdicFJH+mjykctJFoAkUw+SPTJTGjUNdKscFBrqM7IPnCVu6zihtKYa7TkZS1dkQ==} + '@vitest/mocker@2.1.5': resolution: {integrity: sha512-XYW6l3UuBmitWqSUXTNXcVBUCRytDogBsWuNXQijc00dtnU/9OqpXWp4OJroVrad/gLIomAq9aW8yWDBtMthhQ==} peerDependencies: @@ -4750,21 +4853,47 @@ packages: vite: optional: true + '@vitest/mocker@3.2.3': + resolution: {integrity: sha512-cP6fIun+Zx8he4rbWvi+Oya6goKQDZK+Yq4hhlggwQBbrlOQ4qtZ+G4nxB6ZnzI9lyIb+JnvyiJnPC2AGbKSPA==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + '@vitest/pretty-format@2.1.5': resolution: {integrity: sha512-4ZOwtk2bqG5Y6xRGHcveZVr+6txkH7M2e+nPFd6guSoN638v/1XQ0K06eOpi0ptVU/2tW/pIU4IoPotY/GZ9fw==} + '@vitest/pretty-format@3.2.3': + resolution: {integrity: sha512-yFglXGkr9hW/yEXngO+IKMhP0jxyFw2/qys/CK4fFUZnSltD+MU7dVYGrH8rvPcK/O6feXQA+EU33gjaBBbAng==} + '@vitest/runner@2.1.5': resolution: {integrity: sha512-pKHKy3uaUdh7X6p1pxOkgkVAFW7r2I818vHDthYLvUyjRfkKOU6P45PztOch4DZarWQne+VOaIMwA/erSSpB9g==} + '@vitest/runner@3.2.3': + resolution: {integrity: sha512-83HWYisT3IpMaU9LN+VN+/nLHVBCSIUKJzGxC5RWUOsK1h3USg7ojL+UXQR3b4o4UBIWCYdD2fxuzM7PQQ1u8w==} + '@vitest/snapshot@2.1.5': resolution: {integrity: sha512-zmYw47mhfdfnYbuhkQvkkzYroXUumrwWDGlMjpdUr4jBd3HZiV2w7CQHj+z7AAS4VOtWxI4Zt4bWt4/sKcoIjg==} + '@vitest/snapshot@3.2.3': + resolution: {integrity: sha512-9gIVWx2+tysDqUmmM1L0hwadyumqssOL1r8KJipwLx5JVYyxvVRfxvMq7DaWbZZsCqZnu/dZedaZQh4iYTtneA==} + '@vitest/spy@2.1.5': resolution: {integrity: sha512-aWZF3P0r3w6DiYTVskOYuhBc7EMc3jvn1TkBg8ttylFFRqNN2XGD7V5a4aQdk6QiUzZQ4klNBSpCLJgWNdIiNw==} + '@vitest/spy@3.2.3': + resolution: {integrity: sha512-JHu9Wl+7bf6FEejTCREy+DmgWe+rQKbK+y32C/k5f4TBIAlijhJbRBIRIOCEpVevgRsCQR2iHRUH2/qKVM/plw==} + '@vitest/utils@2.1.5': resolution: {integrity: sha512-yfj6Yrp0Vesw2cwJbP+cl04OC+IHFsuQsrsJBL9pyGeQXE56v1UAOQco+SR55Vf1nQzfV0QJg1Qum7AaWUwwYg==} + '@vitest/utils@3.2.3': + resolution: {integrity: sha512-4zFBCU5Pf+4Z6v+rwnZ1HU1yzOKKvDkMXZrymE2PBlbjKJRlrOxbvpfPSvJTGRIwGoahaOGvp+kbCoxifhzJ1Q==} + '@webassemblyjs/ast@1.14.1': resolution: {integrity: sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==} @@ -5117,6 +5246,9 @@ packages: resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} engines: {node: '>=4'} + ast-v8-to-istanbul@0.3.3: + resolution: {integrity: sha512-MuXMrSLVVoA6sYN/6Hke18vMzrT4TZNbZIj/hvh0fnYFpO+/kFXcLIaiPwXXWaQUPg4yJD8fj+lfJ7/1EBconw==} + astral-regex@2.0.0: resolution: {integrity: sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==} engines: {node: '>=8'} @@ -5380,6 +5512,9 @@ packages: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-fill@1.0.0: resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==} @@ -5448,6 +5583,9 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + call-me-maybe@1.0.2: + resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==} + callsites@3.1.0: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} @@ -5504,6 +5642,10 @@ packages: resolution: {integrity: sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==} engines: {node: '>=12'} + chai@5.2.0: + resolution: {integrity: sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==} + engines: {node: '>=12'} + chalk-template@0.4.0: resolution: {integrity: sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==} engines: {node: '>=12'} @@ -5763,6 +5905,10 @@ packages: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} + commander@6.2.0: + resolution: {integrity: sha512-zP4jEKbe8SHzKJYQmq8Y9gYjtO/POJLgIdKgV7B9qNmABVFVc+ctqSX6iXh4mCpJfRBOabiZ2YKPg8ciDw6C+Q==} + engines: {node: '>= 6'} + commander@7.2.0: resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} engines: {node: '>= 10'} @@ -6358,6 +6504,15 @@ packages: supports-color: optional: true + debug@4.4.1: + resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} engines: {node: '>=0.10.0'} @@ -6592,6 +6747,10 @@ packages: resolution: {integrity: sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==} engines: {node: '>=12'} + dotenv@16.5.0: + resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==} + engines: {node: '>=12'} + dotignore@0.1.2: resolution: {integrity: sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==} hasBin: true @@ -6639,6 +6798,9 @@ packages: easy-table@1.1.0: resolution: {integrity: sha512-oq33hWOSSnl2Hoh00tZWaIPi1ievrD9aFG82/IgjlycAnW9hHx5PkJiXpxPsgEE+H7BsbVQXFVFST8TEXS6/pA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + editions@2.3.1: resolution: {integrity: sha512-ptGvkwTvGdGfC0hfhKg0MT+TRLRKGtUiWGBInxOm5pz7ssADezahjCUaYuZ8Dr+C05FW0AECIIPt4WBxVINEhA==} engines: {node: '>=0.8'} @@ -6782,6 +6944,9 @@ packages: es-module-lexer@1.5.4: resolution: {integrity: sha512-MVNK56NiMrOwitFB7cqDwq0CQutbw+0BvLshJSse0MUNU+y1FC3bUS/AQg7oUng+/wKrrki7JfmwtVHkVfPLlw==} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.0.0: resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} engines: {node: '>= 0.4'} @@ -7019,10 +7184,18 @@ packages: resolution: {integrity: sha512-T1C0XCUimhxVQzW4zFipdx0SficT651NnkR0ZSH3yQwh+mFMdLfgjABVi4YtMTtaL4s168593DaoaRLMqryavA==} engines: {node: '>=18.0.0'} + eventsource-parser@3.0.2: + resolution: {integrity: sha512-6RxOBZ/cYgd8usLwsEl+EC09Au/9BcmCKYF2/xbml6DNczf7nv0MQb+7BA2F+li6//I+28VNlQR37XfQtcAJuA==} + engines: {node: '>=18.0.0'} + eventsource@3.0.5: resolution: {integrity: sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==} engines: {node: '>=18.0.0'} + eventsource@4.0.0: + resolution: {integrity: sha512-fvIkb9qZzdMxgZrEQDyll+9oJsyaVvY92I2Re+qK0qEJ+w5s0X3dtz+M0VAPOjP1gtU3iqWyjQ0G3nvd5CLZ2g==} + engines: {node: '>=20.0.0'} + evp_bytestokey@1.0.3: resolution: {integrity: sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==} @@ -7058,6 +7231,10 @@ packages: resolution: {integrity: sha512-bFi65yM+xZgk+u/KRIpekdSYkTB5W1pEf0Lt8Q8Msh7b+eQ7LXVtIB1Bkm4fvclDEL1b2CZkMhv2mOeF8tMdkA==} engines: {node: '>=12.0.0'} + expect-type@1.2.1: + resolution: {integrity: sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==} + engines: {node: '>=12.0.0'} + expect@29.7.0: resolution: {integrity: sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} @@ -7158,6 +7335,14 @@ packages: fbjs@3.0.5: resolution: {integrity: sha512-ztsSx77JBtkuMrEypfhgc3cI0+0h+svqeie7xHbh1k/IKdcydnvadp/mUaGgjAOXQmQSxsqgaRhS3q9fy+1kxg==} + fdir@6.4.6: + resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} @@ -7548,6 +7733,10 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@7.1.6: + resolution: {integrity: sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==} + deprecated: Glob versions prior to v9 are no longer supported + glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} deprecated: Glob versions prior to v9 are no longer supported @@ -8442,6 +8631,10 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + istanbul-reports@3.1.7: resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} engines: {node: '>=8'} @@ -8612,6 +8805,9 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.1: resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} hasBin: true @@ -8686,6 +8882,10 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + jspdf@2.5.2: resolution: {integrity: sha512-myeX9c+p7znDWPk0eTrujCzNjT+CXdXyk7YmJq5nD5V7uLLKmSXnlQ/Jn/kuo3X09Op70Apm0rQSnFWyGK8uEQ==} @@ -8696,6 +8896,12 @@ packages: jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + kapsule@1.14.6: resolution: {integrity: sha512-wSi6tHNOfXrIK2Pvv6BhZ9ukzhbp+XZlOOPWSVGUbqfFsnnli4Eq8FN6TaWJv2e17sY5+fKYVxa4DP2oPGlKhg==} engines: {node: '>=12'} @@ -8922,16 +9128,40 @@ packages: resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + lodash.isequal@4.5.0: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.memoize@4.1.2: resolution: {integrity: sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==} lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.mergewith@4.6.2: + resolution: {integrity: sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.startcase@4.4.0: resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} @@ -8974,6 +9204,9 @@ packages: loupe@3.1.2: resolution: {integrity: sha512-23I4pFZHmAemUnz8WZXbYRSKYj801VDaNv9ETuMh7IrMc7VuVVSo+Z9iLE3ni30+U48iDWfi30d3twAXBYmnCg==} + loupe@3.1.3: + resolution: {integrity: sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==} + lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} @@ -9005,6 +9238,12 @@ packages: magic-string@0.30.13: resolution: {integrity: sha512-8rYBO+MsWkgjDSOvLomYnzhdwEG51olQ4zL5KXnNJWV5MNmrb4rTZdrtkhxjnD/QyZUqR/Z/XDsUs/4ej2nx0g==} + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + make-dir@1.3.0: resolution: {integrity: sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==} engines: {node: '>=4'} @@ -9892,6 +10131,9 @@ packages: resolution: {integrity: sha512-OS+QTnw1/4vrf+9hh1jc1jnYjzSG4ttTBB8UxOwAnInG3Uo4ssetzC1ihqaIHjLJnA5GGlRl6QlZXOTQhRBUvg==} engines: {node: '>=14.16'} + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + opener@1.5.2: resolution: {integrity: sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==} hasBin: true @@ -10055,6 +10297,9 @@ packages: path-browserify@0.0.1: resolution: {integrity: sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==} + path-equal@1.2.5: + resolution: {integrity: sha512-i73IctDr3F2W+bsOWDyyVm/lqsXO47aY9nsFZUjTT/aljSbkxHxxCoyZ9UUrM8jK0JVod+An+rl48RCsvWM+9g==} + path-exists@3.0.0: resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} engines: {node: '>=4'} @@ -10113,6 +10358,9 @@ packages: pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.0: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} @@ -10140,6 +10388,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} + picomatch@4.0.2: + resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==} + engines: {node: '>=12'} + pidtree@0.3.1: resolution: {integrity: sha512-qQbW94hLHEqCg7nhby4yRC7G2+jYHY4Rguc2bjw7Uug4GIJuu1tvf2uHaZv5Q8zdt+WKJ6qK1FOI6amaWUo5FA==} engines: {node: '>=0.10'} @@ -11868,6 +12120,9 @@ packages: std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} + std-env@3.9.0: + resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} + stop-iteration-iterator@1.0.0: resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} engines: {node: '>= 0.4'} @@ -12012,6 +12267,9 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} + strip-literal@3.0.0: + resolution: {integrity: sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==} + strip-outer@1.0.1: resolution: {integrity: sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==} engines: {node: '>=0.10.0'} @@ -12148,6 +12406,24 @@ packages: svgson@4.1.0: resolution: {integrity: sha512-DodISxHtdLKUghDYA+PGK4Qq350+CbBAkdvGLkBFSmWd9WKSg4dijgjB1IiRPTmsUCd+a7KYe+ILHtklYgQyzQ==} + swagger-jsdoc@6.2.8: + resolution: {integrity: sha512-VPvil1+JRpmJ55CgAtn8DIcpBs0bL5L3q5bVQvF4tAW/k/9JYSj7dCpaYCAv5rufe0vcCbBRQXGvzpkWjvLklQ==} + engines: {node: '>=12.0.0'} + hasBin: true + + swagger-parser@10.0.3: + resolution: {integrity: sha512-nF7oMeL4KypldrQhac8RyHerJeGPD1p2xDh900GPvc+Nk7nWP6jX2FcC7WmkinMoAmoO774+AFXcWsW8gMWEIg==} + engines: {node: '>=10'} + + swagger-ui-dist@5.24.1: + resolution: {integrity: sha512-ITeWc7CCAfK53u8jnV39UNqStQZjSt+bVYtJHsOEL3vVj/WV9/8HmsF8Ej4oD8r+Xk1HpWyeW/t59r1QNeAcUQ==} + + swagger-ui-express@5.0.1: + resolution: {integrity: sha512-SrNU3RiBGTLLmFU8GIJdOdanJTl4TOmT27tt3bWWHppqYmAZ6IDuEuBvMU6nZq0zLEe6b/1rACXCgLZqO6ZfrA==} + engines: {node: '>= v0.10.32'} + peerDependencies: + express: '>=4.0.0 || >=5.0.0-beta' + synckit@0.8.5: resolution: {integrity: sha512-L1dapNV6vu2s/4Sputv8xGsCdAVlb5nRDMFU/E27D44l5U6cw1g0dGd45uLc+OXjNMmF4ntiMdCimzcjFKQI8Q==} engines: {node: ^14.18.0 || >=16.0.0} @@ -12221,6 +12497,10 @@ packages: resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} engines: {node: '>=8'} + test-exclude@7.0.1: + resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} + engines: {node: '>=18'} + text-decoder@1.2.3: resolution: {integrity: sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==} @@ -12298,18 +12578,37 @@ packages: tinyexec@0.3.1: resolution: {integrity: sha512-WiCJLEECkO18gwqIp6+hJg0//p23HXp4S+gGtAKu3mI2F2/sXC4FvHvXvB0zJVVaTPhx1/tOwdbRsa1sOBIKqQ==} + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.14: + resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} + engines: {node: '>=12.0.0'} + tinypool@1.0.2: resolution: {integrity: sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA==} engines: {node: ^18.0.0 || >=20.0.0} + tinypool@1.1.0: + resolution: {integrity: sha512-7CotroY9a8DKsKprEy/a14aCCm8jYVmR7aFy4fpkZM8sdpNJbKkixuNjgM50yCmip2ezc8z4N7k3oe2+rfRJCQ==} + engines: {node: ^18.0.0 || >=20.0.0} + tinyrainbow@1.2.0: resolution: {integrity: sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==} engines: {node: '>=14.0.0'} + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + tinyspy@3.0.2: resolution: {integrity: sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==} engines: {node: '>=14.0.0'} + tinyspy@4.0.3: + resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} + engines: {node: '>=14.0.0'} + title@3.5.3: resolution: {integrity: sha512-20JyowYglSEeCvZv3EZ0nZ046vLarO37prvV0mbtQV7C8DJPGgN967r8SJkqd3XK3K3lD3/Iyfp3avjfil8Q2Q==} hasBin: true @@ -12575,6 +12874,10 @@ packages: types-ramda@0.29.10: resolution: {integrity: sha512-5PJiW/eiTPyXXBYGZOYGezMl6qj7keBiZheRwfjJZY26QPHsNrjfJnz0mru6oeqqoTHOni893Jfd6zyUXfQRWg==} + typescript-json-schema@0.65.1: + resolution: {integrity: sha512-tuGH7ff2jPaUYi6as3lHyHcKpSmXIqN7/mu50x3HlYn0EHzLpmt3nplZ7EuhUkO0eqDRc9GqWNkfjgBPIS9kxg==} + hasBin: true + typescript-transform-paths@3.4.6: resolution: {integrity: sha512-qdgpCk9oRHkIBhznxaHAapCFapJt5e4FbFik7Y4qdqtp6VyC3smAIPoDEIkjZ2eiF7x5+QxUPYNwJAtw0thsTw==} peerDependencies: @@ -12585,6 +12888,11 @@ packages: engines: {node: '>=14.17'} hasBin: true + typescript@5.5.4: + resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + engines: {node: '>=14.17'} + hasBin: true + typescript@5.6.3: resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==} engines: {node: '>=14.17'} @@ -12939,6 +13247,11 @@ packages: engines: {node: ^18.0.0 || >=20.0.0} hasBin: true + vite-node@3.2.3: + resolution: {integrity: sha512-gc8aAifGuDIpZHrPjuHyP4dpQmYXqWw7D1GmDnWeNWP654UEXzVfQ5IHPSK5HaHkwB/+p1atpYpSdw/2kOv8iQ==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + vite-plugin-node-externals@0.0.1: resolution: {integrity: sha512-6xE4VXNhbBJ/1vUzQED/5n4AtOeSEwVQzpCBnz/HkILgbclkOeAp7ocV9dZts7UnPQzvlUxNr+LiXWqX6cn1Og==} engines: {node: '>=14.0.0'} @@ -13092,6 +13405,34 @@ packages: jsdom: optional: true + vitest@3.2.3: + resolution: {integrity: sha512-E6U2ZFXe3N/t4f5BwUaVCKRLHqUpk1CBWeMh78UT4VaTPH/2dyvH6ALl29JTovEPu9dVKr/K/J4PkXgrMbw4Ww==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.3 + '@vitest/ui': 3.2.3 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} @@ -13331,6 +13672,10 @@ packages: resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} engines: {node: '>= 6'} + yaml@2.0.0-1: + resolution: {integrity: sha512-W7h5dEhywMKenDJh2iX/LABkbFnBxasD27oyXWDS/feDsxiw0dD5ncXdYXgkvAsXIY2MpW/ZKkr9IU30DBdMNQ==} + engines: {node: '>= 6'} + yaml@2.5.1: resolution: {integrity: sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==} engines: {node: '>= 14'} @@ -13883,6 +14228,27 @@ snapshots: gl-matrix: 3.4.3 tslib: 2.8.1 + '@apidevtools/json-schema-ref-parser@9.1.2': + dependencies: + '@jsdevtools/ono': 7.1.3 + '@types/json-schema': 7.0.15 + call-me-maybe: 1.0.2 + js-yaml: 4.1.0 + + '@apidevtools/openapi-schemas@2.1.0': {} + + '@apidevtools/swagger-methods@3.0.2': {} + + '@apidevtools/swagger-parser@10.0.3(openapi-types@12.1.3)': + dependencies: + '@apidevtools/json-schema-ref-parser': 9.1.2 + '@apidevtools/openapi-schemas': 2.1.0 + '@apidevtools/swagger-methods': 3.0.2 + '@jsdevtools/ono': 7.1.3 + call-me-maybe: 1.0.2 + openapi-types: 12.1.3 + z-schema: 5.0.5 + '@arvinxu/layout-kit@1.4.0(@babel/core@7.26.0)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0)': dependencies: styled-components: 5.3.11(@babel/core@7.26.0)(react-dom@18.2.0(react@18.2.0))(react-is@18.3.1)(react@18.2.0) @@ -13913,7 +14279,7 @@ snapshots: '@babel/traverse': 7.25.9(supports-color@5.5.0) '@babel/types': 7.26.0 convert-source-map: 2.0.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -13933,7 +14299,7 @@ snapshots: '@babel/traverse': 7.25.9(supports-color@5.5.0) '@babel/types': 7.26.0 convert-source-map: 2.0.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -14267,7 +14633,7 @@ snapshots: '@babel/parser': 7.26.2 '@babel/template': 7.25.9 '@babel/types': 7.26.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.0(supports-color@5.5.0) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -14281,6 +14647,8 @@ snapshots: '@bcoe/v8-coverage@0.2.3': {} + '@bcoe/v8-coverage@1.0.2': {} + '@bloomberg/record-tuple-polyfill@0.0.4': {} '@braintree/sanitize-url@6.0.4': {} @@ -15034,7 +15402,7 @@ snapshots: '@eslint/config-array@0.20.0': dependencies: '@eslint/object-schema': 2.1.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -15048,7 +15416,7 @@ snapshots: '@eslint/eslintrc@2.1.4': dependencies: ajv: 6.12.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) espree: 9.6.1 globals: 13.24.0 ignore: 5.3.2 @@ -15062,7 +15430,7 @@ snapshots: '@eslint/eslintrc@3.3.1': dependencies: ajv: 6.12.6 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.4.0(supports-color@5.5.0) espree: 10.3.0 globals: 14.0.0 ignore: 5.3.2 @@ -15220,7 +15588,7 @@ snapshots: '@humanwhocodes/config-array@0.13.0': dependencies: '@humanwhocodes/object-schema': 2.0.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -15240,7 +15608,7 @@ snapshots: '@antfu/install-pkg': 0.1.1 '@antfu/utils': 0.7.10 '@iconify/types': 2.0.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) kolorist: 1.8.0 local-pkg: 0.4.3 transitivePeerDependencies: @@ -15543,6 +15911,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jsdevtools/ono@7.1.3': {} + '@kuzu/kuzu-wasm@0.7.0': dependencies: apache-arrow: 16.1.0 @@ -16263,6 +16633,8 @@ snapshots: colors: 1.2.5 string-argv: 0.3.2 + '@scarf/scarf@1.4.0': {} + '@selderee/plugin-htmlparser2@0.11.0': dependencies: domhandler: 5.0.3 @@ -16561,6 +16933,10 @@ snapshots: '@types/connect': 3.4.38 '@types/node': 22.15.21 + '@types/chai@5.2.2': + dependencies: + '@types/deep-eql': 4.0.2 + '@types/command-line-args@5.2.3': {} '@types/command-line-usage@5.0.4': {} @@ -16696,6 +17072,8 @@ snapshots: dependencies: '@types/ms': 0.7.34 + '@types/deep-eql@4.0.2': {} + '@types/docker-modem@3.0.6': dependencies: '@types/node': 22.15.21 @@ -16815,6 +17193,11 @@ snapshots: dependencies: '@types/node': 22.15.21 + '@types/jsonwebtoken@9.0.9': + dependencies: + '@types/ms': 0.7.34 + '@types/node': 22.15.21 + '@types/katex@0.16.7': {} '@types/keyv@3.1.4': @@ -16843,6 +17226,11 @@ snapshots: '@types/ms@0.7.34': {} + '@types/node-fetch@2.6.12': + dependencies: + '@types/node': 22.15.21 + form-data: 4.0.1 + '@types/node@12.20.55': {} '@types/node@17.0.45': {} @@ -16947,6 +17335,17 @@ snapshots: '@types/stack-utils@2.0.3': {} + '@types/swagger-jsdoc@6.0.4': {} + + '@types/swagger-ui-express@4.1.8': + dependencies: + '@types/express': 5.0.0 + '@types/serve-static': 1.15.7 + + '@types/tar-stream@3.1.4': + dependencies: + '@types/node': 22.15.21 + '@types/triple-beam@1.3.5': {} '@types/unist@2.0.11': {} @@ -16978,7 +17377,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@9.27.0)(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@9.27.0)(typescript@5.6.3) - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 9.27.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -16997,7 +17396,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/type-utils': 5.62.0(eslint@9.27.0)(typescript@5.8.2) '@typescript-eslint/utils': 5.62.0(eslint@9.27.0)(typescript@5.8.2) - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 9.27.0 graphemer: 1.4.0 ignore: 5.3.2 @@ -17017,7 +17416,7 @@ snapshots: '@typescript-eslint/type-utils': 6.21.0(eslint@8.57.1)(typescript@5.8.2) '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.2) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 8.57.1 graphemer: 1.4.0 ignore: 5.3.2 @@ -17034,7 +17433,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 9.27.0 optionalDependencies: typescript: 5.6.3 @@ -17046,7 +17445,7 @@ snapshots: '@typescript-eslint/scope-manager': 5.62.0 '@typescript-eslint/types': 5.62.0 '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.2) - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 9.27.0 optionalDependencies: typescript: 5.8.2 @@ -17059,7 +17458,7 @@ snapshots: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.2) '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 8.57.1 optionalDependencies: typescript: 5.8.2 @@ -17080,7 +17479,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) '@typescript-eslint/utils': 5.62.0(eslint@9.27.0)(typescript@5.6.3) - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 9.27.0 tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: @@ -17092,7 +17491,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.2) '@typescript-eslint/utils': 5.62.0(eslint@9.27.0)(typescript@5.8.2) - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 9.27.0 tsutils: 3.21.0(typescript@5.8.2) optionalDependencies: @@ -17104,7 +17503,7 @@ snapshots: dependencies: '@typescript-eslint/typescript-estree': 6.21.0(typescript@5.8.2) '@typescript-eslint/utils': 6.21.0(eslint@8.57.1)(typescript@5.8.2) - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) eslint: 8.57.1 ts-api-utils: 1.4.3(typescript@5.8.2) optionalDependencies: @@ -17120,10 +17519,10 @@ snapshots: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 - semver: 7.7.1 + semver: 7.7.2 tsutils: 3.21.0(typescript@5.6.3) optionalDependencies: typescript: 5.6.3 @@ -17134,10 +17533,10 @@ snapshots: dependencies: '@typescript-eslint/types': 5.62.0 '@typescript-eslint/visitor-keys': 5.62.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 - semver: 7.7.1 + semver: 7.7.2 tsutils: 3.21.0(typescript@5.8.2) optionalDependencies: typescript: 5.8.2 @@ -17148,7 +17547,7 @@ snapshots: dependencies: '@typescript-eslint/types': 6.21.0 '@typescript-eslint/visitor-keys': 6.21.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) globby: 11.1.0 is-glob: 4.0.3 minimatch: 9.0.3 @@ -17169,7 +17568,7 @@ snapshots: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.6.3) eslint: 9.27.0 eslint-scope: 5.1.1 - semver: 7.7.1 + semver: 7.7.2 transitivePeerDependencies: - supports-color - typescript @@ -17184,7 +17583,7 @@ snapshots: '@typescript-eslint/typescript-estree': 5.62.0(typescript@5.8.2) eslint: 9.27.0 eslint-scope: 5.1.1 - semver: 7.7.1 + semver: 7.7.2 transitivePeerDependencies: - supports-color - typescript @@ -17599,7 +17998,7 @@ snapshots: react-error-overlay: 6.0.9 react-refresh: 0.14.2 resolve: 1.22.8 - semver: 7.7.1 + semver: 7.7.2 yargs-parser: 21.1.1 optionalDependencies: '@umijs/mako-darwin-arm64': 0.9.6 @@ -17815,6 +18214,25 @@ snapshots: transitivePeerDependencies: - supports-color + '@vitest/coverage-v8@3.2.3(vitest@3.2.3(@types/debug@4.1.12)(@types/node@20.17.28)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.5.1))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 1.0.2 + ast-v8-to-istanbul: 0.3.3 + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.1.7 + magic-string: 0.30.17 + magicast: 0.3.5 + std-env: 3.9.0 + test-exclude: 7.0.1 + tinyrainbow: 2.0.0 + vitest: 3.2.3(@types/debug@4.1.12)(@types/node@20.17.28)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.5.1) + transitivePeerDependencies: + - supports-color + '@vitest/expect@2.1.5': dependencies: '@vitest/spy': 2.1.5 @@ -17822,6 +18240,14 @@ snapshots: chai: 5.1.2 tinyrainbow: 1.2.0 + '@vitest/expect@3.2.3': + dependencies: + '@types/chai': 5.2.2 + '@vitest/spy': 3.2.3 + '@vitest/utils': 3.2.3 + chai: 5.2.0 + tinyrainbow: 2.0.0 + '@vitest/mocker@2.1.5(vite@5.4.16(@types/node@22.15.21)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0))': dependencies: '@vitest/spy': 2.1.5 @@ -17830,31 +18256,65 @@ snapshots: optionalDependencies: vite: 5.4.16(@types/node@22.15.21)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) + '@vitest/mocker@3.2.3(vite@6.2.4(@types/node@20.17.28)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.5.1))': + dependencies: + '@vitest/spy': 3.2.3 + estree-walker: 3.0.3 + magic-string: 0.30.17 + optionalDependencies: + vite: 6.2.4(@types/node@20.17.28)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.5.1) + '@vitest/pretty-format@2.1.5': dependencies: tinyrainbow: 1.2.0 + '@vitest/pretty-format@3.2.3': + dependencies: + tinyrainbow: 2.0.0 + '@vitest/runner@2.1.5': dependencies: '@vitest/utils': 2.1.5 pathe: 1.1.2 + '@vitest/runner@3.2.3': + dependencies: + '@vitest/utils': 3.2.3 + pathe: 2.0.3 + strip-literal: 3.0.0 + '@vitest/snapshot@2.1.5': dependencies: '@vitest/pretty-format': 2.1.5 magic-string: 0.30.13 pathe: 1.1.2 + '@vitest/snapshot@3.2.3': + dependencies: + '@vitest/pretty-format': 3.2.3 + magic-string: 0.30.17 + pathe: 2.0.3 + '@vitest/spy@2.1.5': dependencies: tinyspy: 3.0.2 + '@vitest/spy@3.2.3': + dependencies: + tinyspy: 4.0.3 + '@vitest/utils@2.1.5': dependencies: '@vitest/pretty-format': 2.1.5 loupe: 3.1.2 tinyrainbow: 1.2.0 + '@vitest/utils@3.2.3': + dependencies: + '@vitest/pretty-format': 3.2.3 + loupe: 3.1.3 + tinyrainbow: 2.0.0 + '@webassemblyjs/ast@1.14.1': dependencies: '@webassemblyjs/helper-numbers': 1.13.2 @@ -17987,7 +18447,7 @@ snapshots: agent-base@7.1.1: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -18314,6 +18774,12 @@ snapshots: dependencies: tslib: 2.8.1 + ast-v8-to-istanbul@0.3.3: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + estree-walker: 3.0.3 + js-tokens: 9.0.1 + astral-regex@2.0.0: {} astring@1.9.0: {} @@ -18550,7 +19016,7 @@ snapshots: dependencies: bytes: 3.1.2 content-type: 1.0.5 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) http-errors: 2.0.0 iconv-lite: 0.5.2 on-finished: 2.4.1 @@ -18666,6 +19132,8 @@ snapshots: buffer-crc32@1.0.0: {} + buffer-equal-constant-time@1.0.1: {} + buffer-fill@1.0.0: {} buffer-from@0.1.2: {} @@ -18765,6 +19233,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + call-me-maybe@1.0.2: {} + callsites@3.1.0: {} camel-case@4.1.2: @@ -18823,6 +19293,14 @@ snapshots: loupe: 3.1.2 pathval: 2.0.0 + chai@5.2.0: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.1 + deep-eql: 5.0.2 + loupe: 3.1.2 + pathval: 2.0.0 + chalk-template@0.4.0: dependencies: chalk: 4.1.2 @@ -19120,6 +19598,8 @@ snapshots: commander@4.1.1: {} + commander@6.2.0: {} + commander@7.2.0: {} commander@8.3.0: {} @@ -19459,7 +19939,7 @@ snapshots: postcss-modules-scope: 3.2.1(postcss@8.5.3) postcss-modules-values: 4.0.0(postcss@8.5.3) postcss-value-parser: 4.2.0 - semver: 7.7.1 + semver: 7.7.2 webpack: 5.96.1(@swc/core@1.9.2(@swc/helpers@0.5.15))(webpack-cli@5.1.4(webpack@5.96.1)) css-loader@6.7.1(webpack@5.96.1(webpack-cli@5.1.4(webpack@5.96.1))): @@ -19471,7 +19951,7 @@ snapshots: postcss-modules-scope: 3.2.1(postcss@8.5.3) postcss-modules-values: 4.0.0(postcss@8.5.3) postcss-value-parser: 4.2.0 - semver: 7.7.1 + semver: 7.7.2 webpack: 5.96.1(webpack-cli@5.1.4(webpack@5.96.1)) css-prefers-color-scheme@6.0.3(postcss@8.5.3): @@ -19810,13 +20290,17 @@ snapshots: dependencies: ms: 2.1.2 - debug@4.3.7(supports-color@5.5.0): + debug@4.3.7: + dependencies: + ms: 2.1.3 + + debug@4.4.0(supports-color@5.5.0): dependencies: ms: 2.1.3 optionalDependencies: supports-color: 5.5.0 - debug@4.4.0: + debug@4.4.1: dependencies: ms: 2.1.3 @@ -19962,7 +20446,7 @@ snapshots: docker-modem@3.0.8: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) readable-stream: 3.6.2 split-ca: 1.0.1 ssh2: 1.16.0 @@ -20053,6 +20537,8 @@ snapshots: dotenv@16.4.5: {} + dotenv@16.5.0: {} + dotignore@0.1.2: dependencies: minimatch: 3.1.2 @@ -20228,6 +20714,10 @@ snapshots: optionalDependencies: wcwidth: 1.0.1 + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + editions@2.3.1: dependencies: errlop: 2.2.0 @@ -20282,7 +20772,7 @@ snapshots: engine.io-client@6.6.3: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7 engine.io-parser: 5.2.3 ws: 8.17.1 xmlhttprequest-ssl: 2.1.2 @@ -20301,7 +20791,7 @@ snapshots: base64id: 2.0.0 cookie: 0.7.2 cors: 2.8.5 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7 engine.io-parser: 5.2.3 ws: 8.17.1 transitivePeerDependencies: @@ -20440,6 +20930,8 @@ snapshots: es-module-lexer@1.5.4: {} + es-module-lexer@1.7.0: {} + es-object-atoms@1.0.0: dependencies: es-errors: 1.3.0 @@ -20702,7 +21194,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.2.2 @@ -20750,7 +21242,7 @@ snapshots: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.6 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7 escape-string-regexp: 4.0.0 eslint-scope: 8.3.0 eslint-visitor-keys: 4.2.0 @@ -20851,10 +21343,16 @@ snapshots: eventsource-parser@3.0.0: {} + eventsource-parser@3.0.2: {} + eventsource@3.0.5: dependencies: eventsource-parser: 3.0.0 + eventsource@4.0.0: + dependencies: + eventsource-parser: 3.0.2 + evp_bytestokey@1.0.3: dependencies: md5.js: 1.3.5 @@ -20924,6 +21422,8 @@ snapshots: expect-type@1.1.0: {} + expect-type@1.2.1: {} + expect@29.7.0: dependencies: '@jest/expect-utils': 29.7.0 @@ -21184,6 +21684,10 @@ snapshots: transitivePeerDependencies: - encoding + fdir@6.4.6(picomatch@4.0.2): + optionalDependencies: + picomatch: 4.0.2 + fecha@4.2.3: {} fetch-blob@3.2.0: @@ -21263,7 +21767,7 @@ snapshots: finalhandler@2.1.0: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) encodeurl: 2.0.0 escape-html: 1.0.3 on-finished: 2.4.1 @@ -21406,7 +21910,7 @@ snapshots: minimatch: 3.1.2 node-abort-controller: 3.1.1 schema-utils: 3.3.0 - semver: 7.7.1 + semver: 7.7.2 tapable: 2.2.1 typescript: 5.3.3 webpack: 5.96.1(@swc/core@1.9.2(@swc/helpers@0.5.15))(webpack-cli@5.1.4(webpack@5.96.1)) @@ -21423,7 +21927,7 @@ snapshots: minimatch: 3.1.2 node-abort-controller: 3.1.1 schema-utils: 3.3.0 - semver: 7.7.1 + semver: 7.7.2 tapable: 2.2.1 typescript: 5.3.3 webpack: 5.96.1(webpack-cli@5.1.4(webpack@5.96.1)) @@ -21440,7 +21944,7 @@ snapshots: minimatch: 3.1.2 node-abort-controller: 3.1.1 schema-utils: 3.3.0 - semver: 7.7.1 + semver: 7.7.2 tapable: 2.2.1 typescript: 5.8.2 webpack: 5.96.1(webpack-cli@5.1.4(webpack@5.96.1)) @@ -21602,7 +22106,7 @@ snapshots: dependencies: basic-ftp: 5.0.5 data-uri-to-buffer: 6.0.2 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fs-extra: 11.2.0 transitivePeerDependencies: - supports-color @@ -21676,6 +22180,15 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@7.1.6: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -22225,7 +22738,7 @@ snapshots: http-proxy-agent@7.0.2: dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -22268,7 +22781,7 @@ snapshots: https-proxy-agent@7.0.5: dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -22727,7 +23240,7 @@ snapshots: '@babel/parser': 7.26.2 '@istanbuljs/schema': 0.1.3 istanbul-lib-coverage: 3.2.2 - semver: 7.7.1 + semver: 7.7.2 transitivePeerDependencies: - supports-color @@ -22739,12 +23252,20 @@ snapshots: istanbul-lib-source-maps@4.0.1: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) istanbul-lib-coverage: 3.2.2 source-map: 0.6.1 transitivePeerDependencies: - supports-color + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.25 + debug: 4.4.1 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + istanbul-reports@3.1.7: dependencies: html-escaper: 2.0.2 @@ -23235,6 +23756,8 @@ snapshots: js-tokens@4.0.0: {} + js-tokens@9.0.1: {} + js-yaml@3.14.1: dependencies: argparse: 1.0.10 @@ -23298,6 +23821,19 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.2 + jspdf@2.5.2: dependencies: '@babel/runtime': 7.26.0 @@ -23324,6 +23860,17 @@ snapshots: readable-stream: 2.3.8 setimmediate: 1.0.5 + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + kapsule@1.14.6: dependencies: lodash-es: 4.17.21 @@ -23477,7 +24024,7 @@ snapshots: dependencies: chalk: 5.3.0 commander: 12.1.0 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7 execa: 8.0.1 lilconfig: 3.1.2 listr2: 8.2.5 @@ -23545,12 +24092,28 @@ snapshots: lodash.get@4.4.2: {} + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + lodash.isequal@4.5.0: {} + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.memoize@4.1.2: {} lodash.merge@4.6.2: {} + lodash.mergewith@4.6.2: {} + + lodash.once@4.1.1: {} + lodash.startcase@4.4.0: {} lodash.throttle@4.1.1: {} @@ -23595,6 +24158,8 @@ snapshots: loupe@3.1.2: {} + loupe@3.1.3: {} + lower-case@2.0.2: dependencies: tslib: 2.8.1 @@ -23624,6 +24189,16 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.0 + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + + magicast@0.3.5: + dependencies: + '@babel/parser': 7.26.2 + '@babel/types': 7.26.0 + source-map-js: 1.2.1 + make-dir@1.3.0: dependencies: pify: 3.0.0 @@ -23640,7 +24215,7 @@ snapshots: make-dir@4.0.0: dependencies: - semver: 7.7.1 + semver: 7.7.2 make-error@1.3.6: {} @@ -24401,7 +24976,7 @@ snapshots: micromark@3.2.0: dependencies: '@types/debug': 4.1.12 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) decode-named-character-reference: 1.0.2 micromark-core-commonmark: 1.1.0 micromark-factory-space: 1.1.0 @@ -24423,7 +24998,7 @@ snapshots: micromark@4.0.1: dependencies: '@types/debug': 4.1.12 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) decode-named-character-reference: 1.0.2 devlop: 1.1.0 micromark-core-commonmark: 2.0.2 @@ -25084,6 +25659,8 @@ snapshots: is-inside-container: 1.0.0 is-wsl: 2.2.0 + openapi-types@12.1.3: {} + opener@1.5.2: {} optionator@0.9.4: @@ -25161,7 +25738,7 @@ snapshots: dependencies: '@tootallnate/quickjs-emscripten': 0.23.0 agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) get-uri: 6.0.3 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.5 @@ -25306,6 +25883,8 @@ snapshots: path-browserify@0.0.1: {} + path-equal@1.2.5: {} + path-exists@3.0.0: {} path-exists@4.0.0: {} @@ -25345,6 +25924,8 @@ snapshots: pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@2.0.0: {} pbkdf2@3.1.2: @@ -25371,6 +25952,8 @@ snapshots: picomatch@2.3.1: {} + picomatch@4.0.2: {} + pidtree@0.3.1: {} pidtree@0.6.0: {} @@ -25429,7 +26012,7 @@ snapshots: portfinder@1.0.37: dependencies: async: 3.2.6 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -25822,7 +26405,7 @@ snapshots: proxy-agent@6.4.0: dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.5 lru-cache: 7.18.3 @@ -27088,7 +27671,7 @@ snapshots: send@1.1.0: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) destroy: 1.2.0 encodeurl: 2.0.0 escape-html: 1.0.3 @@ -27160,7 +27743,7 @@ snapshots: dependencies: color: 4.2.3 detect-libc: 2.0.3 - semver: 7.7.1 + semver: 7.7.2 optionalDependencies: '@img/sharp-darwin-arm64': 0.33.5 '@img/sharp-darwin-x64': 0.33.5 @@ -27288,7 +27871,7 @@ snapshots: socket.io-adapter@2.5.5: dependencies: - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7 ws: 8.17.1 transitivePeerDependencies: - bufferutil @@ -27298,7 +27881,7 @@ snapshots: socket.io-client@4.8.1: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7 engine.io-client: 6.6.3 socket.io-parser: 4.2.4 transitivePeerDependencies: @@ -27309,7 +27892,7 @@ snapshots: socket.io-parser@4.2.4: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7 transitivePeerDependencies: - supports-color @@ -27318,7 +27901,7 @@ snapshots: accepts: 1.3.8 base64id: 2.0.0 cors: 2.8.5 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7 engine.io: 6.6.4 socket.io-adapter: 2.5.5 socket.io-parser: 4.2.4 @@ -27335,7 +27918,7 @@ snapshots: socks-proxy-agent@8.0.4: dependencies: agent-base: 7.1.1 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) socks: 2.8.3 transitivePeerDependencies: - supports-color @@ -27431,7 +28014,7 @@ snapshots: spdy-transport@3.0.0: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -27442,7 +28025,7 @@ snapshots: spdy@4.0.2: dependencies: - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -27495,6 +28078,8 @@ snapshots: std-env@3.8.0: {} + std-env@3.9.0: {} + stop-iteration-iterator@1.0.0: dependencies: internal-slot: 1.0.7 @@ -27662,6 +28247,10 @@ snapshots: strip-json-comments@3.1.1: {} + strip-literal@3.0.0: + dependencies: + js-tokens: 9.0.1 + strip-outer@1.0.1: dependencies: escape-string-regexp: 1.0.5 @@ -27724,7 +28313,7 @@ snapshots: colord: 2.9.3 cosmiconfig: 7.1.0 css-functions-list: 3.2.3 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fast-glob: 3.3.3 fastest-levenshtein: 1.0.16 file-entry-cache: 6.0.1 @@ -27778,7 +28367,7 @@ snapshots: dependencies: component-emitter: 1.3.1 cookiejar: 2.1.4 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) fast-safe-stringify: 2.1.1 form-data: 4.0.1 formidable: 2.1.5 @@ -27863,6 +28452,32 @@ snapshots: omit-deep: 0.3.0 xml-reader: 2.4.3 + swagger-jsdoc@6.2.8(openapi-types@12.1.3): + dependencies: + commander: 6.2.0 + doctrine: 3.0.0 + glob: 7.1.6 + lodash.mergewith: 4.6.2 + swagger-parser: 10.0.3(openapi-types@12.1.3) + yaml: 2.0.0-1 + transitivePeerDependencies: + - openapi-types + + swagger-parser@10.0.3(openapi-types@12.1.3): + dependencies: + '@apidevtools/swagger-parser': 10.0.3(openapi-types@12.1.3) + transitivePeerDependencies: + - openapi-types + + swagger-ui-dist@5.24.1: + dependencies: + '@scarf/scarf': 1.4.0 + + swagger-ui-express@5.0.1(express@4.21.2): + dependencies: + express: 4.21.2 + swagger-ui-dist: 5.24.1 + synckit@0.8.5: dependencies: '@pkgr/utils': 2.4.2 @@ -27981,6 +28596,12 @@ snapshots: glob: 7.2.3 minimatch: 3.1.2 + test-exclude@7.0.1: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.4.5 + minimatch: 9.0.5 + text-decoder@1.2.3: dependencies: b4a: 1.6.7 @@ -28011,7 +28632,7 @@ snapshots: threads@1.7.0: dependencies: callsites: 3.1.0 - debug: 4.4.0 + debug: 4.4.0(supports-color@5.5.0) is-observable: 2.1.0 observable-fns: 0.6.1 optionalDependencies: @@ -28073,12 +28694,25 @@ snapshots: tinyexec@0.3.1: {} + tinyexec@0.3.2: {} + + tinyglobby@0.2.14: + dependencies: + fdir: 6.4.6(picomatch@4.0.2) + picomatch: 4.0.2 + tinypool@1.0.2: {} + tinypool@1.1.0: {} + tinyrainbow@1.2.0: {} + tinyrainbow@2.0.0: {} + tinyspy@3.0.2: {} + tinyspy@4.0.3: {} + title@3.5.3: dependencies: arg: 1.0.0 @@ -28168,6 +28802,26 @@ snapshots: typescript: 5.8.2 webpack: 5.96.1(webpack-cli@5.1.4(webpack@5.96.1)) + ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@18.19.103)(typescript@5.5.4): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 18.19.103 + acorn: 8.14.0 + acorn-walk: 8.3.4 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.5.4 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optionalDependencies: + '@swc/core': 1.9.2(@swc/helpers@0.5.15) + ts-node@10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@20.17.28)(typescript@5.8.2): dependencies: '@cspotcode/source-map-support': 0.8.1 @@ -28328,6 +28982,20 @@ snapshots: dependencies: ts-toolbelt: 9.6.0 + typescript-json-schema@0.65.1(@swc/core@1.9.2(@swc/helpers@0.5.15)): + dependencies: + '@types/json-schema': 7.0.15 + '@types/node': 18.19.103 + glob: 7.2.3 + path-equal: 1.2.5 + safe-stable-stringify: 2.5.0 + ts-node: 10.9.2(@swc/core@1.9.2(@swc/helpers@0.5.15))(@types/node@18.19.103)(typescript@5.5.4) + typescript: 5.5.4 + yargs: 17.7.2 + transitivePeerDependencies: + - '@swc/core' + - '@swc/wasm' + typescript-transform-paths@3.4.6(typescript@5.3.3): dependencies: minimatch: 3.1.2 @@ -28335,6 +29003,8 @@ snapshots: typescript@5.3.3: {} + typescript@5.5.4: {} + typescript@5.6.3: {} typescript@5.8.2: {} @@ -28771,7 +29441,7 @@ snapshots: vite-node@2.1.5(@types/node@22.15.21)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0): dependencies: cac: 6.7.14 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7 es-module-lexer: 1.5.4 pathe: 1.1.2 vite: 5.4.16(@types/node@22.15.21)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0) @@ -28786,6 +29456,27 @@ snapshots: - supports-color - terser + vite-node@3.2.3(@types/node@20.17.28)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.5.1): + dependencies: + cac: 6.7.14 + debug: 4.4.1 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 6.2.4(@types/node@20.17.28)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.5.1) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vite-plugin-node-externals@0.0.1(rollup@4.38.0)(vite@5.4.16(@types/node@22.9.1)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)): dependencies: rollup-plugin-node-externals: 8.0.0(rollup@4.38.0) @@ -28874,6 +29565,21 @@ snapshots: sass: 1.81.0 terser: 5.36.0 + vite@6.2.4(@types/node@20.17.28)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.5.1): + dependencies: + esbuild: 0.25.2 + postcss: 8.5.3 + rollup: 4.38.0 + optionalDependencies: + '@types/node': 20.17.28 + fsevents: 2.3.3 + less: 4.2.0 + lightningcss: 1.22.1 + sass: 1.81.0 + terser: 5.36.0 + tsx: 4.19.4 + yaml: 2.5.1 + vite@6.2.4(@types/node@22.15.21)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.5.1): dependencies: esbuild: 0.25.2 @@ -28899,7 +29605,7 @@ snapshots: '@vitest/spy': 2.1.5 '@vitest/utils': 2.1.5 chai: 5.1.2 - debug: 4.3.7(supports-color@5.5.0) + debug: 4.3.7 expect-type: 1.1.0 magic-string: 0.30.13 pathe: 1.1.2 @@ -28924,6 +29630,48 @@ snapshots: - supports-color - terser + vitest@3.2.3(@types/debug@4.1.12)(@types/node@20.17.28)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.5.1): + dependencies: + '@types/chai': 5.2.2 + '@vitest/expect': 3.2.3 + '@vitest/mocker': 3.2.3(vite@6.2.4(@types/node@20.17.28)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.5.1)) + '@vitest/pretty-format': 3.2.3 + '@vitest/runner': 3.2.3 + '@vitest/snapshot': 3.2.3 + '@vitest/spy': 3.2.3 + '@vitest/utils': 3.2.3 + chai: 5.2.0 + debug: 4.4.1 + expect-type: 1.2.1 + magic-string: 0.30.17 + pathe: 2.0.3 + picomatch: 4.0.2 + std-env: 3.9.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.14 + tinypool: 1.1.0 + tinyrainbow: 2.0.0 + vite: 6.2.4(@types/node@20.17.28)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.5.1) + vite-node: 3.2.3(@types/node@20.17.28)(less@4.2.0)(lightningcss@1.22.1)(sass@1.81.0)(terser@5.36.0)(tsx@4.19.4)(yaml@2.5.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/debug': 4.1.12 + '@types/node': 20.17.28 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + vm-browserify@1.1.2: {} vscode-oniguruma@1.7.0: {} @@ -29219,6 +29967,8 @@ snapshots: yaml@1.10.2: {} + yaml@2.0.0-1: {} + yaml@2.5.1: {} yargs-parser@20.2.9: {}