diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..4ccb27e --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,15 @@ +{ + "permissions": { + "allow": [ + "Bash(mkdir:*)", + "Bash(pnpm create:*)", + "Bash(pnpm install:*)", + "Bash(pnpm add:*)", + "Bash(pnpm dlx:*)", + "WebFetch(domain:www.shadcn-vue.com)", + "Bash(pnpm build)" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file diff --git a/app.js b/app.js index 8aa677b..2217d39 100644 --- a/app.js +++ b/app.js @@ -8,14 +8,16 @@ import logger from "morgan"; import bodyParser from "body-parser"; import errorHandler from "./middleware/errorHandler.js"; import errors from "./utils/errors.js"; -import { initReadme, getReadmeValue } from "./utils/siteinfo.js"; import { globalLimiter, apiLimiter, methodBasedRateLimiter, + tokenBasedRateLimiter, } from "./middleware/rateLimiter.js"; -import kvRouter from "./routes/kv.js"; +import kvRouter from "./routes/kv-token.js"; +import appsRouter from "./routes/apps.js"; +import deviceAuthRouter from "./routes/device-auth.js"; var app = express(); @@ -33,9 +35,6 @@ app.disable("x-powered-by"); const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); -// 初始化 readme -initReadme(); - // 应用全局限速 app.use(globalLimiter); @@ -73,7 +72,7 @@ app.use((req, res, next) => { next(); }); app.get("/", (req, res) => { - res.render("index.ejs", { readmeValue: getReadmeValue() }); + res.render("index.ejs"); }); app.get("/check", apiLimiter, (req, res) => { res.json({ @@ -83,8 +82,14 @@ app.get("/check", apiLimiter, (req, res) => { }); }); -// Mount the KV store router with method-based rate limiting -app.use("/", methodBasedRateLimiter, kvRouter); +// Mount the Apps router with API rate limiting +app.use("/apps", apiLimiter, appsRouter); + +// Mount the KV store router with token-based rate limiting (更宽松的限速) +app.use("/kv", tokenBasedRateLimiter, kvRouter); + +// Mount the Device Authorization router with API rate limiting +app.use("/auth", apiLimiter, deviceAuthRouter); // 兜底404路由 - 处理所有未匹配的路由 app.use((req, res, next) => { diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000..eefc38c --- /dev/null +++ b/cli/README.md @@ -0,0 +1,98 @@ +# 设备授权流程 - CLI 工具 + +命令行工具,用于通过设备授权流程获取访问令牌。 + +## 使用方法 + +### 基本使用 + +```bash +node cli/get-token.js +``` + +### 配置环境变量 + +```bash +# 设置API服务器地址(默认: http://localhost:3030) +export API_BASE_URL=https://your-api-server.com + +# 设置授权页面地址(默认: https://classworks.xiaomo.tech/authorize) +export AUTH_PAGE_URL=https://your-classworks-frontend.com/authorize + +# 设置应用ID(默认: 1) +export APP_ID=1 + +# 设置站点密钥(如果需要) +export SITE_KEY=your-site-key + +# 运行工具 +node cli/get-token.js +``` + +### 使其可执行(Linux/Mac) + +```bash +chmod +x cli/get-token.js +./cli/get-token.js +``` + +## 工作流程 + +1. **生成设备代码** - 工具会自动调用 API 生成形如 `1234-ABCD` 的授权码 +2. **显示授权链接** - 在终端显示完整的授权URL,包含设备代码 +3. **等待授权** - 用户点击链接或在授权页面手动输入设备代码完成授权 +4. **获取令牌** - 工具自动轮询并获取令牌 +5. **保存令牌** - 令牌会保存到 `~/.classworks/token.txt` + +## 输出示例 + +``` +设备授权流程 - 令牌获取工具 + +✓ 设备授权码生成成功! + +============================================================ + 请访问以下地址完成授权: + + https://classworks.xiaomo.tech/authorize?app_id=1&mode=devicecode&devicecode=1234-ABCD + + 设备授权码: 1234-ABCD +============================================================ +ℹ 授权码有效期: 15 分钟 +ℹ API服务器: http://localhost:3030 + +ℹ 请在浏览器中打开上述地址,或在授权页面手动输入设备代码 +ℹ 等待授权中... + +等待授权... (1/100) +等待授权... (2/100) + +================================================== +✓ 授权成功!令牌获取完成 +================================================== + +您的访问令牌: +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + +✓ 令牌已保存到: /home/user/.classworks/token.txt + +使用示例: + curl -H "Authorization: Bearer eyJhbGc..." http://localhost:3030/kv +``` + +## 配置选项 + +可以通过修改 `cli/get-token.js` 中的 `CONFIG` 对象或设置环境变量来调整: + +- `baseUrl` / `API_BASE_URL` - API 服务器地址(默认: http://localhost:3030) +- `authPageUrl` / `AUTH_PAGE_URL` - Classworks 授权页面地址(默认: https://classworks.xiaomo.tech/authorize) +- `appId` / `APP_ID` - 应用ID(默认: 1) +- `siteKey` / `SITE_KEY` - 站点密钥(如果需要) +- `pollInterval` - 轮询间隔(秒,默认3秒) +- `maxPolls` - 最大轮询次数(默认100次) + +## 错误处理 + +- 如果设备代码过期,会显示错误并退出 +- 如果轮询超时(默认5分钟),会显示超时错误 +- 如果无法连接到服务器,会显示连接错误 diff --git a/cli/get-token.js b/cli/get-token.js new file mode 100644 index 0000000..789570d --- /dev/null +++ b/cli/get-token.js @@ -0,0 +1,233 @@ +#!/usr/bin/env node + +/** + * 设备授权流程 - 命令行工具 + * + * 用于演示设备授权流程,获取访问令牌 + * + * 使用方法: + * node cli/get-token.js + * 或配置为可执行:chmod +x cli/get-token.js && ./cli/get-token.js + */ + +import readline from 'readline'; + +// 配置 +const CONFIG = { + // API服务器地址 + baseUrl: process.env.API_BASE_URL || 'http://localhost:3030', + // 站点密钥 + siteKey: process.env.SITE_KEY || '', + // 应用ID + appId: process.env.APP_ID || '1', + // 授权页面地址(Classworks前端) + authPageUrl: process.env.AUTH_PAGE_URL || 'http://localhost:5173/authorize', + // 轮询间隔(秒) + pollInterval: 3, + // 最大轮询次数 + maxPolls: 100, +}; + +// 颜色输出 +const colors = { + reset: '\x1b[0m', + bright: '\x1b[1m', + dim: '\x1b[2m', + red: '\x1b[31m', + green: '\x1b[32m', + yellow: '\x1b[33m', + blue: '\x1b[34m', + cyan: '\x1b[36m', +}; + +function log(message, color = '') { + console.log(`${color}${message}${colors.reset}`); +} + +function logSuccess(message) { + log(`✓ ${message}`, colors.green); +} + +function logError(message) { + log(`✗ ${message}`, colors.red); +} + +function logInfo(message) { + log(`ℹ ${message}`, colors.cyan); +} + +function logWarning(message) { + log(`⚠ ${message}`, colors.yellow); +} + +// HTTP请求封装 +async function request(path, options = {}) { + const url = `${CONFIG.baseUrl}${path}`; + const headers = { + 'Content-Type': 'application/json', + ...options.headers, + }; + + if (CONFIG.siteKey) { + headers['X-Site-Key'] = CONFIG.siteKey; + } + + try { + const response = await fetch(url, { + ...options, + headers, + }); + + const data = await response.json(); + + if (!response.ok) { + throw new Error(data.message || `HTTP ${response.status}`); + } + + return data; + } catch (error) { + if (error.message.includes('fetch')) { + throw new Error(`无法连接到服务器: ${CONFIG.baseUrl}`); + } + throw error; + } +} + +// 生成设备代码 +async function generateDeviceCode() { + logInfo('正在生成设备授权码...'); + const data = await request('/auth/device/code', { + method: 'POST', + }); + return data; +} + +// 轮询获取令牌 +async function pollForToken(deviceCode) { + let polls = 0; + + return new Promise((resolve, reject) => { + const poll = async () => { + polls++; + + if (polls > CONFIG.maxPolls) { + reject(new Error('轮询超时,请重试')); + return; + } + + try { + const data = await request(`/auth/device/token?device_code=${deviceCode}`); + + if (data.status === 'success') { + resolve(data.token); + } else if (data.status === 'expired') { + reject(new Error('设备代码已过期')); + } else if (data.status === 'pending') { + // 继续轮询 + log(`等待授权... (${polls}/${CONFIG.maxPolls})`, colors.dim); + setTimeout(poll, CONFIG.pollInterval * 1000); + } + } catch (error) { + reject(error); + } + }; + + // 开始轮询 + poll(); + }); +} + +// 显示设备代码和授权链接 +function displayDeviceCode(deviceCode, expiresIn) { + console.log('\n' + '='.repeat(60)); + log(` 请访问以下地址完成授权:`, colors.bright); + console.log(''); + + // 构建授权URL + const authUrl = `${CONFIG.authPageUrl}?app_id=${CONFIG.appId}&mode=devicecode&devicecode=${deviceCode}`; + log(` ${authUrl}`, colors.cyan + colors.bright); + console.log(''); + log(` 设备授权码: ${deviceCode}`, colors.green + colors.bright); + console.log('='.repeat(60)); + logInfo(`授权码有效期: ${Math.floor(expiresIn / 60)} 分钟`); + logInfo(`API服务器: ${CONFIG.baseUrl}`); + console.log(''); +} + +// 保存令牌到文件 +async function saveToken(token) { + const fs = await import('fs'); + const path = await import('path'); + const os = await import('os'); + + const tokenDir = path.join(os.homedir(), '.classworks'); + const tokenFile = path.join(tokenDir, 'token.txt'); + + try { + // 确保目录存在 + if (!fs.existsSync(tokenDir)) { + fs.mkdirSync(tokenDir, { recursive: true }); + } + + // 写入令牌 + fs.writeFileSync(tokenFile, token, 'utf8'); + logSuccess(`令牌已保存到: ${tokenFile}`); + } catch (error) { + logWarning(`无法保存令牌到文件: ${error.message}`); + logInfo('您可以手动保存令牌'); + } +} + +// 主函数 +async function main() { + console.log('\n' + colors.cyan + colors.bright + '设备授权流程 - 令牌获取工具' + colors.reset + '\n'); + + try { + // 检查配置 + if (!CONFIG.siteKey) { + logWarning('未设置 SITE_KEY 环境变量,可能需要站点密钥才能访问'); + logInfo('设置方法: export SITE_KEY=your-site-key'); + console.log(''); + } + + // 1. 生成设备代码 + const { device_code, expires_in } = await generateDeviceCode(); + logSuccess('设备授权码生成成功!'); + + // 2. 显示设备代码和授权链接 + displayDeviceCode(device_code, expires_in); + + // 3. 提示用户授权 + logInfo('请在浏览器中打开上述地址,或在授权页面手动输入设备代码'); + logInfo('等待授权中...\n'); + + // 4. 轮询获取令牌 + const token = await pollForToken(device_code); + + // 5. 显示令牌 + console.log('\n' + '='.repeat(50)); + logSuccess('授权成功!令牌获取完成'); + console.log('='.repeat(50)); + console.log('\n' + colors.bright + '您的访问令牌:' + colors.reset); + log(token, colors.green); + console.log(''); + + // 6. 保存令牌 + await saveToken(token); + + // 7. 使用示例 + console.log('\n' + colors.bright + '使用示例:' + colors.reset); + console.log(` curl -H "Authorization: Bearer ${token}" ${CONFIG.baseUrl}/kv`); + console.log(''); + + process.exit(0); + } catch (error) { + console.log(''); + logError(`错误: ${error.message}`); + console.log(''); + process.exit(1); + } +} + +// 运行 +main(); diff --git a/docs/API_CURL_EXAMPLES.md b/docs/API_CURL_EXAMPLES.md new file mode 100644 index 0000000..c2c9ed8 --- /dev/null +++ b/docs/API_CURL_EXAMPLES.md @@ -0,0 +1,671 @@ +# API 使用示例 - cURL + +本文档提供所有API接口的完整cURL示例。 + +## 环境变量设置 + +```bash +# 设置基础URL和站点密钥 +export BASE_URL="http://localhost:3030" +export SITE_KEY="your-site-key-here" +``` + +## 1. 应用管理 API + +### 1.1 获取应用列表 + +```bash +# 基本查询 +curl -X GET "http://localhost:3030/apps" \ + -H "x-site-key: ${SITE_KEY}" + +# 带分页和搜索 +curl -X GET "http://localhost:3030/apps?limit=10&skip=0&search=my-app" \ + -H "x-site-key: ${SITE_KEY}" +``` + +**响应示例:** +```json +{ + "apps": [ + { + "id": 1, + "name": "我的应用", + "description": "应用描述", + "developerName": "开发者名称", + "developerLink": "https://developer.com", + "homepageLink": "https://app.com", + "iconHash": "abc123", + "metadata": {}, + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" + } + ], + "total": 1, + "limit": 10, + "skip": 0 +} +``` + +### 1.2 获取单个应用详情 + +```bash +curl -X GET "http://localhost:3030/apps/1" \ + -H "x-site-key: ${SITE_KEY}" +``` + +**响应示例:** +```json +{ + "id": 1, + "name": "我的应用", + "description": "应用描述", + "developerName": "开发者名称", + "developerLink": "https://developer.com", + "homepageLink": "https://app.com", + "iconHash": "abc123", + "metadata": {}, + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" +} +``` + +### 1.4 获取应用的所有安装记录 + +```bash +curl -X GET "http://localhost:3030/apps/1/installations?limit=10&skip=0" \ + -H "x-site-key: ${SITE_KEY}" +``` + +**响应示例:** +```json +{ + "appId": 1, + "installations": [ + { + "id": "clx1234567890", + "token": "a1b2c3d4e5f6...", + "device": { + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "name": "我的设备" + }, + "note": "完整访问", + "installedAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" + } + ], + "total": 1, + "limit": 10, + "skip": 0 +} +``` + +## 2. Token 管理 API + +### 2.1 获取设备的所有Token + +```bash +curl -X GET "http://localhost:3030/apps/devices/550e8400-e29b-41d4-a716-446655440000/tokens" \ + -H "x-site-key: ${SITE_KEY}" +``` + +**响应示例:** +```json +{ + "deviceUuid": "550e8400-e29b-41d4-a716-446655440000", + "deviceName": "我的设备", + "tokens": [ + { + "id": "clx1234567890", + "token": "a1b2c3d4e5f6...", + "app": { + "id": 1, + "name": "我的应用", + "description": "应用描述", + "developerName": "开发者", + "iconHash": "abc123" + }, + "note": "完整访问", + "installedAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" + } + ], + "total": 1 +} +``` + +### 2.2 撤销Token + +```bash +curl -X DELETE "http://localhost:3030/apps/tokens/a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" \ + -H "x-site-key: ${SITE_KEY}" +``` + +**成功响应:** HTTP 204 No Content + +**错误响应:** +```json +{ + "statusCode": 404, + "message": "Token不存在" +} +``` + +## 3. KV 操作 API(需要Token) + +**设置Token环境变量:** +```bash +export TOKEN="a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6" +``` + +### 3.1 获取所有键(含元数据) + +```bash +# 基本查询 +curl -X GET "http://localhost:3030/kv" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" + +# 带分页和排序 +curl -X GET "http://localhost:3030/kv?sortBy=key&sortDir=asc&limit=50&skip=0" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" + +# 按更新时间排序 +curl -X GET "http://localhost:3030/kv?sortBy=updatedAt&sortDir=desc&limit=20" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" +``` + +**响应示例:** +```json +{ + "items": [ + { + "deviceId": 1, + "key": "user.profile", + "metadata": { + "creatorIp": "192.168.1.1", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T00:00:00.000Z" + } + } + ], + "total_rows": 1, + "load_more": "/kv?sortBy=key&sortDir=asc&limit=50&skip=50" +} +``` + +### 3.2 获取键名列表(仅键名) + +```bash +curl -X GET "http://localhost:3030/kv/_keys" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" + +# 带分页 +curl -X GET "http://localhost:3030/kv/_keys?limit=100&skip=0" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" +``` + +**响应示例:** +```json +{ + "keys": [ + "user.profile", + "user.settings", + "app.config" + ], + "total_rows": 3, + "current_page": { + "limit": 100, + "skip": 0, + "count": 3 + } +} +``` + +### 3.3 获取键值 + +```bash +# 使用 Authorization header(推荐) +curl -X GET "http://localhost:3030/kv/user.profile" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" + +# 使用 query 参数 +curl -X GET "http://localhost:3030/kv/user.profile?token=${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" +``` + +**响应示例:** +```json +{ + "name": "张三", + "email": "zhangsan@example.com", + "avatar": "https://example.com/avatar.jpg", + "preferences": { + "theme": "dark", + "language": "zh-CN" + } +} +``` + +**错误响应(键不存在):** +```json +{ + "statusCode": 404, + "message": "未找到键名为 'user.profile' 的记录" +} +``` + +### 3.4 获取键的元数据 + +```bash +curl -X GET "http://localhost:3030/kv/user.profile/metadata" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" +``` + +**响应示例:** +```json +{ + "deviceId": 1, + "key": "user.profile", + "metadata": { + "creatorIp": "192.168.1.1", + "createdAt": "2025-01-01T00:00:00.000Z", + "updatedAt": "2025-01-01T12:30:00.000Z" + } +} +``` + +### 3.5 创建/更新键值 + +```bash +# 创建新键 +curl -X POST "http://localhost:3030/kv/user.profile" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-site-key: ${SITE_KEY}" \ + -d '{ + "name": "张三", + "email": "zhangsan@example.com", + "avatar": "https://example.com/avatar.jpg" + }' + +# 更新已存在的键 +curl -X POST "http://localhost:3030/kv/user.profile" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-site-key: ${SITE_KEY}" \ + -d '{ + "name": "张三", + "email": "newemail@example.com", + "avatar": "https://example.com/new-avatar.jpg", + "updatedBy": "admin" + }' + +# 存储数组 +curl -X POST "http://localhost:3030/kv/user.tags" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-site-key: ${SITE_KEY}" \ + -d '["developer", "admin", "vip"]' + +# 存储嵌套对象 +curl -X POST "http://localhost:3030/kv/app.config" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-site-key: ${SITE_KEY}" \ + -d '{ + "database": { + "host": "localhost", + "port": 3306, + "name": "mydb" + }, + "cache": { + "enabled": true, + "ttl": 3600 + } + }' +``` + +**响应示例(创建):** +```json +{ + "deviceId": 1, + "key": "user.profile", + "created": true, + "updatedAt": "2025-01-01T00:00:00.000Z" +} +``` + +**响应示例(更新):** +```json +{ + "deviceId": 1, + "key": "user.profile", + "created": false, + "updatedAt": "2025-01-01T12:30:00.000Z" +} +``` + +### 3.6 批量导入键值对 + +```bash +curl -X POST "http://localhost:3030/kv/_batchimport" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-site-key: ${SITE_KEY}" \ + -d '{ + "user.profile": { + "name": "张三", + "email": "zhangsan@example.com" + }, + "user.settings": { + "theme": "dark", + "language": "zh-CN" + }, + "app.config": { + "version": "1.0.0", + "debug": false + } + }' +``` + +**响应示例:** +```json +{ + "deviceId": 1, + "total": 3, + "successful": 3, + "failed": 0, + "results": [ + { + "key": "user.profile", + "created": true + }, + { + "key": "user.settings", + "created": true + }, + { + "key": "app.config", + "created": false + } + ] +} +``` + +**部分失败响应:** +```json +{ + "deviceId": 1, + "total": 3, + "successful": 2, + "failed": 1, + "results": [ + { + "key": "user.profile", + "created": true + }, + { + "key": "user.settings", + "created": true + } + ], + "errors": [ + { + "key": "invalid.key", + "error": "验证失败" + } + ] +} +``` + +### 3.7 删除键值对 + +```bash +curl -X DELETE "http://localhost:3030/kv/user.profile" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" +``` + +**成功响应:** HTTP 204 No Content + +**错误响应(键不存在):** +```json +{ + "statusCode": 404, + "message": "未找到键名为 'user.profile' 的记录" +} +``` + +## 4. 完整工作流示例 + +### 场景:应用首次访问设备的KV存储 + +```bash +#!/bin/bash + +# 1. 设置环境变量 +export BASE_URL="http://localhost:3030" +export SITE_KEY="your-site-key" +export APP_ID="1" +export DEVICE_UUID="550e8400-e29b-41d4-a716-446655440000" +export DEVICE_PASSWORD="my-password" + +# 2. 为应用授权获取token +echo "正在授权应用..." +RESPONSE=$(curl -s -X POST "http://localhost:3030/apps/${APP_ID}/authorize" \ + -H "Content-Type: application/json" \ + -H "x-site-key: ${SITE_KEY}" \ + -d "{ + \"deviceUuid\": \"${DEVICE_UUID}\", + \"password\": \"${DEVICE_PASSWORD}\", + \"readOnly\": false, + \"note\": \"自动授权\" + }") + +# 3. 提取token +TOKEN=$(echo $RESPONSE | jq -r '.token') +echo "获取到Token: ${TOKEN:0:20}..." + +# 4. 写入数据 +echo "写入用户配置..." +curl -X POST "http://localhost:3030/kv/user.config" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-site-key: ${SITE_KEY}" \ + -d '{ + "theme": "dark", + "notifications": true, + "language": "zh-CN" + }' + +# 5. 读取数据 +echo "读取用户配置..." +curl -X GET "http://localhost:3030/kv/user.config" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" + +# 6. 获取所有键名 +echo "获取所有键名..." +curl -X GET "http://localhost:3030/kv/_keys" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" + +# 7. 批量导入数据 +echo "批量导入数据..." +curl -X POST "http://localhost:3030/kv/_batchimport" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-site-key: ${SITE_KEY}" \ + -d '{ + "user.profile": {"name": "张三", "age": 25}, + "user.preferences": {"color": "blue"}, + "app.version": {"current": "1.0.0"} + }' + +echo "完成!" +``` + +## 5. 错误处理示例 + +### 5.1 Token认证失败 + +```bash +# 使用无效token +curl -X GET "http://localhost:3030/kv/mykey" \ + -H "Authorization: Bearer invalid-token" \ + -H "x-site-key: ${SITE_KEY}" +``` + +**响应:** +```json +{ + "statusCode": 401, + "message": "无效的身份验证令牌" +} +``` + +### 5.2 缺少Token + +```bash +curl -X GET "http://localhost:3030/kv/mykey" \ + -H "x-site-key: ${SITE_KEY}" +``` + +**响应:** +```json +{ + "statusCode": 401, + "message": "未提供身份验证令牌" +} +``` + +### 5.3 站点密钥错误 + +```bash +curl -X GET "http://localhost:3030/apps" \ + -H "x-site-key: wrong-key" +``` + +**响应:** +```json +{ + "statusCode": 401, + "message": "无效的站点密钥" +} +``` + +### 5.4 设备不存在 + +```bash +curl -X POST "http://localhost:3030/apps/1/authorize" \ + -H "Content-Type: application/json" \ + -H "x-site-key: ${SITE_KEY}" \ + -d '{ + "deviceUuid": "non-existent-uuid" + }' +``` + +**响应:** +```json +{ + "statusCode": 404, + "message": "设备不存在" +} +``` + +## 6. 高级用例 + +### 6.1 使用jq处理响应 + +```bash +# 提取所有键名 +curl -s -X GET "http://localhost:3030/kv/_keys" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" \ + | jq -r '.keys[]' + +# 获取token并保存 +TOKEN=$(curl -s -X POST "http://localhost:3030/apps/1/authorize" \ + -H "Content-Type: application/json" \ + -H "x-site-key: ${SITE_KEY}" \ + -d '{"deviceUuid":"550e8400-e29b-41d4-a716-446655440000"}' \ + | jq -r '.token') + +# 格式化输出 +curl -s -X GET "http://localhost:3030/kv/user.profile" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" \ + | jq '.' +``` + +### 6.2 循环批量操作 + +```bash +# 批量创建键值对 +for i in {1..10}; do + curl -X POST "http://localhost:3030/kv/item.${i}" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-site-key: ${SITE_KEY}" \ + -d "{\"id\": ${i}, \"name\": \"Item ${i}\"}" + echo "Created item.${i}" +done + +# 批量读取 +for key in user.profile user.settings app.config; do + echo "Reading ${key}:" + curl -s -X GET "http://localhost:3030/kv/${key}" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" \ + | jq '.' +done +``` + +### 6.3 条件更新模式 + +```bash +# 读取当前值 +CURRENT=$(curl -s -X GET "http://localhost:3030/kv/counter" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}") + +# 修改值 +NEW_VALUE=$(echo $CURRENT | jq '.count += 1') + +# 写回 +curl -X POST "http://localhost:3030/kv/counter" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -H "x-site-key: ${SITE_KEY}" \ + -d "${NEW_VALUE}" +``` + +## 7. 性能测试 + +### 7.1 并发请求测试 + +```bash +# 使用 xargs 进行并发测试 +seq 1 10 | xargs -P 10 -I {} curl -s -X GET "http://localhost:3030/kv/test.key" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" \ + -o /dev/null -w "Request {}: %{http_code} in %{time_total}s\n" +``` + +### 7.2 响应时间测试 + +```bash +# 测量单个请求时间 +curl -X GET "http://localhost:3030/kv/user.profile" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "x-site-key: ${SITE_KEY}" \ + -w "\nTotal time: %{time_total}s\n" \ + -o /dev/null -s +``` \ No newline at end of file diff --git a/docs/API_REFACTOR.md b/docs/API_REFACTOR.md new file mode 100644 index 0000000..d5875fa --- /dev/null +++ b/docs/API_REFACTOR.md @@ -0,0 +1,226 @@ +# API 重构文档 + +## 概述 + +本次重构将数据库从基于 `namespace` (UUID字符串) 的架构迁移到基于 `deviceId` (自增整数) 的架构,并实现了完整的token授权系统。 + +## 数据库变更 + +### Device 表 +- **主键变更**: `uuid` (VARCHAR) → `id` (INT AUTO_INCREMENT) +- **uuid**: 改为 UNIQUE 索引 +- **新增字段**: `accountId` (用于未来关联社区账户) + +### KVStore 表 +- **外键变更**: `namespace` (VARCHAR) → `deviceId` (INT) +- **主键**: `(deviceId, key)` 复合主键 +- **关联**: 外键关联 `Device.id`,支持级联删除 + +### 新增表 + +#### App 表 +```sql +CREATE TABLE `App` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `name` VARCHAR(191) NOT NULL, + `description` VARCHAR(191), + `developerName` VARCHAR(191) NOT NULL, + `developerLink` VARCHAR(191), + `homepageLink` VARCHAR(191), + `iconHash` VARCHAR(191), + `metadata` JSON, + `createdAt` DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL +); +``` + +#### AppInstall 表 +```sql +CREATE TABLE `AppInstall` ( + `id` VARCHAR(191) PRIMARY KEY, + `deviceId` INT NOT NULL, + `appId` INT NOT NULL, + `token` VARCHAR(191) UNIQUE NOT NULL, + `note` VARCHAR(191), + `installedAt` DATETIME(3) DEFAULT CURRENT_TIMESTAMP(3), + `updatedAt` DATETIME(3) NOT NULL, + FOREIGN KEY (`deviceId`) REFERENCES `Device`(`id`) ON DELETE CASCADE, + FOREIGN KEY (`appId`) REFERENCES `App`(`id`) ON DELETE CASCADE +); +``` + +## API 架构 + +### 1. 应用授权流程 + +#### POST /apps/:appId/authorize +为应用获取访问token + +**请求体:** +```json +{ + "deviceUuid": "设备UUID", + "password": "设备密码(如需要)", + "readOnly": false, + "note": "备注信息" +} +``` + +**响应:** +```json +{ + "token": "生成的访问token", + "appId": 1, + "appName": "应用名称", + "deviceUuid": "设备UUID", + "deviceName": "设备名称", + "readOnly": false, + "note": "读写访问", + "authorizedAt": "2025-01-01T00:00:00.000Z" +} +``` + +### 2. Token-based KV 操作(唯一方式) + +⚠️ **重要变更**: 所有KV操作现在仅支持基于token的访问,旧的 `/kv/:namespace/:key` API已移除。 + +#### Token提供方式 +1. Authorization Header: `Authorization: Bearer ` +2. Query 参数: `?token=` +3. Request Body: `{"token": ""}` + +#### KV API端点 +``` +GET /kv - 列出所有键(含元数据) +GET /kv/_keys - 列出所有键名(仅键名) +GET /kv/:key - 获取键值 +GET /kv/:key/metadata - 获取键元数据 +POST /kv/:key - 创建/更新键值 +POST /kv/_batchimport - 批量导入 +DELETE /kv/:key - 删除键值 +``` + +### 3. 主要接口 + +#### 应用管理 +- `GET /apps` - 获取应用列表 +- `GET /apps/:id` - 获取应用详情 +- `POST /apps/:id/authorize` - 授权应用获取token +- `GET /apps/:id/installations` - 获取应用的所有安装记录 + +#### Token管理 +- `GET /apps/devices/:deviceUuid/tokens` - 获取设备的所有token +- `DELETE /apps/tokens/:token` - 撤销token + +#### KV操作(仅Token方式) +- `GET /kv` - 列出所有键(含元数据) +- `GET /kv/_keys` - 列出所有键名(仅键名) +- `GET /kv/:key` - 获取键值 +- `GET /kv/:key/metadata` - 获取键元数据 +- `POST /kv/:key` - 创建/更新键值 +- `POST /kv/_batchimport` - 批量导入 +- `DELETE /kv/:key` - 删除键值 + +## 使用示例 + +### 1. 授权应用 +```javascript +const response = await fetch('http://localhost:3000/apps/1/authorize', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'x-site-key': 'your-site-key' + }, + body: JSON.stringify({ + deviceUuid: 'your-device-uuid', + password: 'device-password-if-needed', + readOnly: false, + note: '我的应用授权' + }) +}); + +const { token } = await response.json(); +``` + +### 2. 使用Token读取KV +```javascript +const response = await fetch('http://localhost:3000/kv/mykey', { + headers: { + 'Authorization': `Bearer ${token}`, + 'x-site-key': 'your-site-key' + } +}); + +const value = await response.json(); +``` + +### 3. 使用Token写入KV +```javascript +const response = await fetch('http://localhost:3000/kv/mykey', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json', + 'x-site-key': 'your-site-key' + }, + body: JSON.stringify({ + data: 'my value', + timestamp: Date.now() + }) +}); +``` + +## 迁移指南 + +### 数据库迁移 + +1. 标记旧迁移为已应用: +```bash +npx prisma migrate resolve --applied 20250524123414_2025_05_25 +``` + +2. 执行新迁移: +```bash +npx prisma migrate deploy +``` + +### 代码更新 + +⚠️ **破坏性变更**: 旧的基于namespace的API已完全移除。 + +**旧代码(不再支持):** +```javascript +// ❌ 已移除 +GET /kv/:namespace/:key +POST /kv/:namespace/:key +DELETE /kv/:namespace/:key +``` + +**新代码(唯一方式):** +```javascript +// ✅ 使用token-based API +GET /kv/:key +POST /kv/:key +DELETE /kv/:key + +// 必须在header中提供token +Headers: { + 'Authorization': 'Bearer ', + 'x-site-key': 'your-site-key' +} +``` + +**迁移步骤:** +1. 为每个需要访问KV的应用调用 `POST /apps/:id/authorize` 获取token +2. 将所有KV API调用从 `/kv/:namespace/:key` 改为 `/kv/:key` +3. 在所有请求中添加 `Authorization: Bearer ` header +4. 测试确保所有功能正常 + +## 优势 + +1. **安全性提升**: Token-based认证,无需在URL中暴露namespace +2. **多设备支持**: 同一UUID可在不同设备上使用不同token +3. **细粒度权限**: 可为每个应用授权只读或读写权限 +4. **易于管理**: 可随时撤销token,不影响其他授权 +5. **性能优化**: 使用整数ID作为外键,查询效率更高 +6. **简化API**: 统一的token认证方式,无需在URL中指定namespace \ No newline at end of file diff --git a/docs/FRONTEND_MIGRATION_GUIDE.md b/docs/FRONTEND_MIGRATION_GUIDE.md new file mode 100644 index 0000000..0f0379f --- /dev/null +++ b/docs/FRONTEND_MIGRATION_GUIDE.md @@ -0,0 +1,600 @@ +# 前端迁移指南 + +## 概述 + +本文档描述了后端中间件系统的重构,以及前端需要如何适配这些变化。核心变化是统一了设备信息获取和权限验证流程。 + +--- + +## 核心变化 + +### 1. 统一的设备中间件系统 + +后端现在使用统一的中间件处理所有与设备UUID相关的操作: + +- **`deviceMiddleware`**: 自动获取或创建设备,设备不存在时自动创建 +- **`requireWriteAuth`**: 验证写权限,检查设备密码 +- **`tokenAuth`**: Token认证,用于应用访问 + +### 2. 设备自动创建 + +**重要变化**: 当使用一个新的UUID访问API时,后端会自动创建该设备,无需手动调用创建设备接口。 + +### 3. 权限模型 + +- **读操作**: 永远不需要密码 +- **写操作**: 如果设备设置了密码则需要验证,否则直接允许 + +--- + + +## 场景1: 基于UUID的直接访问 + +适用于:用户直接操作设备数据(设备配置、设备管理等) + +### 读操作(无需密码) + +**请求方式**: `GET /device/:deviceUuid/*` + +**特点**: +- 设备不存在时自动创建 +- 无需提供密码 +- 任何知道UUID的人都可以读取 + +**请求示例**: +```http +GET /device/550e8400-e29b-41d4-a716-446655440000/info +Headers: + x-site-key: your-site-key +``` + +**成功响应** (200): +```json +{ + "id": 1, + "uuid": "550e8400-e29b-41d4-a716-446655440000", + "name": null, + "password": null, + "passwordHint": null, + "accountId": null, + "createdAt": "2025-01-30T10:00:00.000Z", + "updatedAt": "2025-01-30T10:00:00.000Z" +} +``` + +### 写操作(需要密码验证) + +**请求方式**: `POST|PUT|DELETE /device/:deviceUuid/*` + +**特点**: +- 设备不存在时自动创建 +- 如果设备设置了密码,必须提供正确密码 +- 如果设备没有密码,直接允许写入 + +#### 密码提供方式 + +**方式1: 通过请求体(推荐)** + +```http +POST /device/550e8400-e29b-41d4-a716-446655440000/config +Headers: + Content-Type: application/json + x-site-key: your-site-key +Body: +{ + "password": "device-password", + "data": { + "theme": "dark", + "language": "zh-CN" + } +} +``` + +**方式2: 通过查询参数** + +```http +POST /device/550e8400-e29b-41d4-a716-446655440000/config?password=device-password +Headers: + Content-Type: application/json + x-site-key: your-site-key +Body: +{ + "data": { + "theme": "dark", + "language": "zh-CN" + } +} +``` + +**成功响应** (200): +```json +{ + "message": "数据已更新", + "updatedAt": "2025-01-30T10:05:00.000Z" +} +``` + +**错误响应 - 需要密码** (401): +```json +{ + "statusCode": 401, + "message": "此操作需要密码", + "passwordHint": "您的生日(8位数字)" +} +``` + +**错误响应 - 密码错误** (401): +```json +{ + "statusCode": 401, + "message": "密码错误" +} +``` + +--- + +## 场景2: 基于Token的应用访问 + +适用于:应用访问KV存储数据 + +### 步骤1: 获取Token + +**请求方式**: `POST /apps/:appId/authorize` + +**请求示例**: +```http +POST /apps/1/authorize +Headers: + Content-Type: application/json + x-site-key: your-site-key +Body: +{ + "deviceUuid": "550e8400-e29b-41d4-a716-446655440000", + "password": "device-password", + "note": "我的应用授权" +} +``` + +**说明**: +- `deviceUuid`: 必填,设备UUID +- `password`: 如果设备有密码则必填 +- `note`: 可选,授权备注 + +**成功响应** (200): +```json +{ + "token": "clxxx123456789abcdefg", + "appId": 1, + "appName": "我的应用", + "deviceUuid": "550e8400-e29b-41d4-a716-446655440000", + "deviceName": null, + "note": "我的应用授权", + "authorizedAt": "2025-01-30T10:00:00.000Z" +} +``` + +**错误响应 - 需要密码** (401): +```json +{ + "statusCode": 401, + "message": "此操作需要密码", + "passwordHint": "您的生日(8位数字)" +} +``` + +### 步骤2: 使用Token访问KV存储 + +#### Token提供方式 + +**方式1: Authorization Header(推荐)** +```http +Headers: + Authorization: Bearer clxxx123456789abcdefg +``` + +**方式2: Query参数** +```http +?token=clxxx123456789abcdefg +``` + +**方式3: Request Body** +```json +{ + "token": "clxxx123456789abcdefg", + ... +} +``` + +--- + +### KV API端点 + +| 方法 | 端点 | 说明 | +|------|------|------| +| GET | `/kv` | 列出所有键(含元数据) | +| GET | `/kv/_keys` | 列出所有键名(仅键名) | +| GET | `/kv/:key` | 获取键值 | +| GET | `/kv/:key/metadata` | 获取键元数据 | +| POST | `/kv/:key` | 创建/更新键值 | +| POST | `/kv/_batchimport` | 批量导入 | +| DELETE | `/kv/:key` | 删除键值 | + +--- + +### GET /kv - 列出所有键(含元数据) + +**请求示例**: +```http +GET /kv?sortBy=key&sortDir=asc&limit=10&skip=0 +Headers: + Authorization: Bearer clxxx123456789abcdefg + x-site-key: your-site-key +``` + +**查询参数**: +- `sortBy`: 排序字段(key/createdAt/updatedAt),默认 key +- `sortDir`: 排序方向(asc/desc),默认 asc +- `limit`: 每页数量,默认 100 +- `skip`: 跳过数量,默认 0 + +**成功响应** (200): +```json +{ + "items": [ + { + "deviceId": 1, + "key": "config", + "metadata": { + "creatorIp": "192.168.1.1", + "createdAt": "2025-01-30T10:00:00.000Z", + "updatedAt": "2025-01-30T10:00:00.000Z" + } + }, + { + "deviceId": 1, + "key": "user.name", + "metadata": { + "creatorIp": "192.168.1.1", + "createdAt": "2025-01-30T10:01:00.000Z", + "updatedAt": "2025-01-30T10:01:00.000Z" + } + } + ], + "total_rows": 25, + "load_more": "/kv?sortBy=key&sortDir=asc&limit=10&skip=10" +} +``` + +--- + +### GET /kv/_keys - 列出所有键名 + +**请求示例**: +```http +GET /kv/_keys?limit=50&skip=0 +Headers: + Authorization: Bearer clxxx123456789abcdefg + x-site-key: your-site-key +``` + +**成功响应** (200): +```json +{ + "keys": ["config", "user.name", "user.theme", "app.settings"], + "total_rows": 4, + "current_page": { + "limit": 50, + "skip": 0, + "count": 4 + } +} +``` + +--- + +### GET /kv/:key - 获取键值 + +**请求示例**: +```http +GET /kv/config +Headers: + Authorization: Bearer clxxx123456789abcdefg + x-site-key: your-site-key +``` + +**成功响应** (200): +```json +{ + "theme": "dark", + "language": "zh-CN", + "fontSize": 14 +} +``` + +**错误响应 - 键不存在** (404): +```json +{ + "statusCode": 404, + "message": "未找到键名为 'config' 的记录" +} +``` + +--- + +### GET /kv/:key/metadata - 获取键元数据 + +**请求示例**: +```http +GET /kv/config/metadata +Headers: + Authorization: Bearer clxxx123456789abcdefg + x-site-key: your-site-key +``` + +**成功响应** (200): +```json +{ + "deviceId": 1, + "key": "config", + "metadata": { + "creatorIp": "192.168.1.1", + "createdAt": "2025-01-30T10:00:00.000Z", + "updatedAt": "2025-01-30T10:05:00.000Z" + } +} +``` + +--- + +### POST /kv/:key - 创建/更新键值 + +**请求示例**: +```http +POST /kv/config +Headers: + Authorization: Bearer clxxx123456789abcdefg + Content-Type: application/json + x-site-key: your-site-key +Body: +{ + "theme": "dark", + "language": "zh-CN", + "fontSize": 14 +} +``` + +**成功响应** (200): +```json +{ + "deviceId": 1, + "key": "config", + "created": false, + "updatedAt": "2025-01-30T10:10:00.000Z" +} +``` + +**说明**: +- `created`: true表示新建,false表示更新 + +**错误响应 - 空值** (400): +```json +{ + "statusCode": 400, + "message": "请提供有效的JSON值" +} +``` + +--- + +### POST /kv/_batchimport - 批量导入 + +**请求示例**: +```http +POST /kv/_batchimport +Headers: + Authorization: Bearer clxxx123456789abcdefg + Content-Type: application/json + x-site-key: your-site-key +Body: +{ + "config": { + "theme": "dark", + "language": "zh-CN" + }, + "user.name": { + "firstName": "John", + "lastName": "Doe" + }, + "app.settings": { + "notifications": true + } +} +``` + +**成功响应** (200): +```json +{ + "deviceId": 1, + "total": 3, + "successful": 3, + "failed": 0, + "results": [ + { + "key": "config", + "created": false + }, + { + "key": "user.name", + "created": true + }, + { + "key": "app.settings", + "created": true + } + ] +} +``` + +**部分失败响应** (200): +```json +{ + "deviceId": 1, + "total": 3, + "successful": 2, + "failed": 1, + "results": [ + { + "key": "config", + "created": false + }, + { + "key": "user.name", + "created": true + } + ], + "errors": [ + { + "key": "app.settings", + "error": "Invalid value" + } + ] +} +``` + +--- + +### DELETE /kv/:key - 删除键值 + +**请求示例**: +```http +DELETE /kv/config +Headers: + Authorization: Bearer clxxx123456789abcdefg + x-site-key: your-site-key +``` + +**成功响应** (204): +``` +无响应体 +``` + +**错误响应 - 键不存在** (404): +```json +{ + "statusCode": 404, + "message": "未找到键名为 'config' 的记录" +} +``` + +--- + +## 错误码参考 + +| 状态码 | 说明 | 场景 | +|--------|------|------| +| 200 | 成功 | 操作成功 | +| 204 | 成功(无内容) | 删除成功 | +| 400 | 请求错误 | 参数缺失或格式错误 | +| 401 | 未授权 | 需要密码、密码错误、Token无效 | +| 403 | 禁止访问 | 权限不足 | +| 404 | 未找到 | 资源不存在 | +| 500 | 服务器错误 | 服务器内部错误 | + +--- + +## 401错误详解 + +### 需要密码 +```json +{ + "statusCode": 401, + "message": "此操作需要密码", + "passwordHint": "您的生日(8位数字)" +} +``` + +**处理方式**: 提示用户输入密码,使用 `passwordHint` 作为提示信息 + +### 密码错误 +```json +{ + "statusCode": 401, + "message": "密码错误" +} +``` + +**处理方式**: 提示用户密码错误,允许重试 + +### Token无效 +```json +{ + "statusCode": 401, + "message": "未提供身份验证令牌" +} +``` + +或 + +```json +{ + "statusCode": 401, + "message": "无效的身份验证令牌" +} +``` + +**处理方式**: 清除本地Token,引导用户重新授权 + +--- + +## 迁移检查清单 + +### Phase 1: 基础适配 +- [ ] 移除手动创建设备的逻辑(设备会自动创建) +- [ ] 更新密码提供方式(从header改为body/query) +- [ ] 实现统一的错误处理 +- [ ] 更新API端点路径 + +### Phase 2: Token集成 +- [ ] 实现应用授权流程(POST /apps/:appId/authorize) +- [ ] 集成Token到KV操作 +- [ ] 实现Token存储和管理(localStorage) +- [ ] 处理Token过期/无效场景 + +### Phase 3: 优化 +- [ ] 封装统一的API客户端 +- [ ] 实现请求重试机制 +- [ ] 添加Loading状态管理 +- [ ] 优化错误提示用户体验 + +### Phase 4: 测试 +- [ ] 测试设备自动创建 +- [ ] 测试密码验证流程(需要密码、密码错误、密码正确) +- [ ] 测试Token授权流程 +- [ ] 测试各种错误场景(404、401、400等) + +--- + +## 关键注意事项 + +### 1. 设备自动创建 +- ✅ 无需手动创建设备,首次访问自动创建 +- ✅ 简化前端流程,减少API调用 +- ⚠️ 确保UUID使用正确的格式(建议使用uuidv4) + +### 2. 密码处理 +- ✅ 读操作永远不需要密码 +- ✅ 写操作只在设备设置了密码时才需要 +- ⚠️ 密码通过body或query提供,不要放在header中 +- ⚠️ 注意区分"需要密码"和"密码错误"两种情况 + +### 3. Token管理 +- ✅ Token一次获取,可重复使用 +- ✅ Token与设备和应用绑定 +- ⚠️ Token需要安全存储(localStorage/sessionStorage) +- ⚠️ Token失效时需要重新授权 + +### 4. Header要求 +- 所有请求必须携带 `x-site-key` header +- Token认证使用 `Authorization: Bearer ` header(推荐) + +--- \ No newline at end of file diff --git a/docs/apps.md b/docs/apps.md new file mode 100644 index 0000000..f17ad25 --- /dev/null +++ b/docs/apps.md @@ -0,0 +1,257 @@ +# Apps API + +## 1. 获取应用权限信息 (Get Application Permissions) + +- **GET** `/apps/token/:token/permissions` + +通过token获取应用权限信息。 + +**路径参数:** + +- `token` (string, required): 应用访问令牌 + +**Curl 示例:** + +```bash +curl -X GET "http://localhost:3000/api/apps/token/your-app-token/permissions" \ + -H "X-Site-Key: your-site-key" +``` + +**响应示例 (200 OK):** + +```json +{ + "appId": 1, + "permissionPrefix": "myapp", + "specialPermissions": [], + "permissionKey": [], + "app": { + "id": 1, + "name": "应用名称", + "description": "应用描述", + "developerName": "开发者" + } +} +``` + +## 2. 获取应用列表 (Get App List) + +- **GET** `/apps` + +获取应用列表,支持搜索和分页。 + +**查询参数:** + +- `limit` (integer, optional, default: 20): 每页数量 +- `skip` (integer, optional, default: 0): 跳过数量 +- `search` (string, optional): 搜索关键词 + +**Curl 示例:** + +```bash +curl -X GET "http://localhost:3000/api/apps?limit=10&skip=0&search=test" \ + -H "X-Site-Key: your-site-key" +``` + +**响应示例 (200 OK):** + +```json +{ + "apps": [ + { + "id": 1, + "name": "Test App", + "description": "An application for testing.", + "developerName": "Test Developer", + "permissionPrefix": "testapp", + "specialPermissions": [], + "permissionKey": [], + "version": "1.0.0", + "createdAt": "2023-10-27T10:00:00.000Z", + "updatedAt": "2023-10-27T10:00:00.000Z" + } + ], + "total": 1, + "limit": 10, + "skip": 0 +} +``` + +## 3. 获取单个应用详情 (Get App Details) + +- **GET** `/apps/:id` + +获取单个应用详情。 + +**路径参数:** + +- `id` (integer, required): 应用ID + +**Curl 示例:** + +```bash +curl -X GET "http://localhost:3000/api/apps/1" \ + -H "X-Site-Key: your-site-key" +``` + +**响应示例 (200 OK):** + +```json +{ + "id": 1, + "name": "Test App", + "description": "An application for testing.", + "developerName": "Test Developer", + "permissionPrefix": "testapp", + "specialPermissions": [], + "permissionKey": [], + "version": "1.0.0", + "createdAt": "2023-10-27T10:00:00.000Z", + "updatedAt": "2023-10-27T10:00:00.000Z" +} +``` + +## 4. 为设备授权或升级应用 (Install or Upgrade App for Device) + +- **POST** `/apps/:id/install/:deviceUuid` + +为设备授权应用。如果应用已安装,则会将其升级到最新版本。 + +**路径参数:** + +- `id` (integer, required): 应用ID +- `deviceUuid` (string, required): 设备UUID + +**Curl 示例:** + +```bash +curl -X POST "http://localhost:3000/api/apps/1/install/your-device-uuid" \ + -H "X-Site-Key: your-site-key" +``` + +**响应示例 (200 OK):** + +```json +{ + "token": "a-unique-token-for-this-installation", + "appId": 1, + "permissionPrefix": "testapp", + "permissionKey": [], + "version": "1.1.0", + "authorizedAt": "2023-10-27T11:00:00.000Z" +} +``` + +## 6. 卸载设备上的应用 (Uninstall App from Device) + +- **DELETE** `/apps/:id/uninstall/:deviceUuid` + +卸载设备上已安装的应用。 + +**路径参数:** + +- `id` (integer, required): 应用ID +- `deviceUuid` (string, required): 设备UUID + +**Curl 示例:** + +```bash +curl -X DELETE "http://localhost:3000/api/apps/1/uninstall/your-device-uuid" \ + -H "X-Site-Key: your-site-key" +``` + +**响应示例 (200 OK):** + +```json +{ + "message": "应用卸载成功", + "appId": 1, + "uninstalledAt": "2023-10-27T12:00:00.000Z" +} +``` + +## 7. 获取设备已安装的应用列表 (Get Installed Apps on Device) + +- **GET** `/devices/:deviceUuid/apps` + +获取指定设备上已安装的所有应用列表。 + +**路径参数:** + +- `deviceUuid` (string, required): 设备UUID + +**查询参数:** + +- `limit` (integer, optional, default: 20): 每页数量 +- `skip` (integer, optional, default: 0): 跳过数量 + +**Curl 示例:** + +```bash +curl -X GET "http://localhost:3000/api/devices/your-device-uuid/apps?limit=10" \ + -H "X-Site-Key: your-site-key" +``` + +**响应示例 (200 OK):** + +```json +{ + "installs": [ + { + "id": 1, + "appId": 1, + "token": "a-unique-token-for-this-installation", + "permissionPrefix": "testapp", + "specialPermissions": [], + "permissionKey": [], + "version": "1.0.0", + "installedAt": "2023-10-27T10:00:00.000Z", + "updatedAt": "2023-10-27T10:00:00.000Z", + "app": { + "id": 1, + "name": "Test App", + "description": "An application for testing.", + "developerName": "Test Developer", + "permissionPrefix": "testapp" + } + } + ], + "total": 1, + "limit": 10, + "skip": 0 +} +``` + +## 8. 获取所有带有 permissionKey 的应用列表 (Get Apps with Permission Key) + +- **GET** `/apps/with-permission-key` + +获取所有设置了 `permissionKey` 的应用列表。 + +**Curl 示例:** + +```bash +curl -X GET "http://localhost:3000/api/apps/with-permission-key" \ + -H "X-Site-Key: your-site-key" +``` + +**响应示例 (200 OK):** + +```json +{ + "apps": [ + { + "id": 2, + "name": "App With Keys", + "description": "An application that uses permission keys.", + "developerName": "Key Developer", + "permissionPrefix": "keyapp", + "specialPermissions": [], + "permissionKey": ["read:data", "write:data"], + "version": "1.0.0", + "createdAt": "2023-10-26T10:00:00.000Z", + "updatedAt": "2023-10-26T10:00:00.000Z" + } + ] +} +``` \ No newline at end of file diff --git a/docs/device-auth-frontend.md b/docs/device-auth-frontend.md new file mode 100644 index 0000000..790dec2 --- /dev/null +++ b/docs/device-auth-frontend.md @@ -0,0 +1,131 @@ +# 设备授权流程 - 前端接口文档 + +## 概述 + +类似 Device Authorization Grant 的授权流程,允许应用通过设备代码获取用户的访问令牌。 + +## 前端相关接口 + +### 1. 绑定令牌到设备代码 + +将用户的访问令牌绑定到应用提供的设备代码。 + +**接口地址:** `POST /auth/device/bind` + +**请求头:** +``` +Content-Type: application/json +X-Site-Key: your-site-key +``` + +**请求体:** +```json +{ + "device_code": "1234-ABCD", + "token": "user-access-token-string" +} +``` + +**参数说明:** +- `device_code` (必填): 应用提供给用户的设备授权码,格式如 `1234-ABCD` +- `token` (必填): 用户在系统中已有的有效访问令牌 + +**成功响应:** `200 OK` +```json +{ + "success": true, + "message": "令牌已成功绑定到设备代码" +} +``` + +**错误响应:** + +400 Bad Request - 参数错误 +```json +{ + "statusCode": 400, + "message": "请提供 device_code 和 token" +} +``` + +400 Bad Request - 无效的令牌 +```json +{ + "statusCode": 400, + "message": "无效的令牌" +} +``` + +400 Bad Request - 设备代码不存在或已过期 +```json +{ + "statusCode": 400, + "message": "设备代码不存在或已过期" +} +``` + +--- + +### 2. 查询设备代码状态(可选,用于调试) + +查询设备代码的当前状态,不会删除或修改数据。 + +**接口地址:** `GET /auth/device/status` + +**请求头:** +``` +X-Site-Key: your-site-key +``` + +**查询参数:** +- `device_code` (必填): 设备授权码 + +**请求示例:** +``` +GET /auth/device/status?device_code=1234-ABCD +``` + +**成功响应:** `200 OK` + +设备代码存在: +```json +{ + "device_code": "1234-ABCD", + "exists": true, + "has_token": false, + "expires_in": 850, + "created_at": 1234567890000 +} +``` + +设备代码不存在或已过期: +```json +{ + "device_code": "1234-ABCD", + "exists": false, + "message": "设备代码不存在或已过期" +} +``` + +**字段说明:** +- `exists`: 设备代码是否存在且有效 +- `has_token`: 是否已绑定令牌 +- `expires_in`: 剩余有效时间(秒) +- `created_at`: 创建时间戳(毫秒) + +--- + +## 使用流程 + +1. **应用端**生成设备代码并展示给用户 +2. **用户**在前端页面输入设备代码 +3. **前端**调用 `/auth/device/bind` 接口,将用户的 token 绑定到设备代码 +4. **应用端**轮询获取到令牌,完成授权 + +## 注意事项 + +- 设备代码有效期为 15 分钟 +- 令牌必须是系统中已存在的有效令牌 +- 设备代码格式固定为 `XXXX-XXXX` (4位数字-4位字母/数字) +- 令牌获取后会从服务器内存中删除,只能获取一次 +- 如果需要站点密钥,需在请求头中添加 `X-Site-Key` diff --git a/docs/kv-token.md b/docs/kv-token.md new file mode 100644 index 0000000..1989d04 --- /dev/null +++ b/docs/kv-token.md @@ -0,0 +1,224 @@ +# KV 存储 Token API + +本文档描述了基于令牌的 KV 存储 API。这些 API 端点使用应用程序安装令牌进行身份验证,而不是直接使用设备 UUID。 + +## 身份验证 + +所有请求都需要提供一个有效的应用程序安装令牌。令牌可以通过以下方式之一提供: + +1. **Authorization Header**: +``` +Authorization: Bearer YOUR_TOKEN +``` + +2. **Query Parameter**: +``` +?token=YOUR_TOKEN +``` + +3. **Request Body**: +```json +{ + "token": "YOUR_TOKEN" +} +``` + +## API 端点 + +### 列出键名 + +获取命名空间下的所有键名(不包括值)。 + +```http +GET /kv/token/_keys +``` + +查询参数: +- `sortBy`: 排序字段(默认:'key') +- `sortDir`: 排序方向('asc' 或 'desc',默认:'asc') +- `limit`: 每页记录数(默认:100) +- `skip`: 跳过的记录数(默认:0) + +响应示例: +```json +{ + "keys": ["key1", "key2", "key3"], + "total_rows": 3, + "current_page": { + "limit": 100, + "skip": 0, + "count": 3 + } +} +``` + +### 列出所有键值对 + +获取命名空间下的所有键值对及其元数据。 + +```http +GET /kv/token/ +``` + +查询参数: +- `sortBy`: 排序字段(默认:'key') +- `sortDir`: 排序方向('asc' 或 'desc',默认:'asc') +- `limit`: 每页记录数(默认:100) +- `skip`: 跳过的记录数(默认:0) + +响应示例: +```json +{ + "items": [ + { + "key": "key1", + "value": { "data": "value1" }, + "createdAt": "2024-01-01T00:00:00Z", + "updatedAt": "2024-01-01T00:00:00Z" + } + ], + "total_rows": 1 +} +``` + +### 获取单个键值 + +获取特定键的值。 + +```http +GET /kv/token/:key +``` + +响应示例: +```json +{ + "data": "value1" +} +``` + +### 获取键的元数据 + +获取特定键的元数据信息。 + +```http +GET /kv/token/:key/metadata +``` + +响应示例: +```json +{ + "key": "key1", + "createdAt": "2024-01-01T00:00:00Z", + "updatedAt": "2024-01-01T00:00:00Z", + "creatorIp": "127.0.0.1" +} +``` + +### 批量导入 + +批量导入多个键值对。 + +```http +POST /kv/token/_batchimport +``` + +请求体示例: +```json +{ + "key1": { "data": "value1" }, + "key2": { "data": "value2" } +} +``` + +响应示例: +```json +{ + "namespace": "device-uuid", + "total": 2, + "successful": 2, + "failed": 0, + "results": [ + { + "key": "key1", + "created": true + }, + { + "key": "key2", + "created": true + } + ] +} +``` + +### 创建或更新键值 + +创建新的键值对或更新现有的键值对。 + +```http +POST /kv/token/:key +``` + +请求体示例: +```json +{ + "data": "value1" +} +``` + +响应示例: +```json +{ + "namespace": "device-uuid", + "key": "key1", + "created": true, + "updatedAt": "2024-01-01T00:00:00Z" +} +``` + +### 删除命名空间 + +删除整个命名空间及其所有键值对。 + +```http +DELETE /kv/token/ +``` + +成功时返回 204 No Content。 + +### 删除键值对 + +删除特定的键值对。 + +```http +DELETE /kv/token/:key +``` + +成功时返回 204 No Content。 + +## 错误处理 + +所有错误响应都遵循以下格式: + +```json +{ + "statusCode": 400, + "message": "错误描述" +} +``` + +常见错误代码: +- 400: 请求参数错误 +- 401: 未提供令牌或令牌无效 +- 403: 权限不足 +- 404: 资源不存在 +- 429: 请求过于频繁 +- 500: 服务器内部错误 + +## 权限 + +API 使用以下权限系统: +- `appReadAuthMiddleware`: 用于读取操作 +- `appWriteAuthMiddleware`: 用于写入操作 +- `appListAuthMiddleware`: 用于列表操作 + +这些权限基于应用程序的安装记录中的 `permissionPrefix` 和 `permissionKey` 字段进行验证。 \ No newline at end of file diff --git a/docs/kv.md b/docs/kv.md new file mode 100644 index 0000000..d116b29 --- /dev/null +++ b/docs/kv.md @@ -0,0 +1,614 @@ +# KV Store API + +## 1. 获取设备信息 (Get Device Info) + +- **GET** `/:namespace/_info` + +获取指定命名空间(设备)的详细信息。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 +- `Authorization` (string, required): `Bearer ` + +**Curl 示例:** + +```bash +curl -X GET "http://localhost:3030/kv/your-device-uuid/_info" \ + -H "X-Site-Key: your-site-key" \ + -H "Authorization: Bearer your-read-token" +``` + +**响应示例 (200 OK):** + +```json +{ + "uuid": "your-device-uuid", + "name": "My Device", + "accessType": "PROTECTED", + "hasPassword": true +} +``` + +## 2. 检查设备状态 (Check Device Status) + +- **GET** `/:namespace/_check` + +检查设备是否存在及基本信息。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 + +**Curl 示例:** + +```bash +curl -X GET "http://localhost:3030/kv/your-device-uuid/_check" \ + -H "X-Site-Key: your-site-key" +``` + +**响应示例 (200 OK):** + +```json +{ + "status": "success", + "uuid": "your-device-uuid", + "name": "My Device", + "accessType": "PROTECTED", + "hasPassword": true +} +``` + +## 3. 校验设备密码 (Check Device Password) + +- **POST** `/:namespace/_checkpassword` + +校验设备密码是否正确。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 + +**请求体:** + +```json +{ + "password": "your-device-password" +} +``` + +**Curl 示例:** + +```bash +curl -X POST "http://localhost:3030/kv/your-device-uuid/_checkpassword" \ + -H "X-Site-Key: your-site-key" \ + -H "Content-Type: application/json" \ + -d '{"password": "your-device-password"}' +``` + +**响应示例 (200 OK):** + +```json +{ + "status": "success", + "uuid": "your-device-uuid", + "name": "My Device", + "accessType": "PROTECTED", + "hasPassword": true +} +``` + +## 4. 获取密码提示 (Get Password Hint) + +- **GET** `/:namespace/_hint` + +获取设备的密码提示。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 + +**Curl 示例:** + +```bash +curl -X GET "http://localhost:3030/kv/your-device-uuid/_hint" \ + -H "X-Site-Key: your-site-key" +``` + +**响应示例 (200 OK):** + +```json +{ + "passwordHint": "My favorite pet's name" +} +``` + +## 5. 更新密码提示 (Update Password Hint) + +- **PUT** `/:namespace/_hint` + +更新设备的密码提示。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 +- `Authorization` (string, required): `Bearer ` + +**请求体:** + +```json +{ + "hint": "New password hint" +} +``` + +**Curl 示例:** + +```bash +curl -X PUT "http://localhost:3030/kv/your-device-uuid/_hint" \ + -H "X-Site-Key: your-site-key" \ + -H "Authorization: Bearer your-write-token" \ + -H "Content-Type: application/json" \ + -d '{"hint": "New password hint"}' +``` + +**响应示例 (200 OK):** + +```json +{ + "message": "密码提示已更新", + "passwordHint": "New password hint" +} +``` + +## 6. 更新设备信息 (Update Device Info) + +- **PUT** `/:namespace/_info` + +更新设备名称或访问类型。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 +- `Authorization` (string, required): `Bearer ` + +**请求体:** + +```json +{ + "name": "New Device Name", + "accessType": "PRIVATE" +} +``` + +**Curl 示例:** + +```bash +curl -X PUT "http://localhost:3030/kv/your-device-uuid/_info" \ + -H "X-Site-Key: your-site-key" \ + -H "Authorization: Bearer your-write-token" \ + -H "Content-Type: application/json" \ + -d '{"name": "New Device Name", "accessType": "PRIVATE"}' +``` + +**响应示例 (200 OK):** + +```json +{ + "uuid": "your-device-uuid", + "name": "New Device Name", + "accessType": "PRIVATE", + "hasPassword": true +} +``` + +## 7. 移除设备密码 (Remove Device Password) + +- **DELETE** `/:namespace/_password` + +移除设备的密码。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 +- `Authorization` (string, required): `Bearer ` + +**请求体:** + +```json +{ + "password": "current-password" +} +``` + +**Curl 示例:** + +```bash +curl -X DELETE "http://localhost:3030/kv/your-device-uuid/_password" \ + -H "X-Site-Key: your-site-key" \ + -H "Authorization: Bearer your-write-token" \ + -H "Content-Type: application/json" \ + -d '{"password": "current-password"}' +``` + +**响应示例 (200 OK):** + +```json +{ + "message": "密码已成功移除" +} +``` + +## 8. 获取键名列表 (List Keys) + +- **GET** `/:namespace/_keys` + +获取指定命名空间下的键名列表(不包含值)。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID + +**查询参数:** + +- `sortBy` (string, optional, default: `key`): 排序字段 +- `sortDir` (string, optional, default: `asc`): 排序方向 +- `limit` (integer, optional, default: 100): 每页数量 +- `skip` (integer, optional, default: 0): 跳过数量 + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 +- `Authorization` (string, required): `Bearer ` + +**Curl 示例:** + +```bash +curl -X GET "http://localhost:3030/kv/your-device-uuid/_keys?limit=50&sortDir=desc" \ + -H "X-Site-Key: your-site-key" \ + -H "Authorization: Bearer your-read-token" +``` + +**响应示例 (200 OK):** + +```json +{ + "keys": [ + "key3", + "key2", + "key1" + ], + "total_rows": 3, + "current_page": { + "limit": 50, + "skip": 0, + "count": 3 + } +} +``` + +## 9. 获取所有键值对 (List All Key-Value Pairs) + +- **GET** `/:namespace` + +获取指定命名空间下的所有键值对及元数据。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID + +**查询参数:** + +- `sortBy` (string, optional, default: `key`): 排序字段 +- `sortDir` (string, optional, default: `asc`): 排序方向 +- `limit` (integer, optional, default: 100): 每页数量 +- `skip` (integer, optional, default: 0): 跳过数量 + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 +- `Authorization` (string, required): `Bearer ` + +**Curl 示例:** + +```bash +curl -X GET "http://localhost:3030/kv/your-device-uuid?limit=1" \ + -H "X-Site-Key: your-site-key" \ + -H "Authorization: Bearer your-read-token" +``` + +**响应示例 (200 OK):** + +```json +{ + "items": [ + { + "key": "key1", + "value": {"data": "some value"}, + "createdAt": "2023-10-27T10:00:00.000Z", + "updatedAt": "2023-10-27T10:00:00.000Z", + "creatorIp": "::1" + } + ], + "total_rows": 1, + "load_more": "/api/kv/your-device-uuid?sortBy=key&sortDir=asc&limit=1&skip=1" +} +``` + +## 10. 获取单个键值 (Get Value by Key) + +- **GET** `/:namespace/:key` + +通过键名获取单个键值。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID +- `key` (string, required): 键名 + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 +- `Authorization` (string, required): `Bearer ` + +**Curl 示例:** + +```bash +curl -X GET "http://localhost:3030/kv/your-device-uuid/my-key" \ + -H "X-Site-Key: your-site-key" \ + -H "Authorization: Bearer your-read-token" +``` + +**响应示例 (200 OK):** + +```json +{ + "some_data": "value", + "nested": { + "is_supported": true + } +} +``` + +## 11. 获取键的元数据 (Get Key Metadata) + +- **GET** `/:namespace/:key/metadata` + +获取单个键的元数据。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID +- `key` (string, required): 键名 + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 +- `Authorization` (string, required): `Bearer ` + +**Curl 示例:** + +```bash +curl -X GET "http://localhost:3030/kv/your-device-uuid/my-key/metadata" \ + -H "X-Site-Key: your-site-key" \ + -H "Authorization: Bearer your-read-token" +``` + +**响应示例 (200 OK):** + +```json +{ + "key": "my-key", + "createdAt": "2023-10-27T10:00:00.000Z", + "updatedAt": "2023-10-27T10:00:00.000Z", + "creatorIp": "::1" +} +``` + +## 12. 批量导入键值对 (Batch Import Key-Value Pairs) + +- **POST** `/:namespace/_batchimport` + +批量导入多个键值对。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 +- `Authorization` (string, required): `Bearer ` + +**请求体:** + +```json +{ + "key1": {"data": "value1"}, + "key2": {"data": "value2"} +} +``` + +**Curl 示例:** + +```bash +curl -X POST "http://localhost:3030/kv/your-device-uuid/_batchimport" \ + -H "X-Site-Key: your-site-key" \ + -H "Authorization: Bearer your-write-token" \ + -H "Content-Type: application/json" \ + -d '{"key1": {"data": "value1"}, "key2": {"data": "value2"}}' +``` + +**响应示例 (200 OK):** + +```json +{ + "namespace": "your-device-uuid", + "total": 2, + "successful": 2, + "failed": 0, + "results": [ + { + "key": "key1", + "created": true + }, + { + "key": "key2", + "created": true + } + ] +} +``` + +## 13. 创建或更新键值对 (Create/Update Key-Value Pair) + +- **POST** `/:namespace/:key` + +创建或更新单个键值对。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID +- `key` (string, required): 键名 + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 +- `Authorization` (string, required): `Bearer ` + +**请求体:** + +```json +{ + "new_data": "is here" +} +``` + +**Curl 示例:** + +```bash +curl -X POST "http://localhost:3030/kv/your-device-uuid/my-key" \ + -H "X-Site-Key: your-site-key" \ + -H "Authorization: Bearer your-write-token" \ + -H "Content-Type: application/json" \ + -d '{"new_data": "is here"}' +``` + +**响应示例 (200 OK):** + +```json +{ + "namespace": "your-device-uuid", + "key": "my-key", + "created": false, + "updatedAt": "2023-10-27T11:00:00.000Z" +} +``` + +## 14. 删除命名空间 (Delete Namespace) + +- **DELETE** `/:namespace` + +删除整个命名空间及其所有数据。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 +- `Authorization` (string, required): `Bearer ` + +**Curl 示例:** + +```bash +curl -X DELETE "http://localhost:3030/kv/your-device-uuid" \ + -H "X-Site-Key: your-site-key" \ + -H "Authorization: Bearer your-write-token" +``` + +**响应 (204 No Content):** + +无响应体。 + +## 15. 删除键 (Delete Key) + +- **DELETE** `/:namespace/:key` + +删除单个键值对。 + +**路径参数:** + +- `namespace` (string, required): 设备UUID +- `key` (string, required): 键名 + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 +- `Authorization` (string, required): `Bearer ` + +**Curl 示例:** + +```bash +curl -X DELETE "http://localhost:3030/kv/your-device-uuid/my-key" \ + -H "X-Site-Key: your-site-key" \ + -H "Authorization: Bearer your-write-token" +``` + +**响应 (204 No Content):** + +无响应体。 + +## 16. 生成 UUID (Generate UUID) + +- **GET** `/uuid` + +生成一个新的 UUID,可用作命名空间。 + +**Headers:** + +- `X-Site-Key` (string, required): 站点密钥 + +**Curl 示例:** + +```bash +curl -X GET "http://localhost:3030/kv/uuid" \ + -H "X-Site-Key: your-site-key" +``` + +**响应示例 (200 OK):** + +```json +{ + "namespace": "a-newly-generated-uuid" +} +``` \ No newline at end of file diff --git a/docs/middleware.md b/docs/middleware.md new file mode 100644 index 0000000..2743a33 --- /dev/null +++ b/docs/middleware.md @@ -0,0 +1,479 @@ +# 中间件系统文档 + +## 概述 + +本项目使用中间件系统来处理设备信息获取、权限验证和Token认证。所有与UUID相关的操作都通过统一的中间件处理。 + +## 中间件架构 + +### 1. 设备信息中间件 (`deviceMiddleware`) + +**文件位置**: `middleware/device.js` + +**功能**: 统一处理设备UUID,自动获取或创建设备 + +**使用场景**: +- 所有需要设备信息的接口 +- 不需要密码验证的读操作 +- 需要在后续中间件中访问设备信息的场景 + +**工作流程**: +1. 从 `req.params.deviceUuid`、`req.params.namespace` 或 `req.body.deviceUuid` 获取UUID +2. 在数据库中查找设备 +3. 如果设备不存在,自动创建新设备 +4. 将设备信息存储到 `res.locals.device` + +**代码示例**: +```javascript +import { deviceMiddleware } from './middleware/device.js'; + +// 基本用法 +router.get('/device/:deviceUuid/info', deviceMiddleware, (req, res) => { + // 设备信息可从 res.locals.device 访问 + res.json(res.locals.device); +}); + +// 从body获取UUID +router.post('/device/create', deviceMiddleware, (req, res) => { + // req.body.deviceUuid 会被自动处理 + res.json({ message: '设备已创建', device: res.locals.device }); +}); +``` + +**数据访问**: +```javascript +const device = res.locals.device; +// device: { +// id: 1, +// uuid: 'device-uuid-123', +// name: 'My Device', +// password: 'hashed-password', +// passwordHint: '提示信息', +// accountId: null, +// createdAt: Date, +// updatedAt: Date +// } +``` + +--- + +### 2. 写权限验证中间件 (`requireWriteAuth`) + +**文件位置**: `middleware/tokenAuth.js` + +**功能**: 验证设备密码,控制写权限 + +**依赖**: 必须在 `deviceMiddleware` 之后使用 + +**使用场景**: +- 所有需要修改数据的操作(POST、PUT、DELETE) +- 需要验证设备密码的操作 + +**工作流程**: +1. 从 `res.locals.device` 获取设备信息 +2. 如果设备没有设置密码,直接允许操作 +3. 如果设备设置了密码: + - 从 `req.body.password` 或 `req.query.password` 获取密码 + - 验证密码是否正确 + - 密码正确:继续执行 + - 密码错误或未提供:返回 401 错误 + +**代码示例**: +```javascript +import { deviceMiddleware } from './middleware/device.js'; +import { requireWriteAuth } from './middleware/tokenAuth.js'; + +// 写操作需要密码验证 +router.post('/device/:deviceUuid/data', + deviceMiddleware, // 第一步:获取设备信息 + requireWriteAuth, // 第二步:验证写权限 + (req, res) => { + // 验证通过,执行写操作 + res.json({ message: '数据已更新' }); + } +); + +// 读操作不需要密码 +router.get('/device/:deviceUuid/data', + deviceMiddleware, // 只需要设备信息 + (req, res) => { + res.json({ data: 'some data' }); + } +); +``` + +**密码提供方式**: +```javascript +// 方式1: 通过请求体 +fetch('/device/uuid-123/data', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + password: 'device-password', + data: 'new value' + }) +}); + +// 方式2: 通过查询参数 +fetch('/device/uuid-123/data?password=device-password', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ data: 'new value' }) +}); +``` + +**错误响应**: +```json +// 需要密码但未提供 +{ + "statusCode": 401, + "message": "此操作需要密码", + "passwordHint": "提示信息" +} + +// 密码错误 +{ + "statusCode": 401, + "message": "密码错误" +} +``` + +--- + +### 3. Token认证中间件 (`tokenAuth`) + +**文件位置**: `middleware/tokenAuth.js` + +**功能**: 基于应用安装Token进行认证 + +**使用场景**: +- 应用访问KV数据 +- 需要应用级别认证的接口 +- 不依赖设备UUID的操作 + +**工作流程**: +1. 从 Header、Query 或 Body 中获取 token +2. 在数据库中查找对应的应用安装记录 +3. 验证 token 是否有效 +4. 将应用、设备信息存储到 `res.locals` + +**Token提供方式**: +1. **Authorization Header** (推荐): + ```javascript + headers: { + 'Authorization': 'Bearer ' + } + ``` + +2. **Query参数**: + ```javascript + ?token= + ``` + +3. **Request Body**: + ```javascript + { + "token": "", + "data": "..." + } + ``` + +**代码示例**: +```javascript +import { tokenAuth } from './middleware/tokenAuth.js'; + +// Token认证的接口 +router.get('/kv/:key', tokenAuth, (req, res) => { + // 可访问: + // - res.locals.appInstall (应用安装记录) + // - res.locals.app (应用信息) + // - res.locals.device (设备信息) + // - res.locals.deviceId (设备ID) + + res.json({ + key: req.params.key, + device: res.locals.device.uuid, + app: res.locals.app.name + }); +}); +``` + +**数据访问**: +```javascript +const appInstall = res.locals.appInstall; +// appInstall: { +// id: 'cuid', +// deviceId: 1, +// appId: 1, +// token: 'unique-token', +// note: '备注', +// installedAt: Date, +// updatedAt: Date, +// app: { ... }, +// device: { ... } +// } + +const app = res.locals.app; +// app: { id, name, description, developerName, ... } + +const device = res.locals.device; +// device: { id, uuid, name, password, ... } +``` + +--- + +## 中间件组合使用 + +### 场景1: 基于UUID的读操作(无需密码) +```javascript +router.get('/device/:deviceUuid/data', + deviceMiddleware, + (req, res) => { + const device = res.locals.device; + res.json({ device, data: '...' }); + } +); +``` + +### 场景2: 基于UUID的写操作(需要密码) +```javascript +router.post('/device/:deviceUuid/data', + deviceMiddleware, // 获取设备信息 + requireWriteAuth, // 验证密码 + (req, res) => { + // 执行写操作 + res.json({ message: '成功' }); + } +); +``` + +### 场景3: 基于Token的操作 +```javascript +router.get('/kv/:key', + tokenAuth, // Token认证,自动获取设备信息 + (req, res) => { + const device = res.locals.device; + const app = res.locals.app; + res.json({ device, app, data: '...' }); + } +); +``` + +### 场景4: 批量路由保护 +```javascript +const router = express.Router(); + +// 所有该路由下的接口都需要设备信息 +router.use(deviceMiddleware); + +// 具体接口 +router.get('/info', (req, res) => { + res.json(res.locals.device); +}); + +router.post('/update', requireWriteAuth, (req, res) => { + res.json({ message: '更新成功' }); +}); +``` + +--- + +## 最佳实践 + +### 1. 中间件顺序很重要 +```javascript +// ✅ 正确:先获取设备信息,再验证权限 +router.post('/data', deviceMiddleware, requireWriteAuth, handler); + +// ❌ 错误:requireWriteAuth 依赖 deviceMiddleware +router.post('/data', requireWriteAuth, deviceMiddleware, handler); +``` + +### 2. 选择合适的认证方式 +```javascript +// 用户直接操作设备 → 使用 deviceMiddleware + requireWriteAuth +router.post('/device/:deviceUuid/config', deviceMiddleware, requireWriteAuth, handler); + +// 应用代表用户操作 → 使用 tokenAuth +router.post('/kv/:key', tokenAuth, handler); +``` + +### 3. 读操作不需要密码 +```javascript +// ✅ 读操作只需要设备信息 +router.get('/device/:deviceUuid/data', deviceMiddleware, handler); + +// ❌ 读操作不需要密码验证 +router.get('/device/:deviceUuid/data', deviceMiddleware, requireWriteAuth, handler); +``` + +### 4. 错误处理 +```javascript +router.post('/data', deviceMiddleware, requireWriteAuth, + async (req, res, next) => { + try { + // 业务逻辑 + const device = res.locals.device; + // ... + res.json({ success: true }); + } catch (error) { + next(error); // 传递给全局错误处理器 + } + } +); +``` + +### 5. 密码提示信息 +```javascript +// 设置设备时提供密码提示 +await prisma.device.update({ + where: { uuid: deviceUuid }, + data: { + password: hashedPassword, + passwordHint: '您的生日(8位数字)' // 提供友好的提示 + } +}); +``` + +--- + +## 常见问题 + +### Q1: 为什么设备不存在时会自动创建? +**A**: 这是为了简化客户端逻辑。客户端只需要生成UUID并使用,无需先调用创建接口。首次访问时会自动创建设备记录。 + +### Q2: 读操作为什么不需要密码? +**A**: 根据项目需求,只有写操作需要密码保护。读操作允许任何知道UUID的人访问。如果需要保护读操作,可以在路由中添加 `requireWriteAuth` 中间件。 + +### Q3: deviceMiddleware 和 tokenAuth 有什么区别? +**A**: +- `deviceMiddleware`: 基于UUID获取设备信息,适合用户直接操作 +- `tokenAuth`: 基于应用Token认证,适合应用代表用户操作,包含应用级别的权限控制 + +### Q4: 如何撤销某个设备的访问权限? +**A**: +1. 基于UUID的访问:修改设备密码 +2. 基于Token的访问:删除对应的 `AppInstall` 记录 + +### Q5: 密码错误但操作不需要密码是否可以继续? +**A**: 不可以。`requireWriteAuth` 中间件会检查: +- 如果设备没有密码 → 直接通过 +- 如果设备有密码但未提供 → 拒绝 +- 如果设备有密码但错误 → 拒绝 + +如果操作不需要密码,不要使用 `requireWriteAuth` 中间件。 + +--- + +## 迁移指南 + +### 从旧的认证系统迁移 + +**旧代码**: +```javascript +router.post('/kv/:namespace/:key', authMiddleware, handler); +``` + +**新代码**: +```javascript +// 选项1: 使用 deviceMiddleware (如果通过URL传递UUID) +router.post('/device/:deviceUuid/kv/:key', + deviceMiddleware, + requireWriteAuth, + handler +); + +// 选项2: 使用 tokenAuth (推荐,更安全) +router.post('/kv/:key', tokenAuth, handler); +``` + +### 客户端更新 + +**旧方式**: +```javascript +// UUID + 密码 +fetch('/kv/device-uuid/mykey', { + method: 'POST', + headers: { + 'x-namespace-password': 'password' + }, + body: JSON.stringify({ data: 'value' }) +}); +``` + +**新方式(选项1 - UUID)**: +```javascript +fetch('/device/device-uuid/kv/mykey', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + password: 'password', + data: 'value' + }) +}); +``` + +**新方式(选项2 - Token,推荐)**: +```javascript +// 先获取token +const authResponse = await fetch('/apps/1/authorize', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + deviceUuid: 'device-uuid', + password: 'password' + }) +}); +const { token } = await authResponse.json(); + +// 使用token操作 +fetch('/kv/mykey', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ data: 'value' }) +}); +``` + +--- + +## 技术细节 + +### 密码存储 +密码使用 `bcrypt` 进行哈希处理,存储在 `Device.password` 字段。 + +**加密函数** (`utils/crypto.js`): +```javascript +import bcrypt from 'bcryptjs'; + +export async function hashDevicePassword(password) { + return await bcrypt.hash(password, 10); +} + +export async function verifyDevicePassword(password, hash) { + return await bcrypt.compare(password, hash); +} +``` + +### 性能优化 +- 使用整数ID (`deviceId`) 作为外键,查询效率高于字符串UUID +- 设备信息查询结果缓存在 `res.locals`,避免重复查询 +- 密码验证使用 bcrypt 的异步方法,不阻塞事件循环 + +### 安全考虑 +1. 密码使用 bcrypt 加密存储 +2. Token 使用 `cuid` 生成,具有高随机性 +3. 支持密码提示功能,不暴露实际密码 +4. 写操作强制密码验证(如果设置了密码) +5. 所有中间件使用 `errors.catchAsync` 包装,统一错误处理 + +--- + +## 参考 + +- [API重构文档](./API_REFACTOR.md) +- [Token认证示例](./token-auth-examples.md) +- [KV存储文档](./kv.md) +- [应用管理文档](./apps.md) \ No newline at end of file diff --git a/docs/token-auth-examples.md b/docs/token-auth-examples.md new file mode 100644 index 0000000..b32a569 --- /dev/null +++ b/docs/token-auth-examples.md @@ -0,0 +1,214 @@ +# Token认证系统使用示例 + +本文档展示了如何使用重构后的基于Token的认证系统。 + +## 1. 基本Token认证 + +### 路由配置示例 + +```javascript +import express from 'express'; +import { + tokenOnlyAuthMiddleware, + tokenOnlyReadAuthMiddleware, + tokenOnlyWriteAuthMiddleware +} from './middleware/auth.js'; + +const router = express.Router(); + +// 需要完整认证的接口 +router.use('/secure', tokenOnlyAuthMiddleware); +router.get('/secure/profile', (req, res) => { + // res.locals.device, res.locals.appInstall, res.locals.app 已可用 + res.json({ + device: res.locals.device, + app: res.locals.app + }); +}); + +// 只读接口 +router.get('/data/:key', tokenOnlyReadAuthMiddleware, (req, res) => { + // 处理读取逻辑 + res.json({ key: req.params.key, value: 'some-value' }); +}); + +// 写入接口 +router.post('/data/:key', tokenOnlyWriteAuthMiddleware, (req, res) => { + // 处理写入逻辑 + res.json({ success: true, key: req.params.key }); +}); +``` + +## 2. 客户端请求示例 + +### 通过HTTP Header传递Token + +```javascript +// 使用fetch +fetch('/api/secure/profile', { + headers: { + 'x-app-token': 'your-app-token-here', + 'Content-Type': 'application/json' + } +}) +.then(response => response.json()) +.then(data => console.log(data)); + +// 使用axios +axios.get('/api/secure/profile', { + headers: { + 'x-app-token': 'your-app-token-here' + } +}); +``` + +### 通过查询参数传递Token + +```javascript +// GET请求 +fetch('/api/data/mykey?apptoken=your-app-token-here') + .then(response => response.json()) + .then(data => console.log(data)); +``` + +### 通过请求体传递Token + +```javascript +// POST请求 +fetch('/api/data/mykey', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + apptoken: 'your-app-token-here', + value: 'new-value' + }) +}); +``` + +## 3. 错误处理 + +### 常见错误响应 + +```json +// 缺少Token +{ + "statusCode": 401, + "message": "缺少应用访问令牌,请提供有效的token" +} + +// 无效Token +{ + "statusCode": 401, + "message": "无效的应用访问令牌" +} + +// 权限不足 +{ + "statusCode": 403, + "message": "应用令牌无权访问此命名空间" +} +``` + +### 客户端错误处理示例 + +```javascript +async function apiRequest(url, options = {}) { + try { + const response = await fetch(url, { + ...options, + headers: { + 'x-app-token': 'your-app-token-here', + 'Content-Type': 'application/json', + ...options.headers + } + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(`API错误 ${error.statusCode}: ${error.message}`); + } + + return await response.json(); + } catch (error) { + console.error('API请求失败:', error.message); + throw error; + } +} + +// 使用示例 +try { + const data = await apiRequest('/api/secure/profile'); + console.log('用户数据:', data); +} catch (error) { + // 处理认证错误 + if (error.message.includes('401')) { + // 重新获取token或跳转到登录页 + } +} +``` + +## 4. 迁移指南 + +### 从UUID认证迁移到Token认证 + +```javascript +// 旧的UUID认证方式(已弃用) +router.get('/data/:namespace/:key', authMiddleware, (req, res) => { + // 使用req.params.namespace作为设备标识 +}); + +// 新的Token认证方式(推荐) +router.get('/data/:key', tokenOnlyReadAuthMiddleware, (req, res) => { + // 设备信息通过token自动获取,存储在res.locals.device中 + const deviceUuid = res.locals.device.uuid; +}); +``` + +### 客户端迁移 + +```javascript +// 旧方式:使用UUID和密码 +fetch('/api/data/device-uuid-123/mykey', { + headers: { + 'x-namespace-password': 'device-password' + } +}); + +// 新方式:使用Token +fetch('/api/data/mykey', { + headers: { + 'x-app-token': 'app-token-from-installation' + } +}); +``` + +## 5. 最佳实践 + +1. **优先使用Token认证**:新项目应该直接使用`tokenOnlyAuthMiddleware`等纯Token认证中间件 + +2. **安全存储Token**:在客户端安全存储应用Token,避免在URL中暴露 + +3. **错误处理**:实现完善的错误处理机制,特别是认证失败的情况 + +4. **Token刷新**:实现Token过期和刷新机制(如果需要) + +5. **日志记录**:记录认证相关的操作日志,便于调试和安全审计 + +## 6. 权限前缀系统 + +Token认证系统支持基于前缀的权限控制: + +```javascript +// 应用只能访问以特定前缀开头的键 +// 例如:app.permissionPrefix = "myapp" +// 则只能访问 "myapp.config", "myapp.data" 等键 + +// 使用appReadAuthMiddleware自动进行前缀检查 +router.get('/kv/:key', appReadAuthMiddleware, (req, res) => { + // 自动检查req.params.key是否符合权限前缀 +}); +``` + +这个系统提供了更安全、更灵活的认证机制,建议所有新项目都采用Token认证方式。 \ No newline at end of file diff --git a/kv-admin/.claude/settings.local.json b/kv-admin/.claude/settings.local.json new file mode 100644 index 0000000..5b5f910 --- /dev/null +++ b/kv-admin/.claude/settings.local.json @@ -0,0 +1,9 @@ +{ + "permissions": { + "allow": [ + "Read(//d/Classworks/ClassworksServer/prisma/**)" + ], + "deny": [], + "ask": [] + } +} \ No newline at end of file diff --git a/kv-admin/.env.example b/kv-admin/.env.example new file mode 100644 index 0000000..4a7f32d --- /dev/null +++ b/kv-admin/.env.example @@ -0,0 +1,8 @@ +# Backend API Base URL (后端服务地址) +VITE_API_BASE_URL=http://localhost:3000 + +# Site Key for authentication (站点密钥) +VITE_SITE_KEY=your-site-key-here + +# Assets URL for app icons (应用图标资源地址) +VITE_ASSETS_URL=http://localhost:3000/assets diff --git a/kv-admin/.gitignore b/kv-admin/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/kv-admin/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/kv-admin/.npmrc b/kv-admin/.npmrc new file mode 100644 index 0000000..d67f374 --- /dev/null +++ b/kv-admin/.npmrc @@ -0,0 +1 @@ +node-linker=hoisted diff --git a/kv-admin/.vscode/extensions.json b/kv-admin/.vscode/extensions.json new file mode 100644 index 0000000..a7cea0b --- /dev/null +++ b/kv-admin/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["Vue.volar"] +} diff --git a/kv-admin/README.md b/kv-admin/README.md new file mode 100644 index 0000000..02da2af --- /dev/null +++ b/kv-admin/README.md @@ -0,0 +1,281 @@ +# KV 服务管理应用 + +一个基于 Vue 3 + JavaScript + shadcn-vue 的 KV 存储服务管理界面,支持多应用 Token 管理和本地设备码生成。 + +## 功能特性 + +- 🔑 **多 Token 管理**:管理多个应用的访问 Token +- 🔐 **本地设备码生成**:自动生成设备授权码,无需服务器 +- 📊 **KV 空间信息**:实时显示当前 KV 空间的使用情况 +- 💾 **数据管理**:浏览、创建、编辑和删除 KV 键值对 +- 🔍 **搜索过滤**:支持键名搜索和多种排序方式 +- 📱 **响应式设计**:适配桌面和移动设备 +- 🎨 **现代 UI**:shadcn-vue 组件库,简洁清爽 +- ⚡ **快速开发**:Vite 驱动,HMR 即时更新 +- 🗂️ **约定式路由**:基于文件系统的自动路由 + +## 技术栈 + +- **框架**:Vue 3 + JavaScript +- **构建工具**:Vite +- **UI 组件**:shadcn-vue +- **样式**:Tailwind CSS v4 +- **路由**:Vue Router + unplugin-vue-router (约定式路由) +- **图标**:Lucide Icons +- **状态管理**:LocalStorage (轻量级) + +## 快速开始 + +### 1. 安装依赖 + +```bash +pnpm install +``` + +### 2. 配置环境变量 + +复制 `.env.example` 到 `.env` 并填写配置: + +```bash +cp .env.example .env +``` + +编辑 `.env` 文件: + +```env +VITE_API_BASE_URL=http://localhost:3000 +VITE_SITE_KEY=your-site-key-here +``` + +### 3. 启动开发服务器 + +```bash +pnpm dev +``` + +应用将在 http://localhost:5173 运行 + +### 4. 构建生产版本 + +```bash +pnpm build +``` + +构建产物将输出到 `dist` 目录。 + +## 项目结构 + +``` +kv-admin/ +├── src/ +│ ├── components/ +│ │ └── ui/ # shadcn-vue 组件 +│ ├── pages/ # 约定式路由页面 +│ │ ├── index.vue # Token 管理页面 (/) +│ │ └── dashboard.vue # KV 数据管理 (/dashboard) +│ ├── lib/ +│ │ ├── api.js # API 客户端 +│ │ ├── tokenStore.js # Token 存储管理 +│ │ └── utils.js # 工具函数 +│ ├── App.vue # 根组件 +│ ├── main.js # 入口文件 +│ └── style.css # 全局样式 +├── .env.example # 环境变量模板 +├── components.json # shadcn-vue 配置 +├── jsconfig.json # JavaScript 配置 +├── vite.config.js # Vite 配置 +└── package.json +``` + +## 核心功能说明 + +### 1. Token 管理(首页) + +- **添加应用 Token**:输入应用名称和 Token,系统自动生成设备码 +- **设备码生成**:本地随机生成格式如 `XXXX-XXXX-XXXX-XXXX` 的设备码 +- **多 Token 支持**:可以添加多个应用的 Token,方便切换 +- **活跃 Token**:选择当前要使用的 Token +- **KV 空间信息**:显示当前活跃应用的 KV 数据统计 +- **Token 可见性**:支持显示/隐藏 Token 值 +- **复制功能**:一键复制设备码和 Token + +### 2. 数据管理(Dashboard) + +- **浏览数据**:查看当前应用的所有 KV 键值对 +- **搜索**:通过键名快速查找 +- **排序**:按键名、创建时间或更新时间排序 +- **创建**:添加新的键值对(JSON 格式) +- **编辑**:修改现有键值对的内容 +- **查看详情**:查看完整的键值对信息和元数据 +- **删除**:删除不需要的键值对 +- **分页**:支持大量数据的分页浏览 + +### 设备码说明 + +**什么是设备码?** +- 设备码是应用授权的密钥,相当于一个唯一标识符 +- 格式:`XXXX-XXXX-XXXX-XXXX`(4段,每段4个字母/数字) +- **本地生成**:无需服务器接口,在浏览器端随机生成 +- **用途**:用于标识和授权特定的应用或设备访问 KV 服务 + +**工作流程:** +1. 用户添加应用 Token 时,系统自动生成设备码 +2. 设备码与 Token 绑定存储在本地 +3. 应用可以使用设备码作为标识符进行授权验证 + +## API 端点 + +应用与以下 API 端点交互: + +### KV 存储 +- `GET /kv` - 获取键值对列表 +- `GET /kv/_keys` - 获取键名列表 +- `GET /kv/:key` - 获取指定键的值 +- `GET /kv/:key/metadata` - 获取键的元数据 +- `POST /kv/:key` - 创建或更新键值对 +- `DELETE /kv/:key` - 删除键值对 +- `POST /kv/_batchimport` - 批量导入 + +## 数据存储 + +应用使用 LocalStorage 存储以下数据: + +- `kv_tokens` - Token 列表数据 + ```json + [ + { + "id": "1234567890", + "token": "your-token-here", + "appName": "我的应用", + "deviceCode": "ABCD-1234-EFGH-5678", + "createdAt": "2025-01-01T00:00:00.000Z", + "lastUsed": "2025-01-01T00:00:00.000Z" + } + ] + ``` +- `kv_active_token` - 当前活跃的 Token ID + +## 约定式路由 + +本项目使用 `unplugin-vue-router` 实现约定式路由,无需手动配置路由: + +- `src/pages/index.vue` → `/` (Token 管理页面) +- `src/pages/dashboard.vue` → `/dashboard` (数据管理页面) + +### 路由元信息 + +在页面组件中使用 `defineOptions` 设置路由元信息: + +```vue + +``` + +### 导航守卫 + +路由守卫在 `src/main.js` 中配置,自动处理授权检查: + +```javascript +router.beforeEach((to, _from, next) => { + const requiresAuth = to.meta?.requiresAuth + const activeToken = tokenStore.getActiveToken() + + if (requiresAuth && !activeToken) { + next({ path: '/' }) + } else { + next() + } +}) +``` + +## 开发 + +### 添加新页面 + +在 `src/pages/` 目录下创建新的 `.vue` 文件,路由会自动生成: + +``` +src/pages/ +├── index.vue → / +├── dashboard.vue → /dashboard +└── settings.vue → /settings (自动添加) +``` + +### 添加新组件 + +使用 shadcn-vue CLI 添加组件: + +```bash +pnpm dlx shadcn-vue@latest add [component-name] +``` + +## 部署 + +### Vercel / Netlify + +这些平台会自动检测 Vite 项目并进行构建。只需连接 Git 仓库即可。 + +### 传统服务器 + +构建后将 `dist` 目录部署到您的 Web 服务器,确保配置 SPA 回退规则: + +**Nginx 示例**: +```nginx +location / { + try_files $uri $uri/ /index.html; +} +``` + +## 使用流程 + +### 首次使用 + +1. 访问首页 +2. 点击"添加应用" +3. 输入应用名称(可选)和访问 Token +4. 系统自动生成设备码并保存 +5. 点击"管理数据"进入数据管理页面 + +### 切换应用 + +1. 在首页的应用列表中 +2. 点击要切换的应用行的"选择"按钮 +3. 该应用变为"活跃"状态 +4. KV 空间信息自动更新 +5. 点击"管理数据"查看该应用的数据 + +### 管理数据 + +1. 在数据管理页面可以进行 CRUD 操作 +2. 使用搜索框快速查找键名 +3. 使用排序和分页功能浏览大量数据 +4. 点击左上角的"主页"图标返回 Token 管理页面 + +## 安全建议 + +1. 始终使用 HTTPS 部署生产环境 +2. 定期更换访问 Token +3. 不要在前端代码中硬编码敏感信息 +4. 使用环境变量管理配置 +5. 实施适当的 CORS 策略 +6. LocalStorage 数据在浏览器端存储,注意隐私保护 + +## 技术亮点 + +- ✅ **纯 JavaScript**:无 TypeScript 依赖,更简单轻量 +- ✅ **约定式路由**:基于文件系统,自动生成路由 +- ✅ **本地设备码**:客户端生成,无需服务器接口 +- ✅ **多 Token 管理**:支持多应用切换 +- ✅ **现代化工具链**:Vite + Vue 3 组合式 API +- ✅ **完整的 UI 组件**:44 个 shadcn-vue 组件 +- ✅ **响应式设计**:Tailwind CSS v4 +- ✅ **轻量级状态**:LocalStorage 管理,无需额外状态库 + +## 许可证 + +MIT diff --git a/kv-admin/components.json b/kv-admin/components.json new file mode 100644 index 0000000..17158ce --- /dev/null +++ b/kv-admin/components.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://shadcn-vue.com/schema.json", + "style": "new-york", + "typescript": false, + "tailwind": { + "config": "", + "css": "src/style.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "composables": "@/composables", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/kv-admin/index.html b/kv-admin/index.html new file mode 100644 index 0000000..2584da2 --- /dev/null +++ b/kv-admin/index.html @@ -0,0 +1,13 @@ + + + + + + + KV 服务授权管理 + + +
+ + + diff --git a/kv-admin/jsconfig.json b/kv-admin/jsconfig.json new file mode 100644 index 0000000..0486e3b --- /dev/null +++ b/kv-admin/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "exclude": ["node_modules", "dist"] +} diff --git a/kv-admin/package.json b/kv-admin/package.json new file mode 100644 index 0000000..2261cca --- /dev/null +++ b/kv-admin/package.json @@ -0,0 +1,42 @@ +{ + "name": "kv-admin", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "@radix-ui/react-slot": "^1.2.3", + "@tailwindcss/vite": "^4.1.13", + "@tanstack/vue-table": "^8.21.3", + "@vee-validate/zod": "^4.15.1", + "@vueuse/core": "^13.9.0", + "axios": "^1.12.2", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "lucide-react": "^0.544.0", + "lucide-vue-next": "^0.544.0", + "marked": "^16.3.0", + "radix-vue": "^1.9.17", + "reka-ui": "^2.5.1", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.13", + "vee-validate": "^4.15.1", + "vue": "^3.5.21", + "vue-router": "^4.5.1", + "vue-sonner": "^2.0.9", + "zod": "^3.25.76" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.19", + "@vitejs/plugin-vue": "^6.0.1", + "@vue/devtools": "^8.0.2", + "tw-animate-css": "^1.4.0", + "unplugin-vue-router": "^0.15.0", + "vite": "^7.1.7", + "vite-plugin-vue-devtools": "^8.0.2" + } +} diff --git a/kv-admin/pnpm-lock.yaml b/kv-admin/pnpm-lock.yaml new file mode 100644 index 0000000..6153697 --- /dev/null +++ b/kv-admin/pnpm-lock.yaml @@ -0,0 +1,3898 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(react@19.2.0) + '@tailwindcss/vite': + specifier: ^4.1.13 + version: 4.1.14(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)) + '@tanstack/vue-table': + specifier: ^8.21.3 + version: 8.21.3(vue@3.5.22) + '@vee-validate/zod': + specifier: ^4.15.1 + version: 4.15.1(vue@3.5.22)(zod@3.25.76) + '@vueuse/core': + specifier: ^13.9.0 + version: 13.9.0(vue@3.5.22) + axios: + specifier: ^1.12.2 + version: 1.12.2 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 + lucide-react: + specifier: ^0.544.0 + version: 0.544.0(react@19.2.0) + lucide-vue-next: + specifier: ^0.544.0 + version: 0.544.0(vue@3.5.22) + marked: + specifier: ^16.3.0 + version: 16.3.0 + radix-vue: + specifier: ^1.9.17 + version: 1.9.17(vue@3.5.22) + reka-ui: + specifier: ^2.5.1 + version: 2.5.1(vue@3.5.22) + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 + tailwindcss: + specifier: ^4.1.13 + version: 4.1.14 + vee-validate: + specifier: ^4.15.1 + version: 4.15.1(vue@3.5.22) + vue: + specifier: ^3.5.21 + version: 3.5.22 + vue-router: + specifier: ^4.5.1 + version: 4.5.1(vue@3.5.22) + vue-sonner: + specifier: ^2.0.9 + version: 2.0.9 + zod: + specifier: ^3.25.76 + version: 3.25.76 + devDependencies: + '@tailwindcss/typography': + specifier: ^0.5.19 + version: 0.5.19(tailwindcss@4.1.14) + '@vitejs/plugin-vue': + specifier: ^6.0.1 + version: 6.0.1(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))(vue@3.5.22) + '@vue/devtools': + specifier: ^8.0.2 + version: 8.0.2(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))(vue@3.5.22) + tw-animate-css: + specifier: ^1.4.0 + version: 1.4.0 + unplugin-vue-router: + specifier: ^0.15.0 + version: 0.15.0(@vue/compiler-sfc@3.5.22)(vue-router@4.5.1(vue@3.5.22))(vue@3.5.22) + vite: + specifier: ^7.1.7 + version: 7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) + vite-plugin-vue-devtools: + specifier: ^8.0.2 + version: 8.0.2(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))(vue@3.5.22) + +packages: + + '@babel/code-frame@7.27.1': + resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.28.4': + resolution: {integrity: sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.4': + resolution: {integrity: sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.28.3': + resolution: {integrity: sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.27.3': + resolution: {integrity: sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.27.2': + resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.28.3': + resolution: {integrity: sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.27.1': + resolution: {integrity: sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.27.1': + resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.3': + resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.27.1': + resolution: {integrity: sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.27.1': + resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.27.1': + resolution: {integrity: sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + resolution: {integrity: sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.27.1': + resolution: {integrity: sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.28.4': + resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.28.4': + resolution: {integrity: sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-proposal-decorators@7.28.0': + resolution: {integrity: sha512-zOiZqvANjWDUaUS9xMxbMcK/Zccztbe/6ikvUXaG9nsPH3w6qh5UaPGAnirI/WhIbZ8m3OHU0ReyPrknG+ZKeg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-decorators@7.27.1': + resolution: {integrity: sha512-YMq8Z87Lhl8EGkmb0MwYkt36QnxC+fzCgrl66ereamPlYToRpIk5nUjKUY3QKLWq8mwUB1BgbeXcTJhZOCDg5A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-attributes@7.27.1': + resolution: {integrity: sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-import-meta@7.10.4': + resolution: {integrity: sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-jsx@7.27.1': + resolution: {integrity: sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.27.1': + resolution: {integrity: sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.28.0': + resolution: {integrity: sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.27.2': + resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.28.4': + resolution: {integrity: sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.28.4': + resolution: {integrity: sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==} + engines: {node: '>=6.9.0'} + + '@electron/get@2.0.3': + resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==} + engines: {node: '>=12'} + + '@esbuild/aix-ppc64@0.25.10': + resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.25.10': + resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.25.10': + resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.25.10': + resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.25.10': + resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.25.10': + resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.25.10': + resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.25.10': + resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.25.10': + resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.25.10': + resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.25.10': + resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.25.10': + resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.25.10': + resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.25.10': + resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.25.10': + resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.25.10': + resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.25.10': + resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.25.10': + resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.25.10': + resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.25.10': + resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.25.10': + resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.25.10': + resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.25.10': + resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.25.10': + resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.25.10': + resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.25.10': + resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@floating-ui/vue@1.1.9': + resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==} + + '@internationalized/date@3.9.0': + resolution: {integrity: sha512-yaN3brAnHRD+4KyyOsJyk49XUvj2wtbNACSqg0bz3u8t2VuzhC8Q5dfRnrSxjnnbDb+ienBnkn1TzQfE154vyg==} + + '@internationalized/number@3.6.5': + resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@polka/url@1.0.0-next.29': + resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@rolldown/pluginutils@1.0.0-beta.29': + resolution: {integrity: sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q==} + + '@rollup/rollup-android-arm-eabi@4.52.3': + resolution: {integrity: sha512-h6cqHGZ6VdnwliFG1NXvMPTy/9PS3h8oLh7ImwR+kl+oYnQizgjxsONmmPSb2C66RksfkfIxEVtDSEcJiO0tqw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.52.3': + resolution: {integrity: sha512-wd+u7SLT/u6knklV/ifG7gr5Qy4GUbH2hMWcDauPFJzmCZUAJ8L2bTkVXC2niOIxp8lk3iH/QX8kSrUxVZrOVw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.52.3': + resolution: {integrity: sha512-lj9ViATR1SsqycwFkJCtYfQTheBdvlWJqzqxwc9f2qrcVrQaF/gCuBRTiTolkRWS6KvNxSk4KHZWG7tDktLgjg==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.52.3': + resolution: {integrity: sha512-+Dyo7O1KUmIsbzx1l+4V4tvEVnVQqMOIYtrxK7ncLSknl1xnMHLgn7gddJVrYPNZfEB8CIi3hK8gq8bDhb3h5A==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.52.3': + resolution: {integrity: sha512-u9Xg2FavYbD30g3DSfNhxgNrxhi6xVG4Y6i9Ur1C7xUuGDW3banRbXj+qgnIrwRN4KeJ396jchwy9bCIzbyBEQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.52.3': + resolution: {integrity: sha512-5M8kyi/OX96wtD5qJR89a/3x5x8x5inXBZO04JWhkQb2JWavOWfjgkdvUqibGJeNNaz1/Z1PPza5/tAPXICI6A==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + resolution: {integrity: sha512-IoerZJ4l1wRMopEHRKOO16e04iXRDyZFZnNZKrWeNquh5d6bucjezgd+OxG03mOMTnS1x7hilzb3uURPkJ0OfA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.52.3': + resolution: {integrity: sha512-ZYdtqgHTDfvrJHSh3W22TvjWxwOgc3ThK/XjgcNGP2DIwFIPeAPNsQxrJO5XqleSlgDux2VAoWQ5iJrtaC1TbA==} + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.52.3': + resolution: {integrity: sha512-NcViG7A0YtuFDA6xWSgmFb6iPFzHlf5vcqb2p0lGEbT+gjrEEz8nC/EeDHvx6mnGXnGCC1SeVV+8u+smj0CeGQ==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.52.3': + resolution: {integrity: sha512-d3pY7LWno6SYNXRm6Ebsq0DJGoiLXTb83AIPCXl9fmtIQs/rXoS8SJxxUNtFbJ5MiOvs+7y34np77+9l4nfFMw==} + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loong64-gnu@4.52.3': + resolution: {integrity: sha512-3y5GA0JkBuirLqmjwAKwB0keDlI6JfGYduMlJD/Rl7fvb4Ni8iKdQs1eiunMZJhwDWdCvrcqXRY++VEBbvk6Eg==} + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-ppc64-gnu@4.52.3': + resolution: {integrity: sha512-AUUH65a0p3Q0Yfm5oD2KVgzTKgwPyp9DSXc3UA7DtxhEb/WSPfbG4wqXeSN62OG5gSo18em4xv6dbfcUGXcagw==} + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.52.3': + resolution: {integrity: sha512-1makPhFFVBqZE+XFg3Dkq+IkQ7JvmUrwwqaYBL2CE+ZpxPaqkGaiWFEWVGyvTwZace6WLJHwjVh/+CXbKDGPmg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.52.3': + resolution: {integrity: sha512-OOFJa28dxfl8kLOPMUOQBCO6z3X2SAfzIE276fwT52uXDWUS178KWq0pL7d6p1kz7pkzA0yQwtqL0dEPoVcRWg==} + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.52.3': + resolution: {integrity: sha512-jMdsML2VI5l+V7cKfZx3ak+SLlJ8fKvLJ0Eoa4b9/vCUrzXKgoKxvHqvJ/mkWhFiyp88nCkM5S2v6nIwRtPcgg==} + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.52.3': + resolution: {integrity: sha512-tPgGd6bY2M2LJTA1uGq8fkSPK8ZLYjDjY+ZLK9WHncCnfIz29LIXIqUgzCR0hIefzy6Hpbe8Th5WOSwTM8E7LA==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.52.3': + resolution: {integrity: sha512-BCFkJjgk+WFzP+tcSMXq77ymAPIxsX9lFJWs+2JzuZTLtksJ2o5hvgTdIcZ5+oKzUDMwI0PfWzRBYAydAHF2Mw==} + cpu: [x64] + os: [linux] + + '@rollup/rollup-openharmony-arm64@4.52.3': + resolution: {integrity: sha512-KTD/EqjZF3yvRaWUJdD1cW+IQBk4fbQaHYJUmP8N4XoKFZilVL8cobFSTDnjTtxWJQ3JYaMgF4nObY/+nYkumA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.52.3': + resolution: {integrity: sha512-+zteHZdoUYLkyYKObGHieibUFLbttX2r+58l27XZauq0tcWYYuKUwY2wjeCN9oK1Um2YgH2ibd6cnX/wFD7DuA==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.52.3': + resolution: {integrity: sha512-of1iHkTQSo3kr6dTIRX6t81uj/c/b15HXVsPcEElN5sS859qHrOepM5p9G41Hah+CTqSh2r8Bm56dL2z9UQQ7g==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.52.3': + resolution: {integrity: sha512-s0hybmlHb56mWVZQj8ra9048/WZTPLILKxcvcq+8awSZmyiSUZjjem1AhU3Tf4ZKpYhK4mg36HtHDOe8QJS5PQ==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.52.3': + resolution: {integrity: sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==} + cpu: [x64] + os: [win32] + + '@sec-ant/readable-stream@0.4.1': + resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@sindresorhus/merge-streams@4.0.0': + resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} + engines: {node: '>=18'} + + '@socket.io/component-emitter@3.1.2': + resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} + + '@swc/helpers@0.5.17': + resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==} + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@tailwindcss/node@4.1.14': + resolution: {integrity: sha512-hpz+8vFk3Ic2xssIA3e01R6jkmsAhvkQdXlEbRTk6S10xDAtiQiM3FyvZVGsucefq764euO/b8WUW9ysLdThHw==} + + '@tailwindcss/oxide-android-arm64@4.1.14': + resolution: {integrity: sha512-a94ifZrGwMvbdeAxWoSuGcIl6/DOP5cdxagid7xJv6bwFp3oebp7y2ImYsnZBMTwjn5Ev5xESvS3FFYUGgPODQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.14': + resolution: {integrity: sha512-HkFP/CqfSh09xCnrPJA7jud7hij5ahKyWomrC3oiO2U9i0UjP17o9pJbxUN0IJ471GTQQmzwhp0DEcpbp4MZTA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.14': + resolution: {integrity: sha512-eVNaWmCgdLf5iv6Qd3s7JI5SEFBFRtfm6W0mphJYXgvnDEAZ5sZzqmI06bK6xo0IErDHdTA5/t7d4eTfWbWOFw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.14': + resolution: {integrity: sha512-QWLoRXNikEuqtNb0dhQN6wsSVVjX6dmUFzuuiL09ZeXju25dsei2uIPl71y2Ic6QbNBsB4scwBoFnlBfabHkEw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14': + resolution: {integrity: sha512-VB4gjQni9+F0VCASU+L8zSIyjrLLsy03sjcR3bM0V2g4SNamo0FakZFKyUQ96ZVwGK4CaJsc9zd/obQy74o0Fw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': + resolution: {integrity: sha512-qaEy0dIZ6d9vyLnmeg24yzA8XuEAD9WjpM5nIM1sUgQ/Zv7cVkharPDQcmm/t/TvXoKo/0knI3me3AGfdx6w1w==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.14': + resolution: {integrity: sha512-ISZjT44s59O8xKsPEIesiIydMG/sCXoMBCqsphDm/WcbnuWLxxb+GcvSIIA5NjUw6F8Tex7s5/LM2yDy8RqYBQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.14': + resolution: {integrity: sha512-02c6JhLPJj10L2caH4U0zF8Hji4dOeahmuMl23stk0MU1wfd1OraE7rOloidSF8W5JTHkFdVo/O7uRUJJnUAJg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.14': + resolution: {integrity: sha512-TNGeLiN1XS66kQhxHG/7wMeQDOoL0S33x9BgmydbrWAb9Qw0KYdd8o1ifx4HOGDWhVmJ+Ul+JQ7lyknQFilO3Q==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.14': + resolution: {integrity: sha512-uZYAsaW/jS/IYkd6EWPJKW/NlPNSkWkBlaeVBi/WsFQNP05/bzkebUL8FH1pdsqx4f2fH/bWFcUABOM9nfiJkQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.14': + resolution: {integrity: sha512-Az0RnnkcvRqsuoLH2Z4n3JfAef0wElgzHD5Aky/e+0tBUxUhIeIqFBTMNQvmMRSP15fWwmvjBxZ3Q8RhsDnxAA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.14': + resolution: {integrity: sha512-ttblVGHgf68kEE4om1n/n44I0yGPkCPbLsqzjvybhpwa6mKKtgFfAzy6btc3HRmuW7nHe0OOrSeNP9sQmmH9XA==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.14': + resolution: {integrity: sha512-23yx+VUbBwCg2x5XWdB8+1lkPajzLmALEfMb51zZUBYaYVPDQvBSD/WYDqiVyBIo2BZFa3yw1Rpy3G2Jp+K0dw==} + engines: {node: '>= 10'} + + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + + '@tailwindcss/vite@4.1.14': + resolution: {integrity: sha512-BoFUoU0XqgCUS1UXWhmDJroKKhNXeDzD7/XwabjkDIAbMnc4ULn5e2FuEuBbhZ6ENZoSYzKlzvZ44Yr6EUDUSA==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + + '@tanstack/virtual-core@3.13.12': + resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==} + + '@tanstack/vue-table@8.21.3': + resolution: {integrity: sha512-rusRyd77c5tDPloPskctMyPLFEQUeBzxdQ+2Eow4F7gDPlPOB1UnnhzfpdvqZ8ZyX2rRNGmqNnQWm87OI2OQPw==} + engines: {node: '>=12'} + peerDependencies: + vue: '>=3.2' + + '@tanstack/vue-virtual@3.13.12': + resolution: {integrity: sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==} + peerDependencies: + vue: ^2.7.0 || ^3.0.0 + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + + '@types/cors@2.8.19': + resolution: {integrity: sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/http-cache-semantics@4.0.4': + resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/node@22.18.8': + resolution: {integrity: sha512-pAZSHMiagDR7cARo/cch1f3rXy0AEXwsVsVH09FcyeJVAzCnGgmYis7P3JidtTUjyadhTeSo8TgRPswstghDaw==} + + '@types/node@24.6.2': + resolution: {integrity: sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@types/web-bluetooth@0.0.20': + resolution: {integrity: sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow==} + + '@types/web-bluetooth@0.0.21': + resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + + '@vee-validate/zod@4.15.1': + resolution: {integrity: sha512-329Z4TDBE5Vx0FdbA8S4eR9iGCFFUNGbxjpQ20ff5b5wGueScjocUIx9JHPa79LTG06RnlUR4XogQsjN4tecKA==} + peerDependencies: + zod: ^3.24.0 + + '@vitejs/plugin-vue@6.0.1': + resolution: {integrity: sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + vite: ^5.0.0 || ^6.0.0 || ^7.0.0 + vue: ^3.2.25 + + '@volar/language-core@2.4.23': + resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==} + + '@volar/source-map@2.4.23': + resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==} + + '@vue-macros/common@3.0.0-beta.16': + resolution: {integrity: sha512-8O2gWxWFiaoNkk7PGi0+p7NPGe/f8xJ3/INUufvje/RZOs7sJvlI1jnR4lydtRFa/mU0ylMXUXXjSK0fHDEYTA==} + engines: {node: '>=20.18.0'} + peerDependencies: + vue: ^2.7.0 || ^3.2.25 + peerDependenciesMeta: + vue: + optional: true + + '@vue/babel-helper-vue-transform-on@1.5.0': + resolution: {integrity: sha512-0dAYkerNhhHutHZ34JtTl2czVQHUNWv6xEbkdF5W+Yrv5pCWsqjeORdOgbtW2I9gWlt+wBmVn+ttqN9ZxR5tzA==} + + '@vue/babel-plugin-jsx@1.5.0': + resolution: {integrity: sha512-mneBhw1oOqCd2247O0Yw/mRwC9jIGACAJUlawkmMBiNmL4dGA2eMzuNZVNqOUfYTa6vqmND4CtOPzmEEEqLKFw==} + peerDependencies: + '@babel/core': ^7.0.0-0 + peerDependenciesMeta: + '@babel/core': + optional: true + + '@vue/babel-plugin-resolve-type@1.5.0': + resolution: {integrity: sha512-Wm/60o+53JwJODm4Knz47dxJnLDJ9FnKnGZJbUUf8nQRAtt6P+undLUAVU3Ha33LxOJe6IPoifRQ6F/0RrU31w==} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@vue/compiler-core@3.5.22': + resolution: {integrity: sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ==} + + '@vue/compiler-dom@3.5.22': + resolution: {integrity: sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA==} + + '@vue/compiler-sfc@3.5.22': + resolution: {integrity: sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ==} + + '@vue/compiler-ssr@3.5.22': + resolution: {integrity: sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww==} + + '@vue/devtools-api@6.6.4': + resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==} + + '@vue/devtools-api@7.7.7': + resolution: {integrity: sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==} + + '@vue/devtools-core@8.0.2': + resolution: {integrity: sha512-V7eKTTHoS6KfK8PSGMLZMhGv/9yNDrmv6Qc3r71QILulnzPnqK2frsTyx3e2MrhdUZnENPEm6hcb4z0GZOqNhw==} + peerDependencies: + vue: ^3.0.0 + + '@vue/devtools-electron@8.0.2': + resolution: {integrity: sha512-Zc0Dh6sp60hF44Qom133PyDwqfbzdLTbxVc0nvwi4D/Ikpe/YF5kCkJHTv8WXcXiEkkzxWaEdOvYgX6CnjYt6w==} + + '@vue/devtools-kit@7.7.7': + resolution: {integrity: sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==} + + '@vue/devtools-kit@8.0.2': + resolution: {integrity: sha512-yjZKdEmhJzQqbOh4KFBfTOQjDPMrjjBNCnHBvnTGJX+YLAqoUtY2J+cg7BE+EA8KUv8LprECq04ts75wCoIGWA==} + + '@vue/devtools-shared@7.7.7': + resolution: {integrity: sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==} + + '@vue/devtools-shared@8.0.2': + resolution: {integrity: sha512-mLU0QVdy5Lp40PMGSixDw/Kbd6v5dkQXltd2r+mdVQV7iUog2NlZuLxFZApFZ/mObUBDhoCpf0T3zF2FWWdeHw==} + + '@vue/devtools@8.0.2': + resolution: {integrity: sha512-/v4J6TfYkcVUXmkqEnTMrN2UcU1r5tFo8toFANAQABvZuPw4nMRGsqHhuJaqOIdlubddOmDSZMO0Hbar5tB1+g==} + hasBin: true + + '@vue/language-core@3.1.0': + resolution: {integrity: sha512-a7ns+X9vTbdmk7QLrvnZs8s4E1wwtxG/sELzr6F2j4pU+r/OoAv6jJGSz+5tVTU6e4+3rjepGhSP8jDmBBcb3w==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + '@vue/reactivity@3.5.22': + resolution: {integrity: sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A==} + + '@vue/runtime-core@3.5.22': + resolution: {integrity: sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ==} + + '@vue/runtime-dom@3.5.22': + resolution: {integrity: sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww==} + + '@vue/server-renderer@3.5.22': + resolution: {integrity: sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ==} + peerDependencies: + vue: 3.5.22 + + '@vue/shared@3.5.22': + resolution: {integrity: sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w==} + + '@vueuse/core@10.11.1': + resolution: {integrity: sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==} + + '@vueuse/core@12.8.2': + resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==} + + '@vueuse/core@13.9.0': + resolution: {integrity: sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA==} + peerDependencies: + vue: ^3.5.0 + + '@vueuse/metadata@10.11.1': + resolution: {integrity: sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==} + + '@vueuse/metadata@12.8.2': + resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==} + + '@vueuse/metadata@13.9.0': + resolution: {integrity: sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg==} + + '@vueuse/shared@10.11.1': + resolution: {integrity: sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==} + + '@vueuse/shared@12.8.2': + resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==} + + '@vueuse/shared@13.9.0': + resolution: {integrity: sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g==} + peerDependencies: + vue: ^3.5.0 + + accepts@1.3.8: + resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==} + engines: {node: '>= 0.6'} + + acorn@8.15.0: + resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} + engines: {node: '>=0.4.0'} + hasBin: true + + alien-signals@3.0.0: + resolution: {integrity: sha512-JHoRJf18Y6HN4/KZALr3iU+0vW9LKG+8FMThQlbn4+gv8utsLIkwpomjElGPccGeNwh0FI2HN6BLnyFLo6OyLQ==} + + ansis@4.2.0: + resolution: {integrity: sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==} + engines: {node: '>=14'} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + ast-kit@2.1.2: + resolution: {integrity: sha512-cl76xfBQM6pztbrFWRnxbrDm9EOqDr1BF6+qQnnDZG2Co2LjyUktkN9GTJfBAfdae+DbT2nJf2nCGAdDDN7W2g==} + engines: {node: '>=20.18.0'} + + ast-walker-scope@0.8.2: + resolution: {integrity: sha512-3pYeLyDZ6nJew9QeBhS4Nly02269Dkdk32+zdbbKmL6n4ZuaGorwwA+xx12xgOciA8BF1w9x+dlH7oUkFTW91w==} + engines: {node: '>=20.18.0'} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + axios@1.12.2: + resolution: {integrity: sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==} + + base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + + baseline-browser-mapping@2.8.10: + resolution: {integrity: sha512-uLfgBi+7IBNay8ECBO2mVMGZAc1VgZWEChxm4lv+TobGdG82LnXMjuNGo/BSSZZL4UmkWhxEHP2f5ziLNwGWMA==} + hasBin: true + + birpc@2.6.1: + resolution: {integrity: sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ==} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + browserslist@4.26.3: + resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + + bundle-name@4.1.0: + resolution: {integrity: sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==} + engines: {node: '>=18'} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + caniuse-lite@1.0.30001746: + resolution: {integrity: sha512-eA7Ys/DGw+pnkWWSE/id29f2IcPHVoE8wxtvE5JdvD2V28VTDPy1yEeo11Guz0sJ4ZeGRcm3uaTcAqK1LXaphA==} + + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + confbox@0.1.8: + resolution: {integrity: sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==} + + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie-es@1.2.2: + resolution: {integrity: sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==} + + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + + copy-anything@3.0.5: + resolution: {integrity: sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==} + engines: {node: '>=12.13'} + + cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + crossws@0.3.5: + resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.1.3: + resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + default-browser-id@5.0.0: + resolution: {integrity: sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==} + engines: {node: '>=18'} + + default-browser@5.2.1: + resolution: {integrity: sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==} + engines: {node: '>=18'} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-lazy-prop@3.0.0: + resolution: {integrity: sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==} + engines: {node: '>=12'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + detect-libc@2.1.1: + resolution: {integrity: sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==} + engines: {node: '>=8'} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + electron-to-chromium@1.5.228: + resolution: {integrity: sha512-nxkiyuqAn4MJ1QbobwqJILiDtu/jk14hEAWaMiJmNPh1Z+jqoFlBFZjdXwLWGeVSeu9hGLg6+2G9yJaW8rBIFA==} + + electron@36.9.3: + resolution: {integrity: sha512-eR5yswsA55zVTPDEIA/PSdVNBLOp0q0Wsavgx0S3BmJYOqKoH1gqzS+hggf0/aY5OvUjVNSHiJJA1VsB5aJUug==} + engines: {node: '>= 12.20.55'} + hasBin: true + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + engine.io-client@6.6.3: + resolution: {integrity: sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==} + + engine.io-parser@5.2.3: + resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} + engines: {node: '>=10.0.0'} + + engine.io@6.6.4: + resolution: {integrity: sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==} + engines: {node: '>=10.2.0'} + + enhanced-resolve@5.18.3: + resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==} + engines: {node: '>=10.13.0'} + + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-stack-parser-es@1.0.5: + resolution: {integrity: sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + esbuild@0.25.10: + resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + execa@9.6.0: + resolution: {integrity: sha512-jpWzZ1ZhwUmeWRhS7Qv3mhpOhLfwI+uAX4e5fOcXqwMR7EcJ0pj2kV1CVzHVMX/LphnKWD3LObjZCoJ71lKpHw==} + engines: {node: ^18.19.0 || >=20.5.0} + + exsolve@1.0.7: + resolution: {integrity: sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==} + + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + figures@6.1.0: + resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==} + engines: {node: '>=18'} + + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.4: + resolution: {integrity: sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==} + engines: {node: '>= 6'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@9.0.1: + resolution: {integrity: sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==} + engines: {node: '>=18'} + + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + h3@1.15.4: + resolution: {integrity: sha512-z5cFQWDffyOe4vQ9xIqNfCZdV4p//vy6fBnr8Q1AWnVZ0teurKMG66rLj++TKwKPUP3u7iMUvrvKaEUiQw2QWQ==} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + + hookable@5.5.3: + resolution: {integrity: sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + + human-signals@8.0.1: + resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} + engines: {node: '>=18.18.0'} + + iron-webcrypto@1.2.1: + resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==} + + is-docker@3.0.0: + resolution: {integrity: sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + hasBin: true + + is-inside-container@1.0.0: + resolution: {integrity: sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==} + engines: {node: '>=14.16'} + hasBin: true + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-stream@4.0.1: + resolution: {integrity: sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==} + engines: {node: '>=18'} + + is-unicode-supported@2.1.0: + resolution: {integrity: sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==} + engines: {node: '>=18'} + + is-what@4.1.16: + resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} + engines: {node: '>=12.13'} + + is-wsl@3.1.0: + resolution: {integrity: sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==} + engines: {node: '>=16'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kolorist@1.8.0: + resolution: {integrity: sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==} + + lightningcss-darwin-arm64@1.30.1: + resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.30.1: + resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.30.1: + resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.30.1: + resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.30.1: + resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.30.1: + resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.30.1: + resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.30.1: + resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.30.1: + resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.30.1: + resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.30.1: + resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} + engines: {node: '>= 12.0.0'} + + local-pkg@1.1.2: + resolution: {integrity: sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==} + engines: {node: '>=14'} + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lucide-react@0.544.0: + resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + lucide-vue-next@0.544.0: + resolution: {integrity: sha512-mDp/AdGOPIDkpFHnFiTWgQgCST9aBXHVaiobZfOMIvv7nrOukzF/TP+7KoOwrngdWRaH9TMiepMBIX1vsgKJ3g==} + peerDependencies: + vue: '>=3.0.1' + + magic-string-ast@1.0.2: + resolution: {integrity: sha512-8ngQgLhcT0t3YBdn9CGkZqCYlvwW9pm7aWJwd7AxseVWf1RU8ZHCQvG1mt3N5vvUme+pXTcHB8G/7fE666U8Vw==} + engines: {node: '>=20.18.0'} + + magic-string@0.30.19: + resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==} + + marked@16.3.0: + resolution: {integrity: sha512-K3UxuKu6l6bmA5FUwYho8CfJBlsUWAooKtdGgMcERSpF7gcBUrCGsLH7wDaaNOzwq18JzSUDyoEb/YsrqMac3w==} + engines: {node: '>= 20'} + hasBin: true + + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mitt@3.0.1: + resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + + mlly@1.8.0: + resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} + + mrmime@2.0.1: + resolution: {integrity: sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==} + engines: {node: '>=10'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + muggle-string@0.4.1: + resolution: {integrity: sha512-VNTrAak/KhO2i8dqqnqnAHOa3cYBwXEZe9h+D5h/1ZqFSTEFHdM65lR7RoIqq3tBBYavsOXV84NoHXZ0AkPyqQ==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + nanoid@5.1.6: + resolution: {integrity: sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==} + engines: {node: ^18 || >=20} + hasBin: true + + negotiator@0.6.3: + resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} + engines: {node: '>= 0.6'} + + node-mock-http@1.0.3: + resolution: {integrity: sha512-jN8dK25fsfnMrVsEhluUTPkBFY+6ybu7jSB1n+ri/vOGjJxU8J9CZhpSGkHXSkFjtUhbmoncG/YG9ta5Ludqog==} + + node-releases@2.0.21: + resolution: {integrity: sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + npm-run-path@6.0.0: + resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} + engines: {node: '>=18'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + open@10.2.0: + resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} + engines: {node: '>=18'} + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + parse-ms@4.0.0: + resolution: {integrity: sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==} + engines: {node: '>=18'} + + path-browserify@1.0.1: + resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-key@4.0.0: + resolution: {integrity: sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==} + engines: {node: '>=12'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + + perfect-debounce@1.0.0: + resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} + + perfect-debounce@2.0.0: + resolution: {integrity: sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + pkg-types@1.3.1: + resolution: {integrity: sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==} + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss@8.5.6: + resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} + engines: {node: ^10 || ^12 || >=14} + + pretty-ms@9.3.0: + resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} + engines: {node: '>=18'} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pump@3.0.3: + resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==} + + quansync@0.2.11: + resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + radix-vue@1.9.17: + resolution: {integrity: sha512-mVCu7I2vXt1L2IUYHTt0sZMz7s1K2ZtqKeTIxG3yC5mMFfLBG4FtE1FDeRMpDd+Hhg/ybi9+iXmAP1ISREndoQ==} + peerDependencies: + vue: '>= 3.2.0' + + radix3@1.1.2: + resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==} + + react@19.2.0: + resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==} + engines: {node: '>=0.10.0'} + + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} + + reka-ui@2.5.1: + resolution: {integrity: sha512-QJGB3q21wQ1Kw28HhhNDpjfFe8qpePX1gK4FTBRd68XTh9aEnhR5bTJnlV0jxi8FBPh0xivZBeNFUc3jiGx7mQ==} + peerDependencies: + vue: '>= 3.2.0' + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + + rollup@4.52.3: + resolution: {integrity: sha512-RIDh866U8agLgiIcdpB+COKnlCreHJLfIhWC3LVflku5YHfpnsIKigRZeFfMfCc4dVcqNVfQQ5gO/afOck064A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + run-applescript@7.1.0: + resolution: {integrity: sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==} + engines: {node: '>=18'} + + scule@1.3.0: + resolution: {integrity: sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==} + + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + + sirv@3.0.2: + resolution: {integrity: sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==} + engines: {node: '>=18'} + + socket.io-adapter@2.5.5: + resolution: {integrity: sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==} + + socket.io-client@4.8.1: + resolution: {integrity: sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==} + engines: {node: '>=10.0.0'} + + socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + + socket.io@4.8.1: + resolution: {integrity: sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==} + engines: {node: '>=10.2.0'} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + speakingurl@14.0.1: + resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==} + engines: {node: '>=0.10.0'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + strip-final-newline@4.0.0: + resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} + engines: {node: '>=18'} + + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + + superjson@2.2.2: + resolution: {integrity: sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==} + engines: {node: '>=16'} + + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + + tailwindcss@4.1.14: + resolution: {integrity: sha512-b7pCxjGO98LnxVkKjaZSDeNuljC4ueKUddjENJOADtubtdo8llTaJy7HwBMeLNSSo2N5QIAgklslK1+Ir8r6CA==} + + tapable@2.2.3: + resolution: {integrity: sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==} + engines: {node: '>=6'} + + tar@7.5.1: + resolution: {integrity: sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==} + engines: {node: '>=18'} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + totalist@3.0.1: + resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} + engines: {node: '>=6'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + + type-fest@4.41.0: + resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} + engines: {node: '>=16'} + + ufo@1.6.1: + resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==} + + uncrypto@0.1.3: + resolution: {integrity: sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@7.13.0: + resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==} + + unicorn-magic@0.3.0: + resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==} + engines: {node: '>=18'} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + unplugin-utils@0.2.5: + resolution: {integrity: sha512-gwXJnPRewT4rT7sBi/IvxKTjsms7jX7QIDLOClApuZwR49SXbrB1z2NLUZ+vDHyqCj/n58OzRRqaW+B8OZi8vg==} + engines: {node: '>=18.12.0'} + + unplugin-utils@0.3.0: + resolution: {integrity: sha512-JLoggz+PvLVMJo+jZt97hdIIIZ2yTzGgft9e9q8iMrC4ewufl62ekeW7mixBghonn2gVb/ICjyvlmOCUBnJLQg==} + engines: {node: '>=20.19.0'} + + unplugin-vue-router@0.15.0: + resolution: {integrity: sha512-PyGehCjd9Ny9h+Uer4McbBjjib3lHihcyUEILa7pHKl6+rh8N7sFyw4ZkV+N30Oq2zmIUG7iKs3qpL0r+gXAaQ==} + peerDependencies: + '@vue/compiler-sfc': ^3.5.17 + vue-router: ^4.5.1 + peerDependenciesMeta: + vue-router: + optional: true + + unplugin@2.3.10: + resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} + engines: {node: '>=18.12.0'} + + update-browserslist-db@1.1.3: + resolution: {integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vary@1.1.2: + resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} + engines: {node: '>= 0.8'} + + vee-validate@4.15.1: + resolution: {integrity: sha512-DkFsiTwEKau8VIxyZBGdO6tOudD+QoUBPuHj3e6QFqmbfCRj1ArmYWue9lEp6jLSWBIw4XPlDLjFIZNLdRAMSg==} + peerDependencies: + vue: ^3.4.26 + + vite-dev-rpc@1.1.0: + resolution: {integrity: sha512-pKXZlgoXGoE8sEKiKJSng4hI1sQ4wi5YT24FCrwrLt6opmkjlqPPVmiPWWJn8M8byMxRGzp1CrFuqQs4M/Z39A==} + peerDependencies: + vite: ^2.9.0 || ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.1 || ^7.0.0-0 + + vite-hot-client@2.1.0: + resolution: {integrity: sha512-7SpgZmU7R+dDnSmvXE1mfDtnHLHQSisdySVR7lO8ceAXvM0otZeuQQ6C8LrS5d/aYyP/QZ0hI0L+dIPrm4YlFQ==} + peerDependencies: + vite: ^2.6.0 || ^3.0.0 || ^4.0.0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite-plugin-inspect@11.3.3: + resolution: {integrity: sha512-u2eV5La99oHoYPHE6UvbwgEqKKOQGz86wMg40CCosP6q8BkB6e5xPneZfYagK4ojPJSj5anHCrnvC20DpwVdRA==} + engines: {node: '>=14'} + peerDependencies: + '@nuxt/kit': '*' + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + + vite-plugin-vue-devtools@8.0.2: + resolution: {integrity: sha512-1069qvMBcyAu3yXQlvYrkwoyLOk0lSSR/gTKy/vy+Det7TXnouGei6ZcKwr5TIe938v/14oLlp0ow6FSJkkORA==} + engines: {node: '>=v14.21.3'} + peerDependencies: + vite: ^6.0.0 || ^7.0.0-0 + + vite-plugin-vue-inspector@5.3.2: + resolution: {integrity: sha512-YvEKooQcSiBTAs0DoYLfefNja9bLgkFM7NI2b07bE2SruuvX0MEa9cMaxjKVMkeCp5Nz9FRIdcN1rOdFVBeL6Q==} + peerDependencies: + vite: ^3.0.0-0 || ^4.0.0-0 || ^5.0.0-0 || ^6.0.0-0 || ^7.0.0-0 + + vite@7.1.7: + resolution: {integrity: sha512-VbA8ScMvAISJNJVbRDTJdCwqQoAareR/wutevKanhR2/1EkoXVZVkkORaYm/tNVCjP/UDTKtcw3bAkwOUdedmA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vue-demi@0.14.10: + resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==} + engines: {node: '>=12'} + hasBin: true + peerDependencies: + '@vue/composition-api': ^1.0.0-rc.1 + vue: ^3.0.0-0 || ^2.6.0 + peerDependenciesMeta: + '@vue/composition-api': + optional: true + + vue-router@4.5.1: + resolution: {integrity: sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==} + peerDependencies: + vue: ^3.2.0 + + vue-sonner@2.0.9: + resolution: {integrity: sha512-i6BokNlNDL93fpzNxN/LZSn6D6MzlO+i3qXt6iVZne3x1k7R46d5HlFB4P8tYydhgqOrRbIZEsnRd3kG7qGXyw==} + peerDependencies: + '@nuxt/kit': ^4.0.3 + '@nuxt/schema': ^4.0.3 + nuxt: ^4.0.3 + peerDependenciesMeta: + '@nuxt/kit': + optional: true + '@nuxt/schema': + optional: true + nuxt: + optional: true + + vue@3.5.22: + resolution: {integrity: sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + + webpack-virtual-modules@0.6.2: + resolution: {integrity: sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.17.1: + resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + wsl-utils@0.1.0: + resolution: {integrity: sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==} + engines: {node: '>=18'} + + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} + engines: {node: '>=0.4.0'} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yaml@2.8.1: + resolution: {integrity: sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==} + engines: {node: '>= 14.6'} + hasBin: true + + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + +snapshots: + + '@babel/code-frame@7.27.1': + dependencies: + '@babel/helper-validator-identifier': 7.27.1 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.28.4': {} + + '@babel/core@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-compilation-targets': 7.27.2 + '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.4) + '@babel/helpers': 7.28.4 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.28.3': + dependencies: + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.27.3': + dependencies: + '@babel/types': 7.28.4 + + '@babel/helper-compilation-targets@7.27.2': + dependencies: + '@babel/compat-data': 7.28.4 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.26.3 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/helper-replace-supers': 7.27.1(@babel/core@7.28.4) + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/traverse': 7.28.4 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-member-expression-to-functions@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.27.1': + dependencies: + '@babel/types': 7.28.4 + + '@babel/helper-plugin-utils@7.27.1': {} + + '@babel/helper-replace-supers@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-member-expression-to-functions': 7.27.1 + '@babel/helper-optimise-call-expression': 7.27.1 + '@babel/traverse': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.27.1': + dependencies: + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.27.1': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.28.4': + dependencies: + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + + '@babel/parser@7.28.4': + dependencies: + '@babel/types': 7.28.4 + + '@babel/plugin-proposal-decorators@7.28.0(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-decorators': 7.27.1(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + + '@babel/plugin-syntax-decorators@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-attributes@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-import-meta@7.10.4(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-jsx@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-syntax-typescript@7.27.1(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-plugin-utils': 7.27.1 + + '@babel/plugin-transform-typescript@7.28.0(@babel/core@7.28.4)': + dependencies: + '@babel/core': 7.28.4 + '@babel/helper-annotate-as-pure': 7.27.3 + '@babel/helper-create-class-features-plugin': 7.28.3(@babel/core@7.28.4) + '@babel/helper-plugin-utils': 7.27.1 + '@babel/helper-skip-transparent-expression-wrappers': 7.27.1 + '@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.4) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.27.2': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/parser': 7.28.4 + '@babel/types': 7.28.4 + + '@babel/traverse@7.28.4': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/generator': 7.28.3 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.28.4 + '@babel/template': 7.27.2 + '@babel/types': 7.28.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.28.4': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.27.1 + + '@electron/get@2.0.3': + dependencies: + debug: 4.4.3 + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@esbuild/aix-ppc64@0.25.10': + optional: true + + '@esbuild/android-arm64@0.25.10': + optional: true + + '@esbuild/android-arm@0.25.10': + optional: true + + '@esbuild/android-x64@0.25.10': + optional: true + + '@esbuild/darwin-arm64@0.25.10': + optional: true + + '@esbuild/darwin-x64@0.25.10': + optional: true + + '@esbuild/freebsd-arm64@0.25.10': + optional: true + + '@esbuild/freebsd-x64@0.25.10': + optional: true + + '@esbuild/linux-arm64@0.25.10': + optional: true + + '@esbuild/linux-arm@0.25.10': + optional: true + + '@esbuild/linux-ia32@0.25.10': + optional: true + + '@esbuild/linux-loong64@0.25.10': + optional: true + + '@esbuild/linux-mips64el@0.25.10': + optional: true + + '@esbuild/linux-ppc64@0.25.10': + optional: true + + '@esbuild/linux-riscv64@0.25.10': + optional: true + + '@esbuild/linux-s390x@0.25.10': + optional: true + + '@esbuild/linux-x64@0.25.10': + optional: true + + '@esbuild/netbsd-arm64@0.25.10': + optional: true + + '@esbuild/netbsd-x64@0.25.10': + optional: true + + '@esbuild/openbsd-arm64@0.25.10': + optional: true + + '@esbuild/openbsd-x64@0.25.10': + optional: true + + '@esbuild/openharmony-arm64@0.25.10': + optional: true + + '@esbuild/sunos-x64@0.25.10': + optional: true + + '@esbuild/win32-arm64@0.25.10': + optional: true + + '@esbuild/win32-ia32@0.25.10': + optional: true + + '@esbuild/win32-x64@0.25.10': + optional: true + + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/utils@0.2.10': {} + + '@floating-ui/vue@1.1.9(vue@3.5.22)': + dependencies: + '@floating-ui/dom': 1.7.4 + '@floating-ui/utils': 0.2.10 + vue-demi: 0.14.10(vue@3.5.22) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@internationalized/date@3.9.0': + dependencies: + '@swc/helpers': 0.5.17 + + '@internationalized/number@3.6.5': + dependencies: + '@swc/helpers': 0.5.17 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.2 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@polka/url@1.0.0-next.29': {} + + '@radix-ui/react-compose-refs@1.1.2(react@19.2.0)': + dependencies: + react: 19.2.0 + + '@radix-ui/react-slot@1.2.3(react@19.2.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(react@19.2.0) + react: 19.2.0 + + '@rolldown/pluginutils@1.0.0-beta.29': {} + + '@rollup/rollup-android-arm-eabi@4.52.3': + optional: true + + '@rollup/rollup-android-arm64@4.52.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.52.3': + optional: true + + '@rollup/rollup-darwin-x64@4.52.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.52.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.52.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.52.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.52.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.52.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.52.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.52.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.52.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.52.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.52.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.52.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.52.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.52.3': + optional: true + + '@sec-ant/readable-stream@0.4.1': {} + + '@sindresorhus/is@4.6.0': {} + + '@sindresorhus/merge-streams@4.0.0': {} + + '@socket.io/component-emitter@3.1.2': {} + + '@swc/helpers@0.5.17': + dependencies: + tslib: 2.8.1 + + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + + '@tailwindcss/node@4.1.14': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.18.3 + jiti: 2.6.1 + lightningcss: 1.30.1 + magic-string: 0.30.19 + source-map-js: 1.2.1 + tailwindcss: 4.1.14 + + '@tailwindcss/oxide-android-arm64@4.1.14': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.14': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.14': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.14': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.14': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.14': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.14': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.14': + optional: true + + '@tailwindcss/oxide@4.1.14': + dependencies: + detect-libc: 2.1.1 + tar: 7.5.1 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.14 + '@tailwindcss/oxide-darwin-arm64': 4.1.14 + '@tailwindcss/oxide-darwin-x64': 4.1.14 + '@tailwindcss/oxide-freebsd-x64': 4.1.14 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.14 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.14 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.14 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.14 + '@tailwindcss/oxide-linux-x64-musl': 4.1.14 + '@tailwindcss/oxide-wasm32-wasi': 4.1.14 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.14 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.14 + + '@tailwindcss/typography@0.5.19(tailwindcss@4.1.14)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.1.14 + + '@tailwindcss/vite@4.1.14(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))': + dependencies: + '@tailwindcss/node': 4.1.14 + '@tailwindcss/oxide': 4.1.14 + tailwindcss: 4.1.14 + vite: 7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) + + '@tanstack/table-core@8.21.3': {} + + '@tanstack/virtual-core@3.13.12': {} + + '@tanstack/vue-table@8.21.3(vue@3.5.22)': + dependencies: + '@tanstack/table-core': 8.21.3 + vue: 3.5.22 + + '@tanstack/vue-virtual@3.13.12(vue@3.5.22)': + dependencies: + '@tanstack/virtual-core': 3.13.12 + vue: 3.5.22 + + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.0.4 + '@types/keyv': 3.1.4 + '@types/node': 22.18.8 + '@types/responselike': 1.0.3 + + '@types/cors@2.8.19': + dependencies: + '@types/node': 24.6.2 + + '@types/estree@1.0.8': {} + + '@types/http-cache-semantics@4.0.4': {} + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 22.18.8 + + '@types/node@22.18.8': + dependencies: + undici-types: 6.21.0 + + '@types/node@24.6.2': + dependencies: + undici-types: 7.13.0 + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 22.18.8 + + '@types/web-bluetooth@0.0.20': {} + + '@types/web-bluetooth@0.0.21': {} + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 22.18.8 + optional: true + + '@vee-validate/zod@4.15.1(vue@3.5.22)(zod@3.25.76)': + dependencies: + type-fest: 4.41.0 + vee-validate: 4.15.1(vue@3.5.22) + zod: 3.25.76 + transitivePeerDependencies: + - vue + + '@vitejs/plugin-vue@6.0.1(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))(vue@3.5.22)': + dependencies: + '@rolldown/pluginutils': 1.0.0-beta.29 + vite: 7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) + vue: 3.5.22 + + '@volar/language-core@2.4.23': + dependencies: + '@volar/source-map': 2.4.23 + + '@volar/source-map@2.4.23': {} + + '@vue-macros/common@3.0.0-beta.16(vue@3.5.22)': + dependencies: + '@vue/compiler-sfc': 3.5.22 + ast-kit: 2.1.2 + local-pkg: 1.1.2 + magic-string-ast: 1.0.2 + unplugin-utils: 0.2.5 + optionalDependencies: + vue: 3.5.22 + + '@vue/babel-helper-vue-transform-on@1.5.0': {} + + '@vue/babel-plugin-jsx@1.5.0(@babel/core@7.28.4)': + dependencies: + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/plugin-syntax-jsx': 7.27.1(@babel/core@7.28.4) + '@babel/template': 7.27.2 + '@babel/traverse': 7.28.4 + '@babel/types': 7.28.4 + '@vue/babel-helper-vue-transform-on': 1.5.0 + '@vue/babel-plugin-resolve-type': 1.5.0(@babel/core@7.28.4) + '@vue/shared': 3.5.22 + optionalDependencies: + '@babel/core': 7.28.4 + transitivePeerDependencies: + - supports-color + + '@vue/babel-plugin-resolve-type@1.5.0(@babel/core@7.28.4)': + dependencies: + '@babel/code-frame': 7.27.1 + '@babel/core': 7.28.4 + '@babel/helper-module-imports': 7.27.1 + '@babel/helper-plugin-utils': 7.27.1 + '@babel/parser': 7.28.4 + '@vue/compiler-sfc': 3.5.22 + transitivePeerDependencies: + - supports-color + + '@vue/compiler-core@3.5.22': + dependencies: + '@babel/parser': 7.28.4 + '@vue/shared': 3.5.22 + entities: 4.5.0 + estree-walker: 2.0.2 + source-map-js: 1.2.1 + + '@vue/compiler-dom@3.5.22': + dependencies: + '@vue/compiler-core': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/compiler-sfc@3.5.22': + dependencies: + '@babel/parser': 7.28.4 + '@vue/compiler-core': 3.5.22 + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-ssr': 3.5.22 + '@vue/shared': 3.5.22 + estree-walker: 2.0.2 + magic-string: 0.30.19 + postcss: 8.5.6 + source-map-js: 1.2.1 + + '@vue/compiler-ssr@3.5.22': + dependencies: + '@vue/compiler-dom': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/devtools-api@6.6.4': {} + + '@vue/devtools-api@7.7.7': + dependencies: + '@vue/devtools-kit': 7.7.7 + + '@vue/devtools-core@8.0.2(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))(vue@3.5.22)': + dependencies: + '@vue/devtools-kit': 8.0.2 + '@vue/devtools-shared': 8.0.2 + mitt: 3.0.1 + nanoid: 5.1.6 + pathe: 2.0.3 + vite-hot-client: 2.1.0(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)) + vue: 3.5.22 + transitivePeerDependencies: + - vite + + '@vue/devtools-electron@8.0.2(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))(vue@3.5.22)': + dependencies: + '@vue/devtools-core': 8.0.2(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))(vue@3.5.22) + '@vue/devtools-kit': 8.0.2 + '@vue/devtools-shared': 8.0.2 + electron: 36.9.3 + execa: 9.6.0 + h3: 1.15.4 + pathe: 2.0.3 + socket.io: 4.8.1 + socket.io-client: 4.8.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + - vite + - vue + + '@vue/devtools-kit@7.7.7': + dependencies: + '@vue/devtools-shared': 7.7.7 + birpc: 2.6.1 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 1.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-kit@8.0.2': + dependencies: + '@vue/devtools-shared': 8.0.2 + birpc: 2.6.1 + hookable: 5.5.3 + mitt: 3.0.1 + perfect-debounce: 2.0.0 + speakingurl: 14.0.1 + superjson: 2.2.2 + + '@vue/devtools-shared@7.7.7': + dependencies: + rfdc: 1.4.1 + + '@vue/devtools-shared@8.0.2': + dependencies: + rfdc: 1.4.1 + + '@vue/devtools@8.0.2(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))(vue@3.5.22)': + dependencies: + '@vue/devtools-electron': 8.0.2(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))(vue@3.5.22) + '@vue/devtools-kit': 8.0.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + - vite + - vue + + '@vue/language-core@3.1.0': + dependencies: + '@volar/language-core': 2.4.23 + '@vue/compiler-dom': 3.5.22 + '@vue/shared': 3.5.22 + alien-signals: 3.0.0 + muggle-string: 0.4.1 + path-browserify: 1.0.1 + picomatch: 4.0.3 + + '@vue/reactivity@3.5.22': + dependencies: + '@vue/shared': 3.5.22 + + '@vue/runtime-core@3.5.22': + dependencies: + '@vue/reactivity': 3.5.22 + '@vue/shared': 3.5.22 + + '@vue/runtime-dom@3.5.22': + dependencies: + '@vue/reactivity': 3.5.22 + '@vue/runtime-core': 3.5.22 + '@vue/shared': 3.5.22 + csstype: 3.1.3 + + '@vue/server-renderer@3.5.22(vue@3.5.22)': + dependencies: + '@vue/compiler-ssr': 3.5.22 + '@vue/shared': 3.5.22 + vue: 3.5.22 + + '@vue/shared@3.5.22': {} + + '@vueuse/core@10.11.1(vue@3.5.22)': + dependencies: + '@types/web-bluetooth': 0.0.20 + '@vueuse/metadata': 10.11.1 + '@vueuse/shared': 10.11.1(vue@3.5.22) + vue-demi: 0.14.10(vue@3.5.22) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/core@12.8.2': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 12.8.2 + '@vueuse/shared': 12.8.2 + vue: 3.5.22 + transitivePeerDependencies: + - typescript + + '@vueuse/core@13.9.0(vue@3.5.22)': + dependencies: + '@types/web-bluetooth': 0.0.21 + '@vueuse/metadata': 13.9.0 + '@vueuse/shared': 13.9.0(vue@3.5.22) + vue: 3.5.22 + + '@vueuse/metadata@10.11.1': {} + + '@vueuse/metadata@12.8.2': {} + + '@vueuse/metadata@13.9.0': {} + + '@vueuse/shared@10.11.1(vue@3.5.22)': + dependencies: + vue-demi: 0.14.10(vue@3.5.22) + transitivePeerDependencies: + - '@vue/composition-api' + - vue + + '@vueuse/shared@12.8.2': + dependencies: + vue: 3.5.22 + transitivePeerDependencies: + - typescript + + '@vueuse/shared@13.9.0(vue@3.5.22)': + dependencies: + vue: 3.5.22 + + accepts@1.3.8: + dependencies: + mime-types: 2.1.35 + negotiator: 0.6.3 + + acorn@8.15.0: {} + + alien-signals@3.0.0: {} + + ansis@4.2.0: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + ast-kit@2.1.2: + dependencies: + '@babel/parser': 7.28.4 + pathe: 2.0.3 + + ast-walker-scope@0.8.2: + dependencies: + '@babel/parser': 7.28.4 + ast-kit: 2.1.2 + + asynckit@0.4.0: {} + + axios@1.12.2: + dependencies: + follow-redirects: 1.15.11 + form-data: 4.0.4 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + + base64id@2.0.0: {} + + baseline-browser-mapping@2.8.10: {} + + birpc@2.6.1: {} + + boolean@3.2.0: + optional: true + + browserslist@4.26.3: + dependencies: + baseline-browser-mapping: 2.8.10 + caniuse-lite: 1.0.30001746 + electron-to-chromium: 1.5.228 + node-releases: 2.0.21 + update-browserslist-db: 1.1.3(browserslist@4.26.3) + + buffer-crc32@0.2.13: {} + + bundle-name@4.1.0: + dependencies: + run-applescript: 7.1.0 + + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + caniuse-lite@1.0.30001746: {} + + chokidar@4.0.3: + dependencies: + readdirp: 4.1.2 + + chownr@3.0.0: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + clsx@2.1.1: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + confbox@0.1.8: {} + + confbox@0.2.2: {} + + convert-source-map@2.0.0: {} + + cookie-es@1.2.2: {} + + cookie@0.7.2: {} + + copy-anything@3.0.5: + dependencies: + is-what: 4.1.16 + + cors@2.8.5: + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + crossws@0.3.5: + dependencies: + uncrypto: 0.1.3 + + cssesc@3.0.0: {} + + csstype@3.1.3: {} + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + default-browser-id@5.0.0: {} + + default-browser@5.2.1: + dependencies: + bundle-name: 4.1.0 + default-browser-id: 5.0.0 + + defer-to-connect@2.0.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + optional: true + + define-lazy-prop@3.0.0: {} + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + optional: true + + defu@6.1.4: {} + + delayed-stream@1.0.0: {} + + destr@2.0.5: {} + + detect-libc@2.1.1: {} + + detect-node@2.1.0: + optional: true + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + electron-to-chromium@1.5.228: {} + + electron@36.9.3: + dependencies: + '@electron/get': 2.0.3 + '@types/node': 22.18.8 + extract-zip: 2.0.1 + transitivePeerDependencies: + - supports-color + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + engine.io-client@6.6.3: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + xmlhttprequest-ssl: 2.1.2 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + engine.io-parser@5.2.3: {} + + engine.io@6.6.4: + dependencies: + '@types/cors': 2.8.19 + '@types/node': 24.6.2 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.7.2 + cors: 2.8.5 + debug: 4.3.7 + engine.io-parser: 5.2.3 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + enhanced-resolve@5.18.3: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.3 + + entities@4.5.0: {} + + env-paths@2.2.1: {} + + error-stack-parser-es@1.0.5: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + es6-error@4.1.1: + optional: true + + esbuild@0.25.10: + optionalDependencies: + '@esbuild/aix-ppc64': 0.25.10 + '@esbuild/android-arm': 0.25.10 + '@esbuild/android-arm64': 0.25.10 + '@esbuild/android-x64': 0.25.10 + '@esbuild/darwin-arm64': 0.25.10 + '@esbuild/darwin-x64': 0.25.10 + '@esbuild/freebsd-arm64': 0.25.10 + '@esbuild/freebsd-x64': 0.25.10 + '@esbuild/linux-arm': 0.25.10 + '@esbuild/linux-arm64': 0.25.10 + '@esbuild/linux-ia32': 0.25.10 + '@esbuild/linux-loong64': 0.25.10 + '@esbuild/linux-mips64el': 0.25.10 + '@esbuild/linux-ppc64': 0.25.10 + '@esbuild/linux-riscv64': 0.25.10 + '@esbuild/linux-s390x': 0.25.10 + '@esbuild/linux-x64': 0.25.10 + '@esbuild/netbsd-arm64': 0.25.10 + '@esbuild/netbsd-x64': 0.25.10 + '@esbuild/openbsd-arm64': 0.25.10 + '@esbuild/openbsd-x64': 0.25.10 + '@esbuild/openharmony-arm64': 0.25.10 + '@esbuild/sunos-x64': 0.25.10 + '@esbuild/win32-arm64': 0.25.10 + '@esbuild/win32-ia32': 0.25.10 + '@esbuild/win32-x64': 0.25.10 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: + optional: true + + estree-walker@2.0.2: {} + + execa@9.6.0: + dependencies: + '@sindresorhus/merge-streams': 4.0.0 + cross-spawn: 7.0.6 + figures: 6.1.0 + get-stream: 9.0.1 + human-signals: 8.0.1 + is-plain-obj: 4.1.0 + is-stream: 4.0.1 + npm-run-path: 6.0.0 + pretty-ms: 9.3.0 + signal-exit: 4.1.0 + strip-final-newline: 4.0.0 + yoctocolors: 2.1.2 + + exsolve@1.0.7: {} + + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + + fast-deep-equal@3.1.3: {} + + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + figures@6.1.0: + dependencies: + is-unicode-supported: 2.1.0 + + follow-redirects@1.15.11: {} + + form-data@4.0.4: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.2 + mime-types: 2.1.35 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stream@5.2.0: + dependencies: + pump: 3.0.3 + + get-stream@9.0.1: + dependencies: + '@sec-ant/readable-stream': 0.4.1 + is-stream: 4.0.1 + + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.7.2 + serialize-error: 7.0.1 + optional: true + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + optional: true + + gopd@1.2.0: {} + + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + + graceful-fs@4.2.11: {} + + h3@1.15.4: + dependencies: + cookie-es: 1.2.2 + crossws: 0.3.5 + defu: 6.1.4 + destr: 2.0.5 + iron-webcrypto: 1.2.1 + node-mock-http: 1.0.3 + radix3: 1.1.2 + ufo: 1.6.1 + uncrypto: 0.1.3 + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + optional: true + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hookable@5.5.3: {} + + http-cache-semantics@4.2.0: {} + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + human-signals@8.0.1: {} + + iron-webcrypto@1.2.1: {} + + is-docker@3.0.0: {} + + is-inside-container@1.0.0: + dependencies: + is-docker: 3.0.0 + + is-plain-obj@4.1.0: {} + + is-stream@4.0.1: {} + + is-unicode-supported@2.1.0: {} + + is-what@4.1.16: {} + + is-wsl@3.1.0: + dependencies: + is-inside-container: 1.0.0 + + isexe@2.0.0: {} + + jiti@2.6.1: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-stringify-safe@5.0.1: + optional: true + + json5@2.2.3: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kolorist@1.8.0: {} + + lightningcss-darwin-arm64@1.30.1: + optional: true + + lightningcss-darwin-x64@1.30.1: + optional: true + + lightningcss-freebsd-x64@1.30.1: + optional: true + + lightningcss-linux-arm-gnueabihf@1.30.1: + optional: true + + lightningcss-linux-arm64-gnu@1.30.1: + optional: true + + lightningcss-linux-arm64-musl@1.30.1: + optional: true + + lightningcss-linux-x64-gnu@1.30.1: + optional: true + + lightningcss-linux-x64-musl@1.30.1: + optional: true + + lightningcss-win32-arm64-msvc@1.30.1: + optional: true + + lightningcss-win32-x64-msvc@1.30.1: + optional: true + + lightningcss@1.30.1: + dependencies: + detect-libc: 2.1.1 + optionalDependencies: + lightningcss-darwin-arm64: 1.30.1 + lightningcss-darwin-x64: 1.30.1 + lightningcss-freebsd-x64: 1.30.1 + lightningcss-linux-arm-gnueabihf: 1.30.1 + lightningcss-linux-arm64-gnu: 1.30.1 + lightningcss-linux-arm64-musl: 1.30.1 + lightningcss-linux-x64-gnu: 1.30.1 + lightningcss-linux-x64-musl: 1.30.1 + lightningcss-win32-arm64-msvc: 1.30.1 + lightningcss-win32-x64-msvc: 1.30.1 + + local-pkg@1.1.2: + dependencies: + mlly: 1.8.0 + pkg-types: 2.3.0 + quansync: 0.2.11 + + lowercase-keys@2.0.0: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lucide-react@0.544.0(react@19.2.0): + dependencies: + react: 19.2.0 + + lucide-vue-next@0.544.0(vue@3.5.22): + dependencies: + vue: 3.5.22 + + magic-string-ast@1.0.2: + dependencies: + magic-string: 0.30.19 + + magic-string@0.30.19: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + marked@16.3.0: {} + + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + + math-intrinsics@1.1.0: {} + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + + minipass@7.1.2: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.2 + + mitt@3.0.1: {} + + mlly@1.8.0: + dependencies: + acorn: 8.15.0 + pathe: 2.0.3 + pkg-types: 1.3.1 + ufo: 1.6.1 + + mrmime@2.0.1: {} + + ms@2.1.3: {} + + muggle-string@0.4.1: {} + + nanoid@3.3.11: {} + + nanoid@5.1.6: {} + + negotiator@0.6.3: {} + + node-mock-http@1.0.3: {} + + node-releases@2.0.21: {} + + normalize-url@6.1.0: {} + + npm-run-path@6.0.0: + dependencies: + path-key: 4.0.0 + unicorn-magic: 0.3.0 + + object-assign@4.1.1: {} + + object-keys@1.1.1: + optional: true + + ohash@2.0.11: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + open@10.2.0: + dependencies: + default-browser: 5.2.1 + define-lazy-prop: 3.0.0 + is-inside-container: 1.0.0 + wsl-utils: 0.1.0 + + p-cancelable@2.1.1: {} + + parse-ms@4.0.0: {} + + path-browserify@1.0.1: {} + + path-key@3.1.1: {} + + path-key@4.0.0: {} + + pathe@2.0.3: {} + + pend@1.2.0: {} + + perfect-debounce@1.0.0: {} + + perfect-debounce@2.0.0: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + pkg-types@1.3.1: + dependencies: + confbox: 0.1.8 + mlly: 1.8.0 + pathe: 2.0.3 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.7 + pathe: 2.0.3 + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.6: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + pretty-ms@9.3.0: + dependencies: + parse-ms: 4.0.0 + + progress@2.0.3: {} + + proxy-from-env@1.1.0: {} + + pump@3.0.3: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + quansync@0.2.11: {} + + quick-lru@5.1.1: {} + + radix-vue@1.9.17(vue@3.5.22): + dependencies: + '@floating-ui/dom': 1.7.4 + '@floating-ui/vue': 1.1.9(vue@3.5.22) + '@internationalized/date': 3.9.0 + '@internationalized/number': 3.6.5 + '@tanstack/vue-virtual': 3.13.12(vue@3.5.22) + '@vueuse/core': 10.11.1(vue@3.5.22) + '@vueuse/shared': 10.11.1(vue@3.5.22) + aria-hidden: 1.2.6 + defu: 6.1.4 + fast-deep-equal: 3.1.3 + nanoid: 5.1.6 + vue: 3.5.22 + transitivePeerDependencies: + - '@vue/composition-api' + + radix3@1.1.2: {} + + react@19.2.0: {} + + readdirp@4.1.2: {} + + reka-ui@2.5.1(vue@3.5.22): + dependencies: + '@floating-ui/dom': 1.7.4 + '@floating-ui/vue': 1.1.9(vue@3.5.22) + '@internationalized/date': 3.9.0 + '@internationalized/number': 3.6.5 + '@tanstack/vue-virtual': 3.13.12(vue@3.5.22) + '@vueuse/core': 12.8.2 + '@vueuse/shared': 12.8.2 + aria-hidden: 1.2.6 + defu: 6.1.4 + ohash: 2.0.11 + vue: 3.5.22 + transitivePeerDependencies: + - '@vue/composition-api' + - typescript + + resolve-alpn@1.2.1: {} + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + rfdc@1.4.1: {} + + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + + rollup@4.52.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.52.3 + '@rollup/rollup-android-arm64': 4.52.3 + '@rollup/rollup-darwin-arm64': 4.52.3 + '@rollup/rollup-darwin-x64': 4.52.3 + '@rollup/rollup-freebsd-arm64': 4.52.3 + '@rollup/rollup-freebsd-x64': 4.52.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.52.3 + '@rollup/rollup-linux-arm-musleabihf': 4.52.3 + '@rollup/rollup-linux-arm64-gnu': 4.52.3 + '@rollup/rollup-linux-arm64-musl': 4.52.3 + '@rollup/rollup-linux-loong64-gnu': 4.52.3 + '@rollup/rollup-linux-ppc64-gnu': 4.52.3 + '@rollup/rollup-linux-riscv64-gnu': 4.52.3 + '@rollup/rollup-linux-riscv64-musl': 4.52.3 + '@rollup/rollup-linux-s390x-gnu': 4.52.3 + '@rollup/rollup-linux-x64-gnu': 4.52.3 + '@rollup/rollup-linux-x64-musl': 4.52.3 + '@rollup/rollup-openharmony-arm64': 4.52.3 + '@rollup/rollup-win32-arm64-msvc': 4.52.3 + '@rollup/rollup-win32-ia32-msvc': 4.52.3 + '@rollup/rollup-win32-x64-gnu': 4.52.3 + '@rollup/rollup-win32-x64-msvc': 4.52.3 + fsevents: 2.3.3 + + run-applescript@7.1.0: {} + + scule@1.3.0: {} + + semver-compare@1.0.0: + optional: true + + semver@6.3.1: {} + + semver@7.7.2: + optional: true + + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@4.1.0: {} + + sirv@3.0.2: + dependencies: + '@polka/url': 1.0.0-next.29 + mrmime: 2.0.1 + totalist: 3.0.1 + + socket.io-adapter@2.5.5: + dependencies: + debug: 4.3.7 + ws: 8.17.1 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-client@4.8.1: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + engine.io-client: 6.6.3 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + socket.io-parser@4.2.4: + dependencies: + '@socket.io/component-emitter': 3.1.2 + debug: 4.3.7 + transitivePeerDependencies: + - supports-color + + socket.io@4.8.1: + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.7 + engine.io: 6.6.4 + socket.io-adapter: 2.5.5 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + source-map-js@1.2.1: {} + + speakingurl@14.0.1: {} + + sprintf-js@1.1.3: + optional: true + + strip-final-newline@4.0.0: {} + + sumchecker@3.0.1: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + superjson@2.2.2: + dependencies: + copy-anything: 3.0.5 + + tailwind-merge@3.3.1: {} + + tailwindcss@4.1.14: {} + + tapable@2.2.3: {} + + tar@7.5.1: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.1.0 + yallist: 5.0.0 + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + totalist@3.0.1: {} + + tslib@2.8.1: {} + + tw-animate-css@1.4.0: {} + + type-fest@0.13.1: + optional: true + + type-fest@4.41.0: {} + + ufo@1.6.1: {} + + uncrypto@0.1.3: {} + + undici-types@6.21.0: {} + + undici-types@7.13.0: {} + + unicorn-magic@0.3.0: {} + + universalify@0.1.2: {} + + unplugin-utils@0.2.5: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin-utils@0.3.0: + dependencies: + pathe: 2.0.3 + picomatch: 4.0.3 + + unplugin-vue-router@0.15.0(@vue/compiler-sfc@3.5.22)(vue-router@4.5.1(vue@3.5.22))(vue@3.5.22): + dependencies: + '@vue-macros/common': 3.0.0-beta.16(vue@3.5.22) + '@vue/compiler-sfc': 3.5.22 + '@vue/language-core': 3.1.0 + ast-walker-scope: 0.8.2 + chokidar: 4.0.3 + json5: 2.2.3 + local-pkg: 1.1.2 + magic-string: 0.30.19 + mlly: 1.8.0 + muggle-string: 0.4.1 + pathe: 2.0.3 + picomatch: 4.0.3 + scule: 1.3.0 + tinyglobby: 0.2.15 + unplugin: 2.3.10 + unplugin-utils: 0.2.5 + yaml: 2.8.1 + optionalDependencies: + vue-router: 4.5.1(vue@3.5.22) + transitivePeerDependencies: + - typescript + - vue + + unplugin@2.3.10: + dependencies: + '@jridgewell/remapping': 2.3.5 + acorn: 8.15.0 + picomatch: 4.0.3 + webpack-virtual-modules: 0.6.2 + + update-browserslist-db@1.1.3(browserslist@4.26.3): + dependencies: + browserslist: 4.26.3 + escalade: 3.2.0 + picocolors: 1.1.1 + + util-deprecate@1.0.2: {} + + vary@1.1.2: {} + + vee-validate@4.15.1(vue@3.5.22): + dependencies: + '@vue/devtools-api': 7.7.7 + type-fest: 4.41.0 + vue: 3.5.22 + + vite-dev-rpc@1.1.0(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)): + dependencies: + birpc: 2.6.1 + vite: 7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) + vite-hot-client: 2.1.0(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)) + + vite-hot-client@2.1.0(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)): + dependencies: + vite: 7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) + + vite-plugin-inspect@11.3.3(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)): + dependencies: + ansis: 4.2.0 + debug: 4.4.3 + error-stack-parser-es: 1.0.5 + ohash: 2.0.11 + open: 10.2.0 + perfect-debounce: 2.0.0 + sirv: 3.0.2 + unplugin-utils: 0.3.0 + vite: 7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) + vite-dev-rpc: 1.1.0(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)) + transitivePeerDependencies: + - supports-color + + vite-plugin-vue-devtools@8.0.2(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))(vue@3.5.22): + dependencies: + '@vue/devtools-core': 8.0.2(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1))(vue@3.5.22) + '@vue/devtools-kit': 8.0.2 + '@vue/devtools-shared': 8.0.2 + execa: 9.6.0 + sirv: 3.0.2 + vite: 7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) + vite-plugin-inspect: 11.3.3(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)) + vite-plugin-vue-inspector: 5.3.2(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)) + transitivePeerDependencies: + - '@nuxt/kit' + - supports-color + - vue + + vite-plugin-vue-inspector@5.3.2(vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1)): + dependencies: + '@babel/core': 7.28.4 + '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.4) + '@babel/plugin-syntax-import-attributes': 7.27.1(@babel/core@7.28.4) + '@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.28.4) + '@babel/plugin-transform-typescript': 7.28.0(@babel/core@7.28.4) + '@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.28.4) + '@vue/compiler-dom': 3.5.22 + kolorist: 1.8.0 + magic-string: 0.30.19 + vite: 7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1) + transitivePeerDependencies: + - supports-color + + vite@7.1.7(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(yaml@2.8.1): + dependencies: + esbuild: 0.25.10 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.6 + rollup: 4.52.3 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.6.2 + fsevents: 2.3.3 + jiti: 2.6.1 + lightningcss: 1.30.1 + yaml: 2.8.1 + + vue-demi@0.14.10(vue@3.5.22): + dependencies: + vue: 3.5.22 + + vue-router@4.5.1(vue@3.5.22): + dependencies: + '@vue/devtools-api': 6.6.4 + vue: 3.5.22 + + vue-sonner@2.0.9: {} + + vue@3.5.22: + dependencies: + '@vue/compiler-dom': 3.5.22 + '@vue/compiler-sfc': 3.5.22 + '@vue/runtime-dom': 3.5.22 + '@vue/server-renderer': 3.5.22(vue@3.5.22) + '@vue/shared': 3.5.22 + + webpack-virtual-modules@0.6.2: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + wrappy@1.0.2: {} + + ws@8.17.1: {} + + wsl-utils@0.1.0: + dependencies: + is-wsl: 3.1.0 + + xmlhttprequest-ssl@2.1.2: {} + + yallist@3.1.1: {} + + yallist@5.0.0: {} + + yaml@2.8.1: {} + + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yoctocolors@2.1.2: {} + + zod@3.25.76: {} diff --git a/kv-admin/public/vite.svg b/kv-admin/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/kv-admin/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/kv-admin/src/App.vue b/kv-admin/src/App.vue new file mode 100644 index 0000000..a78fb6f --- /dev/null +++ b/kv-admin/src/App.vue @@ -0,0 +1,7 @@ + + + diff --git a/kv-admin/src/assets/vue.svg b/kv-admin/src/assets/vue.svg new file mode 100644 index 0000000..770e9d3 --- /dev/null +++ b/kv-admin/src/assets/vue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/kv-admin/src/components/AppCard.vue b/kv-admin/src/components/AppCard.vue new file mode 100644 index 0000000..0d5256e --- /dev/null +++ b/kv-admin/src/components/AppCard.vue @@ -0,0 +1,296 @@ + + + diff --git a/kv-admin/src/components/HelloWorld.vue b/kv-admin/src/components/HelloWorld.vue new file mode 100644 index 0000000..b58e52b --- /dev/null +++ b/kv-admin/src/components/HelloWorld.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/kv-admin/src/components/ui/badge/Badge.vue b/kv-admin/src/components/ui/badge/Badge.vue new file mode 100644 index 0000000..c050809 --- /dev/null +++ b/kv-admin/src/components/ui/badge/Badge.vue @@ -0,0 +1,25 @@ + + + diff --git a/kv-admin/src/components/ui/badge/index.js b/kv-admin/src/components/ui/badge/index.js new file mode 100644 index 0000000..447cf8e --- /dev/null +++ b/kv-admin/src/components/ui/badge/index.js @@ -0,0 +1,24 @@ +import { cva } from "class-variance-authority"; + +export { default as Badge } from "./Badge.vue"; + +export const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + }, +); diff --git a/kv-admin/src/components/ui/button/Button.vue b/kv-admin/src/components/ui/button/Button.vue new file mode 100644 index 0000000..8abe477 --- /dev/null +++ b/kv-admin/src/components/ui/button/Button.vue @@ -0,0 +1,24 @@ + + + diff --git a/kv-admin/src/components/ui/button/index.js b/kv-admin/src/components/ui/button/index.js new file mode 100644 index 0000000..83eb0ed --- /dev/null +++ b/kv-admin/src/components/ui/button/index.js @@ -0,0 +1,34 @@ +import { cva } from "class-variance-authority"; + +export { default as Button } from "./Button.vue"; + +export const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90", + destructive: + "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); diff --git a/kv-admin/src/components/ui/card/Card.vue b/kv-admin/src/components/ui/card/Card.vue new file mode 100644 index 0000000..16920e4 --- /dev/null +++ b/kv-admin/src/components/ui/card/Card.vue @@ -0,0 +1,21 @@ + + + diff --git a/kv-admin/src/components/ui/card/CardAction.vue b/kv-admin/src/components/ui/card/CardAction.vue new file mode 100644 index 0000000..4c13362 --- /dev/null +++ b/kv-admin/src/components/ui/card/CardAction.vue @@ -0,0 +1,21 @@ + + + diff --git a/kv-admin/src/components/ui/card/CardContent.vue b/kv-admin/src/components/ui/card/CardContent.vue new file mode 100644 index 0000000..f54f541 --- /dev/null +++ b/kv-admin/src/components/ui/card/CardContent.vue @@ -0,0 +1,13 @@ + + + diff --git a/kv-admin/src/components/ui/card/CardDescription.vue b/kv-admin/src/components/ui/card/CardDescription.vue new file mode 100644 index 0000000..199875d --- /dev/null +++ b/kv-admin/src/components/ui/card/CardDescription.vue @@ -0,0 +1,16 @@ + + + diff --git a/kv-admin/src/components/ui/card/CardFooter.vue b/kv-admin/src/components/ui/card/CardFooter.vue new file mode 100644 index 0000000..a28c159 --- /dev/null +++ b/kv-admin/src/components/ui/card/CardFooter.vue @@ -0,0 +1,16 @@ + + + diff --git a/kv-admin/src/components/ui/card/CardHeader.vue b/kv-admin/src/components/ui/card/CardHeader.vue new file mode 100644 index 0000000..cc387d1 --- /dev/null +++ b/kv-admin/src/components/ui/card/CardHeader.vue @@ -0,0 +1,21 @@ + + + diff --git a/kv-admin/src/components/ui/card/CardTitle.vue b/kv-admin/src/components/ui/card/CardTitle.vue new file mode 100644 index 0000000..bb88bf0 --- /dev/null +++ b/kv-admin/src/components/ui/card/CardTitle.vue @@ -0,0 +1,16 @@ + + + diff --git a/kv-admin/src/components/ui/card/index.js b/kv-admin/src/components/ui/card/index.js new file mode 100644 index 0000000..409685b --- /dev/null +++ b/kv-admin/src/components/ui/card/index.js @@ -0,0 +1,7 @@ +export { default as Card } from "./Card.vue"; +export { default as CardAction } from "./CardAction.vue"; +export { default as CardContent } from "./CardContent.vue"; +export { default as CardDescription } from "./CardDescription.vue"; +export { default as CardFooter } from "./CardFooter.vue"; +export { default as CardHeader } from "./CardHeader.vue"; +export { default as CardTitle } from "./CardTitle.vue"; diff --git a/kv-admin/src/components/ui/dialog/Dialog.vue b/kv-admin/src/components/ui/dialog/Dialog.vue new file mode 100644 index 0000000..c261c6f --- /dev/null +++ b/kv-admin/src/components/ui/dialog/Dialog.vue @@ -0,0 +1,18 @@ + + + diff --git a/kv-admin/src/components/ui/dialog/DialogClose.vue b/kv-admin/src/components/ui/dialog/DialogClose.vue new file mode 100644 index 0000000..80f6f53 --- /dev/null +++ b/kv-admin/src/components/ui/dialog/DialogClose.vue @@ -0,0 +1,14 @@ + + + diff --git a/kv-admin/src/components/ui/dialog/DialogContent.vue b/kv-admin/src/components/ui/dialog/DialogContent.vue new file mode 100644 index 0000000..50befa5 --- /dev/null +++ b/kv-admin/src/components/ui/dialog/DialogContent.vue @@ -0,0 +1,57 @@ + + + diff --git a/kv-admin/src/components/ui/dialog/DialogDescription.vue b/kv-admin/src/components/ui/dialog/DialogDescription.vue new file mode 100644 index 0000000..b981e8e --- /dev/null +++ b/kv-admin/src/components/ui/dialog/DialogDescription.vue @@ -0,0 +1,25 @@ + + + diff --git a/kv-admin/src/components/ui/dialog/DialogFooter.vue b/kv-admin/src/components/ui/dialog/DialogFooter.vue new file mode 100644 index 0000000..eee552a --- /dev/null +++ b/kv-admin/src/components/ui/dialog/DialogFooter.vue @@ -0,0 +1,18 @@ + + + diff --git a/kv-admin/src/components/ui/dialog/DialogHeader.vue b/kv-admin/src/components/ui/dialog/DialogHeader.vue new file mode 100644 index 0000000..91194b0 --- /dev/null +++ b/kv-admin/src/components/ui/dialog/DialogHeader.vue @@ -0,0 +1,16 @@ + + + diff --git a/kv-admin/src/components/ui/dialog/DialogOverlay.vue b/kv-admin/src/components/ui/dialog/DialogOverlay.vue new file mode 100644 index 0000000..e47c602 --- /dev/null +++ b/kv-admin/src/components/ui/dialog/DialogOverlay.vue @@ -0,0 +1,29 @@ + + + diff --git a/kv-admin/src/components/ui/dialog/DialogScrollContent.vue b/kv-admin/src/components/ui/dialog/DialogScrollContent.vue new file mode 100644 index 0000000..95cbf42 --- /dev/null +++ b/kv-admin/src/components/ui/dialog/DialogScrollContent.vue @@ -0,0 +1,71 @@ + + + diff --git a/kv-admin/src/components/ui/dialog/DialogTitle.vue b/kv-admin/src/components/ui/dialog/DialogTitle.vue new file mode 100644 index 0000000..49cda6f --- /dev/null +++ b/kv-admin/src/components/ui/dialog/DialogTitle.vue @@ -0,0 +1,25 @@ + + + diff --git a/kv-admin/src/components/ui/dialog/DialogTrigger.vue b/kv-admin/src/components/ui/dialog/DialogTrigger.vue new file mode 100644 index 0000000..2938e30 --- /dev/null +++ b/kv-admin/src/components/ui/dialog/DialogTrigger.vue @@ -0,0 +1,14 @@ + + + diff --git a/kv-admin/src/components/ui/dialog/index.js b/kv-admin/src/components/ui/dialog/index.js new file mode 100644 index 0000000..6c411de --- /dev/null +++ b/kv-admin/src/components/ui/dialog/index.js @@ -0,0 +1,10 @@ +export { default as Dialog } from "./Dialog.vue"; +export { default as DialogClose } from "./DialogClose.vue"; +export { default as DialogContent } from "./DialogContent.vue"; +export { default as DialogDescription } from "./DialogDescription.vue"; +export { default as DialogFooter } from "./DialogFooter.vue"; +export { default as DialogHeader } from "./DialogHeader.vue"; +export { default as DialogOverlay } from "./DialogOverlay.vue"; +export { default as DialogScrollContent } from "./DialogScrollContent.vue"; +export { default as DialogTitle } from "./DialogTitle.vue"; +export { default as DialogTrigger } from "./DialogTrigger.vue"; diff --git a/kv-admin/src/components/ui/input/Input.vue b/kv-admin/src/components/ui/input/Input.vue new file mode 100644 index 0000000..dc16a12 --- /dev/null +++ b/kv-admin/src/components/ui/input/Input.vue @@ -0,0 +1,32 @@ + + + diff --git a/kv-admin/src/components/ui/input/index.js b/kv-admin/src/components/ui/input/index.js new file mode 100644 index 0000000..110f046 --- /dev/null +++ b/kv-admin/src/components/ui/input/index.js @@ -0,0 +1 @@ +export { default as Input } from "./Input.vue"; diff --git a/kv-admin/src/components/ui/label/Label.vue b/kv-admin/src/components/ui/label/Label.vue new file mode 100644 index 0000000..b20aec0 --- /dev/null +++ b/kv-admin/src/components/ui/label/Label.vue @@ -0,0 +1,29 @@ + + + diff --git a/kv-admin/src/components/ui/label/index.js b/kv-admin/src/components/ui/label/index.js new file mode 100644 index 0000000..38eaa35 --- /dev/null +++ b/kv-admin/src/components/ui/label/index.js @@ -0,0 +1 @@ +export { default as Label } from "./Label.vue"; diff --git a/kv-admin/src/components/ui/select/Select.vue b/kv-admin/src/components/ui/select/Select.vue new file mode 100644 index 0000000..b2cab62 --- /dev/null +++ b/kv-admin/src/components/ui/select/Select.vue @@ -0,0 +1,26 @@ + + + diff --git a/kv-admin/src/components/ui/select/SelectContent.vue b/kv-admin/src/components/ui/select/SelectContent.vue new file mode 100644 index 0000000..459a179 --- /dev/null +++ b/kv-admin/src/components/ui/select/SelectContent.vue @@ -0,0 +1,81 @@ + + + diff --git a/kv-admin/src/components/ui/select/SelectGroup.vue b/kv-admin/src/components/ui/select/SelectGroup.vue new file mode 100644 index 0000000..eecc39b --- /dev/null +++ b/kv-admin/src/components/ui/select/SelectGroup.vue @@ -0,0 +1,14 @@ + + + diff --git a/kv-admin/src/components/ui/select/SelectItem.vue b/kv-admin/src/components/ui/select/SelectItem.vue new file mode 100644 index 0000000..18edeca --- /dev/null +++ b/kv-admin/src/components/ui/select/SelectItem.vue @@ -0,0 +1,47 @@ + + + diff --git a/kv-admin/src/components/ui/select/SelectItemText.vue b/kv-admin/src/components/ui/select/SelectItemText.vue new file mode 100644 index 0000000..eb87548 --- /dev/null +++ b/kv-admin/src/components/ui/select/SelectItemText.vue @@ -0,0 +1,14 @@ + + + diff --git a/kv-admin/src/components/ui/select/SelectLabel.vue b/kv-admin/src/components/ui/select/SelectLabel.vue new file mode 100644 index 0000000..adb66ad --- /dev/null +++ b/kv-admin/src/components/ui/select/SelectLabel.vue @@ -0,0 +1,20 @@ + + + diff --git a/kv-admin/src/components/ui/select/SelectScrollDownButton.vue b/kv-admin/src/components/ui/select/SelectScrollDownButton.vue new file mode 100644 index 0000000..6932136 --- /dev/null +++ b/kv-admin/src/components/ui/select/SelectScrollDownButton.vue @@ -0,0 +1,30 @@ + + + diff --git a/kv-admin/src/components/ui/select/SelectScrollUpButton.vue b/kv-admin/src/components/ui/select/SelectScrollUpButton.vue new file mode 100644 index 0000000..c7a493f --- /dev/null +++ b/kv-admin/src/components/ui/select/SelectScrollUpButton.vue @@ -0,0 +1,30 @@ + + + diff --git a/kv-admin/src/components/ui/select/SelectSeparator.vue b/kv-admin/src/components/ui/select/SelectSeparator.vue new file mode 100644 index 0000000..34f2f59 --- /dev/null +++ b/kv-admin/src/components/ui/select/SelectSeparator.vue @@ -0,0 +1,21 @@ + + + diff --git a/kv-admin/src/components/ui/select/SelectTrigger.vue b/kv-admin/src/components/ui/select/SelectTrigger.vue new file mode 100644 index 0000000..4bd93a8 --- /dev/null +++ b/kv-admin/src/components/ui/select/SelectTrigger.vue @@ -0,0 +1,37 @@ + + + diff --git a/kv-admin/src/components/ui/select/SelectValue.vue b/kv-admin/src/components/ui/select/SelectValue.vue new file mode 100644 index 0000000..db9807f --- /dev/null +++ b/kv-admin/src/components/ui/select/SelectValue.vue @@ -0,0 +1,15 @@ + + + diff --git a/kv-admin/src/components/ui/select/index.js b/kv-admin/src/components/ui/select/index.js new file mode 100644 index 0000000..d911c4e --- /dev/null +++ b/kv-admin/src/components/ui/select/index.js @@ -0,0 +1,11 @@ +export { default as Select } from "./Select.vue"; +export { default as SelectContent } from "./SelectContent.vue"; +export { default as SelectGroup } from "./SelectGroup.vue"; +export { default as SelectItem } from "./SelectItem.vue"; +export { default as SelectItemText } from "./SelectItemText.vue"; +export { default as SelectLabel } from "./SelectLabel.vue"; +export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue"; +export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue"; +export { default as SelectSeparator } from "./SelectSeparator.vue"; +export { default as SelectTrigger } from "./SelectTrigger.vue"; +export { default as SelectValue } from "./SelectValue.vue"; diff --git a/kv-admin/src/components/ui/table/Table.vue b/kv-admin/src/components/ui/table/Table.vue new file mode 100644 index 0000000..71bd0a9 --- /dev/null +++ b/kv-admin/src/components/ui/table/Table.vue @@ -0,0 +1,18 @@ + + + diff --git a/kv-admin/src/components/ui/table/TableBody.vue b/kv-admin/src/components/ui/table/TableBody.vue new file mode 100644 index 0000000..3cfac53 --- /dev/null +++ b/kv-admin/src/components/ui/table/TableBody.vue @@ -0,0 +1,16 @@ + + + diff --git a/kv-admin/src/components/ui/table/TableCaption.vue b/kv-admin/src/components/ui/table/TableCaption.vue new file mode 100644 index 0000000..2a4488e --- /dev/null +++ b/kv-admin/src/components/ui/table/TableCaption.vue @@ -0,0 +1,16 @@ + + + diff --git a/kv-admin/src/components/ui/table/TableCell.vue b/kv-admin/src/components/ui/table/TableCell.vue new file mode 100644 index 0000000..aa0faef --- /dev/null +++ b/kv-admin/src/components/ui/table/TableCell.vue @@ -0,0 +1,21 @@ + + + diff --git a/kv-admin/src/components/ui/table/TableEmpty.vue b/kv-admin/src/components/ui/table/TableEmpty.vue new file mode 100644 index 0000000..0a94d54 --- /dev/null +++ b/kv-admin/src/components/ui/table/TableEmpty.vue @@ -0,0 +1,31 @@ + + + diff --git a/kv-admin/src/components/ui/table/TableFooter.vue b/kv-admin/src/components/ui/table/TableFooter.vue new file mode 100644 index 0000000..89c5ac1 --- /dev/null +++ b/kv-admin/src/components/ui/table/TableFooter.vue @@ -0,0 +1,18 @@ + + + diff --git a/kv-admin/src/components/ui/table/TableHead.vue b/kv-admin/src/components/ui/table/TableHead.vue new file mode 100644 index 0000000..1e8dad9 --- /dev/null +++ b/kv-admin/src/components/ui/table/TableHead.vue @@ -0,0 +1,21 @@ + + + diff --git a/kv-admin/src/components/ui/table/TableHeader.vue b/kv-admin/src/components/ui/table/TableHeader.vue new file mode 100644 index 0000000..616e4dc --- /dev/null +++ b/kv-admin/src/components/ui/table/TableHeader.vue @@ -0,0 +1,13 @@ + + + diff --git a/kv-admin/src/components/ui/table/TableRow.vue b/kv-admin/src/components/ui/table/TableRow.vue new file mode 100644 index 0000000..85f521c --- /dev/null +++ b/kv-admin/src/components/ui/table/TableRow.vue @@ -0,0 +1,21 @@ + + + diff --git a/kv-admin/src/components/ui/table/index.js b/kv-admin/src/components/ui/table/index.js new file mode 100644 index 0000000..0afab4c --- /dev/null +++ b/kv-admin/src/components/ui/table/index.js @@ -0,0 +1,9 @@ +export { default as Table } from "./Table.vue"; +export { default as TableBody } from "./TableBody.vue"; +export { default as TableCaption } from "./TableCaption.vue"; +export { default as TableCell } from "./TableCell.vue"; +export { default as TableEmpty } from "./TableEmpty.vue"; +export { default as TableFooter } from "./TableFooter.vue"; +export { default as TableHead } from "./TableHead.vue"; +export { default as TableHeader } from "./TableHeader.vue"; +export { default as TableRow } from "./TableRow.vue"; diff --git a/kv-admin/src/components/ui/table/utils.js b/kv-admin/src/components/ui/table/utils.js new file mode 100644 index 0000000..ee61875 --- /dev/null +++ b/kv-admin/src/components/ui/table/utils.js @@ -0,0 +1,7 @@ +import { isFunction } from "@tanstack/vue-table"; + +export function valueUpdater(updaterOrValue, ref) { + ref.value = isFunction(updaterOrValue) + ? updaterOrValue(ref.value) + : updaterOrValue; +} diff --git a/kv-admin/src/lib/api.js b/kv-admin/src/lib/api.js new file mode 100644 index 0000000..4e553c7 --- /dev/null +++ b/kv-admin/src/lib/api.js @@ -0,0 +1,103 @@ +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3030' +const SITE_KEY = import.meta.env.VITE_SITE_KEY || '' + +class ApiClient { + constructor(baseUrl, siteKey) { + this.baseUrl = baseUrl + this.siteKey = siteKey + } + + async fetch(endpoint, options = {}) { + const headers = { + 'Content-Type': 'application/json', + 'x-site-key': this.siteKey, + ...options.headers, + } + + const response = await fetch(`${this.baseUrl}${endpoint}`, { + ...options, + headers, + }) + + if (!response.ok) { + const error = await response.json().catch(() => ({ message: 'Unknown error' })) + throw new Error(error.message || `HTTP ${response.status}`) + } + + if (response.status === 204) { + return {} + } + + return response.json() + } + + // 应用相关 API + async getApps(params = {}) { + const query = new URLSearchParams(params).toString() + return this.fetch(`/apps${query ? `?${query}` : ''}`) + } + + async getApp(appId) { + return this.fetch(`/apps/${appId}`) + } + + async getAppInstallations(appId, params = {}) { + const query = new URLSearchParams(params).toString() + return this.fetch(`/apps/${appId}/installations${query ? `?${query}` : ''}`) + } + + // 授权相关 API + async authorizeApp(appId, data) { + return this.fetch(`/apps/${appId}/authorize`, { + method: 'POST', + body: JSON.stringify(data), + }) + } + + // Token 管理 API + async getDeviceTokens(deviceUuid) { + return this.fetch(`/apps/devices/${deviceUuid}/tokens`) + } + + async revokeToken(token) { + return this.fetch(`/apps/tokens/${token}`, { + method: 'DELETE', + }) + } + + // 设备密码管理 API + async setDevicePassword(deviceUuid, data) { + return this.fetch(`/apps/devices/${deviceUuid}/password`, { + method: 'PUT', + body: JSON.stringify(data), + }) + } + + async deleteDevicePassword(deviceUuid, password) { + return this.fetch(`/apps/devices/${deviceUuid}/password`, { + method: 'DELETE', + body: JSON.stringify({ password }), + }) + } + + async verifyDevicePassword(deviceUuid, password) { + return this.fetch(`/apps/devices/${deviceUuid}/password/verify`, { + method: 'POST', + body: JSON.stringify({ password }), + }) + } + + // 设备授权相关 API + async bindDeviceCode(deviceCode, token) { + return this.fetch('/auth/device/bind', { + method: 'POST', + body: JSON.stringify({ device_code: deviceCode, token }), + }) + } + + async getDeviceCodeStatus(deviceCode) { + return this.fetch(`/auth/device/status?device_code=${deviceCode}`) + } +} + +export const apiClient = new ApiClient(API_BASE_URL, SITE_KEY) diff --git a/kv-admin/src/lib/axios.js b/kv-admin/src/lib/axios.js new file mode 100644 index 0000000..67a6b02 --- /dev/null +++ b/kv-admin/src/lib/axios.js @@ -0,0 +1,37 @@ +import axios from 'axios' + +const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:3030' +const SITE_KEY = import.meta.env.VITE_SITE_KEY || '' + +// 创建 axios 实例 +const axiosInstance = axios.create({ + baseURL: API_BASE_URL, + timeout: 30000, + headers: { + 'Content-Type': 'application/json', + 'x-site-key': SITE_KEY, + }, +}) + +// 请求拦截器 +axiosInstance.interceptors.request.use( + (config) => { + return config + }, + (error) => { + return Promise.reject(error) + } +) + +// 响应拦截器 +axiosInstance.interceptors.response.use( + (response) => { + return response.data + }, + (error) => { + const message = error.response?.data?.message || error.message || 'Unknown error' + return Promise.reject(new Error(message)) + } +) + +export default axiosInstance diff --git a/kv-admin/src/lib/deviceStore.js b/kv-admin/src/lib/deviceStore.js new file mode 100644 index 0000000..3ad3308 --- /dev/null +++ b/kv-admin/src/lib/deviceStore.js @@ -0,0 +1,56 @@ +// 生成 UUID v4 +export function generateUUID() { + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + const r = Math.random() * 16 | 0 + const v = c === 'x' ? r : (r & 0x3 | 0x8) + return v.toString(16) + }) +} + +// 设备 UUID 管理 +export const deviceStore = { + // 获取当前设备 UUID + getDeviceUuid() { + return localStorage.getItem('device_uuid') + }, + + // 设置设备 UUID + setDeviceUuid(uuid) { + localStorage.setItem('device_uuid', uuid) + }, + + // 生成并保存新的设备 UUID + generateAndSave() { + const uuid = generateUUID() + this.setDeviceUuid(uuid) + return uuid + }, + + // 获取或生成设备 UUID + getOrGenerate() { + let uuid = this.getDeviceUuid() + if (!uuid) { + uuid = this.generateAndSave() + } + return uuid + }, + + // 清除设备 UUID + clear() { + localStorage.removeItem('device_uuid') + localStorage.removeItem('device_password') + }, + + // 设备密码管理 + hasPassword() { + return localStorage.getItem('device_password') === 'true' + }, + + setHasPassword(hasPassword) { + if (hasPassword) { + localStorage.setItem('device_password', 'true') + } else { + localStorage.removeItem('device_password') + } + } +} diff --git a/kv-admin/src/lib/tokenStore.js b/kv-admin/src/lib/tokenStore.js new file mode 100644 index 0000000..7d6e469 --- /dev/null +++ b/kv-admin/src/lib/tokenStore.js @@ -0,0 +1,66 @@ +// 生成随机设备码 +export function generateDeviceCode() { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' + const segments = [] + + for (let i = 0; i < 4; i++) { + let segment = '' + for (let j = 0; j < 4; j++) { + segment += chars[Math.floor(Math.random() * chars.length)] + } + segments.push(segment) + } + + return segments.join('-') +} + +// Token 管理 +export const tokenStore = { + // 获取所有 token + getTokens() { + const tokens = localStorage.getItem('kv_tokens') + return tokens ? JSON.parse(tokens) : [] + }, + + // 添加 token + addToken(token, appName = '') { + const tokens = this.getTokens() + const newToken = { + id: Date.now().toString(), + token, + appName, + deviceCode: generateDeviceCode(), + createdAt: new Date().toISOString(), + lastUsed: new Date().toISOString() + } + tokens.push(newToken) + localStorage.setItem('kv_tokens', JSON.stringify(tokens)) + return newToken + }, + + // 删除 token + removeToken(id) { + const tokens = this.getTokens().filter(t => t.id !== id) + localStorage.setItem('kv_tokens', JSON.stringify(tokens)) + }, + + // 更新 token + updateToken(id, updates) { + const tokens = this.getTokens().map(t => + t.id === id ? { ...t, ...updates } : t + ) + localStorage.setItem('kv_tokens', JSON.stringify(tokens)) + }, + + // 获取当前活跃的 token + getActiveToken() { + const activeId = localStorage.getItem('kv_active_token') + if (!activeId) return null + return this.getTokens().find(t => t.id === activeId) + }, + + // 设置活跃 token + setActiveToken(id) { + localStorage.setItem('kv_active_token', id) + } +} diff --git a/kv-admin/src/lib/utils.js b/kv-admin/src/lib/utils.js new file mode 100644 index 0000000..b20bf01 --- /dev/null +++ b/kv-admin/src/lib/utils.js @@ -0,0 +1,6 @@ +import { clsx } from "clsx"; +import { twMerge } from "tailwind-merge" + +export function cn(...inputs) { + return twMerge(clsx(inputs)); +} diff --git a/kv-admin/src/main.js b/kv-admin/src/main.js new file mode 100644 index 0000000..8b4ea5e --- /dev/null +++ b/kv-admin/src/main.js @@ -0,0 +1,27 @@ +import { createApp } from 'vue' +import { createRouter, createWebHistory } from 'vue-router' +import { routes } from 'vue-router/auto-routes' +import { tokenStore } from './lib/tokenStore' +import './style.css' +import App from './App.vue' + + + +const router = createRouter({ + history: createWebHistory(), + routes, +}) + +// Navigation guard for authentication +router.beforeEach((to, _from, next) => { + const requiresAuth = to.meta?.requiresAuth + const activeToken = tokenStore.getActiveToken() + + if (requiresAuth && !activeToken) { + next({ path: '/' }) + } else { + next() + } +}) + +createApp(App).use(router).mount('#app') diff --git a/kv-admin/src/pages/authorize.vue b/kv-admin/src/pages/authorize.vue new file mode 100644 index 0000000..e1c3379 --- /dev/null +++ b/kv-admin/src/pages/authorize.vue @@ -0,0 +1,341 @@ + + + diff --git a/kv-admin/src/pages/dashboard.vue b/kv-admin/src/pages/dashboard.vue new file mode 100644 index 0000000..3375509 --- /dev/null +++ b/kv-admin/src/pages/dashboard.vue @@ -0,0 +1,422 @@ + + +